From 5674d32889a030152073796ee713197a76783271 Mon Sep 17 00:00:00 2001 From: Christian Glatthard Date: Tue, 11 Aug 2015 17:47:56 +0200 Subject: [PATCH] container snapshot view --- ipynbsrv/api/permissions.py | 20 ++++ ipynbsrv/api/serializer.py | 9 ++ ipynbsrv/api/urls.py | 2 + ipynbsrv/api/views.py | 58 +++++++++- ipynbsrv/core/models.py | 1 + .../web/container_snapshots/index.html | 96 +++++++++++++++++ .../web/container_snapshots/modal_create.html | 30 ++++++ .../web/snippets/container_grid.html | 2 +- ipynbsrv/web/urls.py | 5 + ipynbsrv/web/views/_messages.py | 3 +- ipynbsrv/web/views/container_snapshots.py | 24 +++++ ipynbsrv/web/views/containers.py | 101 +++++++++++++++++- 12 files changed, 341 insertions(+), 10 deletions(-) create mode 100644 ipynbsrv/web/templates/web/container_snapshots/index.html create mode 100644 ipynbsrv/web/templates/web/container_snapshots/modal_create.html create mode 100644 ipynbsrv/web/views/container_snapshots.py diff --git a/ipynbsrv/api/permissions.py b/ipynbsrv/api/permissions.py index ae0b1ac..ae2bbfd 100644 --- a/ipynbsrv/api/permissions.py +++ b/ipynbsrv/api/permissions.py @@ -200,6 +200,26 @@ class ContainerDetailPermission(IsSuperUserOrIsObjectOwner): pass +class ContainerSnapshotDetailPermission( + permissions.BasePermission, + IsBackendUserMixin, + IsSafeMethodMixin, + IsPublicMixin, + IsObjectOwnerMixin, + IsSuperUserMixin): + """ + Todo: write doc. + """ + def has_object_permission(self, request, view, obj): + if self.is_public and self.is_safe_method(request): + return True + if self.is_superuser(request.user): + return True + if self.is_backend_user(request.user): + return self.is_owner(request.user, obj.container) + return False + + class CollaborationGroupDetailPermission( permissions.BasePermission, IsSuperUserMixin, diff --git a/ipynbsrv/api/serializer.py b/ipynbsrv/api/serializer.py index ced546a..64df83d 100644 --- a/ipynbsrv/api/serializer.py +++ b/ipynbsrv/api/serializer.py @@ -148,6 +148,14 @@ class Meta: model = PortMapping +class FlatContainerSerializer(serializers.ModelSerializer): + """ + Todo: write doc. + """ + class Meta: + model = Container + + class ContainerSerializer(serializers.ModelSerializer): """ Todo: write doc. @@ -201,6 +209,7 @@ class ContainerSnapshotSerializer(serializers.ModelSerializer): Todo: write doc. """ friendly_name = serializers.CharField(read_only=True, source='get_friendly_name') + container = FlatContainerSerializer(read_only=True, many=False) class Meta: model = ContainerSnapshot diff --git a/ipynbsrv/api/urls.py b/ipynbsrv/api/urls.py index f3f4db8..1a4ce33 100644 --- a/ipynbsrv/api/urls.py +++ b/ipynbsrv/api/urls.py @@ -31,8 +31,10 @@ url(r'^containers/(?P[0-9]+)/clone$', views.container_clone, name="container_clone"), url(r'^containers/(?P[0-9]+)/clones$', views.container_clones, name="container_clones"), + url(r'^containers/(?P[0-9]+)/snapshots/?$', views.ContainerSnapshotsList.as_view(), name="container_snapshots"), url(r'^containers/(?P[0-9]+)/commit$', views.container_commit, name="container_commit"), url(r'^containers/(?P[0-9]+)/create_snapshot$', views.container_create_snapshot, name="container_create_snapshot"), + url(r'^containers/(?P[0-9]+)/restore_snapshot$', views.container_restore_snapshot, name="container_restore_snapshot"), url(r'^containers/(?P[0-9]+)/restart$', views.container_restart, name="container_restart"), url(r'^containers/(?P[0-9]+)/resume$', views.container_resume, name="container_resume"), url(r'^containers/(?P[0-9]+)/start$', views.container_start, name="container_start"), diff --git a/ipynbsrv/api/views.py b/ipynbsrv/api/views.py index 13905a3..cfc40b1 100644 --- a/ipynbsrv/api/views.py +++ b/ipynbsrv/api/views.py @@ -55,6 +55,7 @@ def api_root(request, format=None): 'clone': 'Clone the container.', 'clones': 'Get a list of all clones of the container', 'create_snapshot': 'Create a snapshot of the container.', + 'restore_snapshot': 'Restore a snapshot of the container.', 'restart': 'Restart the container.', 'resume': 'Resume the container.', 'start': 'Start the container.', @@ -507,7 +508,7 @@ def container_commit(request, pk): @api_view(['POST']) def container_create_snapshot(request, pk): """ - Make a snapshot of the container. + Create a snapshot of the container. Todo: show params on OPTIONS call. :param pk pk of the container that needs to be cloned :param name @@ -539,6 +540,37 @@ def container_create_snapshot(request, pk): return Response({"error": "Container not found!", "pk": pk}) +@api_view(['POST']) +def container_restore_snapshot(request, pk): + """ + Restore a snapshot of the container. + Todo: show params on OPTIONS call. + :param pk pk of the container that needs to be cloned + """ + params = {} + + data = request.data + + if not data.get('id'): + return Response({"error": "please provide name for the clone: {\"name\" : \"some name \"}"}) + + params['id'] = data.get('id') + + container = get_container(pk) + + # validate permissions + validate_object_permission(ContainerDetailPermission, request, container) + + snapshots = ContainerSnapshot.objects.filter(id=params.get('id')) + if container and snapshots: + s = snapshots.first() + s.restore() + serializer = ContainerSerializer(container) + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + return Response({"error": "Container or Snapshot not found!", "pk": pk}) + + @api_view(['GET']) def container_clones(request, pk): container = get_container(pk) @@ -675,7 +707,25 @@ class ContainerImageDetail(generics.RetrieveUpdateDestroyAPIView): queryset = ContainerImage.objects.all() -class ContainerSnapshotList(generics.ListCreateAPIView): +class ContainerSnapshotsList(generics.ListAPIView): + """ + Get a list of all snapshots for a specific container. + """ + serializer_class = ContainerSnapshotSerializer + + def get_queryset(self): + # get pk of container from url + pk = self.kwargs['pk'] + if self.request.user.is_superuser: + queryset = ContainerSnapshot.objects.all().filter(container__id=pk) + else: + queryset = ContainerSnapshot.objects.filter( + container__owner=self.request.user.backend_user + ).filter(container=pk) + return queryset + + +class ContainerSnapshotList(generics.ListAPIView): """ Get a list of all the container snapshots. """ @@ -696,8 +746,8 @@ class ContainerSnapshotDetail(generics.RetrieveUpdateDestroyAPIView): Get details of a container snapshot. """ serializer_class = ContainerSnapshotSerializer - permission_classes = [ContainerDetailPermission] - queryset = queryset = ContainerSnapshot.objects.all() + permission_classes = [ContainerSnapshotDetailPermission] + queryset = ContainerSnapshot.objects.all() class ServerList(generics.ListCreateAPIView): diff --git a/ipynbsrv/core/models.py b/ipynbsrv/core/models.py index 6dea440..cfb7193 100644 --- a/ipynbsrv/core/models.py +++ b/ipynbsrv/core/models.py @@ -859,6 +859,7 @@ class ContainerSnapshot(models.Model): related_name='snapshots', help_text='The container from which this snapshot was taken/is for.' ) + created_on = models.DateTimeField(auto_now_add=True) def get_friendly_name(self): """ diff --git a/ipynbsrv/web/templates/web/container_snapshots/index.html b/ipynbsrv/web/templates/web/container_snapshots/index.html new file mode 100644 index 0000000..5df0e00 --- /dev/null +++ b/ipynbsrv/web/templates/web/container_snapshots/index.html @@ -0,0 +1,96 @@ +{% extends 'web/base.html' %} + +{% load staticfiles %} + +{% block external_css %} + + +{% endblock %} + +{% block content %} +
+
+ {% include 'web/snippets/messages.html' with messages=messages only %} + +

Container Snapshots

+
+ +
+ + {% if container_snapshots %} +
+
+ + + + + + + + + + + {% for snapshot in container_snapshots %} + + + + + + + {% endfor %} + +
TimestampNameDescriptionActions
{{ snapshot.created_on }}{{ snapshot.friendly_name }}{{ snapshot.description }} + {% if snapshot.container.owner == request.user.backend_user.id %} +
+ {% csrf_token %} + + + +
+
+ {% csrf_token %} + + + +
+ {% endif %} +
+
+
+ {% else %} +
No container snapshots you can access available.
+ {% endif %} + + {% include 'web/container_snapshots/modal_create.html' with container=container csrf_token=csrf_token only %} +
+
+{% endblock %} + +{% block js %} + + + + +{% endblock %} diff --git a/ipynbsrv/web/templates/web/container_snapshots/modal_create.html b/ipynbsrv/web/templates/web/container_snapshots/modal_create.html new file mode 100644 index 0000000..d4ada7c --- /dev/null +++ b/ipynbsrv/web/templates/web/container_snapshots/modal_create.html @@ -0,0 +1,30 @@ + diff --git a/ipynbsrv/web/templates/web/snippets/container_grid.html b/ipynbsrv/web/templates/web/snippets/container_grid.html index 28fb524..d6cb5fb 100644 --- a/ipynbsrv/web/templates/web/snippets/container_grid.html +++ b/ipynbsrv/web/templates/web/snippets/container_grid.html @@ -114,7 +114,7 @@ -
  • Backup
  • +
  • Snapshots
  • Share
  • diff --git a/ipynbsrv/web/urls.py b/ipynbsrv/web/urls.py index 13438ff..d98de51 100644 --- a/ipynbsrv/web/urls.py +++ b/ipynbsrv/web/urls.py @@ -29,6 +29,9 @@ url(r'^containers/$', 'ipynbsrv.web.views.containers.index', name='containers'), url(r'^container/clone$', 'ipynbsrv.web.views.containers.clone', name='container_clone'), url(r'^container/commit$', 'ipynbsrv.web.views.containers.commit', name='container_commit'), + url(r'^container/create_snapshot$', 'ipynbsrv.web.views.containers.create_snapshot', name='container_create_snapshot'), + url(r'^container/delete_snapshot$', 'ipynbsrv.web.views.containers.delete_snapshot', name='container_delete_snapshot'), + url(r'^container/restore_snapshot$', 'ipynbsrv.web.views.containers.restore_snapshot', name='container_restore_snapshot'), url(r'^container/create$', 'ipynbsrv.web.views.containers.create', name='container_create'), url(r'^container/delete$', 'ipynbsrv.web.views.containers.delete', name='container_delete'), url(r'^container/restart$', 'ipynbsrv.web.views.containers.restart', name='container_restart'), @@ -36,6 +39,8 @@ url(r'^container/stop$', 'ipynbsrv.web.views.containers.stop', name='container_stop'), url(r'^container/suspend$', 'ipynbsrv.web.views.containers.suspend', name='container_suspend'), url(r'^container/resume$', 'ipynbsrv.web.views.containers.resume', name='container_resume'), + url(r'^container/(\d+)/snapshots$', 'ipynbsrv.web.views.container_snapshots.index', name='container_snapshots'), + # # /images(s)/... url(r'^images/$', 'ipynbsrv.web.views.images.index', name='images'), diff --git a/ipynbsrv/web/views/_messages.py b/ipynbsrv/web/views/_messages.py index 1004b8f..b0567ff 100644 --- a/ipynbsrv/web/views/_messages.py +++ b/ipynbsrv/web/views/_messages.py @@ -1,8 +1,9 @@ from ipynbsrv import settings +import json def api_error_message(exception, params): if settings.DEBUG: - return "{}. Data: {}".format(exception, params) + return "{}. Data: {}".format(exception, json.dumps(params)) else: return "Whooops, something went wrong when calling the API :(" diff --git a/ipynbsrv/web/views/container_snapshots.py b/ipynbsrv/web/views/container_snapshots.py new file mode 100644 index 0000000..1e76b52 --- /dev/null +++ b/ipynbsrv/web/views/container_snapshots.py @@ -0,0 +1,24 @@ +from django.contrib import messages +from django.contrib.auth.decorators import user_passes_test +from django.shortcuts import render +from ipynbsrv.core.auth.checks import login_allowed +from ipynbsrv.web.api_client_proxy import get_httpclient_instance +from ipynbsrv.web.views._messages import api_error_message + + +@user_passes_test(login_allowed) +def index(request, ct_id): + """ + Get a list of all snapshots for this container. + """ + client = get_httpclient_instance(request) + container = client.containers(ct_id).get() + container_snapshots = client.containers(ct_id).snapshots.get() + new_notifications_count = len(client.notificationlogs.unread.get()) + + return render(request, 'web/container_snapshots/index.html', { + 'title': "Container Snapshots", + 'container_snapshots': container_snapshots, + 'container': container, + 'new_notifications_count': new_notifications_count + }) diff --git a/ipynbsrv/web/views/containers.py b/ipynbsrv/web/views/containers.py index 686033a..6d72bdf 100644 --- a/ipynbsrv/web/views/containers.py +++ b/ipynbsrv/web/views/containers.py @@ -1,14 +1,45 @@ from django.contrib import messages from django.contrib.auth.decorators import user_passes_test -from django.db.models import Q from django.shortcuts import redirect, render from ipynbsrv.core.auth.checks import login_allowed -from ipynbsrv.core.models import Container, ContainerImage, Server from ipynbsrv.web.api_client_proxy import get_httpclient_instance from ipynbsrv.web.views._messages import api_error_message from slumber.exceptions import HttpNotFoundError +@user_passes_test(login_allowed) +def create_snapshot(request): + """ + Todo: write doc. + Todo: get name & description param from GUI. + """ + print(request.POST) + if request.method != "POST": + messages.error(request, "Invalid request method.") + return redirect('containers') + if 'ct_id' not in request.POST or 'name' not in request.POST: + messages.error(request, "Invalid POST request.") + return redirect('containers') + + ct_id = int(request.POST.get('ct_id')) + params = {} + params['name'] = request.POST.get('name', '') + description = request.POST.get('description', '') + if description: + params['description'] = description + + client = get_httpclient_instance(request) + + # create snapshot + try: + client.containers(ct_id).create_snapshot.post(params) + messages.success(request, "Sucessfully created snapshot `{}`.".format(params.get('name'))) + except Exception as e: + messages.error(request, api_error_message(e, params)) + + return redirect('container_snapshots', ct_id) + + @user_passes_test(login_allowed) def clone(request): """ @@ -22,14 +53,16 @@ def clone(request): messages.error(request, "Invalid POST request.") return redirect('containers') + params = {} + ct_id = int(request.POST.get('id')) client = get_httpclient_instance(request) container = client.containers(ct_id).get() - new_name = "{}_clone".format(container.name) + params['new_name'] = "{}_clone".format(container.name) # create clone try: - client.containers(ct_id).clone.post({"name": new_name}) + client.containers(ct_id).clone.post(params) messages.success(request, "Sucessfully created the clone `{}`.".format(new_name)) except Exception as e: messages.error(request, api_error_message(e, params)) @@ -329,3 +362,63 @@ def resume(request): messages.error(request, "Container does not exist.") return redirect('containers') + + +@user_passes_test(login_allowed) +def restore_snapshot(request): + """ + Todo: write doc. + Todo: get name & description param from GUI. + """ + print(request.POST) + if request.method != "POST": + messages.error(request, "Invalid request method.") + return redirect('containers') + if 'id' not in request.POST or 'ct_id' not in request.POST: + messages.error(request, "Invalid POST request.") + return redirect('containers') + + ct_id = int(request.POST.get('ct_id')) + params = {} + params['id'] = int(request.POST.get('id')) + + client = get_httpclient_instance(request) + + # create snapshot + try: + client.containers(ct_id).restore_snapshot.post(params) + messages.success(request, "Sucessfully restored snapshot `{}`.".format(params.get('name'))) + except Exception as e: + messages.error(request, api_error_message(e, params)) + + return redirect('container_snapshots', ct_id) + + +@user_passes_test(login_allowed) +def delete_snapshot(request): + """ + Todo: write doc. + """ + print(request.POST) + if request.method != "POST": + messages.error(request, "Invalid request method.") + return redirect('containers') + if 'id' not in request.POST or 'ct_id' not in request.POST: + messages.error(request, "Invalid POST request.") + return redirect('containers') + + snapshot_id = int(request.POST.get('id')) + params = {} + params['id'] = snapshot_id + ct_id = params['ct_id'] = int(request.POST.get('ct_id')) + + client = get_httpclient_instance(request) + + # create snapshot + try: + client.containers.snapshots(snapshot_id).delete() + messages.success(request, "Sucessfully deleted snapshot.") + except Exception as e: + messages.error(request, api_error_message(e, params)) + + return redirect('container_snapshots', ct_id)