Skip to content

Commit

Permalink
container snapshot view
Browse files Browse the repository at this point in the history
  • Loading branch information
Christian Glatthard committed Aug 11, 2015
1 parent e73f101 commit 5674d32
Show file tree
Hide file tree
Showing 12 changed files with 341 additions and 10 deletions.
20 changes: 20 additions & 0 deletions ipynbsrv/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions ipynbsrv/api/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions ipynbsrv/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@

url(r'^containers/(?P<pk>[0-9]+)/clone$', views.container_clone, name="container_clone"),
url(r'^containers/(?P<pk>[0-9]+)/clones$', views.container_clones, name="container_clones"),
url(r'^containers/(?P<pk>[0-9]+)/snapshots/?$', views.ContainerSnapshotsList.as_view(), name="container_snapshots"),
url(r'^containers/(?P<pk>[0-9]+)/commit$', views.container_commit, name="container_commit"),
url(r'^containers/(?P<pk>[0-9]+)/create_snapshot$', views.container_create_snapshot, name="container_create_snapshot"),
url(r'^containers/(?P<pk>[0-9]+)/restore_snapshot$', views.container_restore_snapshot, name="container_restore_snapshot"),
url(r'^containers/(?P<pk>[0-9]+)/restart$', views.container_restart, name="container_restart"),
url(r'^containers/(?P<pk>[0-9]+)/resume$', views.container_resume, name="container_resume"),
url(r'^containers/(?P<pk>[0-9]+)/start$', views.container_start, name="container_start"),
Expand Down
58 changes: 54 additions & 4 deletions ipynbsrv/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
"""
Expand All @@ -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):
Expand Down
1 change: 1 addition & 0 deletions ipynbsrv/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
96 changes: 96 additions & 0 deletions ipynbsrv/web/templates/web/container_snapshots/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{% extends 'web/base.html' %}

{% load staticfiles %}

{% block external_css %}
<link rel="stylesheet" href="{% static 'bower_components/datatables/media/css/jquery.dataTables.min.css' %}">
<link rel="stylesheet" href="{% static 'bower_components/datatables-plugins-bootstrap3/dist/css/datatables-plugins-bootstrap3.css' %}">
{% endblock %}

{% block content %}
<div class="row">
<div class="col-xs-12">
{% include 'web/snippets/messages.html' with messages=messages only %}

<h1>Container Snapshots</h1>
<form role="form">
<button id="modal-create-snapshot_button" type="button" class="btn btn-primary btn-create pull-right" data-toggle="modal" data-target="#modal-create-snapshot">Create new</button>
</form>

{% if container_snapshots %}
<div class="row shares-table">
<div class="col-xs-12">
<table class="table table-hover table-striped js-table-smart" cellspacing="0" width="100%">
<thead>
<tr>
<th width="20%">Timestamp</th>
<th width="25%">Name</th>
<th width="45%">Description</th>
<th width="10%" style="text-align: right">Actions</th>
</tr>
</thead>
<tbody>
{% for snapshot in container_snapshots %}
<tr>
<td> {{ snapshot.created_on }}</td>
<td>{{ snapshot.friendly_name }}</td>
<td>{{ snapshot.description }}</td>
<td>
{% if snapshot.container.owner == request.user.backend_user.id %}
<form action="{% url 'container_restore_snapshot' %}" method="POST" class="form-action" role="form">
{% csrf_token %}
<input type="hidden" name="id" value="{{ snapshot.id }}">
<input type="hidden" name="ct_id" value="{{ container.id }}">
<button type="submit" class="btn btn-sm btn-warning" title="Restore this image?" data-toggle="confirmation" data-placement="left" disabled>
<i class="glyphicon glyphicon-open"></i>
</button>
</form>
<form action="{% url 'container_delete_snapshot' %}" method="POST" class="form-action" role="form">
{% csrf_token %}
<input type="hidden" name="id" value="{{ snapshot.id }}">
<input type="hidden" name="ct_id" value="{{ container.id }}">
<button type="submit" class="btn btn-sm btn-danger" title="Delete this image?" data-toggle="confirmation" data-placement="left">
<i class="glyphicon glyphicon-remove"></i>
</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class="alert alert-info">No container snapshots you can access available.</div>
{% endif %}

{% include 'web/container_snapshots/modal_create.html' with container=container csrf_token=csrf_token only %}
</div>
</div>
{% endblock %}

{% block js %}
<script src="{% static 'bower_components/datatables/media/js/jquery.dataTables.min.js' %}"></script>
<script src="{% static 'bower_components/datatables-plugins-bootstrap3/dist/js/datatables-plugins-bootstrap3.min.js' %}"></script>
<script src="{% static 'bower_components/bootstrap-confirmation2/bootstrap-confirmation.min.js' %}"></script>
<script>
$(function () {
{% if selected %}
$('#modal-create-snapshot_button').click();
{% endif %}

$('[data-toggle="confirmation"]').confirmation({
popout: true
});

$('.js-table-smart').dataTable({
columnDefs: [{
aTargets: [ 3 ],
bSearchable: false,
bSortable: false
}]
});
});
</script>
{% endblock %}
30 changes: 30 additions & 0 deletions ipynbsrv/web/templates/web/container_snapshots/modal_create.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<div class="modal fade" id="modal-create-snapshot" tabindex="-1" role="dialog" aria-labelledby="modal-create-snapshot" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span><span class="sr-only">Close</span>
</button>
<h4 class="modal-title">New Container Snapshot</h4>
</div>
<form role="form" action="{% url 'container_create_snapshot' %}" method="POST">
{% csrf_token %}
<div class="modal-body">
<input name="ct_id" type="hidden" class="form-control" value="{{ container.id }}">
<div class="form-group">
<label for="name">Name</label>
<input name="name" type="text" pattern="[A-Za-z1-9_-]+" class="form-control" required>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea name="description" class="form-control" rows="3" placeholder="..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Create</button>
</div>
</form>
</div>
</div>
</div>
2 changes: 1 addition & 1 deletion ipynbsrv/web/templates/web/snippets/container_grid.html
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
<button type="submit"></button>
</form>
</li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'images' %}?backup=1&amp;ct={{ ct.id }}">Backup</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'container_snapshots' ct.id %}">Snapshots</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'images' %}?share=1&amp;ct={{ ct.id }}">Share</a></li>
</ul>
</div>
Expand Down
5 changes: 5 additions & 0 deletions ipynbsrv/web/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,18 @@
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'),
url(r'^container/start$', 'ipynbsrv.web.views.containers.start', name='container_start'),
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'),
Expand Down
3 changes: 2 additions & 1 deletion ipynbsrv/web/views/_messages.py
Original file line number Diff line number Diff line change
@@ -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 :("
24 changes: 24 additions & 0 deletions ipynbsrv/web/views/container_snapshots.py
Original file line number Diff line number Diff line change
@@ -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
})
Loading

0 comments on commit 5674d32

Please sign in to comment.