Merge branch 'develop' into develop-2.9

This commit is contained in:
Jeremy Stretch
2020-06-15 16:04:32 -04:00
27 changed files with 384 additions and 157 deletions

View File

@@ -502,13 +502,13 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
return Response(serializer.data)
class FrontPortViewSet(ModelViewSet):
class FrontPortViewSet(CableTraceMixin, ModelViewSet):
queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')
serializer_class = serializers.FrontPortSerializer
filterset_class = filters.FrontPortFilterSet
class RearPortViewSet(ModelViewSet):
class RearPortViewSet(CableTraceMixin, ModelViewSet):
queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags')
serializer_class = serializers.RearPortSerializer
filterset_class = filters.RearPortFilterSet

View File

@@ -21,11 +21,11 @@ from ipam.models import IPAddress, VLAN
from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant, TenantGroup
from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
BOOLEAN_WITH_BLANK_CHOICES, BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField,
CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
form_from_model, JSONField, LabeledComponentForm, SelectWithPK, SmallTextarea, SlugField, StaticSelect2,
StaticSelect2Multiple, TagFilterField,
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField, CSVModelForm,
DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField,
LabeledComponentForm, NumericArrayField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2,
StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
)
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
from .choices import *
@@ -363,7 +363,12 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
class RackGroupForm(BootstrapMixin, forms.ModelForm):
site = DynamicModelChoiceField(
queryset=Site.objects.all()
queryset=Site.objects.all(),
widget=APISelect(
filter_for={
'parent': 'site_id',
}
)
)
parent = DynamicModelChoiceField(
queryset=RackGroup.objects.all(),
@@ -729,21 +734,32 @@ class RackElevationFilterForm(RackFilterForm):
#
class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
rack = forms.ModelChoiceField(
queryset=Rack.objects.all(),
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False,
widget=forms.HiddenInput()
)
# TODO: Change this to an API-backed form field. We can't do this currently because we want to retain
# the multi-line <select> widget for easy selection of multiple rack units.
units = SimpleArrayField(
base_field=forms.IntegerField(),
widget=ArrayFieldSelectMultiple(
attrs={
'size': 10,
widget=APISelect(
filter_for={
'rack_group': 'site_id',
'rack': 'site_id',
}
)
)
rack_group = DynamicModelChoiceField(
queryset=RackGroup.objects.all(),
required=False,
widget=APISelect(
filter_for={
'rack': 'group_id'
}
)
)
rack = DynamicModelChoiceField(
queryset=Rack.objects.all()
)
units = NumericArrayField(
base_field=forms.IntegerField(),
help_text="Comma-separated list of numeric unit IDs. A range may be specified using a hyphen."
)
user = forms.ModelChoiceField(
queryset=User.objects.order_by(
'username'
@@ -760,23 +776,6 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
'rack', 'units', 'user', 'tenant_group', 'tenant', 'description', 'tags',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Populate rack unit choices
if hasattr(self.instance, 'rack'):
self.fields['units'].widget.choices = self._get_unit_choices()
def _get_unit_choices(self):
rack = self.instance.rack
reserved_units = []
for resv in rack.reservations.exclude(pk=self.instance.pk):
for u in resv.units:
reserved_units.append(u)
unit_choices = [(u, {'label': str(u), 'disabled': u in reserved_units}) for u in rack.units]
return unit_choices
class RackReservationCSVForm(CSVModelForm):
site = CSVModelChoiceField(
@@ -1220,11 +1219,21 @@ class PowerOutletTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
queryset=PowerOutletTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
)
device_type = forms.ModelChoiceField(
queryset=DeviceType.objects.all(),
required=False,
disabled=True,
widget=forms.HiddenInput()
)
type = forms.ChoiceField(
choices=add_blank_choice(PowerOutletTypeChoices),
required=False,
widget=StaticSelect2()
)
power_port = forms.ModelChoiceField(
queryset=PowerPortTemplate.objects.all(),
required=False
)
feed_leg = forms.ChoiceField(
choices=add_blank_choice(PowerOutletFeedLegChoices),
required=False,
@@ -1232,7 +1241,18 @@ class PowerOutletTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
)
class Meta:
nullable_fields = ('type', 'feed_leg')
nullable_fields = ('type', 'power_port', 'feed_leg')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit power_port queryset to PowerPortTemplates which belong to the parent DeviceType
if 'device_type' in self.initial:
device_type = DeviceType.objects.filter(pk=self.initial['device_type']).first()
self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(device_type=device_type)
else:
self.fields['power_port'].choices = ()
self.fields['power_port'].widget.attrs['disabled'] = True
class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):

View File

@@ -2,7 +2,7 @@ import django_tables2 as tables
from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT
from utilities.tables import BaseTable, BooleanColumn, ColorColumn, TagColumn, ToggleColumn
from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ColoredLabelColumn, TagColumn, ToggleColumn
from .models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
@@ -72,15 +72,6 @@ RACKROLE_ACTIONS = """
{% endif %}
"""
RACK_ROLE = """
{% if record.role %}
{% load helpers %}
<label class="label" style="color: {{ record.role.color|fgcolor }}; background-color: #{{ record.role.color }}">{{ value }}</label>
{% else %}
&mdash;
{% endif %}
"""
RACK_DEVICE_COUNT = """
<a href="{% url 'dcim:device_list' %}?rack_id={{ record.pk }}">{{ value }}</a>
"""
@@ -137,11 +128,6 @@ PLATFORM_ACTIONS = """
{% endif %}
"""
DEVICE_ROLE = """
{% load helpers %}
<label class="label" style="color: {{ record.device_role.color|fgcolor }}; background-color: #{{ record.device_role.color }}">{{ value }}</label>
"""
STATUS_LABEL = """
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
"""
@@ -325,9 +311,7 @@ class RackTable(BaseTable):
status = tables.TemplateColumn(
template_code=STATUS_LABEL
)
role = tables.TemplateColumn(
template_code=RACK_ROLE
)
role = ColoredLabelColumn()
u_height = tables.TemplateColumn(
template_code="{{ record.u_height }}U",
verbose_name='Height'
@@ -810,8 +794,7 @@ class DeviceTable(BaseTable):
viewname='dcim:rack',
args=[Accessor('rack.pk')]
)
device_role = tables.TemplateColumn(
template_code=DEVICE_ROLE,
device_role = ColoredLabelColumn(
verbose_name='Role'
)
device_type = tables.LinkColumn(

View File

@@ -8,9 +8,9 @@ from dcim.choices import *
from dcim.constants import *
from dcim.models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, Interface, InterfaceTemplate, Manufacturer,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerFeed, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, PowerPanel,
Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
)
from ipam.models import VLAN
from extras.models import Graph
@@ -566,6 +566,111 @@ class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
]
class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
model = FrontPortTemplate
brief_fields = ['id', 'name', 'url']
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
rear_port_templates = (
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 3', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 4', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 5', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 6', type=PortTypeChoices.TYPE_8P8C),
)
RearPortTemplate.objects.bulk_create(rear_port_templates)
front_port_templates = (
FrontPortTemplate(
device_type=devicetype,
name='Front Port Template 1',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_port_templates[0]
),
FrontPortTemplate(
device_type=devicetype,
name='Front Port Template 2',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_port_templates[1]
),
FrontPortTemplate(
device_type=devicetype,
name='Front Port Template 3',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_port_templates[2]
),
)
FrontPortTemplate.objects.bulk_create(front_port_templates)
cls.create_data = [
{
'device_type': devicetype.pk,
'name': 'Front Port Template 4',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_port_templates[3].pk,
'position': 1,
},
{
'device_type': devicetype.pk,
'name': 'Front Port Template 5',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_port_templates[4].pk,
'position': 1,
},
{
'device_type': devicetype.pk,
'name': 'Front Port Template 6',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_port_templates[5].pk,
'position': 1,
},
]
class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
model = RearPortTemplate
brief_fields = ['id', 'name', 'url']
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
rear_port_templates = (
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 3', type=PortTypeChoices.TYPE_8P8C),
)
RearPortTemplate.objects.bulk_create(rear_port_templates)
cls.create_data = [
{
'device_type': devicetype.pk,
'name': 'Rear Port Template 4',
'type': PortTypeChoices.TYPE_8P8C,
},
{
'device_type': devicetype.pk,
'name': 'Rear Port Template 5',
'type': PortTypeChoices.TYPE_8P8C,
},
{
'device_type': devicetype.pk,
'name': 'Rear Port Template 6',
'type': PortTypeChoices.TYPE_8P8C,
},
]
class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
model = DeviceBayTemplate
brief_fields = ['id', 'name', 'url']
@@ -1146,6 +1251,35 @@ class InterfaceTest(APIViewTestCases.APIViewTestCase):
self.assertEqual(len(response.data), 3)
self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?interface=Interface 1&foo=1')
def test_trace_interface(self):
"""
Test tracing an Interface cable.
"""
interface_a = Interface.objects.first()
peer_device = Device.objects.create(
site=Site.objects.first(),
device_type=DeviceType.objects.first(),
device_role=DeviceRole.objects.first(),
name='Peer Device'
)
interface_b = Interface.objects.create(
device=peer_device,
name='Interface X'
)
cable = Cable(termination_a=interface_a, termination_b=interface_b, label='Cable 1')
cable.save()
self.add_permissions('dcim.view_interface')
url = reverse('dcim-api:interface-trace', kwargs={'pk': interface_a.pk})
response = self.client.get(url, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
segment1 = response.data[0]
self.assertEqual(segment1[0]['name'], interface_a.name)
self.assertEqual(segment1[1]['label'], cable.label)
self.assertEqual(segment1[2]['name'], interface_b.name)
class FrontPortTest(APIViewTestCases.APIViewTestCase):
model = FrontPort
@@ -1200,6 +1334,35 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
},
]
def test_trace_frontport(self):
"""
Test tracing a FrontPort cable.
"""
frontport = FrontPort.objects.first()
peer_device = Device.objects.create(
site=Site.objects.first(),
device_type=DeviceType.objects.first(),
device_role=DeviceRole.objects.first(),
name='Peer Device'
)
interface = Interface.objects.create(
device=peer_device,
name='Interface X'
)
cable = Cable(termination_a=frontport, termination_b=interface, label='Cable 1')
cable.save()
self.add_permissions('dcim.view_frontport')
url = reverse('dcim-api:frontport-trace', kwargs={'pk': frontport.pk})
response = self.client.get(url, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
segment1 = response.data[0]
self.assertEqual(segment1[0]['name'], frontport.name)
self.assertEqual(segment1[1]['label'], cable.label)
self.assertEqual(segment1[2]['name'], interface.name)
class RearPortTest(APIViewTestCases.APIViewTestCase):
model = RearPort
@@ -1238,6 +1401,35 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
},
]
def test_trace_rearport(self):
"""
Test tracing a RearPort cable.
"""
rearport = RearPort.objects.first()
peer_device = Device.objects.create(
site=Site.objects.first(),
device_type=DeviceType.objects.first(),
device_role=DeviceRole.objects.first(),
name='Peer Device'
)
interface = Interface.objects.create(
device=peer_device,
name='Interface X'
)
cable = Cable(termination_a=rearport, termination_b=interface, label='Cable 1')
cable.save()
self.add_permissions('dcim.view_rearport')
url = reverse('dcim-api:rearport-trace', kwargs={'pk': rearport.pk})
response = self.client.get(url, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
segment1 = response.data[0]
self.assertEqual(segment1[0]['name'], rearport.name)
self.assertEqual(segment1[1]['label'], cable.label)
self.assertEqual(segment1[2]['name'], interface.name)
class DeviceBayTest(APIViewTestCases.APIViewTestCase):
model = DeviceBay

View File

@@ -198,7 +198,7 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
cls.form_data = {
'rack': rack.pk,
'units': [10, 11, 12],
'units': "10,11,12",
'user': user3.pk,
'tenant': None,
'description': 'Rack reservation',