diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index e81b622140..8274daedc3 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -35,7 +35,7 @@ from django.utils.translation import ugettext_lazy as _ # Django REST Framework from rest_framework.exceptions import PermissionDenied, ParseError from rest_framework.parsers import FormParser -from rest_framework.permissions import AllowAny, IsAuthenticated, SAFE_METHODS +from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.views import exception_handler @@ -100,6 +100,7 @@ from awx.api.views.mixin import ( InstanceGroupMembershipMixin, RelatedJobsPreventDeleteMixin, OrganizationCountsMixin, + ControlledByScmMixin, ) from awx.api.views.organization import ( # noqa OrganizationList, @@ -119,6 +120,23 @@ from awx.api.views.organization import ( # noqa OrganizationAccessList, OrganizationObjectRolesList, ) +from awx.api.views.inventory import ( # noqa + InventoryList, + InventoryDetail, + InventoryUpdateEventsList, + InventoryScriptList, + InventoryScriptDetail, + InventoryScriptObjectRolesList, + InventoryScriptCopy, + InventoryList, + InventoryDetail, + InventoryActivityStreamList, + InventoryInstanceGroupsList, + InventoryAccessList, + InventoryObjectRolesList, + InventoryJobTemplateList, + InventoryCopy, +) logger = logging.getLogger('awx.api.views') @@ -1064,18 +1082,6 @@ class SystemJobEventsList(SubListAPIView): return super(SystemJobEventsList, self).finalize_response(request, response, *args, **kwargs) -class InventoryUpdateEventsList(SubListAPIView): - - model = InventoryUpdateEvent - serializer_class = InventoryUpdateEventSerializer - parent_model = InventoryUpdate - relationship = 'inventory_update_events' - view_name = _('Inventory Update Events List') - search_fields = ('stdout',) - - def finalize_response(self, request, response, *args, **kwargs): - response['X-UI-Max-Events'] = settings.MAX_UI_JOB_EVENTS - return super(InventoryUpdateEventsList, self).finalize_response(request, response, *args, **kwargs) class ProjectUpdateCancel(RetrieveAPIView): @@ -1633,177 +1639,6 @@ class CredentialCopy(CopyAPIView): copy_return_serializer_class = CredentialSerializer -class InventoryScriptList(ListCreateAPIView): - - model = CustomInventoryScript - serializer_class = CustomInventoryScriptSerializer - - -class InventoryScriptDetail(RetrieveUpdateDestroyAPIView): - - model = CustomInventoryScript - serializer_class = CustomInventoryScriptSerializer - - def destroy(self, request, *args, **kwargs): - instance = self.get_object() - can_delete = request.user.can_access(self.model, 'delete', instance) - if not can_delete: - raise PermissionDenied(_("Cannot delete inventory script.")) - for inv_src in InventorySource.objects.filter(source_script=instance): - inv_src.source_script = None - inv_src.save() - return super(InventoryScriptDetail, self).destroy(request, *args, **kwargs) - - -class InventoryScriptObjectRolesList(SubListAPIView): - - model = Role - serializer_class = RoleSerializer - parent_model = CustomInventoryScript - search_fields = ('role_field', 'content_type__model',) - - def get_queryset(self): - po = self.get_parent_object() - content_type = ContentType.objects.get_for_model(self.parent_model) - return Role.objects.filter(content_type=content_type, object_id=po.pk) - - -class InventoryScriptCopy(CopyAPIView): - - model = CustomInventoryScript - copy_return_serializer_class = CustomInventoryScriptSerializer - - -class InventoryList(ListCreateAPIView): - - model = Inventory - serializer_class = InventorySerializer - - def get_queryset(self): - qs = Inventory.accessible_objects(self.request.user, 'read_role') - qs = qs.select_related('admin_role', 'read_role', 'update_role', 'use_role', 'adhoc_role') - qs = qs.prefetch_related('created_by', 'modified_by', 'organization') - return qs - - -class ControlledByScmMixin(object): - ''' - Special method to reset SCM inventory commit hash - if anything that it manages changes. - ''' - - def _reset_inv_src_rev(self, obj): - if self.request.method in SAFE_METHODS or not obj: - return - project_following_sources = obj.inventory_sources.filter( - update_on_project_update=True, source='scm') - if project_following_sources: - # Allow inventory changes unrelated to variables - if self.model == Inventory and ( - not self.request or not self.request.data or - parse_yaml_or_json(self.request.data.get('variables', '')) == parse_yaml_or_json(obj.variables)): - return - project_following_sources.update(scm_last_revision='') - - def get_object(self): - obj = super(ControlledByScmMixin, self).get_object() - self._reset_inv_src_rev(obj) - return obj - - def get_parent_object(self): - obj = super(ControlledByScmMixin, self).get_parent_object() - self._reset_inv_src_rev(obj) - return obj - - -class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView): - - model = Inventory - serializer_class = InventoryDetailSerializer - - def update(self, request, *args, **kwargs): - obj = self.get_object() - kind = self.request.data.get('kind') or kwargs.get('kind') - - # Do not allow changes to an Inventory kind. - if kind is not None and obj.kind != kind: - return self.http_method_not_allowed(request, *args, **kwargs) - return super(InventoryDetail, self).update(request, *args, **kwargs) - - def destroy(self, request, *args, **kwargs): - obj = self.get_object() - if not request.user.can_access(self.model, 'delete', obj): - raise PermissionDenied() - self.check_related_active_jobs(obj) # related jobs mixin - try: - obj.schedule_deletion(getattr(request.user, 'id', None)) - return Response(status=status.HTTP_202_ACCEPTED) - except RuntimeError as e: - return Response(dict(error=_("{0}".format(e))), status=status.HTTP_400_BAD_REQUEST) - - -class InventoryActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): - - model = ActivityStream - serializer_class = ActivityStreamSerializer - parent_model = Inventory - relationship = 'activitystream_set' - search_fields = ('changes',) - - def get_queryset(self): - parent = self.get_parent_object() - self.check_parent_access(parent) - qs = self.request.user.get_queryset(self.model) - return qs.filter(Q(inventory=parent) | Q(host__in=parent.hosts.all()) | Q(group__in=parent.groups.all())) - - -class InventoryInstanceGroupsList(SubListAttachDetachAPIView): - - model = InstanceGroup - serializer_class = InstanceGroupSerializer - parent_model = Inventory - relationship = 'instance_groups' - - -class InventoryAccessList(ResourceAccessList): - - model = User # needs to be User for AccessLists's - parent_model = Inventory - - -class InventoryObjectRolesList(SubListAPIView): - - model = Role - serializer_class = RoleSerializer - parent_model = Inventory - search_fields = ('role_field', 'content_type__model',) - - def get_queryset(self): - po = self.get_parent_object() - content_type = ContentType.objects.get_for_model(self.parent_model) - return Role.objects.filter(content_type=content_type, object_id=po.pk) - - -class InventoryJobTemplateList(SubListAPIView): - - model = JobTemplate - serializer_class = JobTemplateSerializer - parent_model = Inventory - relationship = 'jobtemplates' - - def get_queryset(self): - parent = self.get_parent_object() - self.check_parent_access(parent) - qs = self.request.user.get_queryset(self.model) - return qs.filter(inventory=parent) - - -class InventoryCopy(CopyAPIView): - - model = Inventory - copy_return_serializer_class = InventorySerializer - - class HostRelatedSearchMixin(object): @property diff --git a/awx/api/views/inventory.py b/awx/api/views/inventory.py new file mode 100644 index 0000000000..896fc42add --- /dev/null +++ b/awx/api/views/inventory.py @@ -0,0 +1,211 @@ +# Copyright (c) 2018 Red Hat, Inc. +# All Rights Reserved. + +# Python +import logging + +# Django +from django.conf import settings +from django.db.models import Q +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import ugettext_lazy as _ + +# Django REST Framework +from rest_framework.exceptions import PermissionDenied +from rest_framework.response import Response +from rest_framework import status + +# AWX +from awx.main.models import ( + ActivityStream, + Inventory, + JobTemplate, + Role, + User, + InstanceGroup, + InventoryUpdateEvent, + InventoryUpdate, + InventorySource, + CustomInventoryScript, +) +from awx.api.generics import ( + ListCreateAPIView, + RetrieveUpdateDestroyAPIView, + SubListAPIView, + SubListAttachDetachAPIView, + ResourceAccessList, + CopyAPIView, +) + +from awx.api.serializers import ( + InventorySerializer, + ActivityStreamSerializer, + RoleSerializer, + InstanceGroupSerializer, + InventoryUpdateEventSerializer, + CustomInventoryScriptSerializer, + InventoryDetailSerializer, + JobTemplateSerializer, +) +from awx.api.views.mixin import ( + ActivityStreamEnforcementMixin, + RelatedJobsPreventDeleteMixin, + ControlledByScmMixin, +) + +logger = logging.getLogger('awx.api.views.organization') + + +class InventoryUpdateEventsList(SubListAPIView): + + model = InventoryUpdateEvent + serializer_class = InventoryUpdateEventSerializer + parent_model = InventoryUpdate + relationship = 'inventory_update_events' + view_name = _('Inventory Update Events List') + search_fields = ('stdout',) + + def finalize_response(self, request, response, *args, **kwargs): + response['X-UI-Max-Events'] = settings.MAX_UI_JOB_EVENTS + return super(InventoryUpdateEventsList, self).finalize_response(request, response, *args, **kwargs) + + +class InventoryScriptList(ListCreateAPIView): + + model = CustomInventoryScript + serializer_class = CustomInventoryScriptSerializer + + +class InventoryScriptDetail(RetrieveUpdateDestroyAPIView): + + model = CustomInventoryScript + serializer_class = CustomInventoryScriptSerializer + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + can_delete = request.user.can_access(self.model, 'delete', instance) + if not can_delete: + raise PermissionDenied(_("Cannot delete inventory script.")) + for inv_src in InventorySource.objects.filter(source_script=instance): + inv_src.source_script = None + inv_src.save() + return super(InventoryScriptDetail, self).destroy(request, *args, **kwargs) + + +class InventoryScriptObjectRolesList(SubListAPIView): + + model = Role + serializer_class = RoleSerializer + parent_model = CustomInventoryScript + search_fields = ('role_field', 'content_type__model',) + + def get_queryset(self): + po = self.get_parent_object() + content_type = ContentType.objects.get_for_model(self.parent_model) + return Role.objects.filter(content_type=content_type, object_id=po.pk) + + +class InventoryScriptCopy(CopyAPIView): + + model = CustomInventoryScript + copy_return_serializer_class = CustomInventoryScriptSerializer + + +class InventoryList(ListCreateAPIView): + + model = Inventory + serializer_class = InventorySerializer + + def get_queryset(self): + qs = Inventory.accessible_objects(self.request.user, 'read_role') + qs = qs.select_related('admin_role', 'read_role', 'update_role', 'use_role', 'adhoc_role') + qs = qs.prefetch_related('created_by', 'modified_by', 'organization') + return qs + + +class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView): + + model = Inventory + serializer_class = InventoryDetailSerializer + + def update(self, request, *args, **kwargs): + obj = self.get_object() + kind = self.request.data.get('kind') or kwargs.get('kind') + + # Do not allow changes to an Inventory kind. + if kind is not None and obj.kind != kind: + return self.http_method_not_allowed(request, *args, **kwargs) + return super(InventoryDetail, self).update(request, *args, **kwargs) + + def destroy(self, request, *args, **kwargs): + obj = self.get_object() + if not request.user.can_access(self.model, 'delete', obj): + raise PermissionDenied() + self.check_related_active_jobs(obj) # related jobs mixin + try: + obj.schedule_deletion(getattr(request.user, 'id', None)) + return Response(status=status.HTTP_202_ACCEPTED) + except RuntimeError as e: + return Response(dict(error=_("{0}".format(e))), status=status.HTTP_400_BAD_REQUEST) + + +class InventoryActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): + + model = ActivityStream + serializer_class = ActivityStreamSerializer + parent_model = Inventory + relationship = 'activitystream_set' + search_fields = ('changes',) + + def get_queryset(self): + parent = self.get_parent_object() + self.check_parent_access(parent) + qs = self.request.user.get_queryset(self.model) + return qs.filter(Q(inventory=parent) | Q(host__in=parent.hosts.all()) | Q(group__in=parent.groups.all())) + + +class InventoryInstanceGroupsList(SubListAttachDetachAPIView): + + model = InstanceGroup + serializer_class = InstanceGroupSerializer + parent_model = Inventory + relationship = 'instance_groups' + + +class InventoryAccessList(ResourceAccessList): + + model = User # needs to be User for AccessLists's + parent_model = Inventory + + +class InventoryObjectRolesList(SubListAPIView): + + model = Role + serializer_class = RoleSerializer + parent_model = Inventory + search_fields = ('role_field', 'content_type__model',) + + def get_queryset(self): + po = self.get_parent_object() + content_type = ContentType.objects.get_for_model(self.parent_model) + return Role.objects.filter(content_type=content_type, object_id=po.pk) + + +class InventoryJobTemplateList(SubListAPIView): + + model = JobTemplate + serializer_class = JobTemplateSerializer + parent_model = Inventory + relationship = 'jobtemplates' + + def get_queryset(self): + parent = self.get_parent_object() + self.check_parent_access(parent) + qs = self.request.user.get_queryset(self.model) + return qs.filter(inventory=parent) + + +class InventoryCopy(CopyAPIView): + + model = Inventory + copy_return_serializer_class = InventorySerializer diff --git a/awx/api/views/mixin.py b/awx/api/views/mixin.py index 9a705f7b14..ee174d5091 100644 --- a/awx/api/views/mixin.py +++ b/awx/api/views/mixin.py @@ -13,12 +13,16 @@ from django.shortcuts import get_object_or_404 from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ +from rest_framework.permissions import SAFE_METHODS from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response from rest_framework import status from awx.main.constants import ACTIVE_STATES -from awx.main.utils import get_object_or_400 +from awx.main.utils import ( + get_object_or_400, + parse_yaml_or_json, +) from awx.main.models.ha import ( Instance, InstanceGroup, @@ -273,3 +277,33 @@ class OrganizationCountsMixin(object): full_context['related_field_counts'] = count_context return full_context + + +class ControlledByScmMixin(object): + ''' + Special method to reset SCM inventory commit hash + if anything that it manages changes. + ''' + + def _reset_inv_src_rev(self, obj): + if self.request.method in SAFE_METHODS or not obj: + return + project_following_sources = obj.inventory_sources.filter( + update_on_project_update=True, source='scm') + if project_following_sources: + # Allow inventory changes unrelated to variables + if self.model == Inventory and ( + not self.request or not self.request.data or + parse_yaml_or_json(self.request.data.get('variables', '')) == parse_yaml_or_json(obj.variables)): + return + project_following_sources.update(scm_last_revision='') + + def get_object(self): + obj = super(ControlledByScmMixin, self).get_object() + self._reset_inv_src_rev(obj) + return obj + + def get_parent_object(self): + obj = super(ControlledByScmMixin, self).get_parent_object() + self._reset_inv_src_rev(obj) + return obj