Feature: saved launchtime configurations

Consolidate prompts accept/reject logic in unified models
Break out accept/reject logic for variables
Surface new promptable fields on WFJT nodes, schedules

Make schedules and workflows accurately reject variables
  that are not allowed by the prompting
  rules or the survey rules on the template

Validate against unallowed extra_data in system job schedules
Prevent schedule or WFJT node POST/PATCH with unprompted data
Move system job days validation to new mechanism
Add new psuedo-field for WFJT node credential
Add validation for node related credentials
Add related config model to unified job
Use JobLaunchConfig model for launch RBAC check

Support credential overwrite behavior with multi-creds
  change modern manual launch to use merge behavior
Refactor JobLaunchSerializer, self.instance=None
Modularize job launch view to create "modern" data
Auto-create config object with every job
Add create schedule endpoint for jobs
This commit is contained in:
AlanCoding
2017-11-01 12:08:50 -04:00
parent cd8a4b4669
commit 34a8e0a9b6
49 changed files with 2343 additions and 884 deletions

View File

@@ -3,10 +3,12 @@
# Python
#import urlparse
import logging
# Django
from django.db import models
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
#from django import settings as tower_settings
# AWX
@@ -23,8 +25,8 @@ from awx.main.models.rbac import (
)
from awx.main.fields import ImplicitRoleField
from awx.main.models.mixins import ResourceMixin, SurveyJobTemplateMixin, SurveyJobMixin
from awx.main.models.jobs import LaunchTimeConfig
from awx.main.redact import REPLACE_STR
from awx.main.utils import parse_yaml_or_json
from awx.main.fields import JSONField
from copy import copy
@@ -32,10 +34,11 @@ from urlparse import urljoin
__all__ = ['WorkflowJobTemplate', 'WorkflowJob', 'WorkflowJobOptions', 'WorkflowJobNode', 'WorkflowJobTemplateNode',]
CHAR_PROMPTS_LIST = ['job_type', 'job_tags', 'skip_tags', 'limit']
logger = logging.getLogger('awx.main.models.workflow')
class WorkflowNodeBase(CreatedModifiedModel):
class WorkflowNodeBase(CreatedModifiedModel, LaunchTimeConfig):
class Meta:
abstract = True
app_label = 'main'
@@ -66,78 +69,6 @@ class WorkflowNodeBase(CreatedModifiedModel):
default=None,
on_delete=models.SET_NULL,
)
# Prompting-related fields
inventory = models.ForeignKey(
'Inventory',
related_name='%(class)ss',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
)
credential = models.ForeignKey(
'Credential',
related_name='%(class)ss',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
)
char_prompts = JSONField(
blank=True,
default={}
)
def prompts_dict(self):
data = {}
if self.inventory:
data['inventory'] = self.inventory.pk
if self.credential:
data['credential'] = self.credential.pk
for fd in CHAR_PROMPTS_LIST:
if fd in self.char_prompts:
data[fd] = self.char_prompts[fd]
return data
@property
def job_type(self):
return self.char_prompts.get('job_type', None)
@property
def job_tags(self):
return self.char_prompts.get('job_tags', None)
@property
def skip_tags(self):
return self.char_prompts.get('skip_tags', None)
@property
def limit(self):
return self.char_prompts.get('limit', None)
def get_prompts_warnings(self):
ujt_obj = self.unified_job_template
if ujt_obj is None:
return {}
prompts_dict = self.prompts_dict()
if not hasattr(ujt_obj, '_ask_for_vars_dict'):
if prompts_dict:
return {'ignored': {'all': 'Cannot use prompts on unified_job_template that is not type of job template'}}
else:
return {}
accepted_fields, ignored_fields = ujt_obj._accept_or_ignore_job_kwargs(**prompts_dict)
ignored_dict = {}
for fd in ignored_fields:
ignored_dict[fd] = 'Workflow node provided field, but job template is not set to ask on launch'
scan_errors = ujt_obj._extra_job_type_errors(accepted_fields)
ignored_dict.update(scan_errors)
data = {}
if ignored_dict:
data['ignored'] = ignored_dict
return data
def get_parent_nodes(self):
'''Returns queryset containing all parents of this node'''
@@ -152,7 +83,8 @@ class WorkflowNodeBase(CreatedModifiedModel):
Return field names that should be copied from template node to job node.
'''
return ['workflow_job', 'unified_job_template',
'inventory', 'credential', 'char_prompts']
'extra_data', 'survey_passwords',
'inventory', 'credentials', 'char_prompts']
def create_workflow_job_node(self, **kwargs):
'''
@@ -160,11 +92,20 @@ class WorkflowNodeBase(CreatedModifiedModel):
'''
create_kwargs = {}
for field_name in self._get_workflow_job_field_names():
if field_name == 'credentials':
continue
if field_name in kwargs:
create_kwargs[field_name] = kwargs[field_name]
elif hasattr(self, field_name):
create_kwargs[field_name] = getattr(self, field_name)
return WorkflowJobNode.objects.create(**create_kwargs)
new_node = WorkflowJobNode.objects.create(**create_kwargs)
if self.pk:
allowed_creds = self.credentials.all()
else:
allowed_creds = []
for cred in allowed_creds:
new_node.credentials.add(cred)
return new_node
class WorkflowJobTemplateNode(WorkflowNodeBase):
@@ -186,11 +127,18 @@ class WorkflowJobTemplateNode(WorkflowNodeBase):
is not allowed to access
'''
create_kwargs = {}
allowed_creds = []
for field_name in self._get_workflow_job_field_names():
if field_name == 'credentials':
Credential = self._meta.get_field('credentials').related_model
for cred in self.credentials.all():
if user.can_access(Credential, 'use', cred):
allowed_creds.append(cred)
continue
item = getattr(self, field_name, None)
if item is None:
continue
if field_name in ['inventory', 'credential']:
if field_name == 'inventory':
if not user.can_access(item.__class__, 'use', item):
continue
if field_name in ['unified_job_template']:
@@ -198,7 +146,10 @@ class WorkflowJobTemplateNode(WorkflowNodeBase):
continue
create_kwargs[field_name] = item
create_kwargs['workflow_job_template'] = workflow_job_template
return self.__class__.objects.create(**create_kwargs)
new_node = self.__class__.objects.create(**create_kwargs)
for cred in allowed_creds:
new_node.credentials.add(cred)
return new_node
class WorkflowJobNode(WorkflowNodeBase):
@@ -237,10 +188,14 @@ class WorkflowJobNode(WorkflowNodeBase):
# reject/accept prompted fields
data = {}
ujt_obj = self.unified_job_template
if ujt_obj and hasattr(ujt_obj, '_ask_for_vars_dict'):
accepted_fields, ignored_fields = ujt_obj._accept_or_ignore_job_kwargs(**self.prompts_dict())
for fd in ujt_obj._extra_job_type_errors(accepted_fields):
accepted_fields.pop(fd)
if ujt_obj is not None:
accepted_fields, ignored_fields, errors = ujt_obj._accept_or_ignore_job_kwargs(**self.prompts_dict())
if errors:
logger.info(_('Bad launch configuration starting template {template_pk} as part of '
'workflow {workflow_pk}. Errors:\n{error_text}').format(
template_pk=ujt_obj.pk,
workflow_pk=self.pk,
error_text=errors))
data.update(accepted_fields) # missing fields are handled in the scheduler
# build ancestor artifacts, save them to node model for later
aa_dict = {}
@@ -251,14 +206,16 @@ class WorkflowJobNode(WorkflowNodeBase):
if aa_dict:
self.ancestor_artifacts = aa_dict
self.save(update_fields=['ancestor_artifacts'])
# process password list
password_dict = {}
if '_ansible_no_log' in aa_dict:
for key in aa_dict:
if key != '_ansible_no_log':
password_dict[key] = REPLACE_STR
workflow_job_survey_passwords = self.workflow_job.survey_passwords
if workflow_job_survey_passwords:
password_dict.update(workflow_job_survey_passwords)
if self.workflow_job.survey_passwords:
password_dict.update(self.workflow_job.survey_passwords)
if self.survey_passwords:
password_dict.update(self.survey_passwords)
if password_dict:
data['survey_passwords'] = password_dict
# process extra_vars
@@ -370,7 +327,7 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
base_list = super(WorkflowJobTemplate, cls)._get_unified_jt_copy_names()
base_list.remove('labels')
return (base_list +
['survey_spec', 'survey_enabled', 'organization'])
['survey_spec', 'survey_enabled', 'ask_variables_on_launch', 'organization'])
def get_absolute_url(self, request=None):
return reverse('api:workflow_job_template_detail', kwargs={'pk': self.pk}, request=request)
@@ -398,27 +355,26 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
workflow_job.copy_nodes_from_original(original=self)
return workflow_job
def _accept_or_ignore_job_kwargs(self, extra_vars=None, **kwargs):
# Only accept allowed survey variables
ignored_fields = {}
def _accept_or_ignore_job_kwargs(self, **kwargs):
prompted_fields = {}
prompted_fields['extra_vars'] = {}
ignored_fields['extra_vars'] = {}
extra_vars = parse_yaml_or_json(extra_vars)
if self.survey_enabled and self.survey_spec:
survey_vars = [question['variable'] for question in self.survey_spec.get('spec', [])]
for key in extra_vars:
if key in survey_vars:
prompted_fields['extra_vars'][key] = extra_vars[key]
else:
ignored_fields['extra_vars'][key] = extra_vars[key]
else:
prompted_fields['extra_vars'] = extra_vars
rejected_fields = {}
accepted_vars, rejected_vars, errors_dict = self.accept_or_ignore_variables(kwargs.get('extra_vars', {}))
if accepted_vars:
prompted_fields['extra_vars'] = accepted_vars
if rejected_vars:
rejected_fields['extra_vars'] = rejected_vars
return prompted_fields, ignored_fields
# WFJTs do not behave like JTs, it can not accept inventory, credential, etc.
bad_kwargs = kwargs.copy()
bad_kwargs.pop('extra_vars')
if bad_kwargs:
rejected_fields.update(bad_kwargs)
for field in bad_kwargs:
errors_dict[field] = _('Field is not allowed for use in workflows.')
return prompted_fields, rejected_fields, errors_dict
def can_start_without_user_input(self):
'''Return whether WFJT can be launched without survey passwords.'''
return not bool(
self.variables_needed_to_start or
self.node_templates_missing() or
@@ -431,8 +387,12 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
def node_prompts_rejected(self):
node_list = []
for node in self.workflow_job_template_nodes.prefetch_related('unified_job_template').all():
node_prompts_warnings = node.get_prompts_warnings()
if node_prompts_warnings:
ujt_obj = node.unified_job_template
if ujt_obj is None:
continue
prompts_dict = node.prompts_dict()
accepted_fields, ignored_fields, prompts_errors = ujt_obj._accept_or_ignore_job_kwargs(**prompts_dict)
if prompts_errors:
node_list.append(node.pk)
return node_list