Commit 90122b75 by Őry Máté

Merge branch 'feature-template-list' into 'master'

Template list upgrade and show deleted vms

Closes #234
Closes #258
parents 0963fa06 986782b5
......@@ -1199,7 +1199,12 @@ class VmListSearchForm(forms.Form):
}))
stype = forms.ChoiceField(vm_search_choices, widget=forms.Select(attrs={
'class': "btn btn-default input-tags",
'class': "btn btn-default form-control input-tags",
'style': "min-width: 80px;",
}))
include_deleted = forms.BooleanField(widget=forms.CheckboxInput(attrs={
'id': "vm-list-search-checkbox",
}))
def __init__(self, *args, **kwargs):
......@@ -1209,3 +1214,22 @@ class VmListSearchForm(forms.Form):
data = self.data.copy()
data['stype'] = "all"
self.data = data
class TemplateListSearchForm(forms.Form):
s = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control input-tags",
'placeholder': _("Search...")
}))
stype = forms.ChoiceField(vm_search_choices, widget=forms.Select(attrs={
'class': "btn btn-default input-tags",
}))
def __init__(self, *args, **kwargs):
super(TemplateListSearchForm, self).__init__(*args, **kwargs)
# set initial value, otherwise it would be overwritten by request.GET
if not self.data.get("stype"):
data = self.data.copy()
data['stype'] = "owned"
self.data = data
......@@ -868,10 +868,10 @@ textarea[name="list-new-namelist"] {
padding: 5px 0px;
}
#profile-key-list-table td:last-child, #profile-key-list-table th:last-child,
#profile-key-list-table td:last-child, #profile-key-list-table th:last-child,
#profile-command-list-table td:last-child, #profile-command-list-table th:last-child,
#profile-command-list-table td:nth-child(2), #profile-command-list-table th:nth-child(2) {
text-align: center;
text-align: center;
vertical-align: middle;
}
......@@ -946,3 +946,13 @@ textarea[name="list-new-namelist"] {
#vm-list-search, #vm-mass-ops {
margin-top: 8px;
}
#vm-list-search-checkbox {
margin-top: -1px;
display: inline-block;
vertical-align: middle;
}
#vm-list-search-checkbox-span {
cursor: pointer
}
......@@ -617,3 +617,11 @@ function noJS() {
$('.no-js-hidden').show();
$('.js-hidden').hide();
}
function getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
......@@ -39,6 +39,7 @@ $(function() {
$(".template-list-table thead th").css("cursor", "pointer");
$(".template-list-table th a").on("click", function(event) {
if(!$(this).closest("th").data("sort")) return true;
event.preventDefault();
});
});
......
......@@ -163,9 +163,10 @@ $(function() {
$(this).find('input[type="radio"]').prop("checked", true);
});
if(checkStatusUpdate()) {
if(checkStatusUpdate() || $("#vm-list-table tbody tr").length >= 100) {
updateStatuses(1);
}
});
......@@ -178,6 +179,7 @@ function checkStatusUpdate() {
function updateStatuses(runs) {
var include_deleted = getParameterByName("include_deleted");
$.get("/dashboard/vm/list/?compact", function(result) {
$("#vm-list-table tbody tr").each(function() {
vm = $(this).data("vm-pk");
......@@ -203,7 +205,8 @@ function updateStatuses(runs) {
$(this).find(".node").text(result[vm].node);
}
} else {
$(this).remove();
if(!include_deleted)
$(this).remove();
}
});
......
......@@ -147,13 +147,11 @@ class TemplateListTable(Table):
template_name="dashboard/template-list/column-template-name.html",
attrs={'th': {'data-sort': "string"}}
)
num_cores = Column(
verbose_name=_("Cores"),
attrs={'th': {'data-sort': "int"}}
)
ram_size = TemplateColumn(
"{{ record.ram_size }} MiB",
resources = TemplateColumn(
template_name="dashboard/template-list/column-template-resources.html",
verbose_name=_("Resources"),
attrs={'th': {'data-sort': "int"}},
order_by=("ram_size"),
)
lease = TemplateColumn(
"{{ record.lease.name }}",
......@@ -171,11 +169,14 @@ class TemplateListTable(Table):
verbose_name=_("Owner"),
attrs={'th': {'data-sort': "string"}}
)
created = TemplateColumn(
template_name="dashboard/template-list/column-template-created.html",
verbose_name=_("Created at"),
)
running = TemplateColumn(
template_name="dashboard/template-list/column-template-running.html",
verbose_name=_("Running"),
attrs={'th': {'data-sort': "int"}},
orderable=False,
)
actions = TemplateColumn(
verbose_name=_("Actions"),
......@@ -188,8 +189,8 @@ class TemplateListTable(Table):
model = InstanceTemplate
attrs = {'class': ('table table-bordered table-striped table-hover'
' template-list-table')}
fields = ('name', 'num_cores', 'ram_size', 'system',
'access_method', 'lease', 'owner', 'running', 'actions', )
fields = ('name', 'resources', 'system', 'access_method', 'lease',
'owner', 'created', 'running', 'actions', )
prefix = "template-"
......
{% load i18n %}
{% if user and user.pk %}
{% if user.get_full_name %}
{{ user.get_full_name }}
{% else %}
{{ user.username }}
{% endif %}
{% if user.get_full_name %}{{ user.get_full_name }}{% else %}{{ user.username }}{% endif %}{% if new_line %}<br />{% endif %}
{% if show_org %}
{% if user.profile and user.profile.org_id %}
......
......@@ -70,7 +70,7 @@
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a href="{% url "dashboard.views.connect-command-create" %}"
<a href="{% url "dashboard.views.connect-command-create" %}"
class="pull-right btn btn-success btn-xs" style="margin-right: 10px;">
<i class="fa fa-plus"></i> {% trans "add command template" %}
</a>
......@@ -82,4 +82,4 @@
</div>
</div>
</div>
{% endblock %}
{% endblock %}
......@@ -16,6 +16,23 @@
<h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Templates" %}</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-offset-8 col-md-4" id="template-list-search">
<form action="" method="GET">
<div class="input-group">
{{ search_form.s }}
<div class="input-group-btn">
{{ search_form.stype }}
<button type="submit" class="btn btn-primary input-tags">
<i class="fa fa-search"></i>
</button>
</div>
</div><!-- .input-group -->
</form>
</div><!-- .col-md-4 #template-list-search -->
</div>
</div>
<div class="panel-body">
{% render_table table %}
</div>
</div>
......
{% load i18n %}
<a href="{% url "dashboard.views.vm-create" %}?template={{ record.pk }}"
class="btn btn-success btn-xs customize-vm" title="{% trans "Start" %}">
<i class="fa fa-play"></i>
</a>
<a href="{% url "dashboard.views.template-detail" pk=record.pk%}" id="template-list-edit-button" class="btn btn-default btn-xs" title="{% trans "Edit" %}">
<i class="fa fa-edit"></i>
</a>
......
{{ record.created|date }}
<br />
{{ record.created|time }}
{% include "dashboard/_display-name.html" with user=record.owner show_org=True %}
{% include "dashboard/_display-name.html" with user=record.owner show_org=True new_line=True %}
{% load i18n %}
{{ record.ram_size }}MiB RAM
<br />
{% blocktrans with num_cores=record.num_cores count count=record.num_cores %}
{{ num_cores }} CPU core
{% plural %}
{{ num_cores }} CPU cores
{% endblocktrans %}
<a href="{% url "dashboard.views.vm-list" %}?s=template:{{ record.pk }}%20status:running">
{{ record.get_running_instances.count }}
{{ record.running }}
</a>
......@@ -33,6 +33,12 @@
{{ search_form.s }}
<div class="input-group-btn">
{{ search_form.stype }}
</div>
<label class="input-group-addon input-tags" title="{% trans "Include deleted VMs" %}"
id="vm-list-search-checkbox-span" data-container="body">
{{ search_form.include_deleted }}
</label>
<div class="input-group-btn">
<button type="submit" class="btn btn-primary input-tags">
<i class="fa fa-search"></i>
</button>
......@@ -62,10 +68,20 @@
{% trans "Owner" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="owner" %}
</th>
{% if user.is_superuser %}<th data-sort="string" class="orderable sortable">
{% trans "Node" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="node" %}
</th>{% endif %}
<th data-sort="string" class="orderable sortable">
{% trans "Lease" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="lease" %}
</th>
{% if user.is_superuser %}
<th data-sort="string" class="orderable sortable">
{% trans "IP address" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="ip_addr" %}
</th>
<th data-sort="string" class="orderable sortable">
{% trans "Node" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="node" %}
</th>
{% endif %}
</tr></thead><tbody>
{% for i in object_list %}
<tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}">
......@@ -73,16 +89,27 @@
<td class="name"><a class="real-link" href="{% url "dashboard.views.detail" i.pk %}">{{ i.name }}</a> </td>
<td class="state">
<i class="fa fa-fw
{% if i.is_in_status_change %}
{% if show_acts_in_progress and i.is_in_status_change %}
fa-spin fa-spinner
{% else %}
{{ i.get_status_icon }}{% endif %}"></i>
<span>{{ i.get_status_display }}</span>
</td>
<td>
{% include "dashboard/_display-name.html" with user=i.owner show_org=True %}
{% if i.owner.profile %}
{{ i.owner.profile.get_display_name }}
{% else %}
{{ i.owner.username }}
{% endif %}
{# include "dashboard/_display-name.html" with user=i.owner show_org=True #}
</td>
<td class="lease "data-sort-value="{{ i.lease.name }}">
{{ i.lease.name }}
</td>
{% if user.is_superuser %}
<td class="ip_addr "data-sort-value="{{ i.ipv4 }}">
{{ i.ipv4|default:"-" }}
</td>
<td class="node "data-sort-value="{{ i.node.normalized_name }}">
{{ i.node.name|default:"-" }}
</td>
......@@ -90,7 +117,7 @@
</tr>
{% empty %}
<tr>
<td colspan="5">
<td colspan="7">
{% if request.GET.s %}
<strong>{% trans "No result." %}</strong>
{% else %}
......
......@@ -70,7 +70,8 @@ from .forms import (
VmSaveForm, UserKeyForm, VmRenewForm, VmStateChangeForm,
CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
TraitsForm, RawDataForm, GroupPermissionForm, AclUserAddForm,
VmResourcesForm, VmAddInterfaceForm, VmListSearchForm, ConnectCommandForm
VmResourcesForm, VmAddInterfaceForm, VmListSearchForm,
TemplateListSearchForm, ConnectCommandForm
)
from .tables import (
......@@ -173,6 +174,65 @@ class FilterMixin(object):
return super(FilterMixin,
self).get_queryset().filter(**self.get_queryset_filters())
def create_fake_get(self):
self.request.GET = self._parse_get(self.request.GET)
def _parse_get(self, GET_dict):
"""
Returns a new dict from request's GET dict to filter the vm list
For example: "name:xy node:1" updates the GET dict
to resemble this URL ?name=xy&node=1
"name:xy node:1".split(":") becomes ["name", "xy node", "1"]
we pop the the first element and use it as the first dict key
then we iterate over the rest of the list and split by the last
whitespace, the first part of this list will be the previous key's
value, then last part of the list will be the next key.
The final dict looks like this: {'name': xy, 'node':1}
>>> f = FilterMixin()
>>> o = f._parse_get({'s': "hello"}).items()
>>> sorted(o) # doctest: +ELLIPSIS
[(u'name', u'hello'), (...)]
>>> o = f._parse_get({'s': "name:hello owner:test"}).items()
>>> sorted(o) # doctest: +ELLIPSIS
[(u'name', u'hello'), (u'owner', u'test'), (...)]
>>> o = f._parse_get({'s': "name:hello ws node:node 3 oh"}).items()
>>> sorted(o) # doctest: +ELLIPSIS
[(u'name', u'hello ws'), (u'node', u'node 3 oh'), (...)]
"""
s = GET_dict.get("s")
fake = GET_dict.copy()
if s:
s = s.split(":")
if len(s) < 2: # if there is no ':' in the string, filter by name
got = {'name': s[0]}
else:
latest = s.pop(0)
got = {'%s' % latest: None}
for i in s[:-1]:
new = i.rsplit(" ", 1)
got[latest] = new[0]
latest = new[1] if len(new) > 1 else None
got[latest] = s[-1]
# generate a new GET request, that is kinda fake
for k, v in got.iteritems():
fake[k] = v
return fake
def create_acl_queryset(self, model):
cleaned_data = self.search_form.cleaned_data
stype = cleaned_data.get('stype', "all")
superuser = stype == "all"
shared = stype == "shared"
level = "owner" if stype == "owned" else "user"
queryset = model.get_objects_with_level(
level, self.request.user,
group_also=shared, disregard_superuser=not superuser,
)
return queryset
class IndexView(LoginRequiredMixin, TemplateView):
template_name = "dashboard/index.html"
......@@ -1641,23 +1701,60 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return kwargs
class TemplateList(LoginRequiredMixin, SingleTableView):
class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView):
template_name = "dashboard/template-list.html"
model = InstanceTemplate
table_class = TemplateListTable
table_pagination = False
allowed_filters = {
'name': "name__icontains",
'tags[]': "tags__name__in",
'tags': "tags__name__in", # for search string
'owner': "owner__username",
'ram': "ram_size",
'ram_size': "ram_size",
'cores': "num_cores",
'num_cores': "num_cores",
'access_method': "access_method__iexact",
}
def get_context_data(self, *args, **kwargs):
context = super(TemplateList, self).get_context_data(*args, **kwargs)
context['lease_table'] = LeaseListTable(Lease.objects.all(),
request=self.request)
context['lease_table'] = LeaseListTable(
Lease.get_objects_with_level("user", self.request.user),
request=self.request)
context['search_form'] = self.search_form
return context
def get(self, *args, **kwargs):
self.search_form = TemplateListSearchForm(self.request.GET)
self.search_form.full_clean()
return super(TemplateList, self).get(*args, **kwargs)
def create_acl_queryset(self, model):
queryset = super(TemplateList, self).create_acl_queryset(model)
sql = ("SELECT count(*) FROM vm_instance WHERE "
"vm_instance.template_id = vm_instancetemplate.id and "
"vm_instance.destroyed_at is null and "
"vm_instance.status = 'RUNNING'")
queryset = queryset.extra(select={'running': sql})
return queryset
def get_queryset(self):
logger.debug('TemplateList.get_queryset() called. User: %s',
unicode(self.request.user))
return InstanceTemplate.get_objects_with_level(
'user', self.request.user).all()
qs = self.create_acl_queryset(InstanceTemplate)
self.create_fake_get()
try:
qs = qs.filter(**self.get_queryset_filters()).distinct()
except ValueError:
messages.error(self.request, _("Error during filtering."))
return qs.select_related("lease", "owner", "owner__profile")
class TemplateDelete(LoginRequiredMixin, DeleteView):
......@@ -1715,6 +1812,7 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
else:
context['ops'].append(v)
context['search_form'] = self.search_form
context['show_acts_in_progress'] = self.object_list.count() < 100
return context
def get(self, *args, **kwargs):
......@@ -1759,10 +1857,16 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
content_type="application/json",
)
def create_acl_queryset(self, model):
queryset = super(VmList, self).create_acl_queryset(model)
if not self.search_form.cleaned_data.get("include_deleted"):
queryset = queryset.filter(destroyed_at=None)
return queryset
def get_queryset(self):
logger.debug('VmList.get_queryset() called. User: %s',
unicode(self.request.user))
queryset = self.create_default_queryset()
queryset = self.create_acl_queryset(Instance)
self.create_fake_get()
sort = self.request.GET.get("sort")
......@@ -1774,54 +1878,9 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
queryset = queryset.order_by(sort)
return queryset.filter(
**self.get_queryset_filters()).select_related('owner', 'node'
).distinct()
def create_default_queryset(self):
cleaned_data = self.search_form.cleaned_data
stype = cleaned_data.get('stype', "all")
superuser = stype == "all"
shared = stype == "shared"
level = "owner" if stype == "owned" else "user"
queryset = Instance.get_objects_with_level(
level, self.request.user,
group_also=shared, disregard_superuser=not superuser,
).filter(destroyed_at=None)
return queryset
def create_fake_get(self):
"""
Updates the request's GET dict to filter the vm list
For example: "name:xy node:1" updates the GET dict
to resemble this URL ?name=xy&node=1
"name:xy node:1".split(":") becomes ["name", "xy node", "1"]
we pop the the first element and use it as the first dict key
then we iterate over the rest of the list and split by the last
whitespace, the first part of this list will be the previous key's
value, then last part of the list will be the next key.
The final dict looks like this: {'name': xy, 'node':1}
"""
s = self.request.GET.get("s")
if s:
s = s.split(":")
if len(s) < 2: # if there is no ':' in the string, filter by name
got = {'name': s[0]}
else:
latest = s.pop(0)
got = {'%s' % latest: None}
for i in s[:-1]:
new = i.rsplit(" ", 1)
got[latest] = new[0]
latest = new[1] if len(new) > 1 else None
got[latest] = s[-1]
# generate a new GET request, that is kinda fake
fake = self.request.GET.copy()
for k, v in got.iteritems():
fake[k] = v
self.request.GET = fake
**self.get_queryset_filters()).prefetch_related(
"owner", "node", "owner__profile", "interface_set", "lease",
"interface_set__host").distinct()
class NodeList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
......
......@@ -513,7 +513,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def ipv4(self):
"""Primary IPv4 address of the instance.
"""
return self.primary_host.ipv4 if self.primary_host else None
# return self.primary_host.ipv4 if self.primary_host else None
for i in self.interface_set.all():
if i.host:
return i.host.ipv4
return None
@property
def ipv6(self):
......
......@@ -14,4 +14,3 @@ post-stop script
stop mancelery
stop slowcelery
end script
......@@ -12,4 +12,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 10
end script
......@@ -12,4 +12,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.moncelery worker --autoreload --loglevel=info --hostname=moncelery -B -c 3
end script
......@@ -12,4 +12,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec /home/cloud/.virtualenvs/circle/bin/uwsgi --chdir=/home/cloud/circle/circle -H /home/cloud/.virtualenvs/circle --socket /tmp/uwsgi.sock --wsgi-file circle/wsgi.py --chmod-socket=666
end script
......@@ -14,4 +14,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py runserver '[::]:8080'
end script
......@@ -12,4 +12,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.slowcelery worker --autoreload --loglevel=info --hostname=slowcelery -B -c 5
end script
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