Merge pull request #5747 from john-westcott-iv/collections

Porting Collections Off of Tower CLI

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-02-26 20:35:00 +00:00
committed by GitHub
41 changed files with 1954 additions and 946 deletions

View File

@@ -192,7 +192,7 @@ class APIView(views.APIView):
response.data['detail'] += ' To establish a login session, visit /api/login/.' response.data['detail'] += ' To establish a login session, visit /api/login/.'
logger.info(status_msg) logger.info(status_msg)
else: else:
logger.warn(status_msg) logger.warning(status_msg)
response = super(APIView, self).finalize_response(request, response, *args, **kwargs) response = super(APIView, self).finalize_response(request, response, *args, **kwargs)
time_started = getattr(self, 'time_started', None) time_started = getattr(self, 'time_started', None)
response['X-API-Node'] = settings.CLUSTER_HOST_ID response['X-API-Node'] = settings.CLUSTER_HOST_ID

View File

@@ -2115,7 +2115,13 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
def get_field_from_model_or_attrs(fd): def get_field_from_model_or_attrs(fd):
return attrs.get(fd, self.instance and getattr(self.instance, fd) or None) return attrs.get(fd, self.instance and getattr(self.instance, fd) or None)
if get_field_from_model_or_attrs('source') != 'scm': if get_field_from_model_or_attrs('source') == 'scm':
if (('source' in attrs or 'source_project' in attrs) and
get_field_from_model_or_attrs('source_project') is None):
raise serializers.ValidationError(
{"source_project": _("Project required for scm type sources.")}
)
else:
redundant_scm_fields = list(filter( redundant_scm_fields = list(filter(
lambda x: attrs.get(x, None), lambda x: attrs.get(x, None),
['source_project', 'source_path', 'update_on_project_update'] ['source_project', 'source_path', 'update_on_project_update']
@@ -3716,7 +3722,7 @@ class WorkflowJobNodeSerializer(LaunchConfigurationBaseSerializer):
class Meta: class Meta:
model = WorkflowJobNode model = WorkflowJobNode
fields = ('*', 'job', 'workflow_job', '-name', '-description', 'id', 'url', 'related', fields = ('*', 'job', 'workflow_job', '-name', '-description', 'id', 'url', 'related',
'unified_job_template', 'success_nodes', 'failure_nodes', 'always_nodes', 'unified_job_template', 'success_nodes', 'failure_nodes', 'always_nodes',
'all_parents_must_converge', 'do_not_run',) 'all_parents_must_converge', 'do_not_run',)
def get_related(self, obj): def get_related(self, obj):

View File

@@ -599,9 +599,9 @@ class TestControlledBySCM:
delete(inv_src.get_absolute_url(), admin_user, expect=204) delete(inv_src.get_absolute_url(), admin_user, expect=204)
assert scm_inventory.inventory_sources.count() == 0 assert scm_inventory.inventory_sources.count() == 0
def test_adding_inv_src_ok(self, post, scm_inventory, admin_user): def test_adding_inv_src_ok(self, post, scm_inventory, project, admin_user):
post(reverse('api:inventory_inventory_sources_list', kwargs={'pk': scm_inventory.id}), post(reverse('api:inventory_inventory_sources_list', kwargs={'pk': scm_inventory.id}),
{'name': 'new inv src', 'update_on_project_update': False, 'source': 'scm', 'overwrite_vars': True}, {'name': 'new inv src', 'source_project': project.pk, 'update_on_project_update': False, 'source': 'scm', 'overwrite_vars': True},
admin_user, expect=201) admin_user, expect=201)
def test_adding_inv_src_prohibited(self, post, scm_inventory, project, admin_user): def test_adding_inv_src_prohibited(self, post, scm_inventory, project, admin_user):

View File

@@ -1,7 +1,7 @@
# AWX Ansible Collection # AWX Ansible Collection
This Ansible collection allows for easy interaction with an AWX or Ansible Tower This Ansible collection allows for easy interaction with an AWX or Ansible Tower
server in Ansible playbooks. server via Ansible playbooks.
The previous home for this collection was in https://github.com/ansible/ansible The previous home for this collection was in https://github.com/ansible/ansible
inside the folder `lib/ansible/modules/web_infrastructure/ansible_tower` inside the folder `lib/ansible/modules/web_infrastructure/ansible_tower`
@@ -14,30 +14,33 @@ The release 7.0.0 of the `awx.awx` collection is intended to be identical
to the content prior to the migration, aside from changes necessary to to the content prior to the migration, aside from changes necessary to
have it function as a collection. have it function as a collection.
The following notes are changes that may require changes to playbooks. The following notes are changes that may require changes to playbooks:
- Specifying `inputs` or `injectors` as strings in the - Specifying `inputs` or `injectors` as strings in the
`tower_credential_type` module is no longer supported. Provide as dictionaries instead. `tower_credential_type` module is no longer supported. Provide them as dictionaries instead.
- 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. - 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. - Creating a "scan" type job template is no longer supported.
- `extra_vars` in the `tower_job_launch` module worked with a list previously, but is now configured to work solely in a `dict` format. - `extra_vars` in the `tower_job_launch` module worked with a list previously, but is now configured to work solely in a `dict` format.
- When the `extra_vars` parameter is used with the `tower_job_launch` module, the Job Template launch will fail unless `add_extra_vars` or `survey_enabled` is explicitly set to `True` on the Job Template. - When the `extra_vars` parameter is used with the `tower_job_launch` module, the Job Template launch will fail unless `add_extra_vars` or `survey_enabled` is explicitly set to `True` on the Job Template.
- `tower_group` used to also service inventory sources, but this functionality has been removed from this module; use `tower_inventory_source` instead.
- 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.
- The `variables` parameter in the `tower_group`, `tower_host` and `tower_inventory` modules are now in `dict` format and no longer supports the use of the `C(@)` syntax (for an external `vars` file).
- Some return values (e.g., `credential_type`) have been removed. Use of `id` is recommended.
## Running ## Running
To use this collection, the "old" tower-cli needs to be installed To use this collection, the "old" `tower-cli` needs to be installed
in the virtual environment where the collection runs. in the virtual environment where the collection runs.
You can install it from [PyPI](https://pypi.org/project/ansible-tower-cli/). You can install it from [PyPI](https://pypi.org/project/ansible-tower-cli/).
To use this collection in AWX, you should create a custom virtual environment To use this collection in AWX, you should create a custom virtual environment into which to install the requirements. NOTE: running locally, you will also need
to install the requirement into. NOTE: running locally, you will also need to set the job template `extra_vars` to include `ansible_python_interpreter`
to set the job template extra_vars to include `ansible_python_interpreter` to be the Python in that virtual environment.
to be the python in that virtual environment.
## Running Tests ## Running Tests
Tests to verify compatibility with the most recent AWX code are Tests to verify compatibility with the most recent AWX code are
in `awx_collection/test/awx`. These tests require that python packages in `awx_collection/test/awx`. These tests require that Python packages
are available for all of `awx`, `ansible`, `tower_cli`, and the collection are available for all of `awx`, `ansible`, `tower_cli`, and the collection
itself. itself.
@@ -45,7 +48,7 @@ itself.
The target `make prepare_collection_venv` will prepare some requirements The target `make prepare_collection_venv` will prepare some requirements
in the `awx_collection_test_venv` folder so that `make test_collection` can in the `awx_collection_test_venv` folder so that `make test_collection` can
be ran to actually run the tests. A single test can be ran via: be executed to actually run the tests. A single test can be run via:
``` ```
make test_collection COLLECTION_TEST_DIRS=awx_collection/test/awx/test_organization.py make test_collection COLLECTION_TEST_DIRS=awx_collection/test/awx/test_organization.py
@@ -53,9 +56,9 @@ make test_collection COLLECTION_TEST_DIRS=awx_collection/test/awx/test_organizat
### Manually ### Manually
As a faster alternative if you do not want to use the container, or As a faster alternative (if you do not want to use the container), or to
run against Ansible or tower-cli source, it is possible to set up a run against Ansible or `tower-cli` source, it is possible to set up a
working environment yourself. working environment yourself:
``` ```
mkvirtualenv my_new_venv mkvirtualenv my_new_venv

View File

@@ -36,9 +36,6 @@ options:
- Path to the Tower or AWX config file. - Path to the Tower or AWX config file.
type: path type: path
requirements:
- ansible-tower-cli >= 3.0.2
notes: notes:
- If no I(config_file) is provided we will attempt to use the tower-cli library - If no I(config_file) is provided we will attempt to use the tower-cli library
defaults to find your Tower host information. defaults to find your Tower host information.

View File

@@ -0,0 +1,620 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError
from ansible.module_utils.six import PY2
from ansible.module_utils.six.moves import StringIO
from ansible.module_utils.six.moves.urllib.parse import urlparse, 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 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):
url = None
honorred_settings = ('host', 'username', 'password', 'verify_ssl', 'oauth_token')
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'
def __init__(self, argument_spec, **kwargs):
args = dict(
tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])),
tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])),
tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])),
validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])),
tower_oauthtoken=dict(type='str', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])),
tower_config_file=dict(type='path', required=False, default=None),
)
args.update(argument_spec)
kwargs['supports_check_mode'] = True
self.json_output = {'changed': False}
super(TowerModule, self).__init__(argument_spec=args, **kwargs)
self.load_config_files()
# Parameters specified on command line will override settings in any config
if self.params.get('tower_host'):
self.host = self.params.get('tower_host')
if self.params.get('tower_username'):
self.username = self.params.get('tower_username')
if self.params.get('tower_password'):
self.password = self.params.get('tower_password')
if self.params.get('validate_certs') is not None:
self.verify_ssl = self.params.get('validate_certs')
if self.params.get('tower_oauthtoken'):
self.oauth_token = self.params.get('tower_oauthtoken')
# 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))
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)))
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('The config file {0} is not properly formatted'.format(config_file))
# If we have a specified tower config, load it
if self.params.get('tower_config_file'):
duplicated_params = []
for direct_field in ('tower_host', 'tower_username', 'tower_password', 'validate_certs', 'tower_oauthtoken'):
if self.params.get(direct_field):
duplicated_params.append(direct_field)
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)
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:
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.")
except(AttributeError, yaml.YAMLError, AssertionError):
# 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]{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.honorred_settings:
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.honorred_settings:
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 head_endpoint(self, endpoint, *args, **kwargs):
return self.make_request('HEAD', endpoint, **kwargs)
def get_endpoint(self, endpoint, *args, **kwargs):
return self.make_request('GET', endpoint, **kwargs)
def patch_endpoint(self, endpoint, *args, **kwargs):
# Handle check mode
if self.check_mode:
self.json_output['changed'] = True
self.exit_json(**self.json_output)
return self.make_request('PATCH', endpoint, **kwargs)
def post_endpoint(self, endpoint, *args, **kwargs):
# Handle check mode
if self.check_mode:
self.json_output['changed'] = True
self.exit_json(**self.json_output)
return self.make_request('POST', endpoint, **kwargs)
def delete_endpoint(self, endpoint, *args, **kwargs):
# Handle check mode
if self.check_mode:
self.json_output['changed'] = True
self.exit_json(**self.json_output)
return self.make_request('DELETE', endpoint, **kwargs)
def get_all_endpoint(self, endpoint, *args, **kwargs):
response = self.get_endpoint(endpoint, *args, **kwargs)
next_page = response['json']['next']
if response['json']['count'] > 10000:
self.fail_json(msg='The number of items being queried for is higher than 10,000.')
while next_page is not None:
next_response = self.get_endpoint(next_page)
response['json']['results'] = response['json']['results'] + next_response['json']['results']
next_page = next_response['json']['next']
return response
def get_one(self, endpoint, *args, **kwargs):
response = self.get_endpoint(endpoint, *args, **kwargs)
if response['status_code'] != 200:
self.fail_json(msg="Got a {0} response when trying to get one from {1}".format(response['status_code'], endpoint))
if 'count' not in response['json'] or 'results' not in response['json']:
self.fail_json(msg="The endpoint did not provide count and results")
if response['json']['count'] == 0:
return None
elif response['json']['count'] > 1:
self.fail_json(msg="An unexpected number of items was returned from the API ({0})".format(response['json']['count']))
return response['json']['results'][0]
def resolve_name_to_id(self, endpoint, name_or_id):
# Try to resolve the object by name
response = self.get_endpoint(endpoint, **{'data': {'name': name_or_id}})
if response['json']['count'] == 1:
return response['json']['results'][0]['id']
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 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
if not method:
raise Exception("The HTTP method must be defined")
# Make sure we start with /api/vX
if not endpoint.startswith("/"):
endpoint = "/{0}".format(endpoint)
if not endpoint.startswith("/api/"):
endpoint = "/api/v2{0}".format(endpoint)
if not endpoint.endswith('/') and '?' not in endpoint:
endpoint = "{0}/".format(endpoint)
# Extract the headers, this will be used in a couple of places
headers = kwargs.get('headers', {})
# Authenticate to Tower (if we've not already done so)
if not self.authenticated:
# This method will set a cookie in the cookie jar for us
self.authenticate(**kwargs)
if self.oauth_token:
# If we have a oauth token, we just use a bearer header
headers['Authorization'] = 'Bearer {0}'.format(self.oauth_token)
# Update the URL path with the endpoint
self.url = self.url._replace(path=endpoint)
if method in ['POST', 'PUT', 'PATCH']:
headers.setdefault('Content-Type', 'application/json')
kwargs['headers'] = headers
elif kwargs.get('data'):
self.url = self.url._replace(query=urlencode(kwargs.get('data')))
data = {}
if headers.get('Content-Type', '') == 'application/json':
data = dumps(kwargs.get('data', {}))
try:
response = self.session.open(method, self.url.geturl(), headers=headers, validate_certs=self.verify_ssl, follow_redirects=True, data=data)
self.url = self.url._replace(query=None)
except(SSLValidationError) as ssl_err:
self.fail_json(msg="Could not establish a secure connection to your host ({1}): {0}.".format(self.url.netloc, ssl_err))
except(ConnectionError) as con_err:
self.fail_json(msg="There was a network error of some kind trying to connect to your host ({1}): {0}.".format(self.url.netloc, con_err))
except(HTTPError) as he:
# Sanity check: Did the server send back some kind of internal error?
if he.code >= 500:
self.fail_json(msg='The host sent back a server error ({1}): {0}. Please check the logs and try again later'.format(self.url.path, he))
# Sanity check: Did we fail to authenticate properly? If so, fail out now; this is always a failure.
elif he.code == 401:
self.fail_json(msg='Invalid Tower authentication credentials for {0} (HTTP 401).'.format(self.url.path))
# Sanity check: Did we get a forbidden response, which means that the user isn't allowed to do this? Report that.
elif he.code == 403:
self.fail_json(msg="You don't have permission to {1} to {0} (HTTP 403).".format(self.url.path, method))
# Sanity check: Did we get a 404 response?
# Requests with primary keys will return a 404 if there is no response, and we want to consistently trap these.
elif he.code == 404:
if kwargs.get('return_none_on_404', False):
return None
self.fail_json(msg='The requested object could not be found at {0}.'.format(self.url.path))
# Sanity check: Did we get a 405 response?
# A 405 means we used a method that isn't allowed. Usually this is a bad request, but it requires special treatment because the
# API sends it as a logic error in a few situations (e.g. trying to cancel a job that isn't running).
elif he.code == 405:
self.fail_json(msg="The Tower server says you can't make a request with the {0} method to this endpoing {1}".format(method, self.url.path))
# Sanity check: Did we get some other kind of error? If so, write an appropriate error message.
elif he.code >= 400:
# We are going to return a 400 so the module can decide what to do with it
page_data = he.read()
try:
return {'status_code': he.code, 'json': loads(page_data)}
# JSONDecodeError only available on Python 3.5+
except ValueError:
return {'status_code': he.code, 'text': page_data}
elif he.code == 204 and method == 'DELETE':
# A 204 is a normal response for a delete function
pass
else:
self.fail_json(msg="Unexpected return code when calling {0}: {1}".format(self.url.geturl(), he))
except(Exception) as e:
self.fail_json(msg="There was an unknown error when trying to connect to {2}: {0} {1}".format(type(e).__name__, e, self.url.geturl()))
response_body = ''
try:
response_body = response.read()
except(Exception) as e:
self.fail_json(msg="Failed to read response body: {0}".format(e))
response_json = {}
if response_body and response_body != '':
try:
response_json = loads(response_body)
except(Exception) as e:
self.fail_json(msg="Failed to parse the response json: {0}".format(e))
if PY2:
status_code = response.getcode()
else:
status_code = response.status
return {'status_code': status_code, 'json': response_json}
def authenticate(self, **kwargs):
if self.username and self.password:
# Attempt to get a token from /api/v2/tokens/ by giving it our username/password combo
# If we have a username and password, we need to get a session cookie
login_data = {
"description": "Ansible Tower Module Token",
"application": None,
"scope": "write",
}
# Post to the tokens endpoint with baisc auth to try and get a token
api_token_url = (self.url._replace(path='/api/v2/tokens/')).geturl()
try:
response = self.session.open(
'POST', api_token_url,
validate_certs=self.verify_ssl, follow_redirects=True,
force_basic_auth=True, url_username=self.username, url_password=self.password,
data=dumps(login_data), headers={'Content-Type': 'application/json'}
)
except(Exception) as e:
# Sanity check: Did the server send back some kind of internal error?
self.fail_json(msg='Failed to get token: {0}'.format(e))
token_response = None
try:
token_response = response.read()
response_json = loads(token_response)
self.oauth_token_id = response_json['id']
self.oauth_token = response_json['token']
except(Exception) as e:
self.fail_json(msg="Failed to extract token information from login response: {0}".format(e), **{'response': token_response})
# If we have neither of these, then we can try un-authenticated access
self.authenticated = True
def default_check_mode(self):
'''Execute check mode logic for Ansible Tower modules'''
if self.check_mode:
try:
result = self.get_endpoint('ping')
self.exit_json(**{'changed': True, 'tower_version': '{0}'.format(result['json']['version'])})
except(Exception) as excinfo:
self.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo))
def delete_if_needed(self, existing_item, handle_response=True, on_delete=None):
# This will exit from the module on its own unless handle_response is False.
# If handle_response is True and the method successfully deletes an item and on_delete param is defined,
# the on_delete parameter will be called as a method pasing in this object and the json from the response
# If you pass handle_response=False, it will return one of two things:
# 1. None if the existing_item is not defined (so no delete needs to happen)
# 2. The response from Tower from calling the delete on the endpont. It's up to you to process the response and exit from the module
# Note: common error codes from the Tower API can cause the module to fail even if handle_response is set to False
if existing_item:
# If we have an item, we can try to delete it
try:
item_url = existing_item['url']
item_type = existing_item['type']
item_id = existing_item['id']
except KeyError as ke:
self.fail_json(msg="Unable to process delete of item due to missing data {0}".format(ke))
if 'name' in existing_item:
item_name = existing_item['name']
elif 'username' in existing_item:
item_name = existing_item['username']
else:
self.fail_json(msg="Unable to process delete of {0} due to missing name".format(item_type))
response = self.delete_endpoint(item_url)
if not handle_response:
return response
elif response['status_code'] in [202, 204]:
if on_delete:
on_delete(self, response['json'])
self.json_output['changed'] = True
self.json_output['id'] = item_id
self.exit_json(**self.json_output)
else:
if 'json' in response and '__all__' in response['json']:
self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['json']['__all__'][0]))
elif 'json' in response:
# This is from a project delete (if there is an active job against it)
if 'error' in response['json']:
self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['json']['error']))
else:
self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['json']))
else:
self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['status_code']))
else:
if not handle_response:
return None
else:
self.exit_json(**self.json_output)
def create_if_needed(self, existing_item, new_item, endpoint, handle_response=True, on_create=None, item_type='unknown'):
#
# This will exit from the module on its own unless handle_response is False.
# If handle_response is True and the method successfully creates an item and on_create param is defined,
# the on_create parameter will be called as a method pasing in this object and the json from the response
# If you pass handle_response=False it will return one of two things:
# 1. None if the existing_item is already defined (so no create needs to happen)
# 2. The response from Tower from calling the patch on the endpont. It's up to you to process the response and exit from the module
# Note: common error codes from the Tower API can cause the module to fail even if handle_response is set to False
#
if not endpoint:
self.fail_json(msg="Unable to create new {0} due to missing endpoint".format(item_type))
if existing_item:
try:
existing_item['url']
except KeyError as ke:
self.fail_json(msg="Unable to process delete of item due to missing data {0}".format(ke))
if not handle_response:
return None
else:
self.exit_json(**self.json_output)
else:
# If we don't have an exisitng_item, we can try to create it
# We have to rely on item_type being passed in since we don't have an existing item that declares its type
# We will pull the item_name out from the new_item, if it exists
item_name = new_item.get('name', 'unknown')
response = self.post_endpoint(endpoint, **{'data': new_item})
if not handle_response:
return response
elif response['status_code'] == 201:
self.json_output['name'] = 'unknown'
if 'name' in response['json']:
self.json_output['name'] = response['json']['name']
elif 'username' in response['json']:
# User objects return username instead of name
self.json_output['name'] = response['json']['username']
self.json_output['id'] = response['json']['id']
self.json_output['changed'] = True
if on_create is None:
self.exit_json(**self.json_output)
else:
on_create(self, response['json'])
else:
if 'json' in response and '__all__' in response['json']:
self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['json']['__all__'][0]))
elif 'json' in response:
self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['json']))
else:
self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['status_code']))
def update_if_needed(self, existing_item, new_item, handle_response=True, on_update=None):
# This will exit from the module on its own unless handle_response is False.
# If handle_response is True and 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
# If you pass handle_response=False it will return one of three 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 even if handle_response is set to False
if existing_item:
# If we have an item, we can see if it needs an update
try:
item_url = existing_item['url']
item_name = existing_item['name']
item_type = existing_item['url']
item_id = existing_item['id']
except KeyError as ke:
self.fail_json(msg="Unable to process update of item due to missing data {0}".format(ke))
needs_update = False
for field in new_item:
existing_field = existing_item.get(field, None)
new_field = new_item.get(field, None)
# If the two items don't match and we are not comparing '' to None
if existing_field != new_field and not (existing_field in (None, '') and new_field == ''):
# Something doesn't match so let's update it
needs_update = True
break
if needs_update:
response = self.patch_endpoint(item_url, **{'data': new_item})
if not handle_response:
return response
elif response['status_code'] == 200:
self.json_output['changed'] = True
self.json_output['id'] = item_id
if on_update is None:
self.exit_json(**self.json_output)
else:
on_update(self, response['json'])
elif 'json' in response and '__all__' in response['json']:
self.fail_json(msg=response['json']['__all__'])
else:
self.fail_json(**{'msg': "Unable to update {0} {1}, see response".format(item_type, item_name), 'response': response})
else:
if not handle_response:
return None
# Since we made it here, we don't need to update, status ok
self.json_output['changed'] = False
self.json_output['id'] = item_id
self.exit_json(**self.json_output)
else:
if handle_response:
self.fail_json(msg="The exstiing item is not defined and thus cannot be updated")
else:
raise ItemNotDefined("Not given an existing item to update")
def create_or_update_if_needed(self, existing_item, new_item, endpoint=None, handle_response=True, item_type='unknown', on_create=None, on_update=None):
if existing_item:
return self.update_if_needed(existing_item, new_item, handle_response=handle_response, on_update=on_update)
else:
return self.create_if_needed(existing_item, new_item, endpoint, handle_response=handle_response, on_create=on_create, item_type=item_type)
def logout(self):
if self.oauth_token_id is not None and self.username and self.password:
# 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 = (self.url._replace(path='/api/v2/tokens/{0}/'.format(self.oauth_token_id))).geturl()
try:
self.session.open(
'DELETE',
api_token_url,
validate_certs=self.verify_ssl,
follow_redirects=True,
force_basic_auth=True,
url_username=self.username,
url_password=self.password
)
self.oauth_token_id = None
self.authenticated = False
except(Exception) as e:
# 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()
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 is_job_done(self, job_status):
if job_status in ['new', 'pending', 'waiting', 'running']:
return False
else:
return True

View File

@@ -160,6 +160,10 @@ options:
choices: ["present", "absent"] choices: ["present", "absent"]
default: "present" default: "present"
type: str type: str
requirements:
- ansible-tower-cli >= 3.0.2
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
@@ -278,7 +282,7 @@ def main():
name=dict(required=True), name=dict(required=True),
user=dict(), user=dict(),
team=dict(), team=dict(),
kind=dict(choices=KIND_CHOICES.keys()), kind=dict(choices=list(KIND_CHOICES.keys())),
credential_type=dict(), credential_type=dict(),
inputs=dict(type='dict'), inputs=dict(type='dict'),
host=dict(), host=dict(),

View File

@@ -64,12 +64,12 @@ options:
default: "present" default: "present"
choices: ["present", "absent"] choices: ["present", "absent"]
type: str type: str
validate_certs: tower_oauthtoken:
description: description:
- Tower option to avoid certificates check. - The Tower OAuth token to use.
required: False required: False
type: bool type: str
aliases: [ tower_verify_ssl ] version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
@@ -93,19 +93,7 @@ EXAMPLES = '''
RETURN = ''' # ''' RETURN = ''' # '''
from ..module_utils.ansible_tower import ( from ..module_utils.tower_api import TowerModule
TowerModule,
tower_auth_config,
tower_check_mode
)
try:
import tower_cli
import tower_cli.exceptions as exc
from tower_cli.conf import settings
except ImportError:
pass
KIND_CHOICES = { KIND_CHOICES = {
'ssh': 'Machine', 'ssh': 'Machine',
@@ -118,62 +106,51 @@ KIND_CHOICES = {
def main(): def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
name=dict(required=True), name=dict(required=True),
description=dict(required=False), description=dict(required=False),
kind=dict(required=False, choices=KIND_CHOICES.keys()), kind=dict(required=False, choices=list(KIND_CHOICES.keys())),
inputs=dict(type='dict', required=False), inputs=dict(type='dict', required=False),
injectors=dict(type='dict', required=False), injectors=dict(type='dict', required=False),
state=dict(choices=['present', 'absent'], default='present'), state=dict(choices=['present', 'absent'], default='present'),
) )
module = TowerModule( # Create a module for ourselves
argument_spec=argument_spec, module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
supports_check_mode=False
)
# Extract our parameters
name = module.params.get('name') name = module.params.get('name')
new_name = None
kind = module.params.get('kind') kind = module.params.get('kind')
state = module.params.get('state') state = module.params.get('state')
json_output = {'credential_type': name, 'state': state} # These will be passed into the create/updates
credential_type_params = {
'name': new_name if new_name else name,
'kind': kind,
'managed_by_tower': False,
}
if module.params.get('description'):
credential_type_params['description'] = module.params.get('description')
if module.params.get('inputs'):
credential_type_params['inputs'] = module.params.get('inputs')
if module.params.get('injectors'):
credential_type_params['injectors'] = module.params.get('injectors')
tower_auth = tower_auth_config(module) # Attempt to look up credential_type based on the provided name
with settings.runtime_values(**tower_auth): credential_type = module.get_one('credential_types', **{
tower_check_mode(module) 'data': {
credential_type_res = tower_cli.get_resource('credential_type') 'name': name,
}
})
params = {} if state == 'absent':
params['name'] = name # If the state was absent we can let the module delete it if needed, the module will handle exiting from this
params['kind'] = kind module.delete_if_needed(credential_type)
params['managed_by_tower'] = False elif state == 'present':
# If the state was present and we can let the module build or update the existing credential type, this will return on its own
if module.params.get('description'): module.create_or_update_if_needed(credential_type, credential_type_params, endpoint='credential_types', item_type='credential type')
params['description'] = module.params.get('description')
if module.params.get('inputs'):
params['inputs'] = module.params.get('inputs')
if module.params.get('injectors'):
params['injectors'] = module.params.get('injectors')
try:
if state == 'present':
params['create_on_missing'] = True
result = credential_type_res.modify(**params)
json_output['id'] = result['id']
elif state == 'absent':
params['fail_on_missing'] = False
result = credential_type_res.delete(**params)
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo:
module.fail_json(
msg='Failed to update credential type: {0}'.format(excinfo),
changed=False
)
json_output['changed'] = result['changed']
module.exit_json(**json_output)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -28,6 +28,12 @@ options:
- The name to use for the group. - The name to use for the group.
required: True required: True
type: str type: str
new_name:
description:
- A new name for this group (for renaming)
required: False
type: str
version_added: "3.7"
description: description:
description: description:
- The description to use for the group. - The description to use for the group.
@@ -39,57 +45,20 @@ options:
type: str type: str
variables: variables:
description: description:
- Variables to use for the group, use C(@) for a file. - Variables to use for the group.
type: str type: dict
credential:
description:
- Credential to use for the group.
type: str
source:
description:
- The source to use for this group.
choices: ["manual", "file", "ec2", "vmware", "gce", "azure", "azure_rm", "openstack", "satellite6" , "cloudforms", "custom"]
type: str
source_regions:
description:
- Regions for cloud provider.
type: str
source_vars:
description:
- Override variables from source with variables from this field.
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
source_script:
description:
- Inventory script to be used when group type is C(custom).
type: str
overwrite:
description:
- Delete child groups and hosts not found in source.
type: bool
default: 'no'
overwrite_vars:
description:
- Override vars in child groups and hosts with those from external source.
type: bool
update_on_launch:
description:
- Refresh inventory data from its source each time a job is run.
type: bool
default: 'no'
state: state:
description: description:
- Desired state of the resource. - Desired state of the resource.
default: "present" default: "present"
choices: ["present", "absent"] choices: ["present", "absent"]
type: str type: str
tower_oauthtoken:
description:
- The Tower OAuth token to use.
required: False
type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
@@ -104,86 +73,59 @@ EXAMPLES = '''
tower_config_file: "~/tower_cli.cfg" tower_config_file: "~/tower_cli.cfg"
''' '''
import os from ..module_utils.tower_api import TowerModule
import json
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode
try:
import tower_cli
import tower_cli.exceptions as exc
from tower_cli.conf import settings
except ImportError:
pass
def main(): def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
name=dict(required=True), name=dict(required=True),
description=dict(), new_name=dict(required=False),
description=dict(required=False),
inventory=dict(required=True), inventory=dict(required=True),
variables=dict(), variables=dict(type='dict', required=False),
credential=dict(),
source=dict(choices=["manual", "file", "ec2", "vmware",
"gce", "azure", "azure_rm", "openstack",
"satellite6", "cloudforms", "custom"]),
source_regions=dict(),
source_vars=dict(),
instance_filters=dict(),
group_by=dict(),
source_script=dict(),
overwrite=dict(type='bool'),
overwrite_vars=dict(type='bool'),
update_on_launch=dict(type='bool'),
state=dict(choices=['present', 'absent'], default='present'), state=dict(choices=['present', 'absent'], default='present'),
) )
# Create a module for ourselves
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
# Extract our parameters
name = module.params.get('name') name = module.params.get('name')
new_name = module.params.get('new_name')
inventory = module.params.get('inventory') inventory = module.params.get('inventory')
credential = module.params.get('credential') description = module.params.get('description')
state = module.params.pop('state') state = module.params.pop('state')
variables = module.params.get('variables') variables = module.params.get('variables')
if variables:
if variables.startswith('@'):
filename = os.path.expanduser(variables[1:])
with open(filename, 'r') as f:
variables = f.read()
json_output = {'group': name, 'state': state} # Attempt to look up the related items the user specified (these will fail the module if not found)
inventory_id = module.resolve_name_to_id('inventories', inventory)
tower_auth = tower_auth_config(module) # Attempt to look up the object based on the provided name and inventory ID
with settings.runtime_values(**tower_auth): group = module.get_one('groups', **{
tower_check_mode(module) 'data': {
group = tower_cli.get_resource('group') 'name': name,
try: 'inventory': inventory_id
params = module.params.copy() }
params['create_on_missing'] = True })
params['variables'] = variables
inv_res = tower_cli.get_resource('inventory') # Create the data that gets sent for create and update
inv = inv_res.get(name=inventory) group_fields = {
params['inventory'] = inv['id'] 'name': new_name if new_name else name,
'inventory': inventory_id,
}
if description is not None:
group_fields['description'] = description
if variables is not None:
group_fields['variables'] = json.dumps(variables)
if credential: if state == 'absent':
cred_res = tower_cli.get_resource('credential') # If the state was absent we can let the module delete it if needed, the module will handle exiting from this
cred = cred_res.get(name=credential) module.delete_if_needed(group)
params['credential'] = cred['id'] elif state == 'present':
# If the state was present we can let the module build or update the existing group, this will return on its own
if state == 'present': module.create_or_update_if_needed(group, group_fields, endpoint='groups', item_type='group')
result = group.modify(**params)
json_output['id'] = result['id']
elif state == 'absent':
result = group.delete(**params)
except (exc.NotFound) as excinfo:
module.fail_json(msg='Failed to update the group, inventory not found: {0}'.format(excinfo), changed=False)
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo:
module.fail_json(msg='Failed to update the group: {0}'.format(excinfo), changed=False)
json_output['changed'] = result['changed']
module.exit_json(**json_output)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -28,6 +28,12 @@ options:
- The name to use for the host. - The name to use for the host.
required: True required: True
type: str type: str
new_name:
description:
- To use when changing a hosts's name.
required: False
type: str
version_added: "3.7"
description: description:
description: description:
- The description to use for the host. - The description to use for the host.
@@ -44,14 +50,20 @@ options:
default: 'yes' default: 'yes'
variables: variables:
description: description:
- Variables to use for the host. Use C(@) for a file. - Variables to use for the host.
type: str type: dict
state: state:
description: description:
- Desired state of the resource. - Desired state of the resource.
choices: ["present", "absent"] choices: ["present", "absent"]
default: "present" default: "present"
type: str type: str
tower_oauthtoken:
description:
- The Tower OAuth token to use.
required: False
type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
@@ -69,67 +81,62 @@ EXAMPLES = '''
''' '''
import os from ..module_utils.tower_api import TowerModule
import json
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode
try:
import tower_cli
import tower_cli.exceptions as exc
from tower_cli.conf import settings
except ImportError:
pass
def main(): def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
name=dict(required=True), name=dict(required=True),
description=dict(), new_name=dict(required=False),
description=dict(required=False),
inventory=dict(required=True), inventory=dict(required=True),
enabled=dict(type='bool', default=True), enabled=dict(type='bool', default=True),
variables=dict(), variables=dict(type='dict', required=False),
state=dict(choices=['present', 'absent'], default='present'), state=dict(choices=['present', 'absent'], default='present'),
) )
# Create a module for ourselves
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
# Extract our parameters
name = module.params.get('name') name = module.params.get('name')
new_name = module.params.get('new_name')
description = module.params.get('description') description = module.params.get('description')
inventory = module.params.get('inventory') inventory = module.params.get('inventory')
enabled = module.params.get('enabled') enabled = module.params.get('enabled')
state = module.params.get('state') state = module.params.get('state')
variables = module.params.get('variables') variables = module.params.get('variables')
if variables:
if variables.startswith('@'):
filename = os.path.expanduser(variables[1:])
with open(filename, 'r') as f:
variables = f.read()
json_output = {'host': name, 'state': state} # Attempt to look up the related items the user specified (these will fail the module if not found)
inventory_id = module.resolve_name_to_id('inventories', inventory)
tower_auth = tower_auth_config(module) # Attempt to look up host based on the provided name and inventory ID
with settings.runtime_values(**tower_auth): host = module.get_one('hosts', **{
tower_check_mode(module) 'data': {
host = tower_cli.get_resource('host') 'name': name,
'inventory': inventory_id
}
})
try: # Create the data that gets sent for create and update
inv_res = tower_cli.get_resource('inventory') host_fields = {
inv = inv_res.get(name=inventory) 'name': new_name if new_name else name,
'inventory': inventory_id,
'enabled': enabled,
}
if description is not None:
host_fields['description'] = description
if variables is not None:
host_fields['variables'] = json.dumps(variables)
if state == 'present': if state == 'absent':
result = host.modify(name=name, inventory=inv['id'], enabled=enabled, # If the state was absent we can let the module delete it if needed, the module will handle exiting from this
variables=variables, description=description, create_on_missing=True) module.delete_if_needed(host)
json_output['id'] = result['id'] elif state == 'present':
elif state == 'absent': # If the state was present and we can let the module build or update the existing host, this will return on its own
result = host.delete(name=name, inventory=inv['id']) module.create_or_update_if_needed(host, host_fields, endpoint='hosts', item_type='host')
except (exc.NotFound) as excinfo:
module.fail_json(msg='Failed to update host, inventory not found: {0}'.format(excinfo), changed=False)
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo:
module.fail_json(msg='Failed to update host: {0}'.format(excinfo), changed=False)
json_output['changed'] = result['changed']
module.exit_json(**json_output)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -39,8 +39,9 @@ options:
type: str type: str
variables: variables:
description: description:
- Inventory variables. Use C(@) to get from file. - Inventory variables.
type: str required: False
type: dict
kind: kind:
description: description:
- The kind field. Cannot be modified after created. - The kind field. Cannot be modified after created.
@@ -59,6 +60,12 @@ options:
default: "present" default: "present"
choices: ["present", "absent"] choices: ["present", "absent"]
type: str type: str
tower_oauthtoken:
description:
- The Tower OAuth token to use.
required: False
type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
@@ -74,30 +81,26 @@ EXAMPLES = '''
''' '''
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode from ..module_utils.tower_api import TowerModule
import json
try:
import tower_cli
import tower_cli.exceptions as exc
from tower_cli.conf import settings
except ImportError:
pass
def main(): def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
name=dict(required=True), name=dict(required=True),
description=dict(), description=dict(required=False),
organization=dict(required=True), organization=dict(required=True),
variables=dict(), variables=dict(type='dict', required=False),
kind=dict(choices=['', 'smart'], default=''), kind=dict(choices=['', 'smart'], default=''),
host_filter=dict(), host_filter=dict(),
state=dict(choices=['present', 'absent'], default='present'), state=dict(choices=['present', 'absent'], default='present'),
) )
# Create a module for ourselves
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
# Extract our parameters
name = module.params.get('name') name = module.params.get('name')
description = module.params.get('description') description = module.params.get('description')
organization = module.params.get('organization') organization = module.params.get('organization')
@@ -106,31 +109,39 @@ def main():
kind = module.params.get('kind') kind = module.params.get('kind')
host_filter = module.params.get('host_filter') host_filter = module.params.get('host_filter')
json_output = {'inventory': name, 'state': state} # 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)
tower_auth = tower_auth_config(module) # Attempt to look up inventory based on the provided name and org ID
with settings.runtime_values(**tower_auth): inventory = module.get_one('inventories', **{
tower_check_mode(module) 'data': {
inventory = tower_cli.get_resource('inventory') 'name': name,
'organization': org_id
}
})
try: # Create the data that gets sent for create and update
org_res = tower_cli.get_resource('organization') inventory_fields = {
org = org_res.get(name=organization) 'name': name,
'organization': org_id,
'kind': kind,
'host_filter': host_filter,
}
if description is not None:
inventory_fields['description'] = description
if variables is not None:
inventory_fields['variables'] = json.dumps(variables)
if state == 'present': if state == 'absent':
result = inventory.modify(name=name, organization=org['id'], variables=variables, # If the state was absent we can let the module delete it if needed, the module will handle exiting from this
description=description, kind=kind, host_filter=host_filter, module.delete_if_needed(inventory)
create_on_missing=True) elif state == 'present':
json_output['id'] = result['id'] # We need to perform a check to make sure you are not trying to convert a regular inventory into a smart one.
elif state == 'absent': if inventory and inventory['kind'] == '' and inventory_fields['kind'] == 'smart':
result = inventory.delete(name=name, organization=org['id']) module.fail_json(msg='You cannot turn a regular inventory into a "smart" inventory.')
except (exc.NotFound) as excinfo:
module.fail_json(msg='Failed to update inventory, organization not found: {0}'.format(excinfo), changed=False)
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo:
module.fail_json(msg='Failed to update inventory: {0}'.format(excinfo), changed=False)
json_output['changed'] = result['changed'] # If the state was present and we can let the module build or update the existing inventory, this will return on its own
module.exit_json(**json_output) module.create_or_update_if_needed(inventory, inventory_fields, endpoint='inventories', item_type='inventory')
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -8,9 +8,9 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
ANSIBLE_METADATA = {'status': ['preview'], ANSIBLE_METADATA = {'metadata_version': '1.1',
'supported_by': 'community', 'status': ['preview'],
'metadata_version': '1.1'} 'supported_by': 'community'}
DOCUMENTATION = ''' DOCUMENTATION = '''
@@ -20,7 +20,7 @@ author: "Adrien Fleury (@fleu42)"
version_added: "2.7" version_added: "2.7"
short_description: create, update, or destroy Ansible Tower inventory source. short_description: create, update, or destroy Ansible Tower inventory source.
description: description:
- Create, update, or destroy Ansible Tower inventories source. See - Create, update, or destroy Ansible Tower inventory source. See
U(https://www.ansible.com/tower) for an overview. U(https://www.ansible.com/tower) for an overview.
options: options:
name: name:
@@ -28,325 +28,222 @@ options:
- The name to use for the inventory source. - The name to use for the inventory source.
required: True required: True
type: str type: str
new_name:
description:
- A new name for this assets (will rename the asset)
required: False
type: str
version_added: "3.7"
description: description:
description: description:
- The description to use for the inventory source. - The description to use for the inventory source.
type: str type: str
inventory: inventory:
description: description:
- The inventory the source is linked to. - Inventory the group should be made a member of.
required: True required: True
type: str type: str
organization:
description:
- Organization the inventory belongs to.
type: str
source: source:
description: description:
- Types of inventory source. - The source to use for this group.
choices: choices: [ "scm", "ec2", "gce", "azure_rm", "vmware", "satellite6", "cloudforms", "openstack", "rhv", "tower", "custom" ]
- file
- scm
- ec2
- gce
- azure
- azure_rm
- vmware
- satellite6
- cloudforms
- openstack
- rhv
- tower
- custom
required: True
type: str type: str
credential: required: False
source_path:
description: description:
- Credential to use to retrieve the inventory from. - For an SCM based inventory source, the source path points to the file within the repo to use as an inventory.
type: str type: str
source_script:
description:
- Inventory script to be used when group type is C(custom).
type: str
required: False
source_vars: source_vars:
description: description:
- >- - The variables or environment fields to apply to this source type.
The source_vars allow to Override variables found in the source config type: dict
file. For example with Openstack, specifying *private: false* would credential:
change the output of the openstack.py script. It has to be YAML or description:
JSON. - Credential to use for the source.
type: str 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.
type: bool
default: 'no'
overwrite_vars:
description:
- Override vars in child groups and hosts with those from external source.
type: bool
custom_virtualenv: custom_virtualenv:
version_added: "2.9" version_added: "2.9"
description: description:
- Local absolute file path containing a custom Python virtualenv to use. - Local absolute file path containing a custom Python virtualenv to use.
type: str type: str
required: False required: False
default: ''
timeout: timeout:
description: The amount of time (in seconds) to run before the task is canceled.
type: int
verbosity:
description: The verbosity level to run this inventory source under.
type: int
choices: [ 0, 1, 2 ]
update_on_launch:
description: description:
- Number in seconds after which the Tower API methods will time out. - Refresh inventory data from its source each time a job is run.
type: bool
default: 'no'
update_cache_timeout:
description:
- Time in seconds to consider an inventory sync to be current.
type: int type: int
source_project: source_project:
description: description:
- Use a *project* as a source for the *inventory*. - Project to use as source with scm option
type: str
source_path:
description:
- Path to the file to use as a source in the selected *project*.
type: str type: str
update_on_project_update: update_on_project_update:
description: description: Update this source when the related project updates if source is C(scm)
- >-
That parameter will sync the inventory when the project is synced. It
can only be used with a SCM source.
type: bool type: bool
source_regions:
description:
- >-
List of regions for your cloud provider. You can include multiple all
regions. Only Hosts associated with the selected regions will be
updated. Refer to Ansible Tower documentation for more detail.
type: str
instance_filters:
description:
- >-
Provide a comma-separated list of filter expressions. Hosts are
imported when all of the filters match. Refer to Ansible Tower
documentation for more detail.
type: str
group_by:
description:
- >-
Specify which groups to create automatically. Group names will be
created similar to the options selected. If blank, all groups above
are created. Refer to Ansible Tower documentation for more detail.
type: str
source_script:
description:
- >-
The source custom script to use to build the inventory. It needs to
exist.
type: str
overwrite:
description:
- >-
If set, any hosts and groups that were previously present on the
external source but are now removed will be removed from the Tower
inventory. Hosts and groups that were not managed by the inventory
source will be promoted to the next manually created group or if
there is no manually created group to promote them into, they will be
left in the "all" default group for the inventory. When not checked,
local child hosts and groups not found on the external source will
remain untouched by the inventory update process.
type: bool
overwrite_vars:
description:
- >-
If set, all variables for child groups and hosts will be removed
and replaced by those found on the external source. When not checked,
a merge will be performed, combining local variables with those found
on the external source.
type: bool
update_on_launch:
description:
- >-
Each time a job runs using this inventory, refresh the inventory from
the selected source before executing job tasks.
type: bool
update_cache_timeout:
description:
- >-
Time in seconds to consider an inventory sync to be current. During
job runs and callbacks the task system will evaluate the timestamp of
the latest sync. If it is older than Cache Timeout, it is not
considered current, and a new inventory sync will be performed.
type: int
state: state:
description: description:
- Desired state of the resource. - Desired state of the resource.
default: "present" default: "present"
choices: ["present", "absent"] choices: ["present", "absent"]
type: str type: str
tower_oauthtoken:
description:
- The Tower OAuth token to use.
required: False
type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
EXAMPLES = ''' EXAMPLES = '''
- name: Add tower inventory source - name: Add an inventory source
tower_inventory_source: tower_inventory_source:
name: Inventory source name: "source-inventory"
description: My Inventory source description: Source for inventory
inventory: My inventory inventory: previously-created-inventory
organization: My organization credential: previously-created-credential
credential: Devstack_credential overwrite: True
source: openstack update_on_launch: True
update_on_launch: true source_vars:
overwrite: true private: false
source_vars: '{ private: false }'
state: present
validate_certs: false
''' '''
from ..module_utils.tower_api import TowerModule
RETURN = ''' # ''' from json import dumps
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode
try:
import tower_cli
import tower_cli.exceptions as exc
from tower_cli.conf import settings
except ImportError:
pass
SOURCE_CHOICES = {
'file': 'Directory or Script',
'scm': 'Sourced from a Project',
'ec2': 'Amazon EC2',
'gce': 'Google Compute Engine',
'azure': 'Microsoft Azure',
'azure_rm': 'Microsoft Azure Resource Manager',
'vmware': 'VMware vCenter',
'satellite6': 'Red Hat Satellite 6',
'cloudforms': 'Red Hat CloudForms',
'openstack': 'OpenStack',
'rhv': 'Red Hat Virtualization',
'tower': 'Ansible Tower',
'custom': 'Custom Script',
}
def main(): def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
name=dict(required=True), name=dict(required=True),
new_name=dict(type='str'),
description=dict(required=False), description=dict(required=False),
inventory=dict(required=True), inventory=dict(required=True),
source=dict(required=True, #
choices=SOURCE_CHOICES.keys()), # How do we handle manual and file? Tower does not seem to be able to activate them
credential=dict(required=False), #
source_vars=dict(required=False), source=dict(choices=["scm", "ec2", "gce",
timeout=dict(type='int', required=False), "azure_rm", "vmware", "satellite6", "cloudforms",
source_project=dict(required=False), "openstack", "rhv", "tower", "custom"], required=False),
source_path=dict(required=False), source_path=dict(),
update_on_project_update=dict(type='bool', required=False),
source_regions=dict(required=False),
instance_filters=dict(required=False),
group_by=dict(required=False),
source_script=dict(required=False), source_script=dict(required=False),
overwrite=dict(type='bool', required=False), source_vars=dict(type='dict'),
overwrite_vars=dict(type='bool', required=False), credential=dict(),
custom_virtualenv=dict(type='str', required=False), source_regions=dict(),
update_on_launch=dict(type='bool', required=False), instance_filters=dict(),
update_cache_timeout=dict(type='int', required=False), group_by=dict(),
organization=dict(type='str'), overwrite=dict(type='bool'),
overwrite_vars=dict(type='bool'),
custom_virtualenv=dict(type='str', default=''),
timeout=dict(type='int'),
verbosity=dict(type='int', choices=[0, 1, 2]),
update_on_launch=dict(type='bool'),
update_cache_timeout=dict(type='int'),
source_project=dict(type='str'),
update_on_project_update=dict(type='bool'),
state=dict(choices=['present', 'absent'], default='present'), state=dict(choices=['present', 'absent'], default='present'),
) )
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec)
# Extract our parameters
name = module.params.get('name') name = module.params.get('name')
new_name = module.params.get('new_name')
inventory = module.params.get('inventory') inventory = module.params.get('inventory')
source = module.params.get('source') source_script = module.params.get('source_script')
credential = module.params.get('credential')
source_project = module.params.get('source_project')
state = module.params.get('state') state = module.params.get('state')
organization = module.params.get('organization')
json_output = {'inventory_source': name, 'state': state} # Attempt to look up inventory source based on the provided name and inventory ID
inventory_id = module.resolve_name_to_id('inventories', inventory)
inventory_source = module.get_one('inventory_sources', **{
'data': {
'name': name,
'inventory': inventory_id,
}
})
tower_auth = tower_auth_config(module) # Create the data that gets sent for create and update
with settings.runtime_values(**tower_auth): inventory_source_fields = {
tower_check_mode(module) 'name': new_name if new_name else name,
inventory_source = tower_cli.get_resource('inventory_source') 'inventory': inventory_id,
try: }
params = {}
params['name'] = name
params['source'] = source
if module.params.get('description'): # Attempt to look up the related items the user specified (these will fail the module if not found)
params['description'] = module.params.get('description') if credential is not None:
inventory_source_fields['credential'] = module.resolve_name_to_id('credentials', credential)
if source_project is not None:
inventory_source_fields['source_project'] = module.resolve_name_to_id('projects', source_project)
if source_script is not None:
inventory_source_fields['source_script'] = module.resolve_name_to_id('inventory_scripts', source_script)
if organization: OPTIONAL_VARS = (
try: 'description', 'source', 'source_path', 'source_vars',
org_res = tower_cli.get_resource('organization') 'source_regions', 'instance_filters', 'group_by',
org = org_res.get(name=organization) 'overwrite', 'overwrite_vars', 'custom_virtualenv',
except (exc.NotFound) as excinfo: 'timeout', 'verbosity', 'update_on_launch', 'update_cache_timeout',
module.fail_json( 'update_on_project_update'
msg='Failed to get organization,' )
'organization not found: {0}'.format(excinfo),
changed=False
)
org_id = org['id']
else:
org_id = None # interpreted as not provided
if module.params.get('credential'): # Layer in all remaining optional information
credential_res = tower_cli.get_resource('credential') for field_name in OPTIONAL_VARS:
try: field_val = module.params.get(field_name)
credential = credential_res.get( if field_val:
name=module.params.get('credential'), organization=org_id) inventory_source_fields[field_name] = field_val
params['credential'] = credential['id']
except (exc.NotFound) as excinfo:
module.fail_json(
msg='Failed to update credential source,'
'credential not found: {0}'.format(excinfo),
changed=False
)
if module.params.get('source_project'): # Attempt to JSON encode source vars
source_project_res = tower_cli.get_resource('project') if inventory_source_fields.get('source_vars', None):
try: inventory_source_fields['source_vars'] = dumps(inventory_source_fields['source_vars'])
source_project = source_project_res.get(
name=module.params.get('source_project'), organization=org_id)
params['source_project'] = source_project['id']
except (exc.NotFound) as excinfo:
module.fail_json(
msg='Failed to update source project,'
'project not found: {0}'.format(excinfo),
changed=False
)
if module.params.get('source_script'): # Sanity check on arguments
source_script_res = tower_cli.get_resource('inventory_script') if state == 'present' and not inventory_source and not inventory_source_fields['source']:
try: module.fail_json(msg="If creating a new inventory source, the source param must be present")
script = source_script_res.get(
name=module.params.get('source_script'), organization=org_id)
params['source_script'] = script['id']
except (exc.NotFound) as excinfo:
module.fail_json(
msg='Failed to update source script,'
'script not found: {0}'.format(excinfo),
changed=False
)
try: if state == 'absent':
inventory_res = tower_cli.get_resource('inventory') # If the state was absent we can let the module delete it if needed, the module will handle exiting from this
params['inventory'] = inventory_res.get(name=inventory, organization=org_id)['id'] module.delete_if_needed(inventory_source)
except (exc.NotFound) as excinfo: elif state == 'present':
module.fail_json( # If the state was present we can let the module build or update the existing inventory_source, this will return on its own
msg='Failed to update inventory source, ' module.create_or_update_if_needed(inventory_source, inventory_source_fields, endpoint='inventory_sources', item_type='inventory source')
'inventory not found: {0}'.format(excinfo),
changed=False
)
for key in ('source_vars', 'custom_virtualenv', 'timeout', 'source_path',
'update_on_project_update', 'source_regions',
'instance_filters', 'group_by', 'overwrite',
'overwrite_vars', 'update_on_launch',
'update_cache_timeout'):
if module.params.get(key) is not None:
params[key] = module.params.get(key)
if state == 'present':
params['create_on_missing'] = True
result = inventory_source.modify(**params)
json_output['id'] = result['id']
elif state == 'absent':
params['fail_on_missing'] = False
result = inventory_source.delete(**params)
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo:
module.fail_json(msg='Failed to update inventory source: \
{0}'.format(excinfo), changed=False)
json_output['changed'] = result['changed']
module.exit_json(**json_output)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -30,9 +30,15 @@ options:
type: int type: int
fail_if_not_running: fail_if_not_running:
description: description:
- Fail loudly if the I(job_id) does not reference a running job. - Fail loudly if the I(job_id) can not be canceled
default: False default: False
type: bool type: bool
tower_oauthtoken:
description:
- The Tower OAuth token to use.
required: False
type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
@@ -48,54 +54,55 @@ id:
returned: success returned: success
type: int type: int
sample: 94 sample: 94
status:
description: status of the cancel request
returned: success
type: str
sample: canceled
''' '''
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode from ..module_utils.tower_api import TowerModule
try:
import tower_cli
import tower_cli.exceptions as exc
from tower_cli.conf import settings
except ImportError:
pass
def main(): def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
job_id=dict(type='int', required=True), job_id=dict(type='int', required=True),
fail_if_not_running=dict(type='bool', default=False), fail_if_not_running=dict(type='bool', default=False),
) )
# Create a module for ourselves
module = TowerModule( module = TowerModule(
argument_spec=argument_spec, argument_spec=argument_spec,
supports_check_mode=True, supports_check_mode=True,
) )
# Extract our parameters
job_id = module.params.get('job_id') job_id = module.params.get('job_id')
json_output = {} fail_if_not_running = module.params.get('fail_if_not_running')
tower_auth = tower_auth_config(module) # Attempt to look up the job based on the provided name
with settings.runtime_values(**tower_auth): job = module.get_one('jobs', **{
tower_check_mode(module) 'data': {
job = tower_cli.get_resource('job') 'id': job_id,
params = module.params.copy() }
})
try: if job is None:
result = job.cancel(job_id, **params) module.fail_json(msg="Unable to find job with id {0}".format(job_id))
json_output['id'] = job_id
except (exc.ConnectionError, exc.BadRequest, exc.TowerCLIError, exc.AuthError) as excinfo:
module.fail_json(msg='Unable to cancel job_id/{0}: {1}'.format(job_id, excinfo), changed=False)
json_output['changed'] = result['changed'] cancel_page = module.get_endpoint(job['related']['cancel'])
json_output['status'] = result['status'] if 'json' not in cancel_page or 'can_cancel' not in cancel_page['json']:
module.exit_json(**json_output) module.fail_json(msg="Failed to cancel job, got unexpected response from tower", **{'response': cancel_page})
if not cancel_page['json']['can_cancel']:
if fail_if_not_running:
module.fail_json(msg="Job is not running")
else:
module.exit_json(**{'changed': False})
results = module.post_endpoint(job['related']['cancel'], **{'data': {}})
if results['status_code'] != 202:
module.fail_json(msg="Failed to cancel job, see response for details", **{'response': results})
module.exit_json(**{'changed': True})
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -23,11 +23,12 @@ description:
- Launch an Ansible Tower jobs. See - Launch an Ansible Tower jobs. See
U(https://www.ansible.com/tower) for an overview. U(https://www.ansible.com/tower) for an overview.
options: options:
job_template: name:
description: description:
- Name of the job template to use. - Name of the job template to use.
required: True required: True
type: str type: str
aliases: ['job_template']
job_type: job_type:
description: description:
- Job_type to use for the job, only used if prompt for job_type is set. - Job_type to use for the job, only used if prompt for job_type is set.
@@ -37,13 +38,15 @@ options:
description: description:
- Inventory to use for the job, only used if prompt for inventory is set. - Inventory to use for the job, only used if prompt for inventory is set.
type: str type: str
credential: credentials:
description: description:
- Credential to use for job, only used if prompt for credential is set. - Credential to use for job, only used if prompt for credential is set.
type: str type: list
aliases: ['credential']
elements: str
extra_vars: extra_vars:
description: description:
- extra_vars to use for the Job Template. Prepend C(@) if a file. - extra_vars to use for the Job Template.
- ask_extra_vars needs to be set to True via tower_job_template module - ask_extra_vars needs to be set to True via tower_job_template module
when creating the Job Template. when creating the Job Template.
type: dict type: dict
@@ -55,21 +58,50 @@ options:
description: description:
- Specific tags to use for from playbook. - Specific tags to use for from playbook.
type: list type: list
elements: str
scm_branch:
description:
- A specific of the SCM project to run the template on.
- This is only applicable if your project allows for branch override.
type: str
version_added: "3.7"
skip_tags:
description:
- Specific tags to skip from the playbook.
type: list
elements: str
version_added: "3.7"
verbosity:
description:
- Verbosity level for this job run
type: int
choices: [ 0, 1, 2, 3, 4, 5 ]
version_added: "3.7"
diff_mode:
description:
- Show the changes made by Ansible tasks where supported
type: bool
version_added: "3.7"
credential_passwords:
description:
- Passwords for credentials which are set to prompt on launch
type: dict
version_added: "3.7"
tower_oauthtoken:
description:
- The Tower OAuth token to use.
required: False
type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
EXAMPLES = ''' EXAMPLES = '''
# Launch a job template
- name: Launch a job - name: Launch a job
tower_job_launch: tower_job_launch:
job_template: "My Job Template" job_template: "My Job Template"
register: job register: job
- name: Wait for job max 120s
tower_job_wait:
job_id: "{{ job.id }}"
timeout: 120
- name: Launch a job template with extra_vars on remote Tower instance - name: Launch a job template with extra_vars on remote Tower instance
tower_job_launch: tower_job_launch:
job_template: "My Job Template" job_template: "My Job Template"
@@ -79,7 +111,6 @@ EXAMPLES = '''
var3: "My Third Variable" var3: "My Third Variable"
job_type: run job_type: run
# Launch job template with inventory and credential for prompt on launch
- name: Launch a job with inventory and credential - name: Launch a job with inventory and credential
tower_job_launch: tower_job_launch:
job_template: "My Job Template" job_template: "My Job Template"
@@ -105,91 +136,103 @@ status:
sample: pending sample: pending
''' '''
import json from ..module_utils.tower_api import TowerModule
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode
try:
import tower_cli
import tower_cli.exceptions as exc
from tower_cli.conf import settings
except ImportError:
pass
def update_fields(module, p):
params = p.copy()
params_update = {}
job_template = params.get('job_template')
extra_vars = params.get('extra_vars')
try:
job_template_to_launch = tower_cli.get_resource('job_template').get(name=job_template)
except (exc.NotFound) as excinfo:
module.fail_json(msg='Unable to launch job, job_template/{0} was not found: {1}'.format(job_template, excinfo), changed=False)
ask_extra_vars = job_template_to_launch['ask_variables_on_launch']
survey_enabled = job_template_to_launch['survey_enabled']
if extra_vars and (ask_extra_vars or survey_enabled):
params_update['extra_vars'] = [json.dumps(extra_vars)]
elif extra_vars:
module.fail_json(msg="extra_vars is set on launch but the Job Template does not have ask_extra_vars or survey_enabled set to True.")
params.update(params_update)
return params
def main(): def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
job_template=dict(required=True, type='str'), name=dict(type='str', required=True, aliases=['job_template']),
job_type=dict(choices=['run', 'check']), job_type=dict(type='str', choices=['run', 'check']),
inventory=dict(type='str', default=None), inventory=dict(type='str', default=None),
credential=dict(type='str', default=None), # Credentials will be a str instead of a list for backwards compatability
credentials=dict(type='list', default=None, aliases=['credential'], elements='str'),
limit=dict(), limit=dict(),
tags=dict(type='list'), tags=dict(type='list', elements='str'),
extra_vars=dict(type='dict', required=False), extra_vars=dict(type='dict', required=False),
scm_branch=dict(type='str', required=False),
skip_tags=dict(type='list', required=False, elements='str'),
verbosity=dict(type='int', required=False, choices=[0, 1, 2, 3, 4, 5]),
diff_mode=dict(type='bool', required=False),
credential_passwords=dict(type='dict', required=False),
) )
module = TowerModule( # Create a module for ourselves
argument_spec=argument_spec, module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
supports_check_mode=True
)
json_output = {} optional_args = {}
tags = module.params.get('tags') # Extract our parameters
name = module.params.get('name')
optional_args['job_type'] = module.params.get('job_type')
inventory = module.params.get('inventory')
credentials = module.params.get('credentials')
optional_args['limit'] = module.params.get('limit')
optional_args['tags'] = module.params.get('tags')
optional_args['extra_vars'] = module.params.get('extra_vars')
optional_args['scm_branch'] = module.params.get('scm_branch')
optional_args['skip_tags'] = module.params.get('skip_tags')
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')
tower_auth = tower_auth_config(module) # Create a datastructure to pass into our job launch
with settings.runtime_values(**tower_auth): post_data = {}
tower_check_mode(module) for key in optional_args.keys():
try: if optional_args[key]:
params = module.params.copy() post_data[key] = optional_args[key]
if isinstance(tags, list):
params['tags'] = ','.join(tags)
job = tower_cli.get_resource('job')
params = update_fields(module, params) # Attempt to look up the related items the user specified (these will fail the module if not found)
if inventory:
post_data['inventory'] = module.resolve_name_to_id('inventories', inventory)
lookup_fields = ('job_template', 'inventory', 'credential') if credentials:
for field in lookup_fields: post_data['credentials'] = []
try: for credential in credentials:
name = params.pop(field) post_data['credentials'].append(module.resolve_name_to_id('credentials', credential))
if name:
result = tower_cli.get_resource(field).get(name=name)
params[field] = result['id']
except exc.NotFound as excinfo:
module.fail_json(msg='Unable to launch job, {0}/{1} was not found: {2}'.format(field, name, excinfo), changed=False)
result = job.launch(no_input=True, **params) # Attempt to look up job_template based on the provided name
json_output['id'] = result['id'] job_template = module.get_one('job_templates', **{
json_output['status'] = result['status'] 'data': {
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: 'name': name,
module.fail_json(msg='Unable to launch job: {0}'.format(excinfo), changed=False) }
})
json_output['changed'] = result['changed'] if job_template is None:
module.exit_json(**json_output) module.fail_json(msg="Unable to find job template by name {0}".format(name))
# The API will allow you to submit values to a jb launch that are not prompt on launch.
# Therefore, we will test to see if anything is set which is not prompt on launch and fail.
check_vars_to_prompts = {
'scm_branch': 'ask_scm_branch_on_launch',
'diff_mode': 'ask_diff_mode_on_launch',
'extra_vars': 'ask_variables_on_launch',
'limit': 'ask_limit_on_launch',
'tags': 'ask_tags_on_launch',
'skip_tags': 'ask_skip_tags_on_launch',
'job_type': 'ask_job_type_on_launch',
'verbosity': 'ask_verbosity_on_launch',
'inventory': 'ask_inventory_on_launch',
'credentials': 'ask_credential_on_launch',
}
param_errors = []
for variable_name in check_vars_to_prompts:
if module.params.get(variable_name) and not job_template[check_vars_to_prompts[variable_name]]:
param_errors.append("The field {0} was specified but the job template does not allow for it to be overridden".format(variable_name))
if len(param_errors) > 0:
module.fail_json(msg="Parameters specified which can not be passed into job template, see errors for details", **{'errors': param_errors})
# Launch the job
results = module.post_endpoint(job_template['related']['launch'], **{'data': post_data})
if results['status_code'] != 201:
module.fail_json(msg="Failed to launch job, see response for details", **{'response': results})
module.exit_json(**{
'changed': True,
'id': results['json']['id'],
'status': results['json']['status'],
})
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -41,6 +41,12 @@ options:
description: description:
- Query used to further filter the list of jobs. C({"foo":"bar"}) will be passed at C(?foo=bar) - Query used to further filter the list of jobs. C({"foo":"bar"}) will be passed at C(?foo=bar)
type: dict type: dict
tower_oauthtoken:
description:
- The Tower OAuth token to use.
required: False
type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
@@ -81,18 +87,11 @@ results:
''' '''
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode from ..module_utils.tower_api import TowerModule
try:
import tower_cli
import tower_cli.exceptions as exc
from tower_cli.conf import settings
except ImportError:
pass
def main(): def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
status=dict(choices=['pending', 'waiting', 'running', 'error', 'failed', 'canceled', 'successful']), status=dict(choices=['pending', 'waiting', 'running', 'error', 'failed', 'canceled', 'successful']),
page=dict(type='int'), page=dict(type='int'),
@@ -100,31 +99,35 @@ def main():
query=dict(type='dict'), query=dict(type='dict'),
) )
# Create a module for ourselves
module = TowerModule( module = TowerModule(
argument_spec=argument_spec, argument_spec=argument_spec,
supports_check_mode=True supports_check_mode=True,
mutually_exclusive=[
('page', 'all_pages'),
]
) )
json_output = {} # Extract our parameters
query = module.params.get('query') query = module.params.get('query')
status = module.params.get('status') status = module.params.get('status')
page = module.params.get('page') page = module.params.get('page')
all_pages = module.params.get('all_pages') all_pages = module.params.get('all_pages')
tower_auth = tower_auth_config(module) job_search_data = {}
with settings.runtime_values(**tower_auth): if page:
tower_check_mode(module) job_search_data['page'] = page
try: if status:
job = tower_cli.get_resource('job') job_search_data['status'] = status
params = {'status': status, 'page': page, 'all_pages': all_pages} if query:
if query: job_search_data.update(query)
params['query'] = query.items() if all_pages:
json_output = job.list(**params) job_list = module.get_all_endpoint('jobs', **{'data': job_search_data})
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: else:
module.fail_json(msg='Failed to list jobs: {0}'.format(excinfo), changed=False) job_list = module.get_endpoint('jobs', **{'data': job_search_data})
module.exit_json(**json_output) # Attempt to look up jobs based on the status
module.exit_json(**job_list['json'])
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -65,6 +65,7 @@ options:
version_added: 2.8 version_added: 2.8
type: list type: list
default: [] default: []
elements: str
vault_credential: vault_credential:
description: description:
- Name of the vault credential to use for the job template. - Name of the vault credential to use for the job template.
@@ -219,7 +220,12 @@ options:
default: "present" default: "present"
choices: ["present", "absent"] choices: ["present", "absent"]
type: str type: str
requirements:
- ansible-tower-cli >= 3.0.2
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
notes: notes:
- JSON for survey_spec can be found in Tower API Documentation. See - JSON for survey_spec can be found in Tower API Documentation. See
U(https://docs.ansible.com/ansible-tower/latest/html/towerapi/api_ref.html#/Job_Templates/Job_Templates_job_templates_survey_spec_create) U(https://docs.ansible.com/ansible-tower/latest/html/towerapi/api_ref.html#/Job_Templates/Job_Templates_job_templates_survey_spec_create)
@@ -333,7 +339,7 @@ def main():
credential=dict(default=''), credential=dict(default=''),
vault_credential=dict(default=''), vault_credential=dict(default=''),
custom_virtualenv=dict(type='str', required=False), custom_virtualenv=dict(type='str', required=False),
credentials=dict(type='list', default=[]), credentials=dict(type='list', default=[], elements='str'),
forks=dict(type='int'), forks=dict(type='int'),
limit=dict(default=''), limit=dict(default=''),
verbosity=dict(type='int', choices=[0, 1, 2, 3, 4], default=0), verbosity=dict(type='int', choices=[0, 1, 2, 3, 4], default=0),

View File

@@ -42,6 +42,10 @@ options:
description: description:
- Maximum time in seconds to wait for a job to finish. - Maximum time in seconds to wait for a job to finish.
type: int type: int
requirements:
- ansible-tower-cli >= 3.0.2
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''

View File

@@ -39,6 +39,10 @@ options:
default: "present" default: "present"
choices: ["present", "absent"] choices: ["present", "absent"]
type: str type: str
requirements:
- ansible-tower-cli >= 3.0.2
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''

View File

@@ -0,0 +1,91 @@
#!/usr/bin/python
# coding: utf-8 -*-
# (c) 2019, 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_license
author: "John Westcott IV (@john-westcott-iv)"
version_added: "2.9"
short_description: Set the license for Ansible Tower
description:
- Get or Set Ansible Tower license. See
U(https://www.ansible.com/tower) for an overview.
options:
data:
description:
- The contents of the license file
required: True
type: dict
version_added: "3.7"
eula_accepted:
description:
- Whether or not the EULA is accepted.
required: True
type: bool
version_added: "3.7"
tower_oauthtoken:
description:
- The Tower OAuth token to use.
required: False
type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth
'''
RETURN = ''' # '''
EXAMPLES = '''
- name: Set the license using a file
license:
data: "{{ lookup('file', '/tmp/my_tower.license') }}"
eula_accepted: True
'''
from ..module_utils.tower_api import TowerModule
def main():
module = TowerModule(
argument_spec=dict(
data=dict(type='dict', required=True),
eula_accepted=dict(type='bool', required=True),
),
supports_check_mode=True
)
json_output = {'changed': False}
if not module.params.get('eula_accepted'):
module.fail_json(msg='You must accept the EULA by passing in the param eula_accepted as True')
json_output['old_license'] = module.get_endpoint('settings/system/')['json']['LICENSE']
new_license = module.params.get('data')
if json_output['old_license'] != new_license:
json_output['changed'] = True
# Deal with check mode
if module.check_mode:
module.exit_json(**json_output)
# We need to add in the EULA
new_license['eula_accepted'] = True
module.post_endpoint('config', data=new_license)
module.exit_json(**json_output)
if __name__ == '__main__':
main()

View File

@@ -64,6 +64,7 @@ options:
- The recipients email addresses. Required if I(notification_type=email). - The recipients email addresses. Required if I(notification_type=email).
required: False required: False
type: list type: list
elements: str
use_tls: use_tls:
description: description:
- The TLS trigger. Required if I(notification_type=email). - The TLS trigger. Required if I(notification_type=email).
@@ -94,6 +95,7 @@ options:
- The destination Slack channels. Required if I(notification_type=slack). - The destination Slack channels. Required if I(notification_type=slack).
required: False required: False
type: list type: list
elements: str
token: token:
description: description:
- The access token. Required if I(notification_type=slack), if I(notification_type=pagerduty) or if I(notification_type=hipchat). - The access token. Required if I(notification_type=slack), if I(notification_type=pagerduty) or if I(notification_type=hipchat).
@@ -114,6 +116,7 @@ options:
- The destination phone numbers. Required if I(notification_type=twillio). - The destination phone numbers. Required if I(notification_type=twillio).
required: False required: False
type: list type: list
elements: str
account_sid: account_sid:
description: description:
- The Twillio account SID. Required if I(notification_type=twillio). - The Twillio account SID. Required if I(notification_type=twillio).
@@ -155,6 +158,7 @@ options:
- HipChat rooms to send the notification to. Required if I(notification_type=hipchat). - HipChat rooms to send the notification to. Required if I(notification_type=hipchat).
required: False required: False
type: list type: list
elements: str
notify: notify:
description: description:
- The notify channel trigger. Required if I(notification_type=hipchat). - The notify channel trigger. Required if I(notification_type=hipchat).
@@ -185,12 +189,17 @@ options:
- The destination channels or users. Required if I(notification_type=irc). - The destination channels or users. Required if I(notification_type=irc).
required: False required: False
type: list type: list
elements: str
state: state:
description: description:
- Desired state of the resource. - Desired state of the resource.
default: "present" default: "present"
choices: ["present", "absent"] choices: ["present", "absent"]
type: str type: str
requirements:
- ansible-tower-cli >= 3.0.2
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
@@ -315,17 +324,17 @@ def main():
notification_configuration=dict(required=False), notification_configuration=dict(required=False),
username=dict(required=False), username=dict(required=False),
sender=dict(required=False), sender=dict(required=False),
recipients=dict(required=False, type='list'), recipients=dict(required=False, type='list', elements='str'),
use_tls=dict(required=False, type='bool'), use_tls=dict(required=False, type='bool'),
host=dict(required=False), host=dict(required=False),
use_ssl=dict(required=False, type='bool'), use_ssl=dict(required=False, type='bool'),
password=dict(required=False, no_log=True), password=dict(required=False, no_log=True),
port=dict(required=False, type='int'), port=dict(required=False, type='int'),
channels=dict(required=False, type='list'), channels=dict(required=False, type='list', elements='str'),
token=dict(required=False, no_log=True), token=dict(required=False, no_log=True),
account_token=dict(required=False, no_log=True), account_token=dict(required=False, no_log=True),
from_number=dict(required=False), from_number=dict(required=False),
to_numbers=dict(required=False, type='list'), to_numbers=dict(required=False, type='list', elements='str'),
account_sid=dict(required=False), account_sid=dict(required=False),
subdomain=dict(required=False), subdomain=dict(required=False),
service_key=dict(required=False, no_log=True), service_key=dict(required=False, no_log=True),
@@ -333,13 +342,13 @@ def main():
message_from=dict(required=False), message_from=dict(required=False),
api_url=dict(required=False), api_url=dict(required=False),
color=dict(required=False, choices=['yellow', 'green', 'red', 'purple', 'gray', 'random']), color=dict(required=False, choices=['yellow', 'green', 'red', 'purple', 'gray', 'random']),
rooms=dict(required=False, type='list'), rooms=dict(required=False, type='list', elements='str'),
notify=dict(required=False, type='bool'), notify=dict(required=False, type='bool'),
url=dict(required=False), url=dict(required=False),
headers=dict(required=False, type='dict', default={}), headers=dict(required=False, type='dict', default={}),
server=dict(required=False), server=dict(required=False),
nickname=dict(required=False), nickname=dict(required=False),
targets=dict(required=False, type='list'), targets=dict(required=False, type='list', elements='str'),
state=dict(choices=['present', 'absent'], default='present'), state=dict(choices=['present', 'absent'], default='present'),
) )

View File

@@ -39,12 +39,25 @@ options:
type: str type: str
required: False required: False
default: '' default: ''
max_hosts:
description:
- The max hosts allowed in this organizations
default: "0"
type: int
required: False
version_added: "3.7"
state: state:
description: description:
- Desired state of the resource. - Desired state of the resource.
default: "present" default: "present"
choices: ["present", "absent"] choices: ["present", "absent"]
type: str type: str
tower_oauthtoken:
description:
- The Tower OAuth token to use.
required: False
type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
@@ -66,49 +79,52 @@ EXAMPLES = '''
tower_config_file: "~/tower_cli.cfg" tower_config_file: "~/tower_cli.cfg"
''' '''
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode from ..module_utils.tower_api import TowerModule
try:
import tower_cli
import tower_cli.exceptions as exc
from tower_cli.conf import settings
except ImportError:
pass
def main(): def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
name=dict(required=True), name=dict(type='str', required=True),
description=dict(), description=dict(type='str', required=False),
custom_virtualenv=dict(type='str', required=False), custom_virtualenv=dict(type='str', required=False),
state=dict(choices=['present', 'absent'], default='present'), max_hosts=dict(type='int', required=False, default="0"),
state=dict(type='str', choices=['present', 'absent'], default='present', required=False),
) )
# Create a module for ourselves
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
# Extract our parameters
name = module.params.get('name') name = module.params.get('name')
description = module.params.get('description') description = module.params.get('description')
custom_virtualenv = module.params.get('custom_virtualenv') custom_virtualenv = module.params.get('custom_virtualenv')
max_hosts = module.params.get('max_hosts')
# instance_group_names = module.params.get('instance_groups')
state = module.params.get('state') state = module.params.get('state')
json_output = {'organization': name, 'state': state} # Attempt to look up organization based on the provided name
organization = module.get_one('organizations', **{
'data': {
'name': name,
}
})
tower_auth = tower_auth_config(module) # Create the data that gets sent for create and update
with settings.runtime_values(**tower_auth): org_fields = {'name': name}
tower_check_mode(module) if description is not None:
organization = tower_cli.get_resource('organization') org_fields['description'] = description
try: if custom_virtualenv is not None:
if state == 'present': org_fields['custom_virtualenv'] = custom_virtualenv
result = organization.modify(name=name, description=description, custom_virtualenv=custom_virtualenv, create_on_missing=True) if max_hosts is not None:
json_output['id'] = result['id'] org_fields['max_hosts'] = max_hosts
elif state == 'absent':
result = organization.delete(name=name)
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo:
module.fail_json(msg='Failed to update the organization: {0}'.format(excinfo), changed=False)
json_output['changed'] = result['changed'] if state == 'absent':
module.exit_json(**json_output) # If the state was absent we can let the module delete it if needed, the module will handle exiting from this
module.delete_if_needed(organization)
elif state == 'present':
# If the state was present and we can let the module build or update the existing organization, this will return on its own
module.create_or_update_if_needed(organization, org_fields, endpoint='organizations', item_type='organization')
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -35,7 +35,7 @@ options:
scm_type: scm_type:
description: description:
- Type of SCM resource. - Type of SCM resource.
choices: ["manual", "git", "hg", "svn"] choices: ["manual", "git", "hg", "svn", "insights"]
default: "manual" default: "manual"
type: str type: str
scm_url: scm_url:
@@ -50,6 +50,13 @@ options:
description: description:
- The branch to use for the SCM resource. - The branch to use for the SCM resource.
type: str type: str
default: ''
scm_refspec:
description:
- The refspec to use for the SCM resource.
type: str
default: ''
version_added: "3.7"
scm_credential: scm_credential:
description: description:
- Name of the credential to use with this SCM resource. - Name of the credential to use with this SCM resource.
@@ -74,8 +81,13 @@ options:
description: description:
- Cache Timeout to cache prior project syncs for a certain number of seconds. - Cache Timeout to cache prior project syncs for a certain number of seconds.
Only valid if scm_update_on_launch is to True, otherwise ignored. Only valid if scm_update_on_launch is to True, otherwise ignored.
default: 0
type: int type: int
default: 0
scm_allow_override:
description:
- Allow changing the SCM branch or revision in a job template that uses this project.
type: bool
version_added: "3.7"
job_timeout: job_timeout:
version_added: "2.8" version_added: "2.8"
description: description:
@@ -91,8 +103,9 @@ options:
default: '' default: ''
organization: organization:
description: description:
- Primary key of organization for project. - Name of organization for project.
type: str type: str
required: True
state: state:
description: description:
- Desired state of the resource. - Desired state of the resource.
@@ -105,9 +118,14 @@ options:
before returning before returning
- Can assure playbook files are populated so that job templates that rely - Can assure playbook files are populated so that job templates that rely
on the project may be successfully created on the project may be successfully created
type: bool type: bool
default: True default: True
tower_oauthtoken:
description:
- The Tower OAuth token to use.
required: False
type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
@@ -133,107 +151,129 @@ EXAMPLES = '''
tower_config_file: "~/tower_cli.cfg" tower_config_file: "~/tower_cli.cfg"
''' '''
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode import time
try: from ..module_utils.tower_api import TowerModule
import tower_cli
import tower_cli.exceptions as exc
from tower_cli.conf import settings
except ImportError: def wait_for_project_update(module, last_request):
pass # The current running job for the udpate is in last_request['summary_fields']['current_update']['id']
if 'current_update' in last_request['summary_fields']:
running = True
while running:
result = module.get_endpoint('/project_updates/{0}/'.format(last_request['summary_fields']['current_update']['id']))['json']
if module.is_job_done(result['status']):
time.sleep(1)
running = False
if result['status'] != 'successful':
module.fail_json(msg="Project update failed")
module.exit_json(**module.json_output)
def main(): def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
name=dict(required=True), name=dict(required=True),
description=dict(), description=dict(required=False),
organization=dict(), scm_type=dict(required=False, choices=['manual', 'git', 'hg', 'svn', 'insights'], default='manual'),
scm_type=dict(choices=['manual', 'git', 'hg', 'svn'], default='manual'), scm_url=dict(required=False),
scm_url=dict(), local_path=dict(required=False),
scm_branch=dict(), scm_branch=dict(required=False, default=''),
scm_credential=dict(), scm_refspec=dict(required=False, default=''),
scm_clean=dict(type='bool', default=False), scm_credential=dict(required=False),
scm_delete_on_update=dict(type='bool', default=False), scm_clean=dict(required=False, type='bool', default=False),
scm_update_on_launch=dict(type='bool', default=False), scm_delete_on_update=dict(required=False, type='bool', default=False),
scm_update_cache_timeout=dict(type='int'), scm_update_on_launch=dict(required=False, type='bool', default=False),
job_timeout=dict(type='int', default=0), scm_update_cache_timeout=dict(required=False, type='int', default=0),
custom_virtualenv=dict(type='str', required=False), scm_allow_override=dict(required=False, type='bool'),
local_path=dict(), job_timeout=dict(required=False, type='int', default=0),
state=dict(choices=['present', 'absent'], default='present'), custom_virtualenv=dict(required=False, type='str'),
wait=dict(type='bool', default=True), organization=dict(required=True),
state=dict(required=False, choices=['present', 'absent'], default='present'),
wait=dict(required=False, type='bool', default=True),
) )
# Create a module for ourselves
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
# Extract our parameters
name = module.params.get('name') name = module.params.get('name')
description = module.params.get('description') description = module.params.get('description')
organization = module.params.get('organization')
scm_type = module.params.get('scm_type') scm_type = module.params.get('scm_type')
if scm_type == "manual": if scm_type == "manual":
scm_type = "" scm_type = ""
scm_url = module.params.get('scm_url') scm_url = module.params.get('scm_url')
local_path = module.params.get('local_path') local_path = module.params.get('local_path')
scm_branch = module.params.get('scm_branch') scm_branch = module.params.get('scm_branch')
scm_refspec = module.params.get('scm_refspec')
scm_credential = module.params.get('scm_credential') scm_credential = module.params.get('scm_credential')
scm_clean = module.params.get('scm_clean') scm_clean = module.params.get('scm_clean')
scm_delete_on_update = module.params.get('scm_delete_on_update') scm_delete_on_update = module.params.get('scm_delete_on_update')
scm_update_on_launch = module.params.get('scm_update_on_launch') scm_update_on_launch = module.params.get('scm_update_on_launch')
scm_update_cache_timeout = module.params.get('scm_update_cache_timeout') scm_update_cache_timeout = module.params.get('scm_update_cache_timeout')
scm_allow_override = module.params.get('scm_allow_override')
job_timeout = module.params.get('job_timeout') job_timeout = module.params.get('job_timeout')
custom_virtualenv = module.params.get('custom_virtualenv') custom_virtualenv = module.params.get('custom_virtualenv')
organization = module.params.get('organization')
state = module.params.get('state') state = module.params.get('state')
wait = module.params.get('wait') wait = module.params.get('wait')
json_output = {'project': name, 'state': state} # 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)
tower_auth = tower_auth_config(module) # Attempt to look up project based on the provided name and org ID
with settings.runtime_values(**tower_auth): project = module.get_one('projects', **{
tower_check_mode(module) 'data': {
project = tower_cli.get_resource('project') 'name': name,
try: 'organization': org_id
if state == 'present': }
try: })
org_res = tower_cli.get_resource('organization')
org = org_res.get(name=organization)
except exc.NotFound:
module.fail_json(msg='Failed to update project, organization not found: {0}'.format(organization), changed=False)
if scm_credential: # Create the data that gets sent for create and update
try: project_fields = {
cred_res = tower_cli.get_resource('credential') 'name': name,
try: 'scm_type': scm_type,
cred = cred_res.get(name=scm_credential) 'scm_url': scm_url,
except tower_cli.exceptions.MultipleResults: 'scm_branch': scm_branch,
module.warn('Multiple credentials found for {0}, falling back looking in project organization'.format(scm_credential)) 'scm_refspec': scm_refspec,
cred = cred_res.get(name=scm_credential, organization=org['id']) 'scm_clean': scm_clean,
scm_credential = cred['id'] 'scm_delete_on_update': scm_delete_on_update,
except exc.NotFound: 'timeout': job_timeout,
module.fail_json(msg='Failed to update project, credential not found: {0}'.format(scm_credential), changed=False) 'organization': org_id,
'scm_update_on_launch': scm_update_on_launch,
'scm_update_cache_timeout': scm_update_cache_timeout,
'custom_virtualenv': custom_virtualenv,
}
if description is not None:
project_fields['description'] = description
if scm_credential is not None:
project_fields['credential'] = scm_credential_id
if scm_allow_override is not None:
project_fields['scm_allow_override'] = scm_allow_override
if scm_type == '':
project_fields['local_path'] = local_path
if (scm_update_cache_timeout is not None) and (scm_update_on_launch is not True): if state != 'absent' and (scm_update_cache_timeout != 0 and scm_update_on_launch is not True):
module.warn('scm_update_cache_timeout will be ignored since scm_update_on_launch was not set to true') module.warn('scm_update_cache_timeout will be ignored since scm_update_on_launch was not set to true')
result = project.modify(name=name, description=description, # If we are doing a not manual project, register our on_change method
organization=org['id'], # An on_change function, if registered, will fire after an post_endpoint or update_if_needed completes successfully
scm_type=scm_type, scm_url=scm_url, local_path=local_path, on_change = None
scm_branch=scm_branch, scm_clean=scm_clean, credential=scm_credential, if wait and scm_type != '':
scm_delete_on_update=scm_delete_on_update, on_change = wait_for_project_update
scm_update_on_launch=scm_update_on_launch,
scm_update_cache_timeout=scm_update_cache_timeout,
job_timeout=job_timeout,
custom_virtualenv=custom_virtualenv,
create_on_missing=True)
json_output['id'] = result['id']
if wait and scm_type != '':
project.wait(pk=None, parent_pk=result['id'])
elif state == 'absent':
result = project.delete(name=name)
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo:
module.fail_json(msg='Failed to update project: {0}'.format(excinfo), changed=False)
json_output['changed'] = result['changed'] if state == 'absent':
module.exit_json(**json_output) # If the state was absent we can let the module delete it if needed, the module will handle exiting from this
module.delete_if_needed(project)
elif state == 'present':
# If the state was present and we can let the module build or update the existing project, this will return on its own
module.create_or_update_if_needed(project, project_fields, endpoint='projects', item_type='project', on_create=on_change, on_update=on_change)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -33,56 +33,67 @@ options:
- List of organization names to export - List of organization names to export
default: [] default: []
type: list type: list
elements: str
user: user:
description: description:
- List of user names to export - List of user names to export
default: [] default: []
type: list type: list
elements: str
team: team:
description: description:
- List of team names to export - List of team names to export
default: [] default: []
type: list type: list
elements: str
credential_type: credential_type:
description: description:
- List of credential type names to export - List of credential type names to export
default: [] default: []
type: list type: list
elements: str
credential: credential:
description: description:
- List of credential names to export - List of credential names to export
default: [] default: []
type: list type: list
elements: str
notification_template: notification_template:
description: description:
- List of notification template names to export - List of notification template names to export
default: [] default: []
type: list type: list
elements: str
inventory_script: inventory_script:
description: description:
- List of inventory script names to export - List of inventory script names to export
default: [] default: []
type: list type: list
elements: str
inventory: inventory:
description: description:
- List of inventory names to export - List of inventory names to export
default: [] default: []
type: list type: list
elements: str
project: project:
description: description:
- List of project names to export - List of project names to export
default: [] default: []
type: list type: list
elements: str
job_template: job_template:
description: description:
- List of job template names to export - List of job template names to export
default: [] default: []
type: list type: list
elements: str
workflow: workflow:
description: description:
- List of workflow names to export - List of workflow names to export
default: [] default: []
type: list type: list
elements: str
requirements: requirements:
- "ansible-tower-cli >= 3.3.0" - "ansible-tower-cli >= 3.3.0"
@@ -136,21 +147,23 @@ except ImportError:
def main(): def main():
argument_spec = dict( argument_spec = dict(
all=dict(type='bool', default=False), all=dict(type='bool', default=False),
credential=dict(type='list', default=[]), credential=dict(type='list', default=[], elements='str'),
credential_type=dict(type='list', default=[]), credential_type=dict(type='list', default=[], elements='str'),
inventory=dict(type='list', default=[]), inventory=dict(type='list', default=[], elements='str'),
inventory_script=dict(type='list', default=[]), inventory_script=dict(type='list', default=[], elements='str'),
job_template=dict(type='list', default=[]), job_template=dict(type='list', default=[], elements='str'),
notification_template=dict(type='list', default=[]), notification_template=dict(type='list', default=[], elements='str'),
organization=dict(type='list', default=[]), organization=dict(type='list', default=[], elements='str'),
project=dict(type='list', default=[]), project=dict(type='list', default=[], elements='str'),
team=dict(type='list', default=[]), team=dict(type='list', default=[], elements='str'),
user=dict(type='list', default=[]), user=dict(type='list', default=[], elements='str'),
workflow=dict(type='list', default=[]), workflow=dict(type='list', default=[], elements='str'),
) )
module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) module = TowerModule(argument_spec=argument_spec, supports_check_mode=False)
module.deprecate(msg="This module is being moved to a different collection. Instead of awx.awx it will be migrated into awx.tower_cli", version="3.7")
if not HAS_TOWER_CLI: if not HAS_TOWER_CLI:
module.fail_json(msg='ansible-tower-cli required for this module') module.fail_json(msg='ansible-tower-cli required for this module')

View File

@@ -68,6 +68,10 @@ options:
default: "present" default: "present"
choices: ["present", "absent"] choices: ["present", "absent"]
type: str type: str
requirements:
- ansible-tower-cli >= 3.0.2
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
@@ -137,6 +141,8 @@ def main():
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
module.deprecate(msg="This module is being moved to a different collection. Instead of awx.awx it will be migrated into awx.tower_cli", version="3.7")
role_type = module.params.pop('role') role_type = module.params.pop('role')
state = module.params.pop('state') state = module.params.pop('state')

View File

@@ -35,12 +35,14 @@ options:
required: False required: False
default: [] default: []
type: list type: list
elements: str
prevent: prevent:
description: description:
- A list of asset types to prevent import for - A list of asset types to prevent import for
required: false required: false
default: [] default: []
type: list type: list
elements: str
password_management: password_management:
description: description:
- The password management option to use. - The password management option to use.
@@ -97,13 +99,15 @@ except ImportError:
def main(): def main():
argument_spec = dict( argument_spec = dict(
assets=dict(required=False), assets=dict(required=False),
files=dict(required=False, default=[], type='list'), files=dict(required=False, default=[], type='list', elements='str'),
prevent=dict(required=False, default=[], type='list'), prevent=dict(required=False, default=[], type='list', elements='str'),
password_management=dict(required=False, default='default', choices=['default', 'random']), password_management=dict(required=False, default='default', choices=['default', 'random']),
) )
module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) module = TowerModule(argument_spec=argument_spec, supports_check_mode=False)
module.deprecate(msg="This module is being moved to a different collection. Instead of awx.awx it will be migrated into awx.tower_cli", version="3.7")
if not HAS_TOWER_CLI: if not HAS_TOWER_CLI:
module.fail_json(msg='ansible-tower-cli required for this module') module.fail_json(msg='ansible-tower-cli required for this module')

View File

@@ -26,18 +26,28 @@ options:
name: name:
description: description:
- Name of setting to modify - Name of setting to modify
required: True required: False
type: str type: str
value: value:
description: description:
- Value to be modified for given setting. - Value to be modified for given setting.
required: True required: False
type: str type: str
settings:
description:
- A data structure to be sent into the settings endpoint
required: False
type: dict
version_added: "3.7"
tower_oauthtoken:
description:
- The Tower OAuth token to use.
required: False
type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
RETURN = ''' # '''
EXAMPLES = ''' EXAMPLES = '''
- name: Set the value of AWX_PROOT_BASE_PATH - name: Set the value of AWX_PROOT_BASE_PATH
tower_settings: tower_settings:
@@ -56,50 +66,98 @@ EXAMPLES = '''
name: "AUTH_LDAP_BIND_PASSWORD" name: "AUTH_LDAP_BIND_PASSWORD"
value: "Password" value: "Password"
no_log: true no_log: true
- name: Set all the LDAP Auth Bind Params
tower_settings:
settings:
AUTH_LDAP_BIND_PASSWORD: "password"
AUTH_LDAP_USER_ATTR_MAP:
email: "mail"
first_name: "givenName"
last_name: "surname"
''' '''
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode from ..module_utils.tower_api import TowerModule
from json import loads
try: import re
import tower_cli
import tower_cli.exceptions as exc
from tower_cli.conf import settings
except ImportError:
pass
def main(): def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
name=dict(required=True), name=dict(required=False),
value=dict(required=True), value=dict(required=False),
settings=dict(required=False, type='dict'),
) )
# Create a module for ourselves
module = TowerModule( module = TowerModule(
argument_spec=argument_spec, argument_spec=argument_spec,
supports_check_mode=False supports_check_mode=True,
required_one_of=[['name', 'settings']],
mutually_exclusive=[['name', 'settings']],
required_if=[['name', 'present', ['value']]]
) )
json_output = {} # Extract our parameters
name = module.params.get('name') name = module.params.get('name')
value = module.params.get('value') value = module.params.get('value')
new_settings = module.params.get('settings')
tower_auth = tower_auth_config(module) # If we were given a name/value pair we will just make settings out of that and proceed normally
with settings.runtime_values(**tower_auth): if new_settings is None:
tower_check_mode(module) new_value = value
try: try:
setting = tower_cli.get_resource('setting') new_value = loads(value)
result = setting.modify(setting=name, value=value) except ValueError:
# JSONDecodeError only available on Python 3.5+
# Attempt to deal with old tower_cli array types
if ',' in value:
new_value = re.split(r",\s+", new_value)
json_output['id'] = result['id'] new_settings = {name: new_value}
json_output['value'] = result['value']
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: # Load the existing settings
module.fail_json(msg='Failed to modify the setting: {0}'.format(excinfo), changed=False) existing_settings = module.get_endpoint('settings/all')['json']
json_output['changed'] = result['changed'] # Begin a json response
module.exit_json(**json_output) json_response = {'changed': False, 'old_values': {}}
# Check any of the settings to see if anything needs to be updated
needs_update = False
for a_setting in new_settings:
if a_setting not in existing_settings or existing_settings[a_setting] != new_settings[a_setting]:
# At least one thing is different so we need to patch
needs_update = True
json_response['old_values'][a_setting] = existing_settings[a_setting]
# If nothing needs an update we can simply exit with the response (as not changed)
if not needs_update:
module.exit_json(**json_response)
# Make the call to update the settings
response = module.patch_endpoint('settings/all', **{'data': new_settings})
if response['status_code'] == 200:
# Set the changed response to True
json_response['changed'] = True
# To deal with the old style values we need to return 'value' in the response
new_values = {}
for a_setting in new_settings:
new_values[a_setting] = response['json'][a_setting]
# If we were using a name we will just add a value of a string, otherwise we will return an array in values
if name is not None:
json_response['value'] = new_values[name]
else:
json_response['values'] = new_values
module.exit_json(**json_response)
elif 'json' in response and '__all__' in response['json']:
module.fail_json(msg=response['json']['__all__'])
else:
module.fail_json(**{'msg': "Unable to update settings, see response", 'response': response})
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -28,9 +28,16 @@ options:
- Name to use for the team. - Name to use for the team.
required: True required: True
type: str type: str
new_name:
description:
- To use when changing a team's name.
required: False
type: str
version_added: "3.7"
description: description:
description: description:
- The description to use for the team. - The description to use for the team.
required: False
type: str type: str
organization: organization:
description: description:
@@ -43,6 +50,12 @@ options:
choices: ["present", "absent"] choices: ["present", "absent"]
default: "present" default: "present"
type: str type: str
tower_oauthtoken:
description:
- The Tower OAuth token to use.
required: False
type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
@@ -57,57 +70,54 @@ EXAMPLES = '''
tower_config_file: "~/tower_cli.cfg" tower_config_file: "~/tower_cli.cfg"
''' '''
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode from ..module_utils.tower_api import TowerModule
try:
import tower_cli
import tower_cli.exceptions as exc
from tower_cli.conf import settings
except ImportError:
pass
def main(): def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
name=dict(required=True), name=dict(required=True),
description=dict(), new_name=dict(required=False),
description=dict(required=False),
organization=dict(required=True), organization=dict(required=True),
state=dict(choices=['present', 'absent'], default='present'), state=dict(choices=['present', 'absent'], default='present'),
) )
# Create a module for ourselves
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
# Extract our parameters
name = module.params.get('name') name = module.params.get('name')
new_name = module.params.get('new_name')
description = module.params.get('description') description = module.params.get('description')
organization = module.params.get('organization') organization = module.params.get('organization')
state = module.params.get('state') state = module.params.get('state')
json_output = {'team': name, 'state': state} # 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)
tower_auth = tower_auth_config(module) # Attempt to look up team based on the provided name and org ID
with settings.runtime_values(**tower_auth): team = module.get_one('teams', **{
tower_check_mode(module) 'data': {
team = tower_cli.get_resource('team') 'name': name,
'organization': org_id
}
})
try: # Create the data that gets sent for create and update
org_res = tower_cli.get_resource('organization') team_fields = {
org = org_res.get(name=organization) 'name': new_name if new_name else name,
'organization': org_id
}
if description is not None:
team_fields['description'] = description
if state == 'present': if state == 'absent':
result = team.modify(name=name, organization=org['id'], # If the state was absent we can let the module delete it if needed, the module will handle exiting from this
description=description, create_on_missing=True) module.delete_if_needed(team)
json_output['id'] = result['id'] elif state == 'present':
elif state == 'absent': # If the state was present and we can let the module build or update the existing team, this will return on its own
result = team.delete(name=name, organization=org['id']) module.create_or_update_if_needed(team, team_fields, endpoint='teams', item_type='team')
except (exc.NotFound) as excinfo:
module.fail_json(msg='Failed to update team, organization not found: {0}'.format(excinfo), changed=False)
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo:
module.fail_json(msg='Failed to update team: {0}'.format(excinfo), changed=False)
json_output['changed'] = result['changed']
module.exit_json(**json_output)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -38,8 +38,8 @@ options:
type: str type: str
email: email:
description: description:
- Email address of the user. - Email address of the user. Required if creating a new user.
required: True required: False
type: str type: str
password: password:
description: description:
@@ -61,10 +61,12 @@ options:
default: "present" default: "present"
choices: ["present", "absent"] choices: ["present", "absent"]
type: str type: str
tower_oauthtoken:
requirements: description:
- ansible-tower-cli >= 3.2.0 - The Tower OAuth token to use.
required: False
type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
@@ -106,59 +108,54 @@ EXAMPLES = '''
tower_config_file: "~/tower_cli.cfg" tower_config_file: "~/tower_cli.cfg"
''' '''
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode from ..module_utils.tower_api import TowerModule
try:
import tower_cli
import tower_cli.exceptions as exc
from tower_cli.conf import settings
except ImportError:
pass
def main(): def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
username=dict(required=True), username=dict(required=True),
first_name=dict(), first_name=dict(),
last_name=dict(), last_name=dict(),
password=dict(no_log=True), password=dict(no_log=True),
email=dict(required=True), email=dict(required=False, default=''),
superuser=dict(type='bool', default=False), superuser=dict(type='bool', default=False),
auditor=dict(type='bool', default=False), auditor=dict(type='bool', default=False),
state=dict(choices=['present', 'absent'], default='present'), state=dict(choices=['present', 'absent'], default='present'),
) )
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True, required_if=[['state', 'present', ['email']]])
username = module.params.get('username') # Extract our parameters
first_name = module.params.get('first_name')
last_name = module.params.get('last_name')
password = module.params.get('password')
email = module.params.get('email')
superuser = module.params.get('superuser')
auditor = module.params.get('auditor')
state = module.params.get('state') state = module.params.get('state')
email = module.params.get('email')
json_output = {'username': username, 'state': state} # Create the data that gets sent for create and update
user_fields = {
'username': module.params.get('username'),
'first_name': module.params.get('first_name'),
'last_name': module.params.get('last_name'),
'password': module.params.get('password'),
'superuser': module.params.get('superuser'),
'auditor': module.params.get('auditor'),
}
if email is not None:
user_fields['email'] = email
tower_auth = tower_auth_config(module) # Attempt to look up user based on the provided username
with settings.runtime_values(**tower_auth): user = module.get_one('users', **{
tower_check_mode(module) 'data': {
user = tower_cli.get_resource('user') 'username': user_fields['username'],
try: }
if state == 'present': })
result = user.modify(username=username, first_name=first_name, last_name=last_name,
email=email, password=password, is_superuser=superuser,
is_system_auditor=auditor, create_on_missing=True)
json_output['id'] = result['id']
elif state == 'absent':
result = user.delete(username=username)
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo:
module.fail_json(msg='Failed to update the user: {0}'.format(excinfo), changed=False)
json_output['changed'] = result['changed'] if state == 'absent':
module.exit_json(**json_output) # If the state was absent we can let the module delete it if needed, the module will handle exiting from this
module.delete_if_needed(user)
elif state == 'present':
# If the state was present and we can let the module build or update the existing user, this will return on its own
module.create_or_update_if_needed(user, user_fields, endpoint='users', item_type='user')
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -44,6 +44,8 @@ options:
requirements: requirements:
- "python >= 2.6" - "python >= 2.6"
- ansible-tower-cli >= 3.0.2
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''

View File

@@ -81,6 +81,10 @@ options:
default: "present" default: "present"
choices: ["present", "absent"] choices: ["present", "absent"]
type: str type: str
requirements:
- ansible-tower-cli >= 3.0.2
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''

View File

@@ -5,7 +5,7 @@ import io
import json import json
import datetime import datetime
import importlib import importlib
from contextlib import redirect_stdout from contextlib import redirect_stdout, suppress
from unittest import mock from unittest import mock
import logging import logging
@@ -16,6 +16,12 @@ import pytest
from awx.main.tests.functional.conftest import _request from awx.main.tests.functional.conftest import _request
from awx.main.models import Organization, Project, Inventory, Credential, CredentialType from awx.main.models import Organization, Project, Inventory, Credential, CredentialType
try:
import tower_cli # noqa
HAS_TOWER_CLI = True
except ImportError:
HAS_TOWER_CLI = False
logger = logging.getLogger('awx.main.tests') logger = logging.getLogger('awx.main.tests')
@@ -41,13 +47,28 @@ def sanitize_dict(din):
@pytest.fixture @pytest.fixture
def run_module(request): def collection_import():
"""These tests run assuming that the awx_collection folder is inserted
into the PATH before-hand. But all imports internally to the collection
go through this fixture so that can be changed if needed.
For instance, we could switch to fully-qualified import paths.
"""
def rf(path):
return importlib.import_module(path)
return rf
@pytest.fixture
def run_module(request, collection_import):
def rf(module_name, module_params, request_user): def rf(module_name, module_params, request_user):
def new_request(self, method, url, **kwargs): def new_request(self, method, url, **kwargs):
kwargs_copy = kwargs.copy() kwargs_copy = kwargs.copy()
if 'data' in kwargs: if 'data' in kwargs:
kwargs_copy['data'] = json.loads(kwargs['data']) if not isinstance(kwargs['data'], dict):
kwargs_copy['data'] = json.loads(kwargs['data'])
else:
kwargs_copy['data'] = kwargs['data']
if 'params' in kwargs and method == 'GET': if 'params' in kwargs and method == 'GET':
# query params for GET are handled a bit differently by # query params for GET are handled a bit differently by
# tower-cli and python requests as opposed to REST framework APIRequestFactory # tower-cli and python requests as opposed to REST framework APIRequestFactory
@@ -79,12 +100,16 @@ def run_module(request):
return resp return resp
def new_open(self, method, url, **kwargs):
r = new_request(self, method, url, **kwargs)
return mock.MagicMock(read=mock.MagicMock(return_value=r._content), status=r.status_code)
stdout_buffer = io.StringIO() stdout_buffer = io.StringIO()
# Requies specific PYTHONPATH, see docs # Requies specific PYTHONPATH, see docs
# Note that a proper Ansiballz explosion of the modules will have an import path like: # Note that a proper Ansiballz explosion of the modules will have an import path like:
# ansible_collections.awx.awx.plugins.modules.{} # ansible_collections.awx.awx.plugins.modules.{}
# We should consider supporting that in the future # We should consider supporting that in the future
resource_module = importlib.import_module('plugins.modules.{0}'.format(module_name)) resource_module = collection_import('plugins.modules.{0}'.format(module_name))
if not isinstance(module_params, dict): if not isinstance(module_params, dict):
raise RuntimeError('Module params must be dict, got {0}'.format(type(module_params))) raise RuntimeError('Module params must be dict, got {0}'.format(type(module_params)))
@@ -96,16 +121,24 @@ def run_module(request):
with mock.patch.object(resource_module.TowerModule, '_load_params', new=mock_load_params): with mock.patch.object(resource_module.TowerModule, '_load_params', new=mock_load_params):
# Call the test utility (like a mock server) instead of issuing HTTP requests # Call the test utility (like a mock server) instead of issuing HTTP requests
with mock.patch('tower_cli.api.Session.request', new=new_request): with mock.patch('ansible.module_utils.urls.Request.open', new=new_open):
# Ansible modules return data to the mothership over stdout if HAS_TOWER_CLI:
with redirect_stdout(stdout_buffer): tower_cli_mgr = mock.patch('tower_cli.api.Session.request', new=new_request)
try: else:
resource_module.main() tower_cli_mgr = suppress()
except SystemExit: with tower_cli_mgr:
pass # A system exit indicates successful execution # Ansible modules return data to the mothership over stdout
with redirect_stdout(stdout_buffer):
try:
resource_module.main()
except SystemExit:
pass # A system exit indicates successful execution
module_stdout = stdout_buffer.getvalue().strip() module_stdout = stdout_buffer.getvalue().strip()
result = json.loads(module_stdout) result = json.loads(module_stdout)
# A module exception should never be a test expectation
if 'exception' in result:
raise Exception('Module encountered error:\n{0}'.format(result['exception']))
return result return result
return rf return rf

View File

@@ -79,12 +79,10 @@ def test_create_custom_credential_type(run_module, admin_user):
ct = CredentialType.objects.get(name='Nexus') ct = CredentialType.objects.get(name='Nexus')
result.pop('invocation') result.pop('invocation')
assert result == { assert result == {
"credential_type": "Nexus", "name": "Nexus",
"state": "present",
"id": ct.pk, "id": ct.pk,
"changed": True "changed": True,
} }
assert ct.inputs == {"fields": [{"id": "server", "type": "string", "default": "", "label": ""}], "required": []} assert ct.inputs == {"fields": [{"id": "server", "type": "string", "default": "", "label": ""}], "required": []}
assert ct.injectors == {'extra_vars': {'nexus_credential': 'test'}} assert ct.injectors == {'extra_vars': {'nexus_credential': 'test'}}

View File

@@ -10,25 +10,25 @@ from awx.main.models import Organization, Inventory, Group
def test_create_group(run_module, admin_user): def test_create_group(run_module, admin_user):
org = Organization.objects.create(name='test-org') org = Organization.objects.create(name='test-org')
inv = Inventory.objects.create(name='test-inv', organization=org) inv = Inventory.objects.create(name='test-inv', organization=org)
variables = {"ansible_network_os": "iosxr"}
result = run_module('tower_group', dict( result = run_module('tower_group', dict(
name='Test Group', name='Test Group',
inventory='test-inv', inventory='test-inv',
variables='ansible_network_os: iosxr', variables=variables,
state='present' state='present'
), admin_user) ), admin_user)
assert result.get('changed'), result assert result.get('changed'), result
group = Group.objects.get(name='Test Group') group = Group.objects.get(name='Test Group')
assert group.inventory == inv assert group.inventory == inv
assert group.variables == 'ansible_network_os: iosxr' assert group.variables == '{"ansible_network_os": "iosxr"}'
result.pop('invocation') result.pop('invocation')
assert result == { assert result == {
'id': group.id, 'id': group.id,
'group': 'Test Group', 'name': 'Test Group',
'changed': True, 'changed': True,
'state': 'present'
} }
@@ -40,20 +40,16 @@ def test_tower_group_idempotent(run_module, admin_user):
group = Group.objects.create( group = Group.objects.create(
name='Test Group', name='Test Group',
inventory=inv, inventory=inv,
variables='ansible_network_os: iosxr'
) )
result = run_module('tower_group', dict( result = run_module('tower_group', dict(
name='Test Group', name='Test Group',
inventory='test-inv', inventory='test-inv',
variables='ansible_network_os: iosxr',
state='present' state='present'
), admin_user) ), admin_user)
result.pop('invocation') result.pop('invocation')
assert result == { assert result == {
'id': group.id, 'id': group.id,
'group': 'Test Group',
'changed': False, # idempotency assertion 'changed': False, # idempotency assertion
'state': 'present'
} }

View File

@@ -10,23 +10,29 @@ from awx.main.models import Organization, Inventory, InventorySource, Project
def base_inventory(): def base_inventory():
org = Organization.objects.create(name='test-org') org = Organization.objects.create(name='test-org')
inv = Inventory.objects.create(name='test-inv', organization=org) inv = Inventory.objects.create(name='test-inv', organization=org)
Project.objects.create(
name='test-proj',
organization=org,
scm_type='git',
scm_url='https://github.com/ansible/test-playbooks.git',
)
return inv return inv
@pytest.fixture
def project(base_inventory):
return Project.objects.create(
name='test-proj',
organization=base_inventory.organization,
scm_type='git',
scm_url='https://github.com/ansible/test-playbooks.git',
)
@pytest.mark.django_db @pytest.mark.django_db
def test_inventory_source_create(run_module, admin_user, base_inventory): def test_inventory_source_create(run_module, admin_user, base_inventory, project):
source_path = '/var/lib/awx/example_source_path/'
result = run_module('tower_inventory_source', dict( result = run_module('tower_inventory_source', dict(
name='foo', name='foo',
inventory='test-inv', inventory=base_inventory.name,
state='present', state='present',
source='scm', source='scm',
source_project='test-proj' source_path=source_path,
source_project=project.name
), admin_user) ), admin_user)
assert result.pop('changed', None), result assert result.pop('changed', None), result
@@ -35,8 +41,7 @@ def test_inventory_source_create(run_module, admin_user, base_inventory):
result.pop('invocation') result.pop('invocation')
assert result == { assert result == {
'id': inv_src.id, 'id': inv_src.id,
'inventory_source': 'foo', 'name': 'foo',
'state': 'present'
} }
@@ -45,6 +50,7 @@ def test_create_inventory_source_implied_org(run_module, admin_user):
org = Organization.objects.create(name='test-org') org = Organization.objects.create(name='test-org')
inv = Inventory.objects.create(name='test-inv', organization=org) inv = Inventory.objects.create(name='test-inv', organization=org)
# Credential is not required for ec2 source, because of IAM roles
result = run_module('tower_inventory_source', dict( result = run_module('tower_inventory_source', dict(
name='Test Inventory Source', name='Test Inventory Source',
inventory='test-inv', inventory='test-inv',
@@ -58,8 +64,7 @@ def test_create_inventory_source_implied_org(run_module, admin_user):
result.pop('invocation') result.pop('invocation')
assert result == { assert result == {
"inventory_source": "Test Inventory Source", "name": "Test Inventory Source",
"state": "present",
"id": inv_src.id, "id": inv_src.id,
} }
@@ -67,43 +72,43 @@ def test_create_inventory_source_implied_org(run_module, admin_user):
@pytest.mark.django_db @pytest.mark.django_db
def test_create_inventory_source_multiple_orgs(run_module, admin_user): def test_create_inventory_source_multiple_orgs(run_module, admin_user):
org = Organization.objects.create(name='test-org') org = Organization.objects.create(name='test-org')
inv = Inventory.objects.create(name='test-inv', organization=org) Inventory.objects.create(name='test-inv', organization=org)
# make another inventory by same name in another org # make another inventory by same name in another org
org2 = Organization.objects.create(name='test-org-number-two') org2 = Organization.objects.create(name='test-org-number-two')
Inventory.objects.create(name='test-inv', organization=org2) inv2 = Inventory.objects.create(name='test-inv', organization=org2)
result = run_module('tower_inventory_source', dict( result = run_module('tower_inventory_source', dict(
name='Test Inventory Source', name='Test Inventory Source',
inventory='test-inv', inventory=inv2.id,
source='ec2', source='ec2',
organization='test-org',
state='present' state='present'
), admin_user) ), admin_user)
assert result.pop('changed', None), result assert result.pop('changed', None), result
inv_src = InventorySource.objects.get(name='Test Inventory Source') inv_src = InventorySource.objects.get(name='Test Inventory Source')
assert inv_src.inventory == inv assert inv_src.inventory == inv2
result.pop('invocation') result.pop('invocation')
assert result == { assert result == {
"inventory_source": "Test Inventory Source", "name": "Test Inventory Source",
"state": "present",
"id": inv_src.id, "id": inv_src.id,
} }
@pytest.mark.django_db @pytest.mark.django_db
def test_create_inventory_source_with_venv(run_module, admin_user, base_inventory, mocker): def test_create_inventory_source_with_venv(run_module, admin_user, base_inventory, mocker, project):
path = '/var/lib/awx/venv/custom-venv/foobar13489435/' path = '/var/lib/awx/venv/custom-venv/foobar13489435/'
source_path = '/var/lib/awx/example_source_path/'
with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=[path]): with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=[path]):
result = run_module('tower_inventory_source', dict( result = run_module('tower_inventory_source', dict(
name='foo', name='foo',
inventory='test-inv', inventory=base_inventory.name,
state='present', state='present',
source='scm', source='scm',
source_project='test-proj', source_project=project.name,
custom_virtualenv=path custom_virtualenv=path,
source_path=source_path
), admin_user) ), admin_user)
assert result.pop('changed'), result assert result.pop('changed'), result
@@ -115,16 +120,17 @@ def test_create_inventory_source_with_venv(run_module, admin_user, base_inventor
@pytest.mark.django_db @pytest.mark.django_db
def test_custom_venv_no_op(run_module, admin_user, base_inventory, mocker): def test_custom_venv_no_op(run_module, admin_user, base_inventory, mocker, project):
"""If the inventory source is modified, then it should not blank fields """If the inventory source is modified, then it should not blank fields
unrelated to the params that the user passed. unrelated to the params that the user passed.
This enforces assumptions about the behavior of the AnsibleModule This enforces assumptions about the behavior of the AnsibleModule
default argument_spec behavior. default argument_spec behavior.
""" """
source_path = '/var/lib/awx/example_source_path/'
inv_src = InventorySource.objects.create( inv_src = InventorySource.objects.create(
name='foo', name='foo',
inventory=base_inventory, inventory=base_inventory,
source_project=Project.objects.get(name='test-proj'), source_project=project,
source='scm', source='scm',
custom_virtualenv='/venv/foobar/' custom_virtualenv='/venv/foobar/'
) )
@@ -133,11 +139,93 @@ def test_custom_venv_no_op(run_module, admin_user, base_inventory, mocker):
result = run_module('tower_inventory_source', dict( result = run_module('tower_inventory_source', dict(
name='foo', name='foo',
description='this is the changed description', description='this is the changed description',
inventory='test-inv', inventory=base_inventory.name,
source='scm', # is required, but behavior is arguable source='scm', # is required, but behavior is arguable
state='present' state='present',
source_project=project.name,
source_path=source_path
), admin_user) ), admin_user)
assert result.pop('changed', None), result assert result.pop('changed', None), result
inv_src.refresh_from_db() inv_src.refresh_from_db()
assert inv_src.custom_virtualenv == '/venv/foobar/' assert inv_src.custom_virtualenv == '/venv/foobar/'
assert inv_src.description == 'this is the changed description' assert inv_src.description == 'this is the changed description'
# Tests related to source-specific parameters
#
# We want to let the API return issues with "this doesn't support that", etc.
#
# GUI OPTIONS:
# - - - - - - - manual: file: scm: ec2: gce azure_rm vmware sat cloudforms openstack rhv tower custom
# credential ? ? o o r r r r r r r r o
# source_project ? ? r - - - - - - - - - -
# source_path ? ? r - - - - - - - - - -
# verbosity ? ? o o o o o o o o o o o
# overwrite ? ? o o o o o o o o o o o
# 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
#
# UoPL - update_on_project_launch
# * - source_vars are labeled environment_vars on project and custom sources
@pytest.mark.django_db
def test_missing_required_credential(run_module, admin_user, base_inventory):
result = run_module('tower_inventory_source', dict(
name='Test Azure Source',
inventory=base_inventory.name,
source='azure_rm',
state='present'
), admin_user)
assert result.pop('failed', None) is True, result
assert 'Credential is required for a cloud source' in result.get('msg', '')
@pytest.mark.django_db
def test_source_project_not_for_cloud(run_module, admin_user, base_inventory, project):
result = run_module('tower_inventory_source', dict(
name='Test ec2 Inventory Source',
inventory=base_inventory.name,
source='ec2',
state='present',
source_project=project.name
), admin_user)
assert result.pop('failed', None) is True, result
assert 'Cannot set source_project if not SCM type' in result.get('msg', '')
@pytest.mark.django_db
def test_source_path_not_for_cloud(run_module, admin_user, base_inventory):
result = run_module('tower_inventory_source', dict(
name='Test ec2 Inventory Source',
inventory=base_inventory.name,
source='ec2',
state='present',
source_path='where/am/I'
), admin_user)
assert result.pop('failed', None) is True, result
assert 'Cannot set source_path if not SCM type' in result.get('msg', '')
@pytest.mark.django_db
def test_scm_source_needs_project(run_module, admin_user, base_inventory):
result = run_module('tower_inventory_source', dict(
name='SCM inventory without project',
inventory=base_inventory.name,
state='present',
source='scm',
source_path='/var/lib/awx/example_source_path/'
), admin_user)
assert result.pop('failed', None), result
assert 'Project required for scm type sources' in result.get('msg', '')

View File

@@ -0,0 +1,35 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import sys
from unittest import mock
import json
def test_duplicate_config(collection_import):
# imports done here because of PATH issues unique to this test suite
TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule
data = {
'name': 'zigzoom',
'zig': 'zoom',
'tower_username': 'bob',
'tower_config_file': 'my_config'
}
cli_data = {'ANSIBLE_MODULE_ARGS': data}
testargs = ['module_file.py', json.dumps(cli_data)]
with mock.patch('ansible.module_utils.basic.AnsibleModule.warn') as mock_warn:
with mock.patch.object(sys, 'argv', testargs):
with mock.patch.object(TowerModule, 'load_config') as mock_load:
argument_spec = dict(
name=dict(required=True),
zig=dict(type='str'),
)
TowerModule(argument_spec=argument_spec)
mock_load.mock_calls[-1] == mock.call('my_config')
mock_warn.assert_called_once_with(
'The parameter(s) tower_username were provided at the same time as '
'tower_config_file. Precedence may be unstable, '
'we suggest either using config file or params.'
)

View File

@@ -9,18 +9,28 @@ from awx.main.models import Organization
@pytest.mark.django_db @pytest.mark.django_db
def test_create_organization(run_module, admin_user): def test_create_organization(run_module, admin_user):
module_args = {'name': 'foo', 'description': 'barfoo', 'state': 'present'} module_args = {
'name': 'foo',
'description': 'barfoo',
'state': 'present',
'max_hosts': '0',
'tower_host': None,
'tower_username': None,
'tower_password': None,
'validate_certs': None,
'tower_oauthtoken': None,
'tower_config_file': None,
'custom_virtualenv': None
}
result = run_module('tower_organization', module_args, admin_user) result = run_module('tower_organization', module_args, admin_user)
assert result.get('changed'), result assert result.get('changed'), result
org = Organization.objects.get(name='foo') org = Organization.objects.get(name='foo')
assert result == { assert result == {
"organization": "foo", "name": "foo",
"state": "present",
"id": org.id,
"changed": True, "changed": True,
"id": org.id,
"invocation": { "invocation": {
"module_args": module_args "module_args": module_args
} }
@@ -42,10 +52,8 @@ def test_create_organization_with_venv(run_module, admin_user, mocker):
org = Organization.objects.get(name='foo') org = Organization.objects.get(name='foo')
result.pop('invocation') result.pop('invocation')
assert result == { assert result == {
"organization": "foo", "name": "foo",
"state": "present",
"id": org.id "id": org.id
} }

View File

@@ -3,18 +3,23 @@ __metaclass__ = type
import pytest import pytest
from unittest import mock
from awx.main.models import Project from awx.main.models import Project
@pytest.mark.django_db @pytest.mark.django_db
def test_create_project(run_module, admin_user, organization): def test_create_project(run_module, admin_user, organization):
result = run_module('tower_project', dict( with mock.patch('ansible.module_utils.basic.AnsibleModule.warn') as mock_warn:
name='foo', result = run_module('tower_project', dict(
organization=organization.name, name='foo',
scm_type='git', organization=organization.name,
scm_url='https://foo.invalid', scm_type='git',
wait=False scm_url='https://foo.invalid',
), admin_user) wait=False,
scm_update_cache_timeout=5
), admin_user)
mock_warn.assert_called_once_with('scm_update_cache_timeout will be ignored since scm_update_on_launch was not set to true')
assert result.pop('changed', None), result assert result.pop('changed', None), result
proj = Project.objects.get(name='foo') proj = Project.objects.get(name='foo')
@@ -23,7 +28,6 @@ def test_create_project(run_module, admin_user, organization):
result.pop('invocation') result.pop('invocation')
assert result == { assert result == {
'id': proj.id, 'name': 'foo',
'project': 'foo', 'id': proj.id
'state': 'present'
} }

View File

@@ -2,6 +2,7 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import pytest import pytest
from unittest import mock
import json import json
from awx.main.models import ( from awx.main.models import (
@@ -65,7 +66,9 @@ def test_receive_send_jt(run_module, admin_user, mocker):
# recreate everything # recreate everything
with mocker.patch('sys.stdin.isatty', return_value=True): with mocker.patch('sys.stdin.isatty', return_value=True):
with mocker.patch('tower_cli.models.base.MonitorableResource.wait'): with mocker.patch('tower_cli.models.base.MonitorableResource.wait'):
result = run_module('tower_send', dict(assets=json.dumps(assets)), admin_user) # warns based on password_management param, but not security issue
with mock.patch('ansible.module_utils.basic.AnsibleModule.warn'):
result = run_module('tower_send', dict(assets=json.dumps(assets)), admin_user)
assert not result.get('failed'), result assert not result.get('failed'), result

View File

@@ -0,0 +1,66 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import pytest
from awx.main.models import Organization, Team
@pytest.mark.django_db
def test_create_team(run_module, admin_user):
org = Organization.objects.create(name='foo')
result = run_module('tower_team', {
'name': 'foo_team',
'description': 'fooin around',
'state': 'present',
'organization': 'foo'
}, admin_user)
team = Team.objects.filter(name='foo_team').first()
result.pop('invocation')
assert result == {
"changed": True,
"name": "foo_team",
"id": team.id if team else None,
}
team = Team.objects.get(name='foo_team')
assert team.description == 'fooin around'
assert team.organization_id == org.id
@pytest.mark.django_db
def test_modify_team(run_module, admin_user):
org = Organization.objects.create(name='foo')
team = Team.objects.create(
name='foo_team',
organization=org,
description='flat foo'
)
assert team.description == 'flat foo'
result = run_module('tower_team', {
'name': 'foo_team',
'description': 'fooin around',
'organization': 'foo'
}, admin_user)
team.refresh_from_db()
result.pop('invocation')
assert result == {
"changed": True,
"id": team.id,
}
assert team.description == 'fooin around'
# 2nd modification, should cause no change
result = run_module('tower_team', {
'name': 'foo_team',
'description': 'fooin around',
'organization': 'foo'
}, admin_user)
result.pop('invocation')
assert result == {
"id": team.id,
"changed": False
}

View File

@@ -1,2 +0,0 @@
plugins/modules/tower_group.py use-argspec-type-path
plugins/modules/tower_host.py use-argspec-type-path

View File

@@ -1,2 +0,0 @@
plugins/modules/tower_group.py use-argspec-type-path
plugins/modules/tower_host.py use-argspec-type-path