mirror of
https://github.com/ansible/awx.git
synced 2026-05-08 09:57:35 -02:30
Adjust prompt logic and views to accept workflow inventory
This commit is contained in:
committed by
Jake McDermott
parent
33328c4ad7
commit
44fa3b18a9
@@ -4421,11 +4421,15 @@ class WorkflowJobLaunchSerializer(BaseSerializer):
|
|||||||
variables_needed_to_start = serializers.ReadOnlyField()
|
variables_needed_to_start = serializers.ReadOnlyField()
|
||||||
survey_enabled = serializers.SerializerMethodField()
|
survey_enabled = serializers.SerializerMethodField()
|
||||||
extra_vars = VerbatimField(required=False, write_only=True)
|
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()
|
workflow_job_template_data = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WorkflowJobTemplate
|
model = WorkflowJobTemplate
|
||||||
fields = ('can_start_without_user_input', 'extra_vars',
|
fields = ('can_start_without_user_input', 'extra_vars', 'inventory',
|
||||||
'survey_enabled', 'variables_needed_to_start',
|
'survey_enabled', 'variables_needed_to_start',
|
||||||
'node_templates_missing', 'node_prompts_rejected',
|
'node_templates_missing', 'node_prompts_rejected',
|
||||||
'workflow_job_template_data')
|
'workflow_job_template_data')
|
||||||
@@ -4444,11 +4448,17 @@ class WorkflowJobLaunchSerializer(BaseSerializer):
|
|||||||
accepted, rejected, errors = obj._accept_or_ignore_job_kwargs(
|
accepted, rejected, errors = obj._accept_or_ignore_job_kwargs(
|
||||||
_exclude_errors=['required'],
|
_exclude_errors=['required'],
|
||||||
**attrs)
|
**attrs)
|
||||||
|
self._ignored_fields = rejected
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
raise serializers.ValidationError(errors)
|
||||||
|
|
||||||
WFJT_extra_vars = obj.extra_vars
|
WFJT_extra_vars = obj.extra_vars
|
||||||
attrs = super(WorkflowJobLaunchSerializer, self).validate(attrs)
|
WFJT_inventory = obj.inventory
|
||||||
|
super(WorkflowJobLaunchSerializer, self).validate(attrs)
|
||||||
obj.extra_vars = WFJT_extra_vars
|
obj.extra_vars = WFJT_extra_vars
|
||||||
return attrs
|
obj.inventory = WFJT_inventory
|
||||||
|
return accepted
|
||||||
|
|
||||||
|
|
||||||
class NotificationTemplateSerializer(BaseSerializer):
|
class NotificationTemplateSerializer(BaseSerializer):
|
||||||
|
|||||||
@@ -3106,6 +3106,8 @@ class WorkflowJobTemplateLaunch(WorkflowsEnforcementMixin, RetrieveAPIView):
|
|||||||
extra_vars.setdefault(v, u'')
|
extra_vars.setdefault(v, u'')
|
||||||
if extra_vars:
|
if extra_vars:
|
||||||
data['extra_vars'] = extra_vars
|
data['extra_vars'] = extra_vars
|
||||||
|
if obj.ask_inventory_on_launch:
|
||||||
|
data['inventory'] = obj.inventory_id
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
@@ -3115,14 +3117,12 @@ class WorkflowJobTemplateLaunch(WorkflowsEnforcementMixin, RetrieveAPIView):
|
|||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
prompted_fields, ignored_fields, errors = obj._accept_or_ignore_job_kwargs(**request.data)
|
new_job = obj.create_unified_job(**serializer.validated_data)
|
||||||
|
|
||||||
new_job = obj.create_unified_job(**prompted_fields)
|
|
||||||
new_job.signal_start()
|
new_job.signal_start()
|
||||||
|
|
||||||
data = OrderedDict()
|
data = OrderedDict()
|
||||||
data['workflow_job'] = new_job.id
|
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))
|
data.update(WorkflowJobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job))
|
||||||
headers = {'Location': new_job.get_absolute_url(request)}
|
headers = {'Location': new_job.get_absolute_url(request)}
|
||||||
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Generated by Django 1.11.11 on 2018-09-27 18:47
|
# Generated by Django 1.11.11 on 2018-09-27 19:50
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import awx.main.fields
|
import awx.main.fields
|
||||||
@@ -14,10 +14,15 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workflowjob',
|
||||||
|
name='char_prompts',
|
||||||
|
field=awx.main.fields.JSONField(blank=True, default={}),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='workflowjob',
|
model_name='workflowjob',
|
||||||
name='inventory',
|
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='workflowjobs', to='main.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(
|
migrations.AddField(
|
||||||
model_name='workflowjobtemplate',
|
model_name='workflowjobtemplate',
|
||||||
|
|||||||
@@ -894,19 +894,19 @@ class NullablePromptPsuedoField(object):
|
|||||||
instance.char_prompts[self.field_name] = value
|
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
|
Needed as separate class from LaunchTimeConfig because some models
|
||||||
WFJT / WJ nodes, schedules, and job launch configs (not all implemented yet)
|
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:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
# Prompting-related fields that have to be handled as special cases
|
# Prompting-related fields that have to be handled as special cases
|
||||||
credentials = models.ManyToManyField(
|
|
||||||
'Credential',
|
|
||||||
related_name='%(class)ss'
|
|
||||||
)
|
|
||||||
inventory = models.ForeignKey(
|
inventory = models.ForeignKey(
|
||||||
'Inventory',
|
'Inventory',
|
||||||
related_name='%(class)ss',
|
related_name='%(class)ss',
|
||||||
@@ -915,15 +915,6 @@ class LaunchTimeConfig(BaseModel):
|
|||||||
default=None,
|
default=None,
|
||||||
on_delete=models.SET_NULL,
|
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
|
# All standard fields are stored in this dictionary field
|
||||||
# This is a solution to the nullable CharField problem, specific to prompting
|
# This is a solution to the nullable CharField problem, specific to prompting
|
||||||
char_prompts = JSONField(
|
char_prompts = JSONField(
|
||||||
@@ -933,6 +924,7 @@ class LaunchTimeConfig(BaseModel):
|
|||||||
|
|
||||||
def prompts_dict(self, display=False):
|
def prompts_dict(self, display=False):
|
||||||
data = {}
|
data = {}
|
||||||
|
# Some types may have different prompts, but always subset of JT prompts
|
||||||
for prompt_name in JobTemplate.get_ask_mapping().keys():
|
for prompt_name in JobTemplate.get_ask_mapping().keys():
|
||||||
try:
|
try:
|
||||||
field = self._meta.get_field(prompt_name)
|
field = self._meta.get_field(prompt_name)
|
||||||
@@ -945,11 +937,11 @@ class LaunchTimeConfig(BaseModel):
|
|||||||
if len(prompt_val) > 0:
|
if len(prompt_val) > 0:
|
||||||
data[prompt_name] = prompt_val
|
data[prompt_name] = prompt_val
|
||||||
elif prompt_name == 'extra_vars':
|
elif prompt_name == 'extra_vars':
|
||||||
if self.extra_data:
|
if self.extra_vars:
|
||||||
if display:
|
if display:
|
||||||
data[prompt_name] = self.display_extra_data()
|
data[prompt_name] = self.display_extra_vars()
|
||||||
else:
|
else:
|
||||||
data[prompt_name] = self.extra_data
|
data[prompt_name] = self.extra_vars
|
||||||
if self.survey_passwords and not display:
|
if self.survey_passwords and not display:
|
||||||
data['survey_passwords'] = self.survey_passwords
|
data['survey_passwords'] = self.survey_passwords
|
||||||
else:
|
else:
|
||||||
@@ -958,18 +950,18 @@ class LaunchTimeConfig(BaseModel):
|
|||||||
data[prompt_name] = prompt_val
|
data[prompt_name] = prompt_val
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def display_extra_data(self):
|
def display_extra_vars(self):
|
||||||
'''
|
'''
|
||||||
Hides fields marked as passwords in survey.
|
Hides fields marked as passwords in survey.
|
||||||
'''
|
'''
|
||||||
if self.survey_passwords:
|
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():
|
for key, value in self.survey_passwords.items():
|
||||||
if key in extra_data:
|
if key in extra_vars:
|
||||||
extra_data[key] = value
|
extra_vars[key] = value
|
||||||
return extra_data
|
return extra_vars
|
||||||
else:
|
else:
|
||||||
return self.extra_data
|
return self.extra_vars
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _credential(self):
|
def _credential(self):
|
||||||
@@ -993,6 +985,39 @@ class LaunchTimeConfig(BaseModel):
|
|||||||
return None
|
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():
|
for field_name in JobTemplate.get_ask_mapping().keys():
|
||||||
try:
|
try:
|
||||||
LaunchTimeConfig._meta.get_field(field_name)
|
LaunchTimeConfig._meta.get_field(field_name)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from awx.main.models.mixins import (
|
|||||||
SurveyJobMixin,
|
SurveyJobMixin,
|
||||||
RelatedJobsMixin,
|
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.models.credential import Credential
|
||||||
from awx.main.redact import REPLACE_STR
|
from awx.main.redact import REPLACE_STR
|
||||||
from awx.main.fields import JSONField
|
from awx.main.fields import JSONField
|
||||||
@@ -188,6 +188,16 @@ class WorkflowJobNode(WorkflowNodeBase):
|
|||||||
def get_absolute_url(self, request=None):
|
def get_absolute_url(self, request=None):
|
||||||
return reverse('api:workflow_job_node_detail', kwargs={'pk': self.pk}, request=request)
|
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)
|
||||||
|
# Explination - 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):
|
def get_job_kwargs(self):
|
||||||
'''
|
'''
|
||||||
In advance of creating a new unified job as part of a workflow,
|
In advance of creating a new unified job as part of a workflow,
|
||||||
@@ -280,15 +290,6 @@ class WorkflowJobOptions(BaseModel):
|
|||||||
allow_simultaneous = models.BooleanField(
|
allow_simultaneous = models.BooleanField(
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
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.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
extra_vars_dict = VarsDictProperty('extra_vars', True)
|
extra_vars_dict = VarsDictProperty('extra_vars', True)
|
||||||
|
|
||||||
@@ -299,7 +300,8 @@ class WorkflowJobOptions(BaseModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _get_unified_job_field_names(cls):
|
def _get_unified_job_field_names(cls):
|
||||||
return set(f.name for f in WorkflowJobOptions._meta.fields) | set(
|
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):
|
def _create_workflow_nodes(self, old_node_list, user=None):
|
||||||
@@ -351,6 +353,15 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
|
|||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name='workflows',
|
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(
|
ask_inventory_on_launch = AskForField(
|
||||||
blank=True,
|
blank=True,
|
||||||
default=False,
|
default=False,
|
||||||
@@ -413,23 +424,40 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
|
|||||||
exclude_errors = kwargs.pop('_exclude_errors', [])
|
exclude_errors = kwargs.pop('_exclude_errors', [])
|
||||||
prompted_data = {}
|
prompted_data = {}
|
||||||
rejected_data = {}
|
rejected_data = {}
|
||||||
accepted_vars, rejected_vars, errors_dict = self.accept_or_ignore_variables(
|
errors_dict = {}
|
||||||
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
|
|
||||||
|
|
||||||
# WFJTs do not behave like JTs, it can not accept inventory, credential, etc.
|
# Handle all the fields that have prompting rules
|
||||||
bad_kwargs = kwargs.copy()
|
# NOTE: If WFJTs prompt for other things, this logic can be combined with jobs
|
||||||
bad_kwargs.pop('extra_vars', None)
|
for field_name, ask_field_name in self.get_ask_mapping().items():
|
||||||
bad_kwargs.pop('survey_passwords', None)
|
|
||||||
if bad_kwargs:
|
if field_name == 'extra_vars':
|
||||||
rejected_data.update(bad_kwargs)
|
accepted_vars, rejected_vars, vars_errors = self.accept_or_ignore_variables(
|
||||||
for field in bad_kwargs:
|
kwargs.get('extra_vars', {}),
|
||||||
errors_dict[field] = _('Field is not allowed for use in workflows.')
|
_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)
|
||||||
|
|
||||||
|
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
|
return prompted_data, rejected_data, errors_dict
|
||||||
|
|
||||||
@@ -459,7 +487,7 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
|
|||||||
return WorkflowJob.objects.filter(workflow_job_template=self)
|
return WorkflowJob.objects.filter(workflow_job_template=self)
|
||||||
|
|
||||||
|
|
||||||
class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificationMixin):
|
class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificationMixin, LaunchTimeConfigBase):
|
||||||
class Meta:
|
class Meta:
|
||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
|
|||||||
Reference in New Issue
Block a user