Commit cab22b32 by Őry Máté

Merge branch 'feature-flush-node'

Add Node.flush

*  model
*  tasks
*  view
parents 0964aa9c c086ab65
......@@ -137,6 +137,21 @@ $(function () {
return false;
});
/* for Node flush buttons */
$('.node-flush').click(function() {
var node_pk = $(this).data('node-pk');
var postto = $(this).attr('href');
var dir = window.location.pathname.indexOf('list') == -1;
addModalConfirmation(function(){},
{ 'url': postto,
'data': [],
'pk': node_pk,
'type': "node",
'redirect': dir});
return false;
});
/* for Group removes buttons */
$('.group-delete').click(function() {
var group_pk = $(this).data('group-pk');
......
{% 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">
{% if text %}
{{ text }}
{% else %}
{%blocktrans with object=object%}
Are you sure you want to flush <strong>{{ object }}</strong>?
{%endblocktrans%}
{% endif %}
<div class="pull-right">
<form action="{% url "dashboard.views.flush-node" pk=node.pk %}?next={{next}}" method="POST">
{% csrf_token %}
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button>
<input type="hidden" name="flush" value=""/>
<button class="btn btn-warning">{% trans "Yes" %}</button>
</form>
</div>
<div class="clearfix"></div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{% if title %}
{{ title }}
{% else %}
Flush confirmation
{% endif %}
</h3>
</div>
<div class="panel-body">
{% if text %}
{{ text }}
{% else %}
{%blocktrans with object=object%}
Are you sure you want to flush <strong>{{ object }}</strong>?
{%endblocktrans%}
{% endif %}
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
<a class="btn btn-default">{% trans "Back" %}</a>
<input type="hidden" name="flush" value=""/>
<button class="btn btn-warning">{% trans "Yes" %}</button>
</form>
</div>
</div>
</div>
{% endblock %}
......@@ -29,15 +29,10 @@
<button type="button" class="btn {{ btn_size }} btn-warning nojs-dropdown-toogle dropdown-toggle" data-toggle="dropdown">Action <i class="icon-caret-down"></i></button>
<ul class="dropdown-menu nojs-dropdown-toogle" role="menu">
<li><a href="#" class="node-details-rename-button"><i class="icon-pencil"></i> {% trans "Rename" %}</a></li>
<li><a href="#"><i class="icon-cloud-upload"></i> Flush</a></li>
{% if node.enabled %}
<li><a style="display:none" data-node-pk="{{ node.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=node.pk %}?next={{ request.path }}"><i class="icon-check"></i> Enable</a>
<a style="display:block" data-node-pk="{{ node.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=node.pk %}?next={{ request.path }}"><i class="icon-remove"></i> Disable</a></li>
{% else %}
<li><a style="display:block" data-node-pk="{{ node.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=node.pk %}?next={{ request.path }}" >
<i class="icon-check"></i> Enable</a>
<a style="display:none" data-node-pk="{{ node.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=node.pk %}?next={{ request.path }}" ><i class="icon-remove"></i> Disable</a></li>
{% endif %}
<li><a data-node-pk="{{ node.pk }}" class="real-link node-flush" href="{% url "dashboard.views.flush-node" pk=node.pk %}"><i class="icon-cloud-upload"></i>{% trans "Flush" %}</a>
<li>
<a style="display:{% if node.enabled %}none{% else %}block{% endif %}" data-node-pk="{{ node.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=node.pk %}?next={{ request.path }}"><i class="icon-check"></i>{% trans "Enable" %}</a>
<a style="display:{% if not node.enabled %}none{% else %}block{% endif %}" data-node-pk="{{ node.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=node.pk %}?next={{ request.path }}"><i class="icon-remove"></i>{% trans "Disable" %}</a></li>
<li><a data-node-pk="{{ node.pk }}" class="real-link node-delete" href="{% url "dashboard.views.delete-node" pk=node.pk %}?next={{ request.path }}"><i class="icon-trash"></i> Delete</a></li>
</ul>
</div>
......
{% load i18n %}
<div class="btn-group">
<button type="button" class="btn {{ btn_size }} btn-warning nojs-dropdown-toogle dropdown-toggle" data-toggle="dropdown">Action <i class="icon-caret-down"></i></button>
<ul class="dropdown-menu nojs-dropdown-toogle" role="menu">
<li><a href="#"><i class="icon-cloud-upload"></i> Flush</a></li>
{% if record.enabled %}
<li><a style="display:none" data-status="enable" data-node-pk="{{ record.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=record.pk %}?next={{ request.path }}&status=enable"><i class="icon-check"></i> Enable</a>
<a style="display:block" data-status="disable" data-node-pk="{{ record.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=record.pk %}?next={{ request.path }}&status=disable"><i class="icon-remove"></i> Disable</a></li>
{% else %}
<li><a style="display:block" data-status="enable" data-node-pk="{{ record.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=record.pk %}?next={{ request.path }}&status=enable" >
<i class="icon-check"></i> Enable</a>
<a style="display:none" data-status="disable" data-node-pk="{{ record.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=record.pk %}?next={{ request.path }}&status=disable" ><i class="icon-remove"></i> Disable</a></li>
{% endif %}
<li><a data-node-pk="{{ record.pk }}" class="real-link node-delete" href="{% url "dashboard.views.delete-node" pk=record.pk %}?next={{ request.path }}"><i class="icon-trash"></i> Delete</a></li>
<li><a href="#" class="node-details-rename-button"><i class="icon-pencil"></i> {% trans "Rename" %}</a></li>
<li><a data-node-pk="{{ record.pk }}" class="real-link node-flush" href="{% url "dashboard.views.flush-node" pk=record.pk %}"><i class="icon-cloud-upload"></i>{% trans "Flush" %}</a>
<li><a style={% if record.enabled %}"display:none"{% else %}"display:block"{% endif %} data-status="enable" data-node-pk="{{ record.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=record.pk %}?next={{ request.path }}&status=enable"><i class="icon-check"></i>{% trans "Enable" %}</a>
<a style={% if record.enabled %}"display:block"{% else %}"display:none"{% endif %} data-status="disable" data-node-pk="{{ record.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=record.pk %}?next={{ request.path }}&status=disable"><i class="icon-remove"></i>{% trans "Disable" %}</a></li>
<li><a data-node-pk="{{ record.pk }}" class="real-link node-delete" href="{% url "dashboard.views.delete-node" pk=record.pk %}?next={{ request.path }}"><i class="icon-trash"></i>{% trans "Delete" %}</a></li>
</ul>
</div>
......@@ -4,4 +4,4 @@
<a id="node-list-rename-button" class="btn btn-default btn-xs" title data-original-title="Rename">
<i class="icon-pencil"></i>
</a>
......@@ -5,12 +5,12 @@ from .views import (
AclUpdateView, DiskAddView, FavouriteView, GroupAclUpdateView, GroupDelete,
GroupDetailView, GroupList, GroupUserDelete, IndexView, LeaseCreate,
LeaseDelete, LeaseDetail, MyPreferencesView, NodeAddTraitView, NodeCreate,
NodeDelete, NodeDetailView, NodeGraphView, NodeList, NodeStatus,
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView,
VmRenewView,
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,
)
urlpatterns = patterns(
......@@ -68,6 +68,8 @@ urlpatterns = patterns(
name="dashboard.views.delete-node"),
url(r'^node/status/(?P<pk>\d+)/$', NodeStatus.as_view(),
name="dashboard.views.status-node"),
url(r'^node/flush/(?P<pk>\d+)/$', NodeFlushView.as_view(),
name="dashboard.views.flush-node"),
url(r'^node/create/$', NodeCreate.as_view(),
name='dashboard.views.node-create'),
......
......@@ -505,7 +505,6 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
return context
def post(self, request, *args, **kwargs):
print request.POST
if request.POST.get('new_name'):
return self.__set_name(request)
if request.POST.get('change_status') is not None:
......@@ -1401,6 +1400,42 @@ class NodeStatus(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
return redirect(self.get_success_url())
class NodeFlushView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
template_name = "dashboard/confirm/node-flush.html"
model = Node
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-node-flush.html']
else:
return ['dashboard/confirm/node-flush.html']
def get_success_url(self):
next = self.request.GET.get('next')
if next:
return next
else:
return reverse_lazy("dashboard.views.node-detail",
kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super(NodeFlushView, self).get_context_data(**kwargs)
return context
def post(self, request, *args, **kwargs):
if request.POST.get('flush') is not None:
return self.__flush(request)
return redirect(reverse_lazy("dashboard.views.node-detail",
kwargs={'pk': self.get_object().pk}))
def __flush(self, request):
self.object = self.get_object()
self.object.flush_async(user=request.user)
success_message = _("Node successfully flushed!")
messages.success(request, success_message)
return redirect(self.get_success_url())
class PortDelete(LoginRequiredMixin, DeleteView):
model = Rule
pk_url_kwarg = 'rule'
......
......@@ -1096,10 +1096,15 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
return local_tasks.migrate.apply_async(args=[self, to_node, user],
queue="localhost.man")
def migrate(self, to_node, user=None, task_uuid=None, timeout=120):
def migrate(self, to_node=None, user=None, task_uuid=None, timeout=120):
"""Live migrate running vm to another node. """
with instance_activity(code_suffix='migrate', instance=self,
task_uuid=task_uuid, user=user) as act:
if not to_node:
with act.sub_activity('scheduling') as sa:
to_node = self.select_node()
sa.result = to_node
# Destroy networks
with act.sub_activity('destroying_net'):
for net in self.interface_set.all():
......
......@@ -13,7 +13,7 @@ from taggit.managers import TaggableManager
from common.models import method_cache, WorkerNotFound
from firewall.models import Host
from ..tasks import vm_tasks
from ..tasks import vm_tasks, local_tasks
from .common import Trait
from .activity import node_activity, NodeActivity
......@@ -106,13 +106,33 @@ class Node(TimeStampedModel):
def get_status_display(self):
return self.STATES[self.enabled][self.online][1]
def disable(self, user=None):
def disable(self, user=None, base_activity=None):
''' Disable the node.'''
if self.enabled:
with node_activity(code_suffix='disable', node=self, user=user):
if base_activity:
act_ctx = base_activity.sub_activity('disable')
else:
act_ctx = node_activity('disable', node=self, user=user)
with act_ctx:
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:
......
......@@ -57,3 +57,8 @@ def reboot(instance, 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)
from datetime import datetime
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
from mock import Mock, MagicMock, patch, call
from ..models import (
Lease, Node, Interface, Instance, InstanceTemplate, InstanceActivity,
......@@ -68,12 +70,38 @@ class InstanceTestCase(TestCase):
self.assertTrue(inst.destroyed_at)
inst.save.assert_called()
def test_migrate_with_scheduling(self):
inst = MagicMock(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)
migr.apply_async.assert_called()
self.assertIn(call().__enter__().sub_activity(u'scheduling'),
ia.mock_calls)
inst.select_node.assert_called()
def test_migrate_wo_scheduling(self):
inst = MagicMock(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:
inst.select_node.side_effect = AssertionError
Instance.migrate(inst, inst.node)
migr.apply_async.assert_called()
self.assertNotIn(call().__enter__().sub_activity(u'scheduling'),
ia.mock_calls)
class InterfaceTestCase(TestCase):
def test_interface_create(self):
from firewall.models import Vlan, Domain
from django.contrib.auth.models import User
owner = User()
owner.save()
i = Instance(id=10, owner=owner, access_method='rdp')
......@@ -165,3 +193,64 @@ class InstanceActivityTestCase(TestCase):
patch('vm.models.activity.timezone.now'):
original_method(iaobj, "test", concurrency_check=False)
ia.save.assert_called()
def test_disable_enabled(self):
node = MagicMock(spec=Node, enabled=True)
with patch('vm.models.node.node_activity') as nac:
na = MagicMock()
nac.return_value = na
na.__enter__.return_value = MagicMock()
Node.disable(node)
self.assertFalse(node.enabled)
node.save.assert_called_once()
na.assert_called()
def test_disable_disabled(self):
node = MagicMock(spec=Node, enabled=False)
with patch('vm.models.node.node_activity') as nac:
na = MagicMock()
na.__enter__.side_effect = AssertionError
nac.return_value = na
Node.disable(node)
self.assertFalse(node.enabled)
def test_disable_enabled_sub(self):
node = MagicMock(spec=Node, enabled=True)
act = MagicMock()
subact = MagicMock()
act.sub_activity.return_value = subact
Node.disable(node, base_activity=act)
self.assertFalse(node.enabled)
subact.__enter__.assert_called()
def test_flush(self):
node = MagicMock(spec=Node, enabled=True)
user = MagicMock(spec=User)
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
Node.flush(node, user)
na.__enter__.assert_called()
node.disable.assert_called_with(user, act)
for i in insts:
i.migrate.assert_called()
def test_flush_disabled_wo_user(self):
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
Node.flush(node)
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()
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