Commit cf578809 by Kálmán Viktor

Merge branch 'master' into feature-template-wizard

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
	circle/dashboard/urls.py
parents 8a94925d dd0e44ed
......@@ -21,6 +21,13 @@ _build
celerybeat-schedule
.coverage
*,cover
coverage.xml
# Gettext object file:
*.mo
# saml
circle/attribute-maps/
circle/remote_metadata.xml
circle/samlcert.key
circle/samlcert.pem
from collections import deque
from contextlib import contextmanager
from hashlib import sha224
from logging import getLogger
from time import time
......@@ -32,6 +33,18 @@ def activitycontextimpl(act, on_abort=None, on_commit=None):
act.finish(succeeded=True, event_handler=on_commit)
activity_context = contextmanager(activitycontextimpl)
activity_code_separator = '.'
def join_activity_code(*args):
"""Join the specified parts into an activity code.
"""
return activity_code_separator.join(args)
class ActivityModel(TimeStampedModel):
activity_code = CharField(max_length=100, verbose_name=_('activity code'))
parent = ForeignKey('self', blank=True, null=True, related_name='children')
......
from logging import getLogger
from .models import activity_context
from django.core.exceptions import PermissionDenied
logger = getLogger(__name__)
class Operation(object):
"""Base class for VM operations.
"""
async_queue = 'localhost.man'
required_perms = ()
def __call__(self, **kwargs):
return self.call(**kwargs)
def __init__(self, subject):
"""Initialize a new operation bound to the specified subject.
"""
self.subject = subject
def __unicode__(self):
return self.name
def __prelude(self, kwargs):
"""This method contains the shared prelude of call and async.
"""
skip_checks = kwargs.setdefault('system', False)
user = kwargs.setdefault('user', None)
parent_activity = kwargs.pop('parent_activity', None)
if not skip_checks:
self.check_auth(user)
self.check_precond()
return self.create_activity(parent=parent_activity, user=user)
def _exec_op(self, activity, user, **kwargs):
"""Execute the operation inside the specified activity's context.
"""
with activity_context(activity, on_abort=self.on_abort,
on_commit=self.on_commit):
return self._operation(activity=activity, user=user,
**kwargs)
def _operation(self, activity, user, system, **kwargs):
"""This method is the operation's particular implementation.
Deriving classes should implement this method.
"""
raise NotImplementedError
def async(self, **kwargs):
"""Execute the operation asynchronously.
Only a quick, preliminary check is ran before creating the associated
activity and queuing the job.
The returned value is the handle for the asynchronous job.
For more information, check the synchronous call's documentation.
"""
logger.info("%s called asynchronously with the following parameters: "
"%r", self.__class__.__name__, kwargs)
activity = self.__prelude(kwargs)
return self.async_operation.apply_async(args=(self.id,
self.subject.pk,
activity.pk),
kwargs=kwargs,
queue=self.async_queue)
def call(self, **kwargs):
"""Execute the operation (synchronously).
Anticipated keyword arguments:
* parent_activity: Parent activity for the operation. If this argument
is present, the operation's activity will be created
as a child activity of it.
* system: Indicates that the operation is invoked by the system, not a
User. If this argument is present and has a value of True,
then authorization checks are skipped.
* user: The User invoking the operation. If this argument is not
present, it'll be provided with a default value of None.
"""
logger.info("%s called (synchronously) with the following parameters: "
"%r", self.__class__.__name__, kwargs)
activity = self.__prelude(kwargs)
return self._exec_op(activity=activity, **kwargs)
def check_precond(self):
pass
def check_auth(self, user):
if not user.has_perms(self.required_perms):
raise PermissionDenied("%s doesn't have the required permissions."
% user)
def create_activity(self, parent, user):
raise NotImplementedError
def on_abort(self, activity, error):
"""This method is called when the operation aborts (i.e. raises an
exception).
"""
pass
def on_commit(self, activity):
"""This method is called when the operation executes successfully.
"""
pass
operation_registry_name = '_ops'
class OperatedMixin(object):
def __getattr__(self, name):
# NOTE: __getattr__ is only called if the attribute doesn't already
# exist in your __dict__
cls = self.__class__
ops = getattr(cls, operation_registry_name, {})
op = ops.get(name)
if op:
return op(self)
else:
raise AttributeError("%r object has no attribute %r" %
(self.__class__.__name__, name))
def register_operation(target_cls, op_cls, op_id=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:
op_id = op_cls.id
if not issubclass(target_cls, OperatedMixin):
raise TypeError("%r is not a subclass of %r" %
(target_cls.__name__, OperatedMixin.__name__))
if not hasattr(target_cls, operation_registry_name):
setattr(target_cls, operation_registry_name, dict())
getattr(target_cls, operation_registry_name)[op_id] = op_cls
from mock import MagicMock, patch
from django.test import TestCase
from ..operations import Operation
class OperationTestCase(TestCase):
def test_activity_created_before_async_job(self):
class AbortEx(Exception):
pass
op = Operation(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
op.async_operation = MagicMock(
apply_async=MagicMock(side_effect=AbortEx))
with patch.object(Operation, 'check_precond'):
with patch.object(Operation, 'create_activity') as create_act:
try:
op.async(system=True)
except AbortEx:
self.assertTrue(create_act.called)
def test_check_precond_called_before_create_activity(self):
class AbortEx(Exception):
pass
op = Operation(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
with patch.object(Operation, 'create_activity', side_effect=AbortEx):
with patch.object(Operation, 'check_precond') as chk_pre:
try:
op.call(system=True)
except AbortEx:
self.assertTrue(chk_pre.called)
def test_auth_check_on_non_system_call(self):
op = Operation(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
user = MagicMock()
with patch.object(Operation, 'check_auth') as check_auth:
with patch.object(Operation, 'check_precond'), \
patch.object(Operation, 'create_activity'), \
patch.object(Operation, '_exec_op'):
op.call(user=user)
check_auth.assert_called_with(user)
def test_no_auth_check_on_system_call(self):
op = Operation(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
with patch.object(Operation, 'check_auth', side_effect=AssertionError):
with patch.object(Operation, 'check_precond'), \
patch.object(Operation, 'create_activity'), \
patch.object(Operation, '_exec_op'):
op.call(system=True)
......@@ -33,7 +33,6 @@
"hostname": "devenv",
"modified_at": "2014-02-24T15:55:01.412Z",
"location": "",
"pub_ipv4": null,
"mac": "11:22:33:44:55:66",
"shared_ip": false,
"ipv4": "10.7.0.96",
......
......@@ -37,6 +37,7 @@ class VmCustomizeForm(forms.Form):
cpu_priority = forms.IntegerField()
cpu_count = forms.IntegerField()
ram_size = forms.IntegerField()
amount = forms.IntegerField(min_value=0, initial=1)
disks = forms.ModelMultipleChoiceField(
queryset=None, required=True)
......@@ -72,12 +73,21 @@ class VmCustomizeForm(forms.Form):
self.initial['template'] = self.template.pk
self.initial['customized'] = self.template.pk
# set widget for amount
self.fields['amount'].widget = NumberInput()
self.helper = FormHelper(self)
self.helper.form_show_labels = False
# don't show labels for the sliders
self.helper.form_show_labels = True
self.fields['cpu_count'].label = ""
self.fields['ram_size'].label = ""
self.fields['cpu_priority'].label = ""
self.helper.layout = Layout(
Field("template", type="hidden"),
Field("customized", type="hidden"),
Div( # buttons
Div(
Div(
AnyTag( # tip: don't try to use Button class
"button",
......@@ -88,16 +98,17 @@ class VmCustomizeForm(forms.Form):
HTML(" Start"),
css_id="vm-create-customized-start",
css_class="btn btn-success",
style="float: right; margin-top: 24px;",
),
css_class="col-sm-11 text-right",
Field("name", style="max-width: 350px;"),
css_class="col-sm-12",
),
css_class="row",
),
Div(
Div(
Field("name"),
css_class="col-sm-5",
Field("amount", min="1", style="max-width: 60px;"),
css_class="col-sm-10",
),
css_class="row",
),
......@@ -189,32 +200,36 @@ class VmCustomizeForm(forms.Form):
HTML(_("No disks are added!")),
css_id="vm-create-disk-list",
),
AnyTag(
"h3",
Div(
AnyTag(
"select",
css_class="form-control",
css_id="vm-create-disk-add-select",
),
Div(
AnyTag(
"a",
AnyTag(
"i",
css_class="icon-plus-sign",
),
href="#",
css_id="vm-create-disk-add-button",
css_class="btn btn-success",
),
css_class="input-group-btn"
),
css_class="input-group",
style="max-width: 330px;",
),
css_id="vm-create-disk-add",
),
HTML(""),
style="clear: both;",
),
# AnyTag(
# "h3",
# Div(
# AnyTag(
# "select",
# css_class="form-control",
# css_id="vm-create-disk-add-select",
# ),
# Div(
# AnyTag(
# "a",
# AnyTag(
# "i",
# css_class="icon-plus-sign",
# ),
# href="#",
# css_id="vm-create-disk-add-button",
# css_class="btn btn-success",
# ),
# css_class="input-group-btn"
# ),
# css_class="input-group",
# style="max-width: 330px;",
# ),
# css_id="vm-create-disk-add",
# ),
css_class="no-js-hidden",
),
css_class="col-sm-8",
......
......@@ -79,6 +79,14 @@ html {
color: #fff;
}
.timeline .activity-active .timeline-icon {
background-color: black!important;
}
.timeline a {
color: black;
}
.timeline-icon.timeline-warning {
border-color: #c09853;
border-style: solid;
......@@ -100,6 +108,10 @@ html {
border-left: 3px solid green;
}
.sub-activity-active {
border-left: 8px solid black;
}
.sub-activity-failed {
border-left: 3px solid #d9534f;
}
......@@ -472,3 +484,35 @@ footer a, footer a:hover, footer a:visited {
#vm-details-resources-form {
padding: 5px; /* it's nice this way in the tour */
}
.index-vm-list-name {
display: inline-block;
max-width: 70%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
float: left;
}
#dashboard-vm-list a small {
padding-left: 10px;
}
.index-template-list-name {
display: inline-block;
max-width: 50%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
float: left;
}
#dashboard-template-list a small {
max-width: 50%;
float: left;
padding-top: 2px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
padding-left: 10px;
}
......@@ -3,7 +3,7 @@ $(function () {
var template = $(this).data("template");
$.ajax({
type: 'GET',
url: '/dashboard/vm/create/',
url: '/dashboard/vm/create/' + (typeof template === "undefined" ? '' : '?template=' + template),
success: function(data) {
$('body').append(data);
vmCreateLoaded();
......@@ -12,9 +12,6 @@ $(function () {
$('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove();
});
if(template) {
$('#vm-create-template-select option[value="' + template + '"]').prop("selected", true).trigger("change");
}
}
});
return false;
......@@ -209,6 +206,9 @@ $(function () {
'name': result[i].name.toLowerCase(),
'state': result[i].state,
'fav': result[i].fav,
'host': result[i].host,
'icon': result[i].icon,
'status': result[i].status,
});
}
});
......@@ -225,7 +225,9 @@ $(function () {
}
search_result.sort(compareVmByFav);
for(var i=0; i<5 && i<search_result.length; i++)
html += generateVmHTML(search_result[i].pk, search_result[i].name, search_result[i].fav);
html += generateVmHTML(search_result[i].pk, search_result[i].name,
search_result[i].host, search_result[i].icon,
search_result[i].status, search_result[i].fav);
if(search_result.length == 0)
html += '<div class="list-group-item">No result</div>';
$("#dashboard-vm-list").html(html);
......@@ -251,21 +253,33 @@ $(function () {
});
});
function generateVmHTML(pk, name, fav) {
function generateVmHTML(pk, name, host, icon, _status, fav) {
return '<a href="/dashboard/vm/' + pk + '/" class="list-group-item">' +
'<i class="icon-play-sign"></i> ' + name +
'<div class="pull-right dashboard-vm-favourite" data-vm="' + pk +'">' +
'<i class="title-favourite icon-star' + (fav ? "" : "-empty") + ' text-primary" title="" data-original-title="' +
(fav ? "Un": "Mark as ") + 'favourite"></i>' +
'<span class="index-vm-list-name">' +
'<i class="' + icon + '" title="' + _status + '"></i> ' + name +
'</span>' +
'<small class="text-muted"> ' + host + '</small>' +
'<div class="pull-right dashboard-vm-favourite" data-vm="' + pk + '">' +
(fav ? '<i class="icon-star text-primary title-favourite" title="Unfavourite"></i>' :
'<i class="icon-star-empty text-primary title-favourite" title="Mark as favorite"></i>' ) +
'</div>' +
'<div style="clear: both;"></div>' +
'</a>';
}
/* copare vm-s by fav, pk order */
function compareVmByFav(a, b) {
if(a.fav)
if(a.fav && b.fav) {
return a.pk < b.pk ? -1 : 1;
}
else if(a.fav && !b.fav) {
return -1;
else
}
else if(!a.fav && b.fav) {
return 1;
}
else
return a.pk < b.pk ? -1 : 1;
}
function addSliderMiscs() {
......
......@@ -14,7 +14,6 @@ function vmCreateLoaded() {
$(".customize-vm").click(function() {
var template = $(this).data("template-pk");
console.log(template);
$.get("/dashboard/vm/create/?template=" + template, function(data) {
var r = $('#create-modal'); r.next('div').remove(); r.remove();
......@@ -143,7 +142,6 @@ function vmCustomizeLoaded() {
}
var html = '<option data-managed="' + (managed ? 1 : 0) + '" value="' + pk + '">' + text + '</option>';
if($('#vm-create-network-list span').length < 1) {
$("#vm-create-network-list").html("");
}
......@@ -152,8 +150,14 @@ function vmCustomizeLoaded() {
} else {
$('#vm-create-network-add-select').append(html);
}
});
// if all networks are added add a dummy and disable the add button
if($("#vm-create-network-add-select option").length < 1) {
$("#vm-create-network-add-select").html('<option value="-1">No more networks!</option>');
$('#vm-create-network-add-button').attr('disabled', true);
}
/* build up network list */
$('#vm-create-network-add-vlan option').each(function() {
......@@ -197,23 +201,13 @@ function vmCustomizeLoaded() {
/* remove disk */
// event for disk remove button (icon, X)
$('body').on('click', '.vm-create-remove-disk', function() {
var disk_pk = ($(this).parent('span').prop('id')).replace('vlan-', '')
var disk_pk = ($(this).parent('span').prop('id')).replace('disk-', '')
$(this).parent('span').fadeOut(500, function() {
/* if ther are no more disks disabled the add button */
if($('#vm-create-disk-add-select option')[0].value == -1) {
$('#vm-create-disk-add-button').attr('disabled', false);
$('#vm-create-disk-add-select').html('');
}
/* remove the disk label */
$(this).remove();
var disk_name = $(this).text();
$('#vm-create-disk-add-select').append($('<option>', {
value: disk_pk,
text: disk_name
}));
/* remove the selection from the multiple select */
$('#vm-create-disk-add-form option[value="' + disk_pk + '"]').prop('selected', false);
......@@ -257,7 +251,11 @@ function vmCustomizeLoaded() {
data: $('form').serialize(),
success: function(data, textStatus, xhr) {
if(data.redirect) {
window.location.replace(data.redirect + '#activity');
/* it won't redirect to the same page */
if(window.location.pathname == data.redirect) {
window.location.reload();
}
window.location.href = data.redirect + '#activity';
}
else {
var r = $('#create-modal'); r.next('div').remove(); r.remove();
......@@ -295,5 +293,6 @@ function vmCreateNetworkLabel(pk, name, managed) {
function vmCreateDiskLabel(pk, name) {
return '<span id="vlan-' + pk + '" class="label label-primary"><i class="icon-file"></i> ' + name + ' <a href="#" class="hover-black vm-create-remove-disk"><i class="icon-remove-sign"></i></a></span> ';
var style = "float: left; margin: 5px 5px 5px 0;";
return '<span id="disk-' + pk + '" class="label label-primary" style="' + style + '"><i class="icon-file"></i> ' + name + ' <a href="#" class="hover-black vm-create-remove-disk"><i class="icon-remove-sign"></i></a></span> ';
}
......@@ -199,24 +199,24 @@ function decideActivityRefresh() {
return check;
}
function checkNewActivity(only_state, runs) {
// set default only_state to false
only_state = typeof only_state !== 'undefined' ? only_state : false;
function checkNewActivity(only_status, runs) {
// set default only_status to false
only_status = typeof only_status !== 'undefined' ? only_status : false;
var instance = location.href.split('/'); instance = instance[instance.length - 2];
$.ajax({
type: 'GET',
url: '/dashboard/vm/' + instance + '/activity/',
data: {'only_state': only_state},
data: {'only_status': only_status},
success: function(data) {
if(!only_state) {
if(!only_status) {
$("#activity-timeline").html(data['activities']);
$("[title]").tooltip();
}
$("#vm-details-state i").prop("class", data['icon']);
$("#vm-details-state span").html(data['state']);
if(data['state'] == "RUNNING") {
$("#vm-details-state span").html(data['human_readable_status'].toUpperCase());
if(data['status'] == "RUNNING") {
$("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled");
} else {
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
......@@ -224,7 +224,7 @@ function checkNewActivity(only_state, runs) {
if(runs > 0 && decideActivityRefresh()) {
setTimeout(
function() {checkNewActivity(only_state, runs + 1)},
function() {checkNewActivity(only_status, runs + 1)},
1000 + Math.exp(runs * 0.05)
);
}
......
......@@ -58,12 +58,17 @@
</li>
</ul>
<div style="margin-top: 20px; padding: 0 15px; width: 100%">
<a class="btn btn-primary btn-xs customize-vm" data-template-pk="{{ t.pk }}" href="{% url "dashboard.views.vm-create" %}?template={{ t.pk }}"><i class="icon-wrench"></i> Customize</a>
{% if perms.vm_set_resources %}
<a class="btn btn-primary btn-xs customize-vm" data-template-pk="{{ t.pk }}" href="{% url "dashboard.views.vm-create" %}?template={{ t.pk }}"><i class="icon-wrench"></i> {% trans "Customize" %}</a>
{% endif %}
<form class="pull-right text-right" method="POST" action="{% url "dashboard.views.vm-create" %}">
{% csrf_token %}
<input type="hidden" name="template" value="{{ t.pk }}"/>
<button class="btn btn-success btn-xs vm-create-start" data-template-pk="{{ t.pk }}" type="submit"><i class="icon-play"></i> Start</button>
<button class="btn btn-success btn-xs vm-create-start" data-template-pk="{{ t.pk }}" type="submit">
<i class="icon-play"></i> {% trans "Start" %}
</button>
</form>
<div style="clear: both;"></div>
</div>
</div>
</div>
......
......@@ -12,7 +12,7 @@
{%endblocktrans%}
{% endif %}
<div class="pull-right">
<form action="" method="POST">
<form action="{% url "dashboard.views.status-node" pk=object.pk %}" method="POST">
{% csrf_token %}
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button>
<input type="hidden" name="change_status" value=""/>
......
......@@ -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">
......
......@@ -7,11 +7,15 @@
<h3 class="no-margin"><i class="icon-puzzle-piece"></i> {% trans "Templates" %}
</h3>
</div>
<div class="list-group" id="vm-list-view">
<div class="list-group" id="dashboard-template-list">
{% for t in templates %}
<a href="{% url "dashboard.views.template-detail" pk=t.pk %}" class="list-group-item">
<i class="icon-{{ t.os_type }}"></i> {{ t.name }} <small class="text-muted">{{ t.system }}</small>
<span class="index-template-list-name">
<i class="icon-{{ t.os_type }}"></i> {{ t.name }}
</span>
<small class="text-muted index-template-list-system">{{ t.system }}</small>
<div class="pull-right vm-create" data-template="{{ t.pk }}"><i title="{% trans "Start vm instance" %}" class="icon-play"></i></div>
<div class="clearfix"></div>
</a>
{% empty %}
<div class="alert alert-warning" style="margin: 10px;">
......
......@@ -16,8 +16,11 @@
<div id="dashboard-vm-list">
{% for i in instances %}
<a href="{{ i.get_absolute_url }}" class="list-group-item">
<i class="{{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i> {{ i.name }}
<small class="text-muted">{{ i.primary_host.hostname }}</small>
<span class="index-vm-list-name">
<i class="{{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i>
{{ i.name }}
</span>
<small class="text-muted"> {{ i.primary_host.hostname }}</small>
<div class="pull-right dashboard-vm-favourite" data-vm="{{ i.pk }}">
{% if i.fav %}
<i class="icon-star text-primary title-favourite" title="{% trans "Unfavourite" %}"></i>
......@@ -25,6 +28,7 @@
<i class="icon-star-empty text-primary title-favourite" title="{% trans "Mark as favorite" %}"></i>
{% endif %}
</div>
<div style="clear: both;"></div>
</a>
{% endfor %}
</div>
......@@ -50,19 +54,24 @@
<div class="panel-body" id="vm-graph-view" style="display: none">
<p class="pull-right"> <input class="knob" data-fgColor="chartreuse" data-thickness=".4" data-max="{{ request.user.profile.instance_limit }}" data-width="100" data-height="100" data-readOnly="true" value="{{ instances|length|add:more_instances }}"></p>
<p><span class="bigbig">{% blocktrans with count=running_vm_num %}<big>{{ count }}</big> running{% endblocktrans %}</span>
<ul class="list-inline">
<ul class="list-inline" style="max-height: 95px; overflow: hidden;">
{% for vm in running_vms %}
<li class="label label-success">
<a href="vm.get_absolute_url" title="{{vm.primary_host.get_fqdn}}"><i class="{{vm.get_status_icon}}"></i> {{vm.name}}</a>
<li style="display: inline-block; padding: 2px;">
<a href="{{vm.get_absolute_url}}" title="{{vm.primary_host.get_fqdn}}" class="label label-success">
<i class="{{vm.get_status_icon}}"></i> {{vm.name}}
</a>
</li>
{% endfor %}
</ul>
</p>
<p class="big text-warning">{% blocktrans with count=stopped_vm_num %}<big>{{ count }}</big> stopped{% endblocktrans %}</p>
<div class="clearfix"></div>
<div class="text-right">
<a href="{% url "dashboard.views.vm-list" %}" class="btn btn-primary btn-xs"><i class="icon-chevron-sign-right"></i> <strong>{{ instances|length|add:more_instances }}</strong> machines total</a>
<div>
<a style="float: right; margin-top: 17px;" href="{% url "dashboard.views.vm-list" %}" class="btn btn-primary btn-xs">
<i class="icon-chevron-sign-right"></i>
<strong>{{ instances|length|add:more_instances }}</strong> machines total
</a>
<p class="big text-warning">{% blocktrans with count=stopped_vm_num %}<big>{{ count }}</big> stopped{% endblocktrans %}</p>
</div>
</div>
</div>
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="page-header">
<h1>
{{ object.instance.name }}: {{ object.get_readable_name }}
</h1>
</div>
<div class="row">
<div class="col-md-4" id="vm-info-pane">
<div class="big">
<span id="vm-activity-state" class="label label-{% if object.get_status_id == 'wait' %}info{% else %}{% if object.succeeded %}success{% else %}error{% endif %}{% endif %}">
<span>{{ object.get_status_id|upper }}</span>
</span>
</div>
<div id="vm-activity-context" class="timeline">
{% include "dashboard/vm-detail/_activity-timeline.html" with active=object %}
</div>
</div>
<div class="col-md-8">
<div class="panel panel-default">
<!--<div class="panel-heading"><h2 class="panel-title">{% trans "Activity" %}</h2></div> -->
<div class="panel-body">
<dl>
<dt>{% trans "activity code" %}</dt>
<dd>{{object.activity_code}}</dd>
<dt>{% trans "instance" %}</dt>
<dd><a href="{{object.instance.get_absolute_url}}">{{object.instance}}</a></dd>
<dt>{% trans "time" %}</dt>
<dd>{{object.started|default:'n/a'}} → {{object.finished|default:'n/a'}}</dd>
<dt>{% trans "user" %}</dt>
<dd>{{object.user|default:'(system)'}}</dd>
<dt>{% trans "type" %}</dt>
<dd>
{% if object.parent %}
{% blocktrans with url=object.parent.get_absolute_url name=object.parent %}
subactivity of <a href="{{url}}">{{name}}</a>
{% endblocktrans %}
{% else %}{% trans "top level activity" %}{% endif %}
</dd>
<dt>{% trans "task uuid" %}</dt>
<dd>{{ object.task_uuid|default:'n/a' }}</dd>
<dt>{% trans "status" %}</dt>
<dd>{{ object.get_status_id }}</dd>
<dt>{% trans "result" %}</dt>
<dd><textarea class="form-control">{{object.result}}</textarea></dd>
<dt>{% trans "resultant state" %}</dt>
<dd>{{object.resultant_state|default:'n/a'}}</dd>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
......@@ -34,7 +34,7 @@
</li>
<li>
<strong>{% trans "Flush" %}:</strong>
{% trans "Disable node and move all instances to other ones." %}
{% trans "Disable node and move all instances to other one." %}
</li>
<li>
<strong>{% trans "Enable" %}:</strong>
......@@ -46,7 +46,7 @@
</li>
<li>
<strong>{% trans "Delete" %}:</strong>
{% trans "Remove node and its host." %}
{% trans "Remove node and it's host." %}
</li>
</ul>
</div>
......
......@@ -12,6 +12,7 @@
{% include template %}
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
......
{% load i18n %}
{% for a in activities %}
<div class="activity" data-activity-id="{{ a.pk }}">
<div class="activity{% if a.pk == active.pk %} activity-active{%endif%}" data-activity-id="{{ a.pk }}">
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
<i class="{% if not a.finished %} icon-refresh icon-spin {% else %}icon-plus{% endif %}"></i>
</span>
<strong{% if user.is_superuser and a.result %} title="{{ a.result }}"{% endif %}>
{{ a.get_readable_name }}
{% if user.is_superuser %}<a href="{{ a.get_absolute_url }}">{% endif %}
{{ a.get_readable_name }}{% if user.is_superuser %}</a>{% endif %}
</strong>
{{ a.started|date:"Y-m-d H:i" }}{% if a.user %}, {{ a.user }}{% endif %}
{% if a.children.count > 0 %}
<div class="sub-timeline">
{% for s in a.children.all %}
<div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}">
<div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}{% if s.pk == active.pk %} sub-activity-active{% endif %}">
<span{% if user.is_superuser and s.result %} title="{{ s.result }}"{% endif %}>
{{ s.get_readable_name }}</span> &ndash;
{% if user.is_superuser %}<a href="{{ s.get_absolute_url }}">{% endif %}
{{ s.get_readable_name }}{% if user.is_superuser %}</a>{% endif %}</span> &ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
......
import unittest
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.http import HttpRequest, Http404
from dashboard.views import InstanceActivityDetail, InstanceActivity
class ViewUserTestCase(unittest.TestCase):
def test_404(self):
view = InstanceActivityDetail.as_view()
request = FakeRequestFactory(superuser=True)
with self.assertRaises(Http404):
view(request, pk=1234)
def test_not_superuser(self):
request = FakeRequestFactory(superuser=False)
with patch.object(InstanceActivityDetail, 'get_object') as go:
go.return_value = MagicMock(spec=InstanceActivity)
go.return_value._meta.object_name = "InstanceActivity"
view = InstanceActivityDetail.as_view()
self.assertEquals(view(request, pk=1234).status_code, 302)
def test_found(self):
request = FakeRequestFactory(superuser=True)
with patch.object(InstanceActivityDetail, 'get_object') as go:
act = MagicMock(spec=InstanceActivity)
act._meta.object_name = "InstanceActivity"
go.return_value = act
view = InstanceActivityDetail.as_view()
self.assertEquals(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.
'''
user = UserFactory()
user.is_authenticated = lambda: kwargs.get('authenticated', True)
user.is_superuser = kwargs.get('superuser', False)
request = HttpRequest()
request.user = user
request.session = kwargs.get('session', {})
if kwargs.get('POST'):
request.method = 'POST'
request.POST = kwargs.get('POST')
else:
request.method = 'GET'
request.POST = kwargs.get('GET', {})
return request
class UserFactory(Factory):
''' using the excellent factory_boy library '''
FACTORY_FOR = User
username = Sequence(lambda i: 'test%d' % i)
first_name = 'John'
last_name = 'Doe'
email = Sequence(lambda i: 'test%d@example.com' % i)
......@@ -3,15 +3,16 @@ from django.conf.urls import patterns, url
from vm.models import Instance
from .views import (
AclUpdateView, DiskAddView, FavouriteView, GroupAclUpdateView, GroupDelete,
GroupDetailView, GroupList, GroupUserDelete, IndexView, LeaseCreate,
LeaseDelete, LeaseDetail, MyPreferencesView, NodeAddTraitView, NodeCreate,
NodeDelete, NodeDetailView, NodeFlushView, NodeGraphView, NodeList,
NodeStatus, NotificationView, PortDelete, TemplateAclUpdateView,
TemplateCreate, TemplateDelete, TemplateDetail, TemplateList,
TransferOwnershipConfirmView, TransferOwnershipView, vm_activity, VmCreate,
VmDelete, VmDetailView, VmDetailVncTokenView, VmGraphView, VmList,
VmMassDelete, VmMigrateView, VmRenewView, DiskRemoveView,
get_disk_download_status, TemplateChoose, TemplateClone
GroupDetailView, GroupList, GroupUserDelete, IndexView,
InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail,
MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeFlushView, NodeGraphView, NodeList, NodeStatus,
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView,
VmRenewView, DiskRemoveView, get_disk_download_status,
TemplateChoose, TemplateClone,
)
urlpatterns = patterns(
......@@ -61,6 +62,8 @@ urlpatterns = patterns(
name='dashboard.views.vm-migrate'),
url(r'^vm/(?P<pk>\d+)/renew/((?P<key>.*)/?)$', VmRenewView.as_view(),
name='dashboard.views.vm-renew'),
url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(),
name='dashboard.views.vm-activity'),
url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'),
url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(),
......
......@@ -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
......@@ -16,7 +16,7 @@ class RecordInline(contrib.admin.TabularInline):
class HostAdmin(admin.ModelAdmin):
list_display = ('hostname', 'vlan', 'ipv4', 'ipv6', 'pub_ipv4', 'mac',
list_display = ('hostname', 'vlan', 'ipv4', 'ipv6', 'external_ipv4', 'mac',
'shared_ip', 'owner', 'description', 'reverse',
'list_groups')
ordering = ('hostname', )
......@@ -25,6 +25,10 @@ class HostAdmin(admin.ModelAdmin):
filter_horizontal = ('groups', )
inlines = (RuleInline, RecordInline)
def queryset(self, request):
qs = super(HostAdmin, self).queryset(request)
return qs.prefetch_related('groups')
@staticmethod
def list_groups(instance):
"""Returns instance's groups' names as a comma-separated list."""
......@@ -48,9 +52,9 @@ class VlanAdmin(admin.ModelAdmin):
class RuleAdmin(admin.ModelAdmin):
list_display = ('r_type', 'color_desc', 'owner', 'extra', 'direction',
'accept', 'proto', 'sport', 'dport', 'nat',
'nat_dport', 'used_in')
list_filter = ('vlan', 'owner', 'direction', 'accept',
'action', 'proto', 'sport', 'dport', 'nat',
'nat_external_port', 'used_in')
list_filter = ('vlan', 'owner', 'direction', 'action',
'proto', 'nat')
def color_desc(self, instance):
......@@ -110,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):
......@@ -129,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)
import logging
import re
from collections import OrderedDict
logger = logging.getLogger()
ipv4_re = re.compile(
r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}')
class InvalidRuleExcepion(Exception):
pass
class IptRule(object):
def __init__(self, priority=1000, action=None, src=None, dst=None,
proto=None, sport=None, dport=None, extra=None,
ipv4_only=False, comment=None):
if proto not in ['tcp', 'udp', 'icmp', None]:
raise InvalidRuleExcepion()
if proto not in ['tcp', 'udp'] and (sport is not None or
dport is not None):
raise InvalidRuleExcepion()
self.priority = int(priority)
self.action = action
(self.src4, self.src6) = (None, None)
if isinstance(src, tuple):
(self.src4, self.src6) = src
if not self.src6:
ipv4_only = True
(self.dst4, self.dst6) = (None, None)
if isinstance(dst, tuple):
(self.dst4, self.dst6) = dst
if not self.dst6:
ipv4_only = True
self.proto = proto
self.sport = sport
self.dport = dport
self.extra = extra
self.ipv4_only = (ipv4_only or
extra is not None and bool(ipv4_re.search(extra)))
self.comment = comment
def __hash__(self):
return hash(frozenset(self.__dict__.items()))
def __eq__(self, other):
return self.__dict__ == other.__dict__
def __lt__(self, other):
return self.priority < other.priority
def __repr__(self):
return '<IptRule: @%d %s >' % (self.priority, self.compile())
def __unicode__(self):
return self.__repr__()
def compile(self, proto='ipv4'):
opts = OrderedDict([('src4' if proto == 'ipv4' else 'src6', '-s %s'),
('dst4' if proto == 'ipv4' else 'dst6', '-d %s'),
('proto', '-p %s'),
('sport', '--sport %s'),
('dport', '--dport %s'),
('extra', '%s'),
('comment', '-m comment --comment "%s"'),
('action', '-g %s')])
params = [opts[param] % getattr(self, param)
for param in opts
if getattr(self, param) is not None]
return ' '.join(params)
class IptChain(object):
nat_chains = ('PREROUTING', 'POSTROUTING')
builtin_chains = ('FORWARD', 'INPUT', 'OUTPUT') + nat_chains
def __init__(self, name):
self.rules = set()
self.name = name
def add(self, *args, **kwargs):
for rule in args:
self.rules.add(rule)
def sort(self):
return sorted(list(self.rules))
def __len__(self):
return len(self.rules)
def __repr__(self):
return '<IptChain: %s %s>' % (self.name, self.rules)
def __unicode__(self):
return self.__repr__()
def compile(self, proto='ipv4'):
assert proto in ('ipv4', 'ipv6')
prefix = '-A %s ' % self.name
return '\n'.join([prefix + rule.compile(proto)
for rule in self.sort()
if not (proto == 'ipv6' and rule.ipv4_only)])
def compile_v6(self):
return self.compile('ipv6')
......@@ -26,7 +26,7 @@ def _apply_once(name, queues, task, data):
@celery.task(ignore_result=True)
def periodic_task():
from firewall.fw import Firewall, dhcp, dns, ipset, vlan
from firewall.fw import BuildFirewall, dhcp, dns, ipset, vlan
from remote_tasks import (reload_dns, reload_dhcp, reload_firewall,
reload_firewall_vlan, reload_blacklist)
......@@ -40,7 +40,7 @@ def periodic_task():
_apply_once('dhcp', firewall_queues, reload_dhcp,
lambda: (dhcp(), ))
_apply_once('firewall', firewall_queues, reload_firewall,
lambda: (Firewall(proto=4).get(), Firewall(proto=6).get()))
lambda: (BuildFirewall().build_ipt()))
_apply_once('firewall_vlan', firewall_queues, reload_firewall_vlan,
lambda: (vlan(), ))
_apply_once('blacklist', firewall_queues, reload_blacklist,
......@@ -48,7 +48,7 @@ def periodic_task():
@celery.task
def reloadtask(type='Host'):
def reloadtask(type='Host', timeout=15):
reload = {
'Host': ['dns', 'dhcp', 'firewall'],
'Record': ['dns'],
......
{% if proto == "ipv4" %}
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
{% for chain in nat %}
{{ chain.compile|safe }}
{% endfor %}
COMMIT
{% endif %}
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]
# initialize logging
-N LOG_DROP
# windows port scan are silently dropped
-A LOG_DROP -p tcp --dport 445 -j DROP
-A LOG_DROP -p udp --dport 137 -j DROP
-A LOG_DROP -j LOG --log-level 7 --log-prefix "[ipt][drop]"
-A LOG_DROP -j DROP
-N LOG_ACC
-A LOG_ACC -j LOG --log-level 7 --log-prefix "[ipt][isok]"
-A LOG_ACC -j ACCEPT
# initialize FORWARD chain
-A FORWARD -m set --match-set blacklist src,dst -j DROP
-A FORWARD -m state --state INVALID -g LOG_DROP
-A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
-A FORWARD -p icmp --icmp-type echo-request -g LOG_ACC
# initialize INPUT chain
-A INPUT -m set --match-set blacklist src -j DROP
-A INPUT -m state --state INVALID -g LOG_DROP
-A INPUT -i lo -j ACCEPT
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# initialize OUTPUT chain
-A OUTPUT -m state --state INVALID -g LOG_DROP
-A OUTPUT -o lo -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
{% for chain in filter %}
{% if chain.name not in chain.builtin_chains %}-N {{ chain.name }}{% endif %}
{% if proto == "ipv4" %}
{{ chain.compile|safe }}
{% else %}
{{ chain.compile_v6|safe }}
{% endif %}
{% endfor %}
# close all chains
-A FORWARD -g LOG_DROP
-A INPUT -g LOG_DROP
-A OUTPUT -g LOG_DROP
COMMIT
from netaddr import IPSet
from netaddr import IPSet, AddrFormatError
from django.test import TestCase
from django.contrib.auth.models import User
from ..admin import HostAdmin
from firewall.models import Vlan, Domain, Record, Host
from firewall.models import (Vlan, Domain, Record, Host, VlanGroup, Group,
Rule, Firewall)
from firewall.fw import dns, ipv6_to_octal
from firewall.tasks.local_tasks import periodic_task, reloadtask
from django.forms import ValidationError
from ..iptables import IptRule, IptChain, InvalidRuleExcepion
from mock import patch
import django.conf
settings = django.conf.settings.FIREWALL_SETTINGS
class MockInstance:
......@@ -96,12 +104,12 @@ class HostGetHostnameTestCase(TestCase):
self.vlan.save()
self.h = Host(hostname='h', mac='01:02:03:04:05:00', ipv4='10.0.0.1',
vlan=self.vlan, owner=self.u1, shared_ip=True,
pub_ipv4=self.vlan.snat_ip)
external_ipv4=self.vlan.snat_ip)
self.h.save()
def test_issue_93_wo_record(self):
self.assertEqual(self.h.get_hostname(proto='ipv4', public=True),
unicode(self.h.pub_ipv4))
unicode(self.h.external_ipv4))
def test_issue_93_w_record(self):
self.r = Record(name='vm', type='A', domain=self.d, owner=self.u1,
......@@ -109,3 +117,192 @@ class HostGetHostnameTestCase(TestCase):
self.r.save()
self.assertEqual(self.h.get_hostname(proto='ipv4', public=True),
self.r.fqdn)
class IptablesTestCase(TestCase):
def setUp(self):
self.r = [IptRule(priority=4, action='ACCEPT',
src=('127.0.0.4', None)),
IptRule(priority=4, action='ACCEPT',
src=('127.0.0.4', None)),
IptRule(priority=2, action='ACCEPT',
dst=('127.0.0.2', None),
extra='-p icmp'),
IptRule(priority=6, action='ACCEPT',
dst=('127.0.0.6', None),
proto='tcp', dport=80),
IptRule(priority=1, action='ACCEPT',
dst=('127.0.0.1', None),
proto='udp', dport=53),
IptRule(priority=5, action='ACCEPT',
dst=('127.0.0.5', None),
proto='tcp', dport=443),
IptRule(priority=2, action='ACCEPT',
dst=('127.0.0.2', None),
proto='icmp'),
IptRule(priority=6, action='ACCEPT',
dst=('127.0.0.6', None),
proto='tcp', dport='1337')]
def test_chain_add(self):
ch = IptChain(name='test')
ch.add(*self.r)
self.assertEqual(len(ch), len(self.r) - 1)
def test_rule_compile_ok(self):
assert unicode(self.r[5])
self.assertEqual(self.r[5].compile(),
'-d 127.0.0.5 -p tcp --dport 443 -g ACCEPT')
def test_rule_compile_fail(self):
self.assertRaises(InvalidRuleExcepion,
IptRule, **{'proto': 'test'})
self.assertRaises(InvalidRuleExcepion,
IptRule, **{'priority': 5, 'action': 'ACCEPT',
'dst': '127.0.0.5',
'proto': 'icmp', 'dport': 443})
def test_chain_compile(self):
ch = IptChain(name='test')
ch.add(*self.r)
compiled = ch.compile()
compiled_v6 = ch.compile_v6()
assert unicode(ch)
self.assertEqual(len(compiled.splitlines()), len(ch))
self.assertEqual(len(compiled_v6.splitlines()), 0)
class ReloadTestCase(TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.save()
d = Domain.objects.create(name='example.org', owner=self.u1)
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.0/29',
snat_ip='152.66.243.99',
network6='2001:738:2001:4031::/80', domain=d,
owner=self.u1, network_type='portforward',
dhcp_pool='manual')
self.vlan.save()
self.vlan2 = Vlan(vid=2, name='pub', network4='10.1.0.0/29',
network6='2001:738:2001:4032::/80', domain=d,
owner=self.u1, network_type='public')
self.vlan2.save()
self.vlan.snat_to.add(self.vlan2)
settings["default_vlangroup"] = 'public'
settings["default_host_groups"] = ['netezhet']
vlg = VlanGroup.objects.create(name='public')
vlg.vlans.add(self.vlan, self.vlan2)
self.hg = Group.objects.create(name='netezhet')
Rule.objects.create(action='accept', hostgroup=self.hg,
foreign_network=vlg)
firewall = Firewall.objects.create(name='fw')
Rule.objects.create(action='accept', firewall=firewall,
foreign_network=vlg)
for i in range(1, 6):
h = Host.objects.create(hostname='h-%d' % i, vlan=self.vlan,
mac='01:02:03:04:05:%02d' % i,
ipv4='10.0.0.%d' % i, owner=self.u1)
h.enable_net()
h.groups.add(self.hg)
if i == 5:
h.vlan = self.vlan2
h.save()
self.h5 = h
if i == 1:
self.h1 = h
self.r1 = Record(name='tst', type='A', address='127.0.0.1',
domain=d, owner=self.u1)
self.rb = Record(name='tst', type='AAAA', address='1.0.0.1',
domain=d, owner=self.u1)
self.r2 = Record(name='ts', type='AAAA', address='2001:123:45::6',
domain=d, owner=self.u1)
self.rm = Record(name='asd', type='MX', address='10:teszthu',
domain=d, owner=self.u1)
self.rt = Record(name='asd', type='TXT', address='ASD',
domain=d, owner=self.u1)
self.r1.save()
self.r2.save()
with patch('firewall.models.Record.clean'):
self.rb.save()
self.rm.save()
self.rt.save()
def test_bad_aaaa_record(self):
self.assertRaises(AddrFormatError, ipv6_to_octal, self.rb.address)
def test_good_aaaa_record(self):
ipv6_to_octal(self.r2.address)
def test_dns_func(self):
records = dns()
self.assertEqual(Host.objects.count() * 2 + # soa
len((self.r1, self.r2, self.rm, self.rt)) + 1,
len(records))
def test_host_add_port(self):
h = self.h1
h.ipv6 = '2001:2:3:4::0'
assert h.behind_nat
h.save()
old_rules = h.rules.count()
h.add_port('tcp', private=22)
new_rules = h.rules.count()
self.assertEqual(new_rules, old_rules + 1)
self.assertEqual(len(h.list_ports()), old_rules + 1)
endp = h.get_public_endpoints(22)
self.assertEqual(endp['ipv4'][0], h.ipv4)
assert int(endp['ipv4'][1])
self.assertEqual(endp['ipv6'][0], h.ipv6)
assert int(endp['ipv6'][1])
def test_host_add_port2(self):
h = self.h5
h.ipv6 = '2001:2:3:4::1'
h.save()
assert not h.behind_nat
old_rules = h.rules.count()
h.add_port('tcp', private=22)
new_rules = h.rules.count()
self.assertEqual(new_rules, old_rules + 1)
self.assertEqual(len(h.list_ports()), old_rules + 1)
endp = h.get_public_endpoints(22)
self.assertEqual(endp['ipv4'][0], h.ipv4)
assert int(endp['ipv4'][1])
self.assertEqual(endp['ipv6'][0], h.ipv6)
assert int(endp['ipv6'][1])
def test_host_del_port(self):
h = self.h1
h.ipv6 = '2001:2:3:4::0'
h.save()
h.add_port('tcp', private=22)
old_rules = h.rules.count()
h.del_port('tcp', private=22)
new_rules = h.rules.count()
self.assertEqual(new_rules, old_rules - 1)
def test_host_add_port_wo_vlangroup(self):
VlanGroup.objects.filter(name='public').delete()
h = self.h1
old_rules = h.rules.count()
h.add_port('tcp', private=22)
new_rules = h.rules.count()
self.assertEqual(new_rules, old_rules)
def test_host_add_port_w_validationerror(self):
h = self.h1
self.assertRaises(ValidationError, h.add_port,
'tcp', public=1000, private=22)
def test_periodic_task(self):
#TODO
with patch('firewall.tasks.local_tasks.cache') as cache:
self.test_host_add_port()
self.test_host_add_port2()
periodic_task()
reloadtask()
assert cache.delete.called
......@@ -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):
......@@ -106,7 +106,7 @@ class HostForm(ModelForm):
'ipv4',
'ipv6',
'shared_ip',
'pub_ipv4',
'external_ipv4',
),
Fieldset(
'Information',
......@@ -162,12 +162,14 @@ class RuleForm(ModelForm):
'foreign_network',
'dport',
'sport',
'weight',
'proto',
'extra',
'accept',
'action',
'owner',
'nat',
'nat_dport',
'nat_external_port',
'nat_external_ipv4',
),
Fieldset(
'External',
......@@ -232,6 +234,7 @@ class VlanForm(ModelForm):
'IPv6',
'network6',
'ipv6_template',
'host_ipv6_prefixlen',
),
Fieldset(
'Domain name service',
......
......@@ -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:
......@@ -44,7 +44,7 @@ class HostTable(Table):
model = Host
attrs = {'class': 'table table-striped table-condensed'}
fields = ('hostname', 'vlan', 'mac', 'ipv4', 'ipv6',
'pub_ipv4', 'created_at', 'owner', )
'external_ipv4', 'created_at', 'owner', )
order_by = ('vlan', 'hostname', )
......@@ -128,7 +128,8 @@ class RuleTable(Table):
model = Rule
attrs = {'class': 'table table-striped table-hover table-condensed'}
fields = ('r_type', 'color_desc', 'owner', 'extra', 'direction',
'accept', 'proto', 'sport', 'dport', 'nat', 'nat_dport', )
'action', 'proto', 'sport', 'dport', 'nat',
'nat_external_port', )
order_by = 'direction'
......
......@@ -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">
......
......@@ -33,5 +33,6 @@
{% if record.nat %}
<span class="label label-success">NAT
[ {{ record.dport }} <i class="icon-arrow-right"></i> {{record.nat_dport}} ]</span>
[ {{ record.dport }} <i class="icon-arrow-right"></i>
{{record.nat_external_port}} ]</span>
{% endif %}
......@@ -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> #}
......@@ -18,13 +18,13 @@ from .views import (IndexView,
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)
......@@ -503,6 +504,11 @@ class RuleList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
template_name = "network/rule-list.html"
table_pagination = False
def get_table_data(self):
return Rule.objects.select_related('host', 'hostgroup', 'vlan',
'vlangroup', 'firewall',
'foreign_network', 'owner')
class RuleDetail(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, UpdateView):
......
......@@ -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
# This import is responsible for running the operations' registration code.
from . import operations # noqa
......@@ -5,8 +5,12 @@ from .models import (Instance, InstanceActivity, InstanceTemplate, Interface,
NodeActivity, Trait)
class InstanceActivityAdmin(admin.ModelAdmin):
exclude = ('parent', )
admin.site.register(Instance)
admin.site.register(InstanceActivity)
admin.site.register(InstanceActivity, InstanceActivityAdmin)
admin.site.register(InstanceTemplate)
admin.site.register(Interface)
admin.site.register(InterfaceTemplate)
......
......@@ -2,11 +2,16 @@ from __future__ import absolute_import, unicode_literals
from contextlib import contextmanager
from logging import getLogger
from django.core.urlresolvers import reverse
from django.db.models import CharField, ForeignKey
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from common.models import ActivityModel, activitycontextimpl
from common.models import (
ActivityModel, activitycontextimpl, join_activity_code,
)
logger = getLogger(__name__)
......@@ -23,6 +28,7 @@ class ActivityInProgressError(Exception):
class InstanceActivity(ActivityModel):
ACTIVITY_CODE_BASE = join_activity_code('vm', 'Instance')
instance = ForeignKey('Instance', related_name='activity_log',
help_text=_('Instance this activity works on.'),
verbose_name=_('instance'))
......@@ -42,9 +48,20 @@ class InstanceActivity(ActivityModel):
return '{}({})'.format(self.activity_code,
self.instance)
def get_absolute_url(self):
return reverse('dashboard.views.vm-activity', args=[self.pk])
def get_readable_name(self):
return self.activity_code.split('.')[-1].replace('_', ' ').capitalize()
def get_status_id(self):
if self.succeeded is None:
return 'wait'
elif self.succeeded:
return 'success'
else:
return 'failed'
@classmethod
def create(cls, code_suffix, instance, task_uuid=None, user=None,
concurrency_check=True):
......@@ -53,9 +70,10 @@ class InstanceActivity(ActivityModel):
if concurrency_check and active_activities.exists():
raise ActivityInProgressError(active_activities[0])
act = cls(activity_code='vm.Instance.' + code_suffix,
instance=instance, parent=None, resultant_state=None,
started=timezone.now(), task_uuid=task_uuid, user=user)
activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix)
act = cls(activity_code=activity_code, instance=instance, parent=None,
resultant_state=None, started=timezone.now(),
task_uuid=task_uuid, user=user)
act.save()
return act
......@@ -66,7 +84,7 @@ class InstanceActivity(ActivityModel):
raise ActivityInProgressError(active_children[0])
act = InstanceActivity(
activity_code=self.activity_code + '.' + code_suffix,
activity_code=join_activity_code(self.activity_code, code_suffix),
instance=self.instance, parent=self, resultant_state=None,
started=timezone.now(), task_uuid=task_uuid, user=self.user)
act.save()
......@@ -97,6 +115,7 @@ def instance_activity(code_suffix, instance, on_abort=None, on_commit=None,
class NodeActivity(ActivityModel):
ACTIVITY_CODE_BASE = join_activity_code('vm', 'Node')
node = ForeignKey('Node', related_name='activity_log',
help_text=_('Node this activity works on.'),
verbose_name=_('node'))
......@@ -119,15 +138,15 @@ class NodeActivity(ActivityModel):
@classmethod
def create(cls, code_suffix, node, task_uuid=None, user=None):
act = cls(activity_code='vm.Node.' + code_suffix,
node=node, parent=None, started=timezone.now(),
task_uuid=task_uuid, user=user)
activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix)
act = cls(activity_code=activity_code, node=node, parent=None,
started=timezone.now(), task_uuid=task_uuid, user=user)
act.save()
return act
def create_sub(self, code_suffix, task_uuid=None):
act = NodeActivity(
activity_code=self.activity_code + '.' + code_suffix,
activity_code=join_activity_code(self.activity_code, code_suffix),
node=self.node, parent=self, started=timezone.now(),
task_uuid=task_uuid, user=self.user)
act.save()
......
......@@ -137,7 +137,7 @@ class Interface(Model):
iface.save()
return iface
def deploy(self, user=None, task_uuid=None):
def deploy(self):
if self.destroyed:
from .instance import Instance
raise Instance.InstanceDestroyedError(self.instance,
......@@ -149,16 +149,23 @@ class Interface(Model):
args=[self.get_vmnetwork_desc()],
queue=self.instance.get_remote_queue_name('net'))
def shutdown(self, user=None, task_uuid=None):
net_tasks.destroy.apply_async(
args=[self.get_vmnetwork_desc()],
queue=self.instance.get_remote_queue_name('net'))
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')
net_tasks.destroy.apply_async(args=[self.get_vmnetwork_desc()],
queue=queue_name)
def destroy(self, user=None, task_uuid=None):
def destroy(self):
if self.destroyed:
return
self.shutdown(user, task_uuid)
self.shutdown()
if self.host is not None:
self.host.delete()
......
from __future__ import absolute_import, unicode_literals
from logging import getLogger
from warnings import warn
from django.db.models import (
CharField, IntegerField, ForeignKey, BooleanField, ManyToManyField,
FloatField, permalink,
)
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from warnings import warn
from celery.exceptions import TimeoutError
from model_utils.models import TimeStampedModel
from taggit.managers import TaggableManager
from common.models import method_cache, WorkerNotFound, HumanSortField
from common.operations import OperatedMixin
from firewall.models import Host
from ..tasks import vm_tasks, local_tasks
from .common import Trait
from .activity import node_activity, NodeActivity
from monitor.calvin.calvin import Query
from monitor.calvin.calvin import GraphiteHandler
from django.utils import timezone
from ..tasks import vm_tasks
from .activity import node_activity, NodeActivity
from .common import Trait
logger = getLogger(__name__)
......@@ -38,7 +37,7 @@ def node_available(function):
return decorate
class Node(TimeStampedModel):
class Node(OperatedMixin, TimeStampedModel):
"""A VM host machine, a hypervisor.
"""
......@@ -131,22 +130,6 @@ class Node(TimeStampedModel):
self.enabled = False
self.save()
def flush(self, user=None, task_uuid=None):
"""Disable node and move all instances to other ones.
"""
with node_activity('flush', node=self, user=user,
task_uuid=task_uuid) as act:
self.disable(user, act)
for i in self.instance_set.all():
with act.sub_activity('migrate_instance_%d' % i.pk):
i.migrate()
def flush_async(self, user=None):
"""Execute flush asynchronously.
"""
return local_tasks.flush.apply_async(args=[self, user],
queue="localhost.man")
def enable(self, user=None):
''' Enable the node. '''
if self.enabled is not True:
......@@ -164,10 +147,10 @@ class Node(TimeStampedModel):
@method_cache(30)
def get_remote_queue_name(self, queue_id):
"""Return the name of the remote celery queue for this node.
"""Returns the name of the remote celery queue for this node.
throws Exception if there is no worker on the queue.
Until the cache provide reult there can be dead queues.
Throws Exception if there is no worker on the queue.
The result may include dead queues because of caching.
"""
if vm_tasks.check_queue(self.host.hostname, queue_id):
......@@ -189,7 +172,7 @@ class Node(TimeStampedModel):
else:
logger.debug("The last activity was %s" % act)
if act.activity_code.endswith("offline"):
act = NodeActivity.create(code_suffix='monitor_succes_online',
act = NodeActivity.create(code_suffix='monitor_success_online',
node=self, user=None)
act.started = timezone.now()
act.finished = timezone.now()
......
......@@ -27,7 +27,7 @@ def garbage_collector(timeout=15):
now = timezone.now()
for i in Instance.objects.filter(destroyed_at=None).all():
if i.time_of_delete and now > i.time_of_delete:
i.destroy_async()
i.destroy.async()
logger.info("Expired instance %d destroyed.", i.pk)
try:
i.owner.profile.notify(
......@@ -39,7 +39,7 @@ def garbage_collector(timeout=15):
i.pk, unicode(e))
elif (i.time_of_suspend and now > i.time_of_suspend and
i.state == 'RUNNING'):
i.sleep_async()
i.sleep.async()
logger.info("Expired instance %d suspended." % i.pk)
try:
i.owner.profile.notify(
......
from manager.mancelery import celery
# TODO: Keep synchronised with Instance funcs
@celery.task
def deploy(instance, user):
instance.deploy(task_uuid=deploy.request.id, user=user)
@celery.task
def redeploy(instance, user):
instance.redeploy(task_uuid=redeploy.request.id, user=user)
@celery.task
def shut_off(instance, user):
instance.shut_off(task_uuid=shut_off.request.id, user=user)
def async_instance_operation(operation_id, instance_pk, activity_pk, **kwargs):
from vm.models import Instance, InstanceActivity
instance = Instance.objects.get(pk=instance_pk)
operation = getattr(instance, operation_id)
activity = InstanceActivity.objects.get(pk=activity_pk)
# save async task UUID to activity
activity.task_uuid = async_instance_operation.request.id
activity.save()
@celery.task
def destroy(instance, user):
instance.destroy(task_uuid=destroy.request.id, user=user)
@celery.task
def save_as_template(instance, name, user, params):
instance.save_as_template(name, task_uuid=save_as_template.request.id,
user=user, **params)
@celery.task
def sleep(instance, user):
instance.sleep(task_uuid=sleep.request.id, user=user)
return operation._exec_op(activity=activity, **kwargs)
@celery.task
def wake_up(instance, user):
instance.wake_up(task_uuid=wake_up.request.id, user=user)
def async_node_operation(operation_id, node_pk, activity_pk, **kwargs):
from vm.models import Node, NodeActivity
node = Node.objects.get(pk=node_pk)
operation = getattr(node, operation_id)
activity = NodeActivity.objects.get(pk=activity_pk)
# save async task UUID to activity
activity.task_uuid = async_node_operation.request.id
activity.save()
@celery.task
def shutdown(instance, user):
instance.shutdown(task_uuid=shutdown.request.id, user=user)
@celery.task
def reset(instance, user):
instance.reset(task_uuid=reset.request.id, user=user)
@celery.task
def reboot(instance, user):
instance.reboot(task_uuid=reboot.request.id, user=user)
@celery.task
def migrate(instance, to_node, user):
instance.migrate(to_node, task_uuid=migrate.request.id, user=user)
@celery.task
def flush(node, user):
node.flush(task_uuid=flush.request.id, user=user)
return operation._exec_op(activity=activity, **kwargs)
......@@ -19,18 +19,16 @@ def check_queue(node_hostname, queue_id):
active_queues = get_queues()
if active_queues is None:
return False
# v is List of List of queues dict
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
return False
queue_names = (queue['name'] for worker in active_queues.itervalues()
for queue in worker)
return queue_name in queue_names
def get_queues():
"""Get active celery queues.
Returns a dictionary whose entries are (worker name;list of queues) pairs,
where queues are represented by dictionaries.
Result is cached for 10 seconds!
"""
key = __name__ + u'queues'
......
from datetime import datetime
from mock import Mock, MagicMock, patch, call
from django.contrib.auth.models import User
from django.test import TestCase
from django.utils.translation import ugettext_lazy as _
from mock import Mock, MagicMock, patch, call
from ..models import (
Lease, Node, Interface, Instance, InstanceTemplate, InstanceActivity,
)
from ..models.instance import find_unused_port, ActivityInProgressError
from ..operations import (
DeployOperation, DestroyOperation, FlushOperation, MigrateOperation,
)
class PortFinderTestCase(TestCase):
......@@ -52,50 +55,60 @@ class InstanceTestCase(TestCase):
def test_deploy_destroyed(self):
inst = Mock(destroyed_at=datetime.now(), spec=Instance,
InstanceDestroyedError=Instance.InstanceDestroyedError)
deploy_op = DeployOperation(inst)
with patch.object(DeployOperation, 'create_activity'):
with self.assertRaises(Instance.InstanceDestroyedError):
Instance.deploy(inst)
deploy_op(system=True)
def test_destroy_destroyed(self):
inst = Mock(destroyed_at=datetime.now(), spec=Instance)
Instance.destroy(inst)
inst = Mock(destroyed_at=datetime.now(), spec=Instance,
InstanceDestroyedError=Instance.InstanceDestroyedError)
destroy_op = DestroyOperation(inst)
with patch.object(DestroyOperation, 'create_activity'):
with self.assertRaises(Instance.InstanceDestroyedError):
destroy_op(system=True)
self.assertFalse(inst.save.called)
def test_destroy_sets_destroyed(self):
inst = MagicMock(destroyed_at=None, spec=Instance)
inst = Mock(destroyed_at=None, spec=Instance,
InstanceDestroyedError=Instance.InstanceDestroyedError)
inst.node = MagicMock(spec=Node)
inst.disks.all.return_value = []
with patch('vm.models.instance.instance_activity') as ia:
ia.return_value = MagicMock()
Instance.destroy(inst)
destroy_op = DestroyOperation(inst)
with patch.object(DestroyOperation, 'create_activity'):
destroy_op(system=True)
self.assertTrue(inst.destroyed_at)
inst.save.assert_called()
def test_migrate_with_scheduling(self):
inst = MagicMock(spec=Instance)
inst = Mock(destroyed_at=None, spec=Instance)
inst.interface_set.all.return_value = []
inst.node = MagicMock(spec=Node)
with patch('vm.models.instance.instance_activity') as ia, \
patch('vm.models.instance.vm_tasks.migrate') as migr:
Instance.migrate(inst)
migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr:
act = MagicMock()
with patch.object(MigrateOperation, 'create_activity',
return_value=act):
migrate_op(system=True)
migr.apply_async.assert_called()
self.assertIn(call().__enter__().sub_activity(u'scheduling'),
ia.mock_calls)
self.assertIn(call.sub_activity(u'scheduling'), act.mock_calls)
inst.select_node.assert_called()
def test_migrate_wo_scheduling(self):
inst = MagicMock(spec=Instance)
inst = MagicMock(destroyed_at=None, spec=Instance)
inst.interface_set.all.return_value = []
inst.node = MagicMock(spec=Node)
with patch('vm.models.instance.instance_activity') as ia, \
patch('vm.models.instance.vm_tasks.migrate') as migr:
migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr:
inst.select_node.side_effect = AssertionError
Instance.migrate(inst, inst.node)
act = MagicMock()
with patch.object(MigrateOperation, 'create_activity',
return_value=act):
migrate_op(to_node=inst.node, system=True)
migr.apply_async.assert_called()
self.assertNotIn(call().__enter__().sub_activity(u'scheduling'),
ia.mock_calls)
self.assertNotIn(call.sub_activity(u'scheduling'), act.mock_calls)
def test_status_icon(self):
inst = MagicMock(spec=Instance)
......@@ -162,25 +175,19 @@ class InstanceActivityTestCase(TestCase):
instance.activity_log.filter.return_value.exists.return_value = True
with self.assertRaises(ActivityInProgressError):
InstanceActivity.create("test", instance, concurrency_check=True)
InstanceActivity.create('test', instance, concurrency_check=True)
def test_create_no_concurrency_check(self):
instance = MagicMock(spec=Instance)
instance.activity_log.filter.return_value.exists.return_value = True
original_method = InstanceActivity.create.__func__
with patch('vm.models.activity.InstanceActivity') as ia, \
patch('vm.models.activity.timezone.now'):
# ia.__init__ = MagicMock() raises AttributeError
original_method(ia, "test", instance, concurrency_check=False)
ia.save.assert_called()
# ia.__init__.assert_called_with(activity_code='vm.Instance.test',
# instance=instance, parent=None,
# resultant_state=None, started=now,
# task_uuid=None, user=None)
with patch.object(InstanceActivity, '__new__'):
try:
InstanceActivity.create('test', instance,
concurrency_check=False)
except ActivityInProgressError:
raise AssertionError("'create' method checked for concurrent "
"activities.")
def test_create_sub_concurrency_check(self):
iaobj = MagicMock(spec=InstanceActivity)
......@@ -194,12 +201,13 @@ class InstanceActivityTestCase(TestCase):
iaobj.activity_code = 'test'
iaobj.children.filter.return_value.exists.return_value = True
original_method = InstanceActivity.create_sub
with patch('vm.models.activity.InstanceActivity') as ia, \
patch('vm.models.activity.timezone.now'):
original_method(iaobj, "test", concurrency_check=False)
ia.save.assert_called()
with patch.object(InstanceActivity, '__new__'):
try:
InstanceActivity.create_sub(iaobj, 'test',
concurrency_check=False)
except ActivityInProgressError:
raise AssertionError("'create_sub' method checked for "
"concurrent activities.")
def test_disable_enabled(self):
node = MagicMock(spec=Node, enabled=True)
......@@ -231,33 +239,37 @@ class InstanceActivityTestCase(TestCase):
subact.__enter__.assert_called()
def test_flush(self):
insts = [MagicMock(spec=Instance, migrate=MagicMock()),
MagicMock(spec=Instance, migrate=MagicMock())]
node = MagicMock(spec=Node, enabled=True)
node.instance_set.all.return_value = insts
user = MagicMock(spec=User)
insts = [MagicMock(spec=Instance), MagicMock(spec=Instance)]
flush_op = FlushOperation(node)
with patch('vm.models.node.node_activity') as na:
act = na.return_value.__enter__.return_value = MagicMock()
node.instance_set.all.return_value = insts
with patch.object(FlushOperation, 'create_activity') as create_act:
act = create_act.return_value = MagicMock()
Node.flush(node, user)
flush_op(user=user)
na.__enter__.assert_called()
create_act.assert_called()
node.disable.assert_called_with(user, act)
for i in insts:
i.migrate.assert_called()
def test_flush_disabled_wo_user(self):
insts = [MagicMock(spec=Instance, migrate=MagicMock()),
MagicMock(spec=Instance, migrate=MagicMock())]
node = MagicMock(spec=Node, enabled=False)
insts = [MagicMock(spec=Instance), MagicMock(spec=Instance)]
with patch('vm.models.node.node_activity') as na:
act = na.return_value.__enter__.return_value = MagicMock()
node.instance_set.all.return_value = insts
flush_op = FlushOperation(node)
with patch.object(FlushOperation, 'create_activity') as create_act:
act = create_act.return_value = MagicMock()
Node.flush(node)
flush_op(system=True)
create_act.assert_called()
node.disable.assert_called_with(None, act)
# ^ should be called, but real method no-ops if disabled
na.__enter__.assert_called()
for i in insts:
i.migrate.assert_called()
from django.test import TestCase
from common.operations import operation_registry_name as op_reg_name
from vm.models import Instance, Node
from vm.operations import (
DeployOperation, DestroyOperation, FlushOperation, MigrateOperation,
RebootOperation, ResetOperation, SaveAsTemplateOperation,
ShutdownOperation, ShutOffOperation, SleepOperation, WakeUpOperation,
)
class DeployOperationTestCase(TestCase):
def test_operation_registered(self):
assert DeployOperation.id in getattr(Instance, op_reg_name)
class DestroyOperationTestCase(TestCase):
def test_operation_registered(self):
assert DestroyOperation.id in getattr(Instance, op_reg_name)
class FlushOperationTestCase(TestCase):
def test_operation_registered(self):
assert FlushOperation.id in getattr(Node, op_reg_name)
class MigrateOperationTestCase(TestCase):
def test_operation_registered(self):
assert MigrateOperation.id in getattr(Instance, op_reg_name)
class RebootOperationTestCase(TestCase):
def test_operation_registered(self):
assert RebootOperation.id in getattr(Instance, op_reg_name)
class ResetOperationTestCase(TestCase):
def test_operation_registered(self):
assert ResetOperation.id in getattr(Instance, op_reg_name)
class SaveAsTemplateOperationTestCase(TestCase):
def test_operation_registered(self):
assert SaveAsTemplateOperation.id in getattr(Instance, op_reg_name)
class ShutdownOperationTestCase(TestCase):
def test_operation_registered(self):
assert ShutdownOperation.id in getattr(Instance, op_reg_name)
class ShutOffOperationTestCase(TestCase):
def test_operation_registered(self):
assert ShutOffOperation.id in getattr(Instance, op_reg_name)
class SleepOperationTestCase(TestCase):
def test_operation_registered(self):
assert SleepOperation.id in getattr(Instance, op_reg_name)
class WakeUpOperationTestCase(TestCase):
def test_operation_registered(self):
assert WakeUpOperation.id in getattr(Instance, op_reg_name)
......@@ -4,3 +4,4 @@ coverage==3.6
django-discover-runner==0.4
django-nose==1.2
mock==1.0.1
factory-boy==2.3.1
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