mirror of
https://github.com/ansible/awx.git
synced 2026-01-19 13:41:28 -03:30
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:
commit
154b9c36ac
@ -192,7 +192,7 @@ class APIView(views.APIView):
|
||||
response.data['detail'] += ' To establish a login session, visit /api/login/.'
|
||||
logger.info(status_msg)
|
||||
else:
|
||||
logger.warn(status_msg)
|
||||
logger.warning(status_msg)
|
||||
response = super(APIView, self).finalize_response(request, response, *args, **kwargs)
|
||||
time_started = getattr(self, 'time_started', None)
|
||||
response['X-API-Node'] = settings.CLUSTER_HOST_ID
|
||||
|
||||
@ -2115,7 +2115,13 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
||||
def get_field_from_model_or_attrs(fd):
|
||||
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(
|
||||
lambda x: attrs.get(x, None),
|
||||
['source_project', 'source_path', 'update_on_project_update']
|
||||
@ -3716,7 +3722,7 @@ class WorkflowJobNodeSerializer(LaunchConfigurationBaseSerializer):
|
||||
class Meta:
|
||||
model = WorkflowJobNode
|
||||
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',)
|
||||
|
||||
def get_related(self, obj):
|
||||
|
||||
@ -599,9 +599,9 @@ class TestControlledBySCM:
|
||||
delete(inv_src.get_absolute_url(), admin_user, expect=204)
|
||||
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}),
|
||||
{'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)
|
||||
|
||||
def test_adding_inv_src_prohibited(self, post, scm_inventory, project, admin_user):
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# AWX Ansible Collection
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
`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.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
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.
|
||||
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 install the requirement into. NOTE: running locally, you will also need
|
||||
to set the job template extra_vars to include `ansible_python_interpreter`
|
||||
to be the python in that 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 set the job template `extra_vars` to include `ansible_python_interpreter`
|
||||
to be the Python in that virtual environment.
|
||||
|
||||
## Running Tests
|
||||
|
||||
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
|
||||
itself.
|
||||
|
||||
@ -45,7 +48,7 @@ itself.
|
||||
|
||||
The target `make prepare_collection_venv` will prepare some requirements
|
||||
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
|
||||
@ -53,9 +56,9 @@ make test_collection COLLECTION_TEST_DIRS=awx_collection/test/awx/test_organizat
|
||||
|
||||
### Manually
|
||||
|
||||
As a faster alternative if you do not want to use the container, or
|
||||
run against Ansible or tower-cli source, it is possible to set up a
|
||||
working environment yourself.
|
||||
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
|
||||
working environment yourself:
|
||||
|
||||
```
|
||||
mkvirtualenv my_new_venv
|
||||
|
||||
@ -36,9 +36,6 @@ options:
|
||||
- Path to the Tower or AWX config file.
|
||||
type: path
|
||||
|
||||
requirements:
|
||||
- ansible-tower-cli >= 3.0.2
|
||||
|
||||
notes:
|
||||
- If no I(config_file) is provided we will attempt to use the tower-cli library
|
||||
defaults to find your Tower host information.
|
||||
|
||||
620
awx_collection/plugins/module_utils/tower_api.py
Normal file
620
awx_collection/plugins/module_utils/tower_api.py
Normal 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
|
||||
@ -160,6 +160,10 @@ options:
|
||||
choices: ["present", "absent"]
|
||||
default: "present"
|
||||
type: str
|
||||
|
||||
requirements:
|
||||
- ansible-tower-cli >= 3.0.2
|
||||
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
@ -278,7 +282,7 @@ def main():
|
||||
name=dict(required=True),
|
||||
user=dict(),
|
||||
team=dict(),
|
||||
kind=dict(choices=KIND_CHOICES.keys()),
|
||||
kind=dict(choices=list(KIND_CHOICES.keys())),
|
||||
credential_type=dict(),
|
||||
inputs=dict(type='dict'),
|
||||
host=dict(),
|
||||
|
||||
@ -64,12 +64,12 @@ options:
|
||||
default: "present"
|
||||
choices: ["present", "absent"]
|
||||
type: str
|
||||
validate_certs:
|
||||
tower_oauthtoken:
|
||||
description:
|
||||
- Tower option to avoid certificates check.
|
||||
- The Tower OAuth token to use.
|
||||
required: False
|
||||
type: bool
|
||||
aliases: [ tower_verify_ssl ]
|
||||
type: str
|
||||
version_added: "3.7"
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
@ -93,19 +93,7 @@ EXAMPLES = '''
|
||||
RETURN = ''' # '''
|
||||
|
||||
|
||||
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
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
|
||||
KIND_CHOICES = {
|
||||
'ssh': 'Machine',
|
||||
@ -118,62 +106,51 @@ KIND_CHOICES = {
|
||||
|
||||
|
||||
def main():
|
||||
# Any additional arguments that are not fields of the item can be added here
|
||||
argument_spec = dict(
|
||||
name=dict(required=True),
|
||||
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),
|
||||
injectors=dict(type='dict', required=False),
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
)
|
||||
|
||||
module = TowerModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=False
|
||||
)
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
new_name = None
|
||||
kind = module.params.get('kind')
|
||||
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)
|
||||
with settings.runtime_values(**tower_auth):
|
||||
tower_check_mode(module)
|
||||
credential_type_res = tower_cli.get_resource('credential_type')
|
||||
# Attempt to look up credential_type based on the provided name
|
||||
credential_type = module.get_one('credential_types', **{
|
||||
'data': {
|
||||
'name': name,
|
||||
}
|
||||
})
|
||||
|
||||
params = {}
|
||||
params['name'] = name
|
||||
params['kind'] = kind
|
||||
params['managed_by_tower'] = False
|
||||
|
||||
if module.params.get('description'):
|
||||
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 state == 'absent':
|
||||
# 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(credential_type)
|
||||
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
|
||||
module.create_or_update_if_needed(credential_type, credential_type_params, endpoint='credential_types', item_type='credential type')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -28,6 +28,12 @@ options:
|
||||
- The name to use for the group.
|
||||
required: True
|
||||
type: str
|
||||
new_name:
|
||||
description:
|
||||
- A new name for this group (for renaming)
|
||||
required: False
|
||||
type: str
|
||||
version_added: "3.7"
|
||||
description:
|
||||
description:
|
||||
- The description to use for the group.
|
||||
@ -39,57 +45,20 @@ options:
|
||||
type: str
|
||||
variables:
|
||||
description:
|
||||
- Variables to use for the group, use C(@) for a file.
|
||||
type: str
|
||||
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'
|
||||
- Variables to use for the group.
|
||||
type: dict
|
||||
state:
|
||||
description:
|
||||
- Desired state of the resource.
|
||||
default: "present"
|
||||
choices: ["present", "absent"]
|
||||
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
|
||||
'''
|
||||
|
||||
@ -104,86 +73,59 @@ EXAMPLES = '''
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
'''
|
||||
|
||||
import os
|
||||
|
||||
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
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
import json
|
||||
|
||||
|
||||
def main():
|
||||
# Any additional arguments that are not fields of the item can be added here
|
||||
argument_spec = dict(
|
||||
name=dict(required=True),
|
||||
description=dict(),
|
||||
new_name=dict(required=False),
|
||||
description=dict(required=False),
|
||||
inventory=dict(required=True),
|
||||
variables=dict(),
|
||||
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'),
|
||||
variables=dict(type='dict', required=False),
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
new_name = module.params.get('new_name')
|
||||
inventory = module.params.get('inventory')
|
||||
credential = module.params.get('credential')
|
||||
description = module.params.get('description')
|
||||
state = module.params.pop('state')
|
||||
|
||||
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)
|
||||
with settings.runtime_values(**tower_auth):
|
||||
tower_check_mode(module)
|
||||
group = tower_cli.get_resource('group')
|
||||
try:
|
||||
params = module.params.copy()
|
||||
params['create_on_missing'] = True
|
||||
params['variables'] = variables
|
||||
# Attempt to look up the object based on the provided name and inventory ID
|
||||
group = module.get_one('groups', **{
|
||||
'data': {
|
||||
'name': name,
|
||||
'inventory': inventory_id
|
||||
}
|
||||
})
|
||||
|
||||
inv_res = tower_cli.get_resource('inventory')
|
||||
inv = inv_res.get(name=inventory)
|
||||
params['inventory'] = inv['id']
|
||||
# Create the data that gets sent for create and update
|
||||
group_fields = {
|
||||
'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:
|
||||
cred_res = tower_cli.get_resource('credential')
|
||||
cred = cred_res.get(name=credential)
|
||||
params['credential'] = cred['id']
|
||||
|
||||
if state == 'present':
|
||||
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 state == 'absent':
|
||||
# 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(group)
|
||||
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
|
||||
module.create_or_update_if_needed(group, group_fields, endpoint='groups', item_type='group')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -28,6 +28,12 @@ options:
|
||||
- The name to use for the host.
|
||||
required: True
|
||||
type: str
|
||||
new_name:
|
||||
description:
|
||||
- To use when changing a hosts's name.
|
||||
required: False
|
||||
type: str
|
||||
version_added: "3.7"
|
||||
description:
|
||||
description:
|
||||
- The description to use for the host.
|
||||
@ -44,14 +50,20 @@ options:
|
||||
default: 'yes'
|
||||
variables:
|
||||
description:
|
||||
- Variables to use for the host. Use C(@) for a file.
|
||||
type: str
|
||||
- Variables to use for the host.
|
||||
type: dict
|
||||
state:
|
||||
description:
|
||||
- Desired state of the resource.
|
||||
choices: ["present", "absent"]
|
||||
default: "present"
|
||||
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
|
||||
'''
|
||||
|
||||
@ -69,67 +81,62 @@ EXAMPLES = '''
|
||||
'''
|
||||
|
||||
|
||||
import os
|
||||
|
||||
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
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
import json
|
||||
|
||||
|
||||
def main():
|
||||
# Any additional arguments that are not fields of the item can be added here
|
||||
argument_spec = dict(
|
||||
name=dict(required=True),
|
||||
description=dict(),
|
||||
new_name=dict(required=False),
|
||||
description=dict(required=False),
|
||||
inventory=dict(required=True),
|
||||
enabled=dict(type='bool', default=True),
|
||||
variables=dict(),
|
||||
variables=dict(type='dict', required=False),
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
new_name = module.params.get('new_name')
|
||||
description = module.params.get('description')
|
||||
inventory = module.params.get('inventory')
|
||||
enabled = module.params.get('enabled')
|
||||
state = module.params.get('state')
|
||||
|
||||
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)
|
||||
with settings.runtime_values(**tower_auth):
|
||||
tower_check_mode(module)
|
||||
host = tower_cli.get_resource('host')
|
||||
# Attempt to look up host based on the provided name and inventory ID
|
||||
host = module.get_one('hosts', **{
|
||||
'data': {
|
||||
'name': name,
|
||||
'inventory': inventory_id
|
||||
}
|
||||
})
|
||||
|
||||
try:
|
||||
inv_res = tower_cli.get_resource('inventory')
|
||||
inv = inv_res.get(name=inventory)
|
||||
# Create the data that gets sent for create and update
|
||||
host_fields = {
|
||||
'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':
|
||||
result = host.modify(name=name, inventory=inv['id'], enabled=enabled,
|
||||
variables=variables, description=description, create_on_missing=True)
|
||||
json_output['id'] = result['id']
|
||||
elif state == 'absent':
|
||||
result = host.delete(name=name, inventory=inv['id'])
|
||||
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 state == 'absent':
|
||||
# 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(host)
|
||||
elif state == 'present':
|
||||
# If the state was present and we can let the module build or update the existing host, this will return on its own
|
||||
module.create_or_update_if_needed(host, host_fields, endpoint='hosts', item_type='host')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -39,8 +39,9 @@ options:
|
||||
type: str
|
||||
variables:
|
||||
description:
|
||||
- Inventory variables. Use C(@) to get from file.
|
||||
type: str
|
||||
- Inventory variables.
|
||||
required: False
|
||||
type: dict
|
||||
kind:
|
||||
description:
|
||||
- The kind field. Cannot be modified after created.
|
||||
@ -59,6 +60,12 @@ options:
|
||||
default: "present"
|
||||
choices: ["present", "absent"]
|
||||
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
|
||||
'''
|
||||
|
||||
@ -74,30 +81,26 @@ EXAMPLES = '''
|
||||
'''
|
||||
|
||||
|
||||
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
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
import json
|
||||
|
||||
|
||||
def main():
|
||||
# Any additional arguments that are not fields of the item can be added here
|
||||
argument_spec = dict(
|
||||
name=dict(required=True),
|
||||
description=dict(),
|
||||
description=dict(required=False),
|
||||
organization=dict(required=True),
|
||||
variables=dict(),
|
||||
variables=dict(type='dict', required=False),
|
||||
kind=dict(choices=['', 'smart'], default=''),
|
||||
host_filter=dict(),
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
description = module.params.get('description')
|
||||
organization = module.params.get('organization')
|
||||
@ -106,31 +109,39 @@ def main():
|
||||
kind = module.params.get('kind')
|
||||
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)
|
||||
with settings.runtime_values(**tower_auth):
|
||||
tower_check_mode(module)
|
||||
inventory = tower_cli.get_resource('inventory')
|
||||
# Attempt to look up inventory based on the provided name and org ID
|
||||
inventory = module.get_one('inventories', **{
|
||||
'data': {
|
||||
'name': name,
|
||||
'organization': org_id
|
||||
}
|
||||
})
|
||||
|
||||
try:
|
||||
org_res = tower_cli.get_resource('organization')
|
||||
org = org_res.get(name=organization)
|
||||
# Create the data that gets sent for create and update
|
||||
inventory_fields = {
|
||||
'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':
|
||||
result = inventory.modify(name=name, organization=org['id'], variables=variables,
|
||||
description=description, kind=kind, host_filter=host_filter,
|
||||
create_on_missing=True)
|
||||
json_output['id'] = result['id']
|
||||
elif state == 'absent':
|
||||
result = inventory.delete(name=name, organization=org['id'])
|
||||
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)
|
||||
if state == 'absent':
|
||||
# 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(inventory)
|
||||
elif state == 'present':
|
||||
# We need to perform a check to make sure you are not trying to convert a regular inventory into a smart one.
|
||||
if inventory and inventory['kind'] == '' and inventory_fields['kind'] == 'smart':
|
||||
module.fail_json(msg='You cannot turn a regular inventory into a "smart" inventory.')
|
||||
|
||||
json_output['changed'] = result['changed']
|
||||
module.exit_json(**json_output)
|
||||
# If the state was present and we can let the module build or update the existing inventory, this will return on its own
|
||||
module.create_or_update_if_needed(inventory, inventory_fields, endpoint='inventories', item_type='inventory')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -8,9 +8,9 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'metadata_version': '1.1'}
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
@ -20,7 +20,7 @@ author: "Adrien Fleury (@fleu42)"
|
||||
version_added: "2.7"
|
||||
short_description: create, update, or destroy Ansible Tower inventory source.
|
||||
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.
|
||||
options:
|
||||
name:
|
||||
@ -28,325 +28,222 @@ options:
|
||||
- The name to use for the inventory source.
|
||||
required: True
|
||||
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:
|
||||
- The description to use for the inventory source.
|
||||
type: str
|
||||
inventory:
|
||||
description:
|
||||
- The inventory the source is linked to.
|
||||
- Inventory the group should be made a member of.
|
||||
required: True
|
||||
type: str
|
||||
organization:
|
||||
description:
|
||||
- Organization the inventory belongs to.
|
||||
type: str
|
||||
source:
|
||||
description:
|
||||
- Types of inventory source.
|
||||
choices:
|
||||
- file
|
||||
- scm
|
||||
- ec2
|
||||
- gce
|
||||
- azure
|
||||
- azure_rm
|
||||
- vmware
|
||||
- satellite6
|
||||
- cloudforms
|
||||
- openstack
|
||||
- rhv
|
||||
- tower
|
||||
- custom
|
||||
required: True
|
||||
- The source to use for this group.
|
||||
choices: [ "scm", "ec2", "gce", "azure_rm", "vmware", "satellite6", "cloudforms", "openstack", "rhv", "tower", "custom" ]
|
||||
type: str
|
||||
credential:
|
||||
required: False
|
||||
source_path:
|
||||
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
|
||||
source_script:
|
||||
description:
|
||||
- Inventory script to be used when group type is C(custom).
|
||||
type: str
|
||||
required: False
|
||||
source_vars:
|
||||
description:
|
||||
- >-
|
||||
The source_vars allow to Override variables found in the source config
|
||||
file. For example with Openstack, specifying *private: false* would
|
||||
change the output of the openstack.py script. It has to be YAML or
|
||||
JSON.
|
||||
- The variables or environment fields to apply to this source type.
|
||||
type: dict
|
||||
credential:
|
||||
description:
|
||||
- Credential to use for the source.
|
||||
type: str
|
||||
source_regions:
|
||||
description:
|
||||
- Regions for cloud provider.
|
||||
type: str
|
||||
instance_filters:
|
||||
description:
|
||||
- Comma-separated list of filter expressions for matching hosts.
|
||||
type: str
|
||||
group_by:
|
||||
description:
|
||||
- Limit groups automatically created from inventory source.
|
||||
type: str
|
||||
overwrite:
|
||||
description:
|
||||
- Delete child groups and hosts not found in source.
|
||||
type: bool
|
||||
default: 'no'
|
||||
overwrite_vars:
|
||||
description:
|
||||
- Override vars in child groups and hosts with those from external source.
|
||||
type: bool
|
||||
custom_virtualenv:
|
||||
version_added: "2.9"
|
||||
description:
|
||||
- Local absolute file path containing a custom Python virtualenv to use.
|
||||
type: str
|
||||
required: False
|
||||
default: ''
|
||||
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:
|
||||
- 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
|
||||
source_project:
|
||||
description:
|
||||
- Use a *project* as a source for the *inventory*.
|
||||
type: str
|
||||
source_path:
|
||||
description:
|
||||
- Path to the file to use as a source in the selected *project*.
|
||||
- Project to use as source with scm option
|
||||
type: str
|
||||
update_on_project_update:
|
||||
description:
|
||||
- >-
|
||||
That parameter will sync the inventory when the project is synced. It
|
||||
can only be used with a SCM source.
|
||||
description: Update this source when the related project updates if source is C(scm)
|
||||
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:
|
||||
description:
|
||||
- Desired state of the resource.
|
||||
default: "present"
|
||||
choices: ["present", "absent"]
|
||||
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
|
||||
'''
|
||||
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Add tower inventory source
|
||||
- name: Add an inventory source
|
||||
tower_inventory_source:
|
||||
name: Inventory source
|
||||
description: My Inventory source
|
||||
inventory: My inventory
|
||||
organization: My organization
|
||||
credential: Devstack_credential
|
||||
source: openstack
|
||||
update_on_launch: true
|
||||
overwrite: true
|
||||
source_vars: '{ private: false }'
|
||||
state: present
|
||||
validate_certs: false
|
||||
name: "source-inventory"
|
||||
description: Source for inventory
|
||||
inventory: previously-created-inventory
|
||||
credential: previously-created-credential
|
||||
overwrite: True
|
||||
update_on_launch: True
|
||||
source_vars:
|
||||
private: false
|
||||
'''
|
||||
|
||||
|
||||
RETURN = ''' # '''
|
||||
|
||||
|
||||
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',
|
||||
}
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from json import dumps
|
||||
|
||||
|
||||
def main():
|
||||
# Any additional arguments that are not fields of the item can be added here
|
||||
argument_spec = dict(
|
||||
name=dict(required=True),
|
||||
new_name=dict(type='str'),
|
||||
description=dict(required=False),
|
||||
inventory=dict(required=True),
|
||||
source=dict(required=True,
|
||||
choices=SOURCE_CHOICES.keys()),
|
||||
credential=dict(required=False),
|
||||
source_vars=dict(required=False),
|
||||
timeout=dict(type='int', required=False),
|
||||
source_project=dict(required=False),
|
||||
source_path=dict(required=False),
|
||||
update_on_project_update=dict(type='bool', required=False),
|
||||
source_regions=dict(required=False),
|
||||
instance_filters=dict(required=False),
|
||||
group_by=dict(required=False),
|
||||
#
|
||||
# How do we handle manual and file? Tower does not seem to be able to activate them
|
||||
#
|
||||
source=dict(choices=["scm", "ec2", "gce",
|
||||
"azure_rm", "vmware", "satellite6", "cloudforms",
|
||||
"openstack", "rhv", "tower", "custom"], required=False),
|
||||
source_path=dict(),
|
||||
source_script=dict(required=False),
|
||||
overwrite=dict(type='bool', required=False),
|
||||
overwrite_vars=dict(type='bool', required=False),
|
||||
custom_virtualenv=dict(type='str', required=False),
|
||||
update_on_launch=dict(type='bool', required=False),
|
||||
update_cache_timeout=dict(type='int', required=False),
|
||||
organization=dict(type='str'),
|
||||
source_vars=dict(type='dict'),
|
||||
credential=dict(),
|
||||
source_regions=dict(),
|
||||
instance_filters=dict(),
|
||||
group_by=dict(),
|
||||
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'),
|
||||
)
|
||||
|
||||
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')
|
||||
new_name = module.params.get('new_name')
|
||||
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')
|
||||
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)
|
||||
with settings.runtime_values(**tower_auth):
|
||||
tower_check_mode(module)
|
||||
inventory_source = tower_cli.get_resource('inventory_source')
|
||||
try:
|
||||
params = {}
|
||||
params['name'] = name
|
||||
params['source'] = source
|
||||
# Create the data that gets sent for create and update
|
||||
inventory_source_fields = {
|
||||
'name': new_name if new_name else name,
|
||||
'inventory': inventory_id,
|
||||
}
|
||||
|
||||
if module.params.get('description'):
|
||||
params['description'] = module.params.get('description')
|
||||
# Attempt to look up the related items the user specified (these will fail the module if not found)
|
||||
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:
|
||||
try:
|
||||
org_res = tower_cli.get_resource('organization')
|
||||
org = org_res.get(name=organization)
|
||||
except (exc.NotFound) as excinfo:
|
||||
module.fail_json(
|
||||
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
|
||||
OPTIONAL_VARS = (
|
||||
'description', 'source', 'source_path', 'source_vars',
|
||||
'source_regions', 'instance_filters', 'group_by',
|
||||
'overwrite', 'overwrite_vars', 'custom_virtualenv',
|
||||
'timeout', 'verbosity', 'update_on_launch', 'update_cache_timeout',
|
||||
'update_on_project_update'
|
||||
)
|
||||
|
||||
if module.params.get('credential'):
|
||||
credential_res = tower_cli.get_resource('credential')
|
||||
try:
|
||||
credential = credential_res.get(
|
||||
name=module.params.get('credential'), organization=org_id)
|
||||
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
|
||||
)
|
||||
# Layer in all remaining optional information
|
||||
for field_name in OPTIONAL_VARS:
|
||||
field_val = module.params.get(field_name)
|
||||
if field_val:
|
||||
inventory_source_fields[field_name] = field_val
|
||||
|
||||
if module.params.get('source_project'):
|
||||
source_project_res = tower_cli.get_resource('project')
|
||||
try:
|
||||
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
|
||||
)
|
||||
# Attempt to JSON encode source vars
|
||||
if inventory_source_fields.get('source_vars', None):
|
||||
inventory_source_fields['source_vars'] = dumps(inventory_source_fields['source_vars'])
|
||||
|
||||
if module.params.get('source_script'):
|
||||
source_script_res = tower_cli.get_resource('inventory_script')
|
||||
try:
|
||||
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
|
||||
)
|
||||
# Sanity check on arguments
|
||||
if state == 'present' and not inventory_source and not inventory_source_fields['source']:
|
||||
module.fail_json(msg="If creating a new inventory source, the source param must be present")
|
||||
|
||||
try:
|
||||
inventory_res = tower_cli.get_resource('inventory')
|
||||
params['inventory'] = inventory_res.get(name=inventory, organization=org_id)['id']
|
||||
except (exc.NotFound) as excinfo:
|
||||
module.fail_json(
|
||||
msg='Failed to update 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 state == 'absent':
|
||||
# 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(inventory_source)
|
||||
elif state == 'present':
|
||||
# If the state was present we can let the module build or update the existing inventory_source, this will return on its own
|
||||
module.create_or_update_if_needed(inventory_source, inventory_source_fields, endpoint='inventory_sources', item_type='inventory source')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -30,9 +30,15 @@ options:
|
||||
type: int
|
||||
fail_if_not_running:
|
||||
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
|
||||
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
|
||||
'''
|
||||
|
||||
@ -48,54 +54,55 @@ id:
|
||||
returned: success
|
||||
type: int
|
||||
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
|
||||
|
||||
try:
|
||||
import tower_cli
|
||||
import tower_cli.exceptions as exc
|
||||
|
||||
from tower_cli.conf import settings
|
||||
except ImportError:
|
||||
pass
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
|
||||
|
||||
def main():
|
||||
# Any additional arguments that are not fields of the item can be added here
|
||||
argument_spec = dict(
|
||||
job_id=dict(type='int', required=True),
|
||||
fail_if_not_running=dict(type='bool', default=False),
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
# Extract our parameters
|
||||
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)
|
||||
with settings.runtime_values(**tower_auth):
|
||||
tower_check_mode(module)
|
||||
job = tower_cli.get_resource('job')
|
||||
params = module.params.copy()
|
||||
# Attempt to look up the job based on the provided name
|
||||
job = module.get_one('jobs', **{
|
||||
'data': {
|
||||
'id': job_id,
|
||||
}
|
||||
})
|
||||
|
||||
try:
|
||||
result = job.cancel(job_id, **params)
|
||||
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)
|
||||
if job is None:
|
||||
module.fail_json(msg="Unable to find job with id {0}".format(job_id))
|
||||
|
||||
json_output['changed'] = result['changed']
|
||||
json_output['status'] = result['status']
|
||||
module.exit_json(**json_output)
|
||||
cancel_page = module.get_endpoint(job['related']['cancel'])
|
||||
if 'json' not in cancel_page or 'can_cancel' not in cancel_page['json']:
|
||||
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__':
|
||||
|
||||
@ -23,11 +23,12 @@ description:
|
||||
- Launch an Ansible Tower jobs. See
|
||||
U(https://www.ansible.com/tower) for an overview.
|
||||
options:
|
||||
job_template:
|
||||
name:
|
||||
description:
|
||||
- Name of the job template to use.
|
||||
required: True
|
||||
type: str
|
||||
aliases: ['job_template']
|
||||
job_type:
|
||||
description:
|
||||
- Job_type to use for the job, only used if prompt for job_type is set.
|
||||
@ -37,13 +38,15 @@ options:
|
||||
description:
|
||||
- Inventory to use for the job, only used if prompt for inventory is set.
|
||||
type: str
|
||||
credential:
|
||||
credentials:
|
||||
description:
|
||||
- Credential to use for job, only used if prompt for credential is set.
|
||||
type: str
|
||||
type: list
|
||||
aliases: ['credential']
|
||||
elements: str
|
||||
extra_vars:
|
||||
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
|
||||
when creating the Job Template.
|
||||
type: dict
|
||||
@ -55,21 +58,50 @@ options:
|
||||
description:
|
||||
- Specific tags to use for from playbook.
|
||||
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
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Launch a job template
|
||||
- name: Launch a job
|
||||
tower_job_launch:
|
||||
job_template: "My Job Template"
|
||||
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
|
||||
tower_job_launch:
|
||||
job_template: "My Job Template"
|
||||
@ -79,7 +111,6 @@ EXAMPLES = '''
|
||||
var3: "My Third Variable"
|
||||
job_type: run
|
||||
|
||||
# Launch job template with inventory and credential for prompt on launch
|
||||
- name: Launch a job with inventory and credential
|
||||
tower_job_launch:
|
||||
job_template: "My Job Template"
|
||||
@ -105,91 +136,103 @@ status:
|
||||
sample: pending
|
||||
'''
|
||||
|
||||
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 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
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
|
||||
|
||||
def main():
|
||||
# Any additional arguments that are not fields of the item can be added here
|
||||
argument_spec = dict(
|
||||
job_template=dict(required=True, type='str'),
|
||||
job_type=dict(choices=['run', 'check']),
|
||||
name=dict(type='str', required=True, aliases=['job_template']),
|
||||
job_type=dict(type='str', choices=['run', 'check']),
|
||||
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(),
|
||||
tags=dict(type='list'),
|
||||
tags=dict(type='list', elements='str'),
|
||||
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(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
|
||||
|
||||
json_output = {}
|
||||
tags = module.params.get('tags')
|
||||
optional_args = {}
|
||||
# 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)
|
||||
with settings.runtime_values(**tower_auth):
|
||||
tower_check_mode(module)
|
||||
try:
|
||||
params = module.params.copy()
|
||||
if isinstance(tags, list):
|
||||
params['tags'] = ','.join(tags)
|
||||
job = tower_cli.get_resource('job')
|
||||
# Create a datastructure to pass into our job launch
|
||||
post_data = {}
|
||||
for key in optional_args.keys():
|
||||
if optional_args[key]:
|
||||
post_data[key] = optional_args[key]
|
||||
|
||||
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')
|
||||
for field in lookup_fields:
|
||||
try:
|
||||
name = params.pop(field)
|
||||
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)
|
||||
if credentials:
|
||||
post_data['credentials'] = []
|
||||
for credential in credentials:
|
||||
post_data['credentials'].append(module.resolve_name_to_id('credentials', credential))
|
||||
|
||||
result = job.launch(no_input=True, **params)
|
||||
json_output['id'] = result['id']
|
||||
json_output['status'] = result['status']
|
||||
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo:
|
||||
module.fail_json(msg='Unable to launch job: {0}'.format(excinfo), changed=False)
|
||||
# Attempt to look up job_template based on the provided name
|
||||
job_template = module.get_one('job_templates', **{
|
||||
'data': {
|
||||
'name': name,
|
||||
}
|
||||
})
|
||||
|
||||
json_output['changed'] = result['changed']
|
||||
module.exit_json(**json_output)
|
||||
if job_template is None:
|
||||
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__':
|
||||
|
||||
@ -41,6 +41,12 @@ options:
|
||||
description:
|
||||
- Query used to further filter the list of jobs. C({"foo":"bar"}) will be passed at C(?foo=bar)
|
||||
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
|
||||
'''
|
||||
|
||||
@ -81,18 +87,11 @@ results:
|
||||
'''
|
||||
|
||||
|
||||
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
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
|
||||
|
||||
def main():
|
||||
# Any additional arguments that are not fields of the item can be added here
|
||||
argument_spec = dict(
|
||||
status=dict(choices=['pending', 'waiting', 'running', 'error', 'failed', 'canceled', 'successful']),
|
||||
page=dict(type='int'),
|
||||
@ -100,31 +99,35 @@ def main():
|
||||
query=dict(type='dict'),
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(
|
||||
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')
|
||||
status = module.params.get('status')
|
||||
page = module.params.get('page')
|
||||
all_pages = module.params.get('all_pages')
|
||||
|
||||
tower_auth = tower_auth_config(module)
|
||||
with settings.runtime_values(**tower_auth):
|
||||
tower_check_mode(module)
|
||||
try:
|
||||
job = tower_cli.get_resource('job')
|
||||
params = {'status': status, 'page': page, 'all_pages': all_pages}
|
||||
if query:
|
||||
params['query'] = query.items()
|
||||
json_output = job.list(**params)
|
||||
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo:
|
||||
module.fail_json(msg='Failed to list jobs: {0}'.format(excinfo), changed=False)
|
||||
job_search_data = {}
|
||||
if page:
|
||||
job_search_data['page'] = page
|
||||
if status:
|
||||
job_search_data['status'] = status
|
||||
if query:
|
||||
job_search_data.update(query)
|
||||
if all_pages:
|
||||
job_list = module.get_all_endpoint('jobs', **{'data': job_search_data})
|
||||
else:
|
||||
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__':
|
||||
|
||||
@ -65,6 +65,7 @@ options:
|
||||
version_added: 2.8
|
||||
type: list
|
||||
default: []
|
||||
elements: str
|
||||
vault_credential:
|
||||
description:
|
||||
- Name of the vault credential to use for the job template.
|
||||
@ -219,7 +220,12 @@ options:
|
||||
default: "present"
|
||||
choices: ["present", "absent"]
|
||||
type: str
|
||||
|
||||
requirements:
|
||||
- ansible-tower-cli >= 3.0.2
|
||||
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
|
||||
notes:
|
||||
- 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)
|
||||
@ -333,7 +339,7 @@ def main():
|
||||
credential=dict(default=''),
|
||||
vault_credential=dict(default=''),
|
||||
custom_virtualenv=dict(type='str', required=False),
|
||||
credentials=dict(type='list', default=[]),
|
||||
credentials=dict(type='list', default=[], elements='str'),
|
||||
forks=dict(type='int'),
|
||||
limit=dict(default=''),
|
||||
verbosity=dict(type='int', choices=[0, 1, 2, 3, 4], default=0),
|
||||
|
||||
@ -42,6 +42,10 @@ options:
|
||||
description:
|
||||
- Maximum time in seconds to wait for a job to finish.
|
||||
type: int
|
||||
|
||||
requirements:
|
||||
- ansible-tower-cli >= 3.0.2
|
||||
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
|
||||
@ -39,6 +39,10 @@ options:
|
||||
default: "present"
|
||||
choices: ["present", "absent"]
|
||||
type: str
|
||||
|
||||
requirements:
|
||||
- ansible-tower-cli >= 3.0.2
|
||||
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
|
||||
91
awx_collection/plugins/modules/tower_license.py
Normal file
91
awx_collection/plugins/modules/tower_license.py
Normal 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()
|
||||
@ -64,6 +64,7 @@ options:
|
||||
- The recipients email addresses. Required if I(notification_type=email).
|
||||
required: False
|
||||
type: list
|
||||
elements: str
|
||||
use_tls:
|
||||
description:
|
||||
- The TLS trigger. Required if I(notification_type=email).
|
||||
@ -94,6 +95,7 @@ options:
|
||||
- The destination Slack channels. Required if I(notification_type=slack).
|
||||
required: False
|
||||
type: list
|
||||
elements: str
|
||||
token:
|
||||
description:
|
||||
- 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).
|
||||
required: False
|
||||
type: list
|
||||
elements: str
|
||||
account_sid:
|
||||
description:
|
||||
- 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).
|
||||
required: False
|
||||
type: list
|
||||
elements: str
|
||||
notify:
|
||||
description:
|
||||
- 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).
|
||||
required: False
|
||||
type: list
|
||||
elements: str
|
||||
state:
|
||||
description:
|
||||
- Desired state of the resource.
|
||||
default: "present"
|
||||
choices: ["present", "absent"]
|
||||
type: str
|
||||
|
||||
requirements:
|
||||
- ansible-tower-cli >= 3.0.2
|
||||
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
@ -315,17 +324,17 @@ def main():
|
||||
notification_configuration=dict(required=False),
|
||||
username=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'),
|
||||
host=dict(required=False),
|
||||
use_ssl=dict(required=False, type='bool'),
|
||||
password=dict(required=False, no_log=True),
|
||||
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),
|
||||
account_token=dict(required=False, no_log=True),
|
||||
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),
|
||||
subdomain=dict(required=False),
|
||||
service_key=dict(required=False, no_log=True),
|
||||
@ -333,13 +342,13 @@ def main():
|
||||
message_from=dict(required=False),
|
||||
api_url=dict(required=False),
|
||||
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'),
|
||||
url=dict(required=False),
|
||||
headers=dict(required=False, type='dict', default={}),
|
||||
server=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'),
|
||||
)
|
||||
|
||||
|
||||
@ -39,12 +39,25 @@ options:
|
||||
type: str
|
||||
required: False
|
||||
default: ''
|
||||
max_hosts:
|
||||
description:
|
||||
- The max hosts allowed in this organizations
|
||||
default: "0"
|
||||
type: int
|
||||
required: False
|
||||
version_added: "3.7"
|
||||
state:
|
||||
description:
|
||||
- Desired state of the resource.
|
||||
default: "present"
|
||||
choices: ["present", "absent"]
|
||||
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
|
||||
'''
|
||||
|
||||
@ -66,49 +79,52 @@ EXAMPLES = '''
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
'''
|
||||
|
||||
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
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
|
||||
|
||||
def main():
|
||||
# Any additional arguments that are not fields of the item can be added here
|
||||
argument_spec = dict(
|
||||
name=dict(required=True),
|
||||
description=dict(),
|
||||
name=dict(type='str', required=True),
|
||||
description=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)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
description = module.params.get('description')
|
||||
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')
|
||||
|
||||
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)
|
||||
with settings.runtime_values(**tower_auth):
|
||||
tower_check_mode(module)
|
||||
organization = tower_cli.get_resource('organization')
|
||||
try:
|
||||
if state == 'present':
|
||||
result = organization.modify(name=name, description=description, custom_virtualenv=custom_virtualenv, create_on_missing=True)
|
||||
json_output['id'] = result['id']
|
||||
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)
|
||||
# Create the data that gets sent for create and update
|
||||
org_fields = {'name': name}
|
||||
if description is not None:
|
||||
org_fields['description'] = description
|
||||
if custom_virtualenv is not None:
|
||||
org_fields['custom_virtualenv'] = custom_virtualenv
|
||||
if max_hosts is not None:
|
||||
org_fields['max_hosts'] = max_hosts
|
||||
|
||||
json_output['changed'] = result['changed']
|
||||
module.exit_json(**json_output)
|
||||
if state == 'absent':
|
||||
# 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__':
|
||||
|
||||
@ -35,7 +35,7 @@ options:
|
||||
scm_type:
|
||||
description:
|
||||
- Type of SCM resource.
|
||||
choices: ["manual", "git", "hg", "svn"]
|
||||
choices: ["manual", "git", "hg", "svn", "insights"]
|
||||
default: "manual"
|
||||
type: str
|
||||
scm_url:
|
||||
@ -50,6 +50,13 @@ options:
|
||||
description:
|
||||
- The branch to use for the SCM resource.
|
||||
type: str
|
||||
default: ''
|
||||
scm_refspec:
|
||||
description:
|
||||
- The refspec to use for the SCM resource.
|
||||
type: str
|
||||
default: ''
|
||||
version_added: "3.7"
|
||||
scm_credential:
|
||||
description:
|
||||
- Name of the credential to use with this SCM resource.
|
||||
@ -74,8 +81,13 @@ options:
|
||||
description:
|
||||
- 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.
|
||||
default: 0
|
||||
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:
|
||||
version_added: "2.8"
|
||||
description:
|
||||
@ -91,8 +103,9 @@ options:
|
||||
default: ''
|
||||
organization:
|
||||
description:
|
||||
- Primary key of organization for project.
|
||||
- Name of organization for project.
|
||||
type: str
|
||||
required: True
|
||||
state:
|
||||
description:
|
||||
- Desired state of the resource.
|
||||
@ -105,9 +118,14 @@ options:
|
||||
before returning
|
||||
- Can assure playbook files are populated so that job templates that rely
|
||||
on the project may be successfully created
|
||||
|
||||
type: bool
|
||||
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
|
||||
'''
|
||||
|
||||
@ -133,107 +151,129 @@ EXAMPLES = '''
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
'''
|
||||
|
||||
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode
|
||||
import time
|
||||
|
||||
try:
|
||||
import tower_cli
|
||||
import tower_cli.exceptions as exc
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
|
||||
from tower_cli.conf import settings
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def wait_for_project_update(module, last_request):
|
||||
# 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():
|
||||
# Any additional arguments that are not fields of the item can be added here
|
||||
argument_spec = dict(
|
||||
name=dict(required=True),
|
||||
description=dict(),
|
||||
organization=dict(),
|
||||
scm_type=dict(choices=['manual', 'git', 'hg', 'svn'], default='manual'),
|
||||
scm_url=dict(),
|
||||
scm_branch=dict(),
|
||||
scm_credential=dict(),
|
||||
scm_clean=dict(type='bool', default=False),
|
||||
scm_delete_on_update=dict(type='bool', default=False),
|
||||
scm_update_on_launch=dict(type='bool', default=False),
|
||||
scm_update_cache_timeout=dict(type='int'),
|
||||
job_timeout=dict(type='int', default=0),
|
||||
custom_virtualenv=dict(type='str', required=False),
|
||||
local_path=dict(),
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
wait=dict(type='bool', default=True),
|
||||
description=dict(required=False),
|
||||
scm_type=dict(required=False, choices=['manual', 'git', 'hg', 'svn', 'insights'], default='manual'),
|
||||
scm_url=dict(required=False),
|
||||
local_path=dict(required=False),
|
||||
scm_branch=dict(required=False, default=''),
|
||||
scm_refspec=dict(required=False, default=''),
|
||||
scm_credential=dict(required=False),
|
||||
scm_clean=dict(required=False, type='bool', default=False),
|
||||
scm_delete_on_update=dict(required=False, type='bool', default=False),
|
||||
scm_update_on_launch=dict(required=False, type='bool', default=False),
|
||||
scm_update_cache_timeout=dict(required=False, type='int', default=0),
|
||||
scm_allow_override=dict(required=False, type='bool'),
|
||||
job_timeout=dict(required=False, type='int', default=0),
|
||||
custom_virtualenv=dict(required=False, type='str'),
|
||||
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)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
description = module.params.get('description')
|
||||
organization = module.params.get('organization')
|
||||
scm_type = module.params.get('scm_type')
|
||||
if scm_type == "manual":
|
||||
scm_type = ""
|
||||
scm_url = module.params.get('scm_url')
|
||||
local_path = module.params.get('local_path')
|
||||
scm_branch = module.params.get('scm_branch')
|
||||
scm_refspec = module.params.get('scm_refspec')
|
||||
scm_credential = module.params.get('scm_credential')
|
||||
scm_clean = module.params.get('scm_clean')
|
||||
scm_delete_on_update = module.params.get('scm_delete_on_update')
|
||||
scm_update_on_launch = module.params.get('scm_update_on_launch')
|
||||
scm_update_cache_timeout = module.params.get('scm_update_cache_timeout')
|
||||
scm_allow_override = module.params.get('scm_allow_override')
|
||||
job_timeout = module.params.get('job_timeout')
|
||||
custom_virtualenv = module.params.get('custom_virtualenv')
|
||||
organization = module.params.get('organization')
|
||||
state = module.params.get('state')
|
||||
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)
|
||||
with settings.runtime_values(**tower_auth):
|
||||
tower_check_mode(module)
|
||||
project = tower_cli.get_resource('project')
|
||||
try:
|
||||
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)
|
||||
# Attempt to look up project based on the provided name and org ID
|
||||
project = module.get_one('projects', **{
|
||||
'data': {
|
||||
'name': name,
|
||||
'organization': org_id
|
||||
}
|
||||
})
|
||||
|
||||
if scm_credential:
|
||||
try:
|
||||
cred_res = tower_cli.get_resource('credential')
|
||||
try:
|
||||
cred = cred_res.get(name=scm_credential)
|
||||
except tower_cli.exceptions.MultipleResults:
|
||||
module.warn('Multiple credentials found for {0}, falling back looking in project organization'.format(scm_credential))
|
||||
cred = cred_res.get(name=scm_credential, organization=org['id'])
|
||||
scm_credential = cred['id']
|
||||
except exc.NotFound:
|
||||
module.fail_json(msg='Failed to update project, credential not found: {0}'.format(scm_credential), changed=False)
|
||||
# Create the data that gets sent for create and update
|
||||
project_fields = {
|
||||
'name': name,
|
||||
'scm_type': scm_type,
|
||||
'scm_url': scm_url,
|
||||
'scm_branch': scm_branch,
|
||||
'scm_refspec': scm_refspec,
|
||||
'scm_clean': scm_clean,
|
||||
'scm_delete_on_update': scm_delete_on_update,
|
||||
'timeout': job_timeout,
|
||||
'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):
|
||||
module.warn('scm_update_cache_timeout will be ignored since scm_update_on_launch was not set to 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')
|
||||
|
||||
result = project.modify(name=name, description=description,
|
||||
organization=org['id'],
|
||||
scm_type=scm_type, scm_url=scm_url, local_path=local_path,
|
||||
scm_branch=scm_branch, scm_clean=scm_clean, credential=scm_credential,
|
||||
scm_delete_on_update=scm_delete_on_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)
|
||||
# If we are doing a not manual project, register our on_change method
|
||||
# An on_change function, if registered, will fire after an post_endpoint or update_if_needed completes successfully
|
||||
on_change = None
|
||||
if wait and scm_type != '':
|
||||
on_change = wait_for_project_update
|
||||
|
||||
json_output['changed'] = result['changed']
|
||||
module.exit_json(**json_output)
|
||||
if state == 'absent':
|
||||
# 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__':
|
||||
|
||||
@ -33,56 +33,67 @@ options:
|
||||
- List of organization names to export
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
user:
|
||||
description:
|
||||
- List of user names to export
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
team:
|
||||
description:
|
||||
- List of team names to export
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
credential_type:
|
||||
description:
|
||||
- List of credential type names to export
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
credential:
|
||||
description:
|
||||
- List of credential names to export
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
notification_template:
|
||||
description:
|
||||
- List of notification template names to export
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
inventory_script:
|
||||
description:
|
||||
- List of inventory script names to export
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
inventory:
|
||||
description:
|
||||
- List of inventory names to export
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
project:
|
||||
description:
|
||||
- List of project names to export
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
job_template:
|
||||
description:
|
||||
- List of job template names to export
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
workflow:
|
||||
description:
|
||||
- List of workflow names to export
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
|
||||
requirements:
|
||||
- "ansible-tower-cli >= 3.3.0"
|
||||
@ -136,21 +147,23 @@ except ImportError:
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
all=dict(type='bool', default=False),
|
||||
credential=dict(type='list', default=[]),
|
||||
credential_type=dict(type='list', default=[]),
|
||||
inventory=dict(type='list', default=[]),
|
||||
inventory_script=dict(type='list', default=[]),
|
||||
job_template=dict(type='list', default=[]),
|
||||
notification_template=dict(type='list', default=[]),
|
||||
organization=dict(type='list', default=[]),
|
||||
project=dict(type='list', default=[]),
|
||||
team=dict(type='list', default=[]),
|
||||
user=dict(type='list', default=[]),
|
||||
workflow=dict(type='list', default=[]),
|
||||
credential=dict(type='list', default=[], elements='str'),
|
||||
credential_type=dict(type='list', default=[], elements='str'),
|
||||
inventory=dict(type='list', default=[], elements='str'),
|
||||
inventory_script=dict(type='list', default=[], elements='str'),
|
||||
job_template=dict(type='list', default=[], elements='str'),
|
||||
notification_template=dict(type='list', default=[], elements='str'),
|
||||
organization=dict(type='list', default=[], elements='str'),
|
||||
project=dict(type='list', default=[], elements='str'),
|
||||
team=dict(type='list', default=[], elements='str'),
|
||||
user=dict(type='list', default=[], elements='str'),
|
||||
workflow=dict(type='list', default=[], elements='str'),
|
||||
)
|
||||
|
||||
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:
|
||||
module.fail_json(msg='ansible-tower-cli required for this module')
|
||||
|
||||
|
||||
@ -68,6 +68,10 @@ options:
|
||||
default: "present"
|
||||
choices: ["present", "absent"]
|
||||
type: str
|
||||
|
||||
requirements:
|
||||
- ansible-tower-cli >= 3.0.2
|
||||
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
@ -137,6 +141,8 @@ def main():
|
||||
|
||||
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')
|
||||
state = module.params.pop('state')
|
||||
|
||||
|
||||
@ -35,12 +35,14 @@ options:
|
||||
required: False
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
prevent:
|
||||
description:
|
||||
- A list of asset types to prevent import for
|
||||
required: false
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
password_management:
|
||||
description:
|
||||
- The password management option to use.
|
||||
@ -97,13 +99,15 @@ except ImportError:
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
assets=dict(required=False),
|
||||
files=dict(required=False, default=[], type='list'),
|
||||
prevent=dict(required=False, default=[], type='list'),
|
||||
files=dict(required=False, default=[], type='list', elements='str'),
|
||||
prevent=dict(required=False, default=[], type='list', elements='str'),
|
||||
password_management=dict(required=False, default='default', choices=['default', 'random']),
|
||||
)
|
||||
|
||||
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:
|
||||
module.fail_json(msg='ansible-tower-cli required for this module')
|
||||
|
||||
|
||||
@ -26,18 +26,28 @@ options:
|
||||
name:
|
||||
description:
|
||||
- Name of setting to modify
|
||||
required: True
|
||||
required: False
|
||||
type: str
|
||||
value:
|
||||
description:
|
||||
- Value to be modified for given setting.
|
||||
required: True
|
||||
required: False
|
||||
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
|
||||
'''
|
||||
|
||||
RETURN = ''' # '''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Set the value of AWX_PROOT_BASE_PATH
|
||||
tower_settings:
|
||||
@ -56,50 +66,98 @@ EXAMPLES = '''
|
||||
name: "AUTH_LDAP_BIND_PASSWORD"
|
||||
value: "Password"
|
||||
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
|
||||
|
||||
try:
|
||||
import tower_cli
|
||||
import tower_cli.exceptions as exc
|
||||
|
||||
from tower_cli.conf import settings
|
||||
except ImportError:
|
||||
pass
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from json import loads
|
||||
import re
|
||||
|
||||
|
||||
def main():
|
||||
# Any additional arguments that are not fields of the item can be added here
|
||||
argument_spec = dict(
|
||||
name=dict(required=True),
|
||||
value=dict(required=True),
|
||||
name=dict(required=False),
|
||||
value=dict(required=False),
|
||||
settings=dict(required=False, type='dict'),
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(
|
||||
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')
|
||||
value = module.params.get('value')
|
||||
new_settings = module.params.get('settings')
|
||||
|
||||
tower_auth = tower_auth_config(module)
|
||||
with settings.runtime_values(**tower_auth):
|
||||
tower_check_mode(module)
|
||||
# If we were given a name/value pair we will just make settings out of that and proceed normally
|
||||
if new_settings is None:
|
||||
new_value = value
|
||||
try:
|
||||
setting = tower_cli.get_resource('setting')
|
||||
result = setting.modify(setting=name, value=value)
|
||||
new_value = loads(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']
|
||||
json_output['value'] = result['value']
|
||||
new_settings = {name: new_value}
|
||||
|
||||
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo:
|
||||
module.fail_json(msg='Failed to modify the setting: {0}'.format(excinfo), changed=False)
|
||||
# Load the existing settings
|
||||
existing_settings = module.get_endpoint('settings/all')['json']
|
||||
|
||||
json_output['changed'] = result['changed']
|
||||
module.exit_json(**json_output)
|
||||
# Begin a json response
|
||||
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__':
|
||||
|
||||
@ -28,9 +28,16 @@ options:
|
||||
- Name to use for the team.
|
||||
required: True
|
||||
type: str
|
||||
new_name:
|
||||
description:
|
||||
- To use when changing a team's name.
|
||||
required: False
|
||||
type: str
|
||||
version_added: "3.7"
|
||||
description:
|
||||
description:
|
||||
- The description to use for the team.
|
||||
required: False
|
||||
type: str
|
||||
organization:
|
||||
description:
|
||||
@ -43,6 +50,12 @@ options:
|
||||
choices: ["present", "absent"]
|
||||
default: "present"
|
||||
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
|
||||
'''
|
||||
|
||||
@ -57,57 +70,54 @@ EXAMPLES = '''
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
'''
|
||||
|
||||
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
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
# Any additional arguments that are not fields of the item can be added here
|
||||
argument_spec = dict(
|
||||
name=dict(required=True),
|
||||
description=dict(),
|
||||
new_name=dict(required=False),
|
||||
description=dict(required=False),
|
||||
organization=dict(required=True),
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
new_name = module.params.get('new_name')
|
||||
description = module.params.get('description')
|
||||
organization = module.params.get('organization')
|
||||
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)
|
||||
with settings.runtime_values(**tower_auth):
|
||||
tower_check_mode(module)
|
||||
team = tower_cli.get_resource('team')
|
||||
# Attempt to look up team based on the provided name and org ID
|
||||
team = module.get_one('teams', **{
|
||||
'data': {
|
||||
'name': name,
|
||||
'organization': org_id
|
||||
}
|
||||
})
|
||||
|
||||
try:
|
||||
org_res = tower_cli.get_resource('organization')
|
||||
org = org_res.get(name=organization)
|
||||
# Create the data that gets sent for create and update
|
||||
team_fields = {
|
||||
'name': new_name if new_name else name,
|
||||
'organization': org_id
|
||||
}
|
||||
if description is not None:
|
||||
team_fields['description'] = description
|
||||
|
||||
if state == 'present':
|
||||
result = team.modify(name=name, organization=org['id'],
|
||||
description=description, create_on_missing=True)
|
||||
json_output['id'] = result['id']
|
||||
elif state == 'absent':
|
||||
result = team.delete(name=name, organization=org['id'])
|
||||
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 state == 'absent':
|
||||
# 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(team)
|
||||
elif state == 'present':
|
||||
# If the state was present and we can let the module build or update the existing team, this will return on its own
|
||||
module.create_or_update_if_needed(team, team_fields, endpoint='teams', item_type='team')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -38,8 +38,8 @@ options:
|
||||
type: str
|
||||
email:
|
||||
description:
|
||||
- Email address of the user.
|
||||
required: True
|
||||
- Email address of the user. Required if creating a new user.
|
||||
required: False
|
||||
type: str
|
||||
password:
|
||||
description:
|
||||
@ -61,10 +61,12 @@ options:
|
||||
default: "present"
|
||||
choices: ["present", "absent"]
|
||||
type: str
|
||||
|
||||
requirements:
|
||||
- ansible-tower-cli >= 3.2.0
|
||||
|
||||
tower_oauthtoken:
|
||||
description:
|
||||
- The Tower OAuth token to use.
|
||||
required: False
|
||||
type: str
|
||||
version_added: "3.7"
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
@ -106,59 +108,54 @@ EXAMPLES = '''
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
'''
|
||||
|
||||
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
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
|
||||
|
||||
def main():
|
||||
# Any additional arguments that are not fields of the item can be added here
|
||||
argument_spec = dict(
|
||||
username=dict(required=True),
|
||||
first_name=dict(),
|
||||
last_name=dict(),
|
||||
password=dict(no_log=True),
|
||||
email=dict(required=True),
|
||||
email=dict(required=False, default=''),
|
||||
superuser=dict(type='bool', default=False),
|
||||
auditor=dict(type='bool', default=False),
|
||||
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')
|
||||
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')
|
||||
# Extract our parameters
|
||||
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)
|
||||
with settings.runtime_values(**tower_auth):
|
||||
tower_check_mode(module)
|
||||
user = tower_cli.get_resource('user')
|
||||
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)
|
||||
# Attempt to look up user based on the provided username
|
||||
user = module.get_one('users', **{
|
||||
'data': {
|
||||
'username': user_fields['username'],
|
||||
}
|
||||
})
|
||||
|
||||
json_output['changed'] = result['changed']
|
||||
module.exit_json(**json_output)
|
||||
if state == 'absent':
|
||||
# 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__':
|
||||
|
||||
@ -44,6 +44,8 @@ options:
|
||||
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- ansible-tower-cli >= 3.0.2
|
||||
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
|
||||
@ -81,6 +81,10 @@ options:
|
||||
default: "present"
|
||||
choices: ["present", "absent"]
|
||||
type: str
|
||||
|
||||
requirements:
|
||||
- ansible-tower-cli >= 3.0.2
|
||||
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import io
|
||||
import json
|
||||
import datetime
|
||||
import importlib
|
||||
from contextlib import redirect_stdout
|
||||
from contextlib import redirect_stdout, suppress
|
||||
from unittest import mock
|
||||
import logging
|
||||
|
||||
@ -16,6 +16,12 @@ import pytest
|
||||
from awx.main.tests.functional.conftest import _request
|
||||
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')
|
||||
|
||||
@ -41,13 +47,28 @@ def sanitize_dict(din):
|
||||
|
||||
|
||||
@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 new_request(self, method, url, **kwargs):
|
||||
kwargs_copy = kwargs.copy()
|
||||
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':
|
||||
# query params for GET are handled a bit differently by
|
||||
# tower-cli and python requests as opposed to REST framework APIRequestFactory
|
||||
@ -79,12 +100,16 @@ def run_module(request):
|
||||
|
||||
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()
|
||||
# Requies specific PYTHONPATH, see docs
|
||||
# Note that a proper Ansiballz explosion of the modules will have an import path like:
|
||||
# ansible_collections.awx.awx.plugins.modules.{}
|
||||
# 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):
|
||||
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):
|
||||
# Call the test utility (like a mock server) instead of issuing HTTP requests
|
||||
with mock.patch('tower_cli.api.Session.request', new=new_request):
|
||||
# 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
|
||||
with mock.patch('ansible.module_utils.urls.Request.open', new=new_open):
|
||||
if HAS_TOWER_CLI:
|
||||
tower_cli_mgr = mock.patch('tower_cli.api.Session.request', new=new_request)
|
||||
else:
|
||||
tower_cli_mgr = suppress()
|
||||
with tower_cli_mgr:
|
||||
# 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()
|
||||
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 rf
|
||||
|
||||
@ -79,12 +79,10 @@ def test_create_custom_credential_type(run_module, admin_user):
|
||||
ct = CredentialType.objects.get(name='Nexus')
|
||||
result.pop('invocation')
|
||||
assert result == {
|
||||
"credential_type": "Nexus",
|
||||
"state": "present",
|
||||
"name": "Nexus",
|
||||
"id": ct.pk,
|
||||
"changed": True
|
||||
"changed": True,
|
||||
}
|
||||
|
||||
assert ct.inputs == {"fields": [{"id": "server", "type": "string", "default": "", "label": ""}], "required": []}
|
||||
assert ct.injectors == {'extra_vars': {'nexus_credential': 'test'}}
|
||||
|
||||
|
||||
@ -10,25 +10,25 @@ from awx.main.models import Organization, Inventory, Group
|
||||
def test_create_group(run_module, admin_user):
|
||||
org = Organization.objects.create(name='test-org')
|
||||
inv = Inventory.objects.create(name='test-inv', organization=org)
|
||||
variables = {"ansible_network_os": "iosxr"}
|
||||
|
||||
result = run_module('tower_group', dict(
|
||||
name='Test Group',
|
||||
inventory='test-inv',
|
||||
variables='ansible_network_os: iosxr',
|
||||
variables=variables,
|
||||
state='present'
|
||||
), admin_user)
|
||||
assert result.get('changed'), result
|
||||
|
||||
group = Group.objects.get(name='Test Group')
|
||||
assert group.inventory == inv
|
||||
assert group.variables == 'ansible_network_os: iosxr'
|
||||
assert group.variables == '{"ansible_network_os": "iosxr"}'
|
||||
|
||||
result.pop('invocation')
|
||||
assert result == {
|
||||
'id': group.id,
|
||||
'group': 'Test Group',
|
||||
'name': 'Test Group',
|
||||
'changed': True,
|
||||
'state': 'present'
|
||||
}
|
||||
|
||||
|
||||
@ -40,20 +40,16 @@ def test_tower_group_idempotent(run_module, admin_user):
|
||||
group = Group.objects.create(
|
||||
name='Test Group',
|
||||
inventory=inv,
|
||||
variables='ansible_network_os: iosxr'
|
||||
)
|
||||
|
||||
result = run_module('tower_group', dict(
|
||||
name='Test Group',
|
||||
inventory='test-inv',
|
||||
variables='ansible_network_os: iosxr',
|
||||
state='present'
|
||||
), admin_user)
|
||||
|
||||
result.pop('invocation')
|
||||
assert result == {
|
||||
'id': group.id,
|
||||
'group': 'Test Group',
|
||||
'changed': False, # idempotency assertion
|
||||
'state': 'present'
|
||||
}
|
||||
|
||||
@ -10,23 +10,29 @@ from awx.main.models import Organization, Inventory, InventorySource, Project
|
||||
def base_inventory():
|
||||
org = Organization.objects.create(name='test-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
|
||||
|
||||
|
||||
@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
|
||||
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(
|
||||
name='foo',
|
||||
inventory='test-inv',
|
||||
inventory=base_inventory.name,
|
||||
state='present',
|
||||
source='scm',
|
||||
source_project='test-proj'
|
||||
source_path=source_path,
|
||||
source_project=project.name
|
||||
), admin_user)
|
||||
assert result.pop('changed', None), result
|
||||
|
||||
@ -35,8 +41,7 @@ def test_inventory_source_create(run_module, admin_user, base_inventory):
|
||||
result.pop('invocation')
|
||||
assert result == {
|
||||
'id': inv_src.id,
|
||||
'inventory_source': 'foo',
|
||||
'state': 'present'
|
||||
'name': 'foo',
|
||||
}
|
||||
|
||||
|
||||
@ -45,6 +50,7 @@ def test_create_inventory_source_implied_org(run_module, admin_user):
|
||||
org = Organization.objects.create(name='test-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(
|
||||
name='Test Inventory Source',
|
||||
inventory='test-inv',
|
||||
@ -58,8 +64,7 @@ def test_create_inventory_source_implied_org(run_module, admin_user):
|
||||
|
||||
result.pop('invocation')
|
||||
assert result == {
|
||||
"inventory_source": "Test Inventory Source",
|
||||
"state": "present",
|
||||
"name": "Test Inventory Source",
|
||||
"id": inv_src.id,
|
||||
}
|
||||
|
||||
@ -67,43 +72,43 @@ def test_create_inventory_source_implied_org(run_module, admin_user):
|
||||
@pytest.mark.django_db
|
||||
def test_create_inventory_source_multiple_orgs(run_module, admin_user):
|
||||
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
|
||||
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(
|
||||
name='Test Inventory Source',
|
||||
inventory='test-inv',
|
||||
inventory=inv2.id,
|
||||
source='ec2',
|
||||
organization='test-org',
|
||||
state='present'
|
||||
), admin_user)
|
||||
assert result.pop('changed', None), result
|
||||
|
||||
inv_src = InventorySource.objects.get(name='Test Inventory Source')
|
||||
assert inv_src.inventory == inv
|
||||
assert inv_src.inventory == inv2
|
||||
|
||||
result.pop('invocation')
|
||||
assert result == {
|
||||
"inventory_source": "Test Inventory Source",
|
||||
"state": "present",
|
||||
"name": "Test Inventory Source",
|
||||
"id": inv_src.id,
|
||||
}
|
||||
|
||||
|
||||
@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/'
|
||||
source_path = '/var/lib/awx/example_source_path/'
|
||||
with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=[path]):
|
||||
result = run_module('tower_inventory_source', dict(
|
||||
name='foo',
|
||||
inventory='test-inv',
|
||||
inventory=base_inventory.name,
|
||||
state='present',
|
||||
source='scm',
|
||||
source_project='test-proj',
|
||||
custom_virtualenv=path
|
||||
source_project=project.name,
|
||||
custom_virtualenv=path,
|
||||
source_path=source_path
|
||||
), admin_user)
|
||||
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
|
||||
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
|
||||
unrelated to the params that the user passed.
|
||||
This enforces assumptions about the behavior of the AnsibleModule
|
||||
default argument_spec behavior.
|
||||
"""
|
||||
source_path = '/var/lib/awx/example_source_path/'
|
||||
inv_src = InventorySource.objects.create(
|
||||
name='foo',
|
||||
inventory=base_inventory,
|
||||
source_project=Project.objects.get(name='test-proj'),
|
||||
source_project=project,
|
||||
source='scm',
|
||||
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(
|
||||
name='foo',
|
||||
description='this is the changed description',
|
||||
inventory='test-inv',
|
||||
inventory=base_inventory.name,
|
||||
source='scm', # is required, but behavior is arguable
|
||||
state='present'
|
||||
state='present',
|
||||
source_project=project.name,
|
||||
source_path=source_path
|
||||
), admin_user)
|
||||
assert result.pop('changed', None), result
|
||||
inv_src.refresh_from_db()
|
||||
assert inv_src.custom_virtualenv == '/venv/foobar/'
|
||||
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', '')
|
||||
|
||||
35
awx_collection/test/awx/test_module_utils.py
Normal file
35
awx_collection/test/awx/test_module_utils.py
Normal 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.'
|
||||
)
|
||||
@ -9,18 +9,28 @@ from awx.main.models import Organization
|
||||
@pytest.mark.django_db
|
||||
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)
|
||||
assert result.get('changed'), result
|
||||
|
||||
org = Organization.objects.get(name='foo')
|
||||
|
||||
assert result == {
|
||||
"organization": "foo",
|
||||
"state": "present",
|
||||
"id": org.id,
|
||||
"name": "foo",
|
||||
"changed": True,
|
||||
"id": org.id,
|
||||
"invocation": {
|
||||
"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')
|
||||
result.pop('invocation')
|
||||
|
||||
assert result == {
|
||||
"organization": "foo",
|
||||
"state": "present",
|
||||
"name": "foo",
|
||||
"id": org.id
|
||||
}
|
||||
|
||||
|
||||
@ -3,18 +3,23 @@ __metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from awx.main.models import Project
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_project(run_module, admin_user, organization):
|
||||
result = run_module('tower_project', dict(
|
||||
name='foo',
|
||||
organization=organization.name,
|
||||
scm_type='git',
|
||||
scm_url='https://foo.invalid',
|
||||
wait=False
|
||||
), admin_user)
|
||||
with mock.patch('ansible.module_utils.basic.AnsibleModule.warn') as mock_warn:
|
||||
result = run_module('tower_project', dict(
|
||||
name='foo',
|
||||
organization=organization.name,
|
||||
scm_type='git',
|
||||
scm_url='https://foo.invalid',
|
||||
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
|
||||
|
||||
proj = Project.objects.get(name='foo')
|
||||
@ -23,7 +28,6 @@ def test_create_project(run_module, admin_user, organization):
|
||||
|
||||
result.pop('invocation')
|
||||
assert result == {
|
||||
'id': proj.id,
|
||||
'project': 'foo',
|
||||
'state': 'present'
|
||||
'name': 'foo',
|
||||
'id': proj.id
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
from unittest import mock
|
||||
import json
|
||||
|
||||
from awx.main.models import (
|
||||
@ -65,7 +66,9 @@ def test_receive_send_jt(run_module, admin_user, mocker):
|
||||
# recreate everything
|
||||
with mocker.patch('sys.stdin.isatty', return_value=True):
|
||||
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
|
||||
|
||||
|
||||
66
awx_collection/test/awx/test_team.py
Normal file
66
awx_collection/test/awx/test_team.py
Normal 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
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
plugins/modules/tower_group.py use-argspec-type-path
|
||||
plugins/modules/tower_host.py use-argspec-type-path
|
||||
@ -1,2 +0,0 @@
|
||||
plugins/modules/tower_group.py use-argspec-type-path
|
||||
plugins/modules/tower_host.py use-argspec-type-path
|
||||
Loading…
x
Reference in New Issue
Block a user