From e8581f6892118e28b3a7483fc6a717a8ba53bf26 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 23 Jul 2019 16:01:42 -0400 Subject: [PATCH 01/10] Implement WFJT prompting for limit & scm_branch add feature to UI and awxkit restructure some details of create_unified_job for workflows to allow use of char_prompts hidden field avoid conflict with sliced jobs in char_prompts copy logic update developer docs update migration reference bump migration --- awx/api/serializers.py | 38 ++++++- awx/api/views/__init__.py | 25 ++++- awx/main/migrations/0085_v360_WFJT_prompts.py | 59 +++++++++++ awx/main/models/inventory.py | 2 +- awx/main/models/jobs.py | 44 +++----- awx/main/models/projects.py | 2 +- awx/main/models/workflow.py | 41 +++---- .../tests/functional/models/test_workflow.py | 100 +++++++++++++++++- .../tests/unit/models/test_workflow_unit.py | 10 +- awx/main/utils/common.py | 74 +++++++++++-- awx/ui/client/src/templates/workflows.form.js | 28 +++++ .../edit-workflow/workflow-edit.controller.js | 4 + .../workflow-results.controller.js | 4 +- .../workflow-results.partial.html | 20 ++++ .../workflows/workflow-add.controller-test.js | 2 + .../api/pages/workflow_job_template_nodes.py | 1 + .../api/pages/workflow_job_templates.py | 5 +- docs/prompting.md | 7 +- docs/workflow.md | 9 +- 19 files changed, 390 insertions(+), 85 deletions(-) create mode 100644 awx/main/migrations/0085_v360_WFJT_prompts.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 66c06c3ce6..3ebbe55ecb 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -3314,11 +3314,14 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo 'admin', 'execute', {'copy': 'organization.workflow_admin'} ] + limit = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None) + scm_branch = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None) class Meta: model = WorkflowJobTemplate fields = ('*', 'extra_vars', 'organization', 'survey_enabled', 'allow_simultaneous', - 'ask_variables_on_launch', 'inventory', 'ask_inventory_on_launch',) + 'ask_variables_on_launch', 'inventory', 'limit', 'scm_branch', + 'ask_inventory_on_launch', 'ask_scm_branch_on_launch', 'ask_limit_on_launch',) def get_related(self, obj): res = super(WorkflowJobTemplateSerializer, self).get_related(obj) @@ -3344,6 +3347,22 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo def validate_extra_vars(self, value): return vars_validate_or_raise(value) + def validate(self, attrs): + attrs = super(WorkflowJobTemplateSerializer, self).validate(attrs) + + # process char_prompts, these are not direct fields on the model + mock_obj = self.Meta.model() + for field_name in ('scm_branch', 'limit'): + if field_name in attrs: + setattr(mock_obj, field_name, attrs[field_name]) + attrs.pop(field_name) + + # Model `.save` needs the container dict, not the psuedo fields + if mock_obj.char_prompts: + attrs['char_prompts'] = mock_obj.char_prompts + + return attrs + class WorkflowJobTemplateWithSpecSerializer(WorkflowJobTemplateSerializer): ''' @@ -3356,13 +3375,15 @@ class WorkflowJobTemplateWithSpecSerializer(WorkflowJobTemplateSerializer): class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer): + limit = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None) + scm_branch = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None) class Meta: model = WorkflowJob fields = ('*', 'workflow_job_template', 'extra_vars', 'allow_simultaneous', 'job_template', 'is_sliced_job', '-execution_node', '-event_processing_finished', '-controller_node', - 'inventory',) + 'inventory', 'limit', 'scm_branch',) def get_related(self, obj): res = super(WorkflowJobSerializer, self).get_related(obj) @@ -4180,12 +4201,16 @@ class WorkflowJobLaunchSerializer(BaseSerializer): queryset=Inventory.objects.all(), required=False, write_only=True ) + limit = serializers.CharField(required=False, write_only=True, allow_blank=True) + scm_branch = serializers.CharField(required=False, write_only=True, allow_blank=True) workflow_job_template_data = serializers.SerializerMethodField() class Meta: model = WorkflowJobTemplate - fields = ('ask_inventory_on_launch', 'can_start_without_user_input', 'defaults', 'extra_vars', - 'inventory', 'survey_enabled', 'variables_needed_to_start', + fields = ('ask_inventory_on_launch', 'ask_limit_on_launch', 'ask_scm_branch_on_launch', + 'can_start_without_user_input', 'defaults', 'extra_vars', + 'inventory', 'limit', 'scm_branch', + 'survey_enabled', 'variables_needed_to_start', 'node_templates_missing', 'node_prompts_rejected', 'workflow_job_template_data', 'survey_enabled', 'ask_variables_on_launch') read_only_fields = ('ask_inventory_on_launch', 'ask_variables_on_launch') @@ -4225,9 +4250,14 @@ class WorkflowJobLaunchSerializer(BaseSerializer): WFJT_extra_vars = template.extra_vars WFJT_inventory = template.inventory + WFJT_limit = template.limit + WFJT_scm_branch = template.scm_branch super(WorkflowJobLaunchSerializer, self).validate(attrs) template.extra_vars = WFJT_extra_vars template.inventory = WFJT_inventory + template.limit = WFJT_limit + template.scm_branch = WFJT_scm_branch + return accepted diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 9302249e67..d77ec92b91 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -3111,6 +3111,17 @@ class WorkflowJobTemplateCopy(CopyAPIView): data.update(messages) return Response(data) + def _build_create_dict(self, obj): + """Special processing of fields managed by char_prompts + """ + r = super(WorkflowJobTemplateCopy, self)._build_create_dict(obj) + field_names = set(f.name for f in obj._meta.get_fields()) + for field_name, ask_field_name in obj.get_ask_mapping().items(): + if field_name in r and field_name not in field_names: + r.setdefault('char_prompts', {}) + r['char_prompts'][field_name] = r.pop(field_name) + return r + @staticmethod def deep_copy_permission_check_func(user, new_objs): for obj in new_objs: @@ -3139,7 +3150,6 @@ class WorkflowJobTemplateLabelList(JobTemplateLabelList): class WorkflowJobTemplateLaunch(RetrieveAPIView): - model = models.WorkflowJobTemplate obj_permission_type = 'start' serializer_class = serializers.WorkflowJobLaunchSerializer @@ -3156,10 +3166,15 @@ class WorkflowJobTemplateLaunch(RetrieveAPIView): extra_vars.setdefault(v, u'') if extra_vars: data['extra_vars'] = extra_vars - if obj.ask_inventory_on_launch: - data['inventory'] = obj.inventory_id - else: - data.pop('inventory', None) + modified_ask_mapping = models.WorkflowJobTemplate.get_ask_mapping() + modified_ask_mapping.pop('extra_vars') + for field_name, ask_field_name in obj.get_ask_mapping().items(): + if not getattr(obj, ask_field_name): + data.pop(field_name, None) + elif field_name == 'inventory': + data[field_name] = getattrd(obj, "%s.%s" % (field_name, 'id'), None) + else: + data[field_name] = getattr(obj, field_name) return data def post(self, request, *args, **kwargs): diff --git a/awx/main/migrations/0085_v360_WFJT_prompts.py b/awx/main/migrations/0085_v360_WFJT_prompts.py new file mode 100644 index 0000000000..7df324e9fa --- /dev/null +++ b/awx/main/migrations/0085_v360_WFJT_prompts.py @@ -0,0 +1,59 @@ +# Generated by Django 2.2.2 on 2019-07-23 17:56 + +import awx.main.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0084_v360_token_description'), + ] + + operations = [ + migrations.AddField( + model_name='workflowjobtemplate', + name='ask_limit_on_launch', + field=awx.main.fields.AskForField(blank=True, default=False), + ), + migrations.AddField( + model_name='workflowjobtemplate', + name='ask_scm_branch_on_launch', + field=awx.main.fields.AskForField(blank=True, default=False), + ), + migrations.AddField( + model_name='workflowjobtemplate', + name='char_prompts', + field=awx.main.fields.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='joblaunchconfig', + name='inventory', + field=models.ForeignKey(blank=True, default=None, help_text='Inventory applied as a prompt, assuming job template prompts for inventory', null=True, on_delete=models.deletion.SET_NULL, related_name='joblaunchconfigs', to='main.Inventory'), + ), + migrations.AlterField( + model_name='schedule', + name='inventory', + field=models.ForeignKey(blank=True, default=None, help_text='Inventory applied as a prompt, assuming job template prompts for inventory', null=True, on_delete=models.deletion.SET_NULL, related_name='schedules', to='main.Inventory'), + ), + migrations.AlterField( + model_name='workflowjob', + name='inventory', + field=models.ForeignKey(blank=True, default=None, help_text='Inventory applied as a prompt, assuming job template prompts for inventory', null=True, on_delete=models.deletion.SET_NULL, related_name='workflowjobs', to='main.Inventory'), + ), + migrations.AlterField( + model_name='workflowjobnode', + name='inventory', + field=models.ForeignKey(blank=True, default=None, help_text='Inventory applied as a prompt, assuming job template prompts for inventory', null=True, on_delete=models.deletion.SET_NULL, related_name='workflowjobnodes', to='main.Inventory'), + ), + migrations.AlterField( + model_name='workflowjobtemplate', + name='inventory', + field=models.ForeignKey(blank=True, default=None, help_text='Inventory applied as a prompt, assuming job template prompts for inventory', null=True, on_delete=models.deletion.SET_NULL, related_name='workflowjobtemplates', to='main.Inventory'), + ), + migrations.AlterField( + model_name='workflowjobtemplatenode', + name='inventory', + field=models.ForeignKey(blank=True, default=None, help_text='Inventory applied as a prompt, assuming job template prompts for inventory', null=True, on_delete=models.deletion.SET_NULL, related_name='workflowjobtemplatenodes', to='main.Inventory'), + ), + ] diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 8e5f1733ee..7618b36eb3 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1501,7 +1501,7 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE @classmethod def _get_unified_job_field_names(cls): return set(f.name for f in InventorySourceOptions._meta.fields) | set( - ['name', 'description', 'schedule', 'credentials', 'inventory'] + ['name', 'description', 'credentials', 'inventory'] ) def save(self, *args, **kwargs): diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 4986d6f717..058ef87515 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -39,7 +39,7 @@ from awx.main.models.notifications import ( NotificationTemplate, JobNotificationMixin, ) -from awx.main.utils import parse_yaml_or_json, getattr_dne +from awx.main.utils import parse_yaml_or_json, getattr_dne, NullablePromptPsuedoField from awx.main.fields import ImplicitRoleField, JSONField, AskForField from awx.main.models.mixins import ( ResourceMixin, @@ -271,7 +271,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour @classmethod def _get_unified_job_field_names(cls): return set(f.name for f in JobOptions._meta.fields) | set( - ['name', 'description', 'schedule', 'survey_passwords', 'labels', 'credentials', + ['name', 'description', 'survey_passwords', 'labels', 'credentials', 'job_slice_number', 'job_slice_count'] ) @@ -839,25 +839,6 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana host.save() -# Add on aliases for the non-related-model fields -class NullablePromptPsuedoField(object): - """ - Interface for psuedo-property stored in `char_prompts` dict - Used in LaunchTimeConfig and submodels - """ - def __init__(self, field_name): - self.field_name = field_name - - def __get__(self, instance, type=None): - return instance.char_prompts.get(self.field_name, None) - - def __set__(self, instance, value): - if value in (None, {}): - instance.char_prompts.pop(self.field_name, None) - else: - instance.char_prompts[self.field_name] = value - - class LaunchTimeConfigBase(BaseModel): ''' Needed as separate class from LaunchTimeConfig because some models @@ -878,6 +859,7 @@ class LaunchTimeConfigBase(BaseModel): null=True, default=None, on_delete=models.SET_NULL, + help_text=_('Inventory applied as a prompt, assuming job template prompts for inventory') ) # All standard fields are stored in this dictionary field # This is a solution to the nullable CharField problem, specific to prompting @@ -918,7 +900,7 @@ class LaunchTimeConfigBase(BaseModel): ''' Hides fields marked as passwords in survey. ''' - if self.survey_passwords: + if hasattr(self, 'survey_passwords') and self.survey_passwords: extra_vars = parse_yaml_or_json(self.extra_vars).copy() for key, value in self.survey_passwords.items(): if key in extra_vars: @@ -931,6 +913,15 @@ class LaunchTimeConfigBase(BaseModel): return self.display_extra_vars() +for field_name in JobTemplate.get_ask_mapping().keys(): + if field_name == 'extra_vars': + continue + try: + LaunchTimeConfigBase._meta.get_field(field_name) + except FieldDoesNotExist: + setattr(LaunchTimeConfigBase, field_name, NullablePromptPsuedoField(field_name)) + + class LaunchTimeConfig(LaunchTimeConfigBase): ''' Common model for all objects that save details of a saved launch config @@ -964,15 +955,6 @@ class LaunchTimeConfig(LaunchTimeConfigBase): self.extra_data = extra_vars -for field_name in JobTemplate.get_ask_mapping().keys(): - if field_name == 'extra_vars': - continue - try: - LaunchTimeConfig._meta.get_field(field_name) - except FieldDoesNotExist: - setattr(LaunchTimeConfig, field_name, NullablePromptPsuedoField(field_name)) - - class JobLaunchConfig(LaunchTimeConfig): ''' Historical record of user launch-time overrides for a job diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index afd61e8faa..b7b52dcf6b 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -329,7 +329,7 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn @classmethod def _get_unified_job_field_names(cls): return set(f.name for f in ProjectOptions._meta.fields) | set( - ['name', 'description', 'schedule'] + ['name', 'description'] ) def save(self, *args, **kwargs): diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 10df35e561..b2312ab63d 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -19,7 +19,7 @@ from awx.main.models.notifications import ( NotificationTemplate, JobNotificationMixin ) -from awx.main.models.base import BaseModel, CreatedModifiedModel, VarsDictProperty +from awx.main.models.base import CreatedModifiedModel, VarsDictProperty from awx.main.models.rbac import ( ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR @@ -207,11 +207,14 @@ class WorkflowJobNode(WorkflowNodeBase): def prompts_dict(self, *args, **kwargs): r = super(WorkflowJobNode, self).prompts_dict(*args, **kwargs) # Explanation - WFJT extra_vars still break pattern, so they are not - # put through prompts processing, but inventory is only accepted + # put through prompts processing, but inventory and others are 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 + if self.workflow_job: + if self.workflow_job.inventory_id: + # workflow job inventory takes precedence + r['inventory'] = self.workflow_job.inventory + if self.workflow_job.char_prompts: + r.update(self.workflow_job.char_prompts) return r def get_job_kwargs(self): @@ -298,7 +301,7 @@ class WorkflowJobNode(WorkflowNodeBase): return data -class WorkflowJobOptions(BaseModel): +class WorkflowJobOptions(LaunchTimeConfigBase): class Meta: abstract = True @@ -318,10 +321,11 @@ class WorkflowJobOptions(BaseModel): @classmethod def _get_unified_job_field_names(cls): - return set(f.name for f in WorkflowJobOptions._meta.fields) | set( - # NOTE: if other prompts are added to WFJT, put fields in WJOptions, remove inventory - ['name', 'description', 'schedule', 'survey_passwords', 'labels', 'inventory'] + r = set(f.name for f in WorkflowJobOptions._meta.fields) | set( + ['name', 'description', 'survey_passwords', 'labels', 'limit', 'scm_branch'] ) + r.remove('char_prompts') # needed due to copying launch config to launch config + return r def _create_workflow_nodes(self, old_node_list, user=None): node_links = {} @@ -372,19 +376,18 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl on_delete=models.SET_NULL, related_name='workflows', ) - inventory = models.ForeignKey( - 'Inventory', - related_name='%(class)ss', - blank=True, - null=True, - default=None, - on_delete=models.SET_NULL, - help_text=_('Inventory applied to all job templates in workflow that prompt for inventory.'), - ) ask_inventory_on_launch = AskForField( blank=True, default=False, ) + ask_limit_on_launch = AskForField( + blank=True, + default=False, + ) + ask_scm_branch_on_launch = AskForField( + blank=True, + default=False, + ) admin_role = ImplicitRoleField(parent_role=[ 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, 'organization.workflow_admin_role' @@ -515,7 +518,7 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl return WorkflowJob.objects.filter(workflow_job_template=self) -class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificationMixin, LaunchTimeConfigBase): +class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificationMixin): class Meta: app_label = 'main' ordering = ('id',) diff --git a/awx/main/tests/functional/models/test_workflow.py b/awx/main/tests/functional/models/test_workflow.py index cc4c42ecfe..f4daf7d578 100644 --- a/awx/main/tests/functional/models/test_workflow.py +++ b/awx/main/tests/functional/models/test_workflow.py @@ -1,6 +1,8 @@ # Python import pytest +from unittest import mock +import json # AWX from awx.main.models.workflow import ( @@ -248,7 +250,6 @@ class TestWorkflowJobTemplate: test_view = WorkflowJobTemplateNodeSuccessNodesList() nodes = wfjt.workflow_job_template_nodes.all() # test cycle validation - print(nodes[0].success_nodes.get(id=nodes[1].id).failure_nodes.get(id=nodes[2].id)) assert test_view.is_valid_relation(nodes[2], nodes[0]) == {'Error': 'Cycle detected.'} def test_always_success_failure_creation(self, wfjt, admin, get): @@ -270,6 +271,103 @@ class TestWorkflowJobTemplate: wfjt2.validate_unique() +@pytest.mark.django_db +class TestWorkflowJobTemplatePrompts: + """These are tests for prompts that live on the workflow job template model + not the node, prompts apply for entire workflow + """ + @pytest.fixture + def wfjt_prompts(self): + return WorkflowJobTemplate.objects.create( + ask_inventory_on_launch=True, + ask_variables_on_launch=True, + ask_limit_on_launch=True, + ask_scm_branch_on_launch=True + ) + + @pytest.fixture + def prompts_data(self, inventory): + return dict( + inventory=inventory, + extra_vars={'foo': 'bar'}, + limit='webservers', + scm_branch='release-3.3' + ) + + def test_apply_workflow_job_prompts(self, workflow_job_template, wfjt_prompts, prompts_data, inventory): + # null or empty fields used + workflow_job = workflow_job_template.create_unified_job() + assert workflow_job.limit is None + assert workflow_job.inventory is None + assert workflow_job.scm_branch is None + + # fields from prompts used + workflow_job = workflow_job_template.create_unified_job(**prompts_data) + assert json.loads(workflow_job.extra_vars) == {'foo': 'bar'} + assert workflow_job.limit == 'webservers' + assert workflow_job.inventory == inventory + assert workflow_job.scm_branch == 'release-3.3' + + # non-null fields from WFJT used + workflow_job_template.inventory = inventory + workflow_job_template.limit = 'fooo' + workflow_job_template.scm_branch = 'bar' + workflow_job = workflow_job_template.create_unified_job() + assert workflow_job.limit == 'fooo' + assert workflow_job.inventory == inventory + assert workflow_job.scm_branch == 'bar' + + + @pytest.mark.django_db + def test_process_workflow_job_prompts(self, inventory, workflow_job_template, wfjt_prompts, prompts_data): + accepted, rejected, errors = workflow_job_template._accept_or_ignore_job_kwargs(**prompts_data) + assert accepted == {} + assert rejected == prompts_data + assert errors + accepted, rejected, errors = wfjt_prompts._accept_or_ignore_job_kwargs(**prompts_data) + assert accepted == prompts_data + assert rejected == {} + assert not errors + + + @pytest.mark.django_db + def test_set_all_the_prompts(self, post, organization, inventory, org_admin): + r = post( + url = reverse('api:workflow_job_template_list'), + data = dict( + name='My new workflow', + organization=organization.id, + inventory=inventory.id, + limit='foooo', + ask_limit_on_launch=True, + scm_branch='bar', + ask_scm_branch_on_launch=True + ), + user = org_admin, + expect = 201 + ) + wfjt = WorkflowJobTemplate.objects.get(id=r.data['id']) + assert wfjt.char_prompts == { + 'limit': 'foooo', 'scm_branch': 'bar' + } + assert wfjt.ask_scm_branch_on_launch is True + assert wfjt.ask_limit_on_launch is True + + launch_url = r.data['related']['launch'] + with mock.patch('awx.main.queue.CallbackQueueDispatcher.dispatch', lambda self, obj: None): + r = post( + url = launch_url, + data = dict( + scm_branch = 'prompt_branch', + limit = 'prompt_limit' + ), + user = org_admin, + expect=201 + ) + assert r.data['limit'] == 'prompt_limit' + assert r.data['scm_branch'] == 'prompt_branch' + + @pytest.mark.django_db def test_workflow_ancestors(organization): # Spawn order of templates grandparent -> parent -> child diff --git a/awx/main/tests/unit/models/test_workflow_unit.py b/awx/main/tests/unit/models/test_workflow_unit.py index f904cf3b95..f101168a8b 100644 --- a/awx/main/tests/unit/models/test_workflow_unit.py +++ b/awx/main/tests/unit/models/test_workflow_unit.py @@ -3,7 +3,7 @@ import pytest from awx.main.models.jobs import JobTemplate from awx.main.models import Inventory, CredentialType, Credential, Project from awx.main.models.workflow import ( - WorkflowJobTemplate, WorkflowJobTemplateNode, WorkflowJobOptions, + WorkflowJobTemplate, WorkflowJobTemplateNode, WorkflowJob, WorkflowJobNode ) from unittest import mock @@ -33,11 +33,11 @@ class TestWorkflowJobInheritNodesMixin(): def test__create_workflow_job_nodes(self, mocker, job_template_nodes): workflow_job_node_create = mocker.patch('awx.main.models.WorkflowJobTemplateNode.create_workflow_job_node') - mixin = WorkflowJobOptions() - mixin._create_workflow_nodes(job_template_nodes) + workflow_job = WorkflowJob() + workflow_job._create_workflow_nodes(job_template_nodes) for job_template_node in job_template_nodes: - workflow_job_node_create.assert_any_call(workflow_job=mixin) + workflow_job_node_create.assert_any_call(workflow_job=workflow_job) class TestMapWorkflowJobNodes(): @pytest.fixture @@ -236,4 +236,4 @@ class TestWorkflowJobNodeJobKWARGS: def test_get_ask_mapping_integrity(): - assert list(WorkflowJobTemplate.get_ask_mapping().keys()) == ['extra_vars', 'inventory'] + assert list(WorkflowJobTemplate.get_ask_mapping().keys()) == ['extra_vars', 'inventory', 'limit', 'scm_branch'] diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index cf3a511e28..6b76faa6f4 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -19,9 +19,14 @@ from functools import reduce, wraps from decimal import Decimal # Django -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, FieldDoesNotExist from django.utils.translation import ugettext_lazy as _ +from django.utils.functional import cached_property from django.db.models.fields.related import ForeignObjectRel, ManyToManyField +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ManyToManyDescriptor +) from django.db.models.query import QuerySet from django.db.models import Q @@ -42,7 +47,7 @@ __all__ = ['get_object_or_400', 'camelcase_to_underscore', 'underscore_to_camelc 'get_current_apps', 'set_current_apps', 'extract_ansible_vars', 'get_search_fields', 'get_system_task_capacity', 'get_cpu_capacity', 'get_mem_capacity', 'wrap_args_with_proot', 'build_proot_temp_dir', 'check_proot_installed', 'model_to_dict', - 'model_instance_diff', 'parse_yaml_or_json', 'RequireDebugTrueOrTest', + 'NullablePromptPsuedoField', 'model_instance_diff', 'parse_yaml_or_json', 'RequireDebugTrueOrTest', 'has_model_field_prefetched', 'set_environ', 'IllegalArgumentError', 'get_custom_venv_choices', 'get_external_account', 'task_manager_bulk_reschedule', 'schedule_task_manager', 'classproperty', 'create_temporary_fifo'] @@ -435,6 +440,39 @@ def model_to_dict(obj, serializer_mapping=None): return attr_d +class CharPromptDescriptor: + """Class used for identifying nullable launch config fields from class + ex. Schedule.limit + """ + def __init__(self, field): + self.field = field + + +class NullablePromptPsuedoField: + """ + Interface for psuedo-property stored in `char_prompts` dict + Used in LaunchTimeConfig and submodels, defined here to avoid circular imports + """ + def __init__(self, field_name): + self.field_name = field_name + + @cached_property + def field_descriptor(self): + return CharPromptDescriptor(self) + + def __get__(self, instance, type=None): + if instance is None: + # for inspection on class itself + return self.field_descriptor + return instance.char_prompts.get(self.field_name, None) + + def __set__(self, instance, value): + if value in (None, {}): + instance.char_prompts.pop(self.field_name, None) + else: + instance.char_prompts[self.field_name] = value + + def copy_model_by_class(obj1, Class2, fields, kwargs): ''' Creates a new unsaved object of type Class2 using the fields from obj1 @@ -442,9 +480,10 @@ def copy_model_by_class(obj1, Class2, fields, kwargs): ''' create_kwargs = {} for field_name in fields: - # Foreign keys can be specified as field_name or field_name_id. - id_field_name = '%s_id' % field_name - if hasattr(obj1, id_field_name): + descriptor = getattr(Class2, field_name) + if isinstance(descriptor, ForwardManyToOneDescriptor): # ForeignKey + # Foreign keys can be specified as field_name or field_name_id. + id_field_name = '%s_id' % field_name if field_name in kwargs: value = kwargs[field_name] elif id_field_name in kwargs: @@ -454,15 +493,29 @@ def copy_model_by_class(obj1, Class2, fields, kwargs): if hasattr(value, 'id'): value = value.id create_kwargs[id_field_name] = value + elif isinstance(descriptor, CharPromptDescriptor): + # difficult case of copying one launch config to another launch config + new_val = None + if field_name in kwargs: + new_val = kwargs[field_name] + elif hasattr(obj1, 'char_prompts'): + if field_name in obj1.char_prompts: + new_val = obj1.char_prompts[field_name] + elif hasattr(obj1, field_name): + # extremely rare case where a template spawns a launch config - sliced jobs + new_val = getattr(obj1, field_name) + if new_val is not None: + create_kwargs.setdefault('char_prompts', {}) + create_kwargs['char_prompts'][field_name] = new_val + elif isinstance(descriptor, ManyToManyDescriptor): + continue # not coppied in this method elif field_name in kwargs: if field_name == 'extra_vars' and isinstance(kwargs[field_name], dict): create_kwargs[field_name] = json.dumps(kwargs['extra_vars']) elif not isinstance(Class2._meta.get_field(field_name), (ForeignObjectRel, ManyToManyField)): create_kwargs[field_name] = kwargs[field_name] elif hasattr(obj1, field_name): - field_obj = obj1._meta.get_field(field_name) - if not isinstance(field_obj, ManyToManyField): - create_kwargs[field_name] = getattr(obj1, field_name) + create_kwargs[field_name] = getattr(obj1, field_name) # Apply class-specific extra processing for origination of unified jobs if hasattr(obj1, '_update_unified_job_kwargs') and obj1.__class__ != Class2: @@ -481,7 +534,10 @@ def copy_m2m_relationships(obj1, obj2, fields, kwargs=None): ''' for field_name in fields: if hasattr(obj1, field_name): - field_obj = obj1._meta.get_field(field_name) + try: + field_obj = obj1._meta.get_field(field_name) + except FieldDoesNotExist: + continue if isinstance(field_obj, ManyToManyField): # Many to Many can be specified as field_name src_field_value = getattr(obj1, field_name) diff --git a/awx/ui/client/src/templates/workflows.form.js b/awx/ui/client/src/templates/workflows.form.js index 776e75da49..839abfd265 100644 --- a/awx/ui/client/src/templates/workflows.form.js +++ b/awx/ui/client/src/templates/workflows.form.js @@ -89,6 +89,34 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) { }, ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddOrEdit) || !canEditInventory', }, + limit: { + label: i18n._('Limit'), + type: 'text', + column: 1, + awPopOver: "

" + i18n._("Select a limit for the workflow. This limit is applied to all job template nodes that prompt for a limit.") + "

", + dataTitle: i18n._('Limit'), + dataPlacement: 'right', + dataContainer: "body", + subCheckbox: { + variable: 'ask_limit_on_launch', + text: i18n._('Prompt on launch') + }, + ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddOrEdit) || !canEditInventory', + }, + scm_branch: { + label: i18n._('SCM Branch'), + type: 'text', + column: 1, + awPopOver: "

" + i18n._("Select a branch for the workflow. This branch is applied to all job template nodes that prompt for a branch.") + "

", + dataTitle: i18n._('SCM Branch'), + dataPlacement: 'right', + dataContainer: "body", + subCheckbox: { + variable: 'ask_scm_branch_on_launch', + text: i18n._('Prompt on launch') + }, + ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddOrEdit)', + }, labels: { label: i18n._('Labels'), type: 'select', diff --git a/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js b/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js index 53039f6402..5a2ef48adb 100644 --- a/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js +++ b/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js @@ -54,6 +54,8 @@ export default [ $scope.parseType = 'yaml'; $scope.includeWorkflowMaker = false; $scope.ask_inventory_on_launch = workflowJobTemplateData.ask_inventory_on_launch; + $scope.ask_limit_on_launch = workflowJobTemplateData.ask_limit_on_launch; + $scope.ask_scm_branch_on_launch = workflowJobTemplateData.ask_scm_branch_on_launch; $scope.ask_variables_on_launch = (workflowJobTemplateData.ask_variables_on_launch) ? true : false; if (Inventory){ @@ -91,6 +93,8 @@ export default [ } data.ask_inventory_on_launch = Boolean($scope.ask_inventory_on_launch); + data.ask_limit_on_launch = Boolean($scope.ask_limit_on_launch); + data.ask_scm_branch_on_launch = Boolean($scope.ask_scm_branch_on_launch); data.ask_variables_on_launch = Boolean($scope.ask_variables_on_launch); data.extra_vars = ToJSON($scope.parseType, diff --git a/awx/ui/client/src/workflow-results/workflow-results.controller.js b/awx/ui/client/src/workflow-results/workflow-results.controller.js index 13856a430c..871fa87a66 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.controller.js +++ b/awx/ui/client/src/workflow-results/workflow-results.controller.js @@ -69,7 +69,9 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions', SLICE_TEMPLATE: i18n._('Slice Job Template'), JOB_EXPLANATION: i18n._('Explanation'), SOURCE_WORKFLOW_JOB: i18n._('Source Workflow'), - INVENTORY: i18n._('Inventory') + INVENTORY: i18n._('Inventory'), + LIMIT: i18n._('Inventory Limit'), + SCM_BRANCH: i18n._('SCM Branch') }, details: { HEADER: i18n._('DETAILS'), diff --git a/awx/ui/client/src/workflow-results/workflow-results.partial.html b/awx/ui/client/src/workflow-results/workflow-results.partial.html index 61ca637641..635b69c60f 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.partial.html +++ b/awx/ui/client/src/workflow-results/workflow-results.partial.html @@ -140,6 +140,26 @@ + +
+ +
+ {{ workflow.limit }} +
+
+ + +
+ +
+ {{ workflow.scm_branch }} +
+
+
diff --git a/awx/ui/test/spec/workflows/workflow-add.controller-test.js b/awx/ui/test/spec/workflows/workflow-add.controller-test.js index 543e223467..46cf24f730 100644 --- a/awx/ui/test/spec/workflows/workflow-add.controller-test.js +++ b/awx/ui/test/spec/workflows/workflow-add.controller-test.js @@ -144,6 +144,8 @@ describe('Controller: WorkflowAdd', () => { description: "This is a test description", organization: undefined, inventory: undefined, + limit: undefined, + scm_branch: undefined, labels: undefined, variables: undefined, allow_simultaneous: undefined, diff --git a/awxkit/awxkit/api/pages/workflow_job_template_nodes.py b/awxkit/awxkit/api/pages/workflow_job_template_nodes.py index 94928abe16..4a36968f54 100644 --- a/awxkit/awxkit/api/pages/workflow_job_template_nodes.py +++ b/awxkit/awxkit/api/pages/workflow_job_template_nodes.py @@ -25,6 +25,7 @@ class WorkflowJobTemplateNode(HasCreate, base.Base): 'diff_mode', 'extra_data', 'limit', + 'scm_branch', 'job_tags', 'job_type', 'skip_tags', diff --git a/awxkit/awxkit/api/pages/workflow_job_templates.py b/awxkit/awxkit/api/pages/workflow_job_templates.py index 4bfaba19a8..4bbee6778a 100644 --- a/awxkit/awxkit/api/pages/workflow_job_templates.py +++ b/awxkit/awxkit/api/pages/workflow_job_templates.py @@ -48,8 +48,9 @@ class WorkflowJobTemplate(HasCopy, HasCreate, HasNotifications, HasSurvey, Unifi if kwargs.get('inventory'): payload.inventory = kwargs.get('inventory').id - if kwargs.get('ask_inventory_on_launch'): - payload.ask_inventory_on_launch = kwargs.get('ask_inventory_on_launch') + for field_name in ('ask_inventory_on_launch', 'limit', 'scm_branch', 'ask_scm_branch_on_launch'): + if field_name in kwargs: + setattr(payload, field_name, kwargs.get(field_name)) return payload diff --git a/docs/prompting.md b/docs/prompting.md index ebb9c89d8f..926788dcea 100644 --- a/docs/prompting.md +++ b/docs/prompting.md @@ -59,7 +59,7 @@ actions in the API. - POST to `/api/v2/job_templates/N/launch/` - can accept all prompt-able fields - POST to `/api/v2/workflow_job_templates/N/launch/` - - can accept extra_vars and inventory + - can accept certain fields, see `workflow.md` - POST to `/api/v2/system_job_templates/N/launch/` - can accept certain fields, with no user configuration @@ -174,7 +174,7 @@ job. If a user creates a node that would do this, a 400 response will be returne Workflow JTs are different than other cases, because they do not have a template directly linked, so their prompts are a form of action-at-a-distance. -When the node's prompts are gathered, any prompts from the workflow job +When the node's prompts are gathered to spawn its job, any prompts from the workflow job will take precedence over the node's value. As a special exception, `extra_vars` from a workflow will not obey JT survey @@ -182,8 +182,7 @@ and prompting rules, both both historical and ease-of-understanding reasons. This behavior may change in the future. Other than that exception, JT prompting rules are still adhered to when -a job is spawned, although so far this only applies to the workflow job's -`inventory` field. +a job is spawned. #### Job Relaunch and Re-scheduling diff --git a/docs/workflow.md b/docs/workflow.md index 3ccfb7d367..e01acf2e76 100644 --- a/docs/workflow.md +++ b/docs/workflow.md @@ -8,7 +8,7 @@ A workflow has an associated tree-graph that is composed of multiple nodes. Each ### Workflow Create-Read-Update-Delete (CRUD) Like other job resources, workflow jobs are created from workflow job templates. The API exposes common fields similar to job templates, including labels, schedules, notification templates, extra variables and survey specifications. Other than that, in the API, the related workflow graph nodes can be gotten to via the related workflow_nodes field. -The CRUD operations against a workflow job template and its corresponding workflow jobs are almost identical to those of normal job templates and related jobs. However, from an RBAC perspective, CRUD on workflow job templates/jobs are limited to super users. +The CRUD operations against a workflow job template and its corresponding workflow jobs are almost identical to those of normal job templates and related jobs. By default, organization administrators have full control over all workflow job templates under the same organization, and they share these abilities with users who have the `workflow_admin_role` in that organization. Permissions can be further delegated to other users via the workflow job template roles. @@ -20,7 +20,12 @@ Workflow job template nodes are listed and created under endpoint `/workflow_job #### Workflow Launch Configuration Workflow job templates can contain launch configuration items. So far, these only include -`extra_vars` and `inventory`, and the `extra_vars` may have specifications via + - `extra_vars` + - `inventory` + - `limit` + - `scm_branch` + +The `extra_vars` field may have specifications via a survey, in the same way that job templates work. Workflow nodes may also contain the launch-time configuration for the job it will spawn. From 1406ea302606899f820187efc6ceeea54caa2598 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 21 Aug 2019 22:30:13 -0400 Subject: [PATCH 02/10] Fix missing places for ask_limit and ask_scm_branch --- awx/ui/client/lib/models/WorkflowJobTemplate.js | 3 ++- .../workflows/add-workflow/workflow-add.controller.js | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/lib/models/WorkflowJobTemplate.js b/awx/ui/client/lib/models/WorkflowJobTemplate.js index f171098eb8..06d9e9decc 100644 --- a/awx/ui/client/lib/models/WorkflowJobTemplate.js +++ b/awx/ui/client/lib/models/WorkflowJobTemplate.js @@ -58,8 +58,9 @@ function canLaunchWithoutPrompt () { launchData.can_start_without_user_input && !launchData.ask_inventory_on_launch && !launchData.ask_variables_on_launch && - !launchData.survey_enabled && + !launchData.ask_limit_on_launch && !launchData.ask_scm_branch_on_launch && + !launchData.survey_enabled && launchData.variables_needed_to_start.length === 0 ); } diff --git a/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js b/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js index d46038d0f6..5dcb050dfb 100644 --- a/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js +++ b/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js @@ -70,9 +70,11 @@ export default [ data[fld] = $scope[fld]; } } - + data.ask_inventory_on_launch = Boolean($scope.ask_inventory_on_launch); data.ask_variables_on_launch = Boolean($scope.ask_variables_on_launch); + data.ask_limit_on_launch = Boolean($scope.ask_limit_on_launch); + data.ask_scm_branch_on_launch = Boolean($scope.ask_scm_branch_on_launch); data.extra_vars = ToJSON($scope.parseType, $scope.variables, true); From 291528d82397670e3cf8baca5d9f0c9c25701240 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 22 Aug 2019 09:36:36 -0400 Subject: [PATCH 03/10] adjust UI unit tests again bump migration bump migration again --- .../{0085_v360_WFJT_prompts.py => 0088_v360_WFJT_prompts.py} | 2 +- awx/ui/test/spec/workflows/workflow-add.controller-test.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) rename awx/main/migrations/{0085_v360_WFJT_prompts.py => 0088_v360_WFJT_prompts.py} (97%) diff --git a/awx/main/migrations/0085_v360_WFJT_prompts.py b/awx/main/migrations/0088_v360_WFJT_prompts.py similarity index 97% rename from awx/main/migrations/0085_v360_WFJT_prompts.py rename to awx/main/migrations/0088_v360_WFJT_prompts.py index 7df324e9fa..151f85129b 100644 --- a/awx/main/migrations/0085_v360_WFJT_prompts.py +++ b/awx/main/migrations/0088_v360_WFJT_prompts.py @@ -7,7 +7,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('main', '0084_v360_token_description'), + ('main', '0087_v360_update_credential_injector_help_text'), ] operations = [ diff --git a/awx/ui/test/spec/workflows/workflow-add.controller-test.js b/awx/ui/test/spec/workflows/workflow-add.controller-test.js index 46cf24f730..792ae75138 100644 --- a/awx/ui/test/spec/workflows/workflow-add.controller-test.js +++ b/awx/ui/test/spec/workflows/workflow-add.controller-test.js @@ -151,6 +151,8 @@ describe('Controller: WorkflowAdd', () => { allow_simultaneous: undefined, ask_inventory_on_launch: false, ask_variables_on_launch: false, + ask_limit_on_launch: false, + ask_scm_branch_on_launch: false, extra_vars: undefined }); }); From 711c240baf1a8419bc8f92552db7aeee0802fef5 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 5 Sep 2019 15:28:53 -0400 Subject: [PATCH 04/10] Consistently give WJ extra_vars as text --- awx/main/models/jobs.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 058ef87515..0fc9a95d68 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -896,22 +896,6 @@ class LaunchTimeConfigBase(BaseModel): data[prompt_name] = prompt_val return data - def display_extra_vars(self): - ''' - Hides fields marked as passwords in survey. - ''' - if hasattr(self, 'survey_passwords') and self.survey_passwords: - extra_vars = parse_yaml_or_json(self.extra_vars).copy() - for key, value in self.survey_passwords.items(): - if key in extra_vars: - extra_vars[key] = value - return extra_vars - else: - return self.extra_vars - - def display_extra_data(self): - return self.display_extra_vars() - for field_name in JobTemplate.get_ask_mapping().keys(): if field_name == 'extra_vars': @@ -954,6 +938,22 @@ class LaunchTimeConfig(LaunchTimeConfigBase): def extra_vars(self, extra_vars): self.extra_data = extra_vars + def display_extra_vars(self): + ''' + Hides fields marked as passwords in survey. + ''' + if hasattr(self, 'survey_passwords') and self.survey_passwords: + extra_vars = parse_yaml_or_json(self.extra_vars).copy() + for key, value in self.survey_passwords.items(): + if key in extra_vars: + extra_vars[key] = value + return extra_vars + else: + return self.extra_vars + + def display_extra_data(self): + return self.display_extra_vars() + class JobLaunchConfig(LaunchTimeConfig): ''' From 01bb32ebb03958aa4704f1240f033aba05cbf02f Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 6 Sep 2019 16:04:10 -0400 Subject: [PATCH 05/10] Deal with limit prompting in factory --- awxkit/awxkit/api/pages/workflow_job_templates.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/awxkit/awxkit/api/pages/workflow_job_templates.py b/awxkit/awxkit/api/pages/workflow_job_templates.py index 4bbee6778a..52c89d8eea 100644 --- a/awxkit/awxkit/api/pages/workflow_job_templates.py +++ b/awxkit/awxkit/api/pages/workflow_job_templates.py @@ -34,7 +34,12 @@ class WorkflowJobTemplate(HasCopy, HasCreate, HasNotifications, HasSurvey, Unifi payload = PseudoNamespace(name=kwargs.get('name') or 'WorkflowJobTemplate - {}'.format(random_title()), description=kwargs.get('description') or random_title(10)) - optional_fields = ("allow_simultaneous", "ask_variables_on_launch", "survey_enabled") + optional_fields = ( + "allow_simultaneous", + "ask_variables_on_launch", "ask_inventory_on_launch", "ask_scm_branch_on_launch", "ask_limit_on_launch", + "limit", "scm_branch", + "survey_enabled" + ) update_payload(payload, optional_fields, kwargs) extra_vars = kwargs.get('extra_vars', not_provided) @@ -48,9 +53,6 @@ class WorkflowJobTemplate(HasCopy, HasCreate, HasNotifications, HasSurvey, Unifi if kwargs.get('inventory'): payload.inventory = kwargs.get('inventory').id - for field_name in ('ask_inventory_on_launch', 'limit', 'scm_branch', 'ask_scm_branch_on_launch'): - if field_name in kwargs: - setattr(payload, field_name, kwargs.get(field_name)) return payload From 8ac8fb9016a7e050855a83f20b14e212428ce4f8 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 10 Sep 2019 11:42:28 -0400 Subject: [PATCH 06/10] add more details to workflow limit help text --- awx/ui/client/src/templates/workflows.form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/templates/workflows.form.js b/awx/ui/client/src/templates/workflows.form.js index 839abfd265..9124233c71 100644 --- a/awx/ui/client/src/templates/workflows.form.js +++ b/awx/ui/client/src/templates/workflows.form.js @@ -93,7 +93,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) { label: i18n._('Limit'), type: 'text', column: 1, - awPopOver: "

" + i18n._("Select a limit for the workflow. This limit is applied to all job template nodes that prompt for a limit.") + "

", + awPopOver: "

" + i18n._("Provide a host pattern to further constrain the list of hosts that will be managed or affected by the workflow. This limit is applied to all job template nodes that prompt for a limit. Refer to Ansible documentation for more information and examples on patterns.") + "

", dataTitle: i18n._('Limit'), dataPlacement: 'right', dataContainer: "body", From 84a8559ea095e6468ae7c1454138f3e69ec4d5af Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 10 Sep 2019 11:46:45 -0400 Subject: [PATCH 07/10] psuedo -> pseudo --- awx/api/serializers.py | 4 ++-- awx/main/models/jobs.py | 4 ++-- awx/main/tasks.py | 4 ++-- awx/main/utils/common.py | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 3ebbe55ecb..c7a322489b 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -3357,7 +3357,7 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo setattr(mock_obj, field_name, attrs[field_name]) attrs.pop(field_name) - # Model `.save` needs the container dict, not the psuedo fields + # Model `.save` needs the container dict, not the pseudo fields if mock_obj.char_prompts: attrs['char_prompts'] = mock_obj.char_prompts @@ -3617,7 +3617,7 @@ class LaunchConfigurationBaseSerializer(BaseSerializer): if errors: raise serializers.ValidationError(errors) - # Model `.save` needs the container dict, not the psuedo fields + # Model `.save` needs the container dict, not the pseudo fields if mock_obj.char_prompts: attrs['char_prompts'] = mock_obj.char_prompts diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 0fc9a95d68..a6395c495b 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -39,7 +39,7 @@ from awx.main.models.notifications import ( NotificationTemplate, JobNotificationMixin, ) -from awx.main.utils import parse_yaml_or_json, getattr_dne, NullablePromptPsuedoField +from awx.main.utils import parse_yaml_or_json, getattr_dne, NullablePromptPseudoField from awx.main.fields import ImplicitRoleField, JSONField, AskForField from awx.main.models.mixins import ( ResourceMixin, @@ -903,7 +903,7 @@ for field_name in JobTemplate.get_ask_mapping().keys(): try: LaunchTimeConfigBase._meta.get_field(field_name) except FieldDoesNotExist: - setattr(LaunchTimeConfigBase, field_name, NullablePromptPsuedoField(field_name)) + setattr(LaunchTimeConfigBase, field_name, NullablePromptPseudoField(field_name)) class LaunchTimeConfig(LaunchTimeConfigBase): diff --git a/awx/main/tasks.py b/awx/main/tasks.py index cccf2b4fd7..a9308df432 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -2235,7 +2235,7 @@ class RunInventoryUpdate(BaseTask): getattr(settings, '%s_INSTANCE_ID_VAR' % src.upper()),]) # Add arguments for the source inventory script args.append('--source') - args.append(self.psuedo_build_inventory(inventory_update, private_data_dir)) + args.append(self.pseudo_build_inventory(inventory_update, private_data_dir)) if src == 'custom': args.append("--custom") args.append('-v%d' % inventory_update.verbosity) @@ -2246,7 +2246,7 @@ class RunInventoryUpdate(BaseTask): def build_inventory(self, inventory_update, private_data_dir): return None # what runner expects in order to not deal with inventory - def psuedo_build_inventory(self, inventory_update, private_data_dir): + def pseudo_build_inventory(self, inventory_update, private_data_dir): """Inventory imports are ran through a management command we pass the inventory in args to that command, so this is not considered to be "Ansible" inventory (by runner) even though it is diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index 6b76faa6f4..26f1afe800 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -47,7 +47,7 @@ __all__ = ['get_object_or_400', 'camelcase_to_underscore', 'underscore_to_camelc 'get_current_apps', 'set_current_apps', 'extract_ansible_vars', 'get_search_fields', 'get_system_task_capacity', 'get_cpu_capacity', 'get_mem_capacity', 'wrap_args_with_proot', 'build_proot_temp_dir', 'check_proot_installed', 'model_to_dict', - 'NullablePromptPsuedoField', 'model_instance_diff', 'parse_yaml_or_json', 'RequireDebugTrueOrTest', + 'NullablePromptPseudoField', 'model_instance_diff', 'parse_yaml_or_json', 'RequireDebugTrueOrTest', 'has_model_field_prefetched', 'set_environ', 'IllegalArgumentError', 'get_custom_venv_choices', 'get_external_account', 'task_manager_bulk_reschedule', 'schedule_task_manager', 'classproperty', 'create_temporary_fifo'] @@ -448,9 +448,9 @@ class CharPromptDescriptor: self.field = field -class NullablePromptPsuedoField: +class NullablePromptPseudoField: """ - Interface for psuedo-property stored in `char_prompts` dict + Interface for pseudo-property stored in `char_prompts` dict Used in LaunchTimeConfig and submodels, defined here to avoid circular imports """ def __init__(self, field_name): From 9697e1befba8c087b17fed30d7368ea7020139f3 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 10 Sep 2019 11:48:50 -0400 Subject: [PATCH 08/10] comment fixup --- awx/main/utils/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index 26f1afe800..d36dfa272b 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -508,7 +508,7 @@ def copy_model_by_class(obj1, Class2, fields, kwargs): create_kwargs.setdefault('char_prompts', {}) create_kwargs['char_prompts'][field_name] = new_val elif isinstance(descriptor, ManyToManyDescriptor): - continue # not coppied in this method + continue # not copied in this method elif field_name in kwargs: if field_name == 'extra_vars' and isinstance(kwargs[field_name], dict): create_kwargs[field_name] = json.dumps(kwargs['extra_vars']) From fdf9dd733bc12d0857d00f8af80d6728406eb1a8 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 12 Sep 2019 09:19:09 -0400 Subject: [PATCH 09/10] bump migration --- .../{0088_v360_WFJT_prompts.py => 0089_v360_WFJT_prompts.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename awx/main/migrations/{0088_v360_WFJT_prompts.py => 0089_v360_WFJT_prompts.py} (97%) diff --git a/awx/main/migrations/0088_v360_WFJT_prompts.py b/awx/main/migrations/0089_v360_WFJT_prompts.py similarity index 97% rename from awx/main/migrations/0088_v360_WFJT_prompts.py rename to awx/main/migrations/0089_v360_WFJT_prompts.py index 151f85129b..db080d71ef 100644 --- a/awx/main/migrations/0088_v360_WFJT_prompts.py +++ b/awx/main/migrations/0089_v360_WFJT_prompts.py @@ -7,7 +7,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('main', '0087_v360_update_credential_injector_help_text'), + ('main', '0088_v360_dashboard_optimizations'), ] operations = [ From e3c1189f567caef23c09a95883b64a1f0a9f1613 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 16 Sep 2019 10:02:36 -0400 Subject: [PATCH 10/10] bump migration --- .../{0089_v360_WFJT_prompts.py => 0090_v360_WFJT_prompts.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename awx/main/migrations/{0089_v360_WFJT_prompts.py => 0090_v360_WFJT_prompts.py} (98%) diff --git a/awx/main/migrations/0089_v360_WFJT_prompts.py b/awx/main/migrations/0090_v360_WFJT_prompts.py similarity index 98% rename from awx/main/migrations/0089_v360_WFJT_prompts.py rename to awx/main/migrations/0090_v360_WFJT_prompts.py index db080d71ef..1fa317e71b 100644 --- a/awx/main/migrations/0089_v360_WFJT_prompts.py +++ b/awx/main/migrations/0090_v360_WFJT_prompts.py @@ -7,7 +7,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('main', '0088_v360_dashboard_optimizations'), + ('main', '0089_v360_new_job_event_types'), ] operations = [