Closes: #7854 - Add VDC/Instances/etc (#10787)

* Work on #7854

* Move to new URL scheme.

* Fix PEP8 errors

* Fix PEP8 errors

* Add GraphQL and fix primary_ip missing

* Fix PEP8 on GQL Type

* Fix missing NestedSerializer.

* Fix missing NestedSerializer & rename VDC to VDCs

* Fix migration

* Change Validation for identifier

* Fix missing migration

* Rebase to feature

* Post-review changes

* Remove VDC Type
* Remove M2M Enforcement logic

* Interface related changes

* Add filter fields to filterset for Interface filter
* Add form field to filterset form for Interface filter
* Add VDC display to interface detail template

* Remove VirtualDeviceContextTypeChoices

* Accommodate recent changes in feature branch

* Add tests
Add missing search()

* Update tests, and fix model form

* Update test_api

* Update test_api.InterfaceTest create_data

* Fix issue with tests

* Update interface serializer

* Update serializer and tests

* Update status to be required

* Remove error message for constraint

* Remove extraneous import

* Re-ordered devices menu to place VDC below virtual chassis

* Add helptext for `identifier` field

* Fix breadcrumb link

* Remove add interface link

* Add missing tenant and status fields

* Changes to tests as per Jeremy

* Change for #9623

Co-authored-by: Jeremy Stretch <jstretch@ns1.com>

* Update filterset form for status field

* Remove Rename View

* Change tabs to spaces

* Update netbox/dcim/tables/devices.py

Co-authored-by: Jeremy Stretch <jstretch@ns1.com>

* Update netbox/dcim/tables/devices.py

Co-authored-by: Jeremy Stretch <jstretch@ns1.com>

* Fix tenant in bulk_edit

* Apply suggestions from code review

Co-authored-by: Jeremy Stretch <jstretch@ns1.com>

* Add status field to table.

* Re-order table fields.

Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
This commit is contained in:
Daniel Sheppard
2022-11-11 06:55:49 -06:00
committed by GitHub
parent 653acbf62c
commit b374351154
27 changed files with 890 additions and 14 deletions

View File

@@ -1485,6 +1485,12 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
)
Interface.objects.bulk_create(interfaces)
vdcs = (
VirtualDeviceContext(name='VDC 1', identifier=1, device=device),
VirtualDeviceContext(name='VDC 2', identifier=2, device=device)
)
VirtualDeviceContext.objects.bulk_create(vdcs)
vlans = (
VLAN(name='VLAN 1', vid=1),
VLAN(name='VLAN 2', vid=2),
@@ -1533,6 +1539,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
},
{
'device': device.pk,
'vdcs': [vdcs[0].pk],
'name': 'Interface 6',
'type': 'virtual',
'mode': InterfaceModeChoices.MODE_TAGGED,
@@ -1543,6 +1550,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
},
{
'device': device.pk,
'vdcs': [vdcs[1].pk],
'name': 'Interface 7',
'type': InterfaceTypeChoices.TYPE_80211A,
'tx_power': 10,
@@ -1551,6 +1559,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
},
{
'device': device.pk,
'vdcs': [vdcs[1].pk],
'name': 'Interface 8',
'type': InterfaceTypeChoices.TYPE_80211A,
'tx_power': 10,
@@ -2163,3 +2172,57 @@ class PowerFeedTest(APIViewTestCases.APIViewTestCase):
'type': REDUNDANT,
},
]
class VirtualDeviceContextTest(APIViewTestCases.APIViewTestCase):
model = VirtualDeviceContext
brief_fields = ['device', 'display', 'id', 'identifier', 'name', 'url']
bulk_update_data = {
'status': 'planned',
}
@classmethod
def setUpTestData(cls):
site = Site.objects.create(name='Test Site', slug='test-site')
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type', slug='device-type')
devicerole = DeviceRole.objects.create(name='Device Role', slug='device-role', color='ff0000')
devices = (
Device(name='Device 1', device_type=devicetype, device_role=devicerole, site=site),
Device(name='Device 2', device_type=devicetype, device_role=devicerole, site=site),
Device(name='Device 3', device_type=devicetype, device_role=devicerole, site=site),
)
Device.objects.bulk_create(devices)
vdcs = (
VirtualDeviceContext(device=devices[1], name='VDC 1', identifier=1, status='active'),
VirtualDeviceContext(device=devices[1], name='VDC 2', identifier=2, status='active'),
VirtualDeviceContext(device=devices[2], name='VDC 1', identifier=1, status='active'),
VirtualDeviceContext(device=devices[2], name='VDC 2', identifier=2, status='active'),
VirtualDeviceContext(device=devices[2], name='VDC 3', identifier=3, status='active'),
VirtualDeviceContext(device=devices[2], name='VDC 4', identifier=4, status='active'),
VirtualDeviceContext(device=devices[2], name='VDC 5', identifier=5, status='active'),
)
VirtualDeviceContext.objects.bulk_create(vdcs)
cls.create_data = [
{
'device': devices[0].pk,
'status': 'active',
'name': 'VDC 1',
'identifier': 1,
},
{
'device': devices[0].pk,
'status': 'active',
'name': 'VDC 2',
'identifier': 2,
},
{
'device': devices[1].pk,
'status': 'active',
'name': 'VDC 3',
'identifier': 3,
},
]

View File

@@ -2681,6 +2681,13 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
)
VRF.objects.bulk_create(vrfs)
# Virtual Device Context Creation
vdcs = (
VirtualDeviceContext(device=devices[3], name='VDC 1', identifier=1, status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE),
VirtualDeviceContext(device=devices[3], name='VDC 2', identifier=2, status=VirtualDeviceContextStatusChoices.STATUS_PLANNED),
)
VirtualDeviceContext.objects.bulk_create(vdcs)
# VirtualChassis assignment for filtering
virtual_chassis = VirtualChassis.objects.create(master=devices[0])
Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=virtual_chassis, vc_position=1, vc_priority=1)
@@ -2793,6 +2800,12 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
)
Interface.objects.bulk_create(interfaces)
interfaces[3].vdcs.set([vdcs[0], vdcs[1]])
interfaces[4].vdcs.set([vdcs[0], vdcs[1]])
interfaces[5].vdcs.set([vdcs[0]])
interfaces[6].vdcs.set([vdcs[0]])
interfaces[7].vdcs.set([vdcs[1]])
# Cables
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[3]]).save()
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[4]]).save()
@@ -2997,6 +3010,21 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'vrf': [vrfs[0].rd, vrfs[1].rd]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_vdc(self):
params = {'vdc': ['VDC 1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
devices = Device.objects.last()
vdc = VirtualDeviceContext.objects.filter(device=devices, name='VDC 2')
params = {'vdc_id': vdc.values_list('pk', flat=True)}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_vdc_identifier(self):
devices = Device.objects.last()
vdc = VirtualDeviceContext.objects.filter(device=devices, name='VDC 2')
params = {'vdc_identifier': vdc.values_list('identifier', flat=True)}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = FrontPort.objects.all()
@@ -4254,4 +4282,83 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
# TODO: Connection filters
class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VirtualDeviceContext.objects.all()
filterset = VirtualDeviceContextFilterSet
@classmethod
def setUpTestData(cls):
sites = (
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
Site(name='Site 3', slug='site-3'),
)
Site.objects.bulk_create(sites)
tenants = (
Tenant(name='Tenant 1', slug='tenant-1'),
Tenant(name='Tenant 2', slug='tenant-2'),
Tenant(name='Tenant 3', slug='tenant-3'),
)
Tenant.objects.bulk_create(tenants)
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]),
)
Device.objects.bulk_create(devices)
vdcs = (
VirtualDeviceContext(device=devices[0], name='VDC 1', identifier=1, status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE),
VirtualDeviceContext(device=devices[0], name='VDC 2', identifier=2, status=VirtualDeviceContextStatusChoices.STATUS_PLANNED),
VirtualDeviceContext(device=devices[1], name='VDC 1', status=VirtualDeviceContextStatusChoices.STATUS_OFFLINE),
VirtualDeviceContext(device=devices[1], name='VDC 2', status=VirtualDeviceContextStatusChoices.STATUS_PLANNED),
VirtualDeviceContext(device=devices[2], name='VDC 1', status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE),
VirtualDeviceContext(device=devices[2], name='VDC 2', status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE),
)
VirtualDeviceContext.objects.bulk_create(vdcs)
interfaces = (
Interface(device=devices[0], name='Interface 1', type='virtual'),
Interface(device=devices[0], name='Interface 2', type='virtual'),
)
Interface.objects.bulk_create(interfaces)
interfaces[0].vdcs.set([vdcs[0]])
interfaces[1].vdcs.set([vdcs[1]])
addresses = (
IPAddress(assigned_object=interfaces[0], address='10.1.1.1/24'),
IPAddress(assigned_object=interfaces[1], address='10.1.1.2/24'),
)
IPAddress.objects.bulk_create(addresses)
vdcs[0].primary_ip4 = addresses[0]
vdcs[0].save()
vdcs[1].primary_ip4 = addresses[1]
vdcs[1].save()
def test_device(self):
params = {'device': ['Device 1', 'Device 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_status(self):
params = {'status': ['active']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_device_id(self):
devices = Device.objects.filter(name__in=['Device 1', 'Device 2'])
params = {'device_id': [devices[0].pk, devices[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_has_primary_ip(self):
params = {'has_primary_ip': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'has_primary_ip': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)

View File

@@ -588,3 +588,50 @@ class CableTestCase(TestCase):
cable = Cable(a_terminations=[self.interface2], b_terminations=[wireless_interface])
with self.assertRaises(ValidationError):
cable.clean()
class VirtualDeviceContextTestCase(TestCase):
def setUp(self):
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
)
devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
)
self.device = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='TestDevice1', site=site
)
def test_vdc_and_interface_creation(self):
vdc = VirtualDeviceContext(device=self.device, name="VDC 1", identifier=1, status='active')
vdc.full_clean()
vdc.save()
interface = Interface(device=self.device, name='Eth1/1', type='10gbase-t')
interface.full_clean()
interface.save()
interface.vdcs.set([vdc])
def test_vdc_duplicate_name(self):
vdc1 = VirtualDeviceContext(device=self.device, name="VDC 1", identifier=1, status='active')
vdc1.full_clean()
vdc1.save()
vdc2 = VirtualDeviceContext(device=self.device, name="VDC 1", identifier=2, status='active')
with self.assertRaises(ValidationError):
vdc2.full_clean()
def test_vdc_duplicate_identifier(self):
vdc1 = VirtualDeviceContext(device=self.device, name="VDC 1", identifier=1, status='active')
vdc1.full_clean()
vdc1.save()
vdc2 = VirtualDeviceContext(device=self.device, name="VDC 2", identifier=1, status='active')
with self.assertRaises(ValidationError):
vdc2.full_clean()

View File

@@ -3076,3 +3076,48 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
response = self.client.get(reverse('dcim:powerfeed_trace', kwargs={'pk': powerfeed.pk}))
self.assertHttpStatus(response, 200)
class VirtualDeviceContextTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = VirtualDeviceContext
@classmethod
def setUpTestData(cls):
devices = [create_test_device(name='Device 1')]
vdcs = (
VirtualDeviceContext(name='VDC 1', identifier=1, device=devices[0], status='active'),
VirtualDeviceContext(name='VDC 2', identifier=2, device=devices[0], status='active'),
VirtualDeviceContext(name='VDC 3', identifier=3, device=devices[0], status='active'),
)
VirtualDeviceContext.objects.bulk_create(vdcs)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'device': devices[0].pk,
'status': 'active',
'name': 'VDC 4',
'identifier': 4,
'primary_ip4': None,
'primary_ip6': None,
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"device,status,name,identifier",
"Device 1,active,VDC 5,5",
"Device 1,active,VDC 6,6",
"Device 1,active,VDC 7,7",
)
cls.csv_update_data = (
"id,status",
f"{vdcs[0].pk},{VirtualDeviceContextStatusChoices.STATUS_PLANNED}",
f"{vdcs[1].pk},{VirtualDeviceContextStatusChoices.STATUS_PLANNED}",
f"{vdcs[2].pk},{VirtualDeviceContextStatusChoices.STATUS_PLANNED}",
)
cls.bulk_edit_data = {
'status': VirtualDeviceContextStatusChoices.STATUS_OFFLINE,
}