Commit b6892bef by Dudás Ádám

Merge branch 'feature-more-stats' into 'master'

Disk and template stats

related: circle/storagedriver!14

See merge request !391
parents 29172f7e 10849ccc
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"bootbox": "~4.3.0", "bootbox": "~4.3.0",
"intro.js": "0.9.0", "intro.js": "0.9.0",
"favico.js": "~0.3.5", "favico.js": "~0.3.5",
"datatables": "~1.10.4" "datatables": "~1.10.4",
"chart.js": "2.3.0"
} }
} }
...@@ -245,6 +245,12 @@ PIPELINE_JS = { ...@@ -245,6 +245,12 @@ PIPELINE_JS = {
), ),
"output_filename": "vm-detail.js", "output_filename": "vm-detail.js",
}, },
"datastore": {"source_filenames": (
"chart.js/dist/Chart.min.js",
"dashboard/datastore-details.js"
),
"output_filename": "datastore.js",
},
} }
......
...@@ -1563,3 +1563,28 @@ textarea[name="new_members"] { ...@@ -1563,3 +1563,28 @@ textarea[name="new_members"] {
margin: 15px 0 2px 0; margin: 15px 0 2px 0;
} }
} }
#datastore-chart-legend {
width: 350px;
margin-top: 100px;
margin-left: -120px;
/* Landscape phones and down */
@media (max-width: 992px) {
margin-left: -25px;
}
ul {
list-style: none;
}
li {
font-size: 18px;
margin-bottom: 2px;
span {
display: inline-block;
width: 30px;
height: 18px;
margin-right: 8px;
}
}
}
$(function() {
var data = JSON.parse($("#chart-data").data("data"));
var labels = [];
for(var i=0; i<data.labels.length; i++) {
labels.push(data.labels[i] + " (" + data.readable_data[i] + ")");
}
var pieChart = new Chart(document.getElementById("datastore-chart"), {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: data.data,
backgroundColor: [
"#57b257",
"#538ccc",
"#f0df24",
"#ff9a38",
"#7f7f7f",
]
}]
},
options: {
legend: {
display: false,
},
tooltips: {
callbacks: {
label: function(item, chartData) {
return data.labels[item.index] + ": " + data.readable_data[item.index];
}
}
},
}
});
$("#datastore-chart-legend").html(pieChart.generateLegend());
});
{% for t in templates %}
<a href="{{ t.get_absolute_url }}">
{{ t.name }}</a>{% if not forloop.last %},{% endif %}
{% empty %}
-
{% endfor %}
{% extends "dashboard/base.html" %} {% extends "dashboard/base.html" %}
{% load staticfiles %} {% load pipeline %}
{% load sizefieldtags %}
{% load i18n %} {% load i18n %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
...@@ -105,4 +106,58 @@ ...@@ -105,4 +106,58 @@
</div> </div>
</div> </div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
<i class="fa fa-pie-chart"></i>
{% trans "Disk usage breakdown" %}
</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-9">
<canvas id="datastore-chart"></canvas>
</div>
<div class="col-md-3">
<div id="datastore-chart-legend"></div>
</div>
</div>
<div id="chart-data" data-data='{
"data": [{{stats.template_actual_size}},
{{stats.vm_actual_size}},
{{stats.dumps}},
{{stats.iso_raw}},
{{stats.trash}}],
"readable_data": ["{{stats.template_actual_size|filesize}}",
"{{stats.vm_actual_size|filesize}}",
"{{stats.dumps|filesize}}",
"{{stats.dumps|filesize}}",
"{{stats.iso_raw|filesize}}",
"{{stats.trash|filesize}}"],
"labels": ["{% trans "Templates" %}",
"{% trans "Virtual machines" %}",
"{% trans "Memory dumps" %}",
"{% trans "ISO + Raw images" %}",
"{% trans "Trash" %}"]
}
'>
</div>
<div>
{% trans "Total disk usage of virtual machines" %}:
<strong>{{ stats.vm_actual_size|filesize }}</strong>
<br />
{% trans "Total virtual disk usage of virtual machines" %}:
<strong>{{ stats.vm_size|filesize}}</strong>
</div>
</div><!-- .panel-body -->
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{% javascript "datastore" %}
{% endblock %} {% endblock %}
...@@ -63,18 +63,36 @@ ...@@ -63,18 +63,36 @@
</div> </div>
</div> </div>
{% comment %}
<div class="col-md-6"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="no-margin"><i class="fa fa-desktop"></i> Placeholder</h3> <h3 class="no-margin">
<i class="fa fa-puzzle-piece"></i>
{% trans "Rarely used templates" %}
</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
??? <dl>
<dt>{% trans "Never instantiated" %}</dd>
<dd>
{% include "dashboard/_list-templates.html" with templates=unused_templates.never_instantiated %}
</dd>
<dt>{% trans "Templates without running instances" %}</dd>
<dd>
{% include "dashboard/_list-templates.html" with templates=unused_templates.templates_wo_instances %}
</dd>
<dt>{% trans "Templates without instances, last instance created more than 90 days ago" %}</dd>
<dd>
{% include "dashboard/_list-templates.html" with templates=unused_templates.templates_wo_instances_90 %}
</dd>
<dt>{% trans "Templates without instances, last instance created more than 180 days ago" %}</dd>
<dd>
{% include "dashboard/_list-templates.html" with templates=unused_templates.templates_wo_instances_180 %}
</dd>
</dl>
</div> </div>
</div> </div>
</div> </div>
{% endcomment %}
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
...@@ -91,6 +91,7 @@ class StorageDetail(SuperuserRequiredMixin, UpdateView): ...@@ -91,6 +91,7 @@ class StorageDetail(SuperuserRequiredMixin, UpdateView):
return qs return qs
def _get_stats(self): def _get_stats(self):
# datastore stats
stats = self.object.get_statistics() stats = self.object.get_statistics()
free_space = int(stats['free_space']) free_space = int(stats['free_space'])
free_percent = float(stats['free_percent']) free_percent = float(stats['free_percent'])
...@@ -98,11 +99,32 @@ class StorageDetail(SuperuserRequiredMixin, UpdateView): ...@@ -98,11 +99,32 @@ class StorageDetail(SuperuserRequiredMixin, UpdateView):
total_space = free_space / (free_percent/100.0) total_space = free_space / (free_percent/100.0)
used_space = total_space - free_space used_space = total_space - free_space
# file stats
data = self.get_object().get_file_statistics()
dumps_size = sum(d['size'] for d in data['dumps'])
trash = sum(d['size'] for d in data['trash'])
iso_raw = sum(d['size'] for d in data['disks']
if d['format'] in ("iso", "raw"))
vm_size = vm_actual_size = template_actual_size = 0
for d in data['disks']:
if d['format'] == "qcow2" and d['type'] == "normal":
template_actual_size += d['actual_size']
else:
vm_size += d['size']
vm_actual_size += d['actual_size']
return { return {
'used_percent': int(100 - free_percent), 'used_percent': int(100 - free_percent),
'free_space': filesizeformat(free_space), 'free_space': filesizeformat(free_space),
'used_space': filesizeformat(used_space), 'used_space': filesizeformat(used_space),
'total_space': filesizeformat(total_space), 'total_space': filesizeformat(total_space),
'dumps': dumps_size,
'trash': trash,
'iso_raw': iso_raw,
'vm_size': vm_size,
'vm_actual_size': vm_actual_size,
'template_actual_size': template_actual_size,
} }
def get_success_url(self): def get_success_url(self):
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
from datetime import timedelta
import json import json
import logging import logging
...@@ -24,8 +25,10 @@ from django.contrib.auth.models import User ...@@ -24,8 +25,10 @@ from django.contrib.auth.models import User
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
from django.core.exceptions import PermissionDenied, SuspiciousOperation from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.db.models import Count
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, get_object_or_404 from django.shortcuts import redirect, get_object_or_404
from django.utils import timezone
from django.utils.translation import ugettext as _, ugettext_noop from django.utils.translation import ugettext as _, ugettext_noop
from django.views.generic import ( from django.views.generic import (
TemplateView, CreateView, UpdateView, TemplateView, CreateView, UpdateView,
...@@ -36,7 +39,9 @@ from braces.views import ( ...@@ -36,7 +39,9 @@ from braces.views import (
) )
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from vm.models import InstanceTemplate, InterfaceTemplate, Instance, Lease from vm.models import (
InstanceTemplate, InterfaceTemplate, Instance, Lease, InstanceActivity
)
from storage.models import Disk from storage.models import Disk
from ..forms import ( from ..forms import (
...@@ -203,6 +208,41 @@ class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView): ...@@ -203,6 +208,41 @@ class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView):
context['search_form'] = self.search_form context['search_form'] = self.search_form
# templates without any instances
# [t for t in InstanceTemplate.objects.all()
# if t.instance_set.count() < 1]
never_instantiated = context['object_list'].annotate(
instance_count=Count("instance_set")).filter(instance_count__lt=1)
# templates without active virtual machines
active_statuses = Instance.STATUS._db_values - set(["DESTROYED"])
templates_wo_instances = context['object_list'].exclude(
pk__in=InstanceTemplate.objects.filter(
instance_set__status__in=active_statuses)
).exclude(pk__in=never_instantiated)
def get_create_acts_younger_than(days):
return InstanceActivity.objects.filter(
activity_code="vm.Instance.create",
finished__gt=timezone.now() - timedelta(days=days))
# templates without active virtual machines
# last machine started later than 90 days
templates_wo_i_90 = templates_wo_instances.exclude(
instance_set__activity_log__in=get_create_acts_younger_than(90))
# templates without active virtual machines
# last machine started later than 180 days
templates_wo_i_180 = templates_wo_instances.exclude(
instance_set__activity_log__in=get_create_acts_younger_than(180))
context['unused_templates'] = {
'never_instantiated': never_instantiated,
'templates_wo_instances': templates_wo_instances,
'templates_wo_instances_90': templates_wo_i_90,
'templates_wo_instances_180': templates_wo_i_180,
}
return context return context
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
......
...@@ -110,6 +110,13 @@ class DataStore(Model): ...@@ -110,6 +110,13 @@ class DataStore(Model):
disks = Disk.objects.filter(destroyed__isnull=True, is_ready=True) disks = Disk.objects.filter(destroyed__isnull=True, is_ready=True)
return disks.exclude(filename__in=files) return disks.exclude(filename__in=files)
@method_cache(30)
def get_file_statistics(self, timeout=15):
queue_name = self.get_remote_queue_name('storage', "slow")
data = storage_tasks.get_file_statistics.apply_async(
args=[self.path], queue=queue_name).get(timeout=timeout)
return data
class Disk(TimeStampedModel): class Disk(TimeStampedModel):
......
...@@ -81,3 +81,8 @@ def recover_from_trash(datastore, disk_path): ...@@ -81,3 +81,8 @@ def recover_from_trash(datastore, disk_path):
@celery.task(name='storagedriver.get_storage_stat') @celery.task(name='storagedriver.get_storage_stat')
def get_storage_stat(path): def get_storage_stat(path):
pass pass
@celery.task(name='storagedriver.get_file_statistics')
def get_file_statistics(datastore):
pass
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