Merge pull request #1668 from digitalocean/develop

Release v2.2.3
This commit is contained in:
Jeremy Stretch 2017-10-31 14:02:15 -04:00 committed by GitHub
commit 3067c3f262
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 383 additions and 175 deletions

View File

@ -94,6 +94,8 @@ The following methods are available to log results within a report:
The recording of one or more failure messages will automatically flag a report as failed. It is advised to log a success for each object that is evaluated so that the results will reflect how many objects are being reported on. (The inclusion of a log message is optional for successes.) Messages recorded with `log()` will appear in a report's results but are not associated with a particular object or status. The recording of one or more failure messages will automatically flag a report as failed. It is advised to log a success for each object that is evaluated so that the results will reflect how many objects are being reported on. (The inclusion of a log message is optional for successes.) Messages recorded with `log()` will appear in a report's results but are not associated with a particular object or status.
To perform additional tasks, such as sending an email or calling a webhook, after a report has been run, extend the `post_run()` method. The status of the report is available as `self.failed` and the results object is `self.result`.
Once you have created a report, it will appear in the reports list. Initially, reports will have no results associated with them. To generate results, run the report. Once you have created a report, it will appear in the reports list. Initially, reports will have no results associated with them. To generate results, run the report.
## Running Reports ## Running Reports

View File

@ -7,7 +7,7 @@ from django.db.models import Q
from dcim.models import Site from dcim.models import Site
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter from utilities.filters import NumericInFilter
from .models import Provider, Circuit, CircuitTermination, CircuitType from .models import Provider, Circuit, CircuitTermination, CircuitType
@ -78,11 +78,11 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Circuit type (slug)', label='Circuit type (slug)',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = NullableModelMultipleChoiceFilter( tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',

View File

@ -3,6 +3,8 @@ from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from django.utils.safestring import mark_safe
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import Circuit, CircuitType, Provider from .models import Circuit, CircuitType, Provider
@ -14,6 +16,21 @@ CIRCUITTYPE_ACTIONS = """
""" """
class CircuitTerminationColumn(tables.Column):
def render(self, value):
if value.interface:
return mark_safe('<a href="{}" title="{}">{}</a>'.format(
value.interface.device.get_absolute_url(),
value.site,
value.interface.device
))
return mark_safe('<a href="{}">{}</a>'.format(
value.site.get_absolute_url(),
value.site
))
# #
# Providers # Providers
# #
@ -61,15 +78,9 @@ class CircuitTable(BaseTable):
cid = tables.LinkColumn(verbose_name='ID') cid = tables.LinkColumn(verbose_name='ID')
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')]) provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
a_side = tables.LinkColumn( termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side')
'dcim:site', accessor=Accessor('termination_a.site'), orderable=False, termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side')
args=[Accessor('termination_a.site.slug')]
)
z_side = tables.LinkColumn(
'dcim:site', accessor=Accessor('termination_z.site'), orderable=False,
args=[Accessor('termination_z.site.slug')]
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Circuit model = Circuit
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description') fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'termination_a', 'termination_z', 'description')

View File

@ -134,7 +134,11 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class CircuitListView(ObjectListView): class CircuitListView(ObjectListView):
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site') queryset = Circuit.objects.select_related(
'provider', 'type', 'tenant'
).prefetch_related(
'terminations__site', 'terminations__interface__device'
)
filter = filters.CircuitFilter filter = filters.CircuitFilter
filter_form = forms.CircuitFilterForm filter_form = forms.CircuitFilterForm
table = tables.CircuitTable table = tables.CircuitTable

View File

@ -221,7 +221,7 @@ class WritableRackReservationSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = ['id', 'rack', 'units', 'description'] fields = ['id', 'rack', 'units', 'user', 'description']
# #

View File

@ -9,7 +9,7 @@ from django.db.models import Q
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NullableCharFieldFilter, NullableModelMultipleChoiceFilter, NumericInFilter from utilities.filters import NullableCharFieldFilter, NumericInFilter
from virtualization.models import Cluster from virtualization.models import Cluster
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@ -21,11 +21,11 @@ from .models import (
class RegionFilter(django_filters.FilterSet): class RegionFilter(django_filters.FilterSet):
parent_id = NullableModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
label='Parent region (ID)', label='Parent region (ID)',
) )
parent = NullableModelMultipleChoiceFilter( parent = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Parent region (slug)', label='Parent region (slug)',
@ -42,20 +42,20 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
region_id = NullableModelMultipleChoiceFilter( region_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
label='Region (ID)', label='Region (ID)',
) )
region = NullableModelMultipleChoiceFilter( region = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = NullableModelMultipleChoiceFilter( tenant = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
@ -126,31 +126,31 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
group_id = NullableModelMultipleChoiceFilter( group_id = django_filters.ModelMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
label='Group (ID)', label='Group (ID)',
) )
group = NullableModelMultipleChoiceFilter( group = django_filters.ModelMultipleChoiceFilter(
name='group', name='group',
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Group', label='Group',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = NullableModelMultipleChoiceFilter( tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
) )
role_id = NullableModelMultipleChoiceFilter( role_id = django_filters.ModelMultipleChoiceFilter(
queryset=RackRole.objects.all(), queryset=RackRole.objects.all(),
label='Role (ID)', label='Role (ID)',
) )
role = NullableModelMultipleChoiceFilter( role = django_filters.ModelMultipleChoiceFilter(
name='role', name='role',
queryset=RackRole.objects.all(), queryset=RackRole.objects.all(),
to_field_name='slug', to_field_name='slug',
@ -193,12 +193,12 @@ class RackReservationFilter(django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
group_id = NullableModelMultipleChoiceFilter( group_id = django_filters.ModelMultipleChoiceFilter(
name='rack__group', name='rack__group',
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
label='Group (ID)', label='Group (ID)',
) )
group = NullableModelMultipleChoiceFilter( group = django_filters.ModelMultipleChoiceFilter(
name='rack__group', name='rack__group',
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
@ -368,21 +368,21 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Role (slug)', label='Role (slug)',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = NullableModelMultipleChoiceFilter( tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
) )
platform_id = NullableModelMultipleChoiceFilter( platform_id = django_filters.ModelMultipleChoiceFilter(
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
label='Platform (ID)', label='Platform (ID)',
) )
platform = NullableModelMultipleChoiceFilter( platform = django_filters.ModelMultipleChoiceFilter(
name='platform', name='platform',
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
to_field_name='slug', to_field_name='slug',
@ -405,12 +405,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
label='Rack group (ID)', label='Rack group (ID)',
) )
rack_id = NullableModelMultipleChoiceFilter( rack_id = django_filters.ModelMultipleChoiceFilter(
name='rack', name='rack',
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
label='Rack (ID)', label='Rack (ID)',
) )
cluster_id = NullableModelMultipleChoiceFilter( cluster_id = django_filters.ModelMultipleChoiceFilter(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
label='VM cluster (ID)', label='VM cluster (ID)',
) )
@ -595,7 +595,7 @@ class DeviceBayFilter(DeviceComponentFilterSet):
class InventoryItemFilter(DeviceComponentFilterSet): class InventoryItemFilter(DeviceComponentFilterSet):
parent_id = NullableModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItem.objects.all(), queryset=InventoryItem.objects.all(),
label='Parent inventory item (ID)', label='Parent inventory item (ID)',
) )

View File

@ -4,6 +4,7 @@ from mptt.forms import TreeNodeChoiceField
import re import re
from django import forms from django import forms
from django.contrib.auth.models import User
from django.contrib.postgres.forms.array import SimpleArrayField from django.contrib.postgres.forms.array import SimpleArrayField
from django.db.models import Count, Q from django.db.models import Count, Q
@ -376,10 +377,11 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
class RackReservationForm(BootstrapMixin, forms.ModelForm): class RackReservationForm(BootstrapMixin, forms.ModelForm):
units = SimpleArrayField(forms.IntegerField(), widget=ArrayFieldSelectMultiple(attrs={'size': 10})) units = SimpleArrayField(forms.IntegerField(), widget=ArrayFieldSelectMultiple(attrs={'size': 10}))
user = forms.ModelChoiceField(queryset=User.objects.order_by('username'))
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = ['units', 'description'] fields = ['units', 'user', 'description']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -411,6 +413,15 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form):
) )
class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=RackReservation.objects.all(), widget=forms.MultipleHiddenInput)
user = forms.ModelChoiceField(queryset=User.objects.order_by('username'), required=False)
description = forms.CharField(max_length=100, required=False)
class Meta:
nullable_fields = []
# #
# Manufacturers # Manufacturers
# #
@ -953,6 +964,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
cluster = forms.ModelChoiceField( cluster = forms.ModelChoiceField(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
to_field_name='name', to_field_name='name',
required=False,
help_text='Virtualization cluster', help_text='Virtualization cluster',
error_messages={ error_messages={
'invalid_choice': 'Invalid cluster name.', 'invalid_choice': 'Invalid cluster name.',

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-10-31 17:32
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0048_rack_serial'),
]
operations = [
migrations.AlterField(
model_name='rackreservation',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -256,8 +256,8 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
def clean(self): def clean(self):
# Validate that Rack is tall enough to house the installed Devices
if self.pk: if self.pk:
# Validate that Rack is tall enough to house the installed Devices
top_device = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('-position').first() top_device = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('-position').first()
if top_device: if top_device:
min_height = top_device.position + top_device.device_type.u_height - 1 min_height = top_device.position + top_device.device_type.u_height - 1
@ -267,6 +267,12 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
min_height min_height
) )
}) })
# Validate that Rack was assigned a group of its same site, if applicable
if self.group:
if self.group.site != self.site:
raise ValidationError({
'group': "Rack group must be from the same site, {}.".format(self.site)
})
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -290,6 +296,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
self.tenant.name if self.tenant else None, self.tenant.name if self.tenant else None,
self.role.name if self.role else None, self.role.name if self.role else None,
self.get_type_display() if self.type else None, self.get_type_display() if self.type else None,
self.serial,
self.width, self.width,
self.u_height, self.u_height,
self.desc_units, self.desc_units,
@ -411,7 +418,7 @@ class RackReservation(models.Model):
rack = models.ForeignKey('Rack', related_name='reservations', on_delete=models.CASCADE) rack = models.ForeignKey('Rack', related_name='reservations', on_delete=models.CASCADE)
units = ArrayField(models.PositiveSmallIntegerField()) units = ArrayField(models.PositiveSmallIntegerField())
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, editable=False, on_delete=models.PROTECT) user = models.ForeignKey(User, on_delete=models.PROTECT)
description = models.CharField(max_length=100) description = models.CharField(max_length=100)
class Meta: class Meta:

View File

@ -362,6 +362,7 @@ class DeviceRoleTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name') name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices') device_count = tables.Column(verbose_name='Devices')
vm_count = tables.Column(verbose_name='VMs')
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Label') color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Label')
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
@ -369,7 +370,7 @@ class DeviceRoleTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceRole model = DeviceRole
fields = ('pk', 'name', 'device_count', 'color', 'vm_role', 'slug', 'actions') fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'slug', 'actions')
# #

View File

@ -9,14 +9,29 @@ class RackTestCase(TestCase):
def setUp(self): def setUp(self):
self.site = Site.objects.create( self.site1 = Site.objects.create(
name='TestSite1', name='TestSite1',
slug='my-test-site' slug='test-site-1'
)
self.site2 = Site.objects.create(
name='TestSite2',
slug='test-site-2'
)
self.group1 = RackGroup.objects.create(
name='TestGroup1',
slug='test-group-1',
site=self.site1
)
self.group2 = RackGroup.objects.create(
name='TestGroup2',
slug='test-group-2',
site=self.site2
) )
self.rack = Rack.objects.create( self.rack = Rack.objects.create(
name='TestRack1', name='TestRack1',
facility_id='A101', facility_id='A101',
site=self.site, site=self.site1,
group=self.group1,
u_height=42 u_height=42
) )
self.manufacturer = Manufacturer.objects.create( self.manufacturer = Manufacturer.objects.create(
@ -57,13 +72,51 @@ class RackTestCase(TestCase):
} }
def test_rack_device_outside_height(self):
rack1 = Rack(
name='TestRack2',
facility_id='A102',
site=self.site1,
u_height=42
)
rack1.save()
device1 = Device(
name='TestSwitch1',
device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'),
device_role=DeviceRole.objects.get(slug='switch'),
site=self.site1,
rack=rack1,
position=43,
face=RACK_FACE_FRONT,
)
device1.save()
with self.assertRaises(ValidationError):
rack1.clean()
def test_rack_group_site(self):
rack_invalid_group = Rack(
name='TestRack2',
facility_id='A102',
site=self.site1,
u_height=42,
group=self.group2
)
rack_invalid_group.save()
with self.assertRaises(ValidationError):
rack_invalid_group.clean()
def test_mount_single_device(self): def test_mount_single_device(self):
device1 = Device( device1 = Device(
name='TestSwitch1', name='TestSwitch1',
device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'), device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'),
device_role=DeviceRole.objects.get(slug='switch'), device_role=DeviceRole.objects.get(slug='switch'),
site=self.site, site=self.site1,
rack=self.rack, rack=self.rack,
position=10, position=10,
face=RACK_FACE_REAR, face=RACK_FACE_REAR,
@ -92,7 +145,7 @@ class RackTestCase(TestCase):
name='TestPDU', name='TestPDU',
device_role=self.role.get('PDU'), device_role=self.role.get('PDU'),
device_type=self.device_type.get('cc5000'), device_type=self.device_type.get('cc5000'),
site=self.site, site=self.site1,
rack=self.rack, rack=self.rack,
position=None, position=None,
face=None, face=None,

View File

@ -45,6 +45,7 @@ urlpatterns = [
# Rack reservations # Rack reservations
url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'), url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'),
url(r'^rack-reservations/edit/$', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'), url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'), url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'), url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),

View File

@ -426,6 +426,16 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
return obj.rack.get_absolute_url() return obj.rack.get_absolute_url()
class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rackreservation'
cls = RackReservation
queryset = RackReservation.objects.select_related('rack', 'user')
filter = filters.RackReservationFilter
table = tables.RackReservationTable
form = forms.RackReservationBulkEditForm
default_return_url = 'dcim:rackreservation_list'
class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackreservation' permission_required = 'dcim.delete_rackreservation'
cls = RackReservation cls = RackReservation
@ -517,12 +527,12 @@ class DeviceTypeView(View):
show_header=False show_header=False
) )
if request.user.has_perm('dcim.change_devicetype'): if request.user.has_perm('dcim.change_devicetype'):
consoleport_table.base_columns['pk'].visible = True consoleport_table.columns.show('pk')
consoleserverport_table.base_columns['pk'].visible = True consoleserverport_table.columns.show('pk')
powerport_table.base_columns['pk'].visible = True powerport_table.columns.show('pk')
poweroutlet_table.base_columns['pk'].visible = True poweroutlet_table.columns.show('pk')
interface_table.base_columns['pk'].visible = True interface_table.columns.show('pk')
devicebay_table.base_columns['pk'].visible = True devicebay_table.columns.show('pk')
return render(request, 'dcim/devicetype.html', { return render(request, 'dcim/devicetype.html', {
'devicetype': devicetype, 'devicetype': devicetype,
@ -700,7 +710,10 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class DeviceRoleListView(ObjectListView): class DeviceRoleListView(ObjectListView):
queryset = DeviceRole.objects.annotate(device_count=Count('devices')) queryset = DeviceRole.objects.annotate(
device_count=Count('devices', distinct=True),
vm_count=Count('virtual_machines', distinct=True)
)
table = tables.DeviceRoleTable table = tables.DeviceRoleTable
template_name = 'dcim/devicerole_list.html' template_name = 'dcim/devicerole_list.html'

View File

@ -19,17 +19,28 @@ class CustomFieldFilter(django_filters.Filter):
super(CustomFieldFilter, self).__init__(*args, **kwargs) super(CustomFieldFilter, self).__init__(*args, **kwargs)
def filter(self, queryset, value): def filter(self, queryset, value):
# Skip filter on empty value # Skip filter on empty value
if not value.strip(): if not value.strip():
return queryset return queryset
# Treat 0 as None for Select fields
# Selection fields get special treatment (values must be integers)
if self.cf_type == CF_TYPE_SELECT:
try: try:
if self.cf_type == CF_TYPE_SELECT and int(value) == 0: # Treat 0 as None
if int(value) == 0:
return queryset.exclude( return queryset.exclude(
custom_field_values__field__name=self.name, custom_field_values__field__name=self.name,
) )
# Match on exact CustomFieldChoice PK
else:
return queryset.filter(
custom_field_values__field__name=self.name,
custom_field_values__serialized_value=value,
)
except ValueError: except ValueError:
pass return queryset.none()
return queryset.filter( return queryset.filter(
custom_field_values__field__name=self.name, custom_field_values__field__name=self.name,
custom_field_values__serialized_value__icontains=value, custom_field_values__serialized_value__icontains=value,

View File

@ -177,3 +177,12 @@ class Report(object):
result = ReportResult(report=self.full_name, failed=self.failed, data=self._results) result = ReportResult(report=self.full_name, failed=self.failed, data=self._results)
result.save() result.save()
self.result = result self.result = result
# Perform any post-run tasks
self.post_run()
def post_run(self):
"""
Extend this method to include any tasks which should execute after the report has been run.
"""
pass

View File

@ -9,7 +9,7 @@ from django.db.models import Q
from dcim.models import Site, Device, Interface from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter from utilities.filters import NumericInFilter
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from .models import ( from .models import (
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
@ -23,11 +23,11 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = NullableModelMultipleChoiceFilter( tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
@ -110,37 +110,37 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='filter_mask_length', method='filter_mask_length',
label='Mask length', label='Mask length',
) )
vrf_id = NullableModelMultipleChoiceFilter( vrf_id = django_filters.ModelMultipleChoiceFilter(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
label='VRF', label='VRF',
) )
vrf = NullableModelMultipleChoiceFilter( vrf = django_filters.ModelMultipleChoiceFilter(
name='vrf', name='vrf',
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
to_field_name='rd', to_field_name='rd',
label='VRF (RD)', label='VRF (RD)',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = NullableModelMultipleChoiceFilter( tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
) )
site_id = NullableModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
site = NullableModelMultipleChoiceFilter( site = django_filters.ModelMultipleChoiceFilter(
name='site', name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
vlan_id = NullableModelMultipleChoiceFilter( vlan_id = django_filters.ModelMultipleChoiceFilter(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
label='VLAN (ID)', label='VLAN (ID)',
) )
@ -148,11 +148,11 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
name='vlan__vid', name='vlan__vid',
label='VLAN number (1-4095)', label='VLAN number (1-4095)',
) )
role_id = NullableModelMultipleChoiceFilter( role_id = django_filters.ModelMultipleChoiceFilter(
queryset=Role.objects.all(), queryset=Role.objects.all(),
label='Role (ID)', label='Role (ID)',
) )
role = NullableModelMultipleChoiceFilter( role = django_filters.ModelMultipleChoiceFilter(
name='role', name='role',
queryset=Role.objects.all(), queryset=Role.objects.all(),
to_field_name='slug', to_field_name='slug',
@ -207,21 +207,21 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='filter_mask_length', method='filter_mask_length',
label='Mask length', label='Mask length',
) )
vrf_id = NullableModelMultipleChoiceFilter( vrf_id = django_filters.ModelMultipleChoiceFilter(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
label='VRF', label='VRF',
) )
vrf = NullableModelMultipleChoiceFilter( vrf = django_filters.ModelMultipleChoiceFilter(
name='vrf', name='vrf',
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
to_field_name='rd', to_field_name='rd',
label='VRF (RD)', label='VRF (RD)',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = NullableModelMultipleChoiceFilter( tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
@ -267,12 +267,10 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
return queryset return queryset
qs_filter = Q(description__icontains=value) qs_filter = (
try: Q(description__icontains=value) |
ipaddress = str(IPNetwork(value.strip())) Q(address__istartswith=value)
qs_filter |= Q(address__net_host=ipaddress) )
except (AddrFormatError, ValueError):
pass
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
def search_by_parent(self, queryset, name, value): def search_by_parent(self, queryset, name, value):
@ -292,11 +290,11 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
class VLANGroupFilter(django_filters.FilterSet): class VLANGroupFilter(django_filters.FilterSet):
site_id = NullableModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
site = NullableModelMultipleChoiceFilter( site = django_filters.ModelMultipleChoiceFilter(
name='site', name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
@ -314,41 +312,41 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
site_id = NullableModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
site = NullableModelMultipleChoiceFilter( site = django_filters.ModelMultipleChoiceFilter(
name='site', name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
group_id = NullableModelMultipleChoiceFilter( group_id = django_filters.ModelMultipleChoiceFilter(
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
label='Group (ID)', label='Group (ID)',
) )
group = NullableModelMultipleChoiceFilter( group = django_filters.ModelMultipleChoiceFilter(
name='group', name='group',
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Group', label='Group',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = NullableModelMultipleChoiceFilter( tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
) )
role_id = NullableModelMultipleChoiceFilter( role_id = django_filters.ModelMultipleChoiceFilter(
queryset=Role.objects.all(), queryset=Role.objects.all(),
label='Role (ID)', label='Role (ID)',
) )
role = NullableModelMultipleChoiceFilter( role = django_filters.ModelMultipleChoiceFilter(
name='role', name='role',
queryset=Role.objects.all(), queryset=Role.objects.all(),
to_field_name='slug', to_field_name='slug',

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db.models import Lookup, Transform, IntegerField from django.db.models import Lookup, Transform, IntegerField
from django.db.models.lookups import BuiltinLookup from django.db.models import lookups
class NetFieldDecoratorMixin(object): class NetFieldDecoratorMixin(object):
@ -13,27 +13,27 @@ class NetFieldDecoratorMixin(object):
return lhs_string, lhs_params return lhs_string, lhs_params
class EndsWith(NetFieldDecoratorMixin, BuiltinLookup): class EndsWith(NetFieldDecoratorMixin, lookups.EndsWith):
lookup_name = 'endswith' lookup_name = 'endswith'
class IEndsWith(NetFieldDecoratorMixin, BuiltinLookup): class IEndsWith(NetFieldDecoratorMixin, lookups.IEndsWith):
lookup_name = 'iendswith' lookup_name = 'iendswith'
class StartsWith(NetFieldDecoratorMixin, BuiltinLookup): class StartsWith(NetFieldDecoratorMixin, lookups.StartsWith):
lookup_name = 'startswith' lookup_name = 'startswith'
class IStartsWith(NetFieldDecoratorMixin, BuiltinLookup): class IStartsWith(NetFieldDecoratorMixin, lookups.IStartsWith):
lookup_name = 'istartswith' lookup_name = 'istartswith'
class Regex(NetFieldDecoratorMixin, BuiltinLookup): class Regex(NetFieldDecoratorMixin, lookups.Regex):
lookup_name = 'regex' lookup_name = 'regex'
class IRegex(NetFieldDecoratorMixin, BuiltinLookup): class IRegex(NetFieldDecoratorMixin, lookups.IRegex):
lookup_name = 'iregex' lookup_name = 'iregex'

View File

@ -325,7 +325,7 @@ class AggregateView(View):
prefix_table = tables.PrefixDetailTable(child_prefixes) prefix_table = tables.PrefixDetailTable(child_prefixes)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
prefix_table.base_columns['pk'].visible = True prefix_table.columns.show('pk')
paginate = { paginate = {
'klass': EnhancedPaginator, 'klass': EnhancedPaginator,
@ -495,7 +495,7 @@ class PrefixView(View):
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes) child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
child_prefix_table = tables.PrefixDetailTable(child_prefixes) child_prefix_table = tables.PrefixDetailTable(child_prefixes)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
child_prefix_table.base_columns['pk'].visible = True child_prefix_table.columns.show('pk')
paginate = { paginate = {
'klass': EnhancedPaginator, 'klass': EnhancedPaginator,
@ -538,7 +538,7 @@ class PrefixIPAddressesView(View):
ip_table = tables.IPAddressTable(ipaddresses) ip_table = tables.IPAddressTable(ipaddresses)
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'): if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
ip_table.base_columns['pk'].visible = True ip_table.columns.show('pk')
paginate = { paginate = {
'klass': EnhancedPaginator, 'klass': EnhancedPaginator,

View File

@ -13,7 +13,7 @@ except ImportError:
) )
VERSION = '2.2.2' VERSION = '2.2.3'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -206,6 +206,10 @@ LOGIN_URL = '/{}login/'.format(BASE_PATH)
# Secrets # Secrets
SECRETS_MIN_PUBKEY_SIZE = 2048 SECRETS_MIN_PUBKEY_SIZE = 2048
# Django filters
FILTERS_NULL_CHOICE_LABEL = 'None'
FILTERS_NULL_CHOICE_VALUE = '0' # Must be a string
# Django REST framework (API) # Django REST framework (API)
REST_FRAMEWORK_VERSION = VERSION[0:3] # Use major.minor as API version REST_FRAMEWORK_VERSION = VERSION[0:3] # Use major.minor as API version
REST_FRAMEWORK = { REST_FRAMEWORK = {

View File

@ -106,9 +106,14 @@ label {
label.required { label.required {
font-weight: bold; font-weight: bold;
} }
input[name="pk"] {
margin-top: 0;
}
/* Tables */ /* Tables */
th.pk, td.pk { .table > tbody > tr > th.pk, .table > tbody > tr > td.pk {
padding-bottom: 6px;
padding-top: 10px;
width: 30px; width: 30px;
} }
tfoot td { tfoot td {

View File

@ -54,15 +54,27 @@ $(document).ready(function() {
$.each(json['get_lldp_neighbors'], function(iface, neighbors) { $.each(json['get_lldp_neighbors'], function(iface, neighbors) {
var neighbor = neighbors[0]; var neighbor = neighbors[0];
var row = $('#' + iface.split(".")[0].replace(/(\/)/g, "\\$1")); var row = $('#' + iface.split(".")[0].replace(/(\/)/g, "\\$1"));
// Glean configured hostnames/interfaces from the DOM
var configured_device = row.children('td.configured_device').attr('data'); var configured_device = row.children('td.configured_device').attr('data');
var configured_interface = row.children('td.configured_interface').attr('data'); var configured_interface = row.children('td.configured_interface').attr('data');
if (configured_interface) {
// Match long-form IOS names against short ones (e.g. Gi0/1 == GigabitEthernet0/1).
configured_interface = configured_interface.replace(/^([A-Z][a-z])[^0-9]*([0-9\/]+)$/, "$1$2");
}
// Clean up hostnames/interfaces learned via LLDP
var lldp_device = neighbor['hostname'].split(".")[0]; // Strip off any trailing domain name
var lldp_interface = neighbor['port'].split(".")[0]; // Strip off any trailing subinterface ID
// Add LLDP neighbors to table // Add LLDP neighbors to table
row.children('td.device').html(neighbor['hostname']); row.children('td.device').html(lldp_device);
row.children('td.interface').html(neighbor['port']); row.children('td.interface').html(lldp_interface);
// Apply colors to rows // Apply colors to rows
if (!configured_device && neighbor['hostname']) { if (!configured_device && lldp_device) {
row.addClass('info'); row.addClass('info');
} else if (configured_device == neighbor['hostname'] && configured_interface == neighbor['port'].split(".")[0]) { } else if (configured_device == lldp_device && configured_interface == lldp_interface) {
row.addClass('success'); row.addClass('success');
} else { } else {
row.addClass('danger'); row.addClass('danger');

View File

@ -120,7 +120,7 @@
</td> </td>
<td> <td>
<strong>Network Device</strong><br /> <strong>Network Device</strong><br />
<small class="text-muted">This device {% if devicetype.is_network_device %}has{% else %}does not have{% endif %} non-management network interfaces</small> <small class="text-muted">This device {% if devicetype.is_network_device %}has{% else %}does not have{% endif %} network interfaces</small>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -5,7 +5,7 @@
</td> </td>
{% endif %} {% endif %}
<td> <td>
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp.name }} <i class="fa fa-fw fa-keyboard-o"></i> {{ csp }}
</td> </td>
<td></td> <td></td>
{% if csp.connected_console %} {% if csp.connected_console %}

View File

@ -5,7 +5,7 @@
<h1>{% block title %}Rack Reservations{% endblock %}</h1> <h1>{% block title %}Rack Reservations{% endblock %}</h1>
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackreservation_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rackreservation_bulk_edit' bulk_delete_url='dcim:rackreservation_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}

View File

@ -5,7 +5,7 @@ import django_filters
from django.db.models import Q from django.db.models import Q
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter from utilities.filters import NumericInFilter
from .models import Tenant, TenantGroup from .models import Tenant, TenantGroup
@ -22,11 +22,11 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
group_id = NullableModelMultipleChoiceFilter( group_id = django_filters.ModelMultipleChoiceFilter(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
label='Group (ID)', label='Group (ID)',
) )
group = NullableModelMultipleChoiceFilter( group = django_filters.ModelMultipleChoiceFilter(
name='group', name='group',
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
to_field_name='slug', to_field_name='slug',

View File

@ -4,7 +4,6 @@ import django_filters
import itertools import itertools
from django import forms from django import forms
from django.db.models import Q
from django.utils.encoding import force_text from django.utils.encoding import force_text
@ -66,51 +65,3 @@ class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField):
stripped_value = value stripped_value = value
super(NullableModelMultipleChoiceField, self).clean(stripped_value) super(NullableModelMultipleChoiceField, self).clean(stripped_value)
return value return value
class NullableModelMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
"""
This class extends ModelMultipleChoiceFilter to accept an additional value which implies "is null". The default
queryset filter argument is:
.filter(fieldname=value)
When filtering by the value representing "is null" ('0' by default) the argument is modified to:
.filter(fieldname__isnull=True)
"""
field_class = NullableModelMultipleChoiceField
def __init__(self, *args, **kwargs):
self.null_value = kwargs.get('null_value', 0)
super(NullableModelMultipleChoiceFilter, self).__init__(*args, **kwargs)
def filter(self, qs, value):
value = value or () # Make sure we have an iterable
if self.is_noop(qs, value):
return qs
# Even though not a noop, no point filtering if empty
if not value:
return qs
q = Q()
for v in set(value):
# Filtering by "is null"
if v == force_text(self.null_value):
arg = {'{}__isnull'.format(self.name): True}
# Filtering by a related field (e.g. slug)
elif self.field.to_field_name is not None:
arg = {'{}__{}'.format(self.name, self.field.to_field_name): v}
# Filtering by primary key (default)
else:
arg = {self.name: v}
if self.conjoined:
qs = self.get_method(qs)(**arg)
else:
q |= Q(**arg)
if self.distinct:
return self.get_method(qs)(q).distinct()
return self.get_method(qs)(q)

View File

@ -9,7 +9,7 @@ from django.db.models import Q
from dcim.models import DeviceRole, Interface, Platform, Site from dcim.models import DeviceRole, Interface, Platform, Site
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter from utilities.filters import NumericInFilter
from .constants import STATUS_CHOICES from .constants import STATUS_CHOICES
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@ -20,11 +20,11 @@ class ClusterFilter(CustomFieldFilterSet):
method='search', method='search',
label='Search', label='Search',
) )
group_id = NullableModelMultipleChoiceFilter( group_id = django_filters.ModelMultipleChoiceFilter(
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
label='Parent group (ID)', label='Parent group (ID)',
) )
group = NullableModelMultipleChoiceFilter( group = django_filters.ModelMultipleChoiceFilter(
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Parent group (slug)', label='Parent group (slug)',
@ -72,12 +72,12 @@ class VirtualMachineFilter(CustomFieldFilterSet):
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=STATUS_CHOICES choices=STATUS_CHOICES
) )
cluster_group_id = NullableModelMultipleChoiceFilter( cluster_group_id = django_filters.ModelMultipleChoiceFilter(
name='cluster__group', name='cluster__group',
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
label='Cluster group (ID)', label='Cluster group (ID)',
) )
cluster_group = NullableModelMultipleChoiceFilter( cluster_group = django_filters.ModelMultipleChoiceFilter(
name='cluster__group', name='cluster__group',
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
@ -87,29 +87,29 @@ class VirtualMachineFilter(CustomFieldFilterSet):
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
label='Cluster (ID)', label='Cluster (ID)',
) )
role_id = NullableModelMultipleChoiceFilter( role_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
label='Role (ID)', label='Role (ID)',
) )
role = NullableModelMultipleChoiceFilter( role = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Role (slug)', label='Role (slug)',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = NullableModelMultipleChoiceFilter( tenant = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
) )
platform_id = NullableModelMultipleChoiceFilter( platform_id = django_filters.ModelMultipleChoiceFilter(
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
label='Platform (ID)', label='Platform (ID)',
) )
platform = NullableModelMultipleChoiceFilter( platform = django_filters.ModelMultipleChoiceFilter(
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Platform (slug)', label='Platform (slug)',

View File

@ -0,0 +1,91 @@
[
{
"model": "virtualization.clustertype",
"pk": 1,
"fields": {
"name": "Public Cloud",
"slug": "public-cloud"
}
},
{
"model": "virtualization.clustertype",
"pk": 2,
"fields": {
"name": "vSphere",
"slug": "vsphere"
}
},
{
"model": "virtualization.clustertype",
"pk": 3,
"fields": {
"name": "Hyper-V",
"slug": "hyper-v"
}
},
{
"model": "virtualization.clustertype",
"pk": 4,
"fields": {
"name": "libvirt",
"slug": "libvirt"
}
},
{
"model": "virtualization.clustertype",
"pk": 5,
"fields": {
"name": "LXD",
"slug": "lxd"
}
},
{
"model": "virtualization.clustertype",
"pk": 6,
"fields": {
"name": "Docker",
"slug": "docker"
}
},
{
"model": "virtualization.clustergroup",
"pk": 1,
"fields": {
"name": "VM Host",
"slug": "vm-host"
}
},
{
"model": "virtualization.cluster",
"pk": 1,
"fields": {
"name": "Digital Ocean",
"type": 1,
"group": 1,
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z"
}
},
{
"model": "virtualization.cluster",
"pk": 2,
"fields": {
"name": "Amazon EC2",
"type": 1,
"group": 1,
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z"
}
},
{
"model": "virtualization.cluster",
"pk": 3,
"fields": {
"name": "Microsoft Azure",
"type": 1,
"group": 1,
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z"
}
}
]

View File

@ -314,6 +314,7 @@ class VirtualMachineBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'virtualization.delete_virtualmachine' permission_required = 'virtualization.delete_virtualmachine'
cls = VirtualMachine cls = VirtualMachine
queryset = VirtualMachine.objects.select_related('cluster', 'tenant') queryset = VirtualMachine.objects.select_related('cluster', 'tenant')
filter = filters.VirtualMachineFilter
table = tables.VirtualMachineTable table = tables.VirtualMachineTable
default_return_url = 'virtualization:virtualmachine_list' default_return_url = 'virtualization:virtualmachine_list'

View File

@ -1,7 +1,7 @@
Django>=1.11,<2.0 Django>=1.11,<2.0
django-cors-headers>=2.1 django-cors-headers>=2.1
django-debug-toolbar>=1.8 django-debug-toolbar>=1.8
django-filter>=1.0.4 django-filter>=1.1.0
django-mptt==0.8.7 django-mptt==0.8.7
django-rest-swagger>=2.1.0 django-rest-swagger>=2.1.0
django-tables2>=1.10.0 django-tables2>=1.10.0