mirror of
https://github.com/ZwareBear/awx.git
synced 2026-05-14 07:48:39 -05:00
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:
+41
-14
@@ -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)
|
||||
|
||||
+28
-13
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
+53
-27
@@ -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)
|
||||
|
||||
|
||||
+63
-21
@@ -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')
|
||||
|
||||
|
||||
+14
-4
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -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()', () => {
|
||||
|
||||
Reference in New Issue
Block a user