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 () { ...@@ -137,6 +137,21 @@ $(function () {
return false; 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 */ /* for Group removes buttons */
$('.group-delete').click(function() { $('.group-delete').click(function() {
var group_pk = $(this).data('group-pk'); 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 @@ ...@@ -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> <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"> <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="#" 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> <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>
{% if node.enabled %} <li>
<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:{% 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: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> <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>
{% 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-delete" href="{% url "dashboard.views.delete-node" pk=node.pk %}?next={{ request.path }}"><i class="icon-trash"></i> Delete</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> </ul>
</div> </div>
......
{% load i18n %}
<div class="btn-group"> <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> <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"> <ul class="dropdown-menu nojs-dropdown-toogle" role="menu">
<li><a href="#"><i class="icon-cloud-upload"></i> Flush</a></li> <li><a href="#" class="node-details-rename-button"><i class="icon-pencil"></i> {% trans "Rename" %}</a></li>
{% if record.enabled %} <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="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> <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="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> <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>
{% else %} <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>
<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>
</ul> </ul>
</div> </div>
...@@ -5,12 +5,12 @@ from .views import ( ...@@ -5,12 +5,12 @@ from .views import (
AclUpdateView, DiskAddView, FavouriteView, GroupAclUpdateView, GroupDelete, AclUpdateView, DiskAddView, FavouriteView, GroupAclUpdateView, GroupDelete,
GroupDetailView, GroupList, GroupUserDelete, IndexView, LeaseCreate, GroupDetailView, GroupList, GroupUserDelete, IndexView, LeaseCreate,
LeaseDelete, LeaseDetail, MyPreferencesView, NodeAddTraitView, NodeCreate, LeaseDelete, LeaseDetail, MyPreferencesView, NodeAddTraitView, NodeCreate,
NodeDelete, NodeDetailView, NodeGraphView, NodeList, NodeStatus, NodeDelete, NodeDetailView, NodeFlushView, NodeGraphView, NodeList,
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate, NodeStatus, NotificationView, PortDelete, TemplateAclUpdateView,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView, TemplateCreate, TemplateDelete, TemplateDetail, TemplateList,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView, TransferOwnershipConfirmView, TransferOwnershipView, vm_activity, VmCreate,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView, VmDelete, VmDetailView, VmDetailVncTokenView, VmGraphView, VmList,
VmRenewView, VmMassDelete, VmMigrateView, VmRenewView,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -68,6 +68,8 @@ urlpatterns = patterns( ...@@ -68,6 +68,8 @@ urlpatterns = patterns(
name="dashboard.views.delete-node"), name="dashboard.views.delete-node"),
url(r'^node/status/(?P<pk>\d+)/$', NodeStatus.as_view(), url(r'^node/status/(?P<pk>\d+)/$', NodeStatus.as_view(),
name="dashboard.views.status-node"), 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(), url(r'^node/create/$', NodeCreate.as_view(),
name='dashboard.views.node-create'), name='dashboard.views.node-create'),
......
...@@ -505,7 +505,6 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView): ...@@ -505,7 +505,6 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
return context return context
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
print request.POST
if request.POST.get('new_name'): if request.POST.get('new_name'):
return self.__set_name(request) return self.__set_name(request)
if request.POST.get('change_status') is not None: if request.POST.get('change_status') is not None:
...@@ -1401,6 +1400,42 @@ class NodeStatus(LoginRequiredMixin, SuperuserRequiredMixin, DetailView): ...@@ -1401,6 +1400,42 @@ class NodeStatus(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
return redirect(self.get_success_url()) 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): class PortDelete(LoginRequiredMixin, DeleteView):
model = Rule model = Rule
pk_url_kwarg = 'rule' pk_url_kwarg = 'rule'
......
...@@ -1096,10 +1096,15 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, ...@@ -1096,10 +1096,15 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
return local_tasks.migrate.apply_async(args=[self, to_node, user], return local_tasks.migrate.apply_async(args=[self, to_node, user],
queue="localhost.man") 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. """ """Live migrate running vm to another node. """
with instance_activity(code_suffix='migrate', instance=self, with instance_activity(code_suffix='migrate', instance=self,
task_uuid=task_uuid, user=user) as act: 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 # Destroy networks
with act.sub_activity('destroying_net'): with act.sub_activity('destroying_net'):
for net in self.interface_set.all(): for net in self.interface_set.all():
......
...@@ -13,7 +13,7 @@ from taggit.managers import TaggableManager ...@@ -13,7 +13,7 @@ from taggit.managers import TaggableManager
from common.models import method_cache, WorkerNotFound from common.models import method_cache, WorkerNotFound
from firewall.models import Host from firewall.models import Host
from ..tasks import vm_tasks from ..tasks import vm_tasks, local_tasks
from .common import Trait from .common import Trait
from .activity import node_activity, NodeActivity from .activity import node_activity, NodeActivity
...@@ -106,13 +106,33 @@ class Node(TimeStampedModel): ...@@ -106,13 +106,33 @@ class Node(TimeStampedModel):
def get_status_display(self): def get_status_display(self):
return self.STATES[self.enabled][self.online][1] return self.STATES[self.enabled][self.online][1]
def disable(self, user=None): def disable(self, user=None, base_activity=None):
''' Disable the node.''' ''' Disable the node.'''
if self.enabled: 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.enabled = False
self.save() 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): def enable(self, user=None):
''' Enable the node. ''' ''' Enable the node. '''
if self.enabled is not True: if self.enabled is not True:
......
...@@ -57,3 +57,8 @@ def reboot(instance, user): ...@@ -57,3 +57,8 @@ def reboot(instance, user):
@celery.task @celery.task
def migrate(instance, to_node, user): def migrate(instance, to_node, user):
instance.migrate(to_node, task_uuid=migrate.request.id, user=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 datetime import datetime
from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mock import Mock, MagicMock, patch from mock import Mock, MagicMock, patch, call
from ..models import ( from ..models import (
Lease, Node, Interface, Instance, InstanceTemplate, InstanceActivity, Lease, Node, Interface, Instance, InstanceTemplate, InstanceActivity,
...@@ -68,12 +70,38 @@ class InstanceTestCase(TestCase): ...@@ -68,12 +70,38 @@ class InstanceTestCase(TestCase):
self.assertTrue(inst.destroyed_at) self.assertTrue(inst.destroyed_at)
inst.save.assert_called() 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): class InterfaceTestCase(TestCase):
def test_interface_create(self): def test_interface_create(self):
from firewall.models import Vlan, Domain from firewall.models import Vlan, Domain
from django.contrib.auth.models import User
owner = User() owner = User()
owner.save() owner.save()
i = Instance(id=10, owner=owner, access_method='rdp') i = Instance(id=10, owner=owner, access_method='rdp')
...@@ -165,3 +193,64 @@ class InstanceActivityTestCase(TestCase): ...@@ -165,3 +193,64 @@ class InstanceActivityTestCase(TestCase):
patch('vm.models.activity.timezone.now'): patch('vm.models.activity.timezone.now'):
original_method(iaobj, "test", concurrency_check=False) original_method(iaobj, "test", concurrency_check=False)
ia.save.assert_called() 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