Rick Elrod 0815f935ca
[collection] remove module defaults where API defaults are the same (#13037)
Providing defaults for API parameters where the API already provides
defaults leads to some confusing scenarios, because we end up always
sending those collection-defaulted fields in the request even if the
field isn't provided by the user.

For example, we previously set the `scm_type` default to 'manual' and
someone using the collection to update a project who does not explicitly
include the `scm_type` every time they call the module, would
inadvertently change the `scm_type` of the project back to 'manual'
which is surprising behavior.

This change removes the collection defaults for API parameters, unless
they differed from the API default. We let the API handle the defaults
or otherwise ignore fields not given by the user so that the user does
not end up changing unexpected fields when they use a module.

Signed-off-by: Rick Elrod <rick@elrod.me>
2023-02-01 15:37:08 -06:00

338 lines
11 KiB
Python

#!/usr/bin/python
# coding: utf-8 -*-
# (c) 2017, Wayne Witzel III <wayne@riotousliving.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: job_launch
author: "Wayne Witzel III (@wwitzel3)"
short_description: Launch an Ansible Job.
description:
- Launch an Automation Platform Controller jobs. See
U(https://www.ansible.com/tower) for an overview.
options:
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.
choices: ["run", "check"]
type: str
inventory:
description:
- Inventory to use for the job, only used if prompt for inventory is set.
type: str
organization:
description:
- Organization the job template exists in.
- Used to help lookup the object, cannot be modified using this module.
- If not provided, will lookup by name only, which does not work with duplicates.
type: str
credentials:
description:
- Credential to use for job, only used if prompt for credential is set.
type: list
aliases: ['credential']
elements: str
extra_vars:
description:
- extra_vars to use for the Job Template.
- ask_extra_vars needs to be set to True via job_template module
when creating the Job Template.
type: dict
limit:
description:
- Limit to use for the I(job_template).
type: str
tags:
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
skip_tags:
description:
- Specific tags to skip from the playbook.
type: list
elements: str
verbosity:
description:
- Verbosity level for this job run
type: int
choices: [ 0, 1, 2, 3, 4, 5 ]
diff_mode:
description:
- Show the changes made by Ansible tasks where supported
type: bool
credential_passwords:
description:
- Passwords for credentials which are set to prompt on launch
type: dict
execution_environment:
description:
- Execution environment to use for the job, only used if prompt for execution environment is set.
type: str
forks:
description:
- Forks to use for the job, only used if prompt for forks is set.
type: int
instance_groups:
description:
- Instance groups to use for the job, only used if prompt for instance groups is set.
type: list
elements: str
job_slice_count:
description:
- Job slice count to use for the job, only used if prompt for job slice count is set.
type: int
labels:
description:
- Labels to use for the job, only used if prompt for labels is set.
type: list
elements: str
job_timeout:
description:
- Timeout to use for the job, only used if prompt for timeout is set.
- This parameter is sent through the API to the job.
type: int
wait:
description:
- Wait for the job to complete.
default: False
type: bool
interval:
description:
- The interval to request an update from the controller.
required: False
default: 2
type: float
timeout:
description:
- If waiting for the job to complete this will abort after this
amount of seconds. This happens on the module side.
type: int
extends_documentation_fragment: awx.awx.auth
'''
EXAMPLES = '''
- name: Launch a job
job_launch:
job_template: "My Job Template"
register: job
- name: Launch a job template with extra_vars on remote controller instance
job_launch:
job_template: "My Job Template"
extra_vars:
var1: "My First Variable"
var2: "My Second Variable"
var3: "My Third Variable"
job_type: run
- name: Launch a job with inventory and credential
job_launch:
job_template: "My Job Template"
inventory: "My Inventory"
credential: "My Credential"
register: job
- name: Wait for job max 120s
job_wait:
job_id: "{{ job.id }}"
timeout: 120
'''
RETURN = '''
id:
description: job id of the newly launched job
returned: success
type: int
sample: 86
status:
description: status of newly launched job
returned: success
type: str
sample: pending
'''
from ..module_utils.controller_api import ControllerAPIModule
def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict(
name=dict(required=True, aliases=['job_template']),
job_type=dict(choices=['run', 'check']),
inventory=dict(),
organization=dict(),
# Credentials will be a str instead of a list for backwards compatability
credentials=dict(type='list', aliases=['credential'], elements='str'),
limit=dict(),
tags=dict(type='list', elements='str'),
extra_vars=dict(type='dict'),
scm_branch=dict(),
skip_tags=dict(type='list', elements='str'),
verbosity=dict(type='int', choices=[0, 1, 2, 3, 4, 5]),
diff_mode=dict(type='bool'),
credential_passwords=dict(type='dict', no_log=False),
execution_environment=dict(),
forks=dict(type='int'),
instance_groups=dict(type='list', elements='str'),
job_slice_count=dict(type='int'),
labels=dict(type='list', elements='str'),
job_timeout=dict(type='int'),
wait=dict(default=False, type='bool'),
interval=dict(default=2.0, type='float'),
timeout=dict(type='int'),
)
# Create a module for ourselves
module = ControllerAPIModule(argument_spec=argument_spec)
optional_args = {}
# Extract our parameters
name = module.params.get('name')
inventory = module.params.get('inventory')
organization = module.params.get('organization')
credentials = module.params.get('credentials')
execution_environment = module.params.get('execution_environment')
instance_groups = module.params.get('instance_groups')
labels = module.params.get('labels')
wait = module.params.get('wait')
interval = module.params.get('interval')
timeout = module.params.get('timeout')
for field_name in (
'job_type',
'limit',
'extra_vars',
'scm_branch',
'verbosity',
'diff_mode',
'credential_passwords',
'forks',
'job_slice_count',
'job_timeout',
):
field_val = module.params.get(field_name)
if field_val is not None:
optional_args[field_name] = field_val
# Special treatment of tags parameters
job_tags = module.params.get('tags')
if job_tags is not None:
optional_args['job_tags'] = ",".join(job_tags)
skip_tags = module.params.get('skip_tags')
if skip_tags is not None:
optional_args['skip_tags'] = ",".join(skip_tags)
# job_timeout is special because its actually timeout but we already had a timeout variable
job_timeout = module.params.get('job_timeout')
if job_timeout is not None:
optional_args['timeout'] = job_timeout
# Create a datastructure to pass into our job launch
post_data = {}
for arg_name, arg_value in optional_args.items():
if arg_value:
post_data[arg_name] = arg_value
# 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)
if execution_environment:
post_data['execution_environment'] = module.resolve_name_to_id('execution_environments', execution_environment)
if credentials:
post_data['credentials'] = []
for credential in credentials:
post_data['credentials'].append(module.resolve_name_to_id('credentials', credential))
if labels:
post_data['labels'] = []
for label in labels:
post_data['labels'].append(module.resolve_name_to_id('labels', label))
if instance_groups:
post_data['instance_groups'] = []
for instance_group in instance_groups:
post_data['instance_groups'].append(module.resolve_name_to_id('instance_groups', instance_group))
# Attempt to look up job_template based on the provided name
lookup_data = {}
if organization:
lookup_data['organization'] = module.resolve_name_to_id('organizations', organization)
job_template = module.get_one('job_templates', name_or_id=name, data=lookup_data)
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',
'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, prompt in check_vars_to_prompts.items():
if module.params.get(variable_name) and not job_template[prompt]:
param_errors.append("The field {0} was specified but the job template does not allow for it to be overridden".format(variable_name))
# Check if Either ask_variables_on_launch, or survey_enabled is enabled for use of extra vars.
if module.params.get('extra_vars') and not (job_template['ask_variables_on_launch'] or job_template['survey_enabled']):
param_errors.append("The field extra_vars was specified but the job template does not allow for it to be overridden")
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})
if not wait:
module.exit_json(
**{
'changed': True,
'id': results['json']['id'],
'status': results['json']['status'],
}
)
# Invoke wait function
results = module.wait_on_url(url=results['json']['url'], object_name=name, object_type='Job', timeout=timeout, interval=interval)
module.exit_json(
**{
'changed': True,
'id': results['json']['id'],
'status': results['json']['status'],
}
)
if __name__ == '__main__':
main()