mirror of
https://github.com/ansible/awx.git
synced 2026-02-26 23:46:05 -03:30
Adding unit/functional tests, fixing tests
Making common class for LabelList Fixing related field name Fixing get_effective_slice_ct to look for corerct field and also override _eager_field
This commit is contained in:
committed by
Alan Rominger
parent
42a7866da9
commit
4f5596eb0c
@@ -23,7 +23,7 @@ urls = [
|
|||||||
re_path(r'^(?P<pk>[0-9]+)/always_nodes/$', WorkflowJobNodeAlwaysNodesList.as_view(), name='workflow_job_node_always_nodes_list'),
|
re_path(r'^(?P<pk>[0-9]+)/always_nodes/$', WorkflowJobNodeAlwaysNodesList.as_view(), name='workflow_job_node_always_nodes_list'),
|
||||||
re_path(r'^(?P<pk>[0-9]+)/credentials/$', WorkflowJobNodeCredentialsList.as_view(), name='workflow_job_node_credentials_list'),
|
re_path(r'^(?P<pk>[0-9]+)/credentials/$', WorkflowJobNodeCredentialsList.as_view(), name='workflow_job_node_credentials_list'),
|
||||||
re_path(r'^(?P<pk>[0-9]+)/labels/$', WorkflowJobNodeLabelsList.as_view(), name='workflow_job_node_labels_list'),
|
re_path(r'^(?P<pk>[0-9]+)/labels/$', WorkflowJobNodeLabelsList.as_view(), name='workflow_job_node_labels_list'),
|
||||||
re_path(r'^(?P<pk>[0-9]+)/instance_groups/$', WorkflowJobNodeInstanceGroupsList.as_view(), name='workflow_job_node_instance_group_list'),
|
re_path(r'^(?P<pk>[0-9]+)/instance_groups/$', WorkflowJobNodeInstanceGroupsList.as_view(), name='workflow_job_node_instance_groups_list'),
|
||||||
]
|
]
|
||||||
|
|
||||||
__all__ = ['urls']
|
__all__ = ['urls']
|
||||||
|
|||||||
@@ -209,6 +209,26 @@ def api_exception_handler(exc, context):
|
|||||||
return exception_handler(exc, context)
|
return exception_handler(exc, context)
|
||||||
|
|
||||||
|
|
||||||
|
class LabelList(DeleteLastUnattachLabelMixin, SubListCreateAttachDetachAPIView):
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
# If a label already exists in the database, attach it instead of erroring out
|
||||||
|
# that it already exists
|
||||||
|
if not getattr(self, 'label_filter', None):
|
||||||
|
return Response(dict(msg=_('Class {} missing label filter.'.format(self.__class__.__name__))), status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
if 'id' not in request.data and 'name' in request.data and 'organization' in request.data:
|
||||||
|
existing = models.Label.objects.filter(name=request.data['name'], organization_id=request.data['organization'])
|
||||||
|
if existing.exists():
|
||||||
|
existing = existing[0]
|
||||||
|
request.data['id'] = existing.id
|
||||||
|
del request.data['name']
|
||||||
|
del request.data['organization']
|
||||||
|
if models.Label.objects.filter(**{self.label_filter: self.kwargs['pk']}).count() > 100:
|
||||||
|
return Response(
|
||||||
|
dict(msg=_('Maximum number of labels for {} reached.'.format(self.parent_model._meta.verbose_name_raw))), status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class DashboardView(APIView):
|
class DashboardView(APIView):
|
||||||
|
|
||||||
deprecated = True
|
deprecated = True
|
||||||
@@ -618,28 +638,13 @@ class ScheduleCredentialsList(LaunchConfigCredentialsBase):
|
|||||||
parent_model = models.Schedule
|
parent_model = models.Schedule
|
||||||
|
|
||||||
|
|
||||||
class ScheduleLabelsList(DeleteLastUnattachLabelMixin, SubListCreateAttachDetachAPIView):
|
class ScheduleLabelsList(LabelList):
|
||||||
|
|
||||||
model = models.Label
|
model = models.Label
|
||||||
serializer_class = serializers.LabelSerializer
|
serializer_class = serializers.LabelSerializer
|
||||||
parent_model = models.Schedule
|
parent_model = models.Schedule
|
||||||
relationship = 'labels'
|
relationship = 'labels'
|
||||||
|
label_filter = 'schedule_labels'
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
# If a label already exists in the database, attach it instead of erroring out
|
|
||||||
# that it already exists
|
|
||||||
if 'id' not in request.data and 'name' in request.data and 'organization' in request.data:
|
|
||||||
existing = models.Label.objects.filter(name=request.data['name'], organization_id=request.data['organization'])
|
|
||||||
if existing.exists():
|
|
||||||
existing = existing[0]
|
|
||||||
request.data['id'] = existing.id
|
|
||||||
del request.data['name']
|
|
||||||
del request.data['organization']
|
|
||||||
if models.Label.objects.filter(schedule_labels=self.kwargs['pk']).count() > 100:
|
|
||||||
return Response(
|
|
||||||
dict(msg=_('Maximum number of labels for {} reached.'.format(self.parent_model._meta.verbose_name_raw))), status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
return super(ScheduleLabelsList, self).post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class ScheduleInstanceGroupList(SubListAttachDetachAPIView):
|
class ScheduleInstanceGroupList(SubListAttachDetachAPIView):
|
||||||
@@ -2752,28 +2757,13 @@ class JobTemplateCredentialsList(SubListCreateAttachDetachAPIView):
|
|||||||
return super(JobTemplateCredentialsList, self).is_valid_relation(parent, sub, created)
|
return super(JobTemplateCredentialsList, self).is_valid_relation(parent, sub, created)
|
||||||
|
|
||||||
|
|
||||||
class JobTemplateLabelList(DeleteLastUnattachLabelMixin, SubListCreateAttachDetachAPIView):
|
class JobTemplateLabelList(LabelList):
|
||||||
|
|
||||||
model = models.Label
|
model = models.Label
|
||||||
serializer_class = serializers.LabelSerializer
|
serializer_class = serializers.LabelSerializer
|
||||||
parent_model = models.JobTemplate
|
parent_model = models.JobTemplate
|
||||||
relationship = 'labels'
|
relationship = 'labels'
|
||||||
|
label_filter = 'unifiedjobtemplate_labels'
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
# If a label already exists in the database, attach it instead of erroring out
|
|
||||||
# that it already exists
|
|
||||||
if 'id' not in request.data and 'name' in request.data and 'organization' in request.data:
|
|
||||||
existing = models.Label.objects.filter(name=request.data['name'], organization_id=request.data['organization'])
|
|
||||||
if existing.exists():
|
|
||||||
existing = existing[0]
|
|
||||||
request.data['id'] = existing.id
|
|
||||||
del request.data['name']
|
|
||||||
del request.data['organization']
|
|
||||||
if models.Label.objects.filter(unifiedjobtemplate_labels=self.kwargs['pk']).count() > 100:
|
|
||||||
return Response(
|
|
||||||
dict(msg=_('Maximum number of labels for {} reached.'.format(self.parent_model._meta.verbose_name_raw))), status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
return super(JobTemplateLabelList, self).post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class JobTemplateCallback(GenericAPIView):
|
class JobTemplateCallback(GenericAPIView):
|
||||||
@@ -2999,28 +2989,13 @@ class WorkflowJobNodeCredentialsList(SubListAPIView):
|
|||||||
relationship = 'credentials'
|
relationship = 'credentials'
|
||||||
|
|
||||||
|
|
||||||
class WorkflowJobNodeLabelsList(DeleteLastUnattachLabelMixin, SubListCreateAttachDetachAPIView):
|
class WorkflowJobNodeLabelsList(LabelList):
|
||||||
|
|
||||||
model = models.Label
|
model = models.Label
|
||||||
serializer_class = serializers.LabelSerializer
|
serializer_class = serializers.LabelSerializer
|
||||||
parent_model = models.WorkflowJobNode
|
parent_model = models.WorkflowJobNode
|
||||||
relationship = 'labels'
|
relationship = 'labels'
|
||||||
|
label_filter = 'workflowjobnode_labels'
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
# If a label already exists in the database, attach it instead of erroring out
|
|
||||||
# that it already exists
|
|
||||||
if 'id' not in request.data and 'name' in request.data and 'organization' in request.data:
|
|
||||||
existing = models.Label.objects.filter(name=request.data['name'], organization_id=request.data['organization'])
|
|
||||||
if existing.exists():
|
|
||||||
existing = existing[0]
|
|
||||||
request.data['id'] = existing.id
|
|
||||||
del request.data['name']
|
|
||||||
del request.data['organization']
|
|
||||||
if models.Label.objects.filter(workflowjobnode_labels=self.kwargs['pk']).count() > 100:
|
|
||||||
return Response(
|
|
||||||
dict(msg=_('Maximum number of labels for {} reached.'.format(self.parent_model._meta.verbose_name_raw))), status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
return super(WorkflowJobNodeLabelsList, self).post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowJobNodeInstanceGroupsList(SubListAttachDetachAPIView):
|
class WorkflowJobNodeInstanceGroupsList(SubListAttachDetachAPIView):
|
||||||
@@ -3049,28 +3024,13 @@ class WorkflowJobTemplateNodeCredentialsList(LaunchConfigCredentialsBase):
|
|||||||
parent_model = models.WorkflowJobTemplateNode
|
parent_model = models.WorkflowJobTemplateNode
|
||||||
|
|
||||||
|
|
||||||
class WorkflowJobTemplateNodeLabelsList(DeleteLastUnattachLabelMixin, SubListCreateAttachDetachAPIView):
|
class WorkflowJobTemplateNodeLabelsList(LabelList):
|
||||||
|
|
||||||
model = models.Label
|
model = models.Label
|
||||||
serializer_class = serializers.LabelSerializer
|
serializer_class = serializers.LabelSerializer
|
||||||
parent_model = models.WorkflowJobTemplateNode
|
parent_model = models.WorkflowJobTemplateNode
|
||||||
relationship = 'labels'
|
relationship = 'labels'
|
||||||
|
label_filter = 'workflowjobtemplatenode_labels'
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
# If a label already exists in the database, attach it instead of erroring out
|
|
||||||
# that it already exists
|
|
||||||
if 'id' not in request.data and 'name' in request.data and 'organization' in request.data:
|
|
||||||
existing = models.Label.objects.filter(name=request.data['name'], organization_id=request.data['organization'])
|
|
||||||
if existing.exists():
|
|
||||||
existing = existing[0]
|
|
||||||
request.data['id'] = existing.id
|
|
||||||
del request.data['name']
|
|
||||||
del request.data['organization']
|
|
||||||
if models.Label.objects.filter(workflowjobtemplatenode_labels=self.kwargs['pk']).count() > 100:
|
|
||||||
return Response(
|
|
||||||
dict(msg=_('Maximum number of labels for {} reached.'.format(self.parent_model._meta.verbose_name_raw))), status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
return super(WorkflowJobTemplateNodeLabelsList, self).post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowJobTemplateNodeInstanceGroupsList(SubListAttachDetachAPIView):
|
class WorkflowJobTemplateNodeInstanceGroupsList(SubListAttachDetachAPIView):
|
||||||
|
|||||||
@@ -1923,24 +1923,29 @@ class JobLaunchConfigAccess(BaseAccess):
|
|||||||
return self.check_related('inventory', Inventory, data, obj=obj, role_field='use_role')
|
return self.check_related('inventory', Inventory, data, obj=obj, role_field='use_role')
|
||||||
|
|
||||||
def can_attach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False):
|
def can_attach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False):
|
||||||
|
try:
|
||||||
|
obj_name = obj.name
|
||||||
|
except AttributeError:
|
||||||
|
obj_name = obj.identifier
|
||||||
|
|
||||||
if isinstance(sub_obj, Credential) and relationship == 'credentials':
|
if isinstance(sub_obj, Credential) and relationship == 'credentials':
|
||||||
if not self.user in sub_obj.use_role:
|
if not self.user in sub_obj.use_role:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"User {} not allowed access to credential {} for {} {} ({})".format(self.user.username, sub_obj.name, obj.__class__, obj.name, obj.id)
|
"User {} not allowed access to credential {} for {} {} ({})".format(self.user.username, sub_obj.name, obj.__class__, obj_name, obj.id)
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if isinstance(sub_obj, Label) and relationship == 'labels':
|
if isinstance(sub_obj, Label) and relationship == 'labels':
|
||||||
if not self.user.can_access(Label, 'read', sub_obj):
|
if not self.user.can_access(Label, 'read', sub_obj):
|
||||||
logger.debug("User {} not allowed access to label {} for {} {} ({})".format(self.user.username, sub_obj.name, obj.__class__, obj.name, obj.id))
|
logger.debug("User {} not allowed access to label {} for {} {} ({})".format(self.user.username, sub_obj.name, obj.__class__, obj_name, obj.id))
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if isinstance(sub_obj, InstanceGroup) and relationship == 'instance_groups':
|
if isinstance(sub_obj, InstanceGroup) and relationship == 'instance_groups':
|
||||||
if not sub_obj in self.user.get_queryset(InstanceGroup):
|
if not sub_obj in self.user.get_queryset(InstanceGroup):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"User {} not allowed access to instance_group {} for {} {} ({})".format(self.user.username, sub_obj.name, obj.__class__, obj.name, obj.id)
|
"User {} not allowed access to instance_group {} for {} {} ({})".format(self.user.username, sub_obj.name, obj.__class__, obj_name, obj.id)
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@@ -1948,18 +1953,23 @@ class JobLaunchConfigAccess(BaseAccess):
|
|||||||
raise NotImplementedError('Only credentials, labels and instance groups can be attached to launch configurations.')
|
raise NotImplementedError('Only credentials, labels and instance groups can be attached to launch configurations.')
|
||||||
|
|
||||||
def can_unattach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False):
|
def can_unattach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False):
|
||||||
|
try:
|
||||||
|
obj_name = obj.name
|
||||||
|
except AttributeError:
|
||||||
|
obj_name = obj.identifier
|
||||||
|
|
||||||
if isinstance(sub_obj, Credential) and relationship == 'credentials':
|
if isinstance(sub_obj, Credential) and relationship == 'credentials':
|
||||||
if not skip_sub_obj_read_check:
|
if not skip_sub_obj_read_check:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Skipping check if user {} can access credential {} ({}) for removal from {} {} ({})".format(
|
"Skipping check if user {} can access credential {} ({}) for removal from {} {} ({})".format(
|
||||||
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj.name, obj.id
|
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj_name, obj.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
if not self.user in sub_obj.read_role:
|
if not self.user in sub_obj.read_role:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"User {} can not read credential {} ({}) for removal from {} {} ({})".format(
|
"User {} can not read credential {} ({}) for removal from {} {} ({})".format(
|
||||||
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj.name, obj.id
|
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj_name, obj.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
@@ -1968,7 +1978,7 @@ class JobLaunchConfigAccess(BaseAccess):
|
|||||||
if skip_sub_obj_read_check:
|
if skip_sub_obj_read_check:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Skipping check if user {} can access label {} ({}) for removal from {} {} ({})".format(
|
"Skipping check if user {} can access label {} ({}) for removal from {} {} ({})".format(
|
||||||
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj.name, obj.id
|
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj_name, obj.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
@@ -1976,7 +1986,7 @@ class JobLaunchConfigAccess(BaseAccess):
|
|||||||
return True
|
return True
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"User {} can not read label {} ({}) for removal from {} {} ({})".format(
|
"User {} can not read label {} ({}) for removal from {} {} ({})".format(
|
||||||
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj.name, obj.id
|
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj_name, obj.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
@@ -1984,7 +1994,7 @@ class JobLaunchConfigAccess(BaseAccess):
|
|||||||
if skip_sub_obj_read_check:
|
if skip_sub_obj_read_check:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Skipping check if user {} can access instance_group {} ({}) for removal from {} {} ({})".format(
|
"Skipping check if user {} can access instance_group {} ({}) for removal from {} {} ({})".format(
|
||||||
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj.name, obj.id
|
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj_name, obj.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
@@ -1992,7 +2002,7 @@ class JobLaunchConfigAccess(BaseAccess):
|
|||||||
return True
|
return True
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"User {} can not read instance_group {} ({}) for removal from {} {} ({})".format(
|
"User {} can not read instance_group {} ({}) for removal from {} {} ({})".format(
|
||||||
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj.name, obj.id
|
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj_name, obj.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
@@ -2069,61 +2079,25 @@ class WorkflowJobTemplateNodeAccess(BaseAccess):
|
|||||||
def can_attach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False):
|
def can_attach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False):
|
||||||
if not self.wfjt_admin(obj):
|
if not self.wfjt_admin(obj):
|
||||||
return False
|
return False
|
||||||
if relationship == 'credentials':
|
if relationship in ['credentials', 'labels', 'instance_groups']:
|
||||||
# Need permission to related template to attach a credential
|
# Need permission to related template to attach a credential
|
||||||
if not self.ujt_execute(obj):
|
if not self.ujt_execute(obj):
|
||||||
return False
|
return False
|
||||||
return JobLaunchConfigAccess(self.user).can_attach(obj, sub_obj, relationship, data, skip_sub_obj_read_check=skip_sub_obj_read_check)
|
return JobLaunchConfigAccess(self.user).can_attach(obj, sub_obj, relationship, data, skip_sub_obj_read_check=skip_sub_obj_read_check)
|
||||||
elif relationship in ('success_nodes', 'failure_nodes', 'always_nodes'):
|
elif relationship in ('success_nodes', 'failure_nodes', 'always_nodes'):
|
||||||
return self.check_same_WFJT(obj, sub_obj)
|
return self.check_same_WFJT(obj, sub_obj)
|
||||||
elif relationship == 'labels':
|
|
||||||
if self.user.can_access(Label, 'read', sub_obj):
|
|
||||||
return True
|
|
||||||
logger.debug(
|
|
||||||
"User {} can not read label {} ({}) for removal from {} {} ({})".format(
|
|
||||||
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj.name, obj.id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
elif relationship == 'instance_groups':
|
|
||||||
if sub_obj in self.user.get_queryset(InstanceGroup):
|
|
||||||
return True
|
|
||||||
logger.debug(
|
|
||||||
"User {} can not read instance_group {} ({}) for removal from {} {} ({})".format(
|
|
||||||
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj.name, obj.id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError('Relationship {} not understood for WFJT nodes.'.format(relationship))
|
raise NotImplementedError('Relationship {} not understood for WFJT nodes.'.format(relationship))
|
||||||
|
|
||||||
def can_unattach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False):
|
def can_unattach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False):
|
||||||
if not self.wfjt_admin(obj):
|
if not self.wfjt_admin(obj):
|
||||||
return False
|
return False
|
||||||
if relationship == 'credentials':
|
if relationship in ['credentials', 'labels', 'instance_groups']:
|
||||||
if not self.ujt_execute(obj):
|
if not self.ujt_execute(obj):
|
||||||
return False
|
return False
|
||||||
return JobLaunchConfigAccess(self.user).can_unattach(obj, sub_obj, relationship, data, skip_sub_obj_read_check=skip_sub_obj_read_check)
|
return JobLaunchConfigAccess(self.user).can_unattach(obj, sub_obj, relationship, data, skip_sub_obj_read_check=skip_sub_obj_read_check)
|
||||||
elif relationship in ('success_nodes', 'failure_nodes', 'always_nodes'):
|
elif relationship in ('success_nodes', 'failure_nodes', 'always_nodes'):
|
||||||
return self.check_same_WFJT(obj, sub_obj)
|
return self.check_same_WFJT(obj, sub_obj)
|
||||||
elif relationship == 'labels':
|
|
||||||
if self.user.can_access(Label, 'read', sub_obj):
|
|
||||||
return True
|
|
||||||
logger.debug(
|
|
||||||
"User {} can not read label {} ({}) for removal from {} {} ({})".format(
|
|
||||||
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj.name, obj.id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
elif relationship == 'instance_groups':
|
|
||||||
if sub_obj in self.user.get_queryset(InstanceGroup):
|
|
||||||
return True
|
|
||||||
logger.debug(
|
|
||||||
"User {} can not read instance_group {} ({}) for removal from {} {} ({})".format(
|
|
||||||
self.user.username, sub_obj.name, sub_obj.id, obj.__class__, obj.name, obj.id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError('Relationship {} not understood for WFJT nodes.'.format(relationship))
|
raise NotImplementedError('Relationship {} not understood for WFJT nodes.'.format(relationship))
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 3.2.13 on 2022-08-31 19:15
|
# Generated by Django 3.2.13 on 2022-09-06 19:50
|
||||||
|
|
||||||
import awx.main.fields
|
import awx.main.fields
|
||||||
import awx.main.utils.polymorphic
|
import awx.main.utils.polymorphic
|
||||||
@@ -94,6 +94,21 @@ class Migration(migrations.Migration):
|
|||||||
name='labels',
|
name='labels',
|
||||||
field=models.ManyToManyField(related_name='workflowjobnode_labels', to='main.Label'),
|
field=models.ManyToManyField(related_name='workflowjobnode_labels', to='main.Label'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workflowjobtemplate',
|
||||||
|
name='ask_labels_on_launch',
|
||||||
|
field=awx.main.fields.AskForField(blank=True, default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workflowjobtemplate',
|
||||||
|
name='ask_skip_tags_on_launch',
|
||||||
|
field=awx.main.fields.AskForField(blank=True, default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workflowjobtemplate',
|
||||||
|
name='ask_tags_on_launch',
|
||||||
|
field=awx.main.fields.AskForField(blank=True, default=False),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='workflowjobtemplatenode',
|
model_name='workflowjobtemplatenode',
|
||||||
name='execution_environment',
|
name='execution_environment',
|
||||||
@@ -117,7 +132,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('position', models.PositiveIntegerField(db_index=True, default=None, null=True)),
|
('position', models.PositiveIntegerField(db_index=True, default=None, null=True)),
|
||||||
('instancegroup', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.instancegroup')),
|
('instancegroup', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.instancegroup')),
|
||||||
('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.workflowjobtemplatenode')),
|
('workflowjobtemplatenode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.workflowjobtemplatenode')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
@@ -126,7 +141,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('position', models.PositiveIntegerField(db_index=True, default=None, null=True)),
|
('position', models.PositiveIntegerField(db_index=True, default=None, null=True)),
|
||||||
('instancegroup', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.instancegroup')),
|
('instancegroup', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.instancegroup')),
|
||||||
('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.workflowjobnode')),
|
('workflowjobnode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.workflowjobnode')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
|||||||
@@ -471,7 +471,7 @@ class ScheduleInstanceGroupMembership(models.Model):
|
|||||||
|
|
||||||
class WorkflowJobTemplateNodeBaseInstanceGroupMembership(models.Model):
|
class WorkflowJobTemplateNodeBaseInstanceGroupMembership(models.Model):
|
||||||
|
|
||||||
schedule = models.ForeignKey('WorkflowJobTemplateNode', on_delete=models.CASCADE)
|
workflowjobtemplatenode = models.ForeignKey('WorkflowJobTemplateNode', on_delete=models.CASCADE)
|
||||||
instancegroup = models.ForeignKey('InstanceGroup', on_delete=models.CASCADE)
|
instancegroup = models.ForeignKey('InstanceGroup', on_delete=models.CASCADE)
|
||||||
position = models.PositiveIntegerField(
|
position = models.PositiveIntegerField(
|
||||||
null=True,
|
null=True,
|
||||||
@@ -482,7 +482,7 @@ class WorkflowJobTemplateNodeBaseInstanceGroupMembership(models.Model):
|
|||||||
|
|
||||||
class WorkflowJobNodeBaseInstanceGroupMembership(models.Model):
|
class WorkflowJobNodeBaseInstanceGroupMembership(models.Model):
|
||||||
|
|
||||||
schedule = models.ForeignKey('WorkflowJobNode', on_delete=models.CASCADE)
|
workflowjobnode = models.ForeignKey('WorkflowJobNode', on_delete=models.CASCADE)
|
||||||
instancegroup = models.ForeignKey('InstanceGroup', on_delete=models.CASCADE)
|
instancegroup = models.ForeignKey('InstanceGroup', on_delete=models.CASCADE)
|
||||||
position = models.PositiveIntegerField(
|
position = models.PositiveIntegerField(
|
||||||
null=True,
|
null=True,
|
||||||
|
|||||||
@@ -332,8 +332,11 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
|||||||
if self.ask_inventory_on_launch and 'inventory' in kwargs:
|
if self.ask_inventory_on_launch and 'inventory' in kwargs:
|
||||||
actual_inventory = kwargs['inventory']
|
actual_inventory = kwargs['inventory']
|
||||||
actual_slice_count = self.job_slice_count
|
actual_slice_count = self.job_slice_count
|
||||||
if self.ask_job_slice_count_on_launch and 'slice_count' in kwargs:
|
if self.ask_job_slice_count_on_launch and 'job_slice_count' in kwargs:
|
||||||
actual_slice_count = kwargs['slice_count']
|
actual_slice_count = kwargs['job_slice_count']
|
||||||
|
# Set the eager fields if its there as well
|
||||||
|
if '_eager_fields' in kwargs and 'job_slice_count' in kwargs['_eager_fields']:
|
||||||
|
kwargs['_eager_fields']['job_slice_count'] = actual_slice_count
|
||||||
if actual_inventory:
|
if actual_inventory:
|
||||||
return min(actual_slice_count, actual_inventory.hosts.count())
|
return min(actual_slice_count, actual_inventory.hosts.count())
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from awx.main.models import prevent_search, accepts_json, UnifiedJobTemplate, Un
|
|||||||
from awx.main.models.notifications import NotificationTemplate, JobNotificationMixin
|
from awx.main.models.notifications import NotificationTemplate, JobNotificationMixin
|
||||||
from awx.main.models.base import CreatedModifiedModel, VarsDictProperty
|
from awx.main.models.base import CreatedModifiedModel, VarsDictProperty
|
||||||
from awx.main.models.rbac import ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR
|
from awx.main.models.rbac import ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR
|
||||||
from awx.main.fields import ImplicitRoleField, AskForField, JSONBlob, OrderedManyToManyField
|
from awx.main.fields import ImplicitRoleField, JSONBlob, OrderedManyToManyField
|
||||||
from awx.main.models.mixins import (
|
from awx.main.models.mixins import (
|
||||||
ResourceMixin,
|
ResourceMixin,
|
||||||
SurveyJobTemplateMixin,
|
SurveyJobTemplateMixin,
|
||||||
|
|||||||
@@ -78,4 +78,4 @@ class TestSlicingModels:
|
|||||||
# The inventory slice count will be the min of the number of nodes (4) or the job slice (2)
|
# The inventory slice count will be the min of the number of nodes (4) or the job slice (2)
|
||||||
assert job_template.get_effective_slice_ct({'inventory': inventory2}) == 2
|
assert job_template.get_effective_slice_ct({'inventory': inventory2}) == 2
|
||||||
# Now we are going to pass in an override (like the prompt would) and as long as that is < host count we expect that back
|
# Now we are going to pass in an override (like the prompt would) and as long as that is < host count we expect that back
|
||||||
assert job_template.get_effective_slice_ct({'inventory': inventory2, 'slice_count': 3}) == 3
|
assert job_template.get_effective_slice_ct({'inventory': inventory2, 'job_slice_count': 3}) == 3
|
||||||
|
|||||||
@@ -101,6 +101,80 @@ class TestWorkflowJobTemplateNodeAccess:
|
|||||||
access = WorkflowJobTemplateNodeAccess(rando)
|
access = WorkflowJobTemplateNodeAccess(rando)
|
||||||
assert access.can_delete(wfjt_node)
|
assert access.can_delete(wfjt_node)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"add_wfjt_admin, add_jt_admin, permission_type, expected_result, method_type",
|
||||||
|
[
|
||||||
|
(False, False, None, False, 'can_attach'),
|
||||||
|
(True, False, 'credentials', False, 'can_attach'),
|
||||||
|
(True, True, 'credentials', True, 'can_attach'),
|
||||||
|
(True, False, 'labels', False, 'can_attach'),
|
||||||
|
(True, True, 'labels', True, 'can_attach'),
|
||||||
|
(True, False, 'instance_groups', False, 'can_attach'),
|
||||||
|
(True, True, 'instance_groups', True, 'can_attach'),
|
||||||
|
(False, False, None, False, 'can_unattach'),
|
||||||
|
(True, False, 'credentials', False, 'can_unattach'),
|
||||||
|
(True, True, 'credentials', True, 'can_unattach'),
|
||||||
|
(True, False, 'labels', False, 'can_unattach'),
|
||||||
|
(True, True, 'labels', True, 'can_unattach'),
|
||||||
|
(True, False, 'instance_groups', False, 'can_unattach'),
|
||||||
|
(True, True, 'instance_groups', True, 'can_unattach'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_attacher_permissions(self, wfjt_node, job_template, rando, add_wfjt_admin, permission_type, add_jt_admin, expected_result, mocker, method_type):
|
||||||
|
wfjt = wfjt_node.workflow_job_template
|
||||||
|
if add_wfjt_admin:
|
||||||
|
wfjt.admin_role.members.add(rando)
|
||||||
|
wfjt.unified_job_template = job_template
|
||||||
|
if add_jt_admin:
|
||||||
|
job_template.execute_role.members.add(rando)
|
||||||
|
|
||||||
|
# We have to mock the JobLaunchConfigAccess because the attachment methods will look at the object type and the relation
|
||||||
|
# Since we pass None as the second param this will trigger an NotImplementedError from that object
|
||||||
|
with mocker.patch('awx.main.access.JobLaunchConfigAccess.{}'.format(method_type), return_value=True):
|
||||||
|
access = WorkflowJobTemplateNodeAccess(rando)
|
||||||
|
assert getattr(access, method_type)(wfjt_node, None, permission_type, None) == expected_result
|
||||||
|
|
||||||
|
# The actual attachment of labels, credentials and instance groups are tested from JobLaunchConfigAccess
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"attachment_type, expect_exception, method_type",
|
||||||
|
[
|
||||||
|
("credentials", False, 'can_attach'),
|
||||||
|
("labels", False, 'can_attach'),
|
||||||
|
("instance_groups", False, 'can_attach'),
|
||||||
|
("success_nodes", False, 'can_attach'),
|
||||||
|
("failure_nodes", False, 'can_attach'),
|
||||||
|
("always_nodes", False, 'can_attach'),
|
||||||
|
("junk", True, 'can_attach'),
|
||||||
|
("credentials", False, 'can_unattach'),
|
||||||
|
("labels", False, 'can_unattach'),
|
||||||
|
("instance_groups", False, 'can_unattach'),
|
||||||
|
("success_nodes", False, 'can_unattach'),
|
||||||
|
("failure_nodes", False, 'can_unattach'),
|
||||||
|
("always_nodes", False, 'can_unattach'),
|
||||||
|
("junk", True, 'can_unattach'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_attacher_raise_not_implemented(self, wfjt_node, rando, attachment_type, expect_exception, method_type):
|
||||||
|
wfjt = wfjt_node.workflow_job_template
|
||||||
|
wfjt.admin_role.members.add(rando)
|
||||||
|
access = WorkflowJobTemplateNodeAccess(rando)
|
||||||
|
if expect_exception:
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
access.can_attach(wfjt_node, None, attachment_type, None)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
getattr(access, method_type)(wfjt_node, None, attachment_type, None)
|
||||||
|
except NotImplementedError:
|
||||||
|
# We explicitly catch NotImplemented because the _nodes type will raise a different exception
|
||||||
|
assert False, "Exception was raised when it should not have been"
|
||||||
|
except Exception:
|
||||||
|
# File "/awx_devel/awx/main/access.py", line 2074, in check_same_WFJT
|
||||||
|
# raise Exception('Attaching workflow nodes only allowed for other nodes')
|
||||||
|
pass
|
||||||
|
|
||||||
|
# TODO: Implement additional tests for _nodes attachments here
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestWorkflowJobAccess:
|
class TestWorkflowJobAccess:
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class TestWorkflowJobTemplateSerializerGetRelated:
|
|||||||
'launch',
|
'launch',
|
||||||
'workflow_nodes',
|
'workflow_nodes',
|
||||||
'webhook_key',
|
'webhook_key',
|
||||||
|
'labels',
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_get_related(self, mocker, test_get_related, workflow_job_template, related_resource_name):
|
def test_get_related(self, mocker, test_get_related, workflow_job_template, related_resource_name):
|
||||||
@@ -89,6 +90,8 @@ class TestWorkflowJobTemplateNodeSerializerGetRelated:
|
|||||||
'success_nodes',
|
'success_nodes',
|
||||||
'failure_nodes',
|
'failure_nodes',
|
||||||
'always_nodes',
|
'always_nodes',
|
||||||
|
'labels',
|
||||||
|
'instance_groups',
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_get_related(self, test_get_related, workflow_job_template_node, related_resource_name):
|
def test_get_related(self, test_get_related, workflow_job_template_node, related_resource_name):
|
||||||
@@ -233,6 +236,8 @@ class TestWorkflowJobNodeSerializerGetRelated:
|
|||||||
'success_nodes',
|
'success_nodes',
|
||||||
'failure_nodes',
|
'failure_nodes',
|
||||||
'always_nodes',
|
'always_nodes',
|
||||||
|
'labels',
|
||||||
|
'instance_groups',
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_get_related(self, test_get_related, workflow_job_node, related_resource_name):
|
def test_get_related(self, test_get_related, workflow_job_node, related_resource_name):
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ options:
|
|||||||
labels:
|
labels:
|
||||||
description:
|
description:
|
||||||
- List of labels applied as a prompt, assuming job template prompts for labels
|
- List of labels applied as a prompt, assuming job template prompts for labels
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
credentials:
|
credentials:
|
||||||
description:
|
description:
|
||||||
- List of credentials applied as a prompt, assuming job template prompts for credentials
|
- List of credentials applied as a prompt, assuming job template prompts for credentials
|
||||||
|
|||||||
@@ -172,6 +172,8 @@ options:
|
|||||||
labels:
|
labels:
|
||||||
description:
|
description:
|
||||||
- List of labels applied as a prompt, assuming job template prompts for labels
|
- List of labels applied as a prompt, assuming job template prompts for labels
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
timeout:
|
timeout:
|
||||||
description:
|
description:
|
||||||
- Timeout applied as a prompt, assuming job template prompts for timeout
|
- Timeout applied as a prompt, assuming job template prompts for timeout
|
||||||
|
|||||||
Reference in New Issue
Block a user