Merge pull request #7799 from john-westcott-iv/import-export-collecion-modules

Adding import/export modules around AWX Kit features

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-08-19 20:51:05 +00:00
committed by GitHub
45 changed files with 866 additions and 314 deletions

View File

@@ -72,7 +72,7 @@ from ansible.errors import AnsibleParserError, AnsibleOptionsError
from ansible.plugins.inventory import BaseInventoryPlugin from ansible.plugins.inventory import BaseInventoryPlugin
from ansible.config.manager import ensure_type from ansible.config.manager import ensure_type
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def handle_error(**kwargs): def handle_error(**kwargs):
@@ -104,12 +104,12 @@ class InventoryModule(BaseInventoryPlugin):
# Defer processing of params to logic shared with the modules # Defer processing of params to logic shared with the modules
module_params = {} module_params = {}
for plugin_param, module_param in TowerModule.short_params.items(): for plugin_param, module_param in TowerAPIModule.short_params.items():
opt_val = self.get_option(plugin_param) opt_val = self.get_option(plugin_param)
if opt_val is not None: if opt_val is not None:
module_params[module_param] = opt_val module_params[module_param] = opt_val
module = TowerModule( module = TowerAPIModule(
argument_spec={}, direct_params=module_params, argument_spec={}, direct_params=module_params,
error_callback=handle_error, warn_callback=self.warn_callback error_callback=handle_error, warn_callback=self.warn_callback
) )

View File

@@ -115,7 +115,7 @@ from ansible.plugins.lookup import LookupBase
from ansible.errors import AnsibleError from ansible.errors import AnsibleError
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
from ansible.utils.display import Display from ansible.utils.display import Display
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
class LookupModule(LookupBase): class LookupModule(LookupBase):
@@ -133,13 +133,13 @@ class LookupModule(LookupBase):
# Defer processing of params to logic shared with the modules # Defer processing of params to logic shared with the modules
module_params = {} module_params = {}
for plugin_param, module_param in TowerModule.short_params.items(): for plugin_param, module_param in TowerAPIModule.short_params.items():
opt_val = self.get_option(plugin_param) opt_val = self.get_option(plugin_param)
if opt_val is not None: if opt_val is not None:
module_params[module_param] = opt_val module_params[module_param] = opt_val
# Create our module # Create our module
module = TowerModule( module = TowerAPIModule(
argument_spec={}, direct_params=module_params, argument_spec={}, direct_params=module_params,
error_callback=self.handle_error, warn_callback=self.warn_callback error_callback=self.handle_error, warn_callback=self.warn_callback
) )

View File

@@ -1,37 +1,18 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
from ansible.module_utils.basic import AnsibleModule, env_fallback from . tower_module import TowerModule
from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError
from ansible.module_utils.six import PY2, string_types from ansible.module_utils.six import PY2
from ansible.module_utils.six.moves import StringIO from ansible.module_utils.six.moves.urllib.parse import urlencode
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.urllib.error import HTTPError
from ansible.module_utils.six.moves.http_cookiejar import CookieJar 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 import re
from json import loads, dumps 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): class TowerAPIModule(TowerModule):
pass # TODO: Move the collection version check into tower_module.py
class ItemNotDefined(Exception):
pass
class TowerModule(AnsibleModule):
# This gets set by the make process so whatever is in here is irrelevant # This gets set by the make process so whatever is in here is irrelevant
_COLLECTION_VERSION = "0.0.1-devel" _COLLECTION_VERSION = "0.0.1-devel"
_COLLECTION_TYPE = "awx" _COLLECTION_TYPE = "awx"
@@ -41,197 +22,16 @@ class TowerModule(AnsibleModule):
'awx': 'AWX', 'awx': 'AWX',
'tower': 'Red Hat Ansible Tower', 'tower': 'Red Hat Ansible Tower',
} }
url = None
AUTH_ARGSPEC = dict(
tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])),
tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])),
tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])),
validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])),
tower_oauthtoken=dict(type='raw', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])),
tower_config_file=dict(type='path', required=False, default=None),
)
short_params = {
'host': 'tower_host',
'username': 'tower_username',
'password': 'tower_password',
'verify_ssl': 'validate_certs',
'oauth_token': 'tower_oauthtoken',
}
host = '127.0.0.1'
username = None
password = None
verify_ssl = True
oauth_token = None
oauth_token_id = None
session = None session = None
cookie_jar = CookieJar() cookie_jar = CookieJar()
authenticated = False
config_name = 'tower_cli.cfg'
ENCRYPTED_STRING = "$encrypted$"
version_checked = False
error_callback = None
warn_callback = None
def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs): def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs):
full_argspec = {}
full_argspec.update(TowerModule.AUTH_ARGSPEC)
full_argspec.update(argument_spec)
kwargs['supports_check_mode'] = True kwargs['supports_check_mode'] = True
self.error_callback = error_callback super(TowerAPIModule, self).__init__(argument_spec=argument_spec, direct_params=direct_params,
self.warn_callback = warn_callback error_callback=error_callback, warn_callback=warn_callback, **kwargs)
self.json_output = {'changed': False}
if direct_params is not None:
self.params = direct_params
else:
super(TowerModule, self).__init__(argument_spec=full_argspec, **kwargs)
self.load_config_files()
# Parameters specified on command line will override settings in any config
for short_param, long_param in self.short_params.items():
direct_value = self.params.get(long_param)
if direct_value is not None:
setattr(self, short_param, direct_value)
# Perform magic depending on whether tower_oauthtoken is a string or a dict
if self.params.get('tower_oauthtoken'):
token_param = self.params.get('tower_oauthtoken')
if type(token_param) is dict:
if 'token' in token_param:
self.oauth_token = self.params.get('tower_oauthtoken')['token']
else:
self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry")
elif isinstance(token_param, string_types):
self.oauth_token = self.params.get('tower_oauthtoken')
else:
error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__)
self.fail_json(msg=error_msg)
# Perform some basic validation
if not re.match('^https{0,1}://', self.host):
self.host = "https://{0}".format(self.host)
# Try to parse the hostname as a url
try:
self.url = urlparse(self.host)
except Exception as e:
self.fail_json(msg="Unable to parse tower_host as a URL ({1}): {0}".format(self.host, e))
# Try to resolve the hostname
hostname = self.url.netloc.split(':')[0]
try:
gethostbyname(hostname)
except Exception as e:
self.fail_json(msg="Unable to resolve tower_host ({1}): {0}".format(hostname, e))
self.session = Request(cookies=CookieJar(), validate_certs=self.verify_ssl) self.session = Request(cookies=CookieJar(), validate_certs=self.verify_ssl)
def load_config_files(self):
# Load configs like TowerCLI would have from least import to most
config_files = ['/etc/tower/tower_cli.cfg', join(expanduser("~"), ".{0}".format(self.config_name))]
local_dir = getcwd()
config_files.append(join(local_dir, self.config_name))
while split(local_dir)[1]:
local_dir = split(local_dir)[0]
config_files.insert(2, join(local_dir, ".{0}".format(self.config_name)))
# If we have a specified tower config, load it
if self.params.get('tower_config_file'):
duplicated_params = [
fn for fn in self.AUTH_ARGSPEC
if fn != 'tower_config_file' and self.params.get(fn) is not None
]
if duplicated_params:
self.warn((
'The parameter(s) {0} were provided at the same time as tower_config_file. '
'Precedence may be unstable, we suggest either using config file or params.'
).format(', '.join(duplicated_params)))
try:
# TODO: warn if there are conflicts with other params
self.load_config(self.params.get('tower_config_file'))
except ConfigFileException as cfe:
# Since we were told specifically to load this we want it to fail if we have an error
self.fail_json(msg=cfe)
else:
for config_file in config_files:
if exists(config_file) and not isdir(config_file):
# Only throw a formatting error if the file exists and is not a directory
try:
self.load_config(config_file)
except ConfigFileException:
self.fail_json(msg='The config file {0} is not properly formatted'.format(config_file))
def load_config(self, config_path):
# Validate the config file is an actual file
if not isfile(config_path):
raise ConfigFileException('The specified config file does not exist')
if not access(config_path, R_OK):
raise ConfigFileException("The specified config file cannot be read")
# Read in the file contents:
with open(config_path, 'r') as f:
config_string = f.read()
# First try to yaml load the content (which will also load json)
try:
try_config_parsing = True
if HAS_YAML:
try:
config_data = yaml.load(config_string, Loader=yaml.SafeLoader)
# If this is an actual ini file, yaml will return the whole thing as a string instead of a dict
if type(config_data) is not dict:
raise AssertionError("The yaml config file is not properly formatted as a dict.")
try_config_parsing = False
except(AttributeError, yaml.YAMLError, AssertionError):
try_config_parsing = True
if try_config_parsing:
# TowerCLI used to support a config file with a missing [general] section by prepending it if missing
if '[general]' not in config_string:
config_string = '[general]\n{0}'.format(config_string)
config = ConfigParser()
try:
placeholder_file = StringIO(config_string)
# py2 ConfigParser has readfp, that has been deprecated in favor of read_file in py3
# This "if" removes the deprecation warning
if hasattr(config, 'read_file'):
config.read_file(placeholder_file)
else:
config.readfp(placeholder_file)
# If we made it here then we have values from reading the ini file, so let's pull them out into a dict
config_data = {}
for honorred_setting in self.short_params:
try:
config_data[honorred_setting] = config.get('general', honorred_setting)
except NoOptionError:
pass
except Exception as e:
raise ConfigFileException("An unknown exception occured trying to ini load config file: {0}".format(e))
except Exception as e:
raise ConfigFileException("An unknown exception occured trying to load config file: {0}".format(e))
# If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here
for honorred_setting in self.short_params:
if honorred_setting in config_data:
# Veriffy SSL must be a boolean
if honorred_setting == 'verify_ssl':
if type(config_data[honorred_setting]) is str:
setattr(self, honorred_setting, strtobool(config_data[honorred_setting]))
else:
setattr(self, honorred_setting, bool(config_data[honorred_setting]))
else:
setattr(self, honorred_setting, config_data[honorred_setting])
@staticmethod @staticmethod
def param_to_endpoint(name): def param_to_endpoint(name):
exceptions = { exceptions = {
@@ -650,13 +450,13 @@ class TowerModule(AnsibleModule):
""" """
if isinstance(obj, dict): if isinstance(obj, dict):
for val in obj.values(): for val in obj.values():
if TowerModule.has_encrypted_values(val): if TowerAPIModule.has_encrypted_values(val):
return True return True
elif isinstance(obj, list): elif isinstance(obj, list):
for val in obj: for val in obj:
if TowerModule.has_encrypted_values(val): if TowerAPIModule.has_encrypted_values(val):
return True return True
elif obj == TowerModule.ENCRYPTED_STRING: elif obj == TowerAPIModule.ENCRYPTED_STRING:
return True return True
return False return False
@@ -678,10 +478,9 @@ class TowerModule(AnsibleModule):
# This will exit from the module on its own # This will exit from the module on its own
# If the method successfully updates an item and on_update param is defined, # If the method successfully updates an item and on_update param is defined,
# the on_update parameter will be called as a method pasing in this object and the json from the response # the on_update parameter will be called as a method pasing in this object and the json from the response
# This will return one of three things: # This will return one of two things:
# 1. None if the existing_item does not need to be updated # 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. # 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 # Note: common error codes from the Tower API can cause the module to fail
response = None response = None
if existing_item: if existing_item:
@@ -777,25 +576,6 @@ class TowerModule(AnsibleModule):
# Sanity check: Did the server send back some kind of internal error? # 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)) self.warn('Failed to release tower token {0}: {1}'.format(self.oauth_token_id, e))
def fail_json(self, **kwargs):
# Try to log out if we are authenticated
self.logout()
if self.error_callback:
self.error_callback(**kwargs)
else:
super(TowerModule, self).fail_json(**kwargs)
def exit_json(self, **kwargs):
# Try to log out if we are authenticated
self.logout()
super(TowerModule, self).exit_json(**kwargs)
def warn(self, warning):
if self.warn_callback is not None:
self.warn_callback(warning)
else:
super(TowerModule, self).warn(warning)
def is_job_done(self, job_status): def is_job_done(self, job_status):
if job_status in ['new', 'pending', 'waiting', 'running']: if job_status in ['new', 'pending', 'waiting', 'running']:
return False return False

View File

@@ -0,0 +1,53 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from . tower_module import TowerModule
from ansible.module_utils.basic import missing_required_lib
try:
from awxkit.api.client import Connection
from awxkit.api.pages.api import ApiV2
from awxkit.api import get_registered_page
HAS_AWX_KIT = True
except ImportError:
HAS_AWX_KIT = False
class TowerAWXKitModule(TowerModule):
connection = None
apiV2Ref = None
def __init__(self, argument_spec, **kwargs):
kwargs['supports_check_mode'] = False
super(TowerAWXKitModule, self).__init__(argument_spec=argument_spec, **kwargs)
# Die if we don't have AWX_KIT installed
if not HAS_AWX_KIT:
self.exit_json(msg=missing_required_lib('awxkit'))
# Establish our conneciton object
self.connection = Connection(self.host, verify=self.verify_ssl)
def authenticate(self):
try:
if self.oauth_token:
self.connection.login(None, None, token=self.oauth_token)
self.authenticated = True
elif self.username:
self.connection.login(username=self.username, password=self.password)
self.authenticated = True
except Exception:
self.exit_json("Failed to authenticate")
def get_api_v2_object(self):
if not self.apiV2Ref:
if not self.authenticated:
self.authenticate()
v2_index = get_registered_page('/api/v2/')(self.connection).get()
self.api_ref = ApiV2(connection=self.connection, **{'json': v2_index})
return self.api_ref
def logout(self):
if self.authenticated:
self.connection.logout()

View File

@@ -91,7 +91,7 @@ def tower_check_mode(module):
module.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo)) module.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo))
class TowerModule(AnsibleModule): class TowerLegacyModule(AnsibleModule):
def __init__(self, argument_spec, **kwargs): def __init__(self, argument_spec, **kwargs):
args = dict( args = dict(
tower_host=dict(), tower_host=dict(),
@@ -110,7 +110,7 @@ class TowerModule(AnsibleModule):
('tower_config_file', 'validate_certs'), ('tower_config_file', 'validate_certs'),
)) ))
super(TowerModule, self).__init__(argument_spec=args, **kwargs) super(TowerLegacyModule, self).__init__(argument_spec=args, **kwargs)
if not HAS_TOWER_CLI: if not HAS_TOWER_CLI:
self.fail_json(msg=missing_required_lib('ansible-tower-cli'), self.fail_json(msg=missing_required_lib('ansible-tower-cli'),

View File

@@ -0,0 +1,239 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible.module_utils.six import PY2, string_types
from ansible.module_utils.six.moves import StringIO
from ansible.module_utils.six.moves.urllib.parse import urlparse
from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError
from socket import gethostbyname
import re
from os.path import isfile, expanduser, split, join, exists, isdir
from os import access, R_OK, getcwd
from distutils.util import strtobool
try:
import yaml
HAS_YAML = True
except ImportError:
HAS_YAML = False
class ConfigFileException(Exception):
pass
class ItemNotDefined(Exception):
pass
class TowerModule(AnsibleModule):
url = None
AUTH_ARGSPEC = dict(
tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])),
tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])),
tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])),
validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])),
tower_oauthtoken=dict(type='raw', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])),
tower_config_file=dict(type='path', required=False, default=None),
)
short_params = {
'host': 'tower_host',
'username': 'tower_username',
'password': 'tower_password',
'verify_ssl': 'validate_certs',
'oauth_token': 'tower_oauthtoken',
}
host = '127.0.0.1'
username = None
password = None
verify_ssl = True
oauth_token = None
oauth_token_id = None
authenticated = False
config_name = 'tower_cli.cfg'
ENCRYPTED_STRING = "$encrypted$"
version_checked = False
error_callback = None
warn_callback = None
def __init__(self, argument_spec=None, direct_params=None, error_callback=None, warn_callback=None, **kwargs):
full_argspec = {}
full_argspec.update(TowerModule.AUTH_ARGSPEC)
full_argspec.update(argument_spec)
kwargs['supports_check_mode'] = True
self.error_callback = error_callback
self.warn_callback = warn_callback
self.json_output = {'changed': False}
if direct_params is not None:
self.params = direct_params
else:
super(TowerModule, self).__init__(argument_spec=full_argspec, **kwargs)
self.load_config_files()
# Parameters specified on command line will override settings in any config
for short_param, long_param in self.short_params.items():
direct_value = self.params.get(long_param)
if direct_value is not None:
setattr(self, short_param, direct_value)
# Perform magic depending on whether tower_oauthtoken is a string or a dict
if self.params.get('tower_oauthtoken'):
token_param = self.params.get('tower_oauthtoken')
if type(token_param) is dict:
if 'token' in token_param:
self.oauth_token = self.params.get('tower_oauthtoken')['token']
else:
self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry")
elif isinstance(token_param, string_types):
self.oauth_token = self.params.get('tower_oauthtoken')
else:
error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__)
self.fail_json(msg=error_msg)
# Perform some basic validation
if not re.match('^https{0,1}://', self.host):
self.host = "https://{0}".format(self.host)
# Try to parse the hostname as a url
try:
self.url = urlparse(self.host)
except Exception as e:
self.fail_json(msg="Unable to parse tower_host as a URL ({1}): {0}".format(self.host, e))
# Try to resolve the hostname
hostname = self.url.netloc.split(':')[0]
try:
gethostbyname(hostname)
except Exception as e:
self.fail_json(msg="Unable to resolve tower_host ({1}): {0}".format(hostname, e))
def load_config_files(self):
# Load configs like TowerCLI would have from least import to most
config_files = ['/etc/tower/tower_cli.cfg', join(expanduser("~"), ".{0}".format(self.config_name))]
local_dir = getcwd()
config_files.append(join(local_dir, self.config_name))
while split(local_dir)[1]:
local_dir = split(local_dir)[0]
config_files.insert(2, join(local_dir, ".{0}".format(self.config_name)))
# If we have a specified tower config, load it
if self.params.get('tower_config_file'):
duplicated_params = [
fn for fn in self.AUTH_ARGSPEC
if fn != 'tower_config_file' and self.params.get(fn) is not None
]
if duplicated_params:
self.warn((
'The parameter(s) {0} were provided at the same time as tower_config_file. '
'Precedence may be unstable, we suggest either using config file or params.'
).format(', '.join(duplicated_params)))
try:
# TODO: warn if there are conflicts with other params
self.load_config(self.params.get('tower_config_file'))
except ConfigFileException as cfe:
# Since we were told specifically to load this we want it to fail if we have an error
self.fail_json(msg=cfe)
else:
for config_file in config_files:
if exists(config_file) and not isdir(config_file):
# Only throw a formatting error if the file exists and is not a directory
try:
self.load_config(config_file)
except ConfigFileException:
self.fail_json(msg='The config file {0} is not properly formatted'.format(config_file))
def load_config(self, config_path):
# Validate the config file is an actual file
if not isfile(config_path):
raise ConfigFileException('The specified config file does not exist')
if not access(config_path, R_OK):
raise ConfigFileException("The specified config file cannot be read")
# Read in the file contents:
with open(config_path, 'r') as f:
config_string = f.read()
# First try to yaml load the content (which will also load json)
try:
try_config_parsing = True
if HAS_YAML:
try:
config_data = yaml.load(config_string, Loader=yaml.SafeLoader)
# If this is an actual ini file, yaml will return the whole thing as a string instead of a dict
if type(config_data) is not dict:
raise AssertionError("The yaml config file is not properly formatted as a dict.")
try_config_parsing = False
except(AttributeError, yaml.YAMLError, AssertionError):
try_config_parsing = True
if try_config_parsing:
# TowerCLI used to support a config file with a missing [general] section by prepending it if missing
if '[general]' not in config_string:
config_string = '[general]\n{0}'.format(config_string)
config = ConfigParser()
try:
placeholder_file = StringIO(config_string)
# py2 ConfigParser has readfp, that has been deprecated in favor of read_file in py3
# This "if" removes the deprecation warning
if hasattr(config, 'read_file'):
config.read_file(placeholder_file)
else:
config.readfp(placeholder_file)
# If we made it here then we have values from reading the ini file, so let's pull them out into a dict
config_data = {}
for honorred_setting in self.short_params:
try:
config_data[honorred_setting] = config.get('general', honorred_setting)
except NoOptionError:
pass
except Exception as e:
raise ConfigFileException("An unknown exception occured trying to ini load config file: {0}".format(e))
except Exception as e:
raise ConfigFileException("An unknown exception occured trying to load config file: {0}".format(e))
# If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here
for honorred_setting in self.short_params:
if honorred_setting in config_data:
# Veriffy SSL must be a boolean
if honorred_setting == 'verify_ssl':
if type(config_data[honorred_setting]) is str:
setattr(self, honorred_setting, strtobool(config_data[honorred_setting]))
else:
setattr(self, honorred_setting, bool(config_data[honorred_setting]))
else:
setattr(self, honorred_setting, config_data[honorred_setting])
def logout(self):
# This method is intended to be overridden
pass
def fail_json(self, **kwargs):
# Try to log out if we are authenticated
self.logout()
if self.error_callback:
self.error_callback(**kwargs)
else:
super(TowerModule, self).fail_json(**kwargs)
def exit_json(self, **kwargs):
# Try to log out if we are authenticated
self.logout()
super(TowerModule, self).exit_json(**kwargs)
def warn(self, warning):
if self.warn_callback is not None:
self.warn_callback(warning)
else:
super(TowerModule, self).warn(warning)

View File

@@ -269,7 +269,7 @@ EXAMPLES = '''
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
KIND_CHOICES = { KIND_CHOICES = {
'ssh': 'Machine', 'ssh': 'Machine',
@@ -336,7 +336,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec, required_one_of=[['kind', 'credential_type']]) module = TowerAPIModule(argument_spec=argument_spec, required_one_of=[['kind', 'credential_type']])
# Extract our parameters # Extract our parameters
name = module.params.get('name') name = module.params.get('name')

View File

@@ -70,7 +70,7 @@ EXAMPLES = '''
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def main(): def main():
@@ -85,7 +85,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
description = module.params.get('description') description = module.params.get('description')

View File

@@ -81,7 +81,7 @@ EXAMPLES = '''
RETURN = ''' # ''' RETURN = ''' # '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
KIND_CHOICES = { KIND_CHOICES = {
'ssh': 'Machine', 'ssh': 'Machine',
@@ -105,7 +105,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
name = module.params.get('name') name = module.params.get('name')

View File

@@ -0,0 +1,166 @@
#!/usr/bin/python
# coding: utf-8 -*-
# (c) 2017, John Westcott IV <john.westcott.iv@redhat.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: tower_export
author: "John Westcott IV (@john-westcott-iv)"
version_added: "3.7"
short_description: export resources from Ansible Tower.
description:
- Export assets from Ansible Tower.
options:
all:
description:
- Export all assets
type: bool
default: 'False'
organizations:
description:
- organization name to export
type: str
users:
description:
- user name to export
type: str
teams:
description:
- team name to export
type: str
credential_types:
description:
- credential type name to export
type: str
credentials:
description:
- credential name to export
type: str
notification_templates:
description:
- notification template name to export
type: str
inventory_sources:
description:
- inventory soruce to export
type: str
inventory:
description:
- inventory name to export
type: str
projects:
description:
- project name to export
type: str
job_templates:
description:
- job template name to export
type: str
workflow_job_templates:
description:
- workflow name to export
type: str
requirements:
- "awxkit >= 9.3.0"
notes:
- Specifying a name of "all" for any asset type will export all items of that asset type.
extends_documentation_fragment: awx.awx.auth
'''
EXAMPLES = '''
- name: Export all tower assets
tower_export:
all: True
- name: Export all inventories
tower_export:
inventory: 'all'
- name: Export a job template named "My Template" and all Credentials
tower_export:
job_template: "My Template"
credential: 'all'
'''
from os import environ
import logging
from ansible.module_utils.six.moves import StringIO
from ..module_utils.tower_awxkit import TowerAWXKitModule
try:
from awxkit.api.pages.api import EXPORTABLE_RESOURCES
HAS_EXPORTABLE_RESOURCES = True
except ImportError:
HAS_EXPORTABLE_RESOURCES = False
def main():
argument_spec = dict(
all=dict(type='bool', default=False),
)
# We are not going to raise an error here because the __init__ method of TowerAWXKitModule will do that for us
if HAS_EXPORTABLE_RESOURCES:
for resource in EXPORTABLE_RESOURCES:
argument_spec[resource] = dict(type='str')
module = TowerAWXKitModule(argument_spec=argument_spec)
if not HAS_EXPORTABLE_RESOURCES:
module.fail_json(msg="Your version of awxkit does not have import/export")
# The export process will never change a Tower system
module.json_output['changed'] = False
# The exporter code currently works like the following:
# Empty string == all assets of that type
# Non-Empty string = just one asset of that type (by name or ID)
# Asset type not present or None = skip asset type (unless everything is None, then export all)
# Here we are going to setup a dict of values to export
export_args = {}
for resource in EXPORTABLE_RESOURCES:
if module.params.get('all') or module.params.get(resource) == 'all':
# If we are exporting everything or we got the keyword "all" we pass in an empty string for this asset type
export_args[resource] = ''
else:
# Otherwise we take either the string or None (if the parameter was not passed) to get one or no items
export_args[resource] = module.params.get(resource)
# Currently the import process does not return anything on error
# It simply just logs to pythons logger
# Setup a log gobbler to get error messages from import_assets
log_capture_string = StringIO()
ch = logging.StreamHandler(log_capture_string)
for logger_name in ['awxkit.api.pages.api', 'awxkit.api.pages.page']:
logger = logging.getLogger(logger_name)
logger.setLevel(logging.WARNING)
ch.setLevel(logging.WARNING)
logger.addHandler(ch)
log_contents = ''
# Run the import process
try:
module.json_output['assets'] = module.get_api_v2_object().export_assets(**export_args)
module.exit_json(**module.json_output)
except Exception as e:
module.fail_json(msg="Failed to export assets {0}".format(e))
finally:
# Finally consume the logs incase there were any errors and die if there were
log_contents = log_capture_string.getvalue()
log_capture_string.close()
if log_contents != '':
module.fail_json(msg=log_contents)
if __name__ == '__main__':
main()

View File

@@ -76,7 +76,7 @@ EXAMPLES = '''
tower_config_file: "~/tower_cli.cfg" tower_config_file: "~/tower_cli.cfg"
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
import json import json
@@ -94,7 +94,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
name = module.params.get('name') name = module.params.get('name')

View File

@@ -72,7 +72,7 @@ EXAMPLES = '''
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
import json import json
@@ -89,7 +89,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
name = module.params.get('name') name = module.params.get('name')

View File

@@ -0,0 +1,105 @@
#!/usr/bin/python
# coding: utf-8 -*-
# (c) 2017, John Westcott IV <john.westcott.iv@redhat.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: tower_import
author: "John Westcott (@john-westcott-iv)"
version_added: "3.7"
short_description: import resources into Ansible Tower.
description:
- Import assets into Ansible Tower. See
U(https://www.ansible.com/tower) for an overview.
options:
assets:
description:
- The assets to import.
- This can be the output of tower_export or loaded from a file
required: True
type: dict
requirements:
- "awxkit >= 9.3.0"
extends_documentation_fragment: awx.awx.auth
'''
EXAMPLES = '''
- name: Export all assets
tower_export:
all: True
registeR: export_output
- name: Import all tower assets from our export
tower_import:
assets: "{{ export_output.assets }}"
- name: Load data from a json file created by a command like awx export --organization Default
tower_import:
assets: "{{ lookup('file', 'org.json') | from_json() }}"
'''
from ..module_utils.tower_awxkit import TowerAWXKitModule
# These two lines are not needed if awxkit changes to do progamatic notifications on issues
from ansible.module_utils.six.moves import StringIO
import logging
# In this module we don't use EXPORTABLE_RESOURCES, we just want to validate that our installed awxkit has import/export
try:
from awxkit.api.pages.api import EXPORTABLE_RESOURCES
HAS_EXPORTABLE_RESOURCES = True
except ImportError:
HAS_EXPORTABLE_RESOURCES = False
def main():
argument_spec = dict(
assets=dict(type='dict', required=True)
)
module = TowerAWXKitModule(argument_spec=argument_spec, supports_check_mode=False)
assets = module.params.get('assets')
if not HAS_EXPORTABLE_RESOURCES:
module.fail_json(msg="Your version of awxkit does not appear to have import/export")
# Currently the import process does not return anything on error
# It simply just logs to pythons logger
# Setup a log gobbler to get error messages from import_assets
logger = logging.getLogger('awxkit.api.pages.api')
logger.setLevel(logging.WARNING)
log_capture_string = StringIO()
ch = logging.StreamHandler(log_capture_string)
ch.setLevel(logging.WARNING)
logger.addHandler(ch)
log_contents = ''
# Run the import process
try:
module.json_output['changed'] = module.get_api_v2_object().import_assets(assets)
except Exception as e:
module.fail_json(msg="Failed to import assets {0}".format(e))
finally:
# Finally consume the logs incase there were any errors and die if there were
log_contents = log_capture_string.getvalue()
log_capture_string.close()
if log_contents != '':
module.fail_json(msg=log_contents)
module.exit_json(**module.json_output)
if __name__ == '__main__':
main()

View File

@@ -71,7 +71,7 @@ EXAMPLES = '''
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
import json import json
@@ -88,7 +88,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
name = module.params.get('name') name = module.params.get('name')

View File

@@ -144,7 +144,7 @@ EXAMPLES = '''
private: false private: false
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
from json import dumps from json import dumps
@@ -184,7 +184,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
name = module.params.get('name') name = module.params.get('name')

View File

@@ -50,7 +50,7 @@ id:
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def main(): def main():
@@ -61,7 +61,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
job_id = module.params.get('job_id') job_id = module.params.get('job_id')

View File

@@ -124,7 +124,7 @@ status:
sample: pending sample: pending
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def main(): def main():
@@ -146,7 +146,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
optional_args = {} optional_args = {}
# Extract our parameters # Extract our parameters

View File

@@ -80,7 +80,7 @@ results:
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def main(): def main():
@@ -93,7 +93,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule( module = TowerAPIModule(
argument_spec=argument_spec, argument_spec=argument_spec,
mutually_exclusive=[ mutually_exclusive=[
('page', 'all_pages'), ('page', 'all_pages'),

View File

@@ -317,7 +317,7 @@ EXAMPLES = '''
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
import json import json
@@ -388,7 +388,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
name = module.params.get('name') name = module.params.get('name')

View File

@@ -92,7 +92,7 @@ status:
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
import time import time
@@ -120,7 +120,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
job_id = module.params.get('job_id') job_id = module.params.get('job_id')

View File

@@ -54,7 +54,7 @@ EXAMPLES = '''
organization: My Organization organization: My Organization
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def main(): def main():
@@ -67,7 +67,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
name = module.params.get('name') name = module.params.get('name')

View File

@@ -43,12 +43,12 @@ EXAMPLES = '''
eula_accepted: True eula_accepted: True
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def main(): def main():
module = TowerModule( module = TowerAPIModule(
argument_spec=dict( argument_spec=dict(
data=dict(type='dict', required=True), data=dict(type='dict', required=True),
eula_accepted=dict(type='bool', required=True), eula_accepted=dict(type='bool', required=True),

View File

@@ -62,11 +62,11 @@ EXAMPLES = '''
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def main(): def main():
module = TowerModule(argument_spec={}) module = TowerAPIModule(argument_spec={})
namespace = { namespace = {
'awx': 'awx', 'awx': 'awx',
'tower': 'ansible' 'tower': 'ansible'

View File

@@ -300,7 +300,7 @@ EXAMPLES = '''
RETURN = ''' # ''' RETURN = ''' # '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
OLD_INPUT_NAMES = ( OLD_INPUT_NAMES = (
'username', 'sender', 'recipients', 'use_tls', 'username', 'sender', 'recipients', 'use_tls',
@@ -355,7 +355,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
name = module.params.get('name') name = module.params.get('name')

View File

@@ -88,7 +88,7 @@ EXAMPLES = '''
tower_config_file: "~/tower_cli.cfg" tower_config_file: "~/tower_cli.cfg"
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def main(): def main():
@@ -106,7 +106,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
name = module.params.get('name') name = module.params.get('name')

View File

@@ -157,7 +157,7 @@ EXAMPLES = '''
import time import time
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def wait_for_project_update(module, last_request): def wait_for_project_update(module, last_request):
@@ -205,7 +205,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
name = module.params.get('name') name = module.params.get('name')

View File

@@ -134,7 +134,7 @@ assets:
sample: [ {}, {} ] sample: [ {}, {} ]
''' '''
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, HAS_TOWER_CLI from ..module_utils.tower_legacy import TowerLegacyModule, tower_auth_config, HAS_TOWER_CLI
try: try:
from tower_cli.cli.transfer.receive import Receiver from tower_cli.cli.transfer.receive import Receiver
@@ -163,7 +163,7 @@ def main():
workflow=dict(type='list', default=[], elements='str'), workflow=dict(type='list', default=[], elements='str'),
) )
module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) module = TowerLegacyModule(argument_spec=argument_spec, supports_check_mode=False)
module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI export command.", version="awx.awx:14.0.0") module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI export command.", version="awx.awx:14.0.0")

View File

@@ -89,7 +89,7 @@ EXAMPLES = '''
state: present state: present
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def main(): def main():
@@ -109,7 +109,7 @@ def main():
state=dict(choices=['present', 'absent'], default='present'), state=dict(choices=['present', 'absent'], default='present'),
) )
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
role_type = module.params.pop('role') role_type = module.params.pop('role')
role_field = role_type + '_role' role_field = role_type + '_role'

View File

@@ -136,7 +136,7 @@ EXAMPLES = '''
register: result register: result
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def main(): def main():
@@ -161,7 +161,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
rrule = module.params.get('rrule') rrule = module.params.get('rrule')

View File

@@ -81,7 +81,7 @@ import os
import sys import sys
from ansible.module_utils.six.moves import StringIO from ansible.module_utils.six.moves import StringIO
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, HAS_TOWER_CLI from ..module_utils.tower_legacy import TowerLegacyModule, tower_auth_config, HAS_TOWER_CLI
from tempfile import mkstemp from tempfile import mkstemp
@@ -103,7 +103,7 @@ def main():
password_management=dict(default='default', choices=['default', 'random']), password_management=dict(default='default', choices=['default', 'random']),
) )
module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) module = TowerLegacyModule(argument_spec=argument_spec, supports_check_mode=False)
module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI import command", version="awx.awx:14.0.0") module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI import command", version="awx.awx:14.0.0")

View File

@@ -70,7 +70,7 @@ EXAMPLES = '''
last_name: "surname" last_name: "surname"
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
try: try:
import yaml import yaml
@@ -111,7 +111,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule( module = TowerAPIModule(
argument_spec=argument_spec, argument_spec=argument_spec,
required_one_of=[['name', 'settings']], required_one_of=[['name', 'settings']],
mutually_exclusive=[['name', 'settings']], mutually_exclusive=[['name', 'settings']],

View File

@@ -60,7 +60,7 @@ EXAMPLES = '''
tower_config_file: "~/tower_cli.cfg" tower_config_file: "~/tower_cli.cfg"
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def main(): def main():
@@ -74,7 +74,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
name = module.params.get('name') name = module.params.get('name')

View File

@@ -117,7 +117,7 @@ tower_token:
returned: on successful create returned: on successful create
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def return_token(module, last_response): def return_token(module, last_response):
@@ -143,7 +143,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule( module = TowerAPIModule(
argument_spec=argument_spec, argument_spec=argument_spec,
mutually_exclusive=[ mutually_exclusive=[
('existing_token', 'existing_token_id'), ('existing_token', 'existing_token_id'),

View File

@@ -102,7 +102,7 @@ EXAMPLES = '''
tower_config_file: "~/tower_cli.cfg" tower_config_file: "~/tower_cli.cfg"
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def main(): def main():
@@ -119,7 +119,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
username = module.params.get('username') username = module.params.get('username')

View File

@@ -137,7 +137,7 @@ EXAMPLES = '''
organization: Default organization: Default
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
import json import json
@@ -176,7 +176,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
name = module.params.get('name') name = module.params.get('name')

View File

@@ -157,7 +157,7 @@ EXAMPLES = '''
- my-first-node - my-first-node
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def main(): def main():
@@ -185,7 +185,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
identifier = module.params.get('identifier') identifier = module.params.get('identifier')

View File

@@ -91,7 +91,7 @@ EXAMPLES = '''
wait: False wait: False
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
import json import json
import time import time
@@ -111,7 +111,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
optional_args = {} optional_args = {}
# Extract our parameters # Extract our parameters

View File

@@ -108,8 +108,8 @@ EXAMPLES = '''
RETURN = ''' # ''' RETURN = ''' # '''
from ..module_utils.ansible_tower import ( from ..module_utils.tower_legacy import (
TowerModule, TowerLegacyModule,
tower_auth_config, tower_auth_config,
tower_check_mode tower_check_mode
) )
@@ -140,7 +140,7 @@ def main():
state=dict(choices=['present', 'absent'], default='present'), state=dict(choices=['present', 'absent'], default='present'),
) )
module = TowerModule( module = TowerLegacyModule(
argument_spec=argument_spec, argument_spec=argument_spec,
supports_check_mode=False supports_check_mode=False
) )

View File

@@ -10,7 +10,7 @@ from contextlib import redirect_stdout, suppress
from unittest import mock from unittest import mock
import logging import logging
from requests.models import Response from requests.models import Response, PreparedRequest
import pytest import pytest
@@ -23,6 +23,14 @@ try:
except ImportError: except ImportError:
HAS_TOWER_CLI = False HAS_TOWER_CLI = False
try:
# Because awxkit will be a directory at the root of this makefile and we are using python3, import awxkit will work even if its not installed.
# However, awxkit will not contain api whih causes a stack failure down on line 170 when we try to mock it.
# So here we are importing awxkit.api to prevent that. Then you only get an error on tests for awxkit functionality.
import awxkit.api
HAS_AWX_KIT = True
except ImportError:
HAS_AWX_KIT = False
logger = logging.getLogger('awx.main.tests') logger = logging.getLogger('awx.main.tests')
@@ -90,7 +98,8 @@ def run_module(request, collection_import):
if 'params' in kwargs and method == 'GET': if 'params' in kwargs and method == 'GET':
# query params for GET are handled a bit differently by # query params for GET are handled a bit differently by
# tower-cli and python requests as opposed to REST framework APIRequestFactory # tower-cli and python requests as opposed to REST framework APIRequestFactory
kwargs_copy.setdefault('data', {}) if not kwargs_copy.get('data'):
kwargs_copy['data'] = {}
if isinstance(kwargs['params'], dict): if isinstance(kwargs['params'], dict):
kwargs_copy['data'].update(kwargs['params']) kwargs_copy['data'].update(kwargs['params'])
elif isinstance(kwargs['params'], list): elif isinstance(kwargs['params'], list):
@@ -117,6 +126,8 @@ def run_module(request, collection_import):
request_user.username, resp.status_code request_user.username, resp.status_code
) )
resp.request = PreparedRequest()
resp.request.prepare(method=method, url=url)
return resp return resp
def new_open(self, method, url, **kwargs): def new_open(self, method, url, **kwargs):
@@ -142,11 +153,22 @@ def run_module(request, collection_import):
def mock_load_params(self): def mock_load_params(self):
self.params = module_params self.params = module_params
with mock.patch.object(resource_module.TowerModule, '_load_params', new=mock_load_params): if getattr(resource_module, 'TowerAWXKitModule', None):
resource_class = resource_module.TowerAWXKitModule
elif getattr(resource_module, 'TowerAPIModule', None):
resource_class = resource_module.TowerAPIModule
elif getattr(resource_module, 'TowerLegacyModule', None):
resource_class = resource_module.TowerLegacyModule
else:
raise("The module has neither a TowerLegacyModule, TowerAWXKitModule or a TowerAPIModule")
with mock.patch.object(resource_class, '_load_params', new=mock_load_params):
# Call the test utility (like a mock server) instead of issuing HTTP requests # Call the test utility (like a mock server) instead of issuing HTTP requests
with mock.patch('ansible.module_utils.urls.Request.open', new=new_open): with mock.patch('ansible.module_utils.urls.Request.open', new=new_open):
if HAS_TOWER_CLI: if HAS_TOWER_CLI:
tower_cli_mgr = mock.patch('tower_cli.api.Session.request', new=new_request) tower_cli_mgr = mock.patch('tower_cli.api.Session.request', new=new_request)
elif HAS_AWX_KIT:
tower_cli_mgr = mock.patch('awxkit.api.client.requests.Session.request', new=new_request)
else: else:
tower_cli_mgr = suppress() tower_cli_mgr = suppress()
with tower_cli_mgr: with tower_cli_mgr:

View File

@@ -30,12 +30,12 @@ def mock_ping_response(self, method, url, **kwargs):
def test_version_warning(collection_import, silence_warning): def test_version_warning(collection_import, silence_warning):
TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule
cli_data = {'ANSIBLE_MODULE_ARGS': {}} cli_data = {'ANSIBLE_MODULE_ARGS': {}}
testargs = ['module_file2.py', json.dumps(cli_data)] testargs = ['module_file2.py', json.dumps(cli_data)]
with mock.patch.object(sys, 'argv', testargs): with mock.patch.object(sys, 'argv', testargs):
with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response): with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response):
my_module = TowerModule(argument_spec=dict()) my_module = TowerAPIModule(argument_spec=dict())
my_module._COLLECTION_VERSION = "1.0.0" my_module._COLLECTION_VERSION = "1.0.0"
my_module._COLLECTION_TYPE = "not-junk" my_module._COLLECTION_TYPE = "not-junk"
my_module.collection_to_version['not-junk'] = 'not-junk' my_module.collection_to_version['not-junk'] = 'not-junk'
@@ -46,12 +46,12 @@ def test_version_warning(collection_import, silence_warning):
def test_type_warning(collection_import, silence_warning): def test_type_warning(collection_import, silence_warning):
TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule
cli_data = {'ANSIBLE_MODULE_ARGS': {}} cli_data = {'ANSIBLE_MODULE_ARGS': {}}
testargs = ['module_file2.py', json.dumps(cli_data)] testargs = ['module_file2.py', json.dumps(cli_data)]
with mock.patch.object(sys, 'argv', testargs): with mock.patch.object(sys, 'argv', testargs):
with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response): with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response):
my_module = TowerModule(argument_spec={}) my_module = TowerAPIModule(argument_spec={})
my_module._COLLECTION_VERSION = "1.2.3" my_module._COLLECTION_VERSION = "1.2.3"
my_module._COLLECTION_TYPE = "junk" my_module._COLLECTION_TYPE = "junk"
my_module.collection_to_version['junk'] = 'junk' my_module.collection_to_version['junk'] = 'junk'
@@ -63,7 +63,7 @@ def test_type_warning(collection_import, silence_warning):
def test_duplicate_config(collection_import, silence_warning): def test_duplicate_config(collection_import, silence_warning):
# imports done here because of PATH issues unique to this test suite # imports done here because of PATH issues unique to this test suite
TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule
data = { data = {
'name': 'zigzoom', 'name': 'zigzoom',
'zig': 'zoom', 'zig': 'zoom',
@@ -71,12 +71,12 @@ def test_duplicate_config(collection_import, silence_warning):
'tower_config_file': 'my_config' 'tower_config_file': 'my_config'
} }
with mock.patch.object(TowerModule, 'load_config') as mock_load: with mock.patch.object(TowerAPIModule, 'load_config') as mock_load:
argument_spec = dict( argument_spec = dict(
name=dict(required=True), name=dict(required=True),
zig=dict(type='str'), zig=dict(type='str'),
) )
TowerModule(argument_spec=argument_spec, direct_params=data) TowerAPIModule(argument_spec=argument_spec, direct_params=data)
assert mock_load.mock_calls[-1] == mock.call('my_config') assert mock_load.mock_calls[-1] == mock.call('my_config')
silence_warning.assert_called_once_with( silence_warning.assert_called_once_with(
@@ -92,8 +92,8 @@ def test_no_templated_values(collection_import):
Those replacements should happen at build time, so they should not be Those replacements should happen at build time, so they should not be
checked into source. checked into source.
""" """
TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule
assert TowerModule._COLLECTION_VERSION == "0.0.1-devel", ( assert TowerAPIModule._COLLECTION_VERSION == "0.0.1-devel", (
'The collection version is templated when the collection is built ' 'The collection version is templated when the collection is built '
'and the code should retain the placeholder of "0.0.1-devel".' 'and the code should retain the placeholder of "0.0.1-devel".'
) )

View File

@@ -0,0 +1 @@
skip/python2

View File

@@ -0,0 +1,77 @@
---
- name: Generate a random string for test
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names
set_fact:
org_name1: "AWX-Collection-tests-tower_export-organization-{{ test_id }}"
org_name2: "AWX-Collection-tests-tower_export-organization2-{{ test_id }}"
inventory_name1: "AWX-Collection-tests-tower_export-inv1-{{ test_id }}"
- block:
- name: Create some organizations
tower_organization:
name: "{{ item }}"
loop:
- "{{ org_name1 }}"
- "{{ org_name2 }}"
- name: Create an inventory
tower_inventory:
name: "{{ inventory_name1 }}"
organization: "{{ org_name1 }}"
- name: Export all tower assets
tower_export:
all: true
register: all_assets
- assert:
that:
- all_assets is not changed
- all_assets is successful
- all_assets['assets']['organizations'] | length() >= 2
- name: Export all inventories
tower_export:
inventory: 'all'
register: inventory_export
- assert:
that:
- inventory_export is successful
- inventory_export is not changed
- inventory_export['assets']['inventory'] | length() >= 1
- "'organizations' not in inventory_export['assets']"
# This mimics the example in the module
- name: Export an all and a specific
tower_export:
inventory: 'all'
organizations: "{{ org_name1 }}"
register: mixed_export
- assert:
that:
- mixed_export is successful
- mixed_export is not changed
- mixed_export['assets']['inventory'] | length() >= 1
- mixed_export['assets']['organizations'] | length() == 1
- "'workflow_job_templates' not in mixed_export['assets']"
always:
- name: Remove our inventory
tower_inventory:
name: "{{ inventory_name1 }}"
organization: "{{ org_name1 }}"
state: absent
- name: Remove test organizations
tower_organization:
name: "{{ item }}"
state: absent
loop:
- "{{ org_name1 }}"
- "{{ org_name2 }}"

View File

@@ -0,0 +1 @@
skip/python2

View File

@@ -0,0 +1,108 @@
---
- name: Generate a random string for test
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names
set_fact:
org_name1: "AWX-Collection-tests-tower_import-organization-{{ test_id }}"
org_name2: "AWX-Collection-tests-tower_import-organization2-{{ test_id }}"
- block:
- name: "Import something"
tower_import:
assets:
organizations:
- name: "{{ org_name1 }}"
description: ""
max_hosts: 0
custom_virtualenv: null
related:
notification_templates: []
notification_templates_started: []
notification_templates_success: []
notification_templates_error: []
notification_templates_approvals: []
natural_key:
name: "Default"
type: "organization"
register: import_output
- assert:
that:
- import_output is changed
- name: "Import something again (awxkit is not idempotent, this tests a failure)"
tower_import:
assets:
organizations:
- name: "{{ org_name1 }}"
description: ""
max_hosts: 0
custom_virtualenv: null
related:
notification_templates: []
notification_templates_started: []
notification_templates_success: []
notification_templates_error: []
notification_templates_approvals: []
natural_key:
name: "Default"
type: "organization"
register: import_output
ignore_errors: true
- assert:
that:
- import_output is failed
- "'Organization with this Name already exists' in import_output.msg"
- name: "Write out a json file"
copy:
content: |
{
"organizations": [
{
"name": "{{ org_name2 }}",
"description": "",
"max_hosts": 0,
"custom_virtualenv": null,
"related": {
"notification_templates": [],
"notification_templates_started": [],
"notification_templates_success": [],
"notification_templates_error": [],
"notification_templates_approvals": []
},
"natural_key": {
"name": "Default",
"type": "organization"
}
}
]
}
dest: ./org.json
- name: "Load assets from a file"
tower_import:
assets: "{{ lookup('file', 'org.json') | from_json() }}"
register: import_output
- assert:
that:
- import_output is changed
always:
- name: Remove organizations
tower_organization:
name: "{{ item }}"
state: absent
loop:
- "{{ org_name1 }}"
- "{{ org_name2 }}"
- name: Delete org.json
file:
path: ./org.json
state: absent

View File

@@ -96,7 +96,7 @@ EXAMPLES = '''
{% endif %} {% endif %}
''' '''
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerAPIModule
def main(): def main():
@@ -142,7 +142,7 @@ def main():
) )
# Create a module for ourselves # Create a module for ourselves
module = TowerModule(argument_spec=argument_spec) module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters # Extract our parameters
{% for option in item['json']['actions']['POST'] %} {% for option in item['json']['actions']['POST'] %}