mirror of
https://github.com/ansible/awx.git
synced 2026-01-15 03:40:42 -03:30
Merge pull request #2342 from ansible/workflow_inventory
Workflow level inventory Reviewed-by: https://github.com/softwarefactory-project-zuul[bot]
This commit is contained in:
commit
fccaebdc8e
@ -3599,7 +3599,7 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo
|
||||
class Meta:
|
||||
model = WorkflowJobTemplate
|
||||
fields = ('*', 'extra_vars', 'organization', 'survey_enabled', 'allow_simultaneous',
|
||||
'ask_variables_on_launch',)
|
||||
'ask_variables_on_launch', 'inventory', 'ask_inventory_on_launch',)
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(WorkflowJobTemplateSerializer, self).get_related(obj)
|
||||
@ -3643,7 +3643,8 @@ class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer):
|
||||
model = WorkflowJob
|
||||
fields = ('*', 'workflow_job_template', 'extra_vars', 'allow_simultaneous',
|
||||
'job_template', 'is_sliced_job',
|
||||
'-execution_node', '-event_processing_finished', '-controller_node',)
|
||||
'-execution_node', '-event_processing_finished', '-controller_node',
|
||||
'inventory',)
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(WorkflowJobSerializer, self).get_related(obj)
|
||||
@ -3726,7 +3727,7 @@ class LaunchConfigurationBaseSerializer(BaseSerializer):
|
||||
if obj is None:
|
||||
return ret
|
||||
if 'extra_data' in ret and obj.survey_passwords:
|
||||
ret['extra_data'] = obj.display_extra_data()
|
||||
ret['extra_data'] = obj.display_extra_vars()
|
||||
return ret
|
||||
|
||||
def get_summary_fields(self, obj):
|
||||
@ -4417,37 +4418,63 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
class WorkflowJobLaunchSerializer(BaseSerializer):
|
||||
|
||||
can_start_without_user_input = serializers.BooleanField(read_only=True)
|
||||
defaults = serializers.SerializerMethodField()
|
||||
variables_needed_to_start = serializers.ReadOnlyField()
|
||||
survey_enabled = serializers.SerializerMethodField()
|
||||
extra_vars = VerbatimField(required=False, write_only=True)
|
||||
inventory = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Inventory.objects.all(),
|
||||
required=False, write_only=True
|
||||
)
|
||||
workflow_job_template_data = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = WorkflowJobTemplate
|
||||
fields = ('can_start_without_user_input', 'extra_vars',
|
||||
'survey_enabled', 'variables_needed_to_start',
|
||||
fields = ('ask_inventory_on_launch', 'can_start_without_user_input', 'defaults', 'extra_vars',
|
||||
'inventory', 'survey_enabled', 'variables_needed_to_start',
|
||||
'node_templates_missing', 'node_prompts_rejected',
|
||||
'workflow_job_template_data')
|
||||
'workflow_job_template_data', 'survey_enabled')
|
||||
read_only_fields = ('ask_inventory_on_launch',)
|
||||
|
||||
def get_survey_enabled(self, obj):
|
||||
if obj:
|
||||
return obj.survey_enabled and 'spec' in obj.survey_spec
|
||||
return False
|
||||
|
||||
def get_defaults(self, obj):
|
||||
defaults_dict = {}
|
||||
for field_name in WorkflowJobTemplate.get_ask_mapping().keys():
|
||||
if field_name == 'inventory':
|
||||
defaults_dict[field_name] = dict(
|
||||
name=getattrd(obj, '%s.name' % field_name, None),
|
||||
id=getattrd(obj, '%s.pk' % field_name, None))
|
||||
else:
|
||||
defaults_dict[field_name] = getattr(obj, field_name)
|
||||
return defaults_dict
|
||||
|
||||
def get_workflow_job_template_data(self, obj):
|
||||
return dict(name=obj.name, id=obj.id, description=obj.description)
|
||||
|
||||
def validate(self, attrs):
|
||||
obj = self.instance
|
||||
template = self.instance
|
||||
|
||||
accepted, rejected, errors = obj._accept_or_ignore_job_kwargs(
|
||||
_exclude_errors=['required'],
|
||||
**attrs)
|
||||
accepted, rejected, errors = template._accept_or_ignore_job_kwargs(**attrs)
|
||||
self._ignored_fields = rejected
|
||||
|
||||
WFJT_extra_vars = obj.extra_vars
|
||||
attrs = super(WorkflowJobLaunchSerializer, self).validate(attrs)
|
||||
obj.extra_vars = WFJT_extra_vars
|
||||
return attrs
|
||||
if template.inventory and template.inventory.pending_deletion is True:
|
||||
errors['inventory'] = _("The inventory associated with this Workflow is being deleted.")
|
||||
elif 'inventory' in accepted and accepted['inventory'].pending_deletion:
|
||||
errors['inventory'] = _("The provided inventory is being deleted.")
|
||||
|
||||
if errors:
|
||||
raise serializers.ValidationError(errors)
|
||||
|
||||
WFJT_extra_vars = template.extra_vars
|
||||
WFJT_inventory = template.inventory
|
||||
super(WorkflowJobLaunchSerializer, self).validate(attrs)
|
||||
template.extra_vars = WFJT_extra_vars
|
||||
template.inventory = WFJT_inventory
|
||||
return accepted
|
||||
|
||||
|
||||
class NotificationTemplateSerializer(BaseSerializer):
|
||||
|
||||
@ -3106,23 +3106,31 @@ class WorkflowJobTemplateLaunch(WorkflowsEnforcementMixin, RetrieveAPIView):
|
||||
extra_vars.setdefault(v, u'')
|
||||
if extra_vars:
|
||||
data['extra_vars'] = extra_vars
|
||||
if obj.ask_inventory_on_launch:
|
||||
data['inventory'] = obj.inventory_id
|
||||
else:
|
||||
data.pop('inventory', None)
|
||||
return data
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
|
||||
if 'inventory_id' in request.data:
|
||||
request.data['inventory'] = request.data['inventory_id']
|
||||
|
||||
serializer = self.serializer_class(instance=obj, data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
prompted_fields, ignored_fields, errors = obj._accept_or_ignore_job_kwargs(**request.data)
|
||||
if not request.user.can_access(JobLaunchConfig, 'add', serializer.validated_data, template=obj):
|
||||
raise PermissionDenied()
|
||||
|
||||
new_job = obj.create_unified_job(**prompted_fields)
|
||||
new_job = obj.create_unified_job(**serializer.validated_data)
|
||||
new_job.signal_start()
|
||||
|
||||
data = OrderedDict()
|
||||
data['workflow_job'] = new_job.id
|
||||
data['ignored_fields'] = ignored_fields
|
||||
data['ignored_fields'] = serializer._ignored_fields
|
||||
data.update(WorkflowJobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job))
|
||||
headers = {'Location': new_job.get_absolute_url(request)}
|
||||
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
@ -1835,8 +1835,10 @@ class WorkflowJobTemplateAccess(BaseAccess):
|
||||
if 'survey_enabled' in data and data['survey_enabled']:
|
||||
self.check_license(feature='surveys')
|
||||
|
||||
return self.check_related('organization', Organization, data, role_field='workflow_admin_role',
|
||||
mandatory=True)
|
||||
return (
|
||||
self.check_related('organization', Organization, data, role_field='workflow_admin_role', mandatory=True) and
|
||||
self.check_related('inventory', Inventory, data, role_field='use_role')
|
||||
)
|
||||
|
||||
def can_copy(self, obj):
|
||||
if self.save_messages:
|
||||
@ -1890,8 +1892,11 @@ class WorkflowJobTemplateAccess(BaseAccess):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
|
||||
return (self.check_related('organization', Organization, data, role_field='workflow_admin_role', obj=obj) and
|
||||
self.user in obj.admin_role)
|
||||
return (
|
||||
self.check_related('organization', Organization, data, role_field='workflow_admin_role', obj=obj) and
|
||||
self.check_related('inventory', Inventory, data, role_field='use_role', obj=obj) and
|
||||
self.user in obj.admin_role
|
||||
)
|
||||
|
||||
def can_delete(self, obj):
|
||||
return self.user.is_superuser or self.user in obj.admin_role
|
||||
@ -1949,19 +1954,29 @@ class WorkflowJobAccess(BaseAccess):
|
||||
if not template:
|
||||
return False
|
||||
|
||||
# If job was launched by another user, it could have survey passwords
|
||||
if obj.created_by_id != self.user.pk:
|
||||
# Obtain prompts used to start original job
|
||||
JobLaunchConfig = obj._meta.get_field('launch_config').related_model
|
||||
try:
|
||||
config = JobLaunchConfig.objects.get(job=obj)
|
||||
except JobLaunchConfig.DoesNotExist:
|
||||
config = None
|
||||
# Obtain prompts used to start original job
|
||||
JobLaunchConfig = obj._meta.get_field('launch_config').related_model
|
||||
try:
|
||||
config = JobLaunchConfig.objects.get(job=obj)
|
||||
except JobLaunchConfig.DoesNotExist:
|
||||
if self.save_messages:
|
||||
self.messages['detail'] = _('Workflow Job was launched with unknown prompts.')
|
||||
return False
|
||||
|
||||
if config is None or config.prompts_dict():
|
||||
# Check if access to prompts to prevent relaunch
|
||||
if config.prompts_dict():
|
||||
if obj.created_by_id != self.user.pk:
|
||||
if self.save_messages:
|
||||
self.messages['detail'] = _('Job was launched with prompts provided by another user.')
|
||||
return False
|
||||
if not JobLaunchConfigAccess(self.user).can_add({'reference_obj': config}):
|
||||
if self.save_messages:
|
||||
self.messages['detail'] = _('Job was launched with prompts you lack access to.')
|
||||
return False
|
||||
if config.has_unprompted(template):
|
||||
if self.save_messages:
|
||||
self.messages['detail'] = _('Job was launched with prompts no longer accepted.')
|
||||
return False
|
||||
|
||||
# execute permission to WFJT is mandatory for any relaunch
|
||||
return (self.user in template.execute_role)
|
||||
|
||||
37
awx/main/migrations/0053_v340_workflow_inventory.py
Normal file
37
awx/main/migrations/0053_v340_workflow_inventory.py
Normal file
@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-09-27 19:50
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import awx.main.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0052_v340_remove_project_scm_delete_on_next_update'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='workflowjob',
|
||||
name='char_prompts',
|
||||
field=awx.main.fields.JSONField(blank=True, default={}),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowjob',
|
||||
name='inventory',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workflowjobs', to='main.Inventory'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowjobtemplate',
|
||||
name='ask_inventory_on_launch',
|
||||
field=awx.main.fields.AskForField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowjobtemplate',
|
||||
name='inventory',
|
||||
field=models.ForeignKey(blank=True, default=None, help_text='Inventory applied to all job templates in workflow that prompt for inventory.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workflowjobtemplates', to='main.Inventory'),
|
||||
),
|
||||
]
|
||||
@ -34,7 +34,7 @@ from awx.main.models.notifications import (
|
||||
JobNotificationMixin,
|
||||
)
|
||||
from awx.main.utils import parse_yaml_or_json, getattr_dne
|
||||
from awx.main.fields import ImplicitRoleField
|
||||
from awx.main.fields import ImplicitRoleField, JSONField, AskForField
|
||||
from awx.main.models.mixins import (
|
||||
ResourceMixin,
|
||||
SurveyJobTemplateMixin,
|
||||
@ -43,7 +43,6 @@ from awx.main.models.mixins import (
|
||||
CustomVirtualEnvMixin,
|
||||
RelatedJobsMixin,
|
||||
)
|
||||
from awx.main.fields import JSONField, AskForField
|
||||
|
||||
|
||||
logger = logging.getLogger('awx.main.models.jobs')
|
||||
@ -895,19 +894,19 @@ class NullablePromptPsuedoField(object):
|
||||
instance.char_prompts[self.field_name] = value
|
||||
|
||||
|
||||
class LaunchTimeConfig(BaseModel):
|
||||
class LaunchTimeConfigBase(BaseModel):
|
||||
'''
|
||||
Common model for all objects that save details of a saved launch config
|
||||
WFJT / WJ nodes, schedules, and job launch configs (not all implemented yet)
|
||||
Needed as separate class from LaunchTimeConfig because some models
|
||||
use `extra_data` and some use `extra_vars`. We cannot change the API,
|
||||
so we force fake it in the model definitions
|
||||
- model defines extra_vars - use this class
|
||||
- model needs to use extra data - use LaunchTimeConfig
|
||||
Use this for models which are SurveyMixins and UnifiedJobs or Templates
|
||||
'''
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
# Prompting-related fields that have to be handled as special cases
|
||||
credentials = models.ManyToManyField(
|
||||
'Credential',
|
||||
related_name='%(class)ss'
|
||||
)
|
||||
inventory = models.ForeignKey(
|
||||
'Inventory',
|
||||
related_name='%(class)ss',
|
||||
@ -916,15 +915,6 @@ class LaunchTimeConfig(BaseModel):
|
||||
default=None,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
extra_data = JSONField(
|
||||
blank=True,
|
||||
default={}
|
||||
)
|
||||
survey_passwords = prevent_search(JSONField(
|
||||
blank=True,
|
||||
default={},
|
||||
editable=False,
|
||||
))
|
||||
# All standard fields are stored in this dictionary field
|
||||
# This is a solution to the nullable CharField problem, specific to prompting
|
||||
char_prompts = JSONField(
|
||||
@ -934,6 +924,7 @@ class LaunchTimeConfig(BaseModel):
|
||||
|
||||
def prompts_dict(self, display=False):
|
||||
data = {}
|
||||
# Some types may have different prompts, but always subset of JT prompts
|
||||
for prompt_name in JobTemplate.get_ask_mapping().keys():
|
||||
try:
|
||||
field = self._meta.get_field(prompt_name)
|
||||
@ -946,11 +937,11 @@ class LaunchTimeConfig(BaseModel):
|
||||
if len(prompt_val) > 0:
|
||||
data[prompt_name] = prompt_val
|
||||
elif prompt_name == 'extra_vars':
|
||||
if self.extra_data:
|
||||
if self.extra_vars:
|
||||
if display:
|
||||
data[prompt_name] = self.display_extra_data()
|
||||
data[prompt_name] = self.display_extra_vars()
|
||||
else:
|
||||
data[prompt_name] = self.extra_data
|
||||
data[prompt_name] = self.extra_vars
|
||||
if self.survey_passwords and not display:
|
||||
data['survey_passwords'] = self.survey_passwords
|
||||
else:
|
||||
@ -959,18 +950,18 @@ class LaunchTimeConfig(BaseModel):
|
||||
data[prompt_name] = prompt_val
|
||||
return data
|
||||
|
||||
def display_extra_data(self):
|
||||
def display_extra_vars(self):
|
||||
'''
|
||||
Hides fields marked as passwords in survey.
|
||||
'''
|
||||
if self.survey_passwords:
|
||||
extra_data = parse_yaml_or_json(self.extra_data).copy()
|
||||
extra_vars = parse_yaml_or_json(self.extra_vars).copy()
|
||||
for key, value in self.survey_passwords.items():
|
||||
if key in extra_data:
|
||||
extra_data[key] = value
|
||||
return extra_data
|
||||
if key in extra_vars:
|
||||
extra_vars[key] = value
|
||||
return extra_vars
|
||||
else:
|
||||
return self.extra_data
|
||||
return self.extra_vars
|
||||
|
||||
@property
|
||||
def _credential(self):
|
||||
@ -994,7 +985,42 @@ class LaunchTimeConfig(BaseModel):
|
||||
return None
|
||||
|
||||
|
||||
class LaunchTimeConfig(LaunchTimeConfigBase):
|
||||
'''
|
||||
Common model for all objects that save details of a saved launch config
|
||||
WFJT / WJ nodes, schedules, and job launch configs (not all implemented yet)
|
||||
'''
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
# Special case prompting fields, even more special than the other ones
|
||||
extra_data = JSONField(
|
||||
blank=True,
|
||||
default={}
|
||||
)
|
||||
survey_passwords = prevent_search(JSONField(
|
||||
blank=True,
|
||||
default={},
|
||||
editable=False,
|
||||
))
|
||||
# Credentials needed for non-unified job / unified JT models
|
||||
credentials = models.ManyToManyField(
|
||||
'Credential',
|
||||
related_name='%(class)ss'
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_vars(self):
|
||||
return self.extra_data
|
||||
|
||||
@extra_vars.setter
|
||||
def extra_vars(self, extra_vars):
|
||||
self.extra_data = extra_vars
|
||||
|
||||
|
||||
for field_name in JobTemplate.get_ask_mapping().keys():
|
||||
if field_name == 'extra_vars':
|
||||
continue
|
||||
try:
|
||||
LaunchTimeConfig._meta.get_field(field_name)
|
||||
except FieldDoesNotExist:
|
||||
|
||||
@ -301,14 +301,22 @@ class SurveyJobTemplateMixin(models.Model):
|
||||
accepted.update(extra_vars)
|
||||
extra_vars = {}
|
||||
|
||||
if extra_vars:
|
||||
# Prune the prompted variables for those identical to template
|
||||
tmp_extra_vars = self.extra_vars_dict
|
||||
for key in (set(tmp_extra_vars.keys()) & set(extra_vars.keys())):
|
||||
if tmp_extra_vars[key] == extra_vars[key]:
|
||||
extra_vars.pop(key)
|
||||
|
||||
if extra_vars:
|
||||
# Leftover extra_vars, keys provided that are not allowed
|
||||
rejected.update(extra_vars)
|
||||
# ignored variables does not block manual launch
|
||||
if 'prompts' not in _exclude_errors:
|
||||
errors['extra_vars'] = [_('Variables {list_of_keys} are not allowed on launch. Check the Prompt on Launch setting '+
|
||||
'on the Job Template to include Extra Variables.').format(
|
||||
list_of_keys=', '.join(extra_vars.keys()))]
|
||||
'on the {model_name} to include Extra Variables.').format(
|
||||
list_of_keys=six.text_type(', ').join([six.text_type(key) for key in extra_vars.keys()]),
|
||||
model_name=self._meta.verbose_name.title())]
|
||||
|
||||
return (accepted, rejected, errors)
|
||||
|
||||
|
||||
@ -24,14 +24,14 @@ from awx.main.models.rbac import (
|
||||
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||
ROLE_SINGLETON_SYSTEM_AUDITOR
|
||||
)
|
||||
from awx.main.fields import ImplicitRoleField
|
||||
from awx.main.fields import ImplicitRoleField, AskForField
|
||||
from awx.main.models.mixins import (
|
||||
ResourceMixin,
|
||||
SurveyJobTemplateMixin,
|
||||
SurveyJobMixin,
|
||||
RelatedJobsMixin,
|
||||
)
|
||||
from awx.main.models.jobs import LaunchTimeConfig, JobTemplate
|
||||
from awx.main.models.jobs import LaunchTimeConfigBase, LaunchTimeConfig, JobTemplate
|
||||
from awx.main.models.credential import Credential
|
||||
from awx.main.redact import REPLACE_STR
|
||||
from awx.main.fields import JSONField
|
||||
@ -188,6 +188,16 @@ class WorkflowJobNode(WorkflowNodeBase):
|
||||
def get_absolute_url(self, request=None):
|
||||
return reverse('api:workflow_job_node_detail', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
def prompts_dict(self, *args, **kwargs):
|
||||
r = super(WorkflowJobNode, self).prompts_dict(*args, **kwargs)
|
||||
# Explanation - WFJT extra_vars still break pattern, so they are not
|
||||
# put through prompts processing, but inventory is only accepted
|
||||
# if JT prompts for it, so it goes through this mechanism
|
||||
if self.workflow_job and self.workflow_job.inventory_id:
|
||||
# workflow job inventory takes precedence
|
||||
r['inventory'] = self.workflow_job.inventory
|
||||
return r
|
||||
|
||||
def get_job_kwargs(self):
|
||||
'''
|
||||
In advance of creating a new unified job as part of a workflow,
|
||||
@ -290,7 +300,8 @@ class WorkflowJobOptions(BaseModel):
|
||||
@classmethod
|
||||
def _get_unified_job_field_names(cls):
|
||||
return set(f.name for f in WorkflowJobOptions._meta.fields) | set(
|
||||
['name', 'description', 'schedule', 'survey_passwords', 'labels']
|
||||
# NOTE: if other prompts are added to WFJT, put fields in WJOptions, remove inventory
|
||||
['name', 'description', 'schedule', 'survey_passwords', 'labels', 'inventory']
|
||||
)
|
||||
|
||||
def _create_workflow_nodes(self, old_node_list, user=None):
|
||||
@ -342,6 +353,19 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='workflows',
|
||||
)
|
||||
inventory = models.ForeignKey(
|
||||
'Inventory',
|
||||
related_name='%(class)ss',
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
on_delete=models.SET_NULL,
|
||||
help_text=_('Inventory applied to all job templates in workflow that prompt for inventory.'),
|
||||
)
|
||||
ask_inventory_on_launch = AskForField(
|
||||
blank=True,
|
||||
default=False,
|
||||
)
|
||||
admin_role = ImplicitRoleField(parent_role=[
|
||||
'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||
'organization.workflow_admin_role'
|
||||
@ -396,27 +420,45 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
|
||||
workflow_job.copy_nodes_from_original(original=self)
|
||||
return workflow_job
|
||||
|
||||
def _accept_or_ignore_job_kwargs(self, _exclude_errors=(), **kwargs):
|
||||
def _accept_or_ignore_job_kwargs(self, **kwargs):
|
||||
exclude_errors = kwargs.pop('_exclude_errors', [])
|
||||
prompted_data = {}
|
||||
rejected_data = {}
|
||||
accepted_vars, rejected_vars, errors_dict = self.accept_or_ignore_variables(
|
||||
kwargs.get('extra_vars', {}),
|
||||
_exclude_errors=exclude_errors,
|
||||
extra_passwords=kwargs.get('survey_passwords', {}))
|
||||
if accepted_vars:
|
||||
prompted_data['extra_vars'] = accepted_vars
|
||||
if rejected_vars:
|
||||
rejected_data['extra_vars'] = rejected_vars
|
||||
errors_dict = {}
|
||||
|
||||
# WFJTs do not behave like JTs, it can not accept inventory, credential, etc.
|
||||
bad_kwargs = kwargs.copy()
|
||||
bad_kwargs.pop('extra_vars', None)
|
||||
bad_kwargs.pop('survey_passwords', None)
|
||||
if bad_kwargs:
|
||||
rejected_data.update(bad_kwargs)
|
||||
for field in bad_kwargs:
|
||||
errors_dict[field] = _('Field is not allowed for use in workflows.')
|
||||
# Handle all the fields that have prompting rules
|
||||
# NOTE: If WFJTs prompt for other things, this logic can be combined with jobs
|
||||
for field_name, ask_field_name in self.get_ask_mapping().items():
|
||||
|
||||
if field_name == 'extra_vars':
|
||||
accepted_vars, rejected_vars, vars_errors = self.accept_or_ignore_variables(
|
||||
kwargs.get('extra_vars', {}),
|
||||
_exclude_errors=exclude_errors,
|
||||
extra_passwords=kwargs.get('survey_passwords', {}))
|
||||
if accepted_vars:
|
||||
prompted_data['extra_vars'] = accepted_vars
|
||||
if rejected_vars:
|
||||
rejected_data['extra_vars'] = rejected_vars
|
||||
errors_dict.update(vars_errors)
|
||||
continue
|
||||
|
||||
if field_name not in kwargs:
|
||||
continue
|
||||
new_value = kwargs[field_name]
|
||||
old_value = getattr(self, field_name)
|
||||
|
||||
if new_value == old_value:
|
||||
continue # no-op case: Counted as neither accepted or ignored
|
||||
elif getattr(self, ask_field_name):
|
||||
# accepted prompt
|
||||
prompted_data[field_name] = new_value
|
||||
else:
|
||||
# unprompted - template is not configured to accept field on launch
|
||||
rejected_data[field_name] = new_value
|
||||
# Not considered an error for manual launch, to support old
|
||||
# behavior of putting them in ignored_fields and launching anyway
|
||||
if 'prompts' not in exclude_errors:
|
||||
errors_dict[field_name] = _('Field is not configured to prompt on launch.').format(field_name=field_name)
|
||||
|
||||
return prompted_data, rejected_data, errors_dict
|
||||
|
||||
@ -446,7 +488,7 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
|
||||
return WorkflowJob.objects.filter(workflow_job_template=self)
|
||||
|
||||
|
||||
class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificationMixin):
|
||||
class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificationMixin, LaunchTimeConfigBase):
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
ordering = ('id',)
|
||||
|
||||
@ -6,7 +6,7 @@ import pytest
|
||||
# AWX
|
||||
from awx.api.serializers import JobTemplateSerializer
|
||||
from awx.api.versioning import reverse
|
||||
from awx.main.models import Job, JobTemplate, CredentialType
|
||||
from awx.main.models import Job, JobTemplate, CredentialType, WorkflowJobTemplate
|
||||
from awx.main.migrations import _save_password_keys as save_password_keys
|
||||
|
||||
# Django
|
||||
@ -519,6 +519,24 @@ def test_launch_with_pending_deletion_inventory(get, post, organization_factory,
|
||||
assert resp.data['inventory'] == ['The inventory associated with this Job Template is being deleted.']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_launch_with_pending_deletion_inventory_workflow(get, post, organization, inventory, admin_user):
|
||||
wfjt = WorkflowJobTemplate.objects.create(
|
||||
name='wfjt',
|
||||
organization=organization,
|
||||
inventory=inventory
|
||||
)
|
||||
|
||||
inventory.pending_deletion = True
|
||||
inventory.save()
|
||||
|
||||
resp = post(
|
||||
url=reverse('api:workflow_job_template_launch', kwargs={'pk': wfjt.pk}),
|
||||
user=admin_user, expect=400
|
||||
)
|
||||
assert resp.data['inventory'] == ['The inventory associated with this Workflow is being deleted.']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_launch_with_extra_credentials(get, post, organization_factory,
|
||||
job_template_factory, machine_credential,
|
||||
|
||||
@ -34,6 +34,30 @@ def test_wfjt_schedule_accepted(post, workflow_job_template, admin_user):
|
||||
post(url, {'name': 'test sch', 'rrule': RRULE_EXAMPLE}, admin_user, expect=201)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_wfjt_unprompted_inventory_rejected(post, workflow_job_template, inventory, admin_user):
|
||||
r = post(
|
||||
url=reverse('api:workflow_job_template_schedules_list', kwargs={'pk': workflow_job_template.id}),
|
||||
data={'name': 'test sch', 'rrule': RRULE_EXAMPLE, 'inventory': inventory.pk},
|
||||
user=admin_user,
|
||||
expect=400
|
||||
)
|
||||
assert r.data['inventory'] == ['Field is not configured to prompt on launch.']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_wfjt_unprompted_inventory_accepted(post, workflow_job_template, inventory, admin_user):
|
||||
workflow_job_template.ask_inventory_on_launch = True
|
||||
workflow_job_template.save()
|
||||
r = post(
|
||||
url=reverse('api:workflow_job_template_schedules_list', kwargs={'pk': workflow_job_template.id}),
|
||||
data={'name': 'test sch', 'rrule': RRULE_EXAMPLE, 'inventory': inventory.pk},
|
||||
user=admin_user,
|
||||
expect=201
|
||||
)
|
||||
assert Schedule.objects.get(pk=r.data['id']).inventory == inventory
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_valid_survey_answer(post, admin_user, project, inventory, survey_spec_factory):
|
||||
job_template = JobTemplate.objects.create(
|
||||
|
||||
@ -149,6 +149,20 @@ class TestWorkflowJobAccess:
|
||||
wfjt.execute_role.members.add(alice)
|
||||
assert not WorkflowJobAccess(rando).can_start(workflow_job)
|
||||
|
||||
def test_relaunch_inventory_access(self, workflow_job, inventory, rando):
|
||||
wfjt = workflow_job.workflow_job_template
|
||||
wfjt.execute_role.members.add(rando)
|
||||
assert rando in wfjt.execute_role
|
||||
workflow_job.created_by = rando
|
||||
workflow_job.inventory = inventory
|
||||
workflow_job.save()
|
||||
wfjt.ask_inventory_on_launch = True
|
||||
wfjt.save()
|
||||
JobLaunchConfig.objects.create(job=workflow_job, inventory=inventory)
|
||||
assert not WorkflowJobAccess(rando).can_start(workflow_job)
|
||||
inventory.use_role.members.add(rando)
|
||||
assert WorkflowJobAccess(rando).can_start(workflow_job)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestWFJTCopyAccess:
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import tempfile
|
||||
import json
|
||||
import yaml
|
||||
@ -10,7 +11,9 @@ from awx.main.models import (
|
||||
Job,
|
||||
JobTemplate,
|
||||
JobLaunchConfig,
|
||||
WorkflowJobTemplate
|
||||
WorkflowJobTemplate,
|
||||
Project,
|
||||
Inventory
|
||||
)
|
||||
from awx.main.utils.safe_yaml import SafeLoader
|
||||
|
||||
@ -305,3 +308,49 @@ class TestWorkflowSurveys:
|
||||
)
|
||||
assert wfjt.variables_needed_to_start == ['question2']
|
||||
assert not wfjt.can_start_without_user_input()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize('provided_vars,valid', [
|
||||
({'tmpl_var': 'bar'}, True), # same as template, not counted as prompts
|
||||
({'tmpl_var': 'bar2'}, False), # different value from template, not okay
|
||||
({'tmpl_var': 'bar', 'a': 2}, False), # extra key, not okay
|
||||
({'tmpl_var': 'bar', False: 2}, False), # Falsy key
|
||||
({'tmpl_var': 'bar', u'🐉': u'🐉'}, False), # dragons
|
||||
])
|
||||
class TestExtraVarsNoPrompt:
|
||||
def process_vars_and_assert(self, tmpl, provided_vars, valid):
|
||||
prompted_fields, ignored_fields, errors = tmpl._accept_or_ignore_job_kwargs(
|
||||
extra_vars=provided_vars
|
||||
)
|
||||
if valid:
|
||||
assert not ignored_fields
|
||||
assert not errors
|
||||
else:
|
||||
assert ignored_fields
|
||||
assert errors
|
||||
|
||||
def test_jt_extra_vars_counting(self, provided_vars, valid):
|
||||
jt = JobTemplate(
|
||||
name='foo',
|
||||
extra_vars={'tmpl_var': 'bar'},
|
||||
project=Project(),
|
||||
project_id=42,
|
||||
playbook='helloworld.yml',
|
||||
inventory=Inventory(),
|
||||
inventory_id=42
|
||||
)
|
||||
prompted_fields, ignored_fields, errors = jt._accept_or_ignore_job_kwargs(
|
||||
extra_vars=provided_vars
|
||||
)
|
||||
self.process_vars_and_assert(jt, provided_vars, valid)
|
||||
|
||||
def test_wfjt_extra_vars_counting(self, provided_vars, valid):
|
||||
wfjt = WorkflowJobTemplate(
|
||||
name='foo',
|
||||
extra_vars={'tmpl_var': 'bar'}
|
||||
)
|
||||
prompted_fields, ignored_fields, errors = wfjt._accept_or_ignore_job_kwargs(
|
||||
extra_vars=provided_vars
|
||||
)
|
||||
self.process_vars_and_assert(wfjt, provided_vars, valid)
|
||||
|
||||
@ -236,4 +236,4 @@ class TestWorkflowJobNodeJobKWARGS:
|
||||
|
||||
|
||||
def test_get_ask_mapping_integrity():
|
||||
assert WorkflowJobTemplate.get_ask_mapping().keys() == ['extra_vars']
|
||||
assert WorkflowJobTemplate.get_ask_mapping().keys() == ['extra_vars', 'inventory']
|
||||
|
||||
@ -50,7 +50,9 @@ export default {
|
||||
const searchParam = _.assign($stateParams.job_search, {
|
||||
or__job__inventory: inventoryId,
|
||||
or__adhoccommand__inventory: inventoryId,
|
||||
or__inventoryupdate__inventory_source__inventory: inventoryId });
|
||||
or__inventoryupdate__inventory_source__inventory: inventoryId,
|
||||
or__workflowjob__inventory: inventoryId,
|
||||
});
|
||||
|
||||
const searchPath = GetBasePath('unified_jobs');
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ function TemplatesStrings (BaseString) {
|
||||
PANEL_TITLE: t.s('TEMPLATES'),
|
||||
ADD_DD_JT_LABEL: t.s('Job Template'),
|
||||
ADD_DD_WF_LABEL: t.s('Workflow Template'),
|
||||
OPEN_WORKFLOW_VISUALIZER: t.s('Click here to open the workflow visualizer'),
|
||||
ROW_ITEM_LABEL_ACTIVITY: t.s('Activity'),
|
||||
ROW_ITEM_LABEL_INVENTORY: t.s('Inventory'),
|
||||
ROW_ITEM_LABEL_PROJECT: t.s('Project'),
|
||||
@ -116,9 +117,12 @@ function TemplatesStrings (BaseString) {
|
||||
DELETED: t.s('DELETED'),
|
||||
START: t.s('START'),
|
||||
DETAILS: t.s('DETAILS'),
|
||||
TITLE: t.s('WORKFLOW VISUALIZER')
|
||||
TITLE: t.s('WORKFLOW VISUALIZER'),
|
||||
INVENTORY_WILL_OVERRIDE: t.s('The inventory of this node will be overridden by the parent workflow inventory.'),
|
||||
INVENTORY_WILL_NOT_OVERRIDE: t.s('The inventory of this node will not be overridden by the parent workflow inventory.'),
|
||||
INVENTORY_PROMPT_WILL_OVERRIDE: t.s('The inventory of this node will be overridden if a parent workflow inventory is provided at launch.'),
|
||||
INVENTORY_PROMPT_WILL_NOT_OVERRIDE: t.s('The inventory of this node will not be overridden if a parent workflow inventory is provided at launch.'),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TemplatesStrings.$inject = ['BaseStringService'];
|
||||
|
||||
@ -101,6 +101,14 @@ function ListTemplatesController(
|
||||
|
||||
vm.isPortalMode = $state.includes('portalMode');
|
||||
|
||||
vm.openWorkflowVisualizer = template => {
|
||||
const name = 'templates.editWorkflowJobTemplate.workflowMaker';
|
||||
const params = { workflow_job_template_id: template.id };
|
||||
const options = { reload: true };
|
||||
|
||||
$state.go(name, params, options);
|
||||
};
|
||||
|
||||
vm.deleteTemplate = template => {
|
||||
if (!template) {
|
||||
Alert(strings.get('error.DELETE'), strings.get('alert.MISSING_PARAMETER'));
|
||||
|
||||
@ -93,6 +93,11 @@
|
||||
ng-show="!vm.isPortalMode && template.summary_fields.user_capabilities.copy"
|
||||
tooltip="{{:: vm.strings.get('listActions.COPY', vm.getType(template)) }}">
|
||||
</at-row-action>
|
||||
<at-row-action icon="fa-sitemap" ng-click="vm.openWorkflowVisualizer(template)"
|
||||
ng-show="!vm.isPortalMode && template.summary_fields.user_capabilities.edit"
|
||||
ng-if="template.type === 'workflow_job_template'"
|
||||
tooltip="{{:: vm.strings.get('list.OPEN_WORKFLOW_VISUALIZER') }}">
|
||||
</at-row-action>
|
||||
<at-row-action icon="fa-trash" ng-click="vm.deleteTemplate(template)"
|
||||
ng-show="!vm.isPortalMode && template.summary_fields.user_capabilities.delete"
|
||||
tooltip="{{:: vm.strings.get('listActions.DELETE', vm.getType(template)) }}">
|
||||
|
||||
@ -93,16 +93,15 @@ function atLaunchTemplateCtrl (
|
||||
$state.go('workflowResults', { id: data.workflow_job }, { reload: true });
|
||||
});
|
||||
} else {
|
||||
launchData.data.defaults = {
|
||||
extra_vars: wfjtData.data.extra_vars
|
||||
};
|
||||
launchData.data.defaults.extra_vars = wfjtData.data.extra_vars;
|
||||
|
||||
const promptData = {
|
||||
launchConf: launchData.data,
|
||||
launchConf: selectedWorkflowJobTemplate.getLaunchConf(),
|
||||
launchOptions: launchOptions.data,
|
||||
template: vm.template.id,
|
||||
templateType: vm.template.type,
|
||||
prompts: PromptService.processPromptValues({
|
||||
launchConf: launchData.data,
|
||||
launchConf: selectedWorkflowJobTemplate.getLaunchConf(),
|
||||
launchOptions: launchOptions.data
|
||||
}),
|
||||
triggerModalOpen: true,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
let Base;
|
||||
let JobTemplate;
|
||||
let WorkflowJobTemplate;
|
||||
|
||||
function setDependentResources (id) {
|
||||
this.dependentResources = [
|
||||
@ -8,6 +9,12 @@ function setDependentResources (id) {
|
||||
params: {
|
||||
inventory: id
|
||||
}
|
||||
},
|
||||
{
|
||||
model: new WorkflowJobTemplate(),
|
||||
params: {
|
||||
inventory: id
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@ -21,16 +28,18 @@ function InventoryModel (method, resource, config) {
|
||||
return this.create(method, resource, config);
|
||||
}
|
||||
|
||||
function InventoryModelLoader (BaseModel, JobTemplateModel) {
|
||||
function InventoryModelLoader (BaseModel, JobTemplateModel, WorkflowJobTemplateModel) {
|
||||
Base = BaseModel;
|
||||
JobTemplate = JobTemplateModel;
|
||||
WorkflowJobTemplate = WorkflowJobTemplateModel;
|
||||
|
||||
return InventoryModel;
|
||||
}
|
||||
|
||||
InventoryModelLoader.$inject = [
|
||||
'BaseModel',
|
||||
'JobTemplateModel'
|
||||
'JobTemplateModel',
|
||||
'WorkflowJobTemplateModel',
|
||||
];
|
||||
|
||||
export default InventoryModelLoader;
|
||||
|
||||
@ -47,8 +47,15 @@ function getSurveyQuestions (id) {
|
||||
return $http(req);
|
||||
}
|
||||
|
||||
function getLaunchConf () {
|
||||
// this method is just a pass-through to the underlying launch GET data
|
||||
// we use it to make the access patterns consistent across both types of
|
||||
// templates
|
||||
return this.model.launch.GET;
|
||||
}
|
||||
|
||||
function canLaunchWithoutPrompt () {
|
||||
const launchData = this.model.launch.GET;
|
||||
const launchData = this.getLaunchConf();
|
||||
|
||||
return (
|
||||
launchData.can_start_without_user_input &&
|
||||
@ -61,7 +68,8 @@ function canLaunchWithoutPrompt () {
|
||||
!launchData.ask_skip_tags_on_launch &&
|
||||
!launchData.ask_variables_on_launch &&
|
||||
!launchData.ask_diff_mode_on_launch &&
|
||||
!launchData.survey_enabled
|
||||
!launchData.survey_enabled &&
|
||||
launchData.variables_needed_to_start.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
@ -85,6 +93,7 @@ function JobTemplateModel (method, resource, config) {
|
||||
this.getLaunch = getLaunch.bind(this);
|
||||
this.postLaunch = postLaunch.bind(this);
|
||||
this.getSurveyQuestions = getSurveyQuestions.bind(this);
|
||||
this.getLaunchConf = getLaunchConf.bind(this);
|
||||
this.canLaunchWithoutPrompt = canLaunchWithoutPrompt.bind(this);
|
||||
|
||||
this.model.launch = {};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint camelcase: 0 */
|
||||
let Base;
|
||||
let $http;
|
||||
|
||||
@ -46,12 +47,19 @@ function getSurveyQuestions (id) {
|
||||
return $http(req);
|
||||
}
|
||||
|
||||
function getLaunchConf () {
|
||||
return this.model.launch.GET;
|
||||
}
|
||||
|
||||
function canLaunchWithoutPrompt () {
|
||||
const launchData = this.model.launch.GET;
|
||||
const launchData = this.getLaunchConf();
|
||||
|
||||
return (
|
||||
launchData.can_start_without_user_input &&
|
||||
!launchData.survey_enabled
|
||||
!launchData.ask_inventory_on_launch &&
|
||||
!launchData.ask_variables_on_launch &&
|
||||
!launchData.survey_enabled &&
|
||||
launchData.variables_needed_to_start.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
@ -63,6 +71,7 @@ function WorkflowJobTemplateModel (method, resource, config) {
|
||||
this.getLaunch = getLaunch.bind(this);
|
||||
this.postLaunch = postLaunch.bind(this);
|
||||
this.getSurveyQuestions = getSurveyQuestions.bind(this);
|
||||
this.getLaunchConf = getLaunchConf.bind(this);
|
||||
this.canLaunchWithoutPrompt = canLaunchWithoutPrompt.bind(this);
|
||||
|
||||
this.model.launch = {};
|
||||
@ -79,7 +88,7 @@ function WorkflowJobTemplateModelLoader (BaseModel, _$http_) {
|
||||
|
||||
WorkflowJobTemplateModelLoader.$inject = [
|
||||
'BaseModel',
|
||||
'$http'
|
||||
'$http',
|
||||
];
|
||||
|
||||
export default WorkflowJobTemplateModelLoader;
|
||||
|
||||
@ -41,6 +41,10 @@ function ModelsStrings (BaseString) {
|
||||
|
||||
};
|
||||
|
||||
ns.workflow_job_templates = {
|
||||
LABEL: t.s('Workflow Job Templates')
|
||||
};
|
||||
|
||||
ns.workflow_job_template_nodes = {
|
||||
LABEL: t.s('Workflow Job Template Nodes')
|
||||
|
||||
|
||||
@ -5,9 +5,13 @@ export default [ '$scope', 'Empty', 'Wait', 'GetBasePath', 'Rest', 'ProcessError
|
||||
if (!Empty($scope.inventory.id)) {
|
||||
if ($scope.inventory.total_hosts > 0) {
|
||||
Wait('start');
|
||||
let url = GetBasePath('jobs') + "?type=job&inventory=" + $scope.inventory.id + "&failed=";
|
||||
url += ($scope.inventory.has_active_failures) ? "true" : "false";
|
||||
|
||||
let url = GetBasePath('unified_jobs') + '?';
|
||||
url += `&or__job__inventory=${$scope.inventory.id}`;
|
||||
url += `&or__workflowjob__inventory=${$scope.inventory.id}`;
|
||||
url += `&failed=${$scope.inventory.has_active_failures ? "true" : "false"}`;
|
||||
url += "&order_by=-finished&page_size=5";
|
||||
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
@ -22,8 +26,14 @@ export default [ '$scope', 'Empty', 'Wait', 'GetBasePath', 'Rest', 'ProcessError
|
||||
}
|
||||
};
|
||||
|
||||
$scope.viewJob = function(jobId) {
|
||||
$state.go('output', { id: jobId, type: 'playbook' });
|
||||
$scope.viewJob = function(jobId, type) {
|
||||
let outputType = 'playbook';
|
||||
|
||||
if (type === 'workflow_job') {
|
||||
$state.go('workflowResults', { id: jobId}, { reload: true });
|
||||
} else {
|
||||
$state.go('output', { id: jobId, type: outputType });
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -60,10 +60,10 @@ export default ['templateUrl', 'Wait', '$filter', '$compile', 'i18n',
|
||||
data.results.forEach(function(row) {
|
||||
if ((scope.inventory.has_active_failures && row.status === 'failed') || (!scope.inventory.has_active_failures && row.status === 'successful')) {
|
||||
html += "<tr>\n";
|
||||
html += "<td><a href=\"\" ng-click=\"viewJob(" + row.id + ")\" " + "aw-tool-tip=\"" + row.status.charAt(0).toUpperCase() + row.status.slice(1) +
|
||||
html += "<td><a href=\"\" ng-click=\"viewJob(" + row.id + "," + "'" + row.type + "'" + ")\" " + "aw-tool-tip=\"" + row.status.charAt(0).toUpperCase() + row.status.slice(1) +
|
||||
". Click for details\" aw-tip-placement=\"top\" data-tooltip-outer-class=\"Tooltip-secondary\"><i class=\"fa SmartStatus-tooltip--" + row.status + " icon-job-" + row.status + "\"></i></a></td>\n";
|
||||
html += "<td>" + ($filter('longDate')(row.finished)) + "</td>";
|
||||
html += "<td><a href=\"\" ng-click=\"viewJob(" + row.id + ")\" " + "aw-tool-tip=\"" + row.status.charAt(0).toUpperCase() + row.status.slice(1) +
|
||||
html += "<td><a href=\"\" ng-click=\"viewJob(" + row.id + "," + "'" + row.type + "'" + ")\" " + "aw-tool-tip=\"" + row.status.charAt(0).toUpperCase() + row.status.slice(1) +
|
||||
". Click for details\" aw-tip-placement=\"top\" data-tooltip-outer-class=\"Tooltip-secondary\">" + $filter('sanitize')(ellipsis(row.name)) + "</a></td>";
|
||||
html += "</tr>\n";
|
||||
}
|
||||
|
||||
@ -239,7 +239,19 @@ export default ['$filter', '$state', '$stateParams', '$http', 'Wait',
|
||||
});
|
||||
};
|
||||
|
||||
if(!launchConf.survey_enabled) {
|
||||
if (!launchConf.survey_enabled &&
|
||||
!launchConf.ask_inventory_on_launch &&
|
||||
!launchConf.ask_credential_on_launch &&
|
||||
!launchConf.ask_verbosity_on_launch &&
|
||||
!launchConf.ask_job_type_on_launch &&
|
||||
!launchConf.ask_limit_on_launch &&
|
||||
!launchConf.ask_tags_on_launch &&
|
||||
!launchConf.ask_skip_tags_on_launch &&
|
||||
!launchConf.ask_diff_mode_on_launch &&
|
||||
!launchConf.survey_enabled &&
|
||||
!launchConf.credential_needed_to_start &&
|
||||
!launchConf.inventory_needed_to_start &&
|
||||
launchConf.variables_needed_to_start.length === 0) {
|
||||
$scope.showPromptButton = false;
|
||||
} else {
|
||||
$scope.showPromptButton = true;
|
||||
@ -259,6 +271,7 @@ export default ['$filter', '$state', '$stateParams', '$http', 'Wait',
|
||||
launchConf: responses[1].data,
|
||||
launchOptions: responses[0].data,
|
||||
surveyQuestions: processed.surveyQuestions,
|
||||
templateType: ParentObject.type,
|
||||
template: ParentObject.id,
|
||||
prompts: PromptService.processPromptValues({
|
||||
launchConf: responses[1].data,
|
||||
@ -283,6 +296,7 @@ export default ['$filter', '$state', '$stateParams', '$http', 'Wait',
|
||||
$scope.promptData = {
|
||||
launchConf: responses[1].data,
|
||||
launchOptions: responses[0].data,
|
||||
templateType: ParentObject.type,
|
||||
template: ParentObject.id,
|
||||
prompts: PromptService.processPromptValues({
|
||||
launchConf: responses[1].data,
|
||||
|
||||
@ -424,7 +424,20 @@ function($filter, $state, $stateParams, Wait, $scope, moment,
|
||||
currentValues: scheduleResolve
|
||||
});
|
||||
|
||||
if(!launchConf.survey_enabled) {
|
||||
if (!launchConf.survey_enabled &&
|
||||
!launchConf.ask_inventory_on_launch &&
|
||||
!launchConf.ask_credential_on_launch &&
|
||||
!launchConf.ask_verbosity_on_launch &&
|
||||
!launchConf.ask_job_type_on_launch &&
|
||||
!launchConf.ask_limit_on_launch &&
|
||||
!launchConf.ask_tags_on_launch &&
|
||||
!launchConf.ask_skip_tags_on_launch &&
|
||||
!launchConf.ask_diff_mode_on_launch &&
|
||||
!launchConf.survey_enabled &&
|
||||
!launchConf.credential_needed_to_start &&
|
||||
!launchConf.inventory_needed_to_start &&
|
||||
launchConf.passwords_needed_to_start.length === 0 &&
|
||||
launchConf.variables_needed_to_start.length === 0) {
|
||||
$scope.showPromptButton = false;
|
||||
} else {
|
||||
$scope.showPromptButton = true;
|
||||
@ -446,6 +459,7 @@ function($filter, $state, $stateParams, Wait, $scope, moment,
|
||||
launchOptions: launchOptions,
|
||||
prompts: prompts,
|
||||
surveyQuestions: surveyQuestionRes.data.spec,
|
||||
templateType: ParentObject.type,
|
||||
template: ParentObject.id
|
||||
};
|
||||
|
||||
@ -467,6 +481,7 @@ function($filter, $state, $stateParams, Wait, $scope, moment,
|
||||
launchConf: launchConf,
|
||||
launchOptions: launchOptions,
|
||||
prompts: prompts,
|
||||
templateType: ParentObject.type,
|
||||
template: ParentObject.id
|
||||
};
|
||||
watchForPromptChanges();
|
||||
|
||||
@ -494,6 +494,10 @@ export default ['$compile', 'Attr', 'Icon',
|
||||
html += `></paginate></div>`;
|
||||
}
|
||||
|
||||
if (options.mode === 'lookup' && options.lookupMessage) {
|
||||
html = `<div class="Prompt-bodyQuery">${options.lookupMessage}</div>` + html;
|
||||
}
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
|
||||
@ -806,13 +806,19 @@ function($injector, $stateExtender, $log, i18n) {
|
||||
views: {
|
||||
'modal': {
|
||||
templateProvider: function(ListDefinition, generateList) {
|
||||
let list_html = generateList.build({
|
||||
const listConfig = {
|
||||
mode: 'lookup',
|
||||
list: ListDefinition,
|
||||
input_type: 'radio'
|
||||
});
|
||||
return `<lookup-modal>${list_html}</lookup-modal>`;
|
||||
};
|
||||
|
||||
if (field.lookupMessage) {
|
||||
listConfig.lookupMessage = field.lookupMessage;
|
||||
}
|
||||
|
||||
let list_html = generateList.build(listConfig);
|
||||
|
||||
return `<lookup-modal>${list_html}</lookup-modal>`;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -303,6 +303,23 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
},
|
||||
resolve: {
|
||||
add: {
|
||||
Inventory: ['$stateParams', 'Rest', 'GetBasePath', 'ProcessErrors',
|
||||
function($stateParams, Rest, GetBasePath, ProcessErrors){
|
||||
if($stateParams.inventory_id){
|
||||
let path = `${GetBasePath('inventory')}${$stateParams.inventory_id}`;
|
||||
Rest.setUrl(path);
|
||||
return Rest.get().
|
||||
then(function(data){
|
||||
return data.data;
|
||||
}).catch(function(response) {
|
||||
ProcessErrors(null, response.data, response.status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to get inventory info. GET returned status: ' +
|
||||
response.status
|
||||
});
|
||||
});
|
||||
}
|
||||
}],
|
||||
availableLabels: ['Rest', '$stateParams', 'GetBasePath', 'ProcessErrors', 'TemplatesService',
|
||||
function(Rest, $stateParams, GetBasePath, ProcessErrors, TemplatesService) {
|
||||
return TemplatesService.getAllLabelOptions()
|
||||
@ -354,6 +371,23 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
},
|
||||
resolve: {
|
||||
edit: {
|
||||
Inventory: ['$stateParams', 'Rest', 'GetBasePath', 'ProcessErrors',
|
||||
function($stateParams, Rest, GetBasePath, ProcessErrors){
|
||||
if($stateParams.inventory_id){
|
||||
let path = `${GetBasePath('inventory')}${$stateParams.inventory_id}`;
|
||||
Rest.setUrl(path);
|
||||
return Rest.get().
|
||||
then(function(data){
|
||||
return data.data;
|
||||
}).catch(function(response) {
|
||||
ProcessErrors(null, response.data, response.status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to get inventory info. GET returned status: ' +
|
||||
response.status
|
||||
});
|
||||
});
|
||||
}
|
||||
}],
|
||||
availableLabels: ['Rest', '$stateParams', 'GetBasePath', 'ProcessErrors', 'TemplatesService',
|
||||
function(Rest, $stateParams, GetBasePath, ProcessErrors, TemplatesService) {
|
||||
return TemplatesService.getAllLabelOptions()
|
||||
|
||||
@ -16,10 +16,8 @@ export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel',
|
||||
({ modal } = scope[scope.ns]);
|
||||
|
||||
scope.$watch('vm.promptData.triggerModalOpen', () => {
|
||||
|
||||
vm.actionButtonClicked = false;
|
||||
if(vm.promptData && vm.promptData.triggerModalOpen) {
|
||||
|
||||
scope.$emit('launchModalOpen', true);
|
||||
vm.promptDataClone = _.cloneDeep(vm.promptData);
|
||||
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
<div class="Prompt-footer">
|
||||
<button id="prompt_cancel" class="Prompt-defaultButton" ng-click="vm.cancel()" ng-show="!vm.readOnlyPrompts">{{:: vm.strings.get('CANCEL') }}</button>
|
||||
<button id="prompt_close" class="Prompt-defaultButton" ng-click="vm.cancel()" ng-show="vm.readOnlyPrompts">{{:: vm.strings.get('CLOSE') }}</button>
|
||||
<button id="prompt_inventory_next" class="Prompt-actionButton" ng-show="vm.steps.inventory.tab._active" ng-click="vm.next(vm.steps.inventory.tab)" ng-disabled="!vm.promptDataClone.prompts.inventory.value.id && !vm.readOnlyPrompts">{{:: vm.strings.get('NEXT') }}</button>
|
||||
<button id="prompt_inventory_next" class="Prompt-actionButton" ng-show="vm.steps.inventory.tab._active" ng-click="vm.next(vm.steps.inventory.tab)" ng-disabled="vm.promptData.templateType === 'workflow_job_template' && !vm.promptDataClone.prompts.inventory.value.id && vm.promptDataClone.launchConf.defaults.inventory.id && !vm.readOnlyPrompts">{{:: vm.strings.get('NEXT') }}</button>
|
||||
<button id="prompt_credential_next" class="Prompt-actionButton"
|
||||
ng-show="vm.steps.credential.tab._active"
|
||||
ng-click="vm.next(vm.steps.credential.tab)"
|
||||
|
||||
@ -151,7 +151,7 @@ function PromptService (Empty, $filter) {
|
||||
if (promptData.launchConf.ask_verbosity_on_launch && _.has(promptData, 'prompts.verbosity.value.value')) {
|
||||
launchData.verbosity = promptData.prompts.verbosity.value.value;
|
||||
}
|
||||
if (promptData.launchConf.ask_inventory_on_launch && !Empty(promptData.prompts.inventory.value.id)){
|
||||
if (promptData.launchConf.ask_inventory_on_launch && _.has(promptData, 'prompts.inventory.value.id')) {
|
||||
launchData.inventory_id = promptData.prompts.inventory.value.id;
|
||||
}
|
||||
if (promptData.launchConf.ask_credential_on_launch){
|
||||
@ -180,6 +180,17 @@ function PromptService (Empty, $filter) {
|
||||
});
|
||||
}
|
||||
|
||||
if (_.get(promptData, 'templateType') === 'workflow_job_template') {
|
||||
if (_.get(launchData, 'inventory_id', null) === null) {
|
||||
// It's possible to get here on a workflow job template with an inventory prompt and no
|
||||
// default value by selecting an inventory, removing it, selecting a different inventory,
|
||||
// and then reverting. A null inventory_id may be accepted by the API for prompted workflow
|
||||
// inventories in the future, but for now they will 400. As such, we intercept that case here
|
||||
// and remove it from the request data prior to launching.
|
||||
delete launchData.inventory_id;
|
||||
}
|
||||
}
|
||||
|
||||
return launchData;
|
||||
};
|
||||
|
||||
@ -242,28 +253,30 @@ function PromptService (Empty, $filter) {
|
||||
}
|
||||
}
|
||||
|
||||
const launchConfDefaults = _.get(params, ['promptData', 'launchConf', 'defaults'], {});
|
||||
|
||||
if(_.has(params, 'promptData.prompts.jobType.value.value') && _.get(params, 'promptData.launchConf.ask_job_type_on_launch')) {
|
||||
promptDataToSave.job_type = params.promptData.launchConf.defaults.job_type && params.promptData.launchConf.defaults.job_type === params.promptData.prompts.jobType.value.value ? null : params.promptData.prompts.jobType.value.value;
|
||||
promptDataToSave.job_type = launchConfDefaults.job_type && launchConfDefaults.job_type === params.promptData.prompts.jobType.value.value ? null : params.promptData.prompts.jobType.value.value;
|
||||
}
|
||||
if(_.has(params, 'promptData.prompts.tags.value') && _.get(params, 'promptData.launchConf.ask_tags_on_launch')){
|
||||
const templateDefaultJobTags = params.promptData.launchConf.defaults.job_tags.split(',');
|
||||
const templateDefaultJobTags = launchConfDefaults.job_tags.split(',');
|
||||
promptDataToSave.job_tags = (_.isEqual(templateDefaultJobTags.sort(), params.promptData.prompts.tags.value.map(a => a.value).sort())) ? null : params.promptData.prompts.tags.value.map(a => a.value).join();
|
||||
}
|
||||
if(_.has(params, 'promptData.prompts.skipTags.value') && _.get(params, 'promptData.launchConf.ask_skip_tags_on_launch')){
|
||||
const templateDefaultSkipTags = params.promptData.launchConf.defaults.skip_tags.split(',');
|
||||
const templateDefaultSkipTags = launchConfDefaults.skip_tags.split(',');
|
||||
promptDataToSave.skip_tags = (_.isEqual(templateDefaultSkipTags.sort(), params.promptData.prompts.skipTags.value.map(a => a.value).sort())) ? null : params.promptData.prompts.skipTags.value.map(a => a.value).join();
|
||||
}
|
||||
if(_.has(params, 'promptData.prompts.limit.value') && _.get(params, 'promptData.launchConf.ask_limit_on_launch')){
|
||||
promptDataToSave.limit = params.promptData.launchConf.defaults.limit && params.promptData.launchConf.defaults.limit === params.promptData.prompts.limit.value ? null : params.promptData.prompts.limit.value;
|
||||
promptDataToSave.limit = launchConfDefaults.limit && launchConfDefaults.limit === params.promptData.prompts.limit.value ? null : params.promptData.prompts.limit.value;
|
||||
}
|
||||
if(_.has(params, 'promptData.prompts.verbosity.value.value') && _.get(params, 'promptData.launchConf.ask_verbosity_on_launch')){
|
||||
promptDataToSave.verbosity = params.promptData.launchConf.defaults.verbosity && params.promptData.launchConf.defaults.verbosity === params.promptData.prompts.verbosity.value.value ? null : params.promptData.prompts.verbosity.value.value;
|
||||
promptDataToSave.verbosity = launchConfDefaults.verbosity && launchConfDefaults.verbosity === params.promptData.prompts.verbosity.value.value ? null : params.promptData.prompts.verbosity.value.value;
|
||||
}
|
||||
if(_.has(params, 'promptData.prompts.inventory.value') && _.get(params, 'promptData.launchConf.ask_inventory_on_launch')){
|
||||
promptDataToSave.inventory = params.promptData.launchConf.defaults.inventory && params.promptData.launchConf.defaults.inventory.id === params.promptData.prompts.inventory.value.id ? null : params.promptData.prompts.inventory.value.id;
|
||||
promptDataToSave.inventory = launchConfDefaults.inventory && launchConfDefaults.inventory.id === params.promptData.prompts.inventory.value.id ? null : params.promptData.prompts.inventory.value.id;
|
||||
}
|
||||
if(_.has(params, 'promptData.prompts.diffMode.value') && _.get(params, 'promptData.launchConf.ask_diff_mode_on_launch')){
|
||||
promptDataToSave.diff_mode = params.promptData.launchConf.defaults.diff_mode && params.promptData.launchConf.defaults.diff_mode === params.promptData.prompts.diffMode.value ? null : params.promptData.prompts.diffMode.value;
|
||||
promptDataToSave.diff_mode = launchConfDefaults.diff_mode && launchConfDefaults.diff_mode === params.promptData.prompts.diffMode.value ? null : params.promptData.prompts.diffMode.value;
|
||||
}
|
||||
|
||||
return promptDataToSave;
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
|
||||
import promptInventoryController from './prompt-inventory.controller';
|
||||
|
||||
export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$compile', 'InventoryList',
|
||||
(templateUrl, qs, GetBasePath, GenerateList, $compile, InventoryList) => {
|
||||
export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$compile', 'InventoryList', 'i18n',
|
||||
(templateUrl, qs, GetBasePath, GenerateList, $compile, InventoryList, i18n) => {
|
||||
return {
|
||||
scope: {
|
||||
promptData: '=',
|
||||
@ -46,10 +46,31 @@ export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$com
|
||||
let invList = _.cloneDeep(InventoryList);
|
||||
invList.disableRow = "{{ readOnlyPrompts }}";
|
||||
invList.disableRowValue = "readOnlyPrompts";
|
||||
|
||||
const defaultWarning = i18n._("This inventory is applied to all job template nodes that prompt for an inventory.");
|
||||
const missingWarning = i18n._("This workflow job template has a default inventory which must be included or replaced before proceeding.");
|
||||
|
||||
const updateInventoryWarning = () => {
|
||||
scope.inventoryWarning = null;
|
||||
if (scope.promptData.templateType === "workflow_job_template") {
|
||||
scope.inventoryWarning = defaultWarning;
|
||||
|
||||
const isPrompted = _.get(scope.promptData, 'launchConf.ask_inventory_on_launch');
|
||||
const isDefault = _.get(scope.promptData, 'launchConf.defaults.inventory.id');
|
||||
const isSelected = _.get(scope.promptData, 'prompts.inventory.value.id', null) !== null;
|
||||
|
||||
if (isPrompted && isDefault && !isSelected) {
|
||||
scope.inventoryWarning = missingWarning;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateInventoryWarning();
|
||||
|
||||
let html = GenerateList.build({
|
||||
list: invList,
|
||||
input_type: 'radio',
|
||||
mode: 'lookup'
|
||||
mode: 'lookup',
|
||||
});
|
||||
|
||||
scope.list = invList;
|
||||
@ -67,6 +88,8 @@ export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$com
|
||||
else {
|
||||
scope.inventories[i].checked = 0;
|
||||
}
|
||||
|
||||
updateInventoryWarning();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,5 +14,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="inventoryWarning" class="Prompt-credentialTypeMissing">
|
||||
<span class="fa fa-warning"></span> {{ inventoryWarning }}
|
||||
</div>
|
||||
<div id="prompt-inventory"></div>
|
||||
</div>
|
||||
|
||||
@ -68,6 +68,27 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) {
|
||||
ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate) || !canEditOrg',
|
||||
awLookupWhen: '(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate) && canEditOrg'
|
||||
},
|
||||
inventory: {
|
||||
label: i18n._('Inventory'),
|
||||
type: 'lookup',
|
||||
lookupMessage: i18n._("This inventory is applied to all job template nodes that prompt for an inventory."),
|
||||
basePath: 'inventory',
|
||||
list: 'InventoryList',
|
||||
sourceModel: 'inventory',
|
||||
sourceField: 'name',
|
||||
autopopulateLookup: false,
|
||||
column: 1,
|
||||
awPopOver: "<p>" + i18n._("Select an inventory for the workflow. This inventory is applied to all job template nodes that prompt for an inventory.") + "</p>",
|
||||
dataTitle: i18n._('Inventory'),
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
subCheckbox: {
|
||||
variable: 'ask_inventory_on_launch',
|
||||
ngChange: 'workflow_job_template_form.inventory_name.$validate()',
|
||||
text: i18n._('Prompt on launch')
|
||||
},
|
||||
ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate) || !canEditInventory',
|
||||
},
|
||||
labels: {
|
||||
label: i18n._('Labels'),
|
||||
type: 'select',
|
||||
|
||||
@ -23,6 +23,7 @@ export default [
|
||||
$scope.canAddWorkflowJobTemplate = workflowTemplate.options('actions.POST');
|
||||
|
||||
$scope.canEditOrg = true;
|
||||
$scope.canEditInventory = true;
|
||||
$scope.parseType = 'yaml';
|
||||
$scope.can_edit = true;
|
||||
// apply form definition's default field values
|
||||
@ -68,6 +69,7 @@ export default [
|
||||
data[fld] = $scope[fld];
|
||||
}
|
||||
}
|
||||
data.ask_inventory_on_launch = Boolean($scope.ask_inventory_on_launch);
|
||||
|
||||
data.extra_vars = ToJSON($scope.parseType,
|
||||
$scope.variables, true);
|
||||
@ -152,8 +154,7 @@ export default [
|
||||
$q.all(defers)
|
||||
.then(function() {
|
||||
// If we follow the same pattern as job templates then the survey logic will go here
|
||||
|
||||
$state.go('templates.editWorkflowJobTemplate', {workflow_job_template_id: data.data.id}, {reload: true});
|
||||
$state.go('templates.editWorkflowJobTemplate.workflowMaker', { workflow_job_template_id: data.data.id }, { reload: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -10,12 +10,12 @@ export default [
|
||||
'Wait', 'Empty', 'ToJSON', 'initSurvey', '$state', 'CreateSelect2',
|
||||
'ParseVariableString', 'TemplatesService', 'Rest', 'ToggleNotification',
|
||||
'OrgAdminLookup', 'availableLabels', 'selectedLabels', 'workflowJobTemplateData', 'i18n',
|
||||
'workflowLaunch', '$transitions', 'WorkflowJobTemplateModel',
|
||||
'workflowLaunch', '$transitions', 'WorkflowJobTemplateModel', 'Inventory',
|
||||
function($scope, $stateParams, WorkflowForm, GenerateForm, Alert,
|
||||
ProcessErrors, GetBasePath, $q, ParseTypeChange, Wait, Empty,
|
||||
ToJSON, SurveyControllerInit, $state, CreateSelect2, ParseVariableString,
|
||||
TemplatesService, Rest, ToggleNotification, OrgAdminLookup, availableLabels, selectedLabels, workflowJobTemplateData, i18n,
|
||||
workflowLaunch, $transitions, WorkflowJobTemplate
|
||||
workflowLaunch, $transitions, WorkflowJobTemplate, Inventory
|
||||
) {
|
||||
|
||||
$scope.missingTemplates = _.has(workflowLaunch, 'node_templates_missing') && workflowLaunch.node_templates_missing.length > 0 ? true : false;
|
||||
@ -53,6 +53,12 @@ export default [
|
||||
$scope.mode = 'edit';
|
||||
$scope.parseType = 'yaml';
|
||||
$scope.includeWorkflowMaker = false;
|
||||
$scope.ask_inventory_on_launch = workflowJobTemplateData.ask_inventory_on_launch;
|
||||
|
||||
if (Inventory){
|
||||
$scope.inventory = Inventory.id;
|
||||
$scope.inventory_name = Inventory.name;
|
||||
}
|
||||
|
||||
$scope.openWorkflowMaker = function() {
|
||||
$state.go('.workflowMaker');
|
||||
@ -83,6 +89,8 @@ export default [
|
||||
}
|
||||
}
|
||||
|
||||
data.ask_inventory_on_launch = Boolean($scope.ask_inventory_on_launch);
|
||||
|
||||
data.extra_vars = ToJSON($scope.parseType,
|
||||
$scope.variables, true);
|
||||
|
||||
@ -312,6 +320,16 @@ export default [
|
||||
$scope.canEditOrg = true;
|
||||
}
|
||||
|
||||
if(workflowJobTemplateData.inventory) {
|
||||
OrgAdminLookup.checkForRoleLevelAdminAccess(workflowJobTemplateData.inventory, 'workflow_admin_role')
|
||||
.then(function(canEditInventory){
|
||||
$scope.canEditInventory = canEditInventory;
|
||||
});
|
||||
}
|
||||
else {
|
||||
$scope.canEditInventory = true;
|
||||
}
|
||||
|
||||
$scope.url = workflowJobTemplateData.url;
|
||||
$scope.survey_enabled = workflowJobTemplateData.survey_enabled;
|
||||
|
||||
|
||||
@ -6,10 +6,10 @@
|
||||
|
||||
export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel', 'WorkflowJobTemplateModel',
|
||||
'Empty', 'PromptService', 'Rest', 'TemplatesStrings', '$timeout',
|
||||
'Empty', 'PromptService', 'Rest', 'TemplatesStrings', '$timeout', '$state',
|
||||
function ($scope, WorkflowService, TemplatesService,
|
||||
ProcessErrors, CreateSelect2, $q, JobTemplate, WorkflowJobTemplate,
|
||||
Empty, PromptService, Rest, TemplatesStrings, $timeout) {
|
||||
Empty, PromptService, Rest, TemplatesStrings, $timeout, $state) {
|
||||
|
||||
let promptWatcher, surveyQuestionWatcher, credentialsWatcher;
|
||||
|
||||
@ -409,6 +409,7 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
return $q.all(associatePromises.concat(credentialPromises))
|
||||
.then(function () {
|
||||
$scope.closeDialog();
|
||||
$state.transitionTo('templates');
|
||||
});
|
||||
}).catch(({
|
||||
data,
|
||||
@ -432,6 +433,7 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
$q.all(deletePromises)
|
||||
.then(function () {
|
||||
$scope.closeDialog();
|
||||
$state.transitionTo('templates');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -568,6 +570,7 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
/* EDIT NODE FUNCTIONS */
|
||||
|
||||
$scope.startEditNode = function (nodeToEdit) {
|
||||
$scope.editNodeHelpMessage = null;
|
||||
|
||||
if (!$scope.nodeBeingEdited || ($scope.nodeBeingEdited && $scope.nodeBeingEdited.id !== nodeToEdit.id)) {
|
||||
if ($scope.placeholderNode || $scope.nodeBeingEdited) {
|
||||
@ -749,6 +752,7 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
launchOptions: launchOptions,
|
||||
prompts: prompts,
|
||||
surveyQuestions: surveyQuestionRes.data.spec,
|
||||
templateType: $scope.nodeBeingEdited.unifiedJobTemplate.type,
|
||||
template: $scope.nodeBeingEdited.unifiedJobTemplate.id
|
||||
};
|
||||
|
||||
@ -771,6 +775,7 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
launchConf: launchConf,
|
||||
launchOptions: launchOptions,
|
||||
prompts: prompts,
|
||||
templateType: $scope.nodeBeingEdited.unifiedJobTemplate.type,
|
||||
template: $scope.nodeBeingEdited.unifiedJobTemplate.id
|
||||
};
|
||||
|
||||
@ -985,6 +990,42 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
}
|
||||
};
|
||||
|
||||
function getEditNodeHelpMessage(workflowTemplate, selectedTemplate) {
|
||||
if (selectedTemplate.type === "workflow_job_template") {
|
||||
if (workflowTemplate.inventory) {
|
||||
if (selectedTemplate.ask_inventory_on_launch) {
|
||||
return $scope.strings.get('workflow_maker.INVENTORY_WILL_OVERRIDE');
|
||||
}
|
||||
}
|
||||
|
||||
if (workflowTemplate.ask_inventory_on_launch) {
|
||||
if (selectedTemplate.ask_inventory_on_launch) {
|
||||
return $scope.strings.get('workflow_maker.INVENTORY_PROMPT_WILL_OVERRIDE');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTemplate.type === "job_template") {
|
||||
if (workflowTemplate.inventory) {
|
||||
if (selectedTemplate.ask_inventory_on_launch) {
|
||||
return $scope.strings.get('workflow_maker.INVENTORY_WILL_OVERRIDE');
|
||||
}
|
||||
|
||||
return $scope.strings.get('workflow_maker.INVENTORY_WILL_NOT_OVERRIDE');
|
||||
}
|
||||
|
||||
if (workflowTemplate.ask_inventory_on_launch) {
|
||||
if (selectedTemplate.ask_inventory_on_launch) {
|
||||
return $scope.strings.get('workflow_maker.INVENTORY_PROMPT_WILL_OVERRIDE');
|
||||
}
|
||||
|
||||
return $scope.strings.get('workflow_maker.INVENTORY_PROMPT_WILL_NOT_OVERRIDE');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$scope.templateManuallySelected = function (selectedTemplate) {
|
||||
|
||||
if (promptWatcher) {
|
||||
@ -1000,12 +1041,15 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
}
|
||||
|
||||
$scope.promptData = null;
|
||||
$scope.editNodeHelpMessage = getEditNodeHelpMessage($scope.treeData.workflow_job_template_obj, selectedTemplate);
|
||||
|
||||
if (selectedTemplate.type === "job_template" || selectedTemplate.type === "workflow_job_template") {
|
||||
let jobTemplate = selectedTemplate.type === "workflow_job_template" ? new WorkflowJobTemplate() : new JobTemplate();
|
||||
|
||||
$q.all([jobTemplate.optionsLaunch(selectedTemplate.id), jobTemplate.getLaunch(selectedTemplate.id)])
|
||||
.then((responses) => {
|
||||
let launchConf = responses[1].data;
|
||||
const launchConf = jobTemplate.getLaunchConf();
|
||||
|
||||
if (selectedTemplate.type === 'job_template') {
|
||||
if ((!selectedTemplate.inventory && !launchConf.ask_inventory_on_launch) || !selectedTemplate.project) {
|
||||
$scope.selectedTemplateInvalid = true;
|
||||
@ -1022,24 +1066,13 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
|
||||
$scope.selectedTemplate = angular.copy(selectedTemplate);
|
||||
|
||||
if (!launchConf.survey_enabled &&
|
||||
!launchConf.ask_inventory_on_launch &&
|
||||
!launchConf.ask_credential_on_launch &&
|
||||
!launchConf.ask_verbosity_on_launch &&
|
||||
!launchConf.ask_job_type_on_launch &&
|
||||
!launchConf.ask_limit_on_launch &&
|
||||
!launchConf.ask_tags_on_launch &&
|
||||
!launchConf.ask_skip_tags_on_launch &&
|
||||
!launchConf.ask_diff_mode_on_launch &&
|
||||
!launchConf.credential_needed_to_start &&
|
||||
!launchConf.ask_variables_on_launch &&
|
||||
launchConf.variables_needed_to_start.length === 0) {
|
||||
if (jobTemplate.canLaunchWithoutPrompt()) {
|
||||
$scope.showPromptButton = false;
|
||||
$scope.promptModalMissingReqFields = false;
|
||||
} else {
|
||||
$scope.showPromptButton = true;
|
||||
|
||||
if (selectedTemplate.type === 'job_template') {
|
||||
if (['job_template', 'workflow_job_template'].includes(selectedTemplate.type)) {
|
||||
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory')) {
|
||||
$scope.promptModalMissingReqFields = true;
|
||||
} else {
|
||||
@ -1059,12 +1092,12 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
});
|
||||
|
||||
$scope.missingSurveyValue = processed.missingSurveyValue;
|
||||
|
||||
$scope.promptData = {
|
||||
launchConf: responses[1].data,
|
||||
launchConf,
|
||||
launchOptions: responses[0].data,
|
||||
surveyQuestions: processed.surveyQuestions,
|
||||
template: selectedTemplate.id,
|
||||
templateType: selectedTemplate.type,
|
||||
prompts: PromptService.processPromptValues({
|
||||
launchConf: responses[1].data,
|
||||
launchOptions: responses[0].data
|
||||
@ -1084,10 +1117,12 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
watchForPromptChanges();
|
||||
});
|
||||
} else {
|
||||
|
||||
$scope.promptData = {
|
||||
launchConf: responses[1].data,
|
||||
launchConf,
|
||||
launchOptions: responses[0].data,
|
||||
template: selectedTemplate.id,
|
||||
templateType: selectedTemplate.type,
|
||||
prompts: PromptService.processPromptValues({
|
||||
launchConf: responses[1].data,
|
||||
launchOptions: responses[0].data
|
||||
|
||||
@ -133,6 +133,8 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="editNodeHelpMessage" class="WorkflowMaker-formHelp" ng-bind="editNodeHelpMessage"></div>
|
||||
<br />
|
||||
<div class="buttons Form-buttons" id="workflow_maker_controls">
|
||||
<button type="button" class="btn btn-sm Form-primaryButton Form-primaryButton--noMargin" id="workflow_maker_prompt_btn" ng-show="showPromptButton" ng-click="openPromptModal()"> {{:: strings.get('prompt.PROMPT') }}</button>
|
||||
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_cancel_btn" ng-show="(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)" ng-click="cancelNodeForm()"> {{:: strings.get('CANCEL') }}</button>
|
||||
|
||||
@ -32,6 +32,14 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
|
||||
$scope.cloud_credential_link = getLink('cloud_credential');
|
||||
$scope.network_credential_link = getLink('network_credential');
|
||||
|
||||
if ($scope.workflow.summary_fields.inventory) {
|
||||
if ($scope.workflow.summary_fields.inventory.kind === 'smart') {
|
||||
$scope.inventory_link = '/#/inventories/smart/' + $scope.workflow.inventory;
|
||||
} else {
|
||||
$scope.inventory_link = '/#/inventories/inventory/' + $scope.workflow.inventory;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.strings = {
|
||||
tooltips: {
|
||||
RELAUNCH: i18n._('Relaunch using the same parameters'),
|
||||
@ -54,7 +62,8 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
|
||||
STATUS: i18n._('Status'),
|
||||
SLICE_TEMPLATE: i18n._('Slice Job Template'),
|
||||
JOB_EXPLANATION: i18n._('Explanation'),
|
||||
SOURCE_WORKFLOW_JOB: i18n._('Source Workflow')
|
||||
SOURCE_WORKFLOW_JOB: i18n._('Source Workflow'),
|
||||
INVENTORY: i18n._('Inventory')
|
||||
},
|
||||
details: {
|
||||
HEADER: i18n._('DETAILS'),
|
||||
|
||||
@ -125,6 +125,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- INVENTORY DETAIL -->
|
||||
<div class="WorkflowResults-resultRow"
|
||||
ng-show="workflow.summary_fields.inventory">
|
||||
<label class="WorkflowResults-resultRowLabel">
|
||||
{{ strings.labels.INVENTORY }}
|
||||
</label>
|
||||
<div class="WorkflowResults-resultRowText">
|
||||
<a href="{{ inventory_link }}"
|
||||
aw-tool-tip="{{ strings.tooltips.EDIT_WORKFLOW }}"
|
||||
data-placement="top">
|
||||
{{ workflow.summary_fields.inventory.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TEMPLATE DETAIL -->
|
||||
<div class="WorkflowResults-resultRow"
|
||||
ng-show="workflow.summary_fields.workflow_job_template.name">
|
||||
|
||||
@ -4,6 +4,8 @@ import {
|
||||
getTeam,
|
||||
} from '../fixtures';
|
||||
|
||||
const namespace = 'test-org-permissions';
|
||||
|
||||
let data;
|
||||
const spinny = "//*[contains(@class, 'spinny')]";
|
||||
const checkbox = '//input[@type="checkbox"]';
|
||||
@ -23,7 +25,7 @@ const teamsTab = '//*[@id="teams_tab"]';
|
||||
const permissionsTab = '//*[@id="permissions_tab"]';
|
||||
const usersTab = '//*[@id="users_tab"]';
|
||||
|
||||
const orgsText = 'name.iexact:"test-actions-organization"';
|
||||
const orgsText = `name.iexact:"${namespace}-organization"`;
|
||||
const orgsCheckbox = '//select-list-item[@item="organization"]//input[@type="checkbox"]';
|
||||
const orgDetails = '//*[contains(@class, "OrgCards-label")]';
|
||||
const orgRoleSelector = '//*[contains(@aria-labelledby, "select2-organizations")]';
|
||||
@ -32,12 +34,12 @@ const readRole = '//*[contains(@id, "organizations-role") and text()="Read"]';
|
||||
const memberRoleText = 'member';
|
||||
const readRoleText = 'read';
|
||||
|
||||
const teamsSelector = "//a[contains(text(), 'test-actions-team')]";
|
||||
const teamsText = 'name.iexact:"test-actions-team"';
|
||||
const teamsSelector = `//a[contains(text(), '${namespace}-team')]`;
|
||||
const teamsText = `name.iexact:"${namespace}-team"`;
|
||||
const teamsSearchBadgeCount = '//span[contains(@class, "List-titleBadge") and contains(text(), "1")]';
|
||||
const teamCheckbox = '//*[@item="team"]//input[@type="checkbox"]';
|
||||
const addUserToTeam = '//*[@aw-tool-tip="Add User"]';
|
||||
const userText = 'username.iexact:"test-actions-user"';
|
||||
const userText = `username.iexact:"${namespace}-user"`;
|
||||
|
||||
const trashButton = '//i[contains(@class, "fa-trash")]';
|
||||
const deleteButton = '//*[text()="DELETE"]';
|
||||
@ -46,14 +48,14 @@ const saveButton = '//*[text()="Save"]';
|
||||
const addPermission = '//*[@aw-tool-tip="Grant Permission"]';
|
||||
const addTeamPermission = '//*[@aw-tool-tip="Add a permission"]';
|
||||
const verifyTeamPermissions = '//*[contains(@class, "List-tableRow")]//*[text()="Read"]';
|
||||
const readOrgPermissionResults = '//*[@id="permissions_table"]//*[text()="test-actions-organization"]/parent::*/parent::*//*[contains(text(), "Read")]';
|
||||
const readOrgPermissionResults = `//*[@id="permissions_table"]//*[text()="${namespace}-organization"]/parent::*/parent::*//*[contains(text(), "Read")]`;
|
||||
|
||||
module.exports = {
|
||||
before: (client, done) => {
|
||||
const resources = [
|
||||
getUserExact('test-actions', 'test-actions-user'),
|
||||
getOrganization('test-actions'),
|
||||
getTeam('test-actions'),
|
||||
getUserExact(namespace, `${namespace}-user`),
|
||||
getOrganization(namespace),
|
||||
getTeam(namespace),
|
||||
];
|
||||
|
||||
Promise.all(resources)
|
||||
|
||||
@ -142,14 +142,15 @@ describe('Controller: WorkflowAdd', () => {
|
||||
expect(TemplatesService.createWorkflowJobTemplate).toHaveBeenCalledWith({
|
||||
name: "Test Workflow",
|
||||
description: "This is a test description",
|
||||
labels: undefined,
|
||||
organization: undefined,
|
||||
inventory: undefined,
|
||||
labels: undefined,
|
||||
variables: undefined,
|
||||
extra_vars: undefined,
|
||||
allow_simultaneous: undefined
|
||||
allow_simultaneous: undefined,
|
||||
ask_inventory_on_launch: false,
|
||||
extra_vars: undefined
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('scope.formCancel()', () => {
|
||||
|
||||
@ -64,7 +64,7 @@ actions in the API.
|
||||
- POST to `/api/v2/job_templates/N/launch/`
|
||||
- can accept all prompt-able fields
|
||||
- POST to `/api/v2/workflow_job_templates/N/launch/`
|
||||
- can only accept extra_vars
|
||||
- can accept extra_vars and inventory
|
||||
- POST to `/api/v2/system_job_templates/N/launch/`
|
||||
- can accept certain fields, with no user configuration
|
||||
|
||||
@ -142,6 +142,7 @@ at launch-time that are saved in advance.
|
||||
- Workflow nodes
|
||||
- Schedules
|
||||
- Job relaunch / re-scheduling
|
||||
- (partially) workflow job templates
|
||||
|
||||
In the case of workflow nodes and schedules, the prompted fields are saved
|
||||
directly on the model. Those models include Workflow Job Template Nodes,
|
||||
@ -157,7 +158,7 @@ and only used to prepare the correct launch-time configuration for subsequent
|
||||
re-launch and re-scheduling of the job. To see these prompts for a particular
|
||||
job, do a GET to `/api/v2/jobs/N/create_schedule/`.
|
||||
|
||||
#### Workflow Node Launch Configuration (Changing in Tower 3.3)
|
||||
#### Workflow Node Launch Configuration
|
||||
|
||||
Workflow job nodes will combine `extra_vars` from their parent
|
||||
workflow job with the variables that they provide in
|
||||
@ -168,15 +169,26 @@ the node.
|
||||
All prompts that a workflow node passes to a spawned job abides by the
|
||||
rules of the related template.
|
||||
That means that if the node's job template has `ask_variables_on_launch` set
|
||||
to false with no survey, neither the workflow JT or the artifacts will take effect
|
||||
in the job that is spawned.
|
||||
to false with no survey, the workflow node's variables will not
|
||||
take effect in the job that is spawned.
|
||||
If the node's job template has `ask_inventory_on_launch` set to false and
|
||||
the node provides an inventory, this resource will not be used in the spawned
|
||||
job. If a user creates a node that would do this, a 400 response will be returned.
|
||||
|
||||
Behavior before the 3.3 release cycle was less-restrictive with passing
|
||||
workflow variables to the jobs it spawned, allowing variables to take effect
|
||||
even when the job template was not configured to allow it.
|
||||
#### Workflow Job Template Prompts
|
||||
|
||||
Workflow JTs are different than other cases, because they do not have a
|
||||
template directly linked, so their prompts are a form of action-at-a-distance.
|
||||
When the node's prompts are gathered, any prompts from the workflow job
|
||||
will take precedence over the node's value.
|
||||
|
||||
As a special exception, `extra_vars` from a workflow will not obey JT survey
|
||||
and prompting rules, both both historical and ease-of-understanding reasons.
|
||||
This behavior may change in the future.
|
||||
|
||||
Other than that exception, JT prompting rules are still adhered to when
|
||||
a job is spawned, although so far this only applies to the workflow job's
|
||||
`inventory` field.
|
||||
|
||||
#### Job Relaunch and Re-scheduling
|
||||
|
||||
|
||||
@ -8,19 +8,35 @@ A workflow has an associated tree-graph that is composed of multiple nodes. Each
|
||||
### Workflow Create-Read-Update-Delete (CRUD)
|
||||
Like other job resources, workflow jobs are created from workflow job templates. The API exposes common fields similar to job templates, including labels, schedules, notification templates, extra variables and survey specifications. Other than that, in the API, the related workflow graph nodes can be gotten to via the related workflow_nodes field.
|
||||
|
||||
The CRUD operations against a workflow job template and its corresponding workflow jobs are almost identical to those of normal job templates and related jobs. However, from an RBAC perspective, CRUD on workflow job templates/jobs are limited to super users. That is, an organization administrator takes full control over all workflow job templates/jobs under the same organization, while an organization auditor is able to see workflow job templates/jobs under the same organization. On the other hand, ordinary organization members have no, and are not able to gain, permission over any workflow-related resources.
|
||||
The CRUD operations against a workflow job template and its corresponding workflow jobs are almost identical to those of normal job templates and related jobs. However, from an RBAC perspective, CRUD on workflow job templates/jobs are limited to super users.
|
||||
|
||||
By default, organization administrators have full control over all workflow job templates under the same organization, and they share these abilities with users who have the `workflow_admin_role` in that organization. Permissions can be further delegated to other users via the workflow job template roles.
|
||||
|
||||
### Workflow Nodes
|
||||
Workflow Nodes are containers of workflow spawned job resources and function as nodes of workflow decision trees. Like that of workflow itself, the two types of workflow nodes are workflow job template nodes and workflow job nodes.
|
||||
|
||||
Workflow job template nodes are listed and created under endpoint `/workflow_job_templates/\d+/workflow_nodes/` to be associated with underlying workflow job template, or directly under endpoint `/workflow_job_template_nodes/`. The most important fields of a workflow job template node are `success_nodes`, `failure_nodes`, `always_nodes`, `unified_job_template` and `workflow_job_template`. The former three are lists of workflow job template nodes that, in union, forms the set of all its child nodes, in specific, `success_nodes` are triggered when parent node job succeeds, `failure_nodes` are triggered when parent node job fails, and `always_nodes` are triggered regardless of whether parent job succeeds or fails; The later two reference the job template resource it contains and workflow job template it belongs to.
|
||||
|
||||
#### Workflow Node Launch Configuration
|
||||
#### Workflow Launch Configuration
|
||||
|
||||
Workflow job templates can contain launch configuration items. So far, these only include
|
||||
`extra_vars` and `inventory`, and the `extra_vars` may have specifications via
|
||||
a survey, in the same way that job templates work.
|
||||
|
||||
Workflow nodes may also contain the launch-time configuration for the job it will spawn.
|
||||
As such, they share all the properties common to all saved launch configurations.
|
||||
|
||||
When a workflow job template is launched a workflow job is created. A workflow job node is created for each WFJT node and all fields from the WFJT node are copied. Note that workflow job nodes contain all fields that a workflow job template node contains plus an additional field, `job`, which is a reference to the to-be-spawned job resource.
|
||||
When a workflow job template is launched a workflow job is created. If the workflow
|
||||
job template is set to prompt for a value, then the user may provide this on launch,
|
||||
and the workflow job will assume the user-provided value.
|
||||
|
||||
A workflow job node is created for each WFJT node and all fields from the WFJT node are copied. Note that workflow job nodes contain all fields that a workflow job template node contains plus an additional field, `job`, which is a reference to the to-be-spawned job resource.
|
||||
|
||||
If the workflow job and the node both specify the same prompt, then the workflow job
|
||||
takes precedence and its value will be used. In either case, if the job template
|
||||
the node references does not have the related prompting field set to true
|
||||
(such as `ask_inventory_on_launch`), then the prompt will be ignored, and the
|
||||
job template default, if it exists, will be used instead.
|
||||
|
||||
See the document on saved launch configurations for how these are processed
|
||||
when the job is launched, and the API validation involved in building
|
||||
@ -77,7 +93,7 @@ Other than the normal way of creating workflow job templates, it is also possibl
|
||||
|
||||
Workflow job templates can be copied by POSTing to endpoint `/workflow_job_templates/\d+/copy/`. After copy finished, the resulting new workflow job template will have identical fields including description, extra_vars, and survey-related fields (survey_spec and survey_enabled). More importantly, workflow job template node of the original workflow job template, as well as the topology they bear, will be copied. Note there are RBAC restrictions on copying workflow job template nodes. A workflow job template is allowed to be copied if the user has permission to add an equivalent workflow job template. If the user performing the copy does not have access to a node's related resources (job template, inventory, or credential), those related fields will be null in the copy's version of the node. Schedules and notification templates of the original workflow job template will not be copied nor shared, and the name of the created workflow job template is the original name plus a special-formatted suffix to indicate its copy origin as well as the copy time, such as 'copy_from_name@10:30:00 am'.
|
||||
|
||||
Workflow jobs cannot be copied directly, instead a workflow job is implicitly copied when it needs to relaunch. Relaunching an existing workflow job is done by POSTing to endpoint `/workflow_jobs/\d+/relaunch/`. What happens next is the original workflow job is copied to create a new workflow job. The new workflow job then gets a copy of all nodes of the original as well as the topology they bear. Finally the full-fledged new workflow job is triggered to run, thus fulfilling the purpose of relaunch. Survey password-type answers should also be redacted in the relaunched version of the workflow job.
|
||||
Workflow jobs cannot be copied directly, instead a workflow job is implicitly copied when it needs to relaunch. Relaunching an existing workflow job is done by POSTing to endpoint `/workflow_jobs/\d+/relaunch/`. What happens next is the original workflow job's prompts are re-applied to its workflow job template to create a new workflow job. Finally the full-fledged new workflow job is triggered to run, thus fulfilling the purpose of relaunch. Survey password-type answers should also be redacted in the relaunched version of the workflow job.
|
||||
|
||||
### Artifacts
|
||||
Artifact support starts in Ansible and is carried through in Tower. The `set_stats` module is invoked by users, in a playbook, to register facts. Facts are passed in via `data:` argument. Note that the default `set_stats` parameters are the correct ones to work with Tower (i.e. `per_host: no`). Now that facts are registered, we will describe how facts are used. In Ansible, registered facts are "returned" to the callback plugin(s) via the `playbook_on_stats` event. Ansible users can configure whether or not they want the facts displayed through the global `show_custom_stats` configuration. Note that the `show_custom_stats` does not effect the artifacting feature of Tower. This only controls the displaying of `set_stats` fact data in Ansible output (also the output in Ansible playbooks ran in Tower). Tower uses a custom callback plugin that gathers the fact data set via `set_stats` in the `playbook_on_stats` handler and "ships" it back to Tower, saves it in the database, and makes it available on the job endpoint via the variable `artifacts`. The semantics and usage of `artifacts` throughout a workflow is described elsewhere in this document.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user