Commit a691294f by Kálmán Viktor

Merge branch 'master' into feature-pipeline

parents 7074ec9e 72e1a7ba
......@@ -64,6 +64,13 @@ CACHES = {
########## END CACHE CONFIGURATION
########## ROSETTA CONFIGURATION
INSTALLED_APPS += (
'rosetta',
)
########## END ROSETTA CONFIGURATION
########## TOOLBAR CONFIGURATION
# https://github.com/django-debug-toolbar/django-debug-toolbar#installation
if get_env_variable('DJANGO_TOOLBAR', 'FALSE') == 'TRUE':
......
......@@ -18,9 +18,11 @@
from django.conf.urls import patterns, include, url
from django.views.generic import TemplateView
from django.conf import settings
from django.contrib import admin
from django.shortcuts import redirect
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
from circle.settings.base import get_env_variable
from dashboard.views import circle_login, HelpView
......@@ -71,6 +73,13 @@ urlpatterns = patterns(
)
if 'rosetta' in settings.INSTALLED_APPS:
urlpatterns += patterns(
'',
url(r'^rosetta/', include('rosetta.urls')),
)
if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
urlpatterns += patterns(
'',
......
......@@ -40,7 +40,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm
from django.forms.widgets import TextInput, HiddenInput
from django.template import Context
from django.template.loader import render_to_string
from django.utils.html import escape
from django.utils.html import escape, format_html
from django.utils.translation import ugettext_lazy as _
from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy
......@@ -935,6 +935,56 @@ class VmDeployForm(OperationForm):
"(blank allows scheduling automatically).")))
class VmPortRemoveForm(OperationForm):
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
self.rule = kwargs.pop('default')
super(VmPortRemoveForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'rule', forms.ModelChoiceField(
queryset=choices, initial=self.rule, required=True,
empty_label=None, label=_('Port')))
if self.rule:
self.fields['rule'].widget = HiddenInput()
class VmPortAddForm(OperationForm):
port = forms.IntegerField(required=True, label=_('Port'),
min_value=1, max_value=65535)
proto = forms.ChoiceField((('tcp', 'tcp'), ('udp', 'udp')),
required=True, label=_('Protocol'))
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
self.host = kwargs.pop('default')
super(VmPortAddForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'host', forms.ModelChoiceField(
queryset=choices, initial=self.host, required=True,
empty_label=None, label=_('Host')))
if self.host:
self.fields['host'].widget = HiddenInput()
@property
def helper(self):
helper = super(VmPortAddForm, self).helper
if self.host:
helper.layout = Layout(
AnyTag(
"div",
HTML(format_html(
_("<label>Host:</label> {0}"), self.host)),
css_class="form-group",
),
Field("host"),
Field("proto"),
Field("port"),
)
return helper
class CircleAuthenticationForm(AuthenticationForm):
# fields: username, password
......
......@@ -50,6 +50,21 @@ $(function () {
return false;
});
$('.tx-tpl-ownership').click(function(e) {
$.ajax({
type: 'GET',
url: $('.tx-tpl-ownership').attr('href'),
success: function(data) {
$('body').append(data);
$('#confirmation-modal').modal('show');
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove();
});
}
});
return false;
});
$('.template-choose').click(function(e) {
$.ajax({
type: 'GET',
......
{% extends "dashboard/operate.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block formfields %}
{% if form %}
{% crispy form %}
{% endif %}
{% if form.fields.rule.initial != None %}
{% with rule=form.fields.rule.initial %}
<dl>
<dt>{% trans "Port" %}:</dt>
<dd>{{ rule.dport }}/{{ rule.proto }}</dd>
<dt>{% trans "Host" %}:</dt>
<dd>{{ rule.host.hostname }}</dd>
<dt>{% trans "Vlan" %}:</dt>
<dd>{{ rule.host.vlan.name }}</dd>
</dl>
{% endwith %}
{% endif %}
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{% trans "Ownership transfer" %}
</h3>
</div>
<div class="panel-body">
{% blocktrans with owner=instance.owner name=instance.name id=instance.id%}
<strong>{{ owner }}</strong> offered to take the ownership of
template <strong>{{name}} ({{id}})</strong>.
Do you accept the responsility of being the template's owner?
{% endblocktrans %}
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
<a class="btn btn-default" href="{% url "dashboard.index" %}">{% trans "No" %}</a>
<input type="hidden" name="key" value="{{ key }}"/>
<button class="btn btn-danger" type="submit">{% trans "Yes" %}</button>
</form>
</div>
</div>
</div>
{% endblock %}
......@@ -10,6 +10,16 @@
<dt>{% trans "Enabled" %}:</dt><dd>{{ node.enabled }}</dd>
<dt>{% trans "Host online" %}:</dt><dd> {{ node.online }}</dd>
<dt>{% trans "Priority" %}:</dt><dd>{{ node.priority }}</dd>
<dt>{% trans "Driver Version:" %}</dt>
<dd>
{% if node.driver_version %}
{{ node.driver_version.branch }} at
{{ node.driver_version.commit }} ({{ node.driver_version.commit_text }})
{% if node.driver_version.is_dirty %}
<span class="label label-danger">{% trans "with uncommitted changes!" %}</span>
{% endif %}
{% endif %}
</dd>
<dt>{% trans "Host owner" %}:</dt>
<dd>
{% include "dashboard/_display-name.html" with user=node.host.owner show_org=True %}
......
......@@ -82,6 +82,26 @@
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="no-margin"><i class="fa fa-user"></i> {% trans "Owner" %}</h4>
</div>
<div class="panel-body">
{% if user == object.owner %}
{% blocktrans %}You are the current owner of this template.{% endblocktrans %}
{% else %}
{% url "dashboard.views.profile" username=object.owner.username as url %}
{% blocktrans with owner=object.owner name=object.owner.get_full_name%}
The current owner of this template is <a href="{{url}}">{{name}} ({{owner}})</a>.
{% endblocktrans %}
{% endif %}
{% if user == object.owner or user.is_superuser %}
<a href="{% url "dashboard.views.template-transfer-ownership" object.pk %}"
class="btn btn-link tx-tpl-ownership">{% trans "Transfer ownership..." %}</a>
{% endif %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="no-margin"><i class="fa fa-group"></i> {% trans "Manage access" %}</h4>
</div>
<div class="panel-body">
......
{% load i18n %}
<div class="pull-right">
<form action="{% url "dashboard.views.template-transfer-ownership" pk=object.pk %}" method="POST" style="max-width: 400px;">
{% csrf_token %}
<label>
{{ form.name.label }}
</label>
<div class="input-group">
{{form.name}}
<div class="input-group-btn">
<input type="submit" value="{% trans "Save" %}" class="btn btn-primary">
</div>
</div>
</form>
</div>
......@@ -123,7 +123,14 @@
<dd style="font-size: 10px; text-align: right; padding-top: 8px;">
<div id="vm-details-pw-reset">
{% with op=op.password_reset %}{% if op %}
<a href="{% if op.disabled %}#{% else %}{{op.get_url}}{% endif %}" class="operation operation-{{op.op}}" data-disabled="{% if op.disabled %}true" title="{% trans "Start the VM to change the password." %}"{% else %}false" {% endif %}>{% trans "Generate new password!" %}</a>
<a href="{% if op.disabled %}#{% else %}{{op.get_url}}{% endif %}"
class="operation operation-{{op.op}}"
{% if op.disabled %}
data-disabled="true"
title="{% if instance.has_agent %}{% trans "Start the VM to change the password." %}{% else %}{% trans "This machine has no agent installed." %}{% endif %}"
{% endif %}>
{% trans "Generate new password!" %}
</a>
{% endif %}{% endwith %}
</div>
</dd>
......
{% load i18n %}
<div class="vm-details-network-port-add pull-right">
<form action="" method="POST">
<form action="{{ op.add_port.get_url }}" method="POST">
{% csrf_token %}
<input type="hidden" name="host_pk" value="{{ i.host.pk }}"/>
<input type="hidden" name="host" value="{{ i.host.pk }}"/>
<div class="input-group input-group-sm">
<span class="input-group-addon">
<i class="fa fa-plus"></i> <i class="fa fa-long-arrow-right"></i>
</span>
<input type="text" class="form-control" size="5" style="width: 80px;" name="port"/>
<input type="number" class="form-control" size="5" min="1" max="65535"
style="width: 80px;" name="port" required/>
<span class="input-group-addon">/</span>
<select class="form-control" name="proto" style="width: 70px;"><option>tcp</option><option>udp</option></select>
<div class="input-group-btn">
......
......@@ -4,8 +4,9 @@
{% if user == instance.owner %}
{% blocktrans %}You are the current owner of this instance.{% endblocktrans %}
{% else %}
{% blocktrans with owner=instance.owner %}
The current owner of this instance is {{owner}}.
{% url "dashboard.views.profile" username=instance.owner.username as url %}
{% blocktrans with owner=instance.owner name=instance.owner.get_full_name%}
The current owner of this instance is <a href="{{url}}">{{name}} ({{owner}})</a>.
{% endblocktrans %}
{% endif %}
{% if user == instance.owner or user.is_superuser %}
......
......@@ -78,7 +78,7 @@
{{ l.private }}/{{ l.proto }}
</td>
<td>
<a href="{% url "dashboard.views.remove-port" pk=instance.pk rule=l.ipv4.pk %}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv4.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a>
<a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv4.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a>
</td>
</tr>
{% endif %}
......@@ -110,7 +110,7 @@
{{ l.private }}/{{ l.proto }}
</td>
<td>
<a href="{% url "dashboard.views.remove-port" pk=instance.pk rule=l.ipv4.pk %}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv6.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a>
<a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv6.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a>
</td>
</tr>
{% endif %}
......
......@@ -26,7 +26,8 @@ from django.contrib.auth import authenticate
from dashboard.views import VmAddInterfaceView
from vm.models import Instance, InstanceTemplate, Lease, Node, Trait
from vm.operations import WakeUpOperation, AddInterfaceOperation
from vm.operations import (WakeUpOperation, AddInterfaceOperation,
AddPortOperation)
from ..models import Profile
from firewall.models import Vlan, Host, VlanGroup
from mock import Mock, patch
......@@ -335,51 +336,48 @@ class VmDetailTest(LoginMixin, TestCase):
self.login(c, "user2")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
response = c.post("/dashboard/vm/1/", {'port': True,
'proto': 'tcp',
'port': '1337'})
vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u2, 'user')
inst.add_interface(user=self.u2, vlan=vlan)
host = Host.objects.get(
interface__in=inst.interface_set.all())
with patch.object(AddPortOperation, 'async') as mock_method:
mock_method.side_effect = inst.add_port
response = c.post("/dashboard/vm/1/op/add_port/", {
'proto': 'tcp', 'host': host.pk, 'port': '1337'})
self.assertEqual(response.status_code, 403)
def test_unpermitted_add_port_wo_obj_levels(self):
c = Client()
self.login(c, "user2")
self.u2.user_permissions.add(Permission.objects.get(
name='Can configure port forwards.'))
response = c.post("/dashboard/vm/1/", {'port': True,
'proto': 'tcp',
'port': '1337'})
self.assertEqual(response.status_code, 403)
def test_unpermitted_add_port_w_bad_host(self):
c = Client()
self.login(c, "user2")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u2, 'user')
inst.add_interface(user=self.u2, vlan=vlan, system=True)
host = Host.objects.get(
interface__in=inst.interface_set.all())
self.u2.user_permissions.add(Permission.objects.get(
name='Can configure port forwards.'))
response = c.post("/dashboard/vm/1/", {'proto': 'tcp',
'host_pk': '9999',
'port': '1337'})
with patch.object(AddPortOperation, 'async') as mock_method:
mock_method.side_effect = inst.add_port
response = c.post("/dashboard/vm/1/op/add_port/", {
'proto': 'tcp', 'host': host.pk, 'port': '1337'})
assert not mock_method.called
self.assertEqual(response.status_code, 403)
def test_permitted_add_port_w_unhandled_exception(self):
def test_unpermitted_add_port_w_bad_host(self):
c = Client()
self.login(c, "user2")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u2, 'user')
inst.add_interface(user=self.u2, vlan=vlan)
host = Host.objects.get(
interface__in=inst.interface_set.all())
self.u2.user_permissions.add(Permission.objects.get(
name='Can configure port forwards.'))
port_count = len(host.list_ports())
response = c.post("/dashboard/vm/1/", {'proto': 'tcp',
'host_pk': host.pk,
'port': 'invalid_port'})
self.assertEqual(response.status_code, 302)
self.assertEqual(len(host.list_ports()), port_count)
with patch.object(AddPortOperation, 'async') as mock_method:
mock_method.side_effect = inst.add_port
response = c.post("/dashboard/vm/1/op/add_port/", {
'proto': 'tcp', 'host': '9999', 'port': '1337'})
assert not mock_method.called
self.assertEqual(response.status_code, 200)
def test_permitted_add_port(self):
c = Client()
......@@ -394,9 +392,11 @@ class VmDetailTest(LoginMixin, TestCase):
self.u2.user_permissions.add(Permission.objects.get(
name='Can configure port forwards.'))
port_count = len(host.list_ports())
response = c.post("/dashboard/vm/1/", {'proto': 'tcp',
'host_pk': host.pk,
'port': '1337'})
with patch.object(AddPortOperation, 'async') as mock_method:
mock_method.side_effect = inst.add_port
response = c.post("/dashboard/vm/1/op/add_port/", {
'proto': 'tcp', 'host': host.pk, 'port': '1337'})
assert mock_method.called
self.assertEqual(response.status_code, 302)
self.assertEqual(len(host.list_ports()), port_count + 1)
......
......@@ -26,9 +26,9 @@ from .views import (
InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail,
MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeList, NodeStatus,
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDetailView,
NotificationView, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList,
vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmList,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveUserView,
......@@ -48,6 +48,8 @@ from .views import (
toggle_template_tutorial,
ClientCheck, TokenLogin,
VmGraphView, NodeGraphView, NodeListGraphView,
TransferInstanceOwnershipView, TransferInstanceOwnershipConfirmView,
TransferTemplateOwnershipView, TransferTemplateOwnershipConfirmView,
)
from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops
......@@ -78,15 +80,15 @@ urlpatterns = patterns(
name="dashboard.views.template-list"),
url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(),
name="dashboard.views.template-delete"),
url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(),
name='dashboard.views.remove-port'),
url(r'^template/(?P<pk>\d+)/tx/$', TransferTemplateOwnershipView.as_view(),
name='dashboard.views.template-transfer-ownership'),
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(),
url(r'^vm/(?P<pk>\d+)/tx/$', TransferInstanceOwnershipView.as_view(),
name='dashboard.views.vm-transfer-ownership'),
url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'),
url(r'^vm/create/$', VmCreate.as_view(),
......@@ -108,8 +110,12 @@ urlpatterns = patterns(
name='dashboard.views.node-detail'),
url(r'^node/(?P<pk>\d+)/add-trait/$', NodeAddTraitView.as_view(),
name='dashboard.views.node-addtrait'),
url(r'^tx/(?P<key>.*)/?$', TransferOwnershipConfirmView.as_view(),
url(r'^vm/tx/(?P<key>.*)/?$',
TransferInstanceOwnershipConfirmView.as_view(),
name='dashboard.views.vm-transfer-ownership-confirm'),
url(r'^template/tx/(?P<key>.*)/?$',
TransferTemplateOwnershipConfirmView.as_view(),
name='dashboard.views.template-transfer-ownership-confirm'),
url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(),
name="dashboard.views.delete-node"),
url(r'^node/status/(?P<pk>\d+)/$', NodeStatus.as_view(),
......
......@@ -26,7 +26,7 @@ from django.core.urlresolvers import reverse, reverse_lazy
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, get_object_or_404
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext as _, ugettext_noop
from django.views.generic import (
TemplateView, CreateView, DeleteView, UpdateView,
)
......@@ -44,7 +44,10 @@ from ..forms import (
)
from ..tables import TemplateListTable, LeaseListTable
from .util import AclUpdateView, FilterMixin
from .util import (
AclUpdateView, FilterMixin,
TransferOwnershipConfirmView, TransferOwnershipView,
)
logger = logging.getLogger(__name__)
......@@ -488,3 +491,20 @@ class LeaseDelete(LoginRequiredMixin, DeleteView):
else:
messages.success(request, success_message)
return HttpResponseRedirect(success_url)
class TransferTemplateOwnershipConfirmView(TransferOwnershipConfirmView):
template = "dashboard/confirm/transfer-template-ownership.html"
model = InstanceTemplate
class TransferTemplateOwnershipView(TransferOwnershipView):
confirm_view = TransferTemplateOwnershipConfirmView
model = InstanceTemplate
notification_msg = ugettext_noop(
'%(user)s offered you to take the ownership of '
'his/her template called %(instance)s. '
'<a href="%(token)s" '
'class="btn btn-success btn-small">Accept</a>')
token_url = 'dashboard.views.template-transfer-ownership-confirm'
template = "dashboard/template-tx-owner.html"
......@@ -24,14 +24,15 @@ from urlparse import urljoin
from django.conf import settings
from django.contrib.auth.models import User, Group
from django.core.exceptions import PermissionDenied
from django.core import signing
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import reverse
from django.contrib import messages
from django.contrib.auth.views import redirect_to_login
from django.db.models import Q
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.shortcuts import redirect, render
from django.utils.translation import ugettext_lazy as _, ugettext_noop
from django.views.generic import DetailView, View
from django.views.generic.detail import SingleObjectMixin
......@@ -40,7 +41,8 @@ from braces.views._access import AccessMixin
from celery.exceptions import TimeoutError
from common.models import HumanReadableException, HumanReadableObject
from ..models import GroupProfile
from ..models import GroupProfile, Profile
from ..forms import TransferOwnershipForm
logger = logging.getLogger(__name__)
saml_available = hasattr(settings, "SAML_CONFIG")
......@@ -563,3 +565,132 @@ class GraphMixin(object):
def absolute_url(url):
return urljoin(settings.DJANGO_URL, url)
class TransferOwnershipView(CheckedDetailView, DetailView):
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/_modal.html']
else:
return ['dashboard/nojs-wrapper.html']
def get_context_data(self, *args, **kwargs):
context = super(TransferOwnershipView, self).get_context_data(
*args, **kwargs)
context['form'] = TransferOwnershipForm()
context.update({
'box_title': _("Transfer ownership"),
'ajax_title': True,
'template': self.template,
})
return context
def post(self, request, *args, **kwargs):
form = TransferOwnershipForm(request.POST)
if not form.is_valid():
return self.get(request)
try:
new_owner = search_user(request.POST['name'])
except User.DoesNotExist:
messages.error(request, _('Can not find specified user.'))
return self.get(request, *args, **kwargs)
except KeyError:
raise SuspiciousOperation()
obj = self.get_object()
if not (obj.owner == request.user or
request.user.is_superuser):
raise PermissionDenied()
token = signing.dumps(
(obj.pk, new_owner.pk),
salt=self.confirm_view.get_salt())
token_path = reverse(self.token_url, args=[token])
try:
new_owner.profile.notify(
ugettext_noop('Ownership offer'),
self.notification_msg,
{'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(obj.get_absolute_url())
class TransferOwnershipConfirmView(LoginRequiredMixin, View):
"""User can accept an ownership offer."""
max_age = 3 * 24 * 3600
success_message = _("Ownership successfully transferred to you.")
@classmethod
def get_salt(cls):
return unicode(cls) + unicode(cls.model)
def get(self, request, key, *args, **kwargs):
"""Confirm ownership transfer based on token.
"""
logger.debug('Confirm dialog for token %s.', key)
try:
instance, new_owner = self.get_instance(key, request.user)
except PermissionDenied:
messages.error(request, _('This token is for an other user.'))
raise
except SuspiciousOperation:
messages.error(request, _('This token is invalid or has expired.'))
raise PermissionDenied()
return render(request, self.template,
dictionary={'instance': instance, 'key': key})
def change_owner(self, instance, new_owner):
instance.owner = new_owner
instance.clean()
instance.save()
def post(self, request, key, *args, **kwargs):
"""Really transfer ownership based on token.
"""
instance, owner = self.get_instance(key, request.user)
old = instance.owner
self.change_owner(instance, request.user)
messages.success(request, self.success_message)
logger.info('Ownership of %s transferred from %s to %s.',
unicode(instance), unicode(old), unicode(request.user))
if old.profile:
old.profile.notify(
ugettext_noop('Ownership accepted'),
ugettext_noop('Your ownership offer of %(instance)s has been '
'accepted by %(user)s.'),
{'instance': instance})
return redirect(instance.get_absolute_url())
def get_instance(self, key, user):
"""Get object based on signed token.
"""
try:
instance, new_owner = (
signing.loads(key, max_age=self.max_age,
salt=self.get_salt()))
except (signing.BadSignature, ValueError, TypeError) as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e))
raise SuspiciousOperation()
try:
instance = self.model.objects.get(id=instance)
except self.model.DoesNotExist as e:
logger.error('Tried token to nonexistent instance %d. '
'Token: %s, user: %s. %s',
instance, key, unicode(user), unicode(e))
raise Http404()
if new_owner != user.pk:
logger.error('%s (%d) tried the token for %s. Token: %s.',
unicode(user), user.pk, new_owner, key)
raise PermissionDenied()
return (instance, new_owner)
......@@ -29,7 +29,7 @@ from django.core import signing
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import reverse, reverse_lazy
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.shortcuts import redirect, get_object_or_404, render
from django.shortcuts import redirect, get_object_or_404
from django.template import RequestContext
from django.template.loader import render_to_string
from django.utils.translation import (
......@@ -37,7 +37,7 @@ from django.utils.translation import (
)
from django.views.decorators.http import require_GET
from django.views.generic import (
UpdateView, ListView, TemplateView, DeleteView, DetailView, View,
UpdateView, ListView, TemplateView, DeleteView
)
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
......@@ -54,16 +54,18 @@ from vm.models import (
)
from .util import (
CheckedDetailView, AjaxOperationMixin, OperationView, AclUpdateView,
FormOperationMixin, FilterMixin, search_user, GraphMixin,
FormOperationMixin, FilterMixin, GraphMixin,
TransferOwnershipConfirmView, TransferOwnershipView,
)
from ..forms import (
AclUserOrGroupAddForm, VmResourcesForm, TraitsForm, RawDataForm,
VmAddInterfaceForm, VmCreateDiskForm, VmDownloadDiskForm, VmSaveForm,
VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm,
TransferOwnershipForm, VmDiskResizeForm, RedeployForm, VmDiskRemoveForm,
VmDiskResizeForm, RedeployForm, VmDiskRemoveForm,
VmMigrateForm, VmDeployForm,
VmPortRemoveForm, VmPortAddForm,
)
from ..models import Favourite, Profile
from ..models import Favourite
logger = logging.getLogger(__name__)
......@@ -174,7 +176,6 @@ class VmDetailView(GraphMixin, CheckedDetailView):
'new_description': self.__set_description,
'new_tag': self.__add_tag,
'to_remove': self.__remove_tag,
'port': self.__add_port,
'abort_operation': self.__abort_operation,
}
for k, v in options.iteritems():
......@@ -270,40 +271,6 @@ class VmDetailView(GraphMixin, CheckedDetailView):
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.object.pk}))
def __add_port(self, request):
object = self.get_object()
if not (object.has_level(request.user, "operator") and
request.user.has_perm('vm.config_ports')):
raise PermissionDenied()
port = request.POST.get("port")
proto = request.POST.get("proto")
try:
error = None
interfaces = object.interface_set.all()
host = Host.objects.get(pk=request.POST.get("host_pk"),
interface__in=interfaces)
host.add_port(proto, private=port)
except Host.DoesNotExist:
logger.error('Tried to add port to nonexistent host %d. User: %s. '
'Instance: %s', request.POST.get("host_pk"),
unicode(request.user), object)
raise PermissionDenied()
except ValueError:
error = _("There is a problem with your input.")
except Exception as e:
error = _("Unknown error.")
logger.error(e)
if request.is_ajax():
pass
else:
if error:
messages.error(request, error)
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.get_object().pk}))
def __abort_operation(self, request):
self.object = self.get_object()
......@@ -450,6 +417,62 @@ class VmMigrateView(FormOperationMixin, VmOperationView):
return val
class VmPortRemoveView(FormOperationMixin, VmOperationView):
template_name = 'dashboard/_vm-remove-port.html'
op = 'remove_port'
show_in_toolbar = False
with_reload = True
wait_for_result = 0.5
icon = 'times'
effect = "danger"
form_class = VmPortRemoveForm
def get_form_kwargs(self):
instance = self.get_op().instance
choices = Rule.portforwards().filter(
host__interface__instance=instance)
rule_pk = self.request.GET.get('rule')
if rule_pk:
try:
default = choices.get(pk=rule_pk)
except (ValueError, Rule.DoesNotExist):
raise Http404()
else:
default = None
val = super(VmPortRemoveView, self).get_form_kwargs()
val.update({'choices': choices, 'default': default})
return val
class VmPortAddView(FormOperationMixin, VmOperationView):
op = 'add_port'
show_in_toolbar = False
with_reload = True
wait_for_result = 0.5
icon = 'plus'
effect = "success"
form_class = VmPortAddForm
def get_form_kwargs(self):
instance = self.get_op().instance
choices = Host.objects.filter(interface__instance=instance)
host_pk = self.request.GET.get('host')
if host_pk:
try:
default = choices.get(pk=host_pk)
except (ValueError, Host.DoesNotExist):
raise Http404()
else:
default = None
val = super(VmPortAddView, self).get_form_kwargs()
val.update({'choices': choices, 'default': default})
return val
class VmSaveView(FormOperationMixin, VmOperationView):
op = 'save_as_template'
......@@ -683,6 +706,8 @@ vm_ops = OrderedDict([
op='remove_disk', form_class=VmDiskRemoveForm,
icon='times', effect="danger")),
('add_interface', VmAddInterfaceView),
('remove_port', VmPortRemoveView),
('add_port', VmPortAddView),
('renew', VmRenewView),
('resources_change', VmResourcesChangeView),
('password_reset', VmOperationView.factory(
......@@ -1166,52 +1191,6 @@ def get_disk_download_status(request, pk):
)
class PortDelete(LoginRequiredMixin, DeleteView):
model = Rule
pk_url_kwarg = 'rule'
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def get_context_data(self, **kwargs):
context = super(PortDelete, self).get_context_data(**kwargs)
rule = kwargs.get('object')
instance = rule.host.interface_set.get().instance
context['title'] = _("Port delete confirmation")
context['text'] = _("Are you sure you want to close %(port)d/"
"%(proto)s on %(vm)s?" % {'port': rule.dport,
'proto': rule.proto,
'vm': instance})
return context
def delete(self, request, *args, **kwargs):
rule = Rule.objects.get(pk=kwargs.get("rule"))
instance = rule.host.interface_set.get().instance
if not instance.has_level(request.user, 'owner'):
raise PermissionDenied()
super(PortDelete, self).delete(request, *args, **kwargs)
success_url = self.get_success_url()
success_message = _("Port successfully removed.")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect("%s#network" % success_url)
def get_success_url(self):
return reverse_lazy('dashboard.views.detail',
kwargs={'pk': self.kwargs.get("pk")})
class ClientCheck(LoginRequiredMixin, TemplateView):
def get_template_names(self):
......@@ -1306,139 +1285,29 @@ class FavouriteView(TemplateView):
return HttpResponse("Added.")
class TransferOwnershipView(CheckedDetailView, DetailView):
class TransferInstanceOwnershipConfirmView(TransferOwnershipConfirmView):
template = "dashboard/confirm/transfer-instance-ownership.html"
model = Instance
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/_modal.html']
else:
return ['dashboard/nojs-wrapper.html']
def get_context_data(self, *args, **kwargs):
context = super(TransferOwnershipView, self).get_context_data(
*args, **kwargs)
context['form'] = TransferOwnershipForm()
context.update({
'box_title': _("Transfer ownership"),
'ajax_title': True,
'template': "dashboard/vm-detail/tx-owner.html",
})
return context
def change_owner(self, instance, new_owner):
with instance.activity(
code_suffix='ownership-transferred',
readable_name=ugettext_noop("transfer ownership"),
concurrency_check=False, user=new_owner):
super(TransferInstanceOwnershipConfirmView, self).change_owner(
instance, new_owner)
def post(self, request, *args, **kwargs):
form = TransferOwnershipForm(request.POST)
if not form.is_valid():
return self.get(request)
try:
new_owner = search_user(request.POST['name'])
except User.DoesNotExist:
messages.error(request, _('Can not find specified user.'))
return self.get(request, *args, **kwargs)
except KeyError:
raise SuspiciousOperation()
obj = self.get_object()
if not (obj.owner == request.user or
request.user.is_superuser):
raise PermissionDenied()
token = signing.dumps((obj.pk, new_owner.pk),
salt=TransferOwnershipConfirmView.get_salt())
token_path = reverse(
'dashboard.views.vm-transfer-ownership-confirm', args=[token])
try:
new_owner.profile.notify(
ugettext_noop('Ownership offer'),
ugettext_noop('%(user)s offered you to take the ownership of '
class TransferInstanceOwnershipView(TransferOwnershipView):
confirm_view = TransferInstanceOwnershipConfirmView
model = Instance
notification_msg = ugettext_noop(
'%(user)s offered you to take the ownership of '
'his/her virtual machine called %(instance)s. '
'<a href="%(token)s" '
'class="btn btn-success btn-small">Accept</a>'),
{'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 to you.")
@classmethod
def get_salt(cls):
return unicode(cls)
def get(self, request, key, *args, **kwargs):
"""Confirm ownership transfer based on token.
"""
logger.debug('Confirm dialog for token %s.', key)
try:
instance, new_owner = self.get_instance(key, request.user)
except PermissionDenied:
messages.error(request, _('This token is for an other user.'))
raise
except SuspiciousOperation:
messages.error(request, _('This token is invalid or has expired.'))
raise PermissionDenied()
return render(request,
"dashboard/confirm/base-transfer-ownership.html",
dictionary={'instance': instance, 'key': key})
def post(self, request, key, *args, **kwargs):
"""Really transfer ownership based on token.
"""
instance, owner = self.get_instance(key, request.user)
old = instance.owner
with instance.activity(code_suffix='ownership-transferred',
concurrency_check=False, user=request.user):
instance.owner = request.user
instance.clean()
instance.save()
messages.success(request, self.success_message)
logger.info('Ownership of %s transferred from %s to %s.',
unicode(instance), unicode(old), unicode(request.user))
if old.profile:
old.profile.notify(
ugettext_noop('Ownership accepted'),
ugettext_noop('Your ownership offer of %(instance)s has been '
'accepted by %(user)s.'),
{'instance': instance})
return redirect(instance.get_absolute_url())
def get_instance(self, key, user):
"""Get object based on signed token.
"""
try:
instance, new_owner = (
signing.loads(key, max_age=self.max_age,
salt=self.get_salt()))
except (signing.BadSignature, ValueError, TypeError) as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e))
raise SuspiciousOperation()
try:
instance = Instance.objects.get(id=instance)
except Instance.DoesNotExist as e:
logger.error('Tried token to nonexistent instance %d. '
'Token: %s, user: %s. %s',
instance, key, unicode(user), unicode(e))
raise Http404()
if new_owner != user.pk:
logger.error('%s (%d) tried the token for %s. Token: %s.',
unicode(user), user.pk, new_owner, key)
raise PermissionDenied()
return (instance, new_owner)
'class="btn btn-success btn-small">Accept</a>')
token_url = 'dashboard.views.vm-transfer-ownership-confirm'
template = "dashboard/vm-detail/tx-owner.html"
@login_required
......
......@@ -34,6 +34,10 @@ reverse_domain_re = re.compile(r'^(%\([abcd]\)d|[a-z0-9.-])+$')
ipv6_template_re = re.compile(r'^(%\([abcd]\)[dxX]|[A-Za-z0-9:-])+$')
class mac_custom(mac_unix):
word_fmt = '%.2X'
class MACAddressFormField(forms.Field):
default_error_messages = {
'invalid': _(u'Enter a valid MAC address. %s'),
......@@ -51,9 +55,6 @@ class MACAddressField(models.Field):
description = _('MAC Address object')
__metaclass__ = models.SubfieldBase
class mac_custom(mac_unix):
word_fmt = '%.2X'
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 17
super(MACAddressField, self).__init__(*args, **kwargs)
......@@ -65,7 +66,7 @@ class MACAddressField(models.Field):
if isinstance(value, EUI):
return value
return EUI(value, dialect=MACAddressField.mac_custom)
return EUI(value, dialect=mac_custom)
def get_internal_type(self):
return 'CharField'
......
......@@ -243,6 +243,13 @@ class Rule(models.Model):
return retval
@classmethod
def portforwards(cls, host=None):
qs = cls.objects.filter(dport__isnull=False, direction='in')
if host is not None:
qs = qs.filter(host=host)
return qs
class Meta:
verbose_name = _("rule")
verbose_name_plural = _("rules")
......@@ -762,7 +769,7 @@ class Host(models.Model):
Return a list of ports with forwarding rules set.
"""
retval = []
for rule in self.rules.filter(dport__isnull=False, direction='in'):
for rule in Rule.portforwards(host=self):
forward = {
'proto': rule.proto,
'private': rule.dport,
......
......@@ -817,7 +817,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
return acts
def get_merged_activities(self, user=None):
whitelist = ("create_disk", "download_disk")
whitelist = ("create_disk", "download_disk", "add_port", "remove_port")
acts = self.get_activities(user)
merged_acts = []
latest = None
......
......@@ -290,6 +290,11 @@ class Node(OperatedMixin, TimeStampedModel):
@property
@node_available
def driver_version(self):
return self.info.get('driver_version')
@property
@node_available
def cpu_usage(self):
return self.monitor_info.get('cpu.percent') / 100
......
......@@ -27,7 +27,7 @@ from tarfile import TarFile, TarInfo
import time
from urlparse import urlsplit
from django.core.exceptions import PermissionDenied
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext_noop
from django.conf import settings
......@@ -606,6 +606,41 @@ class RemoveInterfaceOperation(InstanceOperation):
@register_operation
class RemovePortOperation(InstanceOperation):
id = 'remove_port'
name = _("close port")
description = _("Close the specified port.")
concurrency_check = False
required_perms = ('vm.config_ports', )
def _operation(self, activity, rule):
interface = rule.host.interface_set.get()
if interface.instance != self.instance:
raise SuspiciousOperation()
activity.readable_name = create_readable(
ugettext_noop("close %(proto)s/%(port)d on %(host)s"),
proto=rule.proto, port=rule.dport, host=rule.host)
rule.delete()
@register_operation
class AddPortOperation(InstanceOperation):
id = 'add_port'
name = _("open port")
description = _("Open the specified port.")
concurrency_check = False
required_perms = ('vm.config_ports', )
def _operation(self, activity, host, proto, port):
if host.interface_set.get().instance != self.instance:
raise SuspiciousOperation()
host.add_port(proto, private=port)
activity.readable_name = create_readable(
ugettext_noop("open %(proto)s/%(port)d on %(host)s"),
proto=proto, port=port, host=host)
@register_operation
class RemoveDiskOperation(InstanceOperation):
id = 'remove_disk'
name = _("remove disk")
......@@ -1100,6 +1135,7 @@ class ActivateOperation(NodeOperation):
def _operation(self):
self.node.enabled = True
self.node.schedule_enabled = True
self.node.get_info(invalidate_cache=True)
self.node.save()
......@@ -1121,6 +1157,7 @@ class PassivateOperation(NodeOperation):
def _operation(self):
self.node.enabled = True
self.node.schedule_enabled = False
self.node.get_info(invalidate_cache=True)
self.node.save()
......@@ -1179,14 +1216,28 @@ class RecoverOperation(InstanceOperation):
except Instance.InstanceDestroyedError:
pass
def _operation(self):
def _operation(self, user, activity):
with activity.sub_activity(
'recover_instance',
readable_name=ugettext_noop("recover instance")):
self.instance.destroyed_at = None
for disk in self.instance.disks.all():
disk.destroyed = None
disk.restore()
disk.save()
self.instance.destroyed_at = None
self.instance.status = 'PENDING'
self.instance.save()
try:
self.instance.renew(parent_activity=activity)
except:
pass
if self.instance.template:
for net in self.instance.template.interface_set.all():
self.instance.add_interface(
parent_activity=activity, user=user, vlan=net.vlan)
@register_operation
class ResourcesOperation(InstanceOperation):
......
......@@ -2,4 +2,5 @@
-r base.txt
coverage==3.7.1
django-debug-toolbar==1.1
django-rosetta==0.7.4
Sphinx==1.2.2
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment