Commit 5cbfba57 by Kálmán Viktor

Merge remote-tracking branch 'origin/master' into issue-24

Conflicts:
	circle/dashboard/urls.py
parents 391c72e1 99986b1b
=============
cirecle-cloud
=============
============
circle-cloud
============
This is the Django based controller and web portal of the CIRCLE Cloud.
This is the Django based controller and web portal of the CIRCLE Cloud.
\ No newline at end of file
[
{
"pk": 1,
"model": "firewall.vlan",
"fields": {
"comment": "",
"ipv6_template": "2001:7:2:4031:%(b)d:%(c)d:%(d)d:0",
"domain": 1,
"dhcp_pool": "",
"managed": true,
"name": "pub",
"vid": 3066,
"created_at": "2014-02-19T17:00:17.358Z",
"modified_at": "2014-02-19T17:00:17.358Z",
"owner": null,
"snat_ip": null,
"snat_to": [],
"network6": null,
"network4": "10.7.0.93/16",
"reverse_domain": "%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa",
"network_type": "public",
"description": ""
}
},
{
"pk": 1,
"model": "firewall.host",
"fields": {
"comment": "",
"vlan": 1,
"reverse": "",
"created_at": "2014-02-19T17:03:45.365Z",
"hostname": "devenv",
"modified_at": "2014-02-24T15:55:01.412Z",
"location": "",
"pub_ipv4": null,
"mac": "11:22:33:44:55:66",
"shared_ip": false,
"ipv4": "10.7.0.96",
"groups": [],
"ipv6": null,
"owner": 1,
"description": ""
}
},
{
"pk": 1,
"model": "firewall.domain",
"fields": {
"description": "",
"created_at": "2014-02-19T17:00:08.819Z",
"modified_at": "2014-02-19T17:00:08.819Z",
"ttl": 600,
"owner": 1,
"name": "test.ik.bme.hu"
}
},
{
"pk": 1,
"model": "vm.node",
"fields": {
"name": "devenv",
"created": "2014-02-19T17:03:45.322Z",
"overcommit": 1.0,
"enabled": true,
"modified": "2014-02-19T21:11:34.671Z",
"priority": 1,
"traits": [],
"host": 1
}
}
]
......@@ -332,8 +332,6 @@ a.hover-black {
}
<<<<<<< HEAD
.notification-messages {
padding: 10px 8px;
width: 350px;
......
......@@ -4,16 +4,29 @@ $(function() {
/* vm migrate */
$('.vm-migrate').click(function(e) {
var icon = $(this).children("i");
var vm = $(this).data("vm-pk");
icon.removeClass("icon-truck").addClass("icon-spinner icon-spin");
$.ajax({
type: 'GET',
url: '/dashboard/vm/' + vm + '/migrate/',
success: function(data) {
success: function(data) {
icon.addClass("icon-truck").removeClass("icon-spinner icon-spin");
$('body').append(data);
$('#create-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove();
});
$('#vm-migrate-node-list li').click(function(e) {
var li = $(this).closest('li');
if (li.find('input').attr('disabled'))
return true;
$('#vm-migrate-node-list li').removeClass('panel-primary');
li.addClass('panel-primary').find('input').attr('checked', true);
return false;
});
$('#vm-migrate-node-list li input:checked').closest('li').addClass('panel-primary');
}
});
return false;
......
$(function() {
"use strict";
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
"input.js", "display.js", "jsunzip.js", "rfb.js"]);
var rfb;
function updateState(rfb, state, oldstate, msg) {
$('#_console .btn-toolbar button').attr('disabled', !(state === "normal"));
rfb.sendKey(0xffe3); // press and release ctrl to kill screensaver
if (typeof(msg) !== 'undefined') {
$('#noVNC_status').html(msg);
}
}
$('a[data-toggle$="pill"][href!="#console"]').click(function() {
if (rfb) {
rfb.disconnect();
rfb = 0;
}
$("#vm-info-pane").fadeIn();
$("#vm-detail-pane").removeClass("col-md-12");
});
$('#sendCtrlAltDelButton').click(function() {
rfb.sendCtrlAltDel(); return false;});
$('#sendPasswordButton').click(function() {
var pw = $("#vm-details-pw-input").val();
for (var i=0; i < pw.length; i++) {
rfb.sendKey(pw.charCodeAt(i));
} return false;});
$("body").on("click", 'a[href$="console"]', function() {
var host, port, password, path;
$("#vm-info-pane").hide();
$("#vm-detail-pane").addClass("col-md-12");
WebUtil.init_logging('warn');
host = window.location.hostname;
if (window.location.port == 8080) {
port = 9999;
} else {
port = window.location.port == "" ? "443" : window.location.port;
}
password = '';
$('#_console .btn-toolbar button').attr('disabled', true);
$('#noVNC_status').html('Retreiving authorization token.');
$.get(VNC_URL, function(data) {
if (data.indexOf('vnc') != 0) {
$('#noVNC_status').html('No authorization token received.');
}
else {
rfb = new RFB({'target': $D('noVNC_canvas'),
'encrypt': (window.location.protocol === "https:"),
'true_color': true,
'local_cursor': true,
'shared': true,
'view_only': false,
'updateState': updateState});
rfb.connect(host, port, password, data);
}
}).fail(function(){
$('#noVNC_status').html("Can't connect to console.");
});
});
if (window.location.hash == "#console")
window.onscriptsload = function(){$('a[href$="console"]').click();};
});
......@@ -131,7 +131,11 @@ $(function() {
location.reload();
},
error: function(xhr, textStatus, error) {
if (xhr.status == 500) {
addMessage("Internal Server Error", "danger");
} else {
addMessage(xhr.status + " Unknown Error", "danger");
}
}
});
} else {
......
......@@ -10,7 +10,7 @@
</span>
<div style="clear: both;"></div>
<div class="notification-message-text">
{{ n.message }}
{{ n.message|safe }}
</div>
</li>
{% empty %}
......
{% load i18n %}
{% load sizefieldtags %}
<form method="POST" action="{% url "dashboard.views.vm-migrate" pk=vm %}">
<form method="POST" action="{% url "dashboard.views.vm-migrate" pk=vm.pk %}">
{% csrf_token %}
<ul id="vm-migrate-node-list">
{% for n in nodes %}
<li>
<strong>{{ n }}</strong>
<input type="radio" name="node" value="{{ n.pk }}" style="float: right;"/>
<span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span>
<span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span>
<div style="clear: both;"></div>
</li>
{% endfor %}
{% with current=vm.node.pk selected=vm.select_node.pk %}
{% for n in nodes %}
<li class="panel panel-default"><div class="panel-body">
<label for="migrate-to-{{n.pk}}">
<strong>{{ n }}</strong>
{% if current == n.pk %}<div class="label label-info">{% trans "current" %}</div>{% endif %}
{% if selected == n.pk %}<div class="label label-success">{% trans "recommended" %}</div>{% endif %}
</label>
<input id="migrate-to-{{n.pk}}" type="radio" name="node" value="{{ n.pk }}" style="float: right;"
{% if current == n.pk %}disabled="disabled"{% endif %}
{% if selected == n.pk %}checked="checked"{% endif %} />
<span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span>
<span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span>
<div style="clear: both;"></div>
</div></li>
{% endfor %}
{% endwith %}
</ul>
<button type="submit" class="btn btn-primary btn-sm"><i class="icon-truck"></i> Migrate</button>
</form>
{%load i18n%}
{%blocktrans with instance=instance.name user=user.name%}
Your ownership offer of {{instance}} has been accepted by {{user}}.
{%endblocktrans%}
{%load i18n%}
{%blocktrans with instance=instance.name user=user.name%}
{{user}} offered you to take the ownership of his/her virtual machine
called {{instance}}.{%endblocktrans%}
<a href="{{token}}" class="btn btn-success btn-small">{%trans "Accept"%}</a>
......@@ -80,7 +80,7 @@
<dt>Password:</dt>
<dd>
<div class="input-group">
<input type="text" class="form-control input-sm input-tags" value="{{ instance.pw }}"/>
<input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags" value="{{ instance.pw }}"/>
<span class="input-group-addon input-tags" id="vm-details-pw-show">
<i class="icon-eye-open" id="vm-details-pw-eye" title="Show password"></i>
</span>
......@@ -113,8 +113,8 @@
<i class="icon-tasks icon-2x"></i><br>
{% trans "Resources" %}</a>
</li>
<li {% if instance.state != "RUNNING" %}class="disabled"{% endif %}>
<a href="#{% if instance.state == "RUNNING" %}console" data-toggle="pill{% endif %}" data-target="#_console" class="text-center">
<li {% if not instance.is_console_available %}class="disabled"{% endif %}>
<a href="#console" data-toggle="pill" data-target="#_console" class="text-center">
<i class="icon-desktop icon-2x"></i><br>
{% trans "Console" %}</a></li>
<li>
......@@ -152,4 +152,5 @@
{% block extra_js %}
<script src="{{ STATIC_URL }}dashboard/vm-details.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-common.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-console.js"></script>
{% endblock %}
......@@ -4,13 +4,16 @@
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
<i class="{% if not a.finished %} icon-refresh icon-spin {% else %}icon-plus{% endif %}"></i>
</span>
<strong>{{ a.get_readable_name }}</strong>
{{ a.started|date:"Y-m-d H:i" }}, {{ a.user }}
<strong{% if user.is_superuser and a.result %} title="{{ a.result }}"{% endif %}>
{{ a.get_readable_name }}
</strong>
{{ a.started|date:"Y-m-d H:i" }}{% if a.user %}, {{ a.user }}{% endif %}
{% if a.children.count > 0 %}
<div class="sub-timeline">
{% for s in a.children.all %}
<div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}">
{{ s.get_readable_name }} -
<span{% if user.is_superuser and s.result %} title="{{ s.result }}"{% endif %}>
{{ s.get_readable_name }}</span> &ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
......
{% load i18n %}
<h3>{% trans "Owner" %}</h3>
<p>
{% if user == instance.owner %}
{% blocktrans %}You are the current owner of this instance.{% endblocktrans %}
<a href="#" class="btn btn-link">{% trans "Transfer ownership..." %}</a>
{% else %}
{% blocktrans with owner=instance.owner %}
The current owner of this instance is {{owner}}.
{% endblocktrans %}
{% endif %}
{% if user == instance.owner or user.is_superuser %}
<a href="{% url "dashboard.views.vm-transfer-ownership" instance.pk %}"
class="btn btn-link">{% trans "Transfer ownership..." %}</a>
{% endif %}
</p>
<h3>{% trans "Permissions"|capfirst %}</h3>
<form action="{{acl.url}}" method="post">{% csrf_token %}
......
<div class="btn-toolbar">
<button id="sendCtrlAltDelButton" class="btn btn-danger small" href="#">Send CtrlAltDel</button>
<button id="sendPasswordButton" class="btn btn-default small" href="#">Type password</button>
<button id="sendCtrlAltDelButton" class="btn btn-danger small">Send CtrlAltDel</button>
<button id="sendPasswordButton" class="btn btn-default small">Type password</button>
</div>
<div class="alert alert-info" id="noVNC_status">
</div>
......@@ -10,68 +10,6 @@
<script src="{{ STATIC_URL }}dashboard/novnc/util.js"></script>
<script>
"use strict";
var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/';
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
"input.js", "display.js", "jsunzip.js", "rfb.js"]);
var rfb;
function updateState(rfb, state, oldstate, msg) {
var s, sb, cad
s = $('#noVNC_status')[0];
cad = $('#sendCtrlAltDelButton')[0];
if (state === "normal") { cad.disabled = false; }
else { cad.disabled = true; }
if (typeof(msg) !== 'undefined') {
s.innerHTML = msg;
}
}
$('a[data-toggle$="pill"][href!="#console"]').click(function() {
if (rfb) {
rfb.disconnect();
rfb = 0;
}
$("#vm-info-pane").fadeIn();
$("#vm-detail-pane").removeClass("col-md-12");
});
$('#sendCtrlAltDelButton').click(function() {
rfb.sendCtrlAltDel(); return false;});
$('#sendPasswordButton').click(function() {
var pw = '{{instance.pw}}';
for (var i=0; i < pw.length; i++) {
rfb.sendKey(pw.charCodeAt(i));
} return false;});
$("body").on("click", 'a[href$="console"]', function() {
var host, port, password, path;
$("#vm-info-pane").hide();
$("#vm-detail-pane").addClass("col-md-12");
WebUtil.init_logging('warn');
host = window.location.hostname;
if (window.location.port == 8080) {
port = 9999;
} else {
port = window.location.port == "" ? "443" : window.location.port;
}
password = '';
path = 'vnc/?d={{ vnc_url }}';
rfb = new RFB({'target': $D('noVNC_canvas'),
'encrypt': (window.location.protocol === "https:"),
'true_color': true,
'local_cursor': true,
'shared': true,
'view_only': false,
'updateState': updateState});
rfb.connect(host, port, password, path);
});
</script>
var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/';
var VNC_URL = "{{ vnc_url }}";
</script>
......@@ -13,6 +13,7 @@
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
E-mail address or identifier of user:
<input name="name">
<input type="submit">
</form>
......
......@@ -15,7 +15,7 @@ class NotificationTestCase(TestCase):
def test_notification_send(self):
c1 = self.u1.notification_set.count()
c2 = self.u1.notification_set.count()
c2 = self.u2.notification_set.count()
profile = self.u1.profile
msg = profile.notify('subj',
'dashboard/test_message.txt',
......
from django.test import TestCase
from django.test.client import Client
from django.contrib.auth.models import User, Group
from django.core.exceptions import SuspiciousOperation
from vm.models import Instance, InstanceTemplate, Lease
from vm.models import Instance, InstanceTemplate, Lease, Node
from ..models import Profile
from storage.models import Disk
from firewall.models import Vlan
class VmDetailTest(TestCase):
class LoginMixin(object):
def login(self, client, username, password='password'):
response = client.post('/accounts/login/', {'username': username,
'password': password})
self.assertNotEqual(response.status_code, 403)
class VmDetailTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json']
def setUp(self):
......@@ -32,11 +41,6 @@ class VmDetailTest(TestCase):
self.us.delete()
self.g1.delete()
def login(self, client, username, password='password'):
response = client.post('/accounts/login/', {'username': username,
'password': password})
self.assertNotEqual(response.status_code, 403)
def test_404_vm_page(self):
c = Client()
self.login(c, 'user1')
......@@ -242,3 +246,99 @@ class VmDetailTest(TestCase):
response = c.get("/dashboard/notifications/")
self.assertEqual(response.status_code, 200)
assert self.u1.notification_set.get().status == 'read'
class VmDetailVncTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json', 'node.json']
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.save()
def test_permitted_vm_console(self):
c = Client()
self.login(c, 'user1')
inst = Instance.objects.get(pk=1)
inst.node = Node.objects.all()[0]
inst.save()
inst.set_level(self.u1, 'operator')
response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 200)
def test_not_permitted_vm_console(self):
c = Client()
self.login(c, 'user1')
inst = Instance.objects.get(pk=1)
inst.node = Node.objects.all()[0]
inst.save()
inst.set_level(self.u1, 'user')
response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 403)
class TransferOwnershipViewTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json']
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.save()
Profile.objects.create(user=self.u1)
self.u2 = User.objects.create(username='user2', is_staff=True)
self.u2.set_password('password')
self.u2.save()
Profile.objects.create(user=self.u2)
self.us = User.objects.create(username='superuser', is_superuser=True)
self.us.set_password('password')
self.us.save()
Profile.objects.create(user=self.us)
inst = Instance.objects.get(pk=1)
inst.owner = self.u1
inst.save()
def test_non_owner_offer(self):
c2 = self.u2.notification_set.count()
c = Client()
self.login(c, 'user2')
with self.assertRaises(SuspiciousOperation):
c.post('/dashboard/vm/1/tx/')
self.assertEqual(self.u2.notification_set.count(), c2)
def test_owned_offer(self):
c2 = self.u2.notification_set.count()
c = Client()
self.login(c, 'user1')
response = c.get('/dashboard/vm/1/tx/')
assert response.status_code == 200
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
self.assertEqual(self.u2.notification_set.count(), c2 + 1)
def test_transfer(self):
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
c = Client()
self.login(c, 'user2')
response = c.post(url)
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u2.pk)
def test_transfer_token_used_by_others(self):
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
response = c.post(url) # token is for user2
assert response.status_code == 403
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u1.pk)
def test_transfer_by_superuser(self):
c = Client()
self.login(c, 'superuser')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
c = Client()
self.login(c, 'user2')
response = c.post(url)
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u2.pk)
......@@ -9,7 +9,7 @@ from .views import (
FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete,
VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete,
GroupAclUpdateView, GroupUserDelete, NotificationView, NodeGraphView,
VmMigrateView, DiskAddView
VmMigrateView, DiskAddVie, VmDetailVncTokenView,
)
urlpatterns = patterns(
......@@ -37,6 +37,8 @@ urlpatterns = patterns(
name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
name='dashboard.views.detail'),
url(r'^vm/(?P<pk>\d+)/vnctoken/$', VmDetailVncTokenView.as_view(),
name='dashboard.views.detail-vnc'),
url(r'^vm/(?P<pk>\d+)/acl/$', AclUpdateView.as_view(model=Instance),
name='dashboard.views.vm-acl'),
url(r'^vm/(?P<pk>\d+)/tx/$', TransferOwnershipView.as_view(),
......@@ -55,7 +57,7 @@ urlpatterns = patterns(
url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'),
url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(),
name='dashboard.views.node-detail'),
url(r'^tx/$', TransferOwnershipConfirmView.as_view(),
url(r'^tx/(?P<key>.*)/?$', TransferOwnershipConfirmView.as_view(),
name='dashboard.views.vm-transfer-ownership-confirm'),
url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(),
name="dashboard.views.delete-node"),
......
......@@ -37,7 +37,7 @@ from vm.models import (Instance, InstanceTemplate, InterfaceTemplate,
InstanceActivity, Node, instance_activity, Lease,
Interface, NodeActivity)
from firewall.models import Vlan, Host, Rule
from dashboard.models import Favourite
from dashboard.models import Favourite, Profile
logger = logging.getLogger(__name__)
......@@ -148,6 +148,25 @@ class CheckedDetailView(LoginRequiredMixin, DetailView):
return context
class VmDetailVncTokenView(CheckedDetailView):
template_name = "dashboard/vm-detail.html"
model = Instance
def get(self, request, **kwargs):
self.object = self.get_object()
if not self.object.has_level(request.user, 'operator'):
raise PermissionDenied()
if self.object.node:
port = self.object.vnc_port
host = str(self.object.node.host.ipv4)
value = signing.dumps({'host': host,
'port': port},
key=getenv("PROXY_SECRET", 'asdasd')),
return HttpResponse('vnc/?d=%s' % value)
else:
raise Http404()
class VmDetailView(CheckedDetailView):
template_name = "dashboard/vm-detail.html"
model = Instance
......@@ -155,16 +174,11 @@ class VmDetailView(CheckedDetailView):
def get_context_data(self, **kwargs):
context = super(VmDetailView, self).get_context_data(**kwargs)
instance = context['instance']
if instance.node:
port = instance.vnc_port
host = str(instance.node.host.ipv4)
value = signing.dumps({'host': host,
'port': port},
key=getenv("PROXY_SECRET", 'asdasd')),
context.update({
'graphite_enabled': VmGraphView.get_graphite_url() is not None,
'vnc_url': '%s' % value
})
context.update({
'graphite_enabled': VmGraphView.get_graphite_url() is not None,
'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
kwargs={'pk': self.object.pk})
})
# activity data
ia = InstanceActivity.objects.filter(
......@@ -1464,7 +1478,12 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView):
try:
new_owner = User.objects.get(username=request.POST['name'])
except User.DoesNotExist:
raise Http404()
new_owner = User.objects.get(email=request.POST['name'])
except User.DoesNotExist:
new_owner = User.objects.get(profile__org_id=request.POST['name'])
except User.DoesNotExist:
messages.error(request, _('Can not find specified user.'))
return self.get(request, *args, **kwargs)
except KeyError:
raise SuspiciousOperation()
......@@ -1475,29 +1494,41 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView):
token = signing.dumps((obj.pk, new_owner.pk),
salt=TransferOwnershipConfirmView.get_salt())
return HttpResponse("%s?key=%s" % (
reverse('dashboard.views.vm-transfer-ownership-confirm'), token),
content_type="text/plain")
token_path = reverse(
'dashboard.views.vm-transfer-ownership-confirm', args=[token])
try:
new_owner.profile.notify(
_('Ownership offer'),
'dashboard/notifications/ownership-offer.html',
{'instance': obj, 'token': token_path})
except Profile.DoesNotExist:
messages.error(request, _('Can not notify selected user.'))
else:
messages.success(request,
_('User %s is notified about the offer.') % (
unicode(new_owner), ))
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': obj.pk}))
class TransferOwnershipConfirmView(LoginRequiredMixin, View):
"""User can accept an ownership offer."""
max_age = 3 * 24 * 3600
success_message = _("Ownership successfully transferred.")
success_message = _("Ownership successfully transferred to you.")
@classmethod
def get_salt(cls):
return unicode(cls)
def get(self, request, *args, **kwargs):
def get(self, request, key, *args, **kwargs):
"""Confirm ownership transfer based on token.
"""
logger.debug('Confirm dialog for token %s.', key)
try:
key = request.GET['key']
logger.debug('Confirm dialog for token %s.', key)
instance, new_owner = self.get_instance(key, request.user)
except KeyError:
raise Http404()
except PermissionDenied():
except PermissionDenied: