From 020144d1ee08175291b4407599ca845a037410c6 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 18 Oct 2016 12:39:24 -0400 Subject: [PATCH 01/11] add surveys on workflow models --- awx/api/serializers.py | 12 +- awx/api/urls.py | 1 + awx/api/views.py | 13 +- .../migrations/0041_v310_workflow_surveys.py | 30 ++++ awx/main/models/jobs.py | 168 +----------------- awx/main/models/mixins.py | 1 + awx/main/models/unified_jobs.py | 153 +++++++++++++++- awx/main/models/workflow.py | 29 ++- awx/main/utils.py | 16 ++ 9 files changed, 254 insertions(+), 169 deletions(-) create mode 100644 awx/main/migrations/0041_v310_workflow_surveys.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 08073a7bc0..2409dc1e74 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2191,7 +2191,7 @@ class WorkflowJobTemplateSerializer(LabelsListMixin, UnifiedJobTemplateSerialize class Meta: model = WorkflowJobTemplate - fields = ('*', 'extra_vars', 'organization') + fields = ('*', 'extra_vars', 'organization', 'survey_enabled',) def get_related(self, obj): res = super(WorkflowJobTemplateSerializer, self).get_related(obj) @@ -2205,7 +2205,7 @@ class WorkflowJobTemplateSerializer(LabelsListMixin, UnifiedJobTemplateSerialize #notification_templates_any = reverse('api:system_job_template_notification_templates_any_list', args=(obj.pk,)), #notification_templates_success = reverse('api:system_job_template_notification_templates_success_list', args=(obj.pk,)), #notification_templates_error = reverse('api:system_job_template_notification_templates_error_list', args=(obj.pk,)), - + survey_spec = reverse('api:workflow_job_template_survey_spec', args=(obj.pk,)), )) return res @@ -2236,6 +2236,14 @@ class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer): res['cancel'] = reverse('api:workflow_job_cancel', args=(obj.pk,)) return res + def to_representation(self, obj): + ret = super(WorkflowJobSerializer, self).to_representation(obj) + if obj is None: + return ret + if 'extra_vars' in ret: + ret['extra_vars'] = obj.display_extra_vars() + return ret + # TODO: class WorkflowJobListSerializer(WorkflowJobSerializer, UnifiedJobListSerializer): pass diff --git a/awx/api/urls.py b/awx/api/urls.py index bdb9063c39..4699d9f2bf 100644 --- a/awx/api/urls.py +++ b/awx/api/urls.py @@ -263,6 +263,7 @@ workflow_job_template_urls = patterns('awx.api.views', url(r'^(?P[0-9]+)/jobs/$', 'workflow_job_template_jobs_list'), url(r'^(?P[0-9]+)/launch/$', 'workflow_job_template_launch'), url(r'^(?P[0-9]+)/schedules/$', 'workflow_job_template_schedules_list'), + url(r'^(?P[0-9]+)/survey_spec/$', 'workflow_job_template_survey_spec'), url(r'^(?P[0-9]+)/workflow_nodes/$', 'workflow_job_template_workflow_nodes_list'), url(r'^(?P[0-9]+)/labels/$', 'workflow_job_template_label_list'), # url(r'^(?P[0-9]+)/cancel/$', 'workflow_job_template_cancel'), diff --git a/awx/api/views.py b/awx/api/views.py index cc0cb8e100..0ed269bc08 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2376,6 +2376,11 @@ class JobTemplateSurveySpec(GenericAPIView): obj.save() return Response() +class WorkflowJobTemplateSurveySpec(JobTemplateSurveySpec): + + model = WorkflowJobTemplate + parent_model = WorkflowJobTemplate + class JobTemplateActivityStreamList(SubListAPIView): model = ActivityStream @@ -2792,6 +2797,7 @@ class WorkflowJobTemplateLaunch(GenericAPIView): data = {} obj = self.get_object() data['warnings'] = obj.get_warnings() + data['passwords_needed_to_start'] = obj.passwords_needed_to_start return Response(data) def post(self, request, *args, **kwargs): @@ -2799,9 +2805,12 @@ class WorkflowJobTemplateLaunch(GenericAPIView): if not request.user.can_access(self.model, 'start', obj): raise PermissionDenied() - new_job = obj.create_unified_job(**request.data) - new_job.signal_start(**request.data) + prompted_fields, ignored_fields = obj._accept_or_ignore_job_kwargs(**request.data) + + new_job = obj.create_unified_job(**prompted_fields) + new_job.signal_start(**prompted_fields) data = dict(workflow_job=new_job.id) + data['ignored_fields'] = ignored_fields return Response(data, status=status.HTTP_201_CREATED) # TODO: diff --git a/awx/main/migrations/0041_v310_workflow_surveys.py b/awx/main/migrations/0041_v310_workflow_surveys.py new file mode 100644 index 0000000000..1df93bfe35 --- /dev/null +++ b/awx/main/migrations/0041_v310_workflow_surveys.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0040_v310_artifacts'), + ] + + operations = [ + migrations.AddField( + model_name='workflowjob', + name='survey_passwords', + field=jsonfield.fields.JSONField(default={}, editable=False, blank=True), + ), + migrations.AddField( + model_name='workflowjobtemplate', + name='survey_enabled', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='workflowjobtemplate', + name='survey_spec', + field=jsonfield.fields.JSONField(default={}, blank=True), + ), + ] diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 5c8599052b..a5bc0d0a27 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -5,7 +5,6 @@ import datetime import hmac import json -import yaml import logging import time from urlparse import urljoin @@ -33,7 +32,11 @@ from awx.main.models.notifications import ( NotificationTemplate, JobNotificationMixin, ) -from awx.main.utils import ignore_inventory_computed_fields +from awx.main.utils import ( + decrypt_field, + ignore_inventory_computed_fields, + parse_yaml_or_json, +) from awx.main.redact import PlainTextCleaner from awx.main.fields import ImplicitRoleField from awx.main.models.mixins import ResourceMixin @@ -188,7 +191,7 @@ class JobOptions(BaseModel): else: return [] -class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): +class JobTemplate(UnifiedJobTemplate, SurveyJobTemplate, JobOptions, ResourceMixin): ''' A job template is a reusable job definition for applying a project (with playbook) to an inventory source with a given credential. @@ -232,15 +235,6 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): blank=True, default=False, ) - - survey_enabled = models.BooleanField( - default=False, - ) - - survey_spec = JSONField( - blank=True, - default={}, - ) admin_role = ImplicitRoleField( parent_role=['project.organization.admin_role', 'inventory.organization.admin_role'] ) @@ -318,125 +312,6 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): not self.passwords_needed_to_start and not self.variables_needed_to_start) - @property - def variables_needed_to_start(self): - vars = [] - if self.survey_enabled and 'spec' in self.survey_spec: - for survey_element in self.survey_spec['spec']: - if survey_element['required']: - vars.append(survey_element['variable']) - return vars - - def survey_password_variables(self): - vars = [] - if self.survey_enabled and 'spec' in self.survey_spec: - # Get variables that are type password - for survey_element in self.survey_spec['spec']: - if survey_element['type'] == 'password': - vars.append(survey_element['variable']) - return vars - - def survey_variable_validation(self, data): - errors = [] - if not self.survey_enabled: - return errors - if 'name' not in self.survey_spec: - errors.append("'name' missing from survey spec.") - if 'description' not in self.survey_spec: - errors.append("'description' missing from survey spec.") - for survey_element in self.survey_spec.get("spec", []): - if survey_element['variable'] not in data and \ - survey_element['required']: - errors.append("'%s' value missing" % survey_element['variable']) - elif survey_element['type'] in ["textarea", "text", "password"]: - if survey_element['variable'] in data: - if type(data[survey_element['variable']]) not in (str, unicode): - errors.append("Value %s for '%s' expected to be a string." % (data[survey_element['variable']], - survey_element['variable'])) - continue - if 'min' in survey_element and survey_element['min'] not in ["", None] and len(data[survey_element['variable']]) < int(survey_element['min']): - errors.append("'%s' value %s is too small (length is %s must be at least %s)." % - (survey_element['variable'], data[survey_element['variable']], len(data[survey_element['variable']]), survey_element['min'])) - if 'max' in survey_element and survey_element['max'] not in ["", None] and len(data[survey_element['variable']]) > int(survey_element['max']): - errors.append("'%s' value %s is too large (must be no more than %s)." % - (survey_element['variable'], data[survey_element['variable']], survey_element['max'])) - elif survey_element['type'] == 'integer': - if survey_element['variable'] in data: - if type(data[survey_element['variable']]) != int: - errors.append("Value %s for '%s' expected to be an integer." % (data[survey_element['variable']], - survey_element['variable'])) - continue - if 'min' in survey_element and survey_element['min'] not in ["", None] and survey_element['variable'] in data and \ - data[survey_element['variable']] < int(survey_element['min']): - errors.append("'%s' value %s is too small (must be at least %s)." % - (survey_element['variable'], data[survey_element['variable']], survey_element['min'])) - if 'max' in survey_element and survey_element['max'] not in ["", None] and survey_element['variable'] in data and \ - data[survey_element['variable']] > int(survey_element['max']): - errors.append("'%s' value %s is too large (must be no more than %s)." % - (survey_element['variable'], data[survey_element['variable']], survey_element['max'])) - elif survey_element['type'] == 'float': - if survey_element['variable'] in data: - if type(data[survey_element['variable']]) not in (float, int): - errors.append("Value %s for '%s' expected to be a numeric type." % (data[survey_element['variable']], - survey_element['variable'])) - continue - if 'min' in survey_element and survey_element['min'] not in ["", None] and data[survey_element['variable']] < float(survey_element['min']): - errors.append("'%s' value %s is too small (must be at least %s)." % - (survey_element['variable'], data[survey_element['variable']], survey_element['min'])) - if 'max' in survey_element and survey_element['max'] not in ["", None] and data[survey_element['variable']] > float(survey_element['max']): - errors.append("'%s' value %s is too large (must be no more than %s)." % - (survey_element['variable'], data[survey_element['variable']], survey_element['max'])) - elif survey_element['type'] == 'multiselect': - if survey_element['variable'] in data: - if type(data[survey_element['variable']]) != list: - errors.append("'%s' value is expected to be a list." % survey_element['variable']) - else: - for val in data[survey_element['variable']]: - if val not in survey_element['choices']: - errors.append("Value %s for '%s' expected to be one of %s." % (val, survey_element['variable'], - survey_element['choices'])) - elif survey_element['type'] == 'multiplechoice': - if survey_element['variable'] in data: - if data[survey_element['variable']] not in survey_element['choices']: - errors.append("Value %s for '%s' expected to be one of %s." % (data[survey_element['variable']], - survey_element['variable'], - survey_element['choices'])) - return errors - - def _update_unified_job_kwargs(self, **kwargs): - if 'launch_type' in kwargs and kwargs['launch_type'] == 'relaunch': - return kwargs - - # Job Template extra_vars - extra_vars = self.extra_vars_dict - - # Overwrite with job template extra vars with survey default vars - if self.survey_enabled and 'spec' in self.survey_spec: - for survey_element in self.survey_spec.get("spec", []): - if 'default' in survey_element and survey_element['default']: - extra_vars[survey_element['variable']] = survey_element['default'] - - # transform to dict - if 'extra_vars' in kwargs: - kwargs_extra_vars = kwargs['extra_vars'] - if not isinstance(kwargs_extra_vars, dict): - try: - kwargs_extra_vars = json.loads(kwargs_extra_vars) - except Exception: - try: - kwargs_extra_vars = yaml.safe_load(kwargs_extra_vars) - assert isinstance(kwargs_extra_vars, dict) - except: - kwargs_extra_vars = {} - else: - kwargs_extra_vars = {} - - # Overwrite job template extra vars with explicit job extra vars - # and add on job extra vars - extra_vars.update(kwargs_extra_vars) - kwargs['extra_vars'] = json.dumps(extra_vars) - return kwargs - def _ask_for_vars_dict(self): return dict( extra_vars=self.ask_variables_on_launch, @@ -466,16 +341,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): if field == 'extra_vars' and self.survey_enabled and self.survey_spec: # Accept vars defined in the survey and no others survey_vars = [question['variable'] for question in self.survey_spec.get('spec', [])] - extra_vars = kwargs[field] - if isinstance(extra_vars, basestring): - try: - extra_vars = json.loads(extra_vars) - except (ValueError, TypeError): - try: - extra_vars = yaml.safe_load(extra_vars) - assert isinstance(extra_vars, dict) - except (yaml.YAMLError, TypeError, AttributeError, AssertionError): - extra_vars = {} + extra_vars = parse_yaml_or_json(kwargs[field]) for key in extra_vars: if key in survey_vars: prompted_fields[field][key] = extra_vars[key] @@ -529,7 +395,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): any_notification_templates = set(any_notification_templates + list(base_notification_templates.filter(organization_notification_templates_for_any=self.project.organization))) return dict(error=list(error_notification_templates), success=list(success_notification_templates), any=list(any_notification_templates)) -class Job(UnifiedJob, JobOptions, JobNotificationMixin): +class Job(UnifiedJob, SurveyJob, JobOptions, JobNotificationMixin): ''' A job applies a project (with playbook) to an inventory source with a given credential. It represents a single invocation of ansible-playbook with the @@ -554,11 +420,6 @@ class Job(UnifiedJob, JobOptions, JobNotificationMixin): editable=False, through='JobHostSummary', ) - survey_passwords = JSONField( - blank=True, - default={}, - editable=False, - ) artifacts = JSONField( blank=True, default={}, @@ -732,19 +593,6 @@ class Job(UnifiedJob, JobOptions, JobNotificationMixin): evars.update(extra_vars) self.update_fields(extra_vars=json.dumps(evars)) - def display_extra_vars(self): - ''' - Hides fields marked as passwords in survey. - ''' - if self.survey_passwords: - extra_vars = json.loads(self.extra_vars) - for key, value in self.survey_passwords.items(): - if key in extra_vars: - extra_vars[key] = value - return json.dumps(extra_vars) - else: - return self.extra_vars - def display_artifacts(self): ''' Hides artifacts if they are marked as no_log type artifacts. diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 3f9f2043d5..9cb6b18b77 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -2,6 +2,7 @@ from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User # noqa +from jsonfield import JSONField # AWX from awx.main.models.rbac import ( diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index e9925a6b17..da37caeebd 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -36,7 +36,7 @@ from awx.main.utils import decrypt_field, _inventory_updates from awx.main.redact import UriCleaner, REPLACE_STR from awx.main.consumers import emit_channel_notification -__all__ = ['UnifiedJobTemplate', 'UnifiedJob'] +__all__ = ['UnifiedJobTemplate', 'UnifiedJob', 'SurveyJobTemplate', 'SurveyJob'] logger = logging.getLogger('awx.main.models.unified_jobs') @@ -937,3 +937,154 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique if settings.BROKER_URL.startswith('amqp://'): self._force_cancel() return self.cancel_flag + +class SurveyJobTemplate(models.Model): + class Meta: + abstract = True + + survey_enabled = models.BooleanField( + default=False, + ) + survey_spec = JSONField( + blank=True, + default={}, + ) + + def survey_password_variables(self): + vars = [] + if self.survey_enabled and 'spec' in self.survey_spec: + # Get variables that are type password + for survey_element in self.survey_spec['spec']: + if survey_element['type'] == 'password': + vars.append(survey_element['variable']) + return vars + + @property + def variables_needed_to_start(self): + vars = [] + if self.survey_enabled and 'spec' in self.survey_spec: + for survey_element in self.survey_spec['spec']: + if survey_element['required']: + vars.append(survey_element['variable']) + return vars + + def _update_unified_job_kwargs(self, **kwargs): + ''' + Combine extra_vars with variable precedence order: + JT extra_vars -> JT survey defaults -> runtime extra_vars + ''' + if 'launch_type' in kwargs and kwargs['launch_type'] == 'relaunch': + return kwargs + + # Job Template extra_vars + extra_vars = self.extra_vars_dict + + # Overwrite with job template extra vars with survey default vars + if self.survey_enabled and 'spec' in self.survey_spec: + for survey_element in self.survey_spec.get("spec", []): + if 'default' in survey_element and survey_element['default']: + extra_vars[survey_element['variable']] = survey_element['default'] + + # transform to dict + if 'extra_vars' in kwargs: + kwargs_extra_vars = kwargs['extra_vars'] + kwargs_extra_vars = parse_yaml_or_json(kwargs_extra_vars) + else: + kwargs_extra_vars = {} + + # Overwrite job template extra vars with explicit job extra vars + # and add on job extra vars + extra_vars.update(kwargs_extra_vars) + kwargs['extra_vars'] = json.dumps(extra_vars) + return kwargs + + def survey_variable_validation(self, data): + errors = [] + if not self.survey_enabled: + return errors + if 'name' not in self.survey_spec: + errors.append("'name' missing from survey spec.") + if 'description' not in self.survey_spec: + errors.append("'description' missing from survey spec.") + for survey_element in self.survey_spec.get("spec", []): + if survey_element['variable'] not in data and \ + survey_element['required']: + errors.append("'%s' value missing" % survey_element['variable']) + elif survey_element['type'] in ["textarea", "text", "password"]: + if survey_element['variable'] in data: + if type(data[survey_element['variable']]) not in (str, unicode): + errors.append("Value %s for '%s' expected to be a string." % (data[survey_element['variable']], + survey_element['variable'])) + continue + if 'min' in survey_element and survey_element['min'] not in ["", None] and len(data[survey_element['variable']]) < int(survey_element['min']): + errors.append("'%s' value %s is too small (length is %s must be at least %s)." % + (survey_element['variable'], data[survey_element['variable']], len(data[survey_element['variable']]), survey_element['min'])) + if 'max' in survey_element and survey_element['max'] not in ["", None] and len(data[survey_element['variable']]) > int(survey_element['max']): + errors.append("'%s' value %s is too large (must be no more than %s)." % + (survey_element['variable'], data[survey_element['variable']], survey_element['max'])) + elif survey_element['type'] == 'integer': + if survey_element['variable'] in data: + if type(data[survey_element['variable']]) != int: + errors.append("Value %s for '%s' expected to be an integer." % (data[survey_element['variable']], + survey_element['variable'])) + continue + if 'min' in survey_element and survey_element['min'] not in ["", None] and survey_element['variable'] in data and \ + data[survey_element['variable']] < int(survey_element['min']): + errors.append("'%s' value %s is too small (must be at least %s)." % + (survey_element['variable'], data[survey_element['variable']], survey_element['min'])) + if 'max' in survey_element and survey_element['max'] not in ["", None] and survey_element['variable'] in data and \ + data[survey_element['variable']] > int(survey_element['max']): + errors.append("'%s' value %s is too large (must be no more than %s)." % + (survey_element['variable'], data[survey_element['variable']], survey_element['max'])) + elif survey_element['type'] == 'float': + if survey_element['variable'] in data: + if type(data[survey_element['variable']]) not in (float, int): + errors.append("Value %s for '%s' expected to be a numeric type." % (data[survey_element['variable']], + survey_element['variable'])) + continue + if 'min' in survey_element and survey_element['min'] not in ["", None] and data[survey_element['variable']] < float(survey_element['min']): + errors.append("'%s' value %s is too small (must be at least %s)." % + (survey_element['variable'], data[survey_element['variable']], survey_element['min'])) + if 'max' in survey_element and survey_element['max'] not in ["", None] and data[survey_element['variable']] > float(survey_element['max']): + errors.append("'%s' value %s is too large (must be no more than %s)." % + (survey_element['variable'], data[survey_element['variable']], survey_element['max'])) + elif survey_element['type'] == 'multiselect': + if survey_element['variable'] in data: + if type(data[survey_element['variable']]) != list: + errors.append("'%s' value is expected to be a list." % survey_element['variable']) + else: + for val in data[survey_element['variable']]: + if val not in survey_element['choices']: + errors.append("Value %s for '%s' expected to be one of %s." % (val, survey_element['variable'], + survey_element['choices'])) + elif survey_element['type'] == 'multiplechoice': + if survey_element['variable'] in data: + if data[survey_element['variable']] not in survey_element['choices']: + errors.append("Value %s for '%s' expected to be one of %s." % (data[survey_element['variable']], + survey_element['variable'], + survey_element['choices'])) + return errors + + +class SurveyJob(models.Model): + class Meta: + abstract = True + + survey_passwords = JSONField( + blank=True, + default={}, + editable=False, + ) + + def display_extra_vars(self): + ''' + Hides fields marked as passwords in survey. + ''' + if self.survey_passwords: + extra_vars = json.loads(self.extra_vars) + for key, value in self.survey_passwords.items(): + if key in extra_vars: + extra_vars[key] = value + return json.dumps(extra_vars) + else: + return self.extra_vars diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index ff457fd207..a25bfebb3c 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -13,6 +13,7 @@ from jsonfield import JSONField # AWX from awx.main.models import UnifiedJobTemplate, UnifiedJob +from awx.main.models.unified_jobs import SurveyJobTemplate, SurveyJob from awx.main.models.notifications import JobNotificationMixin from awx.main.models.base import BaseModel, CreatedModifiedModel, VarsDictProperty from awx.main.models.rbac import ( @@ -22,6 +23,7 @@ from awx.main.models.rbac import ( from awx.main.fields import ImplicitRoleField from awx.main.models.mixins import ResourceMixin from awx.main.redact import REPLACE_STR +from awx.main.utils import parse_yaml_or_json from copy import copy @@ -260,7 +262,9 @@ class WorkflowJobOptions(BaseModel): default='', ) -class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, ResourceMixin): + extra_vars_dict = VarsDictProperty('extra_vars', True) + +class WorkflowJobTemplate(UnifiedJobTemplate, SurveyJobTemplate, WorkflowJobOptions, ResourceMixin): class Meta: app_label = 'main' @@ -318,6 +322,25 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, ResourceMixin) workflow_job.inherit_job_template_workflow_nodes() return workflow_job + def _accept_or_ignore_job_kwargs(self, extra_vars=None, **kwargs): + # Only accept allowed survey variables + ignored_fields = {} + 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[field][key] = extra_vars[key] + else: + ignored_fields[field][key] = extra_vars[key] + else: + prompted_fields['extra_vars'] = extra_vars + + return prompted_fields, ignored_fields + def get_warnings(self): warning_data = {} for node in self.workflow_job_template_nodes.all(): @@ -372,7 +395,7 @@ class WorkflowJobInheritNodesMixin(object): self._inherit_relationship(old_node, new_node, node_ids_map, node_type) -class WorkflowJob(UnifiedJob, WorkflowJobOptions, JobNotificationMixin, WorkflowJobInheritNodesMixin): +class WorkflowJob(UnifiedJob, SurveyJob, WorkflowJobOptions, JobNotificationMixin, WorkflowJobInheritNodesMixin): class Meta: app_label = 'main' @@ -387,8 +410,6 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, JobNotificationMixin, Workflow on_delete=models.SET_NULL, ) - extra_vars_dict = VarsDictProperty('extra_vars', True) - @classmethod def _get_parent_field_name(cls): return 'workflow_job_template' diff --git a/awx/main/utils.py b/awx/main/utils.py index 58123972b7..c513a9bf5d 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -478,6 +478,22 @@ def cache_list_capabilities(page, prefetch_list, model, user): if obj.pk in ids_with_role: obj.capabilities_cache[display_method] = True +def parse_yaml_or_json(vars_str): + ''' + Attempt to parse a string with variables, and if attempt fails, + return an empty dictionary. + ''' + if isinstance(vars_str, dict): + return vars_str + try: + vars_dict = json.loads(vars_str) + except (ValueError, TypeError): + try: + vars_dict = yaml.safe_load(vars_str) + assert isinstance(extra_vars, dict) + except (yaml.YAMLError, TypeError, AttributeError, AssertionError): + vars_dict = {} + return vars_dict @memoize() def get_system_task_capacity(): From 21c6dd6b1eb3e8f6aa84386c66e6e1528cabba40 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 18 Oct 2016 17:15:26 -0400 Subject: [PATCH 02/11] move survey models to mixins, start WFJT launch endpoint --- awx/api/serializers.py | 90 +++++++++++++++++++ awx/api/views.py | 27 ++++-- awx/main/models/jobs.py | 6 +- awx/main/models/mixins.py | 154 +++++++++++++++++++++++++++++++- awx/main/models/unified_jobs.py | 153 +------------------------------ awx/main/models/workflow.py | 13 +-- 6 files changed, 276 insertions(+), 167 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 2409dc1e74..f3e8419752 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2607,6 +2607,96 @@ class JobLaunchSerializer(BaseSerializer): obj.credential = JT_credential return attrs +class WorkflowJobLaunchSerializer(BaseSerializer): + + can_start_without_user_input = serializers.BooleanField(read_only=True) + variables_needed_to_start = serializers.ReadOnlyField() + + survey_enabled = serializers.SerializerMethodField() + extra_vars = VerbatimField(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', + 'workflow_job_template_data') + + def get_survey_enabled(self, obj): + if obj: + return obj.survey_enabled and 'spec' in obj.survey_spec + return False + + def get_workflow_job_template_data(self, obj): + return dict(name=obj.name, id=obj.id, description=obj.description) + + def validate(self, attrs): + errors = {} + obj = self.context.get('obj') + data = self.context.get('data') + + for field in obj.resources_needed_to_start: + if not (attrs.get(field, False) and obj._ask_for_vars_dict().get(field, False)): + errors[field] = "Job Template '%s' is missing or undefined." % field + + if (not obj.ask_credential_on_launch) or (not attrs.get('credential', None)): + credential = obj.credential + else: + credential = attrs.get('credential', None) + + # fill passwords dict with request data passwords + if credential and credential.passwords_needed: + passwords = self.context.get('passwords') + try: + for p in credential.passwords_needed: + passwords[p] = data[p] + except KeyError: + errors['passwords_needed_to_start'] = credential.passwords_needed + + extra_vars = attrs.get('extra_vars', {}) + + if isinstance(extra_vars, basestring): + try: + extra_vars = json.loads(extra_vars) + except (ValueError, TypeError): + try: + extra_vars = yaml.safe_load(extra_vars) + assert isinstance(extra_vars, dict) + except (yaml.YAMLError, TypeError, AttributeError, AssertionError): + errors['extra_vars'] = 'Must be a valid JSON or YAML dictionary.' + + if not isinstance(extra_vars, dict): + extra_vars = {} + + if self.get_survey_enabled(obj): + validation_errors = obj.survey_variable_validation(extra_vars) + if validation_errors: + errors['variables_needed_to_start'] = validation_errors + + # Special prohibited cases for scan jobs + errors.update(obj._extra_job_type_errors(data)) + + if errors: + raise serializers.ValidationError(errors) + + JT_extra_vars = obj.extra_vars + JT_limit = obj.limit + JT_job_type = obj.job_type + JT_job_tags = obj.job_tags + JT_skip_tags = obj.skip_tags + JT_inventory = obj.inventory + JT_credential = obj.credential + attrs = super(JobLaunchSerializer, self).validate(attrs) + obj.extra_vars = JT_extra_vars + obj.limit = JT_limit + obj.job_type = JT_job_type + obj.skip_tags = JT_skip_tags + obj.job_tags = JT_job_tags + obj.inventory = JT_inventory + obj.credential = JT_credential + return attrs + class NotificationTemplateSerializer(BaseSerializer): show_capabilities = ['edit', 'delete'] diff --git a/awx/api/views.py b/awx/api/views.py index 0ed269bc08..4431a6e825 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2790,21 +2790,36 @@ class WorkflowJobTemplateLabelList(JobTemplateLabelList): class WorkflowJobTemplateLaunch(GenericAPIView): model = WorkflowJobTemplate - serializer_class = EmptySerializer + serializer_class = WorkflowJobLaunchSerializer new_in_310 = True - def get(self, request, *args, **kwargs): - data = {} + def update_raw_data(self, data): obj = self.get_object() - data['warnings'] = obj.get_warnings() - data['passwords_needed_to_start'] = obj.passwords_needed_to_start - return Response(data) + extra_vars = data.pop('extra_vars', None) or {} + if obj: + for v in obj.variables_needed_to_start: + extra_vars.setdefault(v, u'') + if extra_vars: + data['extra_vars'] = extra_vars + return data + + # def get(self, request, *args, **kwargs): + # data = {} + # obj = self.get_object() + # data['warnings'] = obj.get_warnings() + # data['variables_needed_to_start'] = obj.variables_needed_to_start + # return Response(data) def post(self, request, *args, **kwargs): obj = self.get_object() if not request.user.can_access(self.model, 'start', obj): raise PermissionDenied() + +# serializer = self.serializer_class(instance=obj, data=request.data, context={'obj': obj, 'data': request.data, 'passwords': passwords}) +# if not serializer.is_valid(): +# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + prompted_fields, ignored_fields = obj._accept_or_ignore_job_kwargs(**request.data) new_job = obj.create_unified_job(**prompted_fields) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index a5bc0d0a27..05f02bd5af 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -39,7 +39,7 @@ from awx.main.utils import ( ) from awx.main.redact import PlainTextCleaner from awx.main.fields import ImplicitRoleField -from awx.main.models.mixins import ResourceMixin +from awx.main.models.mixins import ResourceMixin, SurveyJobTemplateMixin, SurveyJobMixin from awx.main.models.base import PERM_INVENTORY_SCAN from awx.main.consumers import emit_channel_notification @@ -191,7 +191,7 @@ class JobOptions(BaseModel): else: return [] -class JobTemplate(UnifiedJobTemplate, SurveyJobTemplate, JobOptions, ResourceMixin): +class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, ResourceMixin): ''' A job template is a reusable job definition for applying a project (with playbook) to an inventory source with a given credential. @@ -395,7 +395,7 @@ class JobTemplate(UnifiedJobTemplate, SurveyJobTemplate, JobOptions, ResourceMix any_notification_templates = set(any_notification_templates + list(base_notification_templates.filter(organization_notification_templates_for_any=self.project.organization))) return dict(error=list(error_notification_templates), success=list(success_notification_templates), any=list(any_notification_templates)) -class Job(UnifiedJob, SurveyJob, JobOptions, JobNotificationMixin): +class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin): ''' A job applies a project (with playbook) to an inventory source with a given credential. It represents a single invocation of ansible-playbook with the diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 9cb6b18b77..39675cae7c 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -10,7 +10,7 @@ from awx.main.models.rbac import ( ) -__all__ = ['ResourceMixin'] +__all__ = ['ResourceMixin', 'SurveyJobTemplateMixin', 'SurveyJobMixin'] class ResourceMixin(models.Model): @@ -61,3 +61,155 @@ class ResourceMixin(models.Model): return get_roles_on_resource(self, accessor) + +class SurveyJobTemplateMixin(models.Model): + class Meta: + abstract = True + + survey_enabled = models.BooleanField( + default=False, + ) + survey_spec = JSONField( + blank=True, + default={}, + ) + + def survey_password_variables(self): + vars = [] + if self.survey_enabled and 'spec' in self.survey_spec: + # Get variables that are type password + for survey_element in self.survey_spec['spec']: + if survey_element['type'] == 'password': + vars.append(survey_element['variable']) + return vars + + @property + def variables_needed_to_start(self): + vars = [] + if self.survey_enabled and 'spec' in self.survey_spec: + for survey_element in self.survey_spec['spec']: + if survey_element['required']: + vars.append(survey_element['variable']) + return vars + + def _update_unified_job_kwargs(self, **kwargs): + ''' + Combine extra_vars with variable precedence order: + JT extra_vars -> JT survey defaults -> runtime extra_vars + ''' + if 'launch_type' in kwargs and kwargs['launch_type'] == 'relaunch': + return kwargs + + # Job Template extra_vars + extra_vars = self.extra_vars_dict + + # Overwrite with job template extra vars with survey default vars + if self.survey_enabled and 'spec' in self.survey_spec: + for survey_element in self.survey_spec.get("spec", []): + if 'default' in survey_element and survey_element['default']: + extra_vars[survey_element['variable']] = survey_element['default'] + + # transform to dict + if 'extra_vars' in kwargs: + kwargs_extra_vars = kwargs['extra_vars'] + kwargs_extra_vars = parse_yaml_or_json(kwargs_extra_vars) + else: + kwargs_extra_vars = {} + + # Overwrite job template extra vars with explicit job extra vars + # and add on job extra vars + extra_vars.update(kwargs_extra_vars) + kwargs['extra_vars'] = json.dumps(extra_vars) + return kwargs + + def survey_variable_validation(self, data): + errors = [] + if not self.survey_enabled: + return errors + if 'name' not in self.survey_spec: + errors.append("'name' missing from survey spec.") + if 'description' not in self.survey_spec: + errors.append("'description' missing from survey spec.") + for survey_element in self.survey_spec.get("spec", []): + if survey_element['variable'] not in data and \ + survey_element['required']: + errors.append("'%s' value missing" % survey_element['variable']) + elif survey_element['type'] in ["textarea", "text", "password"]: + if survey_element['variable'] in data: + if type(data[survey_element['variable']]) not in (str, unicode): + errors.append("Value %s for '%s' expected to be a string." % (data[survey_element['variable']], + survey_element['variable'])) + continue + if 'min' in survey_element and survey_element['min'] not in ["", None] and len(data[survey_element['variable']]) < int(survey_element['min']): + errors.append("'%s' value %s is too small (length is %s must be at least %s)." % + (survey_element['variable'], data[survey_element['variable']], len(data[survey_element['variable']]), survey_element['min'])) + if 'max' in survey_element and survey_element['max'] not in ["", None] and len(data[survey_element['variable']]) > int(survey_element['max']): + errors.append("'%s' value %s is too large (must be no more than %s)." % + (survey_element['variable'], data[survey_element['variable']], survey_element['max'])) + elif survey_element['type'] == 'integer': + if survey_element['variable'] in data: + if type(data[survey_element['variable']]) != int: + errors.append("Value %s for '%s' expected to be an integer." % (data[survey_element['variable']], + survey_element['variable'])) + continue + if 'min' in survey_element and survey_element['min'] not in ["", None] and survey_element['variable'] in data and \ + data[survey_element['variable']] < int(survey_element['min']): + errors.append("'%s' value %s is too small (must be at least %s)." % + (survey_element['variable'], data[survey_element['variable']], survey_element['min'])) + if 'max' in survey_element and survey_element['max'] not in ["", None] and survey_element['variable'] in data and \ + data[survey_element['variable']] > int(survey_element['max']): + errors.append("'%s' value %s is too large (must be no more than %s)." % + (survey_element['variable'], data[survey_element['variable']], survey_element['max'])) + elif survey_element['type'] == 'float': + if survey_element['variable'] in data: + if type(data[survey_element['variable']]) not in (float, int): + errors.append("Value %s for '%s' expected to be a numeric type." % (data[survey_element['variable']], + survey_element['variable'])) + continue + if 'min' in survey_element and survey_element['min'] not in ["", None] and data[survey_element['variable']] < float(survey_element['min']): + errors.append("'%s' value %s is too small (must be at least %s)." % + (survey_element['variable'], data[survey_element['variable']], survey_element['min'])) + if 'max' in survey_element and survey_element['max'] not in ["", None] and data[survey_element['variable']] > float(survey_element['max']): + errors.append("'%s' value %s is too large (must be no more than %s)." % + (survey_element['variable'], data[survey_element['variable']], survey_element['max'])) + elif survey_element['type'] == 'multiselect': + if survey_element['variable'] in data: + if type(data[survey_element['variable']]) != list: + errors.append("'%s' value is expected to be a list." % survey_element['variable']) + else: + for val in data[survey_element['variable']]: + if val not in survey_element['choices']: + errors.append("Value %s for '%s' expected to be one of %s." % (val, survey_element['variable'], + survey_element['choices'])) + elif survey_element['type'] == 'multiplechoice': + if survey_element['variable'] in data: + if data[survey_element['variable']] not in survey_element['choices']: + errors.append("Value %s for '%s' expected to be one of %s." % (data[survey_element['variable']], + survey_element['variable'], + survey_element['choices'])) + return errors + + +class SurveyJobMixin(models.Model): + class Meta: + abstract = True + + survey_passwords = JSONField( + blank=True, + default={}, + editable=False, + ) + + def display_extra_vars(self): + ''' + Hides fields marked as passwords in survey. + ''' + if self.survey_passwords: + extra_vars = json.loads(self.extra_vars) + for key, value in self.survey_passwords.items(): + if key in extra_vars: + extra_vars[key] = value + return json.dumps(extra_vars) + else: + return self.extra_vars + diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index da37caeebd..e9925a6b17 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -36,7 +36,7 @@ from awx.main.utils import decrypt_field, _inventory_updates from awx.main.redact import UriCleaner, REPLACE_STR from awx.main.consumers import emit_channel_notification -__all__ = ['UnifiedJobTemplate', 'UnifiedJob', 'SurveyJobTemplate', 'SurveyJob'] +__all__ = ['UnifiedJobTemplate', 'UnifiedJob'] logger = logging.getLogger('awx.main.models.unified_jobs') @@ -937,154 +937,3 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique if settings.BROKER_URL.startswith('amqp://'): self._force_cancel() return self.cancel_flag - -class SurveyJobTemplate(models.Model): - class Meta: - abstract = True - - survey_enabled = models.BooleanField( - default=False, - ) - survey_spec = JSONField( - blank=True, - default={}, - ) - - def survey_password_variables(self): - vars = [] - if self.survey_enabled and 'spec' in self.survey_spec: - # Get variables that are type password - for survey_element in self.survey_spec['spec']: - if survey_element['type'] == 'password': - vars.append(survey_element['variable']) - return vars - - @property - def variables_needed_to_start(self): - vars = [] - if self.survey_enabled and 'spec' in self.survey_spec: - for survey_element in self.survey_spec['spec']: - if survey_element['required']: - vars.append(survey_element['variable']) - return vars - - def _update_unified_job_kwargs(self, **kwargs): - ''' - Combine extra_vars with variable precedence order: - JT extra_vars -> JT survey defaults -> runtime extra_vars - ''' - if 'launch_type' in kwargs and kwargs['launch_type'] == 'relaunch': - return kwargs - - # Job Template extra_vars - extra_vars = self.extra_vars_dict - - # Overwrite with job template extra vars with survey default vars - if self.survey_enabled and 'spec' in self.survey_spec: - for survey_element in self.survey_spec.get("spec", []): - if 'default' in survey_element and survey_element['default']: - extra_vars[survey_element['variable']] = survey_element['default'] - - # transform to dict - if 'extra_vars' in kwargs: - kwargs_extra_vars = kwargs['extra_vars'] - kwargs_extra_vars = parse_yaml_or_json(kwargs_extra_vars) - else: - kwargs_extra_vars = {} - - # Overwrite job template extra vars with explicit job extra vars - # and add on job extra vars - extra_vars.update(kwargs_extra_vars) - kwargs['extra_vars'] = json.dumps(extra_vars) - return kwargs - - def survey_variable_validation(self, data): - errors = [] - if not self.survey_enabled: - return errors - if 'name' not in self.survey_spec: - errors.append("'name' missing from survey spec.") - if 'description' not in self.survey_spec: - errors.append("'description' missing from survey spec.") - for survey_element in self.survey_spec.get("spec", []): - if survey_element['variable'] not in data and \ - survey_element['required']: - errors.append("'%s' value missing" % survey_element['variable']) - elif survey_element['type'] in ["textarea", "text", "password"]: - if survey_element['variable'] in data: - if type(data[survey_element['variable']]) not in (str, unicode): - errors.append("Value %s for '%s' expected to be a string." % (data[survey_element['variable']], - survey_element['variable'])) - continue - if 'min' in survey_element and survey_element['min'] not in ["", None] and len(data[survey_element['variable']]) < int(survey_element['min']): - errors.append("'%s' value %s is too small (length is %s must be at least %s)." % - (survey_element['variable'], data[survey_element['variable']], len(data[survey_element['variable']]), survey_element['min'])) - if 'max' in survey_element and survey_element['max'] not in ["", None] and len(data[survey_element['variable']]) > int(survey_element['max']): - errors.append("'%s' value %s is too large (must be no more than %s)." % - (survey_element['variable'], data[survey_element['variable']], survey_element['max'])) - elif survey_element['type'] == 'integer': - if survey_element['variable'] in data: - if type(data[survey_element['variable']]) != int: - errors.append("Value %s for '%s' expected to be an integer." % (data[survey_element['variable']], - survey_element['variable'])) - continue - if 'min' in survey_element and survey_element['min'] not in ["", None] and survey_element['variable'] in data and \ - data[survey_element['variable']] < int(survey_element['min']): - errors.append("'%s' value %s is too small (must be at least %s)." % - (survey_element['variable'], data[survey_element['variable']], survey_element['min'])) - if 'max' in survey_element and survey_element['max'] not in ["", None] and survey_element['variable'] in data and \ - data[survey_element['variable']] > int(survey_element['max']): - errors.append("'%s' value %s is too large (must be no more than %s)." % - (survey_element['variable'], data[survey_element['variable']], survey_element['max'])) - elif survey_element['type'] == 'float': - if survey_element['variable'] in data: - if type(data[survey_element['variable']]) not in (float, int): - errors.append("Value %s for '%s' expected to be a numeric type." % (data[survey_element['variable']], - survey_element['variable'])) - continue - if 'min' in survey_element and survey_element['min'] not in ["", None] and data[survey_element['variable']] < float(survey_element['min']): - errors.append("'%s' value %s is too small (must be at least %s)." % - (survey_element['variable'], data[survey_element['variable']], survey_element['min'])) - if 'max' in survey_element and survey_element['max'] not in ["", None] and data[survey_element['variable']] > float(survey_element['max']): - errors.append("'%s' value %s is too large (must be no more than %s)." % - (survey_element['variable'], data[survey_element['variable']], survey_element['max'])) - elif survey_element['type'] == 'multiselect': - if survey_element['variable'] in data: - if type(data[survey_element['variable']]) != list: - errors.append("'%s' value is expected to be a list." % survey_element['variable']) - else: - for val in data[survey_element['variable']]: - if val not in survey_element['choices']: - errors.append("Value %s for '%s' expected to be one of %s." % (val, survey_element['variable'], - survey_element['choices'])) - elif survey_element['type'] == 'multiplechoice': - if survey_element['variable'] in data: - if data[survey_element['variable']] not in survey_element['choices']: - errors.append("Value %s for '%s' expected to be one of %s." % (data[survey_element['variable']], - survey_element['variable'], - survey_element['choices'])) - return errors - - -class SurveyJob(models.Model): - class Meta: - abstract = True - - survey_passwords = JSONField( - blank=True, - default={}, - editable=False, - ) - - def display_extra_vars(self): - ''' - Hides fields marked as passwords in survey. - ''' - if self.survey_passwords: - extra_vars = json.loads(self.extra_vars) - for key, value in self.survey_passwords.items(): - if key in extra_vars: - extra_vars[key] = value - return json.dumps(extra_vars) - else: - return self.extra_vars diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index a25bfebb3c..70e34e64eb 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -13,7 +13,6 @@ from jsonfield import JSONField # AWX from awx.main.models import UnifiedJobTemplate, UnifiedJob -from awx.main.models.unified_jobs import SurveyJobTemplate, SurveyJob from awx.main.models.notifications import JobNotificationMixin from awx.main.models.base import BaseModel, CreatedModifiedModel, VarsDictProperty from awx.main.models.rbac import ( @@ -21,7 +20,7 @@ from awx.main.models.rbac import ( ROLE_SINGLETON_SYSTEM_AUDITOR ) from awx.main.fields import ImplicitRoleField -from awx.main.models.mixins import ResourceMixin +from awx.main.models.mixins import ResourceMixin, SurveyJobTemplateMixin, SurveyJobMixin from awx.main.redact import REPLACE_STR from awx.main.utils import parse_yaml_or_json @@ -264,7 +263,7 @@ class WorkflowJobOptions(BaseModel): extra_vars_dict = VarsDictProperty('extra_vars', True) -class WorkflowJobTemplate(UnifiedJobTemplate, SurveyJobTemplate, WorkflowJobOptions, ResourceMixin): +class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTemplateMixin, ResourceMixin): class Meta: app_label = 'main' @@ -294,7 +293,7 @@ class WorkflowJobTemplate(UnifiedJobTemplate, SurveyJobTemplate, WorkflowJobOpti @classmethod def _get_unified_job_field_names(cls): - return ['name', 'description', 'extra_vars', 'labels', 'schedule', 'launch_type'] + return ['name', 'description', 'extra_vars', 'labels', 'survey_passwords', 'schedule', 'launch_type'] def get_absolute_url(self): return reverse('api:workflow_job_template_detail', args=(self.pk,)) @@ -341,6 +340,10 @@ class WorkflowJobTemplate(UnifiedJobTemplate, SurveyJobTemplate, WorkflowJobOpti return prompted_fields, ignored_fields + def can_start_without_user_input(self): + '''Return whether WFJT can be launched without survey passwords.''' + return bool(self.variables_needed_to_start) + def get_warnings(self): warning_data = {} for node in self.workflow_job_template_nodes.all(): @@ -395,7 +398,7 @@ class WorkflowJobInheritNodesMixin(object): self._inherit_relationship(old_node, new_node, node_ids_map, node_type) -class WorkflowJob(UnifiedJob, SurveyJob, WorkflowJobOptions, JobNotificationMixin, WorkflowJobInheritNodesMixin): +class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificationMixin, WorkflowJobInheritNodesMixin): class Meta: app_label = 'main' From 9b7d046cec78b8e0790c3220fbbfa6bf91732f63 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 19 Oct 2016 09:02:09 -0400 Subject: [PATCH 03/11] move job tests to new survey testing file --- awx/api/serializers.py | 42 ++----------------- awx/api/views.py | 7 ++-- ...test_job_unit.py => test_survey_models.py} | 0 3 files changed, 7 insertions(+), 42 deletions(-) rename awx/main/tests/unit/models/{test_job_unit.py => test_survey_models.py} (100%) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index f3e8419752..205d66aa7f 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2633,26 +2633,7 @@ class WorkflowJobLaunchSerializer(BaseSerializer): def validate(self, attrs): errors = {} - obj = self.context.get('obj') - data = self.context.get('data') - - for field in obj.resources_needed_to_start: - if not (attrs.get(field, False) and obj._ask_for_vars_dict().get(field, False)): - errors[field] = "Job Template '%s' is missing or undefined." % field - - if (not obj.ask_credential_on_launch) or (not attrs.get('credential', None)): - credential = obj.credential - else: - credential = attrs.get('credential', None) - - # fill passwords dict with request data passwords - if credential and credential.passwords_needed: - passwords = self.context.get('passwords') - try: - for p in credential.passwords_needed: - passwords[p] = data[p] - except KeyError: - errors['passwords_needed_to_start'] = credential.passwords_needed + obj = self.instance extra_vars = attrs.get('extra_vars', {}) @@ -2674,27 +2655,12 @@ class WorkflowJobLaunchSerializer(BaseSerializer): if validation_errors: errors['variables_needed_to_start'] = validation_errors - # Special prohibited cases for scan jobs - errors.update(obj._extra_job_type_errors(data)) - if errors: raise serializers.ValidationError(errors) - JT_extra_vars = obj.extra_vars - JT_limit = obj.limit - JT_job_type = obj.job_type - JT_job_tags = obj.job_tags - JT_skip_tags = obj.skip_tags - JT_inventory = obj.inventory - JT_credential = obj.credential - attrs = super(JobLaunchSerializer, self).validate(attrs) - obj.extra_vars = JT_extra_vars - obj.limit = JT_limit - obj.job_type = JT_job_type - obj.skip_tags = JT_skip_tags - obj.job_tags = JT_job_tags - obj.inventory = JT_inventory - obj.credential = JT_credential + WFJT_extra_vars = obj.extra_vars + attrs = super(WorkflowJobLaunchSerializer, self).validate(attrs) + obj.extra_vars = WFJT_extra_vars return attrs class NotificationTemplateSerializer(BaseSerializer): diff --git a/awx/api/views.py b/awx/api/views.py index 4431a6e825..972204a98b 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2815,10 +2815,9 @@ class WorkflowJobTemplateLaunch(GenericAPIView): if not request.user.can_access(self.model, 'start', obj): raise PermissionDenied() - -# serializer = self.serializer_class(instance=obj, data=request.data, context={'obj': obj, 'data': request.data, 'passwords': passwords}) -# if not serializer.is_valid(): -# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + 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 = obj._accept_or_ignore_job_kwargs(**request.data) diff --git a/awx/main/tests/unit/models/test_job_unit.py b/awx/main/tests/unit/models/test_survey_models.py similarity index 100% rename from awx/main/tests/unit/models/test_job_unit.py rename to awx/main/tests/unit/models/test_survey_models.py From 8ff671bfd6c7ad7362eb0287c4e72546f23f2ad8 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 19 Oct 2016 10:11:04 -0400 Subject: [PATCH 04/11] finish editing the WFJT launch validate method --- awx/api/serializers.py | 15 ++++++++------- awx/api/views.py | 7 ++++--- .../tests/unit/models/test_survey_models.py | 18 +++++++++++++++++- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 205d66aa7f..d04bf0e348 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2609,19 +2609,20 @@ class JobLaunchSerializer(BaseSerializer): class WorkflowJobLaunchSerializer(BaseSerializer): - can_start_without_user_input = serializers.BooleanField(read_only=True) - variables_needed_to_start = serializers.ReadOnlyField() - + # can_start_without_user_input = serializers.BooleanField(read_only=True) + # variables_needed_to_start = serializers.ReadOnlyField() + survey_enabled = serializers.SerializerMethodField() extra_vars = VerbatimField(required=False, write_only=True) - workflow_job_template_data = serializers.SerializerMethodField() + # workflow_job_template_data = serializers.SerializerMethodField() + # warnings = class Meta: model = WorkflowJobTemplate - fields = ('can_start_without_user_input', + fields = ('*',#'can_start_without_user_input', 'extra_vars', - 'survey_enabled', 'variables_needed_to_start', - 'workflow_job_template_data') + 'survey_enabled')#, 'variables_needed_to_start', + # 'workflow_job_template_data') def get_survey_enabled(self, obj): if obj: diff --git a/awx/api/views.py b/awx/api/views.py index 972204a98b..6d1ddbe57b 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2786,12 +2786,13 @@ class WorkflowJobTemplateLabelList(JobTemplateLabelList): new_in_310 = True -# TODO: -class WorkflowJobTemplateLaunch(GenericAPIView): +class WorkflowJobTemplateLaunch(RetrieveAPIView): model = WorkflowJobTemplate serializer_class = WorkflowJobLaunchSerializer new_in_310 = True + is_job_start = True + always_allow_superuser = False def update_raw_data(self, data): obj = self.get_object() @@ -2802,7 +2803,7 @@ class WorkflowJobTemplateLaunch(GenericAPIView): if extra_vars: data['extra_vars'] = extra_vars return data - + # def get(self, request, *args, **kwargs): # data = {} # obj = self.get_object() diff --git a/awx/main/tests/unit/models/test_survey_models.py b/awx/main/tests/unit/models/test_survey_models.py index ff55d7103b..e197e9339a 100644 --- a/awx/main/tests/unit/models/test_survey_models.py +++ b/awx/main/tests/unit/models/test_survey_models.py @@ -2,7 +2,11 @@ import pytest import json from awx.main.tasks import RunJob -from awx.main.models import Job +from awx.main.models import ( + Job, + WorkflowJob, + WorkflowJobTemplate +) @pytest.fixture @@ -65,3 +69,15 @@ def test_job_args_unredacted_passwords(job): ev_index = args.index('-e') + 1 extra_vars = json.loads(args[ev_index]) assert extra_vars['secret_key'] == 'my_password' + +class TestWorkflowSurveys: + def test_update_kwargs_survey_defaults(self, survey_spec_factory): + "Assure that the survey default over-rides a JT variable" + spec = survey_spec_factory('var1') + spec['spec'][0]['default'] = 3 + wfjt = WorkflowJobTemplate( + name="test-wfjt", + survey_spec=spec, + extra_vars="var1: 5" + ) + assert json.loads(wfjt._update_unified_job_kwargs()['extra_vars'])['var1'] == 3 From 1bd8bd245b96f5789a2c718a43550043af997af2 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 28 Oct 2016 11:52:00 -0400 Subject: [PATCH 05/11] bump migrations, and enable survey license check --- awx/main/access.py | 14 +++++++------- ...ow_surveys.py => 0045_v310_workflow_surveys.py} | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) rename awx/main/migrations/{0041_v310_workflow_surveys.py => 0045_v310_workflow_surveys.py} (93%) diff --git a/awx/main/access.py b/awx/main/access.py index a33503920f..e12adc60d6 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1520,8 +1520,8 @@ class WorkflowJobTemplateAccess(BaseAccess): return True # will check this if surveys are added to WFJT - # if 'survey_enabled' in data and data['survey_enabled']: - # self.check_license(feature='surveys') + if 'survey_enabled' in data and data['survey_enabled']: + self.check_license(feature='surveys') return self.check_related('organization', Organization, data) @@ -1530,8 +1530,8 @@ class WorkflowJobTemplateAccess(BaseAccess): # check basic license, node count self.check_license() # if surveys are added to WFJTs, check license here - # if obj.survey_enabled: - # self.check_license(feature='surveys') + if obj.survey_enabled: + self.check_license(feature='surveys') # Super users can start any job if self.user.is_superuser: @@ -1540,9 +1540,9 @@ class WorkflowJobTemplateAccess(BaseAccess): return self.user in obj.execute_role def can_change(self, obj, data): - # # Check survey license if surveys are added to WFJTs - # if 'survey_enabled' in data and obj.survey_enabled != data['survey_enabled'] and data['survey_enabled']: - # self.check_license(feature='surveys') + # Check survey license if surveys are added to WFJTs + if 'survey_enabled' in data and obj.survey_enabled != data['survey_enabled'] and data['survey_enabled']: + self.check_license(feature='surveys') if self.user.is_superuser: return True diff --git a/awx/main/migrations/0041_v310_workflow_surveys.py b/awx/main/migrations/0045_v310_workflow_surveys.py similarity index 93% rename from awx/main/migrations/0041_v310_workflow_surveys.py rename to awx/main/migrations/0045_v310_workflow_surveys.py index 1df93bfe35..36cd0a78e0 100644 --- a/awx/main/migrations/0041_v310_workflow_surveys.py +++ b/awx/main/migrations/0045_v310_workflow_surveys.py @@ -8,7 +8,7 @@ import jsonfield.fields class Migration(migrations.Migration): dependencies = [ - ('main', '0040_v310_artifacts'), + ('main', '0044_v310_project_playbook_files'), ] operations = [ From 64b5e2ba5b5c4b2af21fbe7151d13eaf34a00e53 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 7 Nov 2016 14:43:53 -0500 Subject: [PATCH 06/11] bump migration --- ...5_v310_workflow_surveys.py => 0048_v310_workflow_surveys.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename awx/main/migrations/{0045_v310_workflow_surveys.py => 0048_v310_workflow_surveys.py} (93%) diff --git a/awx/main/migrations/0045_v310_workflow_surveys.py b/awx/main/migrations/0048_v310_workflow_surveys.py similarity index 93% rename from awx/main/migrations/0045_v310_workflow_surveys.py rename to awx/main/migrations/0048_v310_workflow_surveys.py index 36cd0a78e0..2515f618d7 100644 --- a/awx/main/migrations/0045_v310_workflow_surveys.py +++ b/awx/main/migrations/0048_v310_workflow_surveys.py @@ -8,7 +8,7 @@ import jsonfield.fields class Migration(migrations.Migration): dependencies = [ - ('main', '0044_v310_project_playbook_files'), + ('main', '0047_v310_tower_state'), ] operations = [ From a87a56f5187a4e81f555c73a3987b9264571f519 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 7 Nov 2016 15:13:20 -0500 Subject: [PATCH 07/11] workflow JT survey and launch serializer in functional state --- awx/api/serializers.py | 19 ++++++++++--------- awx/api/views.py | 12 ++++-------- awx/main/access.py | 3 ++- awx/main/models/workflow.py | 4 ++-- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index d04bf0e348..c8a7b98cfc 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2609,20 +2609,21 @@ class JobLaunchSerializer(BaseSerializer): class WorkflowJobLaunchSerializer(BaseSerializer): - # can_start_without_user_input = serializers.BooleanField(read_only=True) - # variables_needed_to_start = serializers.ReadOnlyField() - + can_start_without_user_input = serializers.BooleanField(read_only=True) + variables_needed_to_start = serializers.ReadOnlyField() survey_enabled = serializers.SerializerMethodField() extra_vars = VerbatimField(required=False, write_only=True) - # workflow_job_template_data = serializers.SerializerMethodField() - # warnings = + warnings = serializers.SerializerMethodField() + workflow_job_template_data = serializers.SerializerMethodField() class Meta: model = WorkflowJobTemplate - fields = ('*',#'can_start_without_user_input', - 'extra_vars', - 'survey_enabled')#, 'variables_needed_to_start', - # 'workflow_job_template_data') + fields = ('can_start_without_user_input', 'extra_vars', 'warnings', + 'survey_enabled', 'variables_needed_to_start', + 'workflow_job_template_data') + + def get_warnings(self, obj): + return obj.get_warnings() def get_survey_enabled(self, obj): if obj: diff --git a/awx/api/views.py b/awx/api/views.py index 6d1ddbe57b..ba76e777b3 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2803,13 +2803,6 @@ class WorkflowJobTemplateLaunch(RetrieveAPIView): if extra_vars: data['extra_vars'] = extra_vars return data - - # def get(self, request, *args, **kwargs): - # data = {} - # obj = self.get_object() - # data['warnings'] = obj.get_warnings() - # data['variables_needed_to_start'] = obj.variables_needed_to_start - # return Response(data) def post(self, request, *args, **kwargs): obj = self.get_object() @@ -2824,8 +2817,11 @@ class WorkflowJobTemplateLaunch(RetrieveAPIView): new_job = obj.create_unified_job(**prompted_fields) new_job.signal_start(**prompted_fields) - data = dict(workflow_job=new_job.id) + + data = OrderedDict() data['ignored_fields'] = ignored_fields + data.update(WorkflowJobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job)) + data['workflow_job'] = new_job.id return Response(data, status=status.HTTP_201_CREATED) # TODO: diff --git a/awx/main/access.py b/awx/main/access.py index e12adc60d6..c3dc82a098 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1541,7 +1541,8 @@ class WorkflowJobTemplateAccess(BaseAccess): def can_change(self, obj, data): # Check survey license if surveys are added to WFJTs - if 'survey_enabled' in data and obj.survey_enabled != data['survey_enabled'] and data['survey_enabled']: + if (data and 'survey_enabled' in data and + obj.survey_enabled != data['survey_enabled'] and data['survey_enabled']): self.check_license(feature='surveys') if self.user.is_superuser: diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 70e34e64eb..f22694ca94 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -332,9 +332,9 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl survey_vars = [question['variable'] for question in self.survey_spec.get('spec', [])] for key in extra_vars: if key in survey_vars: - prompted_fields[field][key] = extra_vars[key] + prompted_fields['extra_vars'][key] = extra_vars[key] else: - ignored_fields[field][key] = extra_vars[key] + ignored_fields['extra_vars'][key] = extra_vars[key] else: prompted_fields['extra_vars'] = extra_vars From 7f41f16509880595cdb183f6a490e9b292cb5737 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 7 Nov 2016 15:36:57 -0500 Subject: [PATCH 08/11] passwords in WFJT surveys working correctly --- awx/main/models/mixins.py | 3 +++ awx/main/models/workflow.py | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 39675cae7c..2dcc7befef 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -1,3 +1,6 @@ +# Python +import json + # Django from django.db import models from django.contrib.contenttypes.models import ContentType diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index f22694ca94..b24a0dc563 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -232,12 +232,15 @@ class WorkflowJobNode(WorkflowNodeBase): if aa_dict: self.ancestor_artifacts = aa_dict self.save(update_fields=['ancestor_artifacts']) + password_dict = {} if '_ansible_no_log' in aa_dict: - # TODO: merge Workflow Job survey passwords into this - password_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 password_dict: data['survey_passwords'] = password_dict # process extra_vars # TODO: still lack consensus about variable precedence From 76eb0bb86675176f19f6facb2e3f3b388361a03d Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 8 Nov 2016 08:31:27 -0500 Subject: [PATCH 09/11] bump workflow survey migration number --- ...8_v310_workflow_surveys.py => 0049_v310_workflow_surveys.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename awx/main/migrations/{0048_v310_workflow_surveys.py => 0049_v310_workflow_surveys.py} (94%) diff --git a/awx/main/migrations/0048_v310_workflow_surveys.py b/awx/main/migrations/0049_v310_workflow_surveys.py similarity index 94% rename from awx/main/migrations/0048_v310_workflow_surveys.py rename to awx/main/migrations/0049_v310_workflow_surveys.py index 2515f618d7..bc3865d33c 100644 --- a/awx/main/migrations/0048_v310_workflow_surveys.py +++ b/awx/main/migrations/0049_v310_workflow_surveys.py @@ -8,7 +8,7 @@ import jsonfield.fields class Migration(migrations.Migration): dependencies = [ - ('main', '0047_v310_tower_state'), + ('main', '0048_v310_instance_capacity'), ] operations = [ From a1c17a72436d2b109af11f0d8d6224e860f02ede Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 8 Nov 2016 09:19:29 -0500 Subject: [PATCH 10/11] Fix issue where unified_job method overrode the SurveyMixin methods --- awx/main/models/unified_jobs.py | 11 ++++------ awx/main/models/workflow.py | 2 +- .../tests/unit/models/test_survey_models.py | 22 ++++++++++++++++++- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index e9925a6b17..2c46157007 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -292,12 +292,6 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio ''' raise NotImplementedError # Implement in subclass. - def _update_unified_job_kwargs(self, **kwargs): - ''' - Hook for subclasses to update kwargs. - ''' - return kwargs # Override if needed in subclass. - @property def notification_templates(self): ''' @@ -346,7 +340,10 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio m2m_fields[field_name] = getattr(self, field_name) else: create_kwargs[field_name] = getattr(self, field_name) - new_kwargs = self._update_unified_job_kwargs(**create_kwargs) + if hasattr(self, '_update_unified_job_kwargs'): + new_kwargs = self._update_unified_job_kwargs(**create_kwargs) + else: + new_kwargs = create_kwargs unified_job = unified_job_class(**new_kwargs) # For JobTemplate-based jobs with surveys, add passwords to list for perma-redaction if hasattr(self, 'survey_spec') and getattr(self, 'survey_enabled', False): diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index b24a0dc563..0a9c081680 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -345,7 +345,7 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl def can_start_without_user_input(self): '''Return whether WFJT can be launched without survey passwords.''' - return bool(self.variables_needed_to_start) + return not bool(self.variables_needed_to_start) def get_warnings(self): warning_data = {} diff --git a/awx/main/tests/unit/models/test_survey_models.py b/awx/main/tests/unit/models/test_survey_models.py index e197e9339a..bf6148ec55 100644 --- a/awx/main/tests/unit/models/test_survey_models.py +++ b/awx/main/tests/unit/models/test_survey_models.py @@ -75,9 +75,29 @@ class TestWorkflowSurveys: "Assure that the survey default over-rides a JT variable" spec = survey_spec_factory('var1') spec['spec'][0]['default'] = 3 + spec['spec'][0]['required'] = False wfjt = WorkflowJobTemplate( name="test-wfjt", survey_spec=spec, + survey_enabled=True, extra_vars="var1: 5" ) - assert json.loads(wfjt._update_unified_job_kwargs()['extra_vars'])['var1'] == 3 + updated_extra_vars = wfjt._update_unified_job_kwargs() + assert 'extra_vars' in updated_extra_vars + assert json.loads(updated_extra_vars['extra_vars'])['var1'] == 3 + assert wfjt.can_start_without_user_input() + + def test_variables_needed_to_start(self, survey_spec_factory): + "Assure that variables_needed_to_start output contains mandatory vars" + spec = survey_spec_factory(['question1', 'question2', 'question3']) + spec['spec'][0]['required'] = False + spec['spec'][1]['required'] = True + spec['spec'][2]['required'] = False + wfjt = WorkflowJobTemplate( + name="test-wfjt", + survey_spec=spec, + survey_enabled=True, + extra_vars="question2: hiworld" + ) + assert wfjt.variables_needed_to_start == ['question2'] + assert not wfjt.can_start_without_user_input() From 105175b6b44e36b7d905a029029ddef6b32d6430 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 8 Nov 2016 09:46:59 -0500 Subject: [PATCH 11/11] Fix errors in workflow surveys found through tests --- awx/main/models/jobs.py | 1 - awx/main/models/mixins.py | 1 + awx/main/tests/unit/models/test_survey_models.py | 1 - awx/main/utils.py | 3 ++- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 05f02bd5af..7621649889 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -33,7 +33,6 @@ from awx.main.models.notifications import ( JobNotificationMixin, ) from awx.main.utils import ( - decrypt_field, ignore_inventory_computed_fields, parse_yaml_or_json, ) diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 2dcc7befef..efe36bcefc 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -11,6 +11,7 @@ from jsonfield import JSONField from awx.main.models.rbac import ( Role, RoleAncestorEntry, get_roles_on_resource ) +from awx.main.utils import parse_yaml_or_json __all__ = ['ResourceMixin', 'SurveyJobTemplateMixin', 'SurveyJobMixin'] diff --git a/awx/main/tests/unit/models/test_survey_models.py b/awx/main/tests/unit/models/test_survey_models.py index bf6148ec55..4f9653755e 100644 --- a/awx/main/tests/unit/models/test_survey_models.py +++ b/awx/main/tests/unit/models/test_survey_models.py @@ -4,7 +4,6 @@ import json from awx.main.tasks import RunJob from awx.main.models import ( Job, - WorkflowJob, WorkflowJobTemplate ) diff --git a/awx/main/utils.py b/awx/main/utils.py index c513a9bf5d..3614a99039 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -5,6 +5,7 @@ import base64 import hashlib import json +import yaml import logging import os import re @@ -490,7 +491,7 @@ def parse_yaml_or_json(vars_str): except (ValueError, TypeError): try: vars_dict = yaml.safe_load(vars_str) - assert isinstance(extra_vars, dict) + assert isinstance(vars_dict, dict) except (yaml.YAMLError, TypeError, AttributeError, AssertionError): vars_dict = {} return vars_dict