github.com/amrnt/deis@v1.3.1/controller/api/views.py (about)

     1  """
     2  RESTful view classes for presenting Deis API objects.
     3  """
     4  
     5  from django.conf import settings
     6  from django.core.exceptions import ValidationError
     7  from django.contrib.auth.models import User
     8  from django.shortcuts import get_object_or_404
     9  from guardian.shortcuts import assign_perm, get_objects_for_user, \
    10      get_users_with_perms, remove_perm
    11  from rest_framework import mixins, renderers, status
    12  from rest_framework.decorators import permission_classes
    13  from rest_framework.exceptions import PermissionDenied
    14  from rest_framework.permissions import IsAuthenticated
    15  from rest_framework.response import Response
    16  from rest_framework.viewsets import GenericViewSet
    17  
    18  from api import authentication, models, permissions, serializers, viewsets
    19  
    20  
    21  class UserRegistrationViewSet(GenericViewSet,
    22                                mixins.CreateModelMixin):
    23      """ViewSet to handle registering new users. The logic is in the serializer."""
    24      authentication_classes = [authentication.AnonymousAuthentication]
    25      permission_classes = [permissions.IsAnonymous, permissions.HasRegistrationAuth]
    26      serializer_class = serializers.UserSerializer
    27  
    28  
    29  class UserManagementViewSet(GenericViewSet,
    30                              mixins.DestroyModelMixin):
    31      serializer_class = serializers.UserSerializer
    32  
    33      def get_queryset(self):
    34          return User.objects.filter(pk=self.request.user.pk)
    35  
    36      def get_object(self):
    37          return self.get_queryset()[0]
    38  
    39      def passwd(self, request, **kwargs):
    40          obj = self.get_object()
    41          if not obj.check_password(request.data['password']):
    42              return Response({'detail': 'Current password does not match'},
    43                              status=status.HTTP_400_BAD_REQUEST)
    44          obj.set_password(request.data['new_password'])
    45          obj.save()
    46          return Response({'status': 'password set'})
    47  
    48  
    49  class BaseDeisViewSet(viewsets.OwnerViewSet):
    50      """
    51      A generic ViewSet for objects related to Deis.
    52  
    53      To use it, at minimum you'll need to provide the `serializer_class` attribute and
    54      the `model` attribute shortcut.
    55      """
    56      lookup_field = 'id'
    57      permission_classes = [IsAuthenticated, permissions.IsAppUser]
    58      renderer_classes = [renderers.JSONRenderer]
    59  
    60      def create(self, request, *args, **kwargs):
    61          try:
    62              return super(BaseDeisViewSet, self).create(request, *args, **kwargs)
    63          # If the scheduler oopsie'd
    64          except RuntimeError as e:
    65              return Response({'detail': str(e)}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
    66  
    67  
    68  class AppResourceViewSet(BaseDeisViewSet):
    69      """A viewset for objects which are attached to an application."""
    70  
    71      def get_app(self):
    72          app = get_object_or_404(models.App, id=self.kwargs['id'])
    73          self.check_object_permissions(self.request, app)
    74          return app
    75  
    76      def get_queryset(self, **kwargs):
    77          app = self.get_app()
    78          return self.model.objects.filter(app=app)
    79  
    80      def get_object(self, **kwargs):
    81          return self.get_queryset(**kwargs).latest('created')
    82  
    83      def create(self, request, **kwargs):
    84          request.data['app'] = self.get_app()
    85          return super(AppResourceViewSet, self).create(request, **kwargs)
    86  
    87  
    88  class ReleasableViewSet(AppResourceViewSet):
    89      """A viewset for application resources which affect the release cycle.
    90  
    91      When a resource is created, a new release is created for the application
    92      and it returns some success headers regarding the new release.
    93  
    94      To use it, at minimum you'll need to provide a `release` attribute tied to your class before
    95      calling post_save().
    96      """
    97      def get_object(self):
    98          """Retrieve the object based on the latest release's value"""
    99          return getattr(self.get_app().release_set.latest(), self.model.__name__.lower())
   100  
   101      def get_success_headers(self, data, **kwargs):
   102          headers = super(ReleasableViewSet, self).get_success_headers(data)
   103          headers.update({'X-Deis-Release': self.release.version})
   104          return headers
   105  
   106  
   107  class AppViewSet(BaseDeisViewSet):
   108      """A viewset for interacting with App objects."""
   109      model = models.App
   110      serializer_class = serializers.AppSerializer
   111  
   112      def get_queryset(self, *args, **kwargs):
   113          return self.model.objects.all(*args, **kwargs)
   114  
   115      def list(self, request, *args, **kwargs):
   116          """
   117          HACK: Instead of filtering by the queryset, we limit the queryset to list only the apps
   118          which are owned by the user as well as any apps they have been given permission to
   119          interact with.
   120          """
   121          queryset = super(AppViewSet, self).get_queryset(**kwargs) | \
   122              get_objects_for_user(self.request.user, 'api.use_app')
   123          instance = self.filter_queryset(queryset)
   124          page = self.paginate_queryset(instance)
   125          if page is not None:
   126              serializer = self.get_pagination_serializer(page)
   127          else:
   128              serializer = self.get_serializer(instance, many=True)
   129          return Response(serializer.data)
   130  
   131      def post_save(self, app):
   132          app.create()
   133  
   134      def scale(self, request, **kwargs):
   135          new_structure = {}
   136          app = self.get_object()
   137          try:
   138              for target, count in request.data.items():
   139                  new_structure[target] = int(count)
   140              models.validate_app_structure(new_structure)
   141              app.scale(request.user, new_structure)
   142          except (TypeError, ValueError):
   143              return Response({'detail': 'Invalid scaling format'},
   144                              status=status.HTTP_400_BAD_REQUEST)
   145          except (EnvironmentError, ValidationError) as e:
   146              return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
   147          except RuntimeError as e:
   148              return Response({'detail': str(e)}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
   149          return Response(status=status.HTTP_204_NO_CONTENT)
   150  
   151      def logs(self, request, **kwargs):
   152          app = self.get_object()
   153          try:
   154              return Response(app.logs(), status=status.HTTP_200_OK, content_type='text/plain')
   155          except EnvironmentError:
   156              return Response("No logs for {}".format(app.id),
   157                              status=status.HTTP_204_NO_CONTENT,
   158                              content_type='text/plain')
   159  
   160      def run(self, request, **kwargs):
   161          app = self.get_object()
   162          try:
   163              output_and_rc = app.run(self.request.user, request.data['command'])
   164          except EnvironmentError as e:
   165              return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
   166          except RuntimeError as e:
   167              return Response({'detail': str(e)}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
   168          return Response(output_and_rc, status=status.HTTP_200_OK,
   169                          content_type='text/plain')
   170  
   171  
   172  class BuildViewSet(ReleasableViewSet):
   173      """A viewset for interacting with Build objects."""
   174      model = models.Build
   175      serializer_class = serializers.BuildSerializer
   176  
   177      def post_save(self, build):
   178          self.release = build.create(self.request.user)
   179          super(BuildViewSet, self).post_save(build)
   180  
   181  
   182  class ConfigViewSet(ReleasableViewSet):
   183      """A viewset for interacting with Config objects."""
   184      model = models.Config
   185      serializer_class = serializers.ConfigSerializer
   186  
   187      def post_save(self, config):
   188          release = config.app.release_set.latest()
   189          self.release = release.new(self.request.user, config=config, build=release.build)
   190          try:
   191              config.app.deploy(self.request.user, self.release)
   192          except RuntimeError:
   193              self.release.delete()
   194              raise
   195  
   196  
   197  class ContainerViewSet(AppResourceViewSet):
   198      """A viewset for interacting with Container objects."""
   199      model = models.Container
   200      serializer_class = serializers.ContainerSerializer
   201  
   202      def get_queryset(self, **kwargs):
   203          qs = super(ContainerViewSet, self).get_queryset(**kwargs)
   204          container_type = self.kwargs.get('type')
   205          if container_type:
   206              qs = qs.filter(type=container_type)
   207          else:
   208              qs = qs.exclude(type='run')
   209          return qs
   210  
   211      def get_object(self, **kwargs):
   212          qs = self.get_queryset(**kwargs)
   213          return qs.get(num=self.kwargs['num'])
   214  
   215  
   216  class DomainViewSet(AppResourceViewSet):
   217      """A viewset for interacting with Domain objects."""
   218      model = models.Domain
   219      serializer_class = serializers.DomainSerializer
   220  
   221  
   222  class KeyViewSet(BaseDeisViewSet):
   223      """A viewset for interacting with Key objects."""
   224      model = models.Key
   225      serializer_class = serializers.KeySerializer
   226  
   227  
   228  class ReleaseViewSet(AppResourceViewSet):
   229      """A viewset for interacting with Release objects."""
   230      model = models.Release
   231      serializer_class = serializers.ReleaseSerializer
   232  
   233      def get_object(self, **kwargs):
   234          """Get release by version always"""
   235          return self.get_queryset(**kwargs).get(version=self.kwargs['version'])
   236  
   237      def rollback(self, request, **kwargs):
   238          """
   239          Create a new release as a copy of the state of the compiled slug and config vars of a
   240          previous release.
   241          """
   242          app = self.get_app()
   243          try:
   244              release = app.release_set.latest()
   245              version_to_rollback_to = release.version - 1
   246              if request.data.get('version'):
   247                  version_to_rollback_to = int(request.data['version'])
   248              new_release = release.rollback(request.user, version_to_rollback_to)
   249              response = {'version': new_release.version}
   250              return Response(response, status=status.HTTP_201_CREATED)
   251          except EnvironmentError as e:
   252              return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
   253          except RuntimeError:
   254              new_release.delete()
   255              raise
   256  
   257  
   258  class BaseHookViewSet(BaseDeisViewSet):
   259      permission_classes = [permissions.HasBuilderAuth]
   260  
   261  
   262  class PushHookViewSet(BaseHookViewSet):
   263      """API hook to create new :class:`~api.models.Push`"""
   264      model = models.Push
   265      serializer_class = serializers.PushSerializer
   266  
   267      def create(self, request, *args, **kwargs):
   268          app = get_object_or_404(models.App, id=request.data['receive_repo'])
   269          request.user = get_object_or_404(User, username=request.data['receive_user'])
   270          # check the user is authorized for this app
   271          if not permissions.is_app_user(request, app):
   272              raise PermissionDenied()
   273          request.data['app'] = app
   274          request.data['owner'] = request.user
   275          return super(PushHookViewSet, self).create(request, *args, **kwargs)
   276  
   277  
   278  class BuildHookViewSet(BaseHookViewSet):
   279      """API hook to create new :class:`~api.models.Build`"""
   280      model = models.Build
   281      serializer_class = serializers.BuildSerializer
   282  
   283      def create(self, request, *args, **kwargs):
   284          app = get_object_or_404(models.App, id=request.data['receive_repo'])
   285          self.user = request.user = get_object_or_404(User, username=request.data['receive_user'])
   286          # check the user is authorized for this app
   287          if not permissions.is_app_user(request, app):
   288              raise PermissionDenied()
   289          request.data['app'] = app
   290          request.data['owner'] = self.user
   291          super(BuildHookViewSet, self).create(request, *args, **kwargs)
   292          # return the application databag
   293          response = {'release': {'version': app.release_set.latest().version},
   294                      'domains': ['.'.join([app.id, settings.DEIS_DOMAIN])]}
   295          return Response(response, status=status.HTTP_200_OK)
   296  
   297      def post_save(self, build):
   298          build.create(self.user)
   299  
   300  
   301  class ConfigHookViewSet(BaseHookViewSet):
   302      """API hook to grab latest :class:`~api.models.Config`"""
   303      model = models.Config
   304      serializer_class = serializers.ConfigSerializer
   305  
   306      def create(self, request, *args, **kwargs):
   307          app = get_object_or_404(models.App, id=request.data['receive_repo'])
   308          request.user = get_object_or_404(User, username=request.data['receive_user'])
   309          # check the user is authorized for this app
   310          if not permissions.is_app_user(request, app):
   311              raise PermissionDenied()
   312          config = app.release_set.latest().config
   313          serializer = self.get_serializer(config)
   314          return Response(serializer.data, status=status.HTTP_200_OK)
   315  
   316  
   317  class AppPermsViewSet(BaseDeisViewSet):
   318      """RESTful views for sharing apps with collaborators."""
   319  
   320      model = models.App  # models class
   321      perm = 'use_app'    # short name for permission
   322  
   323      def get_queryset(self):
   324          return self.model.objects.all()
   325  
   326      @permission_classes([permissions.IsAppUser])
   327      def list(self, request, **kwargs):
   328          app = self.get_object()
   329          perm_name = "api.{}".format(self.perm)
   330          usernames = [u.username for u in get_users_with_perms(app)
   331                       if u.has_perm(perm_name, app)]
   332          return Response({'users': usernames})
   333  
   334      @permission_classes([permissions.IsOwnerOrAdmin])
   335      def create(self, request, **kwargs):
   336          app = self.get_object()
   337          user = get_object_or_404(User, username=request.data['username'])
   338          assign_perm(self.perm, user, app)
   339          models.log_event(app, "User {} was granted access to {}".format(user, app))
   340          return Response(status=status.HTTP_201_CREATED)
   341  
   342      @permission_classes([permissions.IsOwnerOrAdmin])
   343      def destroy(self, request, **kwargs):
   344          app = self.get_object()
   345          user = get_object_or_404(User, username=kwargs['username'])
   346          if not user.has_perm(self.perm, app):
   347              raise PermissionDenied()
   348          remove_perm(self.perm, user, app)
   349          models.log_event(app, "User {} was revoked access to {}".format(user, app))
   350          return Response(status=status.HTTP_204_NO_CONTENT)
   351  
   352  
   353  class AdminPermsViewSet(BaseDeisViewSet):
   354      """RESTful views for sharing admin permissions with other users."""
   355  
   356      model = User
   357      serializer_class = serializers.AdminUserSerializer
   358      permission_classes = [permissions.IsAdmin]
   359  
   360      def get_queryset(self, **kwargs):
   361          self.check_object_permissions(self.request, self.request.user)
   362          return self.model.objects.filter(is_active=True, is_superuser=True)
   363  
   364      def create(self, request, **kwargs):
   365          user = get_object_or_404(User, username=request.data['username'])
   366          user.is_superuser = user.is_staff = True
   367          user.save(update_fields=['is_superuser', 'is_staff'])
   368          return Response(status=status.HTTP_201_CREATED)
   369  
   370      def destroy(self, request, **kwargs):
   371          user = get_object_or_404(User, username=kwargs['username'])
   372          user.is_superuser = user.is_staff = False
   373          user.save(update_fields=['is_superuser', 'is_staff'])
   374          return Response(status=status.HTTP_204_NO_CONTENT)