Commit dedcfc62 by Kálmán Viktor

Merge branch 'master' into issue-vm-detail-fixes

Conflicts:
	circle/dashboard/tests/test_views.py
parents 1a890736 5d753e30
......@@ -25,3 +25,13 @@ coverage.xml
# Gettext object file:
*.mo
# saml
circle/attribute-maps
circle/remote_metadata.xml
circle/*.key
circle/*.pem
# collected static files:
circle/static
circle/static_collected
......@@ -125,11 +125,6 @@ STATIC_ROOT = normpath(join(SITE_ROOT, 'static_collected'))
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
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
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
......
......@@ -13,6 +13,7 @@ class Operation(object):
"""
async_queue = 'localhost.man'
required_perms = ()
do_not_call_in_templates = True
def __call__(self, **kwargs):
return self.call(**kwargs)
......@@ -28,11 +29,11 @@ class Operation(object):
def __prelude(self, kwargs):
"""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)
parent_activity = kwargs.pop('parent_activity', None)
if not skip_checks:
if not skip_auth_check:
self.check_auth(user)
self.check_precond()
return self.create_activity(parent=parent_activity, user=user)
......@@ -42,7 +43,7 @@ class Operation(object):
"""
with activity_context(activity, on_abort=self.on_abort,
on_commit=self.on_commit):
return self._operation(activity, user, **kwargs)
return self._operation(activity=activity, user=user, **kwargs)
def _operation(self, activity, user, system, **kwargs):
"""This method is the operation's particular implementation.
......@@ -127,15 +128,46 @@ class OperatedMixin(object):
raise AttributeError("%r object has no attribute %r" %
(self.__class__.__name__, name))
def get_available_operations(self, user):
"""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(target_cls, op_cls, op_id=None):
def register_operation(op_cls, op_id=None, target_cls=None):
"""Register the specified operation with the target class.
You can optionally specify an ID to be used for the registration;
otherwise, the operation class' 'id' attribute will be used.
"""
if op_id is None:
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):
raise TypeError("%r is not a subclass of %r" %
......
from __future__ import absolute_import
from datetime import timedelta
from django.contrib.auth.models import User
......
from __future__ import absolute_import
from itertools import chain
from logging import getLogger
......
......@@ -2,22 +2,21 @@
$(function() {
/* vm migrate */
$('.vm-migrate').click(function(e) {
var icon = $(this).children("i");
var vm = $(this).data("vm-pk");
icon.removeClass("icon-truck").addClass("icon-spinner icon-spin");
/* vm operations */
$('#ops').on('click', '.operation.btn', function(e) {
var icon = $(this).children("i").addClass('icon-spinner icon-spin');
$.ajax({
type: 'GET',
url: '/dashboard/vm/' + vm + '/migrate/',
url: $(this).attr('href'),
success: function(data) {
icon.addClass("icon-truck").removeClass("icon-spinner icon-spin");
icon.removeClass("icon-spinner icon-spin");
$('body').append(data);
$('#create-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove();
$('#confirmation-modal').modal('show');
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove();
});
$('#vm-migrate-node-list li').click(function(e) {
var li = $(this).closest('li');
if (li.find('input').attr('disabled'))
......
......@@ -277,6 +277,7 @@ function checkNewActivity(only_status, runs) {
success: function(data) {
if(!only_status) {
$("#activity-timeline").html(data['activities']);
$("#ops").html(data['ops']);
$("[title]").tooltip();
}
......
from __future__ import absolute_import
from django.contrib.auth.models import Group, User
from django_tables2 import Table, A
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 sizefieldtags %}
<form method="POST" action="{% url "dashboard.views.vm-migrate" pk=vm.pk %}">
{% csrf_token %}
<ul id="vm-migrate-node-list">
{% with current=vm.node.pk selected=vm.select_node.pk %}
{% block question %}
<p>
{% blocktrans with obj=object op=op.name %}
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 %}
<li class="panel panel-default"><div class="panel-body">
<label for="migrate-to-{{n.pk}}">
......@@ -22,5 +31,4 @@
{% endfor %}
{% endwith %}
</ul>
<button type="submit" class="btn btn-primary btn-sm"><i class="icon-truck"></i> Migrate</button>
</form>
{% endblock %}
......@@ -16,7 +16,7 @@
<div class="list-group" id="node-list-view">
{% for i in nodes %}
<a href="{% url "dashboard.views.node-detail" pk=i.pk %}" class="list-group-item">
<i class="icon-{% if i.enabled == True %}play-sign{% else %}pause{% endif %}"></i> {{ i.name }} <div class="pull-right"><i class="icon-star text-primary" title="Mark as favorite."></i></div>
<i class="icon-{% if i.enabled == True %}play-sign{% else %}pause{% endif %}"></i> {{ i.name }}
</a>
{% endfor %}
<div href="#" class="list-group-item list-group-footer">
......
{% 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>
......@@ -6,54 +6,8 @@
{% block content %}
<div class="body-content">
<div class="page-header">
<div class="pull-right" style="padding-top: 15px;">
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% 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 %}">
{% 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;" 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 class="pull-right" style="padding-top: 15px;" id="ops">
{% include "dashboard/vm-detail/_operations.html" %}
</div>
<h1>
<div id="vm-details-rename" class="vm-details-home-rename-form-div">
......@@ -72,46 +26,6 @@
</div>
<small>{{ instance.primary_host.get_fqdn }}</small>
</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>
<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
from mock import patch, MagicMock
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 dashboard.views import InstanceActivityDetail, InstanceActivity
from ..views import InstanceActivityDetail, InstanceActivity
from ..views import vm_ops, Instance
class ViewUserTestCase(unittest.TestCase):
......@@ -36,6 +37,82 @@ class ViewUserTestCase(unittest.TestCase):
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):
''' FakeRequestFactory, FakeMessages and FakeRequestContext are good for
mocking out django views; they are MUCH faster than the Django test client.
......@@ -48,12 +125,12 @@ def FakeRequestFactory(*args, **kwargs):
request = HttpRequest()
request.user = user
request.session = kwargs.get('session', {})
if kwargs.get('POST'):
if kwargs.get('POST') is not None:
request.method = 'POST'
request.POST = kwargs.get('POST')
else:
request.method = 'GET'
request.POST = kwargs.get('GET', {})
request.GET = kwargs.get('GET', {})
return request
......
import json
from unittest import skip
from django.test import TestCase
from django.test.client import Client
from django.contrib.auth.models import User, Group
......@@ -279,6 +280,7 @@ class VmDetailTest(LoginMixin, TestCase):
self.assertEqual(response.status_code, 403)
self.assertEqual(disks, inst.disks.count())
@skip("until fix merged")
def test_permitted_vm_disk_add(self):
c = Client()
self.login(c, "user1")
......@@ -495,9 +497,11 @@ class VmDetailTest(LoginMixin, TestCase):
mock_method.side_effect = inst.wake_up
inst.manual_state_change('RUNNING')
inst.set_level(self.u2, 'owner')
self.assertRaises(inst.WrongStateError, c.post,
"/dashboard/vm/1/", {'wake_up': True})
self.assertEqual(inst.status, 'RUNNING')
with patch('dashboard.views.messages') as msg:
c.post("/dashboard/vm/1/op/wake_up/")
assert msg.error.called
inst = Instance.objects.get(pk=1)
self.assertEqual(inst.status, 'RUNNING') # mocked anyway
assert mock_method.called
def test_permitted_wake_up(self):
......@@ -511,7 +515,9 @@ class VmDetailTest(LoginMixin, TestCase):
inst.get_remote_queue_name = Mock(return_value='test')
inst.manual_state_change('SUSPENDED')
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(inst.status, 'RUNNING')
assert new_wake_up.called
......@@ -523,8 +529,11 @@ class VmDetailTest(LoginMixin, TestCase):
inst = Instance.objects.get(pk=1)
inst.manual_state_change('SUSPENDED')
inst.set_level(self.u2, 'user')
response = c.post("/dashboard/vm/1/", {'wake_up': True})
self.assertEqual(response.status_code, 403)
with patch('dashboard.views.messages') as msg:
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')
def test_non_existing_template_get(self):
......
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 .views import (
......@@ -34,7 +35,7 @@ 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+)/op/', include('dashboard.vm.urls')),
url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(),
name='dashboard.views.remove-port'),
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
import json
import logging
import re
from datetime import datetime
import requests
from django.conf import settings
......@@ -47,7 +46,7 @@ from vm.models import (
)
from storage.models import Disk
from firewall.models import Vlan, Host, Rule
from dashboard.models import Favourite, Profile
from .models import Favourite, Profile
logger = logging.getLogger(__name__)
......@@ -203,7 +202,8 @@ class VmDetailView(CheckedDetailView):
context.update({
'graphite_enabled': VmGraphView.get_graphite_url() is not None,
'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
......@@ -243,14 +243,6 @@ class VmDetailView(CheckedDetailView):
'to_remove': self.__remove_tag,
'port': self.__add_port,
'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():
if request.POST.get(k) is not None:
......@@ -437,75 +429,132 @@ class VmDetailView(CheckedDetailView):
return redirect("%s#network" % reverse_lazy(
"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")
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())
class OperationView(DetailView):
def __shut_down(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
template_name = 'dashboard/operate.html'
self.object.shutdown.async(user=request.user)
return redirect("%s#activity" % self.object.get_absolute_url())
@property
def name(self):
return self.get_op().name
def __sleep(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
@property
def description(self):
return self.get_op().description
self.object.sleep.async(user=request.user)
return redirect("%s#activity" % self.object.get_absolute_url())
@classmethod
def get_urlname(cls):
return 'dashboard.vm.op.%s' % cls.op
def __wake_up(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
def get_url(self):
return reverse(self.get_urlname(), args=(self.get_object().pk, ))
self.object.wake_up.async(user=request.user)
return redirect("%s#activity" % self.object.get_absolute_url())
def get_wrapper_template_name(self):
if self.request.is_ajax():
return 'dashboard/_modal.html'
else:
return 'dashboard/_base.html'
def __deploy(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
@classmethod
def get_op_by_object(cls, obj):
return getattr(obj, cls.op)
self.object.deploy.async(user=request.user)
return redirect("%s#activity" % self.object.get_absolute_url())
def get_op(self):
if not hasattr(self, '_opobj'):
setattr(self, '_opobj', getattr(self.get_object(), self.op))
return self._opobj
def __reset(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
def get_context_data(self, **kwargs):
ctx = super(OperationView, self).get_context_data(**kwargs)
ctx['op'] = self.get_op()
ctx['url'] = self.request.path
return ctx
self.object.reset.async(user=request.user)
return redirect("%s#activity" % self.object.get_absolute_url())
def get(self, request, *args, **kwargs):
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()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
self.object.reboot.async(user=request.user)
if extra is None:
extra = {}
try:
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())
def __shut_off(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
@classmethod
def factory(cls, op, icon='cog'):
return type(str(cls.__name__ + op),
(cls, ), {'op': op, 'icon': icon})
self.object.shut_off.async(user=request.user)
return redirect("%s#activity" % self.object.get_absolute_url())
@classmethod
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):
......@@ -1536,8 +1585,11 @@ class VmMassDelete(LoginRequiredMixin, View):
raise PermissionDenied() # no need for rollback or proper
# error message, this can't
# normally happen.
try:
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 "
"deleted: %s!") % u', '.join(names)
......@@ -1612,23 +1664,28 @@ def vm_activity(request, pk):
raise PermissionDenied()
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['status'] = instance.status
response['icon'] = instance.get_status_icon()
if only_status == "false": # instance activity
context = {
'instance': instance,
'activities': InstanceActivity.objects.filter(
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",
RequestContext(request, context),
)
response['activities'] = activities
response['ops'] = render_to_string(
"dashboard/vm-detail/_operations.html",
RequestContext(request, context),
)
return HttpResponse(
json.dumps(response),
......@@ -2038,40 +2095,6 @@ class NotificationView(LoginRequiredMixin, TemplateView):
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):
authentication_form = CircleAuthenticationForm
extra_context = {
......
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()))
......@@ -2,7 +2,7 @@
from django.contrib import admin
from firewall.models import (Rule, Host, Vlan, Group, VlanGroup, Firewall,
Domain, Record, Blacklist,
Domain, Record, BlacklistItem,
SwitchPort, EthernetDevice)
from django import contrib
......@@ -38,7 +38,7 @@ class HostAdmin(admin.ModelAdmin):
class HostInline(contrib.admin.TabularInline):
model = Host
fields = ('hostname', 'ipv4', 'ipv6', 'pub_ipv4', 'mac', 'shared_ip',
fields = ('hostname', 'ipv4', 'ipv6', 'external_ipv4', 'mac', 'shared_ip',
'owner', 'reverse')
......@@ -114,8 +114,8 @@ class RecordAdmin(admin.ModelAdmin):
list_display = ('name', 'type', 'address', 'ttl', 'host', 'owner')
class BlacklistAdmin(admin.ModelAdmin):
list_display = ('ipv4', 'reason', 'created_at', 'modified_at')
class BlacklistItemAdmin(admin.ModelAdmin):
list_display = ('ipv4', 'type', 'reason', 'created_at', 'modified_at')
class SwitchPortAdmin(admin.ModelAdmin):
......@@ -133,6 +133,6 @@ admin.site.register(VlanGroup)
admin.site.register(Firewall, FirewallAdmin)
admin.site.register(Domain, DomainAdmin)
admin.site.register(Record, RecordAdmin)
admin.site.register(Blacklist, BlacklistAdmin)
admin.site.register(BlacklistItem, BlacklistItemAdmin)
admin.site.register(SwitchPort)
admin.site.register(EthernetDevice, EthernetDeviceAdmin)
......@@ -5,7 +5,8 @@ from netaddr import IPAddress, AddrFormatError
from datetime import datetime, timedelta
from itertools import product
from .models import (Host, Rule, Vlan, Domain, Record, Blacklist, SwitchPort)
from .models import (Host, Rule, Vlan, Domain, Record, BlacklistItem,
SwitchPort)
from .iptables import IptRule, IptChain
import django.conf
from django.db.models import Q
......@@ -136,7 +137,7 @@ def ipset():
week = datetime.now() - timedelta(days=2)
filter_ban = (Q(type='tempban', modified_at__gte=week) |
Q(type='permban'))
return Blacklist.objects.filter(filter_ban).values('ipv4', 'reason')
return BlacklistItem.objects.filter(filter_ban).values('ipv4', 'reason')
def ipv6_to_octal(ipv6):
......
......@@ -89,7 +89,7 @@ class IptChain(object):
self.rules.add(rule)
def sort(self):
return sorted(list(self.rules))
return sorted(list(self.rules), reverse=True)
def __len__(self):
return len(self.rules)
......
......@@ -32,8 +32,8 @@ class Migration(SchemaMigration):
if rule.nat:
# swap
tmp = rule.dport
# rule.dport = rule.nat_external_port
# rule.nat_external_port = tmp
rule.dport = rule.nat_external_port
rule.nat_external_port = tmp
if rule.direction == '0':
rule.direction = 'out'
elif rule.direction == '1':
......
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
db.rename_table(u'firewall_blacklist', 'firewall_blacklistitem')
def backwards(self, orm):
db.rename_table(u'firewall_blacklistitem', 'firewall_blacklist')
models = {
u'acl.level': {
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Level'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'weight': ('django.db.models.fields.IntegerField', [], {'null': 'True'})
},
u'acl.objectlevel': {
'Meta': {'unique_together': "(('content_type', 'object_id', 'level'),)", 'object_name': 'ObjectLevel'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['acl.Level']"}),
'object_id': ('django.db.models.fields.IntegerField', [], {}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False'})
},
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'firewall.blacklistitem': {
'Meta': {'object_name': 'BlacklistItem'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']", 'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv4': ('django.db.models.fields.GenericIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'reason': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'snort_message': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'default': "'tempban'", 'max_length': '10'})
},
u'firewall.domain': {
'Meta': {'object_name': 'Domain'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'ttl': ('django.db.models.fields.IntegerField', [], {'default': '600'})
},
u'firewall.ethernetdevice': {
'Meta': {'object_name': 'EthernetDevice'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'switch_port': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ethernet_devices'", 'to': u"orm['firewall.SwitchPort']"})
},
u'firewall.firewall': {
'Meta': {'object_name': 'Firewall'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'})
},
u'firewall.group': {
'Meta': {'object_name': 'Group'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'firewall.host': {
'Meta': {'ordering': "('normalized_hostname', 'vlan')", 'unique_together': "(('hostname', 'vlan'),)", 'object_name': 'Host'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'external_ipv4': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Group']", 'null': 'True', 'blank': 'True'}),
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv4': ('firewall.fields.IPAddressField', [], {'unique': 'True', 'max_length': '100'}),
'ipv6': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'mac': ('firewall.fields.MACAddressField', [], {'unique': 'True', 'max_length': '17'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'normalized_hostname': ('common.models.HumanSortField', [], {'default': "''", 'maximum_number_length': '4', 'max_length': '80', 'monitor': "'hostname'", 'blank': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'reverse': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
'shared_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Vlan']"})
},
u'firewall.record': {
'Meta': {'ordering': "('domain', 'name')", 'object_name': 'Record'},
'address': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Domain']"}),
'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']", 'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'ttl': ('django.db.models.fields.IntegerField', [], {'default': '600'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '6'})
},
u'firewall.rule': {
'Meta': {'ordering': "('direction', 'proto', 'sport', 'dport', 'nat_external_port', 'host')", 'object_name': 'Rule'},
'action': ('django.db.models.fields.CharField', [], {'default': "'drop'", 'max_length': '10'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'direction': ('django.db.models.fields.CharField', [], {'max_length': '3'}),
'dport': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'extra': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'firewall': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'rules'", 'null': 'True', 'to': u"orm['firewall.Firewall']"}),
'foreign_network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ForeignRules'", 'to': u"orm['firewall.VlanGroup']"}),
'host': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'rules'", 'null': 'True', 'to': u"orm['firewall.Host']"}),
'hostgroup': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'rules'", 'null': 'True', 'to': u"orm['firewall.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'nat': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'nat_external_ipv4': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'nat_external_port': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'proto': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
'sport': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'rules'", 'null': 'True', 'to': u"orm['firewall.Vlan']"}),
'vlangroup': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'rules'", 'null': 'True', 'to': u"orm['firewall.VlanGroup']"}),
'weight': ('django.db.models.fields.IntegerField', [], {'default': '30000'})
},
u'firewall.switchport': {
'Meta': {'object_name': 'SwitchPort'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'tagged_vlans': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tagged_ports'", 'null': 'True', 'to': u"orm['firewall.VlanGroup']"}),
'untagged_vlan': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'untagged_ports'", 'to': u"orm['firewall.Vlan']"})
},
u'firewall.vlan': {
'Meta': {'object_name': 'Vlan'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'dhcp_pool': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Domain']"}),
'host_ipv6_prefixlen': ('django.db.models.fields.IntegerField', [], {'default': '112'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv6_template': ('django.db.models.fields.TextField', [], {'default': "'2001:738:2001:4031:%(b)d:%(c)d:%(d)d:0'"}),
'managed': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'network4': ('firewall.fields.IPNetworkField', [], {'max_length': '100'}),
'network6': ('firewall.fields.IPNetworkField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'network_type': ('django.db.models.fields.CharField', [], {'default': "'portforward'", 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'reverse_domain': ('django.db.models.fields.TextField', [], {'default': "'%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa'"}),
'snat_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
'snat_to': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Vlan']", 'null': 'True', 'blank': 'True'}),
'vid': ('django.db.models.fields.IntegerField', [], {'unique': 'True'})
},
u'firewall.vlangroup': {
'Meta': {'object_name': 'VlanGroup'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'vlans': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Vlan']", 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['firewall']
......@@ -923,7 +923,7 @@ class EthernetDevice(models.Model):
return self.name
class Blacklist(models.Model):
class BlacklistItem(models.Model):
CHOICES_type = (('permban', 'permanent ban'), ('tempban', 'temporary ban'),
('whitelist', 'whitelist'), ('tempwhite', 'tempwhite'))
ipv4 = models.GenericIPAddressField(protocol='ipv4', unique=True)
......@@ -945,11 +945,15 @@ class Blacklist(models.Model):
def save(self, *args, **kwargs):
self.full_clean()
super(Blacklist, self).save(*args, **kwargs)
super(BlacklistItem, self).save(*args, **kwargs)
def __unicode__(self):
return self.ipv4
class Meta(object):
verbose_name = _('blacklist item')
verbose_name_plural = _('blacklist')
@models.permalink
def get_absolute_url(self):
return ('network.blacklist', None, {'pk': self.pk})
......@@ -959,7 +963,7 @@ def send_task(sender, instance, created=False, **kwargs):
reloadtask.apply_async(queue='localhost.man', args=[sender.__name__])
for sender in [Host, Rule, Domain, Record, Vlan, Firewall, Group, Blacklist,
SwitchPort, EthernetDevice]:
for sender in [Host, Rule, Domain, Record, Vlan, Firewall, Group,
BlacklistItem, SwitchPort, EthernetDevice]:
post_save.connect(send_task, sender=sender)
post_delete.connect(send_task, sender=sender)
......@@ -11,7 +11,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from .tasks.local_tasks import reloadtask
from .models import Blacklist, Host
from .models import BlacklistItem, Host
def reload_firewall(request):
......@@ -38,7 +38,7 @@ def firewall_api(request):
raise Exception(_("Wrong password."))
if command == "blacklist":
obj, created = Blacklist.objects.get_or_create(ipv4=data["ip"])
obj, created = BlacklistItem.objects.get_or_create(ipv4=data["ip"])
obj.reason = data["reason"]
obj.snort_message = data["snort_message"]
if created:
......
......@@ -5,7 +5,7 @@ from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Fieldset, Div, Submit, BaseInput
from crispy_forms.bootstrap import FormActions
from firewall.models import (Host, Vlan, Domain, Group, Record, Blacklist,
from firewall.models import (Host, Vlan, Domain, Group, Record, BlacklistItem,
Rule, VlanGroup, SwitchPort)
......@@ -26,7 +26,7 @@ class LinkButton(BaseInput):
super(LinkButton, self).__init__(name, text, *args, **kwargs)
class BlacklistForm(ModelForm):
class BlacklistItemForm(ModelForm):
helper = FormHelper()
helper.layout = Layout(
Div(
......@@ -45,7 +45,7 @@ class BlacklistForm(ModelForm):
)
class Meta:
model = Blacklist
model = BlacklistItem
class DomainForm(ModelForm):
......
......@@ -4,7 +4,7 @@ from django_tables2.columns import LinkColumn, TemplateColumn
from firewall.models import Host, Vlan, Domain, Group, Record, Rule, SwitchPort
class BlacklistTable(Table):
class BlacklistItemTable(Table):
ipv4 = LinkColumn('network.blacklist', args=[A('pk')])
class Meta:
......
......@@ -7,7 +7,7 @@
{% block content %}
<div class="page-header">
<h2>{% trans "Create a blacklist" %}</h2>
<h2>{% trans "Create a blacklist item" %}</h2>
</div>
<div class="row">
<div class="col-sm-4">
......
......@@ -7,7 +7,7 @@
{% block content %}
<div class="page-header">
<a href="{% url "network.blacklist_delete" pk=blacklist_pk %}" class="btn btn-danger pull-right"><i class="icon-remove-sign"></i> {% trans "Delete this blaclist" %}</a>
<a href="{% url "network.blacklist_delete" pk=blacklist_pk %}" class="btn btn-danger pull-right"><i class="icon-remove-sign"></i> {% trans "Delete this blaclist item" %}</a>
<h2>{{ form.ipv4.value }} <small>{{ form.type.value }}</small></h2>
</div>
<div class="row">
......
......@@ -6,8 +6,8 @@
{% block content %}
<div class="page-header">
<a href="{% url "network.blacklist_create" %}" class="btn btn-success pull-right"><i class="icon-plus-sign"></i> {% trans "Create a new blacklist" %}</a>
<h1>{% trans "Blacklists" %} <small></small></h1>
<a href="{% url "network.blacklist_create" %}" class="btn btn-success pull-right"><i class="icon-plus-sign"></i> {% trans "Create a new blacklist item" %}</a>
<h1>{% trans "Blacklist" %} <small></small></h1>
</div>
<div class="table-responsive">
......
......@@ -14,7 +14,7 @@
{% trans "Records" as t %}
{% include "network/menu-item.html" with href=u text=t %}
{% url "network.blacklist_list" as u %}
{% trans "Blacklists" as t %}
{% trans "Blacklist" as t %}
{% include "network/menu-item.html" with href=u text=t %}
{% url "network.rule_list" as u %}
{% trans "Rules" as t %}
......@@ -43,4 +43,4 @@
{# <li><a href="/firewalls/">{% trans "Firewalls" %}</a></li> #}
{# <li><a href="/domains/">{% trans "Domains" %}</a></li> #}
{# <li><a href="/records/">{% trans "DNS records" %}</a></li> #}
{# <li><a href="/blacklists/">{% trans "Blacklists" %}</a></li> #}
{# <li><a href="/blacklist/">{% trans "Blacklist" %}</a></li> #}
......@@ -22,13 +22,13 @@ js_info_dict = {
urlpatterns = patterns(
'',
url('^$', IndexView.as_view(), name='network.index'),
url('^blacklists/$', BlacklistList.as_view(),
url('^blacklist/$', BlacklistList.as_view(),
name='network.blacklist_list'),
url('^blacklists/create$', BlacklistCreate.as_view(),
url('^blacklist/create$', BlacklistCreate.as_view(),
name='network.blacklist_create'),
url('^blacklists/(?P<pk>\d+)/$', BlacklistDetail.as_view(),
url('^blacklist/(?P<pk>\d+)/$', BlacklistDetail.as_view(),
name='network.blacklist'),
url('^blacklists/delete/(?P<pk>\d+)/$', BlacklistDelete.as_view(),
url('^blacklist/delete/(?P<pk>\d+)/$', BlacklistDelete.as_view(),
name="network.blacklist_delete"),
url('^domains/$', DomainList.as_view(), name='network.domain_list'),
url('^domains/create$', DomainCreate.as_view(),
......
......@@ -6,15 +6,15 @@ from django.http import HttpResponse
from django_tables2 import SingleTableView
from firewall.models import (Host, Vlan, Domain, Group, Record, Blacklist,
from firewall.models import (Host, Vlan, Domain, Group, Record, BlacklistItem,
Rule, VlanGroup, SwitchPort, EthernetDevice)
from vm.models import Interface
from .tables import (HostTable, VlanTable, SmallHostTable, DomainTable,
GroupTable, RecordTable, BlacklistTable, RuleTable,
GroupTable, RecordTable, BlacklistItemTable, RuleTable,
VlanGroupTable, SmallRuleTable, SmallGroupRuleTable,
SmallRecordTable, SwitchPortTable)
from .forms import (HostForm, VlanForm, DomainForm, GroupForm, RecordForm,
BlacklistForm, RuleForm, VlanGroupForm, SwitchPortForm)
BlacklistItemForm, RuleForm, VlanGroupForm, SwitchPortForm)
from django.contrib import messages
from django.views.generic.edit import FormMixin
......@@ -51,7 +51,8 @@ class IndexView(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
context = super(IndexView, self).get_context_data(**kwargs)
size = 13
blacklists = Blacklist.objects.all().order_by('-modified_at')[:size]
blacklists = BlacklistItem.objects.all().order_by(
'-modified_at')[:size]
domains = Domain.objects.all().order_by('-modified_at')[:size]
groups = Group.objects.all().order_by('-modified_at')[:size]
hosts = Host.objects.all().order_by('-modified_at')[:size]
......@@ -80,18 +81,18 @@ class IndexView(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
class BlacklistList(LoginRequiredMixin, SuperuserRequiredMixin,
SingleTableView):
model = Blacklist
table_class = BlacklistTable
model = BlacklistItem
table_class = BlacklistItemTable
template_name = "network/blacklist-list.html"
table_pagination = False
class BlacklistDetail(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, UpdateView):
model = Blacklist
model = BlacklistItem
template_name = "network/blacklist-edit.html"
form_class = BlacklistForm
success_message = _(u'Successfully modified blacklist '
form_class = BlacklistItemForm
success_message = _(u'Successfully modified blacklist item'
'%(ipv4)s - %(type)s!')
def get_success_url(self):
......@@ -106,22 +107,22 @@ class BlacklistDetail(LoginRequiredMixin, SuperuserRequiredMixin,
class BlacklistCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView):
model = Blacklist
model = BlacklistItem
template_name = "network/blacklist-create.html"
form_class = BlacklistForm
success_message = _(u'Successfully created blacklist '
form_class = BlacklistItemForm
success_message = _(u'Successfully created blacklist item '
'%(ipv4)s - %(type)s!')
class BlacklistDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
model = Blacklist
model = BlacklistItem
template_name = "network/confirm/base_delete.html"
def get_context_data(self, **kwargs):
""" display more information about the object """
context = super(BlacklistDelete, self).get_context_data(**kwargs)
if 'pk' in self.kwargs:
to_delete = Blacklist.objects.get(pk=self.kwargs['pk'])
to_delete = BlacklistItem.objects.get(pk=self.kwargs['pk'])
context['object'] = "%s - %s - %s" % (to_delete.ipv4,
to_delete.reason,
to_delete.type)
......
......@@ -5,7 +5,8 @@ from .models import Disk, DataStore, DiskActivity
class DiskAdmin(contrib.admin.ModelAdmin):
list_display = ('name', 'datastore')
list_display = ('id', 'name', 'base', 'type', 'datastore')
ordering = ('-id', )
class DataStoreAdmin(contrib.admin.ModelAdmin):
......
......@@ -122,7 +122,7 @@ class Disk(AclBase, TimeStampedModel):
self.disk = disk
@property
def ready(self):
def is_ready(self):
""" Returns True if the disk is physically ready on the storage.
It needs at least 1 successfull deploy action.
......@@ -310,7 +310,7 @@ class Disk(AclBase, TimeStampedModel):
self.destroyed = None
self.save()
if self.ready:
if self.is_ready:
return True
with disk_activity(code_suffix='deploy', disk=self,
task_uuid=task_uuid, user=user) as act:
......@@ -355,7 +355,14 @@ class Disk(AclBase, TimeStampedModel):
return disk
@classmethod
def create_empty(cls, instance=None, user=None, **kwargs):
def create_empty_async(cls, instance=None, user=None, **kwargs):
"""Execute deploy asynchronously.
"""
return local_tasks.create_empty.apply_async(
args=[cls, instance, user, kwargs], queue="localhost.man")
@classmethod
def create_empty(cls, instance=None, user=None, task_uuid=None, **kwargs):
"""Create empty Disk object.
:param instance: Instance or template attach the Disk to.
......@@ -366,6 +373,7 @@ class Disk(AclBase, TimeStampedModel):
:return: Disk object without a real image, to be .deploy()ed later.
"""
disk = Disk.create(instance, user, **kwargs)
disk.deploy(user=user, task_uuid=task_uuid)
return disk
@classmethod
......@@ -466,19 +474,72 @@ class Disk(AclBase, TimeStampedModel):
local_tasks.restore.apply_async(args=[self, user],
queue='localhost.man')
def save_as_async(self, disk, task_uuid=None, timeout=300, user=None):
return local_tasks.save_as.apply_async(args=[disk, timeout, user],
def clone_async(self, new_disk=None, timeout=300, user=None):
"""Clone a Disk to another Disk
:param new_disk: optional, the new Disk object to clone in
:type new_disk: storage.models.Disk
:param user: Creator of the disk.
:type user: django.contrib.auth.User
:return: AsyncResult
"""
return local_tasks.clone.apply_async(args=[self, new_disk,
timeout, user],
queue="localhost.man")
def clone(self, disk=None, user=None, task_uuid=None, timeout=300):
"""Cloning Disk into another Disk.
The Disk.type can'T be snapshot.
:param new_disk: optional, the new Disk object to clone in
:type new_disk: storage.models.Disk
:param user: Creator of the disk.
:type user: django.contrib.auth.User
:return: the cloned Disk object.
"""
banned_types = ['qcow2-snap']
if self.type in banned_types:
raise self.WrongDiskTypeError(self.type)
if self.is_in_use:
raise self.DiskInUseError(self)
if not self.is_ready:
raise self.DiskIsNotReady(self)
if not disk:
base = None
if self.type == "iso":
base = self
disk = Disk.create(datastore=self.datastore,
name=self.name, size=self.size,
type=self.type, base=base)
with disk_activity(code_suffix="clone", disk=self,
user=user, task_uuid=task_uuid):
with disk_activity(code_suffix="deploy", disk=disk,
user=user, task_uuid=task_uuid):
queue_name = self.get_remote_queue_name('storage')
remote_tasks.merge.apply_async(args=[self.get_disk_desc(),
disk.get_disk_desc()],
queue=queue_name
).get() # Timeout
return disk
def save_as(self, user=None, task_uuid=None, timeout=300):
"""Save VM as template.
Based on disk type:
qcow2-norm, qcow2-snap --> qcow2-norm
iso --> iso (with base)
VM must be in STOPPED state to perform this action.
The timeout parameter is not used now.
"""
mapping = {
'qcow2-snap': ('qcow2-norm', self.base),
'qcow2-norm': ('qcow2-norm', self),
'qcow2-snap': ('qcow2-norm', None),
'qcow2-norm': ('qcow2-norm', None),
'iso': ("iso", self),
}
if self.type not in mapping.keys():
raise self.WrongDiskTypeError(self.type)
......@@ -486,7 +547,7 @@ class Disk(AclBase, TimeStampedModel):
if self.is_in_use:
raise self.DiskInUseError(self)
if not self.ready:
if not self.is_ready:
raise self.DiskIsNotReady(self)
# from this point on, the caller has to guarantee that the disk is not
......@@ -494,7 +555,8 @@ class Disk(AclBase, TimeStampedModel):
new_type, new_base = mapping[self.type]
disk = Disk.create(base=new_base, datastore=self.datastore,
disk = Disk.create(datastore=self.datastore,
base=new_base,
name=self.name, size=self.size,
type=new_type)
......
......@@ -10,16 +10,14 @@ def check_queue(storage, queue_id):
drivers = ['storage', 'download']
worker_list = [storage + "." + d for d in drivers]
queue_name = storage + "." + queue_id
# v is List of List of queues dict
active_queues = celery.control.inspect(worker_list).active_queues()
if active_queues is not None:
node_workers = [v for k, v in active_queues.iteritems()]
for worker in node_workers:
for queue in worker:
if queue['name'] == queue_name:
return True
if active_queues is None:
return False
queue_names = (queue['name'] for worker in active_queues.itervalues()
for queue in worker)
return queue_name in queue_names
@celery.task
def save_as(disk, timeout, user):
......@@ -28,6 +26,12 @@ def save_as(disk, timeout, user):
@celery.task
def clone(disk, new_disk, timeout, user):
disk.clone(task_uuid=save_as.request.id, user=user,
disk=new_disk, timeout=timeout)
@celery.task
def deploy(disk, user):
disk.deploy(task_uuid=deploy.request.id, user=user)
......@@ -57,6 +61,7 @@ create_from_url = CreateFromURLTask()
@celery.task
def create_empty(Disk, instance, params, user):
Disk.create_empty(instance, params, user,
task_uuid=create_empty.request.id)
def create_empty(Disk, instance, user, params):
Disk.create_empty(instance, user,
task_uuid=create_empty.request.id,
**params)
......@@ -10,7 +10,6 @@ logger = logging.getLogger(__name__)
def garbage_collector(timeout=15):
""" Garbage collector for disk images.
Moves 1 day old deleted images to trash folder.
If there is not enough free space on datastore (default 10%)
deletes oldest images from trash.
......
......@@ -78,7 +78,7 @@ class DiskTestCase(TestCase):
d.DiskIsNotReady = MockException
d.type = "qcow2-norm"
d.is_in_use = False
d.ready = False
d.is_ready = False
with self.assertRaises(MockException):
Disk.save_as(d)
......@@ -89,4 +89,4 @@ class DiskTestCase(TestCase):
def test_undeployed_disk_ready(self):
d = self._disk()
assert not d.ready
assert not d.is_ready
......@@ -22,7 +22,6 @@ from taggit.managers import TaggableManager
from acl.models import AclBase
from common.operations import OperatedMixin
from storage.models import Disk
from ..tasks import vm_tasks, agent_tasks
from .activity import (ActivityInProgressError, instance_activity,
InstanceActivity)
......@@ -117,8 +116,9 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
description = TextField(verbose_name=_('description'), blank=True)
parent = ForeignKey('self', null=True, blank=True,
verbose_name=_('parent template'),
on_delete=SET_NULL,
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',
help_text=_('Disks which are to be mounted.'))
owner = ForeignKey(User)
......@@ -151,6 +151,10 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
else:
return 'linux'
@property
def is_ready(self):
return all(disk.is_ready for disk in self.disks)
def save(self, *args, **kwargs):
is_new = getattr(self, "pk", None) is None
super(InstanceTemplate, self).save(*args, **kwargs)
......@@ -206,7 +210,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
related_name='instance_set',
help_text=_("Current hypervisor of this instance."),
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."),
verbose_name=_('disks'))
vnc_port = IntegerField(blank=True, default=None, null=True,
......
......@@ -121,10 +121,10 @@ class Interface(Model):
host.owner = owner
if vlan.network_type == 'public':
host.shared_ip = False
host.pub_ipv4 = None
host.external_ipv4 = None
elif vlan.network_type == 'portforward':
host.shared_ip = True
host.pub_ipv4 = vlan.snat_ip
host.external_ipv4 = vlan.snat_ip
host.full_clean()
host.save()
host.enable_net()
......
from __future__ import absolute_import, unicode_literals
from logging import getLogger
from re import search
from django.core.exceptions import PermissionDenied
from django.utils import timezone
......@@ -8,7 +9,6 @@ from django.utils.translation import ugettext_lazy as _
from celery.exceptions import TimeLimitExceeded
from common.operations import Operation, register_operation
from storage.models import Disk
from .tasks.local_tasks import async_instance_operation, async_node_operation
from .models import (
Instance, InstanceActivity, InstanceTemplate, Node, NodeActivity,
......@@ -21,6 +21,7 @@ logger = getLogger(__name__)
class InstanceOperation(Operation):
acl_level = 'owner'
async_operation = async_instance_operation
host_cls = Instance
def __init__(self, instance):
super(InstanceOperation, self).__init__(subject=instance)
......@@ -55,15 +56,12 @@ class InstanceOperation(Operation):
user=user)
def register_instance_operation(op_cls, op_id=None):
return register_operation(Instance, op_cls, op_id)
class DeployOperation(InstanceOperation):
activity_code_suffix = 'deploy'
id = 'deploy'
name = _("deploy")
description = _("Deploy new virtual machine with network.")
icon = 'play'
def on_commit(self, activity):
activity.resultant_state = 'RUNNING'
......@@ -92,7 +90,7 @@ class DeployOperation(InstanceOperation):
self.instance.renew(which='both', base_activity=activity)
register_instance_operation(DeployOperation)
register_operation(DeployOperation)
class DestroyOperation(InstanceOperation):
......@@ -100,6 +98,7 @@ class DestroyOperation(InstanceOperation):
id = 'destroy'
name = _("destroy")
description = _("Destroy virtual machine and its networks.")
icon = 'remove'
def on_commit(self, activity):
activity.resultant_state = 'DESTROYED'
......@@ -132,7 +131,7 @@ class DestroyOperation(InstanceOperation):
self.instance.save()
register_instance_operation(DestroyOperation)
register_operation(DestroyOperation)
class MigrateOperation(InstanceOperation):
......@@ -140,6 +139,7 @@ class MigrateOperation(InstanceOperation):
id = 'migrate'
name = _("migrate")
description = _("Live migrate running VM to another node.")
icon = 'truck'
def _operation(self, activity, user, system, to_node=None, timeout=120):
if not to_node:
......@@ -162,7 +162,7 @@ class MigrateOperation(InstanceOperation):
self.instance.deploy_net()
register_instance_operation(MigrateOperation)
register_operation(MigrateOperation)
class RebootOperation(InstanceOperation):
......@@ -170,12 +170,13 @@ class RebootOperation(InstanceOperation):
id = 'reboot'
name = _("reboot")
description = _("Reboot virtual machine with Ctrl+Alt+Del signal.")
icon = 'refresh'
def _operation(self, activity, user, system, timeout=5):
self.instance.reboot_vm(timeout=timeout)
register_instance_operation(RebootOperation)
register_operation(RebootOperation)
class ResetOperation(InstanceOperation):
......@@ -183,11 +184,12 @@ class ResetOperation(InstanceOperation):
id = 'reset'
name = _("reset")
description = _("Reset virtual machine (reset button).")
icon = 'bolt'
def _operation(self, activity, user, system, timeout=5):
self.instance.reset_vm(timeout=timeout)
register_instance_operation(ResetOperation)
register_operation(ResetOperation)
class SaveAsTemplateOperation(InstanceOperation):
......@@ -199,12 +201,27 @@ class SaveAsTemplateOperation(InstanceOperation):
Template can be shared with groups and users.
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):
if with_shutdown:
try:
ShutdownOperation(self.instance).call(parent_activity=activity,
user=user)
except Instance.WrongStateError:
pass
# prepare parameters
params = {
'access_method': self.instance.access_method,
......@@ -213,7 +230,7 @@ class SaveAsTemplateOperation(InstanceOperation):
'description': self.instance.description,
'lease': self.instance.lease, # Can be problem in new VM
'max_ram_size': self.instance.max_ram_size,
'name': name,
'name': self._rename(self.instance.name),
'num_cores': self.instance.num_cores,
'owner': user,
'parent': self.instance.template, # Can be problem
......@@ -224,20 +241,24 @@ class SaveAsTemplateOperation(InstanceOperation):
}
params.update(kwargs)
from storage.models import Disk
def __try_save_disk(disk):
try:
return disk.save_as()
except Disk.WrongDiskTypeError:
return disk
with activity.sub_activity('saving_disks'):
disks = [__try_save_disk(disk)
for disk in self.instance.disks.all()]
# create template and do additional setup
tmpl = InstanceTemplate(**params)
tmpl.full_clean() # Avoiding database errors.
tmpl.save()
try:
with activity.sub_activity('saving_disks'):
tmpl.disks.add(*[__try_save_disk(disk)
for disk in self.instance.disks.all()])
tmpl.disks.add(*disks)
# create interface templates
for i in self.instance.interface_set.all():
i.save_as_template(tmpl)
......@@ -248,7 +269,7 @@ class SaveAsTemplateOperation(InstanceOperation):
return tmpl
register_instance_operation(SaveAsTemplateOperation)
register_operation(SaveAsTemplateOperation)
class ShutdownOperation(InstanceOperation):
......@@ -256,6 +277,12 @@ class ShutdownOperation(InstanceOperation):
id = 'shutdown'
name = _("shutdown")
description = _("Shutdown virtual machine with ACPI signal.")
icon = 'off'
def check_precond(self):
super(ShutdownOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
def on_abort(self, activity, error):
if isinstance(error, TimeLimitExceeded):
......@@ -272,7 +299,7 @@ class ShutdownOperation(InstanceOperation):
self.instance.yield_vnc_port()
register_instance_operation(ShutdownOperation)
register_operation(ShutdownOperation)
class ShutOffOperation(InstanceOperation):
......@@ -280,6 +307,7 @@ class ShutOffOperation(InstanceOperation):
id = 'shut_off'
name = _("shut off")
description = _("Shut off VM (plug-out).")
icon = 'ban-circle'
def on_commit(self, activity):
activity.resultant_state = 'STOPPED'
......@@ -298,7 +326,7 @@ class ShutOffOperation(InstanceOperation):
self.instance.yield_vnc_port()
register_instance_operation(ShutOffOperation)
register_operation(ShutOffOperation)
class SleepOperation(InstanceOperation):
......@@ -306,6 +334,7 @@ class SleepOperation(InstanceOperation):
id = 'sleep'
name = _("sleep")
description = _("Suspend virtual machine with memory dump.")
icon = 'moon'
def check_precond(self):
super(SleepOperation, self).check_precond()
......@@ -334,7 +363,7 @@ class SleepOperation(InstanceOperation):
# VNC port needs to be kept
register_instance_operation(SleepOperation)
register_operation(SleepOperation)
class WakeUpOperation(InstanceOperation):
......@@ -345,6 +374,7 @@ class WakeUpOperation(InstanceOperation):
Power on Virtual Machine and load its memory from dump.
""")
icon = 'sun'
def check_precond(self):
super(WakeUpOperation, self).check_precond()
......@@ -374,11 +404,12 @@ class WakeUpOperation(InstanceOperation):
self.instance.renew(which='both', base_activity=activity)
register_instance_operation(WakeUpOperation)
register_operation(WakeUpOperation)
class NodeOperation(Operation):
async_operation = async_node_operation
host_cls = Node
def __init__(self, node):
super(NodeOperation, self).__init__(subject=node)
......@@ -401,10 +432,6 @@ class NodeOperation(Operation):
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):
activity_code_suffix = 'flush'
id = 'flush'
......@@ -418,4 +445,4 @@ class FlushOperation(NodeOperation):
i.migrate()
register_node_operation(FlushOperation)
register_operation(FlushOperation)
......@@ -43,6 +43,14 @@ class SaveAsTemplateOperationTestCase(TestCase):
def test_operation_registered(self):
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):
def test_operation_registered(self):
......
......@@ -129,7 +129,8 @@ Install the required Python libraries to the virtual environment::
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
You can now start the development server::
......
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 @@
-r base.txt
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