[last PR stuff] + Add warning if configs specified in 2 params (#5)

* Lean on API validation for tower_inventory_source arg errors

used for
 - validating needed credential is given
 - missing source_project for scm sources

* Add warning when config is specified in 2 places

Fix up unit tests, address multiple comments re: backwards compatibility, redundant methods, etc.

Update new_name and variables parameters, update unit tests
This commit is contained in:
Alan Rominger
2020-02-18 16:02:05 -05:00
committed by beeankha
parent 2e4e687d69
commit 768280c9ba
16 changed files with 219 additions and 243 deletions

View File

@@ -59,20 +59,8 @@ class TowerModule(AnsibleModule):
self.json_output = {'changed': False}
# We have to take off mutually_exclusive_if in order to init with Ansible
mutually_exclusive_if = kwargs.pop('mutually_exclusive_if', None)
super(TowerModule, self).__init__(argument_spec=args, **kwargs)
# Eventually, we would like to push this as a feature to Ansible core for others to use...
# Test mutually_exclusive if
if mutually_exclusive_if:
for (var_name, var_value, exclusive_names) in mutually_exclusive_if:
if self.params.get(var_name) == var_value:
for excluded_param_name in exclusive_names:
if self.params.get(excluded_param_name) is not None:
self.fail_json(msg='Arguments {0} can not be set if source is {1}'.format(', '.join(exclusive_names), var_value))
self.load_config_files()
# Parameters specified on command line will override settings in any config
@@ -125,7 +113,17 @@ class TowerModule(AnsibleModule):
# 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
@@ -620,30 +618,3 @@ class TowerModule(AnsibleModule):
return False
else:
return True
def load_variables_if_file_specified(self, vars_value, var_name):
if not vars_value.startswith('@'):
return vars_value
if not HAS_YAML:
self.fail_json(msg=self.missing_required_lib('yaml'))
file_name = None
file_content = None
try:
file_name = expanduser(vars_value[1:])
with open(file_name, 'r') as f:
file_content = f.read()
except Exception as e:
self.fail_json(msg="Failed to load file {0} for {1} : {2}".format(file_name, var_name, e))
try:
vars_value = yaml.safe_load(file_content)
except yaml.YAMLError:
# Maybe it wasn't a YAML structure... lets try JSON
try:
vars_value = loads(file_content)
except ValueError:
self.fail_json(msg="Failed to load file {0} specifed by {1} as yaml or json".format(file_name, var_name))
return dumps(vars_value)

View File

@@ -144,10 +144,6 @@ def main():
}
})
# Add entries to json_output to match old module
module.json_output['credential_type'] = name
module.json_output['state'] = state
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)

View File

@@ -44,8 +44,8 @@ options:
type: str
variables:
description:
- Variables to use for the group, use C(@) for a file.
type: str
- Variables to use for the group.
type: dict
state:
description:
- Desired state of the resource.
@@ -72,6 +72,7 @@ EXAMPLES = '''
'''
from ..module_utils.tower_api import TowerModule
import json
def main():
@@ -81,7 +82,7 @@ def main():
new_name=dict(required=False),
description=dict(),
inventory=dict(required=True),
variables=dict(),
variables=dict(type='dict', required=False),
state=dict(choices=['present', 'absent'], default='present'),
)
@@ -107,10 +108,6 @@ def main():
}
})
# If the variables were specified as a file, load them
if variables:
variables = module.load_variables_if_file_specified(variables, 'variables')
# Create the data that gets sent for create and update
group_fields = {
'name': new_name if new_name else name,
@@ -119,7 +116,7 @@ def main():
if description:
group_fields['description'] = description
if variables:
group_fields['variables'] = variables
group_fields['variables'] = json.dumps(variables)
if state == 'absent':
# If the state was absent we can let the module delete it if needed, the module will handle exiting from this

View File

@@ -49,8 +49,8 @@ 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.
@@ -80,6 +80,7 @@ EXAMPLES = '''
from ..module_utils.tower_api import TowerModule
import json
def main():
@@ -90,7 +91,7 @@ def main():
description=dict(default=''),
inventory=dict(required=True),
enabled=dict(type='bool', default=True),
variables=dict(default=''),
variables=dict(type='dict', default=''),
state=dict(choices=['present', 'absent'], default='present'),
)
@@ -106,9 +107,6 @@ def main():
state = module.params.get('state')
variables = module.params.get('variables')
if variables:
variables = module.load_variables_if_file_specified(variables, 'variables')
# 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)
@@ -128,7 +126,7 @@ def main():
'enabled': enabled,
}
if variables:
host_fields['variables'] = variables
host_fields['variables'] = json.dumps(variables)
if state == 'absent':
# If the state was absent we can let the module delete it if needed, the module will handle exiting from this

View File

@@ -179,164 +179,20 @@ def main():
state=dict(choices=['present', 'absent'], default='present'),
)
# One question here is do we want to end up supporting this within the ansible module itself (i.e. required if, etc)
# Or do 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
# update_on_project_launch ? ? 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
#
# * - source_vars are labeled environment_vars on project and custom sources
# Create a module for ourselves
module = TowerModule(argument_spec=argument_spec,
supports_check_mode=True,
required_if=[
# We don't want to require source if state is present because
# you might be doing an update to an existing source.
# Later on in the code, we will do a test so that if state: present
# and if we don't have an object, we must have source.
('source', 'scm', ['source_project', 'source_path']),
('source', 'gce', ['credential']),
('source', 'azure_rm', ['credential']),
('source', 'vmware', ['credential']),
('source', 'satellite6', ['credential']),
('source', 'cloudforms', ['credential']),
('source', 'openstack', ['credential']),
('source', 'rhv', ['credential']),
('source', 'tower', ['credential']),
('source', 'custom', ['source_script']),
],
# This is provided by our module, it's not a core thing
mutually_exclusive_if=[
('source', 'scm', ['source_regions',
'instance_filters',
'group_by',
'source_script'
]),
('source', 'ec2', ['source_project',
'source_path',
'update_on_project_launch',
'source_script'
]),
('source', 'gce', ['source_project',
'source_path',
'update_on_project_launch',
'instance_filters',
'group_by',
'source_vars',
'source_script'
]),
('source', 'azure_rm', ['source_project',
'source_path',
'update_on_project_launch',
'instance_filters',
'group_by',
'source_script'
]),
('source', 'vmware', ['source_project', 'source_path', 'update_on_project_launch', 'source_regions', 'source_script']),
('source', 'satellite6', ['source_project',
'source_path',
'update_on_project_launch',
'source_regions',
'instance_filters',
'group_by',
'source_script'
]),
('source', 'cloudforms', ['source_project',
'source_path',
'update_on_project_launch',
'source_regions',
'instance_filters',
'group_by',
'source_script'
]),
('source', 'openstack', ['source_project',
'source_path',
'update_on_project_launch',
'source_regions',
'instance_filters',
'group_by',
'source_script'
]),
('source', 'rhv', ['source_project',
'source_path',
'update_on_project_launch',
'source_regions',
'instance_filters',
'group_by',
'source_vars',
'source_script'
]),
('source', 'tower', ['source_project',
'source_path',
'update_on_project_launch',
'source_regions',
'group_by',
'source_vars',
'source_script'
]),
('source', 'custom', ['source_project',
'source_path',
'update_on_project_launch',
'source_regions',
'instance_filters',
'group_by'
]),
])
module = TowerModule(argument_spec=argument_spec)
optional_vars = {}
# Extract our parameters
name = module.params.get('name')
new_name = module.params.get('new_name')
optional_vars['description'] = module.params.get('description')
inventory = module.params.get('inventory')
optional_vars['source'] = module.params.get('source')
optional_vars['source_path'] = module.params.get('source_path')
source_script = module.params.get('source_script')
optional_vars['source_vars'] = module.params.get('source_vars')
credential = module.params.get('credential')
optional_vars['source_regions'] = module.params.get('source_regions')
optional_vars['instance_filters'] = module.params.get('instance_filters')
optional_vars['group_by'] = module.params.get('group_by')
optional_vars['overwrite'] = module.params.get('overwrite')
optional_vars['overwrite_vars'] = module.params.get('overwrite_vars')
optional_vars['custom_virtualenv'] = module.params.get('custom_virtualenv')
optional_vars['timeout'] = module.params.get('timeout')
optional_vars['verbosity'] = module.params.get('verbosity')
optional_vars['update_on_launch'] = module.params.get('update_on_launch')
optional_vars['update_cache_timeout'] = module.params.get('update_cache_timeout')
source_project = module.params.get('source_project')
optional_vars['update_on_project_update'] = module.params.get('update_on_project_update')
state = module.params.get('state')
# Attempt to JSON encode source vars
if optional_vars['source_vars']:
optional_vars['source_vars'] = dumps(optional_vars['source_vars'])
# 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)
if credential:
optional_vars['credential'] = module.resolve_name_to_id('credentials', credential)
if source_project:
optional_vars['source_project'] = module.resolve_name_to_id('projects', source_project)
if source_script:
optional_vars['source_script'] = module.resolve_name_to_id('inventory_scripts', source_script)
# 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,
@@ -344,19 +200,41 @@ def main():
}
})
# Sanity check on arguments
if state == 'present' and not inventory_source and not optional_vars['source']:
module.fail_json(msg="If creating a new inventory source, the source param must be present")
# Create the data that gets sent for create and update
inventory_source_fields = {
'name': new_name if new_name else name,
'inventory': inventory_id,
}
# Attempt to look up the related items the user specified (these will fail the module if not found)
if credential:
inventory_source_fields['credential'] = module.resolve_name_to_id('credentials', credential)
if source_project:
inventory_source_fields['source_project'] = module.resolve_name_to_id('projects', source_project)
if source_script:
inventory_source_fields['source_script'] = module.resolve_name_to_id('inventory_scripts', source_script)
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'
)
# Layer in all remaining optional information
for field_name in optional_vars:
if optional_vars[field_name]:
inventory_source_fields[field_name] = optional_vars[field_name]
for field_name in OPTIONAL_VARS:
field_val = module.params.get(field_name)
if field_val:
inventory_source_fields[field_name] = field_val
# 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'])
# 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")
if state == 'absent':
# If the state was absent we can let the module delete it if needed, the module will handle exiting from this

View File

@@ -45,7 +45,7 @@ options:
aliases: ['credential']
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