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 | 39 +++---- .../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, 389 insertions(+), 84 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 66c06c3ce6a2..3ebbe55ecb18 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 @@ def get_related(self, obj): 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 Meta: 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 @@ def validate(self, attrs): 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 9302249e670a..d77ec92b9174 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -3111,6 +3111,17 @@ def get(self, request, *args, **kwargs): 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 @@ def update_raw_data(self, data): 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 000000000000..7df324e9fa41 --- /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 8e5f1733ee68..7618b36eb3ef 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1501,7 +1501,7 @@ def _get_unified_job_class(cls): @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 4986d6f71784..058ef875153c 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -39,7 +39,7 @@ 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 @@ def _get_unified_job_class(cls): @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 @@ def finish_job_fact_cache(self, destination, modification_times): 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 Meta: 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 @@ def display_extra_vars(self): ''' 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 @@ def display_extra_data(self): 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 @@ def extra_vars(self, extra_vars): self.extra_data = extra_vars -for field_name in JobTemplate.get_ask_mapping().keys(): - if field_name == 'extra_vars': - continue - try: - LaunchTimeConfig._meta.get_field(field_name) - except FieldDoesNotExist: - 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 afd61e8faa10..b7b52dcf6bd8 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -329,7 +329,7 @@ def _get_unified_job_class(cls): @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 10df35e561f6..b2312ab63df7 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -19,7 +19,7 @@ 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 @@ def get_absolute_url(self, request=None): 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 @@ def get_job_kwargs(self): return data -class WorkflowJobOptions(BaseModel): +class WorkflowJobOptions(LaunchTimeConfigBase): class Meta: abstract = True @@ -318,10 +321,11 @@ def workflow_nodes(self): @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,16 +376,15 @@ class Meta: on_delete=models.SET_NULL, related_name='workflows', ) - inventory = models.ForeignKey( - 'Inventory', - related_name='%(class)ss', + ask_inventory_on_launch = AskForField( 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.'), + default=False, ) - ask_inventory_on_launch = AskForField( + ask_limit_on_launch = AskForField( + blank=True, + default=False, + ) + ask_scm_branch_on_launch = AskForField( blank=True, default=False, ) @@ -515,7 +518,7 @@ def _get_related_jobs(self): 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 cc4c42ecfe1e..f4daf7d57870 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 @@ def test_topology_validator(self, wfjt): 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 @@ def test_wfjt_unique_together_with_org(self, organization): 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 f904cf3b957b..f101168a8ba4 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 @@ 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 @@ def job_template_nodes(self, job_templates): 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 @@ def test_no_accepted_project_node_prompts(self, job_node_no_prompts, project_uni 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 cf3a511e28c6..6b76faa6f41d 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -19,9 +19,14 @@ 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 @@ '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 776e75da494c..839abfd265c6 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 53039f640287..5a2ef48adb42 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 13856a430ce8..871fa87a6632 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 61ca63764155..635b69c60f9c 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 543e2234673b..46cf24f73008 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 94928abe166a..4a36968f54cf 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 @@ def payload(self, workflow_job_template, unified_job_template, **kwargs): '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 4bfaba19a851..4bbee6778aa4 100644 --- a/awxkit/awxkit/api/pages/workflow_job_templates.py +++ b/awxkit/awxkit/api/pages/workflow_job_templates.py @@ -48,8 +48,9 @@ def payload(self, **kwargs): 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 ebb9c89d8f91..926788dcea1a 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 3ccfb7d36765..e01acf2e767d 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 f171098eb833..06d9e9deccdc 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 d46038d0f6d3..5dcb050dfbea 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 7df324e9fa41..151f85129b54 100644 --- a/awx/main/migrations/0085_v360_WFJT_prompts.py +++ b/awx/main/migrations/0088_v360_WFJT_prompts.py @@ -7,7 +7,7 @@ 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 46cf24f73008..792ae751386a 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 058ef875153c..0fc9a95d68ab 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -896,22 +896,6 @@ def prompts_dict(self, display=False): 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 @@ def extra_vars(self): 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 4bbee6778aa4..52c89d8eeaa5 100644 --- a/awxkit/awxkit/api/pages/workflow_job_templates.py +++ b/awxkit/awxkit/api/pages/workflow_job_templates.py @@ -34,7 +34,12 @@ def payload(self, **kwargs): 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 @@ def payload(self, **kwargs): 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 839abfd265c6..9124233c7102 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 3ebbe55ecb18..c7a322489bd1 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -3357,7 +3357,7 @@ def validate(self, attrs): 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 @@ def validate(self, attrs): 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 0fc9a95d68ab..a6395c495b12 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -39,7 +39,7 @@ 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 @@ def prompts_dict(self, display=False): 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 cccf2b4fd784..a9308df43268 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -2235,7 +2235,7 @@ def build_args(self, inventory_update, private_data_dir, passwords): 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 @@ def build_args(self, inventory_update, private_data_dir, passwords): 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 6b76faa6f41d..26f1afe8004e 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -47,7 +47,7 @@ '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 @@ def __init__(self, field): 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 26f1afe8004e..d36dfa272bec 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 151f85129b54..db080d71ef3a 100644 --- a/awx/main/migrations/0088_v360_WFJT_prompts.py +++ b/awx/main/migrations/0089_v360_WFJT_prompts.py @@ -7,7 +7,7 @@ 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 db080d71ef3a..1fa317e71b6e 100644 --- a/awx/main/migrations/0089_v360_WFJT_prompts.py +++ b/awx/main/migrations/0090_v360_WFJT_prompts.py @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ('main', '0088_v360_dashboard_optimizations'), + ('main', '0089_v360_new_job_event_types'), ] operations = [