Commit 3d8b3e44 by Bach Dániel

Merge branch issue-107

Fix VM scalability problems #107

 template based view instead of tables2
 revise columns
 add i18n to template
 cache state to Instance.state on activity saves
 tests (92% coverage)
parents e60ac87d 50a6f530
......@@ -1352,7 +1352,7 @@
"pk": 1,
"model": "vm.instance",
"fields": {
"destroyed": null,
"destroyed_at": null,
"disks": [
1
],
......@@ -1383,7 +1383,7 @@
"pk": 12,
"model": "vm.instance",
"fields": {
"destroyed": null,
"destroyed_at": null,
"disks": [],
"boot_menu": false,
"owner": 1,
......
......@@ -24,7 +24,7 @@
{% elif node.state == 'MISSING' %}label-danger
{% elif node.state == 'DISABLED' %}label-warning
{% elif node.state == 'OFFLINE' %}label-warning
{% endif %}">{{ node.state }}</span>
{% endif %}">{{ node.get_status_display|upper }}</span>
<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">
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin"><i class="icon-desktop"></i> Your virtual machines</h3>
<h3 class="no-margin"><i class="icon-desktop"></i>{% trans "Virtual machines" %}</h3>
</div>
<div class="pull-right" style="max-width: 250px; margin-top: 15px; margin-right: 15px;">
<form action="" method="GET" class="input-group">
<input type="text" name="s"{% if request.GET.s %} value="{{ request.GET.s }}"{% endif %} class="form-control input-tags" placeholder="Search..." />
<input type="text" name="s"{% if request.GET.s %} value="{{ request.GET.s }}"{% endif %} class="form-control input-tags" placeholder="{% trans "Search..."%}" />
<div class="input-group-btn">
<button type="submit" class="form-control btn btn-primary input-tags" title="search"><i class="icon-search"></i></button>
</div>
......@@ -19,31 +18,47 @@
</div>
<div class="panel-body vm-list-group-control">
<p>
<strong>Group actions</strong>
<button id="vm-list-group-select-all" class="btn btn-info btn-xs">Select all</button>
<a class="btn btn-default btn-xs" id="vm-list-group-migrate" disabled><i class="icon-truck"></i> Migrate</a>
<a disabled href="#" class="btn btn-default btn-xs"><i class="icon-refresh"></i> Reboot</a>
<a disabled href="#" class="btn btn-default btn-xs"><i class="icon-off"></i> Shutdown</a>
<a id="vm-list-group-delete" disabled href="#" class="btn btn-danger btn-xs"><i class="icon-remove"></i> Discard</a>
<strong>{% trans "Group actions" %}</strong>
<button id="vm-list-group-select-all" class="btn btn-info btn-xs">{% trans "Select all" %}</button>
<a class="btn btn-default btn-xs" id="vm-list-group-migrate" disabled><i class="icon-truck"></i> {% trans "Migrate" %}</a>
<a disabled href="#" class="btn btn-default btn-xs"><i class="icon-refresh"></i> {% trans "Reboot" %}</a>
<a disabled href="#" class="btn btn-default btn-xs"><i class="icon-off"></i> {% trans "Shutdown" %}</a>
<a id="vm-list-group-delete" disabled href="#" class="btn btn-danger btn-xs"><i class="icon-remove"></i> {% trans "Destroy" %}</a>
</p>
</div>
<div class="panel-body">
{% render_table table %}
</div>
<table class="table table-bordered table-striped table-hover vm-list-table">
<thead><tr>
<th class="orderable pk sortable vm-list-table-thin"><a href="?sort=pk">{% trans "ID" %}</a></th>
<th class="name orderable sortable"><a href="?sort=name">{% trans "Name" %}</a></th>
<th>{% trans "State" %}</th>
<th class="orderable sortable"><a href="?sort=owner">{% trans "Owner" %}</a></th>
{% if user.is_superuser %}<th class="orderable sortable"><a href="?sort=node">{% trans "Node" %}</a></th>{% endif %}
</tr></thead><tbody>
{% for i in object_list %}
<tr class="{% cycle 'odd' 'even' %}">
<td class="pk"><div id="vm-{{i.pk}}">{{i.pk}}</div> </td>
<td class="name"><a class="real-link" href="{% url "dashboard.views.detail" i.pk %}">{{ i.name }}</a> </td>
<td class="state">{{ i.get_status_display }}</td>
<td>{{ i.owner }}</td>
{% if user.is_superuser %}<td>{{ i.node.name|default:"-" }}</td>{% endif %}
</tr>
{% empty %}
<tr><td colspan="4">{% trans "You have no virtual machines." %}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="alert alert-info">
Tip #1: you can select multiple vm instances while holding down the <strong>CTRL</strong> key!
</div>
<div class="alert alert-info">
Tip #2: if you want to select multiple instances by one click select an instance then hold down <strong>SHIFT</strong> key and select another one!
{% trans "You can select multiple vm instances while holding down the <strong>CTRL</strong> key." %}
{% trans "If you want to select multiple instances by one click select an instance then hold down <strong>SHIFT</strong> key and select another one!" %}
</div>
<style>
.popover {
max-width: 600px;
......
......@@ -20,7 +20,7 @@ from django.shortcuts import redirect, render, get_object_or_404
from django.views.decorators.http import require_GET
from django.views.generic.detail import SingleObjectMixin
from django.views.generic import (TemplateView, DetailView, View, DeleteView,
UpdateView, CreateView)
UpdateView, CreateView, ListView)
from django.contrib import messages
from django.utils.translation import ugettext as _
from django.template.defaultfilters import title as title_filter
......@@ -37,7 +37,7 @@ from .forms import (
CircleAuthenticationForm, DiskAddForm, HostForm, LeaseForm, NodeForm,
TemplateForm, TraitForm, VmCustomizeForm,
)
from .tables import (VmListTable, NodeListTable, NodeVmListTable,
from .tables import (NodeListTable, NodeVmListTable,
TemplateListTable, LeaseListTable, GroupListTable,)
from vm.models import (
Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
......@@ -89,7 +89,7 @@ class IndexView(LoginRequiredMixin, TemplateView):
favs = Instance.objects.filter(favourite__user=self.request.user)
instances = Instance.get_objects_with_level(
'user', user).filter(destroyed=None)
'user', user).filter(destroyed_at=None)
display = list(favs) + list(set(instances) - set(favs))
for d in display:
d.fav = True if d in favs else False
......@@ -118,13 +118,13 @@ class IndexView(LoginRequiredMixin, TemplateView):
}
})
running = [i for i in instances if i.state == 'RUNNING']
stopped = [i for i in instances if i.state not in ['RUNNING',
'NOSTATE']]
running = instances.filter(status='RUNNING')
stopped = instances.exclude(status__in=('RUNNING', 'NOSTATE'))
context.update({
'running_vms': running,
'running_vm_num': len(running),
'stopped_vm_num': len(stopped)
'running_vms': running[:20],
'running_vm_num': running.count(),
'stopped_vm_num': stopped.count()
})
context['templates'] = InstanceTemplate.objects.all()[:5]
......@@ -198,10 +198,11 @@ class VmDetailView(CheckedDetailView):
})
# activity data
ia = InstanceActivity.objects.filter(
instance=self.object, parent=None
).order_by('-started').select_related()
context['activities'] = ia
context['activities'] = (
InstanceActivity.objects.filter(
instance=self.object, parent=None).
order_by('-started').
select_related('user').prefetch_related('children'))
context['vlans'] = Vlan.get_objects_with_level(
'user', self.request.user
......@@ -884,11 +885,8 @@ class TemplateDelete(LoginRequiredMixin, DeleteView):
return HttpResponseRedirect(success_url)
class VmList(LoginRequiredMixin, SingleTableView):
class VmList(LoginRequiredMixin, ListView):
template_name = "dashboard/vm-list.html"
table_class = VmListTable
table_pagination = False
model = Instance
def get(self, *args, **kwargs):
if self.request.is_ajax():
......@@ -896,7 +894,7 @@ class VmList(LoginRequiredMixin, SingleTableView):
favourite__user=self.request.user).values_list('pk', flat=True)
instances = Instance.get_objects_with_level(
'user', self.request.user).filter(
destroyed=None).all()
destroyed_at=None).all()
instances = [{
'pk': i.pk,
'name': i.name,
......@@ -913,11 +911,11 @@ class VmList(LoginRequiredMixin, SingleTableView):
logger.debug('VmList.get_queryset() called. User: %s',
unicode(self.request.user))
queryset = Instance.get_objects_with_level(
'user', self.request.user).filter(destroyed=None)
'user', self.request.user).filter(destroyed_at=None)
s = self.request.GET.get("s")
if s:
queryset = queryset.filter(name__icontains=s)
return queryset
return queryset.select_related('owner', 'node')
class NodeList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
......
......@@ -162,7 +162,7 @@ class DomainDetail(LoginRequiredMixin, SuperuserRequiredMixin,
domain=self.object,
host__in=Host.objects.filter(
interface__in=Interface.objects.filter(
instance__destroyed=None)
instance__destroyed_at=None)
)
)
context['record_list'] = SmallRecordTable(q)
......@@ -618,7 +618,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin,
context = super(VlanDetail, self).get_context_data(**kwargs)
q = Host.objects.filter(interface__in=Interface.objects.filter(
vlan=self.object, instance__destroyed=None
vlan=self.object, instance__destroyed_at=None
))
context['host_list'] = SmallHostTable(q)
......
......@@ -79,6 +79,11 @@ class InstanceActivity(ActivityModel):
act = self.create_sub(code_suffix, task_uuid)
return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit)
def save(self, *args, **kwargs):
ret = super(InstanceActivity, self).save(*args, **kwargs)
self.instance._update_status()
return ret
@contextmanager
def instance_activity(code_suffix, instance, on_abort=None, on_commit=None,
......
......@@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals
from datetime import timedelta
from logging import getLogger
from importlib import import_module
from warnings import warn
import string
import django.conf
......@@ -16,7 +17,8 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from celery.exceptions import TimeLimitExceeded
from model_utils.models import TimeStampedModel
from model_utils import Choices
from model_utils.models import TimeStampedModel, StatusModel
from taggit.managers import TaggableManager
from acl.models import AclBase
......@@ -69,7 +71,7 @@ class InstanceActiveManager(Manager):
def get_query_set(self):
return super(InstanceActiveManager,
self).get_query_set().filter(destroyed=None)
self).get_query_set().filter(destroyed_at=None)
class VirtualMachineDescModel(BaseResourceConfigModel):
......@@ -161,7 +163,8 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
return ('dashboard.views.template-detail', None, {'pk': self.pk})
class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
class Instance(AclBase, VirtualMachineDescModel, StatusModel,
TimeStampedModel):
"""Virtual machine instance.
"""
......@@ -170,6 +173,15 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
('operator', _('operator')), # console, networking, change state
('owner', _('owner')), # superuser, can delete, delegate perms
)
STATUS = Choices(
('NOSTATE', _('no state')),
('RUNNING', _('running')),
('STOPPED', _('stopped')),
('SUSPENDED', _('suspended')),
('ERROR', _('error')),
('PENDING', _('pending')),
('DESTROYED', _('destroyed')),
)
name = CharField(blank=True, max_length=100, verbose_name=_('name'),
help_text=_("Human readable name of instance."))
description = TextField(blank=True, verbose_name=_('description'))
......@@ -202,7 +214,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
help_text=_("TCP port where VNC console listens."),
unique=True, verbose_name=_('vnc_port'))
owner = ForeignKey(User)
destroyed = DateTimeField(blank=True, null=True,
destroyed_at = DateTimeField(blank=True, null=True,
help_text=_("The virtual machine's time of "
"destruction."))
objects = Manager()
......@@ -258,7 +270,22 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
@property
def state(self):
"""State of the virtual machine instance.
warn('Use Instance.status (or get_status_display) instead.',
DeprecationWarning)
return self.status
def _update_status(self):
"""Set the proper status of the instance to Instance.status.
"""
old = self.status
self.status = self._compute_status()
if old != self.status:
logger.info('Status of Instance#%d changed to %s',
self.pk, self.status)
self.save()
def _compute_status(self):
"""Return the proper status of the instance based on activities.
"""
# check special cases
if self.activity_log.filter(activity_code__endswith='migrate',
......@@ -745,7 +772,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
asynchronously.
:type task_uuid: str
"""
if self.destroyed:
if self.destroyed_at:
raise self.InstanceDestroyedError(self)
def __on_commit(activity):
......@@ -893,7 +920,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
asynchronously.
:type task_uuid: str
"""
if self.destroyed:
if self.destroyed_at:
return # already destroyed, nothing to do here
def __on_commit(activity):
......@@ -913,7 +940,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
self.__cleanup_after_destroy_vm(act)
self.destroyed = timezone.now()
self.destroyed_at = timezone.now()
self.save()
def destroy_async(self, user=None):
......
......@@ -59,7 +59,7 @@ class Interface(Model):
@property
def destroyed(self):
return self.instance.destroyed
return self.instance.destroyed_at
@property
def mac(self):
......
......@@ -91,18 +91,20 @@ class Node(TimeStampedModel):
num_cores = property(get_num_cores)
@property
def state(self):
STATES = {False: {False: ('OFFLINE', _('offline')),
True: ('DISABLED', _('disabled'))},
True: {False: ('MISSING', _('missing')),
True: ('ONLINE', _('online'))}}
def get_state(self):
"""The state combined of online and enabled attributes.
"""
if self.enabled and self.online:
return 'ONLINE'
elif self.enabled and not self.online:
return 'MISSING'
elif not self.enabled and self.online:
return 'DISABLED'
else:
return 'OFFLINE'
return self.STATES[self.enabled][self.online][0]
state = property(get_state)
def get_status_display(self):
return self.STATES[self.enabled][self.online][1]
def disable(self, user=None):
''' Disable the node.'''
......
......@@ -25,7 +25,7 @@ def garbage_collector(timeout=15):
:type timeout: int
"""
now = timezone.now()
for i in Instance.objects.filter(destroyed=None).all():
for i in Instance.objects.filter(destroyed_at=None).all():
if i.time_of_delete and now > i.time_of_delete:
i.destroy_async()
logger.info("Expired instance %d destroyed.", i.pk)
......
from datetime import datetime
from django.test import TestCase
from django.utils.translation import ugettext_lazy as _
from mock import Mock, MagicMock, patch
from ..models.common import (
Lease
)
from ..models.instance import (
find_unused_port, InstanceTemplate, Instance, ActivityInProgressError
)
from ..models.network import (
Interface
from ..models import (
Lease, Node, Interface, Instance, InstanceTemplate,
)
from ..models.instance import find_unused_port, ActivityInProgressError
class PortFinderTestCase(TestCase):
......@@ -50,6 +47,27 @@ class InstanceTestCase(TestCase):
self.assertEquals(inst.node, node)
self.assertEquals(inst.vnc_port, port)
def test_deploy_destroyed(self):
inst = Mock(destroyed_at=datetime.now(), spec=Instance,
InstanceDestroyedError=Instance.InstanceDestroyedError)
with self.assertRaises(Instance.InstanceDestroyedError):
Instance.deploy(inst)
def test_destroy_destroyed(self):
inst = Mock(destroyed_at=datetime.now(), spec=Instance)
Instance.destroy(inst)
self.assertFalse(inst.save.called)
def test_destroy_sets_destroyed(self):
inst = MagicMock(destroyed_at=None, spec=Instance)
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)
self.assertTrue(inst.destroyed_at)
inst.save.assert_called()
class InterfaceTestCase(TestCase):
......@@ -89,3 +107,14 @@ class LeaseTestCase(TestCase):
l.suspend_interval = None
assert "never" in unicode(l)
class NodeTestCase(TestCase):
def test_state(self):
node = Mock(spec=Node)
node.online = True
node.enabled = True
node.STATES = Node.STATES
self.assertEqual(Node.get_state(node), "ONLINE")
assert isinstance(Node.get_status_display(node), _("").__class__)
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