Added virtualization filters

This commit is contained in:
Jeremy Stretch 2017-08-16 17:00:17 -04:00
parent 4ef55502b4
commit 9acd792abe
10 changed files with 281 additions and 20 deletions

View File

@ -17,7 +17,7 @@
<input type="text" name="q" class="form-control" placeholder="Search virtual machines" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span>
<span class="fa fa-search"></span>
</button>
</span>
</div>
@ -27,13 +27,13 @@
<div class="pull-right">
{% if perms.virtualization.change_virtualmachine %}
<a href="{% url 'virtualization:virtualmachine_edit' pk=vm.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>
<span class="fa fa-pencil"></span>
Edit this VM
</a>
{% endif %}
{% if perms.virtualization.delete_virtualmachine %}
<a href="{% url 'virtualization:virtualmachine_delete' pk=vm.pk %}" class="btn btn-danger">
<span class="fa fa-trash" aria-hidden="true"></span>
<span class="fa fa-trash"></span>
Delete this VM
</a>
{% endif %}
@ -53,7 +53,68 @@
</tr>
<tr>
<td>Cluster</td>
<td><a href="{{ vm.cluster.get_absolute_url }}">{{ vm.cluster }}</a></td>
<td>
{% if vm.cluster.group %}
<a href="{{ vm.cluster.group.get_absolute_url }}">{{ vm.cluster.group }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ vm.cluster.get_absolute_url }}">{{ vm.cluster }}</a>
</td>
</tr>
<tr>
<td>Cluster Type</td>
<td>{{ vm.cluster.type }}</td>
</tr>
<tr>
<td>Tenant</td>
<td>
{% if vm.tenant %}
{% if vm.tenant.group %}
<a href="{{ vm.tenant.group.get_absolute_url }}">{{ vm.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ vm.tenant.get_absolute_url }}">{{ vm.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
</table>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Resources</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Virtual CPUs</td>
<td>
{% if vm.vcpus %}
{{ vm.vcpus }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Memory</td>
<td>
{% if vm.memory %}
{{ vm.memory }} MB
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Disk Space</td>
<td>
{% if vm.disk %}
{{ vm.disk }} GB
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
</table>
</div>

View File

@ -6,13 +6,17 @@
<div class="panel-heading"><strong>Virtual Machine</strong></div>
<div class="panel-body">
{% render_field form.name %}
{% render_field form.cluster_group %}
{% render_field form.cluster %}
{% render_field form.platform %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Cluster</strong></div>
<div class="panel-heading"><strong>Resources</strong></div>
<div class="panel-body">
{% render_field form.cluster %}
{% render_field form.vcpus %}
{% render_field form.memory %}
{% render_field form.disk %}
</div>
</div>
<div class="panel panel-default">

View File

@ -24,3 +24,32 @@
</div>
</div>
{% endblock %}
{% block javascript %}
<script type="text/javascript">
$(document).ready(function() {
var cluster_group_list = $('#id_cluster_group');
var cluster_list = $('#id_cluster_id');
// Update cluster options based on selected group
cluster_group_list.change(function() {
var selected_groups = $(this).val();
if (selected_groups) {
cluster_list.empty();
$.ajax({
url: netbox_api_path + 'virtualization/clusters/?limit=500&group=' + selected_groups.join('&group='),
dataType: 'json',
success: function (response, status) {
$.each(response["results"], function (index, cluster) {
var option = $("<option></option>").attr("value", cluster.id).text(cluster.name);
cluster_list.append(option);
});
}
});
}
});
});
</script>
{% endblock %}

View File

@ -4,6 +4,7 @@ from rest_framework.viewsets import ModelViewSet
from extras.api.views import CustomFieldModelViewSet
from utilities.api import WritableSerializerMixin
from virtualization import filters
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
from . import serializers
@ -26,6 +27,7 @@ class ClusterViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Cluster.objects.select_related('type', 'group')
serializer_class = serializers.ClusterSerializer
write_serializer_class = serializers.WritableClusterSerializer
filter_class = filters.ClusterFilter
#
@ -36,6 +38,7 @@ class VirtualMachineViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = VirtualMachine.objects.all()
serializer_class = serializers.VirtualMachineSerializer
write_serializer_class = serializers.WritableVirtualMachineSerializer
filter_class = filters.VirtualMachineFilter
class VMInterfaceViewSet(WritableSerializerMixin, ModelViewSet):

View File

@ -0,0 +1,99 @@
from __future__ import unicode_literals
import django_filters
from django.db.models import Q
from dcim.models import Platform
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
class ClusterFilter(CustomFieldFilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
)
group_id = NullableModelMultipleChoiceFilter(
queryset=ClusterGroup.objects.all(),
label='Parent group (ID)',
)
group = NullableModelMultipleChoiceFilter(
queryset=ClusterGroup.objects.all(),
to_field_name='slug',
label='Parent group (slug)',
)
type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ClusterType.objects.all(),
label='Cluster type (ID)',
)
type = django_filters.ModelMultipleChoiceFilter(
name='type__slug',
queryset=ClusterType.objects.all(),
to_field_name='slug',
label='Cluster type (slug)',
)
class Meta:
model = Cluster
fields = ['name']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(comments__icontains=value)
)
class VirtualMachineFilter(CustomFieldFilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
)
cluster_group_id = NullableModelMultipleChoiceFilter(
name='cluster__group',
queryset=ClusterGroup.objects.all(),
label='Cluster group (ID)',
)
cluster_group = NullableModelMultipleChoiceFilter(
name='cluster__group__slug',
queryset=ClusterGroup.objects.all(),
to_field_name='slug',
label='Cluster group (slug)',
)
tenant_id = NullableModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = NullableModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
platform_id = NullableModelMultipleChoiceFilter(
queryset=Platform.objects.all(),
label='Platform (ID)',
)
platform = NullableModelMultipleChoiceFilter(
name='platform',
queryset=Platform.objects.all(),
to_field_name='slug',
label='Platform (slug)',
)
class Meta:
model = VirtualMachine
fields = ['name', 'cluster']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(comments__icontains=value)
)

View File

@ -1,11 +1,12 @@
from __future__ import unicode_literals
from django import forms
from django.db.models import Count
from extras.forms import CustomFieldBulkEditForm, CustomFieldForm
from extras.forms import CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import BootstrapMixin, SlugField
from utilities.forms import APISelect, BootstrapMixin, ChainedModelChoiceField, FilterChoiceField, SlugField
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@ -67,15 +68,58 @@ class ClusterCSVForm(forms.ModelForm):
fields = ['name', 'type', 'group']
class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Cluster
q = forms.CharField(required=False, label='Search')
group = FilterChoiceField(
queryset=ClusterGroup.objects.annotate(filter_count=Count('clusters')),
to_field_name='slug',
null_option=(0, 'None'),
required=False,
)
type = FilterChoiceField(
queryset=ClusterType.objects.annotate(filter_count=Count('clusters')),
to_field_name='slug',
required=False,
)
#
# Virtual Machines
#
class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
cluster_group = forms.ModelChoiceField(
queryset=ClusterGroup.objects.all(),
required=False,
widget=forms.Select(
attrs={'filter-for': 'cluster', 'nullable': 'true'}
)
)
cluster = ChainedModelChoiceField(
queryset=Cluster.objects.all(),
chains=(
('group', 'cluster_group'),
),
widget=APISelect(
api_url='/api/virtualization/clusters/?group_id={{cluster_group}}'
)
)
class Meta:
model = VirtualMachine
fields = ['name', 'cluster', 'tenant', 'platform', 'comments']
fields = ['name', 'cluster_group', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments']
def __init__(self, *args, **kwargs):
# Initialize helper selector
instance = kwargs.get('instance')
if instance.pk and instance.cluster is not None:
initial = kwargs.get('initial', {}).copy()
initial['cluster_group'] = instance.cluster.group
kwargs['initial'] = initial
super(VirtualMachineForm, self).__init__(*args, **kwargs)
class VirtualMachineCSVForm(forms.ModelForm):
@ -89,7 +133,7 @@ class VirtualMachineCSVForm(forms.ModelForm):
)
class Meta:
fields = ['cluster', 'name', 'tenant']
fields = ['cluster', 'name', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments']
class VirtualMachineBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@ -99,3 +143,17 @@ class VirtualMachineBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class Meta:
nullable_fields = ['tenant']
class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = VirtualMachine
q = forms.CharField(required=False, label='Search')
cluster_group = FilterChoiceField(
queryset=ClusterGroup.objects.all(),
to_field_name='slug',
null_option=(0, 'None'),
)
cluster_id = FilterChoiceField(
queryset=Cluster.objects.annotate(filter_count=Count('virtual_machines')),
label='Cluster'
)

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-16 19:22
# Generated by Django 1.11.4 on 2017-08-16 19:27
from __future__ import unicode_literals
import dcim.fields
@ -13,9 +13,9 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('dcim', '0041_napalm_integration'),
('ipam', '0018_remove_service_uniqueness_constraint'),
('tenancy', '0003_unicode_literals'),
('dcim', '0041_napalm_integration'),
]
operations = [
@ -63,9 +63,9 @@ class Migration(migrations.Migration):
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=64, unique=True)),
('vcpus', models.PositiveSmallIntegerField(blank=True, verbose_name='vCPUs')),
('memory', models.PositiveIntegerField(blank=True, verbose_name='Memory (MB)')),
('disk', models.PositiveIntegerField(blank=True, verbose_name='Disk (GB)')),
('vcpus', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='vCPUs')),
('memory', models.PositiveIntegerField(blank=True, null=True, verbose_name='Memory (MB)')),
('disk', models.PositiveIntegerField(blank=True, null=True, verbose_name='Disk (GB)')),
('comments', models.TextField(blank=True)),
('cluster', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='virtual_machines', to='virtualization.Cluster')),
('platform', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='virtual_machines', to='dcim.Platform')),

View File

@ -161,14 +161,17 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
)
vcpus = models.PositiveSmallIntegerField(
blank=True,
null=True,
verbose_name='vCPUs'
)
memory = models.PositiveIntegerField(
blank=True,
null=True,
verbose_name='Memory (MB)'
)
disk = models.PositiveIntegerField(
blank=True,
null=True,
verbose_name='Disk (GB)'
)
comments = models.TextField(

View File

@ -77,8 +77,9 @@ class ClusterTable(BaseTable):
class VirtualMachineTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn()
cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')])
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
class Meta(BaseTable.Meta):
model = VirtualMachine
fields = ('pk', 'name', 'tenant')
fields = ('pk', 'name', 'cluster', 'tenant', 'vcpus', 'memory', 'disk')

View File

@ -11,6 +11,7 @@ from utilities.views import (
ObjectDeleteView, ObjectEditView, ObjectListView,
)
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
from . import filters
from . import forms
from . import tables
@ -84,6 +85,8 @@ class ClusterGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class ClusterListView(ObjectListView):
queryset = Cluster.objects.annotate(vm_count=Count('virtual_machines'))
table = tables.ClusterTable
filter = filters.ClusterFilter
filter_form = forms.ClusterFilterForm
template_name = 'virtualization/cluster_list.html'
@ -138,8 +141,8 @@ class ClusterBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class VirtualMachineListView(ObjectListView):
queryset = VirtualMachine.objects.select_related('tenant')
# filter = filters.VirtualMachineFilter
# filter_form = forms.VirtualMachineFilterForm
filter = filters.VirtualMachineFilter
filter_form = forms.VirtualMachineFilterForm
table = tables.VirtualMachineTable
template_name = 'virtualization/virtualmachine_list.html'
@ -184,7 +187,7 @@ class VirtualMachineBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'virtualization.change_virtualmachine'
cls = VirtualMachine
queryset = VirtualMachine.objects.select_related('tenant')
# filter = filters.VirtualMachineFilter
filter = filters.VirtualMachineFilter
table = tables.VirtualMachineTable
form = forms.VirtualMachineBulkEditForm
default_return_url = 'virtualization:virtualmachine_list'