Merge pull request #4369 from AlanCoding/workflow_limit

Implementation of WFJT limit & SCM_branch prompting

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2019-09-16 20:17:59 +00:00
committed by GitHub
22 changed files with 414 additions and 102 deletions

View File

@@ -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 pseudo 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)
@@ -3596,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
@@ -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

View File

@@ -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):

View File

@@ -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', '0089_v360_new_job_event_types'),
]
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'),
),
]

View File

@@ -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):

View File

@@ -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, NullablePromptPseudoField
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
@@ -914,21 +896,14 @@ class LaunchTimeConfigBase(BaseModel):
data[prompt_name] = prompt_val
return data
def display_extra_vars(self):
'''
Hides fields marked as passwords in survey.
'''
if 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':
continue
try:
LaunchTimeConfigBase._meta.get_field(field_name)
except FieldDoesNotExist:
setattr(LaunchTimeConfigBase, field_name, NullablePromptPseudoField(field_name))
class LaunchTimeConfig(LaunchTimeConfigBase):
@@ -963,14 +938,21 @@ 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
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))
def display_extra_data(self):
return self.display_extra_vars()
class JobLaunchConfig(LaunchTimeConfig):

View File

@@ -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):

View File

@@ -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',)

View File

@@ -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

View File

@@ -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

View File

@@ -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']

View File

@@ -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',
'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']
@@ -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 NullablePromptPseudoField:
"""
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):
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 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'])
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)

View File

@@ -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
);
}

View File

@@ -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: "<p>" + 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.") + "</p>",
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: "<p>" + i18n._("Select a branch for the workflow. This branch is applied to all job template nodes that prompt for a branch.") + "</p>",
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',

View File

@@ -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);

View File

@@ -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,

View File

@@ -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'),

View File

@@ -140,6 +140,26 @@
</div>
</div>
<!-- LIMIT -->
<div class="WorkflowResults-resultRow" ng-show="workflow.limit">
<label class="WorkflowResults-resultRowLabel">
{{ strings.labels.LIMIT }}
</label>
<div class="WorkflowResults-resultRowText">
{{ workflow.limit }}
</div>
</div>
<!-- BRANCH -->
<div class="WorkflowResults-resultRow" ng-show="workflow.scm_branch">
<label class="WorkflowResults-resultRowLabel">
{{ strings.labels.SCM_BRANCH }}
</label>
<div class="WorkflowResults-resultRowText">
{{ workflow.scm_branch }}
</div>
</div>
<!-- TEMPLATE DETAIL -->
<div class="WorkflowResults-resultRow"
ng-show="workflow.summary_fields.workflow_job_template.name">

View File

@@ -144,11 +144,15 @@ 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,
ask_inventory_on_launch: false,
ask_variables_on_launch: false,
ask_limit_on_launch: false,
ask_scm_branch_on_launch: false,
extra_vars: undefined
});
});