Commit a46d3f9c by Kálmán Viktor

Merge branch 'master' into feature-template-wizard

Conflicts:
	circle/dashboard/templates/dashboard/vm-detail.html
	circle/dashboard/urls.py
	circle/dashboard/views.py
parents e139bff7 46104fe6
...@@ -27,7 +27,11 @@ coverage.xml ...@@ -27,7 +27,11 @@ coverage.xml
*.mo *.mo
# saml # saml
circle/attribute-maps/ circle/attribute-maps
circle/remote_metadata.xml circle/remote_metadata.xml
circle/samlcert.key circle/*.key
circle/samlcert.pem circle/*.pem
# collected static files:
circle/static
circle/static_collected
...@@ -5,6 +5,7 @@ from os.path import abspath, basename, dirname, join, normpath, isfile ...@@ -5,6 +5,7 @@ from os.path import abspath, basename, dirname, join, normpath, isfile
from sys import path from sys import path
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import ugettext_lazy as _
from json import loads from json import loads
...@@ -93,7 +94,13 @@ except: ...@@ -93,7 +94,13 @@ except:
TIME_ZONE = get_env_variable('DJANGO_TIME_ZONE', default=systz) TIME_ZONE = get_env_variable('DJANGO_TIME_ZONE', default=systz)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code # See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = get_env_variable("DJANGO_LANGUAGE_CODE", "en")
# https://docs.djangoproject.com/en/dev/ref/settings/#languages
LANGUAGES = (
('en', _('English')),
('hu', _('Hungarian')),
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id # See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id
SITE_ID = 1 SITE_ID = 1
...@@ -125,11 +132,6 @@ STATIC_ROOT = normpath(join(SITE_ROOT, 'static_collected')) ...@@ -125,11 +132,6 @@ STATIC_ROOT = normpath(join(SITE_ROOT, 'static_collected'))
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
STATIC_URL = get_env_variable('DJANGO_STATIC_URL', default='/static/') STATIC_URL = get_env_variable('DJANGO_STATIC_URL', default='/static/')
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
STATICFILES_DIRS = (
normpath(join(SITE_ROOT, 'static')),
)
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
......
...@@ -13,6 +13,7 @@ class Operation(object): ...@@ -13,6 +13,7 @@ class Operation(object):
""" """
async_queue = 'localhost.man' async_queue = 'localhost.man'
required_perms = () required_perms = ()
do_not_call_in_templates = True
def __call__(self, **kwargs): def __call__(self, **kwargs):
return self.call(**kwargs) return self.call(**kwargs)
...@@ -28,11 +29,11 @@ class Operation(object): ...@@ -28,11 +29,11 @@ class Operation(object):
def __prelude(self, kwargs): def __prelude(self, kwargs):
"""This method contains the shared prelude of call and async. """This method contains the shared prelude of call and async.
""" """
skip_checks = kwargs.setdefault('system', False) skip_auth_check = kwargs.setdefault('system', False)
user = kwargs.setdefault('user', None) user = kwargs.setdefault('user', None)
parent_activity = kwargs.pop('parent_activity', None) parent_activity = kwargs.pop('parent_activity', None)
if not skip_checks: if not skip_auth_check:
self.check_auth(user) self.check_auth(user)
self.check_precond() self.check_precond()
return self.create_activity(parent=parent_activity, user=user) return self.create_activity(parent=parent_activity, user=user)
...@@ -42,8 +43,7 @@ class Operation(object): ...@@ -42,8 +43,7 @@ class Operation(object):
""" """
with activity_context(activity, on_abort=self.on_abort, with activity_context(activity, on_abort=self.on_abort,
on_commit=self.on_commit): on_commit=self.on_commit):
return self._operation(activity=activity, user=user, return self._operation(activity=activity, user=user, **kwargs)
**kwargs)
def _operation(self, activity, user, system, **kwargs): def _operation(self, activity, user, system, **kwargs):
"""This method is the operation's particular implementation. """This method is the operation's particular implementation.
...@@ -128,15 +128,46 @@ class OperatedMixin(object): ...@@ -128,15 +128,46 @@ class OperatedMixin(object):
raise AttributeError("%r object has no attribute %r" % raise AttributeError("%r object has no attribute %r" %
(self.__class__.__name__, name)) (self.__class__.__name__, name))
def get_available_operations(self, user):
def register_operation(target_cls, op_cls, op_id=None): """Yield Operations that match permissions of user and preconditions.
"""
for name in getattr(self, operation_registry_name, {}):
try:
op = getattr(self, name)
op.check_auth(user)
op.check_precond()
except:
pass # unavailable
else:
yield op
def register_operation(op_cls, op_id=None, target_cls=None):
"""Register the specified operation with the target class. """Register the specified operation with the target class.
You can optionally specify an ID to be used for the registration; You can optionally specify an ID to be used for the registration;
otherwise, the operation class' 'id' attribute will be used. otherwise, the operation class' 'id' attribute will be used.
""" """
if op_id is None: if op_id is None:
op_id = op_cls.id try:
op_id = op_cls.id
except AttributeError:
raise NotImplementedError("Operations should specify an 'id' "
"attribute designating the name the "
"operation can be called by on its "
"host. Alternatively, provide the name "
"in the 'op_id' parameter to this call.")
if target_cls is None:
try:
target_cls = op_cls.host_cls
except AttributeError:
raise NotImplementedError("Operations should specify a 'host_cls' "
"attribute designating the host class "
"the operation should be registered to. "
"Alternatively, provide the host class "
"in the 'target_cls' parameter to this "
"call.")
if not issubclass(target_cls, OperatedMixin): if not issubclass(target_cls, OperatedMixin):
raise TypeError("%r is not a subclass of %r" % raise TypeError("%r is not a subclass of %r" %
......
# -*- coding: utf-8 -*-
from django import contrib
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from dashboard.models import Profile
class ProfileInline(contrib.admin.TabularInline):
model = Profile
UserAdmin.inlines = (ProfileInline, )
contrib.admin.site.unregister(User)
contrib.admin.site.register(User, UserAdmin)
from __future__ import absolute_import
from datetime import timedelta from datetime import timedelta
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.forms import ( from django.contrib.auth.forms import (
AuthenticationForm, PasswordResetForm, SetPasswordForm, AuthenticationForm, PasswordResetForm, SetPasswordForm,
PasswordChangeForm,
) )
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
...@@ -1075,9 +1078,20 @@ class MyProfileForm(forms.ModelForm): ...@@ -1075,9 +1078,20 @@ class MyProfileForm(forms.ModelForm):
def helper(self): def helper(self):
helper = FormHelper() helper = FormHelper()
helper.layout = Layout('preferred_language', ) helper.layout = Layout('preferred_language', )
helper.add_input(Submit("submit", _("Save"))) helper.add_input(Submit("submit", _("Change language")))
return helper return helper
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
value = super(MyProfileForm, self).save(*args, **kwargs) value = super(MyProfileForm, self).save(*args, **kwargs)
return value return value
class CirclePasswordChangeForm(PasswordChangeForm):
@property
def helper(self):
helper = FormHelper()
helper.add_input(Submit("submit", _("Change password"),
css_class="btn btn-primary",
css_id="submit-password-button"))
return helper
from __future__ import absolute_import
from itertools import chain from itertools import chain
from logging import getLogger from logging import getLogger
......
...@@ -2,22 +2,21 @@ ...@@ -2,22 +2,21 @@
$(function() { $(function() {
/* vm migrate */ /* vm operations */
$('.vm-migrate').click(function(e) { $('#ops').on('click', '.operation.btn', function(e) {
var icon = $(this).children("i"); var icon = $(this).children("i").addClass('icon-spinner icon-spin');
var vm = $(this).data("vm-pk");
icon.removeClass("icon-truck").addClass("icon-spinner icon-spin");
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/dashboard/vm/' + vm + '/migrate/', url: $(this).attr('href'),
success: function(data) { success: function(data) {
icon.addClass("icon-truck").removeClass("icon-spinner icon-spin"); icon.removeClass("icon-spinner icon-spin");
$('body').append(data); $('body').append(data);
$('#create-modal').modal('show'); $('#confirmation-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() { $('#confirmation-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove(); $('#confirmation-modal').remove();
}); });
$('#vm-migrate-node-list li').click(function(e) { $('#vm-migrate-node-list li').click(function(e) {
var li = $(this).closest('li'); var li = $(this).closest('li');
if (li.find('input').attr('disabled')) if (li.find('input').attr('disabled'))
......
...@@ -211,6 +211,7 @@ function checkNewActivity(only_status, runs) { ...@@ -211,6 +211,7 @@ function checkNewActivity(only_status, runs) {
success: function(data) { success: function(data) {
if(!only_status) { if(!only_status) {
$("#activity-timeline").html(data['activities']); $("#activity-timeline").html(data['activities']);
$("#ops").html(data['ops']);
$("[title]").tooltip(); $("[title]").tooltip();
} }
......
from __future__ import absolute_import
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django_tables2 import Table, A from django_tables2 import Table, A
from django_tables2.columns import (TemplateColumn, Column, BooleanColumn, from django_tables2.columns import (TemplateColumn, Column, BooleanColumn,
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default" style="margin-top: 60px;">
<div class="panel-heading">
<h3 class="no-margin">
{% if title %}
{{ title }}
{% else %}
{% trans "Confirmation" %}
{% endif %}
</h3>
</div>
<div class="panel-body">
{{ body|safe|default:"(body missing from context.)" }}
</div>
</div>
{% endblock %}
{% load i18n %}
<div class="modal fade" id="confirmation-modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
{{ body|safe|default:"(body missing from context.)" }}
<div class="clearfix"></div>
</div>
<div class="clearfix"></div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
{% extends "dashboard/operate.html" %}
{% load i18n %} {% load i18n %}
{% load sizefieldtags %} {% load sizefieldtags %}
<form method="POST" action="{% url "dashboard.views.vm-migrate" pk=vm.pk %}"> {% block question %}
{% csrf_token %} <p>
<ul id="vm-migrate-node-list"> {% blocktrans with obj=object op=op.name %}
{% with current=vm.node.pk selected=vm.select_node.pk %} Choose a compute node to migrate {{obj}} to.
{% endblocktrans %}
</p>
<p class="text-info">{{op.name}}: {{op.description}}</p>
{% endblock %}
{% block formfields %}
<ul id="vm-migrate-node-list" class="list-unstyled">
{% with current=object.node.pk selected=object.select_node.pk %}
{% for n in nodes %} {% for n in nodes %}
<li class="panel panel-default"><div class="panel-body"> <li class="panel panel-default"><div class="panel-body">
<label for="migrate-to-{{n.pk}}"> <label for="migrate-to-{{n.pk}}">
...@@ -22,5 +31,4 @@ ...@@ -22,5 +31,4 @@
{% endfor %} {% endfor %}
{% endwith %} {% endwith %}
</ul> </ul>
<button type="submit" class="btn btn-primary btn-sm"><i class="icon-truck"></i> Migrate</button> {% endblock %}
</form>
{% load i18n %}
{% block question %}
<p>
{% blocktrans with obj=object op=op.name %}
Do you want to do the following operation on {{obj}}:
<strong>{{op}}</strong>?
{% endblocktrans %}
</p>
<p class="text-info">{{op.name}}: {{op.description}}</p>
{% endblock %}
<form method="POST" action="{{url}}">{% csrf_token %}
{% block formfields %}{% endblock %}
<div class="pull-right">
<a class="btn btn-default" href="{{object.get_absolute_url}}"
data-dismiss="modal">{% trans "Cancel" %}</a>
<button class="btn btn-danger" type="submit">{% if op.icon %}<i class="icon-{{op.icon}}"></i> {% endif %}{{ op|capfirst }}</button>
</div>
</form>
...@@ -14,7 +14,20 @@ ...@@ -14,7 +14,20 @@
<h3 class="no-margin"><i class="icon-desktop"></i> {% trans "My profile" %}</h3> <h3 class="no-margin"><i class="icon-desktop"></i> {% trans "My profile" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{% crispy form %} <div class="row">
<div class="col-sm-4" style="margin-bottom: 50px;">
<fieldset>
<legend>{% trans "Password change" %}</legend>
{% crispy forms.change_password %}
</fieldset>
</div>
<div class="col-sm-offset-5 col-sm-3">
<fieldset>
<legend>{% trans "Language selection" %}</legend>
{% crispy forms.change_language %}
</fieldset>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -40,54 +40,8 @@ ...@@ -40,54 +40,8 @@
{% endif %} {% endif %}
<div class="body-content"> <div class="body-content">
<div class="page-header"> <div class="page-header">
<div class="pull-right" style="padding-top: 15px;"> <div class="pull-right" style="padding-top: 15px;" id="ops">
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}"> {% include "dashboard/vm-detail/_operations.html" %}
{% csrf_token %}
<input type="hidden" name="sleep" />
<button title="{% trans "Sleep" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-moon"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}" id="vm-details-button-deploy">
{% csrf_token %}
<input type="hidden" name="deploy" />
<button title="{% trans "Deploy" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-play"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="wake_up" />
<button title="{% trans "Wake up" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-sun"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="shut_down" />
<button title="{% trans "Shut down" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-off"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="reboot" />
<button title="{% trans "Reboot (ctrl + alt + del)" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-refresh"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="reset" />
<button title="{% trans "Reset (power cycle)" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-bolt"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="shut_off"/>
<button title="{% trans "Shut off" %}" class="btn btn-default btn-xs" type="submit">
<i class="icon-ban-circle"></i>
</button>
</form>
<a title="Migrate" data-vm-pk="{{ instance.pk }}" href="{% url "dashboard.views.vm-migrate" pk=instance.pk %}" class="btn btn-default btn-xs vm-migrate">
<i class="icon-truck"></i>
</a>
<form style="display: inline;" class="vm-details-button-save-as" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="save_as" />
<button title="{% trans "Save as template" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-save"></i></button>
</form>
<a title="{% trans "Destroy" %}" href="{% url "dashboard.views.delete-vm" pk=instance.pk %}" class="btn btn-default btn-xs vm-delete" data-vm-pk="{{ instance.pk }}"><i class="icon-remove"></i></a>
<a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs vm-details-help-button"><i class="icon-question"></i></a>
</div> </div>
<h1> <h1>
<div id="vm-details-rename"> <div id="vm-details-rename">
...@@ -102,46 +56,6 @@ ...@@ -102,46 +56,6 @@
</div> </div>
<small>{{ instance.primary_host.get_fqdn }}</small> <small>{{ instance.primary_host.get_fqdn }}</small>
</h1> </h1>
<div class="vm-details-help js-hidden">
<ul style="list-style: none;">
<li>
<strong>{% trans "Sleep" %}:</strong>
{% trans "Suspend virtual machine with memory dump." %}
</li>
<li>
<strong>{% trans "Wake up" %}:</strong>
{% trans "Wake up suspended machine." %}
</li>
<li>
<strong>{% trans "Shutdown" %}:</strong>
{% trans "Shutdown virtual machine with ACPI signal." %}
</li>
<li>
<strong>{% trans "Reboot (ctrl + alt + del)" %}:</strong>
{% trans "Reboot virtual machine with Ctrl+Alt+Del signal." %}
</li>
<li>
<strong>{% trans "Reset (power cycle)" %}:</strong>
{% trans "Reset virtual machine (reset button)" %}
</li>
<li>
<strong>{% trans "Shut off" %}:</strong>
{% trans "Shut off VM. (plug-out)" %}
</li>
<li>
<strong>{% trans "Migrate" %}:</strong>
{% trans "Live migrate running vm to another node." %}
</li>
<li>
<strong>{% trans "Save as template" %}:</strong>
{% trans "Shut down the virtual machine, and save it as a new template." %}
</li>
<li>
<strong>{% trans "Destroy" %}:</strong>
{% trans "Remove virtual machine and its networks." %}
</li>
</ul>
</div>
<div style="clear: both;"></div> <div style="clear: both;"></div>
</div> </div>
<div class="row"> <div class="row">
......
{% load i18n %}
{% for op in ops %}
<a href="{{op.get_url}}" class="operation operation-{{op.op}} btn btn-default btn-xs"
title="{{op.name}}: {{op.description}}">
<i class="icon-{{op.icon}}"></i>
<span class="sr-only">{{op.name}}</span>
</a>
{% endfor %}
...@@ -3,10 +3,11 @@ from factory import Factory, Sequence ...@@ -3,10 +3,11 @@ from factory import Factory, Sequence
from mock import patch, MagicMock from mock import patch, MagicMock
from django.contrib.auth.models import User from django.contrib.auth.models import User
# from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import HttpRequest, Http404 from django.http import HttpRequest, Http404
from dashboard.views import InstanceActivityDetail, InstanceActivity from ..views import InstanceActivityDetail, InstanceActivity
from ..views import vm_ops, Instance
class ViewUserTestCase(unittest.TestCase): class ViewUserTestCase(unittest.TestCase):
...@@ -36,6 +37,82 @@ class ViewUserTestCase(unittest.TestCase): ...@@ -36,6 +37,82 @@ class ViewUserTestCase(unittest.TestCase):
self.assertEquals(view(request, pk=1234).render().status_code, 200) self.assertEquals(view(request, pk=1234).render().status_code, 200)
class VmOperationViewTestCase(unittest.TestCase):
def test_available(self):
request = FakeRequestFactory(superuser=True)
view = vm_ops['destroy']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.destroy = Instance._ops['destroy'](inst)
go.return_value = inst
self.assertEquals(
view.as_view()(request, pk=1234).render().status_code, 200)
def test_unpermitted(self):
request = FakeRequestFactory()
view = vm_ops['destroy']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.destroy = Instance._ops['destroy'](inst)
inst.has_level.return_value = False
go.return_value = inst
with self.assertRaises(PermissionDenied):
view.as_view()(request, pk=1234).render()
def test_migrate(self):
request = FakeRequestFactory(POST={'node': 1})
view = vm_ops['migrate']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.messages') as msg, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock()
inst.has_level.return_value = True
go.return_value = inst
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234)['location']
assert not msg.error.called
def test_migrate_failed(self):
request = FakeRequestFactory(POST={'node': 1})
view = vm_ops['migrate']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.messages') as msg, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock()
inst.migrate.async.side_effect = Exception
inst.has_level.return_value = True
go.return_value = inst
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234)['location']
assert msg.error.called
def test_migrate_template(self):
request = FakeRequestFactory()
view = vm_ops['migrate']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.has_level.return_value = True
go.return_value = inst
self.assertEquals(
view.as_view()(request, pk=1234).render().status_code, 200)
def FakeRequestFactory(*args, **kwargs): def FakeRequestFactory(*args, **kwargs):
''' FakeRequestFactory, FakeMessages and FakeRequestContext are good for ''' FakeRequestFactory, FakeMessages and FakeRequestContext are good for
mocking out django views; they are MUCH faster than the Django test client. mocking out django views; they are MUCH faster than the Django test client.
...@@ -48,12 +125,12 @@ def FakeRequestFactory(*args, **kwargs): ...@@ -48,12 +125,12 @@ def FakeRequestFactory(*args, **kwargs):
request = HttpRequest() request = HttpRequest()
request.user = user request.user = user
request.session = kwargs.get('session', {}) request.session = kwargs.get('session', {})
if kwargs.get('POST'): if kwargs.get('POST') is not None:
request.method = 'POST' request.method = 'POST'
request.POST = kwargs.get('POST') request.POST = kwargs.get('POST')
else: else:
request.method = 'GET' request.method = 'GET'
request.POST = kwargs.get('GET', {}) request.GET = kwargs.get('GET', {})
return request return request
......
from unittest import skip
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.auth import authenticate
from vm.models import Instance, InstanceTemplate, Lease, Node, Trait from vm.models import Instance, InstanceTemplate, Lease, Node, Trait
from vm.operations import WakeUpOperation from vm.operations import WakeUpOperation
...@@ -277,6 +279,7 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -277,6 +279,7 @@ class VmDetailTest(LoginMixin, TestCase):
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
self.assertEqual(disks, inst.disks.count()) self.assertEqual(disks, inst.disks.count())
@skip("until fix merged")
def test_permitted_vm_disk_add(self): def test_permitted_vm_disk_add(self):
c = Client() c = Client()
self.login(c, "user1") self.login(c, "user1")
...@@ -493,9 +496,11 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -493,9 +496,11 @@ class VmDetailTest(LoginMixin, TestCase):
mock_method.side_effect = inst.wake_up mock_method.side_effect = inst.wake_up
inst.manual_state_change('RUNNING') inst.manual_state_change('RUNNING')
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
self.assertRaises(inst.WrongStateError, c.post, with patch('dashboard.views.messages') as msg:
"/dashboard/vm/1/", {'wake_up': True}) c.post("/dashboard/vm/1/op/wake_up/")
self.assertEqual(inst.status, 'RUNNING') assert msg.error.called
inst = Instance.objects.get(pk=1)
self.assertEqual(inst.status, 'RUNNING') # mocked anyway
assert mock_method.called assert mock_method.called
def test_permitted_wake_up(self): def test_permitted_wake_up(self):
...@@ -509,7 +514,9 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -509,7 +514,9 @@ class VmDetailTest(LoginMixin, TestCase):
inst.get_remote_queue_name = Mock(return_value='test') inst.get_remote_queue_name = Mock(return_value='test')
inst.manual_state_change('SUSPENDED') inst.manual_state_change('SUSPENDED')
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
response = c.post("/dashboard/vm/1/", {'wake_up': True}) with patch('dashboard.views.messages') as msg:
response = c.post("/dashboard/vm/1/op/wake_up/")
assert not msg.error.called
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(inst.status, 'RUNNING') self.assertEqual(inst.status, 'RUNNING')
assert new_wake_up.called assert new_wake_up.called
...@@ -521,8 +528,11 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -521,8 +528,11 @@ class VmDetailTest(LoginMixin, TestCase):
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.manual_state_change('SUSPENDED') inst.manual_state_change('SUSPENDED')
inst.set_level(self.u2, 'user') inst.set_level(self.u2, 'user')
response = c.post("/dashboard/vm/1/", {'wake_up': True}) with patch('dashboard.views.messages') as msg:
self.assertEqual(response.status_code, 403) response = c.post("/dashboard/vm/1/op/wake_up/")
assert msg.error.called
self.assertEqual(response.status_code, 302)
inst = Instance.objects.get(pk=1)
self.assertEqual(inst.status, 'SUSPENDED') self.assertEqual(inst.status, 'SUSPENDED')
def test_non_existing_template_get(self): def test_non_existing_template_get(self):
...@@ -982,3 +992,68 @@ class IndexViewTest(LoginMixin, TestCase): ...@@ -982,3 +992,68 @@ class IndexViewTest(LoginMixin, TestCase):
self.u1.profile.notify("urgent", "dashboard/test_message.txt", ) self.u1.profile.notify("urgent", "dashboard/test_message.txt", )
response = c.get("/dashboard/") response = c.get("/dashboard/")
self.assertEqual(response.context['NEW_NOTIFICATIONS_COUNT'], 1) self.assertEqual(response.context['NEW_NOTIFICATIONS_COUNT'], 1)
class ProfileViewTest(LoginMixin, TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.save()
self.p1 = Profile.objects.create(user=self.u1)
self.p1.save()
def test_permitted_language_change(self):
c = Client()
self.login(c, "user1")
old_language_cookie_value = c.cookies['django_language'].value
old_language_db_value = self.u1.profile.preferred_language
response = c.post("/dashboard/profile/", {
'preferred_language': "hu",
})
self.assertEqual(response.status_code, 302)
self.assertNotEqual(old_language_cookie_value,
c.cookies['django_language'].value)
self.assertNotEqual(old_language_db_value,
User.objects.get(
username="user1").profile.preferred_language)
def test_permitted_valid_password_change(self):
c = Client()
self.login(c, "user1")
c.post("/dashboard/profile/", {
'old_password': "password",
'new_password1': "asd",
'new_password2': "asd",
})
self.assertIsNone(authenticate(username="user1", password="password"))
self.assertIsNotNone(authenticate(username="user1", password="asd"))
def test_permitted_invalid_password_changes(self):
c = Client()
self.login(c, "user1")
# wrong current password
c.post("/dashboard/profile/", {
'old_password': "password1",
'new_password1': "asd",
'new_password2': "asd",
})
self.assertIsNotNone(authenticate(username="user1",
password="password"))
self.assertIsNone(authenticate(username="user1", password="asd"))
# wrong pw confirmation
c.post("/dashboard/profile/", {
'old_password': "password",
'new_password1': "asd",
'new_password2': "asd1",
})
self.assertIsNotNone(authenticate(username="user1",
password="password"))
self.assertIsNone(authenticate(username="user1", password="asd"))
from django.conf.urls import patterns, url from __future__ import absolute_import
from django.conf.urls import patterns, url, include
from vm.models import Instance from vm.models import Instance
from .views import ( from .views import (
...@@ -40,6 +41,7 @@ urlpatterns = patterns( ...@@ -40,6 +41,7 @@ urlpatterns = patterns(
url(r"^template/clone/(?P<pk>\d+)/$", TemplateClone.as_view(), url(r"^template/clone/(?P<pk>\d+)/$", TemplateClone.as_view(),
name="dashboard.views.template-clone"), name="dashboard.views.template-clone"),
url(r'^vm/(?P<pk>\d+)/op/', include('dashboard.vm.urls')),
url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(), url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(),
name='dashboard.views.remove-port'), name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(), url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
......
from __future__ import unicode_literals from __future__ import unicode_literals, absolute_import
from os import getenv from os import getenv
import json import json
import logging import logging
import re import re
from datetime import datetime
import requests import requests
from django.conf import settings from django.conf import settings
...@@ -37,7 +36,8 @@ from braces.views import ( ...@@ -37,7 +36,8 @@ from braces.views import (
from .forms import ( from .forms import (
CircleAuthenticationForm, DiskAddForm, HostForm, LeaseForm, MyProfileForm, CircleAuthenticationForm, DiskAddForm, HostForm, LeaseForm, MyProfileForm,
NodeForm, TemplateForm, TraitForm, VmCustomizeForm, TemplateCloneForm NodeForm, TemplateForm, TraitForm, VmCustomizeForm, TemplateCloneForm,
CirclePasswordChangeForm
) )
from .tables import (NodeListTable, NodeVmListTable, from .tables import (NodeListTable, NodeVmListTable,
TemplateListTable, LeaseListTable, GroupListTable,) TemplateListTable, LeaseListTable, GroupListTable,)
...@@ -47,7 +47,7 @@ from vm.models import ( ...@@ -47,7 +47,7 @@ from vm.models import (
) )
from storage.models import Disk from storage.models import Disk
from firewall.models import Vlan, Host, Rule from firewall.models import Vlan, Host, Rule
from dashboard.models import Favourite, Profile from .models import Favourite, Profile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -203,7 +203,8 @@ class VmDetailView(CheckedDetailView): ...@@ -203,7 +203,8 @@ class VmDetailView(CheckedDetailView):
context.update({ context.update({
'graphite_enabled': VmGraphView.get_graphite_url() is not None, 'graphite_enabled': VmGraphView.get_graphite_url() is not None,
'vnc_url': reverse_lazy("dashboard.views.detail-vnc", 'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
kwargs={'pk': self.object.pk}) kwargs={'pk': self.object.pk}),
'ops': get_operations(instance, self.request.user),
}) })
# activity data # activity data
...@@ -242,14 +243,6 @@ class VmDetailView(CheckedDetailView): ...@@ -242,14 +243,6 @@ class VmDetailView(CheckedDetailView):
'to_remove': self.__remove_tag, 'to_remove': self.__remove_tag,
'port': self.__add_port, 'port': self.__add_port,
'new_network_vlan': self.__new_network, 'new_network_vlan': self.__new_network,
'save_as': self.__save_as,
'shut_down': self.__shut_down,
'sleep': self.__sleep,
'wake_up': self.__wake_up,
'deploy': self.__deploy,
'reset': self.__reset,
'reboot': self.__reboot,
'shut_off': self.__shut_off,
} }
for k, v in options.iteritems(): for k, v in options.iteritems():
if request.POST.get(k) is not None: if request.POST.get(k) is not None:
...@@ -404,8 +397,7 @@ class VmDetailView(CheckedDetailView): ...@@ -404,8 +397,7 @@ class VmDetailView(CheckedDetailView):
if not vlan.has_level(request.user, 'user'): if not vlan.has_level(request.user, 'user'):
raise PermissionDenied() raise PermissionDenied()
try: try:
Interface.create(vlan=vlan, instance=self.object, self.object.add_interface(vlan=vlan, user=request.user)
managed=vlan.managed, owner=request.user)
messages.success(request, _("Successfully added new interface!")) messages.success(request, _("Successfully added new interface!"))
except Exception, e: except Exception, e:
error = u' '.join(e.messages) error = u' '.join(e.messages)
...@@ -414,75 +406,132 @@ class VmDetailView(CheckedDetailView): ...@@ -414,75 +406,132 @@ class VmDetailView(CheckedDetailView):
return redirect("%s#network" % reverse_lazy( return redirect("%s#network" % reverse_lazy(
"dashboard.views.detail", kwargs={'pk': self.object.pk})) "dashboard.views.detail", kwargs={'pk': self.object.pk}))
def __save_as(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
date = datetime.now().strftime("%Y-%m-%d %H:%M") class OperationView(DetailView):
new_name = "Saved from %s (#%d) at %s" % (
self.object.name, self.object.pk, date
)
self.object.save_as_template.async(name=new_name,
user=request.user)
messages.success(request, _("Saving instance as template!"))
return redirect("%s#activity" % self.object.get_absolute_url())
def __shut_down(self, request): template_name = 'dashboard/operate.html'
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
self.object.shutdown.async(user=request.user) @property
return redirect("%s#activity" % self.object.get_absolute_url()) def name(self):
return self.get_op().name
def __sleep(self, request): @property
self.object = self.get_object() def description(self):
if not self.object.has_level(request.user, 'owner'): return self.get_op().description
raise PermissionDenied()
self.object.sleep.async(user=request.user) @classmethod
return redirect("%s#activity" % self.object.get_absolute_url()) def get_urlname(cls):
return 'dashboard.vm.op.%s' % cls.op
def __wake_up(self, request): def get_url(self):
self.object = self.get_object() return reverse(self.get_urlname(), args=(self.get_object().pk, ))
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
self.object.wake_up.async(user=request.user) def get_wrapper_template_name(self):
return redirect("%s#activity" % self.object.get_absolute_url()) if self.request.is_ajax():
return 'dashboard/_modal.html'
else:
return 'dashboard/_base.html'
def __deploy(self, request): @classmethod
self.object = self.get_object() def get_op_by_object(cls, obj):
if not self.object.has_level(request.user, 'owner'): return getattr(obj, cls.op)
raise PermissionDenied()
self.object.deploy.async(user=request.user) def get_op(self):
return redirect("%s#activity" % self.object.get_absolute_url()) if not hasattr(self, '_opobj'):
setattr(self, '_opobj', getattr(self.get_object(), self.op))
return self._opobj
def __reset(self, request): def get_context_data(self, **kwargs):
self.object = self.get_object() ctx = super(OperationView, self).get_context_data(**kwargs)
if not self.object.has_level(request.user, 'owner'): ctx['op'] = self.get_op()
raise PermissionDenied() ctx['url'] = self.request.path
return ctx
self.object.reset.async(user=request.user) def get(self, request, *args, **kwargs):
return redirect("%s#activity" % self.object.get_absolute_url()) self.get_op().check_auth(request.user)
response = super(OperationView, self).get(request, *args, **kwargs)
response.render()
response.content = render_to_string(self.get_wrapper_template_name(),
{'body': response.content})
return response
def __reboot(self, request): def post(self, request, extra=None, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'): if extra is None:
raise PermissionDenied() extra = {}
try:
self.object.reboot.async(user=request.user) self.get_op().async(user=request.user, **extra)
except Exception as e:
messages.error(request, _('Could not start operation.'))
logger.error(e)
return redirect("%s#activity" % self.object.get_absolute_url()) return redirect("%s#activity" % self.object.get_absolute_url())
def __shut_off(self, request): @classmethod
self.object = self.get_object() def factory(cls, op, icon='cog'):
if not self.object.has_level(request.user, 'owner'): return type(str(cls.__name__ + op),
raise PermissionDenied() (cls, ), {'op': op, 'icon': icon})
self.object.shut_off.async(user=request.user) @classmethod
return redirect("%s#activity" % self.object.get_absolute_url()) def bind_to_object(cls, instance):
v = cls()
v.get_object = lambda: instance
return v
class VmOperationView(OperationView):
model = Instance
class VmMigrateView(VmOperationView):
op = 'migrate'
icon = 'truck'
template_name = 'dashboard/_vm-migrate.html'
def get_context_data(self, **kwargs):
ctx = super(VmOperationView, self).get_context_data(**kwargs)
ctx['nodes'] = [n for n in Node.objects.filter(enabled=True)
if n.state == "ONLINE"]
return ctx
def post(self, request, extra=None, *args, **kwargs):
if extra is None:
extra = {}
node = self.request.POST.get("node")
if node:
node = get_object_or_404(Node, pk=node)
extra["to_node"] = node
return super(VmMigrateView, self).post(request, extra, *args, **kwargs)
vm_ops = {
'reset': VmOperationView.factory(op='reset', icon='bolt'),
'deploy': VmOperationView.factory(op='deploy', icon='play'),
'migrate': VmMigrateView,
'reboot': VmOperationView.factory(op='reboot', icon='refresh'),
'shut_off': VmOperationView.factory(op='shut_off', icon='ban-circle'),
'shutdown': VmOperationView.factory(op='shutdown', icon='off'),
'save_as_template': VmOperationView.factory(
op='save_as_template', icon='save'),
'destroy': VmOperationView.factory(op='destroy', icon='remove'),
'sleep': VmOperationView.factory(op='sleep', icon='moon'),
'wake_up': VmOperationView.factory(op='wake_up', icon='sun'),
}
def get_operations(instance, user):
ops = []
for k, v in vm_ops.iteritems():
try:
op = v.get_op_by_object(instance)
op.check_auth(user)
op.check_precond()
except:
pass # unavailable
else:
ops.append(v.bind_to_object(instance))
return ops
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView): class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
...@@ -1615,8 +1664,11 @@ class VmMassDelete(LoginRequiredMixin, View): ...@@ -1615,8 +1664,11 @@ class VmMassDelete(LoginRequiredMixin, View):
raise PermissionDenied() # no need for rollback or proper raise PermissionDenied() # no need for rollback or proper
# error message, this can't # error message, this can't
# normally happen. # normally happen.
i.destroy.async(user=request.user) try:
names.append(i.name) i.destroy.async(user=request.user)
names.append(i.name)
except Exception as e:
logger.error(e)
success_message = _("Mass delete complete, the following VMs were " success_message = _("Mass delete complete, the following VMs were "
"deleted: %s!") % u', '.join(names) "deleted: %s!") % u', '.join(names)
...@@ -1691,23 +1743,28 @@ def vm_activity(request, pk): ...@@ -1691,23 +1743,28 @@ def vm_activity(request, pk):
raise PermissionDenied() raise PermissionDenied()
response = {} response = {}
only_status = request.GET.get("only_status") only_status = request.GET.get("only_status", "false")
response['human_readable_status'] = instance.get_status_display() response['human_readable_status'] = instance.get_status_display()
response['status'] = instance.status response['status'] = instance.status
response['icon'] = instance.get_status_icon() response['icon'] = instance.get_status_icon()
if only_status == "false": # instance activity if only_status == "false": # instance activity
context = { context = {
'instance': instance,
'activities': InstanceActivity.objects.filter( 'activities': InstanceActivity.objects.filter(
instance=instance, parent=None instance=instance, parent=None
).order_by('-started').select_related() ).order_by('-started').select_related(),
'ops': get_operations(instance, request.user),
} }
activities = render_to_string( response['activities'] = render_to_string(
"dashboard/vm-detail/_activity-timeline.html", "dashboard/vm-detail/_activity-timeline.html",
RequestContext(request, context), RequestContext(request, context),
) )
response['activities'] = activities response['ops'] = render_to_string(
"dashboard/vm-detail/_operations.html",
RequestContext(request, context),
)
return HttpResponse( return HttpResponse(
json.dumps(response), json.dumps(response),
...@@ -2117,40 +2174,6 @@ class NotificationView(LoginRequiredMixin, TemplateView): ...@@ -2117,40 +2174,6 @@ class NotificationView(LoginRequiredMixin, TemplateView):
return response return response
class VmMigrateView(SuperuserRequiredMixin, TemplateView):
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/modal-wrapper.html']
else:
return ['dashboard/nojs-wrapper.html']
def get(self, request, form=None, *args, **kwargs):
context = self.get_context_data(**kwargs)
vm = Instance.objects.get(pk=kwargs['pk'])
context.update({
'template': 'dashboard/_vm-migrate.html',
'box_title': _('Migrate %(name)s' % {'name': vm.name}),
'ajax_title': True,
'vm': vm,
'nodes': [n for n in Node.objects.filter(enabled=True)
if n.state == "ONLINE"]
})
return self.render_to_response(context)
def post(self, *args, **kwargs):
node = self.request.POST.get("node")
vm = Instance.objects.get(pk=kwargs['pk'])
if node:
node = Node.objects.get(pk=node)
vm.migrate.async(to_node=node, user=self.request.user)
else:
messages.error(self.request, _("You didn't select a node!"))
return redirect("%s#activity" % vm.get_absolute_url())
def circle_login(request): def circle_login(request):
authentication_form = CircleAuthenticationForm authentication_form = CircleAuthenticationForm
extra_context = { extra_context = {
...@@ -2204,9 +2227,17 @@ class DiskAddView(TemplateView): ...@@ -2204,9 +2227,17 @@ class DiskAddView(TemplateView):
class MyPreferencesView(UpdateView): class MyPreferencesView(UpdateView):
model = Profile model = Profile
form_class = MyProfileForm
def get_context_data(self, *args, **kwargs):
context = super(MyPreferencesView, self).get_context_data(*args,
**kwargs)
context['forms'] = {
'change_password': CirclePasswordChangeForm(
user=self.request.user),
'change_language': MyProfileForm(instance=self.get_object()),
}
return context
def get_object(self, queryset=None): def get_object(self, queryset=None):
if self.request.user.is_anonymous(): if self.request.user.is_anonymous():
...@@ -2216,10 +2247,36 @@ class MyPreferencesView(UpdateView): ...@@ -2216,10 +2247,36 @@ class MyPreferencesView(UpdateView):
except Profile.DoesNotExist: except Profile.DoesNotExist:
raise Http404(_("You don't have a profile.")) raise Http404(_("You don't have a profile."))
def form_valid(self, form): def post(self, request, *args, **kwargs):
response = super(MyPreferencesView, self).form_valid(form) self.ojbect = self.get_object()
set_language_cookie(self.request, response) redirect_response = HttpResponseRedirect(
return response reverse("dashboard.views.profile"))
if "preferred_language" in request.POST:
form = MyProfileForm(request.POST, instance=self.get_object())
if form.is_valid():
lang = form.cleaned_data.get("preferred_language")
set_language_cookie(self.request, redirect_response, lang)
form.save()
else:
form = CirclePasswordChangeForm(user=request.user,
data=request.POST)
if form.is_valid():
form.save()
if form.is_valid():
return redirect_response
else:
return self.get(request, form=form, *args, **kwargs)
def get(self, request, form=None, *args, **kwargs):
# if this is not here, it won't work
self.object = self.get_object()
context = self.get_context_data(*args, **kwargs)
if form is not None:
# a little cheating, users can't post invalid
# language selection forms (without modifying the HTML)
context['forms']['change_password'] = form
return self.render_to_response(context)
def set_language_cookie(request, response, lang=None): def set_language_cookie(request, response, lang=None):
......
from django.conf.urls import patterns, url
from ..views import vm_ops
urlpatterns = patterns('',
*(url(r'^%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_ops.iteritems()))
...@@ -38,7 +38,7 @@ class HostAdmin(admin.ModelAdmin): ...@@ -38,7 +38,7 @@ class HostAdmin(admin.ModelAdmin):
class HostInline(contrib.admin.TabularInline): class HostInline(contrib.admin.TabularInline):
model = Host model = Host
fields = ('hostname', 'ipv4', 'ipv6', 'pub_ipv4', 'mac', 'shared_ip', fields = ('hostname', 'ipv4', 'ipv6', 'external_ipv4', 'mac', 'shared_ip',
'owner', 'reverse') 'owner', 'reverse')
......
...@@ -89,7 +89,7 @@ class IptChain(object): ...@@ -89,7 +89,7 @@ class IptChain(object):
self.rules.add(rule) self.rules.add(rule)
def sort(self): def sort(self):
return sorted(list(self.rules)) return sorted(list(self.rules), reverse=True)
def __len__(self): def __len__(self):
return len(self.rules) return len(self.rules)
......
...@@ -32,8 +32,8 @@ class Migration(SchemaMigration): ...@@ -32,8 +32,8 @@ class Migration(SchemaMigration):
if rule.nat: if rule.nat:
# swap # swap
tmp = rule.dport tmp = rule.dport
# rule.dport = rule.nat_external_port rule.dport = rule.nat_external_port
# rule.nat_external_port = tmp rule.nat_external_port = tmp
if rule.direction == '0': if rule.direction == '0':
rule.direction = 'out' rule.direction = 'out'
elif rule.direction == '1': elif rule.direction == '1':
......
...@@ -5,7 +5,8 @@ from .models import Disk, DataStore, DiskActivity ...@@ -5,7 +5,8 @@ from .models import Disk, DataStore, DiskActivity
class DiskAdmin(contrib.admin.ModelAdmin): class DiskAdmin(contrib.admin.ModelAdmin):
list_display = ('name', 'datastore') list_display = ('id', 'name', 'base', 'type', 'datastore')
ordering = ('-id', )
class DataStoreAdmin(contrib.admin.ModelAdmin): class DataStoreAdmin(contrib.admin.ModelAdmin):
......
...@@ -22,7 +22,6 @@ from taggit.managers import TaggableManager ...@@ -22,7 +22,6 @@ from taggit.managers import TaggableManager
from acl.models import AclBase from acl.models import AclBase
from common.operations import OperatedMixin from common.operations import OperatedMixin
from storage.models import Disk
from ..tasks import vm_tasks, agent_tasks from ..tasks import vm_tasks, agent_tasks
from .activity import (ActivityInProgressError, instance_activity, from .activity import (ActivityInProgressError, instance_activity,
InstanceActivity) InstanceActivity)
...@@ -118,8 +117,9 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -118,8 +117,9 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
description = TextField(verbose_name=_('description'), blank=True) description = TextField(verbose_name=_('description'), blank=True)
parent = ForeignKey('self', null=True, blank=True, parent = ForeignKey('self', null=True, blank=True,
verbose_name=_('parent template'), verbose_name=_('parent template'),
on_delete=SET_NULL,
help_text=_('Template which this one is derived of.')) help_text=_('Template which this one is derived of.'))
disks = ManyToManyField(Disk, verbose_name=_('disks'), disks = ManyToManyField('storage.Disk', verbose_name=_('disks'),
related_name='template_set', related_name='template_set',
help_text=_('Disks which are to be mounted.')) help_text=_('Disks which are to be mounted.'))
owner = ForeignKey(User) owner = ForeignKey(User)
...@@ -211,7 +211,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -211,7 +211,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
related_name='instance_set', related_name='instance_set',
help_text=_("Current hypervisor of this instance."), help_text=_("Current hypervisor of this instance."),
verbose_name=_('host node')) verbose_name=_('host node'))
disks = ManyToManyField(Disk, related_name='instance_set', disks = ManyToManyField('storage.Disk', related_name='instance_set',
help_text=_("Set of mounted disks."), help_text=_("Set of mounted disks."),
verbose_name=_('disks')) verbose_name=_('disks'))
vnc_port = IntegerField(blank=True, default=None, null=True, vnc_port = IntegerField(blank=True, default=None, null=True,
......
...@@ -58,10 +58,6 @@ class Interface(Model): ...@@ -58,10 +58,6 @@ class Interface(Model):
return 'cloud-' + str(self.instance.id) + '-' + str(self.vlan.vid) return 'cloud-' + str(self.instance.id) + '-' + str(self.vlan.vid)
@property @property
def destroyed(self):
return self.instance.destroyed_at
@property
def mac(self): def mac(self):
try: try:
return self.host.mac return self.host.mac
...@@ -120,10 +116,10 @@ class Interface(Model): ...@@ -120,10 +116,10 @@ class Interface(Model):
host.owner = owner host.owner = owner
if vlan.network_type == 'public': if vlan.network_type == 'public':
host.shared_ip = False host.shared_ip = False
host.pub_ipv4 = None host.external_ipv4 = None
elif vlan.network_type == 'portforward': elif vlan.network_type == 'portforward':
host.shared_ip = True host.shared_ip = True
host.pub_ipv4 = vlan.snat_ip host.external_ipv4 = vlan.snat_ip
host.full_clean() host.full_clean()
host.save() host.save()
host.enable_net() host.enable_net()
...@@ -138,34 +134,16 @@ class Interface(Model): ...@@ -138,34 +134,16 @@ class Interface(Model):
return iface return iface
def deploy(self): def deploy(self):
if self.destroyed: queue_name = self.instance.get_remote_queue_name('net')
from .instance import Instance return net_tasks.create.apply_async(args=[self.get_vmnetwork_desc()],
raise Instance.InstanceDestroyedError(self.instance, queue=queue_name).get()
"The associated instance "
"(%s) has already been "
"destroyed" % self.instance)
net_tasks.create.apply_async(
args=[self.get_vmnetwork_desc()],
queue=self.instance.get_remote_queue_name('net'))
def shutdown(self): def shutdown(self):
if self.destroyed:
from .instance import Instance
raise Instance.InstanceDestroyedError(self.instance,
"The associated instance "
"(%s) has already been "
"destroyed" % self.instance)
queue_name = self.instance.get_remote_queue_name('net') queue_name = self.instance.get_remote_queue_name('net')
net_tasks.destroy.apply_async(args=[self.get_vmnetwork_desc()], return net_tasks.destroy.apply_async(args=[self.get_vmnetwork_desc()],
queue=queue_name) queue=queue_name).get()
def destroy(self): def destroy(self):
if self.destroyed:
return
self.shutdown()
if self.host is not None: if self.host is not None:
self.host.delete() self.host.delete()
......
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from logging import getLogger from logging import getLogger
from re import search
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.utils import timezone from django.utils import timezone
...@@ -10,7 +11,8 @@ from celery.exceptions import TimeLimitExceeded ...@@ -10,7 +11,8 @@ from celery.exceptions import TimeLimitExceeded
from common.operations import Operation, register_operation from common.operations import Operation, register_operation
from .tasks.local_tasks import async_instance_operation, async_node_operation from .tasks.local_tasks import async_instance_operation, async_node_operation
from .models import ( from .models import (
Instance, InstanceActivity, InstanceTemplate, Node, NodeActivity, Instance, InstanceActivity, InstanceTemplate, Interface, Node,
NodeActivity,
) )
...@@ -20,6 +22,7 @@ logger = getLogger(__name__) ...@@ -20,6 +22,7 @@ logger = getLogger(__name__)
class InstanceOperation(Operation): class InstanceOperation(Operation):
acl_level = 'owner' acl_level = 'owner'
async_operation = async_instance_operation async_operation = async_instance_operation
host_cls = Instance
def __init__(self, instance): def __init__(self, instance):
super(InstanceOperation, self).__init__(subject=instance) super(InstanceOperation, self).__init__(subject=instance)
...@@ -54,8 +57,27 @@ class InstanceOperation(Operation): ...@@ -54,8 +57,27 @@ class InstanceOperation(Operation):
user=user) user=user)
def register_instance_operation(op_cls, op_id=None): class AddInterfaceOperation(InstanceOperation):
return register_operation(Instance, op_cls, op_id) activity_code_suffix = 'add_interface'
id = 'add_interface'
name = _("add interface")
description = _("Add a new network interface for the specified VLAN to "
"the VM.")
def _operation(self, activity, user, system, vlan, managed=None):
if managed is None:
managed = vlan.managed
net = Interface.create(base_activity=activity, instance=self.instance,
managed=managed, owner=user, vlan=vlan)
if self.instance.is_running:
net.deploy()
return net
register_operation(AddInterfaceOperation)
class DeployOperation(InstanceOperation): class DeployOperation(InstanceOperation):
...@@ -63,6 +85,7 @@ class DeployOperation(InstanceOperation): ...@@ -63,6 +85,7 @@ class DeployOperation(InstanceOperation):
id = 'deploy' id = 'deploy'
name = _("deploy") name = _("deploy")
description = _("Deploy new virtual machine with network.") description = _("Deploy new virtual machine with network.")
icon = 'play'
def on_commit(self, activity): def on_commit(self, activity):
activity.resultant_state = 'RUNNING' activity.resultant_state = 'RUNNING'
...@@ -91,7 +114,7 @@ class DeployOperation(InstanceOperation): ...@@ -91,7 +114,7 @@ class DeployOperation(InstanceOperation):
self.instance.renew(which='both', base_activity=activity) self.instance.renew(which='both', base_activity=activity)
register_instance_operation(DeployOperation) register_operation(DeployOperation)
class DestroyOperation(InstanceOperation): class DestroyOperation(InstanceOperation):
...@@ -99,6 +122,7 @@ class DestroyOperation(InstanceOperation): ...@@ -99,6 +122,7 @@ class DestroyOperation(InstanceOperation):
id = 'destroy' id = 'destroy'
name = _("destroy") name = _("destroy")
description = _("Destroy virtual machine and its networks.") description = _("Destroy virtual machine and its networks.")
icon = 'remove'
def on_commit(self, activity): def on_commit(self, activity):
activity.resultant_state = 'DESTROYED' activity.resultant_state = 'DESTROYED'
...@@ -107,6 +131,7 @@ class DestroyOperation(InstanceOperation): ...@@ -107,6 +131,7 @@ class DestroyOperation(InstanceOperation):
if self.instance.node: if self.instance.node:
# Destroy networks # Destroy networks
with activity.sub_activity('destroying_net'): with activity.sub_activity('destroying_net'):
self.instance.shutdown_net()
self.instance.destroy_net() self.instance.destroy_net()
# Delete virtual machine # Delete virtual machine
...@@ -131,7 +156,7 @@ class DestroyOperation(InstanceOperation): ...@@ -131,7 +156,7 @@ class DestroyOperation(InstanceOperation):
self.instance.save() self.instance.save()
register_instance_operation(DestroyOperation) register_operation(DestroyOperation)
class MigrateOperation(InstanceOperation): class MigrateOperation(InstanceOperation):
...@@ -139,6 +164,7 @@ class MigrateOperation(InstanceOperation): ...@@ -139,6 +164,7 @@ class MigrateOperation(InstanceOperation):
id = 'migrate' id = 'migrate'
name = _("migrate") name = _("migrate")
description = _("Live migrate running VM to another node.") description = _("Live migrate running VM to another node.")
icon = 'truck'
def _operation(self, activity, user, system, to_node=None, timeout=120): def _operation(self, activity, user, system, to_node=None, timeout=120):
if not to_node: if not to_node:
...@@ -161,7 +187,7 @@ class MigrateOperation(InstanceOperation): ...@@ -161,7 +187,7 @@ class MigrateOperation(InstanceOperation):
self.instance.deploy_net() self.instance.deploy_net()
register_instance_operation(MigrateOperation) register_operation(MigrateOperation)
class RebootOperation(InstanceOperation): class RebootOperation(InstanceOperation):
...@@ -169,12 +195,30 @@ class RebootOperation(InstanceOperation): ...@@ -169,12 +195,30 @@ class RebootOperation(InstanceOperation):
id = 'reboot' id = 'reboot'
name = _("reboot") name = _("reboot")
description = _("Reboot virtual machine with Ctrl+Alt+Del signal.") description = _("Reboot virtual machine with Ctrl+Alt+Del signal.")
icon = 'refresh'
def _operation(self, activity, user, system, timeout=5): def _operation(self, activity, user, system, timeout=5):
self.instance.reboot_vm(timeout=timeout) self.instance.reboot_vm(timeout=timeout)
register_instance_operation(RebootOperation) register_operation(RebootOperation)
class RemoveInterfaceOperation(InstanceOperation):
activity_code_suffix = 'remove_interface'
id = 'remove_interface'
name = _("remove interface")
description = _("Remove the specified network interface from the VM.")
def _operation(self, activity, user, system, interface):
if self.instance.is_running:
interface.shutdown()
interface.destroy()
interface.delete()
register_operation(RemoveInterfaceOperation)
class ResetOperation(InstanceOperation): class ResetOperation(InstanceOperation):
...@@ -182,11 +226,12 @@ class ResetOperation(InstanceOperation): ...@@ -182,11 +226,12 @@ class ResetOperation(InstanceOperation):
id = 'reset' id = 'reset'
name = _("reset") name = _("reset")
description = _("Reset virtual machine (reset button).") description = _("Reset virtual machine (reset button).")
icon = 'bolt'
def _operation(self, activity, user, system, timeout=5): def _operation(self, activity, user, system, timeout=5):
self.instance.reset_vm(timeout=timeout) self.instance.reset_vm(timeout=timeout)
register_instance_operation(ResetOperation) register_operation(ResetOperation)
class SaveAsTemplateOperation(InstanceOperation): class SaveAsTemplateOperation(InstanceOperation):
...@@ -198,8 +243,19 @@ class SaveAsTemplateOperation(InstanceOperation): ...@@ -198,8 +243,19 @@ class SaveAsTemplateOperation(InstanceOperation):
Template can be shared with groups and users. Template can be shared with groups and users.
Users can instantiate Virtual Machines from Templates. Users can instantiate Virtual Machines from Templates.
""") """)
icon = 'save'
@staticmethod
def _rename(name):
m = search(r" v(\d+)$", name)
if m:
v = int(m.group(1)) + 1
name = search(r"^(.*) v(\d+)$", name).group(1)
else:
v = 1
return "%s v%d" % (name, v)
def _operation(self, activity, name, user, system, timeout=300, def _operation(self, activity, user, system, timeout=300,
with_shutdown=True, **kwargs): with_shutdown=True, **kwargs):
if with_shutdown: if with_shutdown:
try: try:
...@@ -216,7 +272,7 @@ class SaveAsTemplateOperation(InstanceOperation): ...@@ -216,7 +272,7 @@ class SaveAsTemplateOperation(InstanceOperation):
'description': self.instance.description, 'description': self.instance.description,
'lease': self.instance.lease, # Can be problem in new VM 'lease': self.instance.lease, # Can be problem in new VM
'max_ram_size': self.instance.max_ram_size, 'max_ram_size': self.instance.max_ram_size,
'name': name, 'name': self._rename(self.instance.name),
'num_cores': self.instance.num_cores, 'num_cores': self.instance.num_cores,
'owner': user, 'owner': user,
'parent': self.instance.template, # Can be problem 'parent': self.instance.template, # Can be problem
...@@ -255,7 +311,7 @@ class SaveAsTemplateOperation(InstanceOperation): ...@@ -255,7 +311,7 @@ class SaveAsTemplateOperation(InstanceOperation):
return tmpl return tmpl
register_instance_operation(SaveAsTemplateOperation) register_operation(SaveAsTemplateOperation)
class ShutdownOperation(InstanceOperation): class ShutdownOperation(InstanceOperation):
...@@ -263,6 +319,7 @@ class ShutdownOperation(InstanceOperation): ...@@ -263,6 +319,7 @@ class ShutdownOperation(InstanceOperation):
id = 'shutdown' id = 'shutdown'
name = _("shutdown") name = _("shutdown")
description = _("Shutdown virtual machine with ACPI signal.") description = _("Shutdown virtual machine with ACPI signal.")
icon = 'off'
def check_precond(self): def check_precond(self):
super(ShutdownOperation, self).check_precond() super(ShutdownOperation, self).check_precond()
...@@ -284,7 +341,7 @@ class ShutdownOperation(InstanceOperation): ...@@ -284,7 +341,7 @@ class ShutdownOperation(InstanceOperation):
self.instance.yield_vnc_port() self.instance.yield_vnc_port()
register_instance_operation(ShutdownOperation) register_operation(ShutdownOperation)
class ShutOffOperation(InstanceOperation): class ShutOffOperation(InstanceOperation):
...@@ -292,6 +349,7 @@ class ShutOffOperation(InstanceOperation): ...@@ -292,6 +349,7 @@ class ShutOffOperation(InstanceOperation):
id = 'shut_off' id = 'shut_off'
name = _("shut off") name = _("shut off")
description = _("Shut off VM (plug-out).") description = _("Shut off VM (plug-out).")
icon = 'ban-circle'
def on_commit(self, activity): def on_commit(self, activity):
activity.resultant_state = 'STOPPED' activity.resultant_state = 'STOPPED'
...@@ -310,7 +368,7 @@ class ShutOffOperation(InstanceOperation): ...@@ -310,7 +368,7 @@ class ShutOffOperation(InstanceOperation):
self.instance.yield_vnc_port() self.instance.yield_vnc_port()
register_instance_operation(ShutOffOperation) register_operation(ShutOffOperation)
class SleepOperation(InstanceOperation): class SleepOperation(InstanceOperation):
...@@ -318,6 +376,7 @@ class SleepOperation(InstanceOperation): ...@@ -318,6 +376,7 @@ class SleepOperation(InstanceOperation):
id = 'sleep' id = 'sleep'
name = _("sleep") name = _("sleep")
description = _("Suspend virtual machine with memory dump.") description = _("Suspend virtual machine with memory dump.")
icon = 'moon'
def check_precond(self): def check_precond(self):
super(SleepOperation, self).check_precond() super(SleepOperation, self).check_precond()
...@@ -346,7 +405,7 @@ class SleepOperation(InstanceOperation): ...@@ -346,7 +405,7 @@ class SleepOperation(InstanceOperation):
# VNC port needs to be kept # VNC port needs to be kept
register_instance_operation(SleepOperation) register_operation(SleepOperation)
class WakeUpOperation(InstanceOperation): class WakeUpOperation(InstanceOperation):
...@@ -357,6 +416,7 @@ class WakeUpOperation(InstanceOperation): ...@@ -357,6 +416,7 @@ class WakeUpOperation(InstanceOperation):
Power on Virtual Machine and load its memory from dump. Power on Virtual Machine and load its memory from dump.
""") """)
icon = 'sun'
def check_precond(self): def check_precond(self):
super(WakeUpOperation, self).check_precond() super(WakeUpOperation, self).check_precond()
...@@ -386,11 +446,12 @@ class WakeUpOperation(InstanceOperation): ...@@ -386,11 +446,12 @@ class WakeUpOperation(InstanceOperation):
self.instance.renew(which='both', base_activity=activity) self.instance.renew(which='both', base_activity=activity)
register_instance_operation(WakeUpOperation) register_operation(WakeUpOperation)
class NodeOperation(Operation): class NodeOperation(Operation):
async_operation = async_node_operation async_operation = async_node_operation
host_cls = Node
def __init__(self, node): def __init__(self, node):
super(NodeOperation, self).__init__(subject=node) super(NodeOperation, self).__init__(subject=node)
...@@ -413,10 +474,6 @@ class NodeOperation(Operation): ...@@ -413,10 +474,6 @@ class NodeOperation(Operation):
node=self.node, user=user) node=self.node, user=user)
def register_node_operation(op_cls, op_id=None):
return register_operation(Node, op_cls, op_id)
class FlushOperation(NodeOperation): class FlushOperation(NodeOperation):
activity_code_suffix = 'flush' activity_code_suffix = 'flush'
id = 'flush' id = 'flush'
...@@ -430,4 +487,4 @@ class FlushOperation(NodeOperation): ...@@ -430,4 +487,4 @@ class FlushOperation(NodeOperation):
i.migrate() i.migrate()
register_node_operation(FlushOperation) register_operation(FlushOperation)
...@@ -43,6 +43,14 @@ class SaveAsTemplateOperationTestCase(TestCase): ...@@ -43,6 +43,14 @@ class SaveAsTemplateOperationTestCase(TestCase):
def test_operation_registered(self): def test_operation_registered(self):
assert SaveAsTemplateOperation.id in getattr(Instance, op_reg_name) assert SaveAsTemplateOperation.id in getattr(Instance, op_reg_name)
def test_rename(self):
self.assertEqual(SaveAsTemplateOperation._rename("foo"), "foo v1")
self.assertEqual(SaveAsTemplateOperation._rename("foo v2"), "foo v3")
self.assertEqual(SaveAsTemplateOperation._rename("foo v"), "foo v v1")
self.assertEqual(SaveAsTemplateOperation._rename("foo v9"), "foo v10")
self.assertEqual(
SaveAsTemplateOperation._rename("foo v111"), "foo v112")
class ShutdownOperationTestCase(TestCase): class ShutdownOperationTestCase(TestCase):
def test_operation_registered(self): def test_operation_registered(self):
......
...@@ -129,7 +129,8 @@ Install the required Python libraries to the virtual environment:: ...@@ -129,7 +129,8 @@ Install the required Python libraries to the virtual environment::
Sync the database and create a superuser:: Sync the database and create a superuser::
$ circle/manage.py syncdb --migrate --noinput $ circle/manage.py syncdb --all --noinput
$ circle/manage.py migrate --fake
$ circle/manage.py createsuperuser --username=test --email=test@example.org $ circle/manage.py createsuperuser --username=test --email=test@example.org
You can now start the development server:: You can now start the development server::
...@@ -179,4 +180,4 @@ configure vim like we do:: ...@@ -179,4 +180,4 @@ configure vim like we do::
filetype plugin indent on filetype plugin indent on
syntax on syntax on
END END
$ sudo pip install pyflakes rope pep8 mccabe $ sudo pip install pyflakes rope pep8 mccabe
\ No newline at end of file
ignore_invalid_headers on;
server {
listen 443 ssl default;
root /usr/share/nginx/www;
index index.html index.htm;
ssl on;
ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
ssl_session_timeout 5m;
ssl_protocols SSLv3 TLSv1;
ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
ssl_prefer_server_ciphers on;
client_max_body_size 75M;
client_body_buffer_size 512k;
location /media {
alias /home/cloud/circle/circle/static_collected; # your Django project's media files
}
location /static {
alias /home/cloud/circle/circle/static_collected; # your Django project's static files
}
location /doc {
alias /home/cloud/circle-website/_build/html;
}
location / {
uwsgi_pass unix:///tmp/uwsgi.sock;
include /etc/nginx/uwsgi_params; # or the uwsgi_params you installed manually
}
location /vnc/ {
proxy_pass http://localhost:9999;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# WebSocket support (nginx 1.4)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
server {
listen 80 default;
rewrite ^ https://$host/; # permanent;
}
description "CIRCLE django server"
start on runlevel [2345]
stop on runlevel [!2345]
respawn
respawn limit 30 30
setgid cloud
setuid cloud
script
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec /home/cloud/.virtualenvs/circle/bin/uwsgi --chdir=/home/cloud/circle/circle -H /home/cloud/.virtualenvs/circle --socket /tmp/uwsgi.sock --wsgi-file circle/wsgi.py --chmod-socket=666
end script
...@@ -3,3 +3,4 @@ ...@@ -3,3 +3,4 @@
-r base.txt -r base.txt
gunicorn==0.17.4 gunicorn==0.17.4
uWSGI==2.0.3
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