mirror of
https://github.com/netbox-community/netbox.git
synced 2025-09-09 07:30:39 -06:00
Add new VLANDeviceMapping model and appropriate views, filters, forms, api and graphql objects.
This commit is contained in:
parent
21ca8d7d6d
commit
a3543e4c7b
@ -21,6 +21,7 @@ __all__ = [
|
||||
'NestedServiceTemplateSerializer',
|
||||
'NestedVLANGroupSerializer',
|
||||
'NestedVLANSerializer',
|
||||
'NestedVLANDeviceMappingSerializer',
|
||||
'NestedVRFSerializer',
|
||||
]
|
||||
|
||||
@ -159,6 +160,14 @@ class NestedVLANSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display', 'vid', 'name']
|
||||
|
||||
|
||||
class NestedVLANDeviceMappingSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlandevicemapping-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.VLANDeviceMapping
|
||||
fields = ['id', 'url', 'display', ]
|
||||
|
||||
|
||||
#
|
||||
# Prefixes
|
||||
#
|
||||
|
@ -257,6 +257,20 @@ class VLANSerializer(NetBoxModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class VLANDeviceMappingSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlandevicemapping-detail')
|
||||
device = NestedDeviceSerializer(required=True)
|
||||
vlan = NestedVLANSerializer(required=True)
|
||||
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = VLANDeviceMapping
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'vlan', 'l2vpn_termination', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class AvailableVLANSerializer(serializers.Serializer):
|
||||
"""
|
||||
Representation of a VLAN which does not exist in the database.
|
||||
|
@ -21,6 +21,7 @@ router.register('fhrp-groups', views.FHRPGroupViewSet)
|
||||
router.register('fhrp-group-assignments', views.FHRPGroupAssignmentViewSet)
|
||||
router.register('vlan-groups', views.VLANGroupViewSet)
|
||||
router.register('vlans', views.VLANViewSet)
|
||||
router.register('vlandevicemappings', views.VLANDeviceMappingViewSet)
|
||||
router.register('service-templates', views.ServiceTemplateViewSet)
|
||||
router.register('services', views.ServiceViewSet)
|
||||
|
||||
|
@ -163,6 +163,14 @@ class VLANViewSet(NetBoxModelViewSet):
|
||||
filterset_class = filtersets.VLANFilterSet
|
||||
|
||||
|
||||
class VLANDeviceMappingViewSet(NetBoxModelViewSet):
|
||||
queryset = VLANDeviceMapping.objects.prefetch_related(
|
||||
'device', 'vlan', 'tags'
|
||||
)
|
||||
serializer_class = serializers.VLANDeviceMappingSerializer
|
||||
filterset_class = filtersets.VLANDeviceMappingFilterSet
|
||||
|
||||
|
||||
class ServiceTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = ServiceTemplate.objects.prefetch_related('tags')
|
||||
serializer_class = serializers.ServiceTemplateSerializer
|
||||
|
@ -15,7 +15,7 @@ from utilities.filters import (
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from vpn.models import L2VPN
|
||||
from vpn.models import L2VPN, L2VPNTermination
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@ -35,6 +35,7 @@ __all__ = (
|
||||
'ServiceFilterSet',
|
||||
'ServiceTemplateFilterSet',
|
||||
'VLANFilterSet',
|
||||
'VLANDeviceMappingFilterSet',
|
||||
'VLANGroupFilterSet',
|
||||
'VRFFilterSet',
|
||||
)
|
||||
@ -992,6 +993,71 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
return queryset.get_for_virtualmachine(value)
|
||||
|
||||
|
||||
class VLANDeviceMappingFilterSet(NetBoxModelFilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
label=_('Device (ID)'),
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__name',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label=_('Device (name)'),
|
||||
)
|
||||
vlan_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VLAN.objects.all(),
|
||||
label=_('VLAN (ID)'),
|
||||
)
|
||||
vlan_vid = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vlan__vid',
|
||||
queryset=VLAN.objects.all(),
|
||||
to_field_name='vid',
|
||||
label=_('VLAN (vid)'),
|
||||
)
|
||||
vlan = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vlan__name',
|
||||
queryset=VLAN.objects.all(),
|
||||
to_field_name='name',
|
||||
label=_('VLAN (name)'),
|
||||
)
|
||||
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='l2vpn_terminations__l2vpn',
|
||||
queryset=L2VPN.objects.all(),
|
||||
label=_('L2VPN (ID)'),
|
||||
)
|
||||
l2vpn_identifier = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='l2vpn_terminations__l2vpn__identifier',
|
||||
queryset=L2VPN.objects.all(),
|
||||
to_field_name='identifier',
|
||||
label=_('L2VPN'),
|
||||
)
|
||||
l2vpn = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='l2vpn_terminations__l2vpn__name',
|
||||
queryset=L2VPN.objects.all(),
|
||||
to_field_name='name',
|
||||
label=_('L2VPN'),
|
||||
)
|
||||
l2vpn_termination_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='l2vpn_terminations',
|
||||
queryset=L2VPNTermination.objects.all(),
|
||||
label=_('L2VPN Termination (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLANDeviceMapping
|
||||
fields = ['id', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(device__name__icontains=value) | Q(vlan__name__icontains=value)
|
||||
try:
|
||||
qs_filter |= Q(vlan__vid=int(value.strip()))
|
||||
except ValueError:
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class ServiceTemplateFilterSet(NetBoxModelFilterSet):
|
||||
port = NumericArrayFilter(
|
||||
field_name='ports',
|
||||
|
@ -27,6 +27,7 @@ __all__ = (
|
||||
'ServiceImportForm',
|
||||
'ServiceTemplateImportForm',
|
||||
'VLANImportForm',
|
||||
'VLANDeviceMappingImportForm',
|
||||
'VLANGroupImportForm',
|
||||
'VRFImportForm',
|
||||
)
|
||||
@ -472,6 +473,25 @@ class VLANImportForm(NetBoxModelImportForm):
|
||||
fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'comments', 'tags')
|
||||
|
||||
|
||||
class VLANDeviceMappingImportForm(NetBoxModelImportForm):
|
||||
device = CSVModelChoiceField(
|
||||
label=_('Device'),
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned device')
|
||||
)
|
||||
vlan = CSVModelChoiceField(
|
||||
label=_('VLAN'),
|
||||
queryset=VLANGroup.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned VLAN')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLANDeviceMapping
|
||||
fields = ('device', 'vlan', 'description', 'comments', 'tags')
|
||||
|
||||
|
||||
class ServiceTemplateImportForm(NetBoxModelImportForm):
|
||||
protocol = CSVChoiceField(
|
||||
label=_('Protocol'),
|
||||
|
@ -26,6 +26,7 @@ __all__ = (
|
||||
'ServiceFilterForm',
|
||||
'ServiceTemplateFilterForm',
|
||||
'VLANFilterForm',
|
||||
'VLANDeviceMappingFilterForm',
|
||||
'VLANGroupFilterForm',
|
||||
'VRFFilterForm',
|
||||
)
|
||||
@ -499,6 +500,32 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VLANDeviceMappingFilterForm(NetBoxModelFilterSetForm):
|
||||
model = VLANDeviceMapping
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
(_('Device'), ('device_id', )),
|
||||
(_('VLAN'), ('vlan_id', )),
|
||||
(_('L2VPN'), ('l2vpn_id', )),
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
label=_('Device')
|
||||
)
|
||||
vlan_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('VLAN')
|
||||
)
|
||||
l2vpn_id = DynamicModelMultipleChoiceField(
|
||||
queryset=L2VPN.objects.all(),
|
||||
required=False,
|
||||
label=_('L2VPN')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
|
||||
model = ServiceTemplate
|
||||
fieldsets = (
|
||||
|
@ -37,6 +37,7 @@ __all__ = (
|
||||
'ServiceCreateForm',
|
||||
'ServiceTemplateForm',
|
||||
'VLANForm',
|
||||
'VLANDeviceMappingForm',
|
||||
'VLANGroupForm',
|
||||
'VRFForm',
|
||||
)
|
||||
@ -658,6 +659,24 @@ class VLANForm(TenancyForm, NetBoxModelForm):
|
||||
]
|
||||
|
||||
|
||||
class VLANDeviceMappingForm(NetBoxModelForm):
|
||||
vlan = DynamicModelChoiceField(
|
||||
label=_('VLAN'),
|
||||
queryset=VLAN.objects.all()
|
||||
)
|
||||
device = DynamicModelChoiceField(
|
||||
label=_('Device'),
|
||||
queryset=Device.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = VLANDeviceMapping
|
||||
fields = [
|
||||
'device', 'vlan', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class ServiceTemplateForm(NetBoxModelForm):
|
||||
ports = NumericArrayField(
|
||||
label=_('Ports'),
|
||||
|
@ -97,6 +97,12 @@ class IPAMQuery(graphene.ObjectType):
|
||||
def resolve_vlan_group_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.VLANGroup.objects.all(), info)
|
||||
|
||||
vlan_device_mapping = ObjectField(VLANDeviceMappingType)
|
||||
vlan_device_mapping_list = ObjectListField(VLANDeviceMappingType)
|
||||
|
||||
def resolve_vlan_device_mapping_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.VLANDeviceMapping.objects.all(), info)
|
||||
|
||||
vrf = ObjectField(VRFType)
|
||||
vrf_list = ObjectListField(VRFType)
|
||||
|
||||
|
@ -20,6 +20,7 @@ __all__ = (
|
||||
'ServiceTemplateType',
|
||||
'VLANType',
|
||||
'VLANGroupType',
|
||||
'VLANDeviceMappingType',
|
||||
'VRFType',
|
||||
)
|
||||
|
||||
@ -179,6 +180,14 @@ class VLANGroupType(OrganizationalObjectType):
|
||||
filterset_class = filtersets.VLANGroupFilterSet
|
||||
|
||||
|
||||
class VLANDeviceMappingType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.VLANDeviceMapping
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.VLANDeviceMappingFilterSet
|
||||
|
||||
|
||||
class VRFType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
|
@ -15,6 +15,7 @@ from virtualization.models import VMInterface
|
||||
__all__ = (
|
||||
'VLAN',
|
||||
'VLANGroup',
|
||||
'VLANDeviceMapping',
|
||||
)
|
||||
|
||||
|
||||
@ -256,3 +257,59 @@ class VLAN(PrimaryModel):
|
||||
@property
|
||||
def l2vpn_termination(self):
|
||||
return self.l2vpn_terminations.first()
|
||||
|
||||
|
||||
class VLANDeviceMapping(PrimaryModel):
|
||||
"""
|
||||
A VLAN to device termination is a unique VLAN to device mapping. Mainly used as an additional termination point for
|
||||
the L2VPN model.
|
||||
|
||||
Each termination must have both a unique VLAN and device
|
||||
"""
|
||||
device = models.ForeignKey(
|
||||
to='dcim.Device',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='devices',
|
||||
help_text=_("Device")
|
||||
)
|
||||
vlan = models.ForeignKey(
|
||||
to='ipam.VLAN',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='vlans',
|
||||
help_text=_("VLAN")
|
||||
)
|
||||
|
||||
l2vpn_terminations = GenericRelation(
|
||||
to='vpn.L2VPNTermination',
|
||||
content_type_field='assigned_object_type',
|
||||
object_id_field='assigned_object_id',
|
||||
related_query_name='vlandevicemapping'
|
||||
)
|
||||
|
||||
clone_fields = [
|
||||
'device', 'vlan',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', 'vlan', 'pk')
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('device', 'vlan'),
|
||||
name='%(app_label)s_%(class)s_unique_device_vlan'
|
||||
),
|
||||
)
|
||||
verbose_name = _('VLAN Device Mapping')
|
||||
verbose_name_plural = _('VLAN Device Mapping')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.device} ({self.vlan.name if self.vlan.name else self.vlan.vid})'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:vlandevicemapping', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@property
|
||||
def l2vpn_termination(self):
|
||||
return self.l2vpn_terminations.first()
|
||||
|
@ -15,6 +15,7 @@ __all__ = (
|
||||
'VLANGroupTable',
|
||||
'VLANMembersTable',
|
||||
'VLANTable',
|
||||
'VLANDeviceMappingTable',
|
||||
'VLANVirtualMachinesTable',
|
||||
)
|
||||
|
||||
@ -156,6 +157,30 @@ class VLANTable(TenancyColumnsMixin, NetBoxTable):
|
||||
}
|
||||
|
||||
|
||||
class VLANDeviceMappingTable(NetBoxTable):
|
||||
device = tables.Column(
|
||||
verbose_name=_('Device'),
|
||||
linkify=True
|
||||
)
|
||||
vlan = tables.Column(
|
||||
verbose_name=_('VLAN'),
|
||||
linkify=True
|
||||
)
|
||||
|
||||
l2vpn_termination = tables.Column(
|
||||
verbose_name=_('L2VPN'),
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = VLANDeviceMapping
|
||||
fields = (
|
||||
'pk', 'id', 'device', 'vlan', 'l2vpn_termination', 'description', 'comments', 'tags', 'created',
|
||||
'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'id', 'device', 'vlan', 'description')
|
||||
|
||||
|
||||
class VLANMembersTable(NetBoxTable):
|
||||
"""
|
||||
Base table for Interface and VMInterface assignments
|
||||
|
@ -1018,6 +1018,57 @@ class VLANTest(APIViewTestCases.APIViewTestCase):
|
||||
self.assertTrue(content['detail'].startswith('Unable to delete object.'))
|
||||
|
||||
|
||||
class VLANDeviceMappingTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VLANDeviceMapping
|
||||
brief_fields = ['display', 'id', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
|
||||
role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', site=site, device_type=devicetype, role=role),
|
||||
Device(name='Device 2', site=site, device_type=devicetype, role=role),
|
||||
Device(name='Device 3', site=site, device_type=devicetype, role=role),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
vlans = (
|
||||
VLAN(name='VLAN 1', vid=1),
|
||||
VLAN(name='VLAN 2', vid=2),
|
||||
VLAN(name='VLAN 3', vid=2),
|
||||
)
|
||||
VLAN.objects.bulk_create(vlans)
|
||||
|
||||
vlandevicemappings = (
|
||||
VLANDeviceMapping(device=devices[0], vlan=vlans[0]),
|
||||
VLANDeviceMapping(device=devices[1], vlan=vlans[0]),
|
||||
VLANDeviceMapping(device=devices[2], vlan=vlans[0]),
|
||||
)
|
||||
VLANDeviceMapping.objects.bulk_create(vlandevicemappings)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'device': devices[0].pk,
|
||||
'vlan': vlans[2].pk,
|
||||
},
|
||||
{
|
||||
'device': devices[1].pk,
|
||||
'vlan': vlans[2].pk,
|
||||
},
|
||||
{
|
||||
'device': devices[2].pk,
|
||||
'vlan': vlans[2].pk,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class ServiceTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ServiceTemplate
|
||||
brief_fields = ['display', 'id', 'name', 'ports', 'protocol', 'url']
|
||||
|
@ -10,6 +10,7 @@ from ipam.models import *
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||
from vpn.models import L2VPN, L2VPNTermination
|
||||
|
||||
|
||||
class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
@ -1496,6 +1497,96 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) # 5 scoped + 1 global
|
||||
|
||||
|
||||
class VLANDeviceMappingTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VLANDeviceMapping.objects.all()
|
||||
filterset = VLANDeviceMappingFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
|
||||
role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', site=site, device_type=devicetype, role=role),
|
||||
Device(name='Device 2', site=site, device_type=devicetype, role=role),
|
||||
Device(name='Device 3', site=site, device_type=devicetype, role=role),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
vlans = (
|
||||
VLAN(name='VLAN 1', vid=1),
|
||||
VLAN(name='VLAN 2', vid=2),
|
||||
VLAN(name='VLAN 3', vid=2),
|
||||
)
|
||||
VLAN.objects.bulk_create(vlans)
|
||||
|
||||
vlandevicemappings = (
|
||||
VLANDeviceMapping(device=devices[0], vlan=vlans[0]),
|
||||
VLANDeviceMapping(device=devices[1], vlan=vlans[0]),
|
||||
VLANDeviceMapping(device=devices[2], vlan=vlans[0]),
|
||||
VLANDeviceMapping(device=devices[0], vlan=vlans[1]),
|
||||
VLANDeviceMapping(device=devices[1], vlan=vlans[1]),
|
||||
VLANDeviceMapping(device=devices[2], vlan=vlans[1]),
|
||||
VLANDeviceMapping(device=devices[0], vlan=vlans[2]),
|
||||
VLANDeviceMapping(device=devices[1], vlan=vlans[2]),
|
||||
VLANDeviceMapping(device=devices[2], vlan=vlans[2]),
|
||||
)
|
||||
VLANDeviceMapping.objects.bulk_create(vlandevicemappings)
|
||||
|
||||
l2vpns = (
|
||||
L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan'),
|
||||
L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vxlan'),
|
||||
)
|
||||
L2VPN.objects.bulk_create(l2vpns)
|
||||
|
||||
L2VPNTermination.objects.bulk_create((
|
||||
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlandevicemappings[0]),
|
||||
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlandevicemappings[1]),
|
||||
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlandevicemappings[2]),
|
||||
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlandevicemappings[3]),
|
||||
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlandevicemappings[4]),
|
||||
L2VPNTermination(l2vpn=l2vpns[1], assigned_object=vlandevicemappings[5]),
|
||||
L2VPNTermination(l2vpn=l2vpns[1], assigned_object=vlandevicemappings[6]),
|
||||
L2VPNTermination(l2vpn=l2vpns[1], assigned_object=vlandevicemappings[7]),
|
||||
L2VPNTermination(l2vpn=l2vpns[1], assigned_object=vlandevicemappings[8]),)
|
||||
)
|
||||
|
||||
def test_device(self):
|
||||
devices = Device.objects.all()
|
||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'device': [devices[0].name, devices[1].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_vlan(self):
|
||||
vlans = VLAN.objects.all()
|
||||
params = {'vlan_id': [vlans[0].pk, vlans[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'vlan_vid': [vlans[0].vid, vlans[1].vid]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'vlan': [vlans[0].name, vlans[1].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_l2vpn(self):
|
||||
l2vpns = L2VPN.objects.all()
|
||||
params = {'l2vpn_id': [l2vpns[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'l2vpn': [l2vpns[1].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_device_vlan(self):
|
||||
vlan = VLAN.objects.first()
|
||||
devices = Device.objects.all()
|
||||
params = {'vlan_id': [vlan.pk], 'device_id': [devices[1].pk, devices[2].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'vlan_vid': [vlan.vid], 'device_id': [devices[1].pk, devices[2].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'vlan': [vlan.name], 'device': [devices[1].name, devices[2].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ServiceTemplate.objects.all()
|
||||
filterset = ServiceTemplateFilterSet
|
||||
|
@ -858,6 +858,75 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class VLANDeviceMappingTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VLANDeviceMapping
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
|
||||
role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', site=site, device_type=devicetype, role=role),
|
||||
Device(name='Device 2', site=site, device_type=devicetype, role=role),
|
||||
Device(name='Device 3', site=site, device_type=devicetype, role=role),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
vlans = (
|
||||
VLAN(name='VLAN 1', vid=1),
|
||||
VLAN(name='VLAN 2', vid=2),
|
||||
VLAN(name='VLAN 3', vid=2),
|
||||
)
|
||||
VLAN.objects.bulk_create(vlans)
|
||||
|
||||
vlandevicemappings = (
|
||||
VLANDeviceMapping(device=devices[0], vlan=vlans[0]),
|
||||
VLANDeviceMapping(device=devices[1], vlan=vlans[0]),
|
||||
VLANDeviceMapping(device=devices[2], vlan=vlans[0]),
|
||||
VLANDeviceMapping(device=devices[0], vlan=vlans[1]),
|
||||
VLANDeviceMapping(device=devices[1], vlan=vlans[1]),
|
||||
VLANDeviceMapping(device=devices[2], vlan=vlans[1]),
|
||||
VLANDeviceMapping(device=devices[0], vlan=vlans[2]),
|
||||
VLANDeviceMapping(device=devices[1], vlan=vlans[2]),
|
||||
VLANDeviceMapping(device=devices[2], vlan=vlans[2]),
|
||||
)
|
||||
VLANDeviceMapping.objects.bulk_create(vlandevicemappings)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': devices[0].pk,
|
||||
'vlan': vlans[2].pk,
|
||||
'description': 'A new VLAN',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"device,vlan",
|
||||
f"{devices[1].name},{vlans[2].name}",
|
||||
f"{devices[2].name},{vlans[2].name}",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,name,description",
|
||||
f"{vlans[0].pk},VLAN107,New description 7",
|
||||
f"{vlans[1].pk},VLAN108,New description 8",
|
||||
f"{vlans[2].pk},VLAN109,New description 9",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'site': sites[1].pk,
|
||||
'group': vlangroups[1].pk,
|
||||
'tenant': None,
|
||||
'status': VLANStatusChoices.STATUS_RESERVED,
|
||||
'role': roles[1].pk,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class ServiceTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = ServiceTemplate
|
||||
|
||||
|
@ -116,6 +116,13 @@ urlpatterns = [
|
||||
path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
||||
path('vlans/<int:pk>/', include(get_model_urls('ipam', 'vlan'))),
|
||||
|
||||
# VLANs
|
||||
path('vlans-device-mappings/', views.VLANDeviceMappingListView.as_view(), name='vlandevicemapping_list'),
|
||||
path('vlans-device-mappings/add/', views.VLANDeviceMappingEditView.as_view(), name='vlandevicemapping_add'),
|
||||
path('vlans-device-mappings/import/', views.VLANDeviceMappingBulkImportView.as_view(), name='vlandevicemapping_import'),
|
||||
path('vlans-device-mappings/delete/', views.VLANDeviceMappingBulkDeleteView.as_view(), name='vlandevicemapping_bulk_delete'),
|
||||
path('vlans-device-mappings/<int:pk>/', include(get_model_urls('ipam', 'vlandevicemapping'))),
|
||||
|
||||
# Service templates
|
||||
path('service-templates/', views.ServiceTemplateListView.as_view(), name='servicetemplate_list'),
|
||||
path('service-templates/add/', views.ServiceTemplateEditView.as_view(), name='servicetemplate_add'),
|
||||
|
@ -1146,6 +1146,44 @@ class VLANBulkDeleteView(generic.BulkDeleteView):
|
||||
table = tables.VLANTable
|
||||
|
||||
|
||||
#
|
||||
# VLAN Device Terminations
|
||||
#
|
||||
|
||||
class VLANDeviceMappingListView(generic.ObjectListView):
|
||||
queryset = VLANDeviceMapping.objects.all()
|
||||
filterset = filtersets.VLANDeviceMappingFilterSet
|
||||
filterset_form = forms.VLANDeviceMappingFilterForm
|
||||
table = tables.VLANDeviceMappingTable
|
||||
|
||||
|
||||
@register_model_view(VLANDeviceMapping)
|
||||
class VLANDeviceMappingView(generic.ObjectView):
|
||||
queryset = VLANDeviceMapping.objects.all()
|
||||
|
||||
|
||||
@register_model_view(VLANDeviceMapping, 'edit')
|
||||
class VLANDeviceMappingEditView(generic.ObjectEditView):
|
||||
queryset = VLANDeviceMapping.objects.all()
|
||||
form = forms.VLANDeviceMappingForm
|
||||
|
||||
|
||||
@register_model_view(VLANDeviceMapping, 'delete')
|
||||
class VLANDeviceMappingDeleteView(generic.ObjectDeleteView):
|
||||
queryset = VLANDeviceMapping.objects.all()
|
||||
|
||||
|
||||
class VLANDeviceMappingBulkImportView(generic.BulkImportView):
|
||||
queryset = VLANDeviceMapping.objects.all()
|
||||
model_form = forms.VLANDeviceMappingImportForm
|
||||
|
||||
|
||||
class VLANDeviceMappingBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VLANDeviceMapping.objects.all()
|
||||
filterset = filtersets.VLANDeviceMappingFilterSet
|
||||
table = tables.VLANDeviceMappingTable
|
||||
|
||||
|
||||
#
|
||||
# Service templates
|
||||
#
|
||||
|
@ -182,6 +182,7 @@ IPAM_MENU = Menu(
|
||||
items=(
|
||||
get_model_item('ipam', 'vlan', _('VLANs')),
|
||||
get_model_item('ipam', 'vlangroup', _('VLAN Groups')),
|
||||
get_model_item('ipam', 'vlandevicemapping', _('VLAN Device Mappings')),
|
||||
),
|
||||
),
|
||||
MenuGroup(
|
||||
|
38
netbox/templates/ipam/vlandevicemapping.html
Normal file
38
netbox/templates/ipam/vlandevicemapping.html
Normal file
@ -0,0 +1,38 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "VLAN Mapping" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Device" %}</th>
|
||||
<td>{{ object.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VLAN" %}</th>
|
||||
<td>{{ object.vlan|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "L2VPN" %}</th>
|
||||
<td>{{ object.l2vpn_terminationn|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -15,10 +15,6 @@
|
||||
<th scope="row">{% trans "L2VPN" %}</th>
|
||||
<td>{{ object.l2vpn|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Device" %}</th>
|
||||
<td>{{ object.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Assigned Object" %}</th>
|
||||
<td>{{ object.assigned_object|linkify }}</td>
|
||||
|
@ -17,6 +17,11 @@
|
||||
{% trans "VLAN" %}
|
||||
</button>
|
||||
</li>
|
||||
<li role="presentation" class="nav-item">
|
||||
<button role="tab" type="button" id="vlandevicemapping_tab" data-bs-toggle="tab" aria-controls="vlandevicemapping" data-bs-target="#vlandevicemapping" class="nav-link {% if form.initial.vlandevicemapping %}active{% endif %}">
|
||||
{% trans "VLAN Device Mapping" %}
|
||||
</button>
|
||||
</li>
|
||||
<li role="presentation" class="nav-item">
|
||||
<button role="tab" type="button" id="interface_tab" data-bs-toggle="tab" aria-controls="interface" data-bs-target="#interface" class="nav-link {% if form.initial.interface %}active{% endif %}">
|
||||
{% trans "Device" %}
|
||||
@ -33,9 +38,11 @@
|
||||
<div class="row mb-3">
|
||||
<div class="tab-content p-0 border-0">
|
||||
<div class="tab-pane {% if not form.initial.interface or form.initial.vminterface %}active{% endif %}" id="vlan" role="tabpanel" aria-labeled-by="vlan_tab">
|
||||
{% render_field form.device %}
|
||||
{% render_field form.vlan %}
|
||||
</div>
|
||||
<div class="tab-pane {% if form.initial.vlandevicemapping %}active{% endif %}" id="vlandevicemapping" role="tabpanel" aria-labeled-by="vlandevicemapping_tab">
|
||||
{% render_field form.vlandevicemapping %}
|
||||
</div>
|
||||
<div class="tab-pane {% if form.initial.interface %}active{% endif %}" id="interface" role="tabpanel" aria-labeled-by="interface_tab">
|
||||
{% render_field form.interface %}
|
||||
</div>
|
||||
|
@ -3,5 +3,6 @@ from django.db.models import Q
|
||||
L2VPN_ASSIGNMENT_MODELS = Q(
|
||||
Q(app_label='dcim', model='interface') |
|
||||
Q(app_label='ipam', model='vlan') |
|
||||
Q(app_label='virtualization', model='vminterface')
|
||||
Q(app_label='virtualization', model='vminterface') |
|
||||
Q(app_label='ipam', model='vlandevicemapping')
|
||||
)
|
||||
|
@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.models import Device, Interface
|
||||
from ipam.models import IPAddress, VLAN
|
||||
from ipam.models import IPAddress, VLAN, VLANDeviceMapping
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField
|
||||
@ -264,7 +264,7 @@ class L2VPNTerminationImportForm(NetBoxModelImportForm):
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Parent device (for interface)')
|
||||
help_text=_('Parent device (for interface or Device-VLAN)')
|
||||
)
|
||||
virtual_machine = CSVModelChoiceField(
|
||||
label=_('Virtual machine'),
|
||||
@ -310,13 +310,24 @@ class L2VPNTerminationImportForm(NetBoxModelImportForm):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.cleaned_data.get('device') and self.cleaned_data.get('virtual_machine'):
|
||||
device = self.cleaned_data.get('device')
|
||||
vm = self.cleaned_data.get('virtual_machine')
|
||||
interface = self.cleaned_data.get('interface')
|
||||
vlan = self.cleaned_data.get('vlan')
|
||||
|
||||
if device and vm:
|
||||
raise ValidationError(_('Cannot import device and VM interface terminations simultaneously.'))
|
||||
if not self.instance and not (self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')):
|
||||
if not self.instance and not (interface or vlan):
|
||||
raise ValidationError(_('Each termination must specify either an interface or a VLAN.'))
|
||||
if self.cleaned_data.get('interface') and self.cleaned_data.get('vlan'):
|
||||
if interface and vlan:
|
||||
raise ValidationError(_('Cannot assign both an interface and a VLAN.'))
|
||||
|
||||
# if this is an update we might not have interface or vlan in the form data
|
||||
if self.cleaned_data.get('interface') or self.cleaned_data.get('vlan'):
|
||||
if vlan and device:
|
||||
try:
|
||||
vlandevicemapping = VLANDeviceMapping.objects.get(device=device, vlan=vlan)
|
||||
except VLANDeviceMapping.DoesNotExist:
|
||||
raise ValidationError(_('Cannot find Device VLAN Mapping'))
|
||||
self.instance.assigned_object = vlandevicemapping
|
||||
elif interface or vlan:
|
||||
self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')
|
||||
|
@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.models import Device, Interface
|
||||
from ipam.models import IPAddress, RouteTarget, VLAN
|
||||
from ipam.models import IPAddress, RouteTarget, VLAN, VLANDeviceMapping
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
||||
@ -406,6 +406,12 @@ class L2VPNTerminationForm(NetBoxModelForm):
|
||||
selector=True,
|
||||
label=_('VLAN')
|
||||
)
|
||||
vlandevicemapping = DynamicModelChoiceField(
|
||||
queryset=VLANDeviceMapping.objects.all(),
|
||||
required=False,
|
||||
selector=True,
|
||||
label=_('VLAN Device Mapping')
|
||||
)
|
||||
interface = DynamicModelChoiceField(
|
||||
label=_('Interface'),
|
||||
queryset=Interface.objects.all(),
|
||||
@ -432,6 +438,8 @@ class L2VPNTerminationForm(NetBoxModelForm):
|
||||
initial['interface'] = instance.assigned_object
|
||||
elif type(instance.assigned_object) is VLAN:
|
||||
initial['vlan'] = instance.assigned_object
|
||||
elif type(instance.assigned_object) is VLANDeviceMapping:
|
||||
initial['vlandevicemapping'] = instance.assigned_object
|
||||
elif type(instance.assigned_object) is VMInterface:
|
||||
initial['vminterface'] = instance.assigned_object
|
||||
kwargs['initial'] = initial
|
||||
@ -444,10 +452,11 @@ class L2VPNTerminationForm(NetBoxModelForm):
|
||||
interface = self.cleaned_data.get('interface')
|
||||
vminterface = self.cleaned_data.get('vminterface')
|
||||
vlan = self.cleaned_data.get('vlan')
|
||||
vlandevicemapping = self.cleaned_data.get('vlandevicemapping')
|
||||
|
||||
if not (interface or vminterface or vlan):
|
||||
if not (interface or vminterface or vlan or vlandevicemapping):
|
||||
raise ValidationError(_('A termination must specify an interface or VLAN.'))
|
||||
if len([x for x in (interface, vminterface, vlan) if x]) > 1:
|
||||
if len([x for x in (interface, vminterface, vlan, vlandevicemapping) if x]) > 1:
|
||||
raise ValidationError(_('A termination can only have one terminating object (an interface or VLAN).'))
|
||||
|
||||
self.instance.assigned_object = interface or vminterface or vlan
|
||||
self.instance.assigned_object = interface or vminterface or vlan or vlandevicemapping
|
||||
|
@ -152,8 +152,8 @@ class L2VPNTermination(NetBoxModel):
|
||||
return self.assigned_object.virtual_machine
|
||||
elif obj_type.model == 'interface':
|
||||
return self.assigned_object.device
|
||||
elif obj_type.model == 'vminterface':
|
||||
return self.assigned_object.virtual_machine
|
||||
elif obj_type.model == 'vlandevicemapping':
|
||||
return self.assigned_object.vlan
|
||||
return None
|
||||
|
||||
@property
|
||||
|
@ -55,11 +55,6 @@ class L2VPNTerminationTable(NetBoxTable):
|
||||
verbose_name=_('L2VPN'),
|
||||
linkify=True
|
||||
)
|
||||
device = tables.Column(
|
||||
linkify=True,
|
||||
orderable=False,
|
||||
verbose_name=_('Device')
|
||||
)
|
||||
assigned_object_type = columns.ContentTypeColumn(
|
||||
verbose_name=_('Object Type')
|
||||
)
|
||||
@ -77,8 +72,8 @@ class L2VPNTerminationTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = L2VPNTermination
|
||||
fields = (
|
||||
'pk', 'l2vpn', 'device', 'assigned_object_type', 'assigned_object', 'assigned_object_site', 'actions',
|
||||
'pk', 'l2vpn', 'assigned_object_type', 'assigned_object', 'assigned_object_site', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'l2vpn', 'device', 'assigned_object_type', 'assigned_object', 'actions',
|
||||
'pk', 'l2vpn', 'assigned_object_type', 'assigned_object', 'actions',
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user