From 094cdf1371cd21a87fc832cd948cee568498d6e2 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Sun, 22 Sep 2024 15:05:19 -0500 Subject: [PATCH 1/2] Fix issues identified by tests --- netbox_routing/api/_serializers/ospf.py | 6 ++-- netbox_routing/filtersets/ospf.py | 46 ++++++++++++++++++++++++- netbox_routing/graphql/filters.py | 24 +++++++++++++ netbox_routing/graphql/schema.py | 13 +++++++ netbox_routing/graphql/types.py | 43 +++++++++++++++++++++++ 5 files changed, 128 insertions(+), 4 deletions(-) diff --git a/netbox_routing/api/_serializers/ospf.py b/netbox_routing/api/_serializers/ospf.py index 97fed8e..9cfb2f8 100644 --- a/netbox_routing/api/_serializers/ospf.py +++ b/netbox_routing/api/_serializers/ospf.py @@ -19,8 +19,8 @@ class OSPFInstanceSerializer(NetBoxModelSerializer): class Meta: model = OSPFInstance - fields = ('url', 'id', 'display', 'name', 'router_id', 'process_id', 'device', 'description', 'comments',) - brief_fields = ('url', 'id', 'display', 'name', 'router_id', 'process_id', 'device') + fields = ('url', 'id', 'display', 'name', 'router_id', 'process_id', 'device', 'description', 'comments', ) + brief_fields = ('url', 'id', 'display', 'name', 'router_id', 'process_id', 'device', ) class OSPFAreaSerializer(NetBoxModelSerializer): @@ -29,7 +29,7 @@ class OSPFAreaSerializer(NetBoxModelSerializer): class Meta: model = OSPFArea fields = ('url', 'id', 'display', 'area_id', 'description', 'comments',) - brief_fields = ('url', 'id', 'display', 'area_id') + brief_fields = ('url', 'id', 'display', 'area_id',) class OSPFInterfaceSerializer(NetBoxModelSerializer): diff --git a/netbox_routing/filtersets/ospf.py b/netbox_routing/filtersets/ospf.py index 0ee40c2..3439112 100644 --- a/netbox_routing/filtersets/ospf.py +++ b/netbox_routing/filtersets/ospf.py @@ -3,7 +3,7 @@ from django.db.models import Q from django.utils.translation import gettext as _ -from dcim.models import Device +from dcim.models import Device, Interface from utilities.filters import MultiValueCharFilter from netbox.filtersets import NetBoxModelFilterSet @@ -83,6 +83,50 @@ def filter_aid(self, queryset, name, value): class OSPFInterfaceFilterSet(NetBoxModelFilterSet): + instance_id = django_filters.ModelMultipleChoiceFilter( + field_name='instance', + queryset=OSPFInstance.objects.all(), + label='Instance (ID)', + ) + instance = django_filters.ModelMultipleChoiceFilter( + field_name='instance__name', + queryset=OSPFInstance.objects.all(), + to_field_name='name', + label='Instance', + ) + area_id = django_filters.ModelMultipleChoiceFilter( + field_name='area', + queryset=OSPFArea.objects.all(), + label='Area (ID)', + ) + area = django_filters.ModelMultipleChoiceFilter( + field_name='area__area_id', + queryset=OSPFArea.objects.all(), + to_field_name='area_id', + label='Area', + ) + device_id = django_filters.ModelMultipleChoiceFilter( + field_name='interface__device', + queryset=Device.objects.all(), + label='Device (ID)', + ) + device = django_filters.ModelMultipleChoiceFilter( + field_name='interface__device__name', + queryset=Device.objects.all(), + to_field_name='name', + label='Device', + ) + interface_id = django_filters.ModelMultipleChoiceFilter( + field_name='interface', + queryset=Interface.objects.all(), + label='Area (ID)', + ) + interface = django_filters.ModelMultipleChoiceFilter( + field_name='interface__name', + queryset=Interface.objects.all(), + to_field_name='name', + label='Area', + ) class Meta: model = OSPFInterface diff --git a/netbox_routing/graphql/filters.py b/netbox_routing/graphql/filters.py index 95a57a2..62468f3 100644 --- a/netbox_routing/graphql/filters.py +++ b/netbox_routing/graphql/filters.py @@ -1,3 +1,6 @@ +from typing import Annotated + +import strawberry import strawberry_django from netbox_routing import filtersets, models @@ -6,6 +9,9 @@ __all__ = ( 'StaticRouteFilter', + 'OSPFInstanceFilter', + 'OSPFAreaFilter', + 'OSPFInterfaceFilter', ) @@ -14,3 +20,21 @@ class StaticRouteFilter(BaseFilterMixin): prefix: str next_hop: str + + +@strawberry_django.filter(models.OSPFInstance, lookups=True) +@autotype_decorator(filtersets.OSPFInstanceFilterSet) +class OSPFInstanceFilter(BaseFilterMixin): + router_id: str + + +@strawberry_django.filter(models.OSPFArea, lookups=True) +@autotype_decorator(filtersets.OSPFAreaFilterSet) +class OSPFAreaFilter(BaseFilterMixin): + pass + + +@strawberry_django.filter(models.OSPFInterface, lookups=True) +@autotype_decorator(filtersets.OSPFInterfaceFilterSet) +class OSPFInterfaceFilter(BaseFilterMixin): + pass diff --git a/netbox_routing/graphql/schema.py b/netbox_routing/graphql/schema.py index cb61742..07a73b2 100644 --- a/netbox_routing/graphql/schema.py +++ b/netbox_routing/graphql/schema.py @@ -12,6 +12,19 @@ class StaticRouteQuery: static_route_list: List[StaticRouteType] = strawberry_django.field() +@strawberry.type(name="Query") +class OSPFQuery: + ospf_instance: OSPFInstanceType = strawberry_django.field() + ospf_instance_list: List[OSPFInstanceType] = strawberry_django.field() + + ospf_area: OSPFAreaType = strawberry_django.field() + ospf_area_list: List[OSPFAreaType] = strawberry_django.field() + + ospf_interface: OSPFInterfaceType = strawberry_django.field() + ospf_interface_list: List[OSPFInterfaceType] = strawberry_django.field() + + schema = [ StaticRouteQuery, + OSPFQuery ] diff --git a/netbox_routing/graphql/types.py b/netbox_routing/graphql/types.py index dc2d432..b12ee81 100644 --- a/netbox_routing/graphql/types.py +++ b/netbox_routing/graphql/types.py @@ -10,6 +10,9 @@ __all__ = ( 'StaticRouteType', + 'OSPFInstanceType', + 'OSPFAreaType', + 'OSPFInterfaceType', ) @@ -28,3 +31,43 @@ class StaticRouteType(NetBoxObjectType): metric: int | None permanent: bool | None + +@strawberry_django.type( + models.OSPFInstance, + fields='__all__', + filters=OSPFInstanceFilter +) +class OSPFInstanceType(NetBoxObjectType): + + name: str + device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] + router_id: str + process_id: str + + +@strawberry_django.type( + models.OSPFArea, + fields='__all__', + filters=OSPFAreaFilter +) +class OSPFAreaType(NetBoxObjectType): + + area_id: str + + +@strawberry_django.type( + models.OSPFInterface, + fields='__all__', + filters=OSPFInterfaceFilter +) +class OSPFInterfaceType(NetBoxObjectType): + + device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] + instance: Annotated["OSPFInstanceType", strawberry.lazy('netbox_routing.graphql.types')] + area: Annotated["OSPFAreaType", strawberry.lazy('netbox_routing.graphql.types')] + interface: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] + priority: str | None + bfd: bool | None + authentication: str | None + passphrase: str | None + From 1d18e788a2e1cc833ee7ba3480a1cb606fcf1e97 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Sun, 22 Sep 2024 23:15:32 -0500 Subject: [PATCH 2/2] Fix up more errors detected with tests. Add some final missing tests --- netbox_routing/fields/ip.py | 2 - netbox_routing/forms/__init__.py | 5 + netbox_routing/forms/bulk_edit/__init__.py | 4 +- netbox_routing/forms/bulk_edit/ospf.py | 52 ++++++- netbox_routing/forms/bulk_edit/static.py | 11 +- netbox_routing/forms/bulk_import/__init__.py | 4 +- netbox_routing/forms/ospf.py | 21 ++- netbox_routing/forms/static.py | 3 +- netbox_routing/graphql/types.py | 1 - netbox_routing/models/ospf.py | 13 ++ netbox_routing/tests/base.py | 3 + netbox_routing/tests/test_api.py | 18 ++- netbox_routing/tests/test_forms.py | 89 ++++++++++- netbox_routing/tests/test_views.py | 155 +++++++++++++++++-- netbox_routing/urls.py | 4 + netbox_routing/views/ospf.py | 33 +++- 16 files changed, 372 insertions(+), 46 deletions(-) diff --git a/netbox_routing/fields/ip.py b/netbox_routing/fields/ip.py index bd3bd05..ac15df5 100644 --- a/netbox_routing/fields/ip.py +++ b/netbox_routing/fields/ip.py @@ -25,8 +25,6 @@ def to_python(self, value): raise ValidationError(e) def get_prep_value(self, value): - if not value: - return None if isinstance(value, list): return [str(self.to_python(v)) for v in value] return str(self.to_python(value)) diff --git a/netbox_routing/forms/__init__.py b/netbox_routing/forms/__init__.py index f873546..686bae5 100644 --- a/netbox_routing/forms/__init__.py +++ b/netbox_routing/forms/__init__.py @@ -13,14 +13,19 @@ # OSPF 'OSPFAreaForm', + 'OSPFAreaBulkEditForm', + 'OSPFAreaImportForm', 'OSPFAreaFilterForm', 'OSPFInstanceForm', + 'OSPFInstanceBulkEditForm', 'OSPFInstanceFilterForm', + 'OSPFInstanceImportForm', 'OSPFInterfaceForm', 'OSPFInterfaceFilterForm', 'OSPFInterfaceBulkEditForm', + 'OSPFInterfaceImportForm', 'BGPRouterForm', 'BGPScopeForm', diff --git a/netbox_routing/forms/bulk_edit/__init__.py b/netbox_routing/forms/bulk_edit/__init__.py index 57c16b9..0cff3d5 100644 --- a/netbox_routing/forms/bulk_edit/__init__.py +++ b/netbox_routing/forms/bulk_edit/__init__.py @@ -1,6 +1,6 @@ from .static import * from .objects import * -from .ospf import OSPFInterfaceBulkEditForm +from .ospf import * __all__ = ( @@ -8,7 +8,9 @@ 'StaticRouteBulkEditForm', # OSPF + 'OSPFInstanceBulkEditForm', 'OSPFInterfaceBulkEditForm', + 'OSPFAreaBulkEditForm', # Route Objects 'PrefixListEntryBulkEditForm', diff --git a/netbox_routing/forms/bulk_edit/ospf.py b/netbox_routing/forms/bulk_edit/ospf.py index 3d9133b..9d0272e 100644 --- a/netbox_routing/forms/bulk_edit/ospf.py +++ b/netbox_routing/forms/bulk_edit/ospf.py @@ -1,20 +1,62 @@ from django import forms from django.utils.translation import gettext as _ +from dcim.models import Device from netbox.forms import NetBoxModelBulkEditForm from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice -from utilities.forms.fields import DynamicModelChoiceField +from utilities.forms.fields import DynamicModelChoiceField, CommentField from netbox_routing import choices from netbox_routing.models import OSPFArea, OSPFInstance, OSPFInterface __all__ = ( 'OSPFInterfaceBulkEditForm', + 'OSPFInstanceBulkEditForm', + 'OSPFAreaBulkEditForm', ) from utilities.forms.rendering import FieldSet +class OSPFInstanceBulkEditForm(NetBoxModelBulkEditForm): + device = DynamicModelChoiceField( + queryset=Device.objects.all(), + label=_('Device'), + required=False, + selector=True + ) + + description = forms.CharField( + label=_('Description'), + max_length=200, + required=False + ) + comments = CommentField() + + model = OSPFInstance + fieldsets = ( + FieldSet('device', name='OSPF'), + FieldSet('description', ), + ) + nullable_fields = () + + +class OSPFAreaBulkEditForm(NetBoxModelBulkEditForm): + + description = forms.CharField( + label=_('Description'), + max_length=200, + required=False + ) + comments = CommentField() + + model = OSPFArea + fieldsets = ( + FieldSet('description'), + ) + nullable_fields = () + + class OSPFInterfaceBulkEditForm(NetBoxModelBulkEditForm): instance = DynamicModelChoiceField( queryset=OSPFInstance.objects.all(), @@ -37,9 +79,17 @@ class OSPFInterfaceBulkEditForm(NetBoxModelBulkEditForm): ) passphrase = forms.CharField(label=_('Passphrase'), required=False) + description = forms.CharField( + label=_('Description'), + max_length=200, + required=False + ) + comments = CommentField() + model = OSPFInterface fieldsets = ( FieldSet('instance', 'area', name='OSPF'), FieldSet('priority', 'bfd', 'authentication', 'passphrase', name='Attributes'), + FieldSet('description'), ) nullable_fields = () diff --git a/netbox_routing/forms/bulk_edit/static.py b/netbox_routing/forms/bulk_edit/static.py index bdf514a..076446f 100644 --- a/netbox_routing/forms/bulk_edit/static.py +++ b/netbox_routing/forms/bulk_edit/static.py @@ -5,7 +5,7 @@ from ipam.models import VRF from netbox.forms import NetBoxModelBulkEditForm from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES -from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, CommentField from utilities.forms.rendering import FieldSet from netbox_routing.models import StaticRoute @@ -32,10 +32,17 @@ class StaticRouteBulkEditForm(NetBoxModelBulkEditForm): metric = forms.IntegerField(label=_('Metric'), required=False) permanent = forms.ChoiceField(label=_('Permanent'), choices=BOOLEAN_WITH_BLANK_CHOICES, required=False) + description = forms.CharField( + label=_('Description'), + max_length=200, + required=False + ) + comments = CommentField() + model = StaticRoute fieldsets = ( FieldSet('devices', 'vrf', 'prefix', 'next_hop', name='Route'), FieldSet('metric', 'permanent', name='Attributes'), - FieldSet('description', 'comments') + FieldSet('description', ) ) nullable_fields = ('devices', 'vrf', 'metric', 'permanent', 'description', 'comments') diff --git a/netbox_routing/forms/bulk_import/__init__.py b/netbox_routing/forms/bulk_import/__init__.py index f9e5850..fb69772 100644 --- a/netbox_routing/forms/bulk_import/__init__.py +++ b/netbox_routing/forms/bulk_import/__init__.py @@ -1,8 +1,10 @@ -from .ospf import OSPFInterfaceImportForm +from .ospf import * __all__ = ( # OSPF + 'OSPFInstanceImportForm', + 'OSPFAreaImportForm', 'OSPFInterfaceImportForm', ) diff --git a/netbox_routing/forms/ospf.py b/netbox_routing/forms/ospf.py index 5fecc49..a05723d 100644 --- a/netbox_routing/forms/ospf.py +++ b/netbox_routing/forms/ospf.py @@ -1,10 +1,11 @@ from django import forms +from django.core.exceptions import ValidationError from django.utils.translation import gettext as _ from dcim.models import Interface, Device from netbox.forms import NetBoxModelForm from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES -from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, CommentField from netbox_routing.models import OSPFArea, OSPFInstance, OSPFInterface @@ -23,6 +24,7 @@ class OSPFInstanceForm(NetBoxModelForm): selector=True, label=_('Device'), ) + comments = CommentField() class Meta: model = OSPFInstance @@ -30,16 +32,17 @@ class Meta: class OSPFAreaForm(NetBoxModelForm): + comments = CommentField() class Meta: model = OSPFArea - fields = ('area_id', 'description', 'comments', ) + fields = ('area_id', 'description', 'comments', ) class OSPFInterfaceForm(NetBoxModelForm): device = DynamicModelChoiceField( queryset=Device.objects.all(), - required=True, + required=False, selector=True, label=_('Device'), ) @@ -67,6 +70,7 @@ class OSPFInterfaceForm(NetBoxModelForm): 'device_id': '$device', } ) + comments = CommentField() class Meta: @@ -84,3 +88,14 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance.pk: self.initial['device'] = self.instance.interface.device.pk + + def clean(self): + super().clean() + if self.cleaned_data.get('instance') and self.cleaned_data.get('interface'): + if self.cleaned_data.get('instance').device != self.cleaned_data.get('interface').device: + raise ValidationError( + { + 'instance': _('OSPF Instance Device and Interface Device must match'), + 'interface': _('OSPF Instance Device and Interface Device must match') + } + ) diff --git a/netbox_routing/forms/static.py b/netbox_routing/forms/static.py index 9d8ad30..4927992 100644 --- a/netbox_routing/forms/static.py +++ b/netbox_routing/forms/static.py @@ -2,7 +2,7 @@ from ipam.models import VRF from netbox.forms import NetBoxModelForm from netbox_routing.models import StaticRoute -from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, CommentField class StaticRouteForm(NetBoxModelForm): @@ -14,6 +14,7 @@ class StaticRouteForm(NetBoxModelForm): required=False, label='VRF' ) + comments = CommentField() class Meta: model = StaticRoute diff --git a/netbox_routing/graphql/types.py b/netbox_routing/graphql/types.py index b12ee81..ca74368 100644 --- a/netbox_routing/graphql/types.py +++ b/netbox_routing/graphql/types.py @@ -62,7 +62,6 @@ class OSPFAreaType(NetBoxObjectType): ) class OSPFInterfaceType(NetBoxObjectType): - device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] instance: Annotated["OSPFInstanceType", strawberry.lazy('netbox_routing.graphql.types')] area: Annotated["OSPFAreaType", strawberry.lazy('netbox_routing.graphql.types')] interface: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] diff --git a/netbox_routing/models/ospf.py b/netbox_routing/models/ospf.py index 44a6aa8..be26e2b 100644 --- a/netbox_routing/models/ospf.py +++ b/netbox_routing/models/ospf.py @@ -1,3 +1,5 @@ +import netaddr +from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from django.utils.translation import gettext as _ @@ -56,6 +58,17 @@ def __str__(self): def get_absolute_url(self): return reverse('plugins:netbox_routing:ospfarea', args=[self.pk]) + def clean(self): + super().clean() + area_id = self.area_id + try: + int(area_id) + except ValueError: + try: + str(netaddr.IPAddress(area_id)) + except netaddr.core.AddrFormatError: + raise ValidationError({'area_id': ['This field must be an integer or a valid net address']}) + class OSPFInterface(PrimaryModel): instance = models.ForeignKey( diff --git a/netbox_routing/tests/base.py b/netbox_routing/tests/base.py index 89f8254..c00c2ed 100644 --- a/netbox_routing/tests/base.py +++ b/netbox_routing/tests/base.py @@ -8,4 +8,7 @@ def model_to_dict(self, instance, fields, api=False): if api: if type(value) is IPAddress: model_dict[key] = str(value) + elif not api and key in ['router_id', ]: + if type(value) is IPAddress: + model_dict[key] = str(value) return model_dict \ No newline at end of file diff --git a/netbox_routing/tests/test_api.py b/netbox_routing/tests/test_api.py index b3bc5ec..cc6205c 100644 --- a/netbox_routing/tests/test_api.py +++ b/netbox_routing/tests/test_api.py @@ -78,11 +78,12 @@ def setUpTestData(cls): ] -class OSPFInstanceTest(IPAddressFieldMixin , APIViewTestCases.APIViewTestCase): +class OSPFInstanceTest(IPAddressFieldMixin, APIViewTestCases.APIViewTestCase): model = OSPFInstance view_namespace = 'plugins-api:netbox_routing' brief_fields = ['device', 'display', 'id', 'name', 'process_id', 'router_id', 'url', ] + user_permissions = ('dcim.view_device', ) bulk_update_data = { 'description': "A test description" @@ -115,7 +116,6 @@ class OSPFAreaTest(IPAddressFieldMixin , APIViewTestCases.APIViewTestCase): view_namespace = 'plugins-api:netbox_routing' brief_fields = ['area_id', 'display', 'id', 'url', ] - bulk_update_data = { 'description': "A test description" } @@ -140,8 +140,11 @@ def setUpTestData(cls): class OSPFInterfaceTest(IPAddressFieldMixin, APIViewTestCases.APIViewTestCase): model = OSPFInterface view_namespace = 'plugins-api:netbox_routing' - brief_fields = ['device', 'display', 'id', 'name', 'process_id', 'router_id', 'url', ] + brief_fields = ['area', 'display', 'id', 'instance', 'interface', 'url', ] + user_permissions = ( + 'netbox_routing.view_ospfinstance', 'netbox_routing.view_ospfarea', 'dcim.view_device', 'dcim.view_interface', + ) bulk_update_data = { 'description': "A test description" @@ -163,20 +166,19 @@ def setUpTestData(cls): Interface.objects.bulk_create(interfaces) data = ( - cls.model(device=device, instance=instance, area=area, interface=interfaces[0], ), - cls.model(device=device, instance=instance, area=area, interface=interfaces[1], ), - cls.model(device=device, instance=instance, area=area, interface=interfaces[2], ), + cls.model(instance=instance, area=area, interface=interfaces[0], ), + cls.model(instance=instance, area=area, interface=interfaces[1], ), + cls.model(instance=instance, area=area, interface=interfaces[2], ), ) cls.model.objects.bulk_create(data) cls.create_data = [ { - 'device': device.pk, 'instance': instance.pk, 'area': area.pk, 'interface': interfaces[3].pk, 'priority': 2, - 'authentication': 'passphrase', + 'authentication': 'message-digest', 'passphrase': 'test-password', 'bfd': True, }, diff --git a/netbox_routing/tests/test_forms.py b/netbox_routing/tests/test_forms.py index f1de860..a020cec 100644 --- a/netbox_routing/tests/test_forms.py +++ b/netbox_routing/tests/test_forms.py @@ -1,6 +1,6 @@ from django.test import TestCase -from dcim.models import Device +from dcim.models import Device, Interface from utilities.testing import create_test_device from netbox_routing.forms import * @@ -24,3 +24,90 @@ def test_staticroute(self): }) self.assertTrue(form.is_valid()) self.assertTrue(form.save()) + + +class OSPFInstanceTestCase(TestCase): + + @classmethod + def setUpTestData(cls): + device = create_test_device(name='Device 1') + + def test_instance(self): + form = OSPFInstanceForm(data={ + 'name': 'Instance 1', + 'process_id': '0', + 'router_id': '10.10.10.1', + 'device': Device.objects.first().pk, + }) + if not form.is_valid(): + print(form.errors) + self.assertTrue(form.is_valid()) + self.assertTrue(form.save()) + + +class OSPFAreaTestCase(TestCase): + + @classmethod + def setUpTestData(cls): + pass + + def test_valid_area_id_with_ip(self): + form = OSPFAreaForm(data={ + 'area_id': '0.0.0.0', + }) + self.assertTrue(form.is_valid()) + self.assertTrue(form.save()) + + def test_valid_area_id_with_integer(self): + form = OSPFAreaForm(data={ + 'area_id': '0', + }) + self.assertTrue(form.is_valid()) + self.assertTrue(form.save()) + + def test_invalid_area(self): + form = OSPFAreaForm(data={ + 'area_id': 'a.a.a.a', + }) + self.assertFalse(form.is_valid()) + with self.assertRaises(ValueError): + form.save() + + +class OSPFInterfaceTestCase(TestCase): + + @classmethod + def setUpTestData(cls): + device = create_test_device(name='Device 1') + interface = Interface.objects.create(name='Interface 1', device=device, type='virtual') + instance = OSPFInstance.objects.create( + name='Instance 1', + router_id='10.10.10.1', + process_id=0, + device_id=device.pk + ) + area = OSPFArea.objects.create(area_id='0.0.0.0') + + def test_interface_with_correct_device(self): + form = OSPFInterfaceForm(data={ + 'device': Device.objects.first().pk, + 'interface': Interface.objects.first().pk, + 'instance': OSPFInstance.objects.first().pk, + 'area': OSPFArea.objects.first().pk, + }) + self.assertTrue(form.is_valid()) + self.assertTrue(form.save()) + + def test_interface_with_incorrect_device(self): + device = create_test_device(name='Device 2') + interface = Interface.objects.create(name='Interface 1', device=device, type='virtual') + + form = OSPFInterfaceForm(data={ + 'device': device.pk, + 'interface': interface.pk, + 'instance': OSPFInstance.objects.first().pk, + 'area': OSPFArea.objects.first().pk, + }) + self.assertFalse(form.is_valid()) + with self.assertRaises(ValueError): + form.save() diff --git a/netbox_routing/tests/test_views.py b/netbox_routing/tests/test_views.py index ccbaf9b..93b201a 100644 --- a/netbox_routing/tests/test_views.py +++ b/netbox_routing/tests/test_views.py @@ -1,11 +1,11 @@ import netaddr -from dcim.models import Device -from extras.models import Tag +from dcim.models import Interface from ipam.models import VRF -from utilities.testing import ViewTestCases, create_tags, create_test_device +from utilities.testing import ViewTestCases, create_test_device -from netbox_routing.models import StaticRoute +from netbox_routing.models import StaticRoute, OSPFInstance, OSPFArea, OSPFInterface +from netbox_routing.tests.base import IPAddressFieldMixin class StaticRouteTestCase( @@ -52,21 +52,142 @@ def setUpTestData(cls): 'metric': 10, } - """ - cls.csv_data = ( - "name,slug,description", - "Region 4,region-4,Fourth region", - "Region 5,region-5,Fifth region", - "Region 6,region-6,Sixth region", + def _get_base_url(self): + return 'plugins:netbox_routing:staticroute_{}' + + +class OSPFInstanceTestCase( + IPAddressFieldMixin, + ViewTestCases.GetObjectViewTestCase, + ViewTestCases.GetObjectChangelogViewTestCase, + ViewTestCases.CreateObjectViewTestCase, + ViewTestCases.EditObjectViewTestCase, + ViewTestCases.DeleteObjectViewTestCase, + ViewTestCases.ListObjectsViewTestCase, + ViewTestCases.BulkEditObjectsViewTestCase, + ViewTestCases.BulkDeleteObjectsViewTestCase, +): + # ViewTestCases.BulkImportObjectsViewTestCase, + model = OSPFInstance + + @classmethod + def setUpTestData(cls): + devices = [create_test_device(name='Device 1'), create_test_device(name='Device 2')] + + routes = ( + cls.model(name="Instance 0", device=devices[0], router_id='0.0.0.0', process_id='0'), + cls.model(name="Instance 1", device=devices[0], router_id='1.1.1.1', process_id='1'), + cls.model(name="Instance 2", device=devices[0], router_id='2.2.2.2', process_id='2'), ) + cls.model.objects.bulk_create(routes) - cls.csv_update_data = ( - "id,name,description", - f"{regions[0].pk},Region 7,Fourth region7", - f"{regions[1].pk},Region 8,Fifth region8", - f"{regions[2].pk},Region 0,Sixth region9", + cls.form_data = { + 'name': 'Instance X', + 'device': devices[1].pk, + 'router_id': '4.4.4.4', + 'process_id': 4, + } + + cls.bulk_edit_data = { + 'description': 'A test Instance description' + } + + def _get_base_url(self): + return 'plugins:netbox_routing:ospfinstance_{}' + + +class OSPFAreaTestCase( + IPAddressFieldMixin, + ViewTestCases.GetObjectViewTestCase, + ViewTestCases.GetObjectChangelogViewTestCase, + ViewTestCases.CreateObjectViewTestCase, + ViewTestCases.EditObjectViewTestCase, + ViewTestCases.DeleteObjectViewTestCase, + ViewTestCases.ListObjectsViewTestCase, + ViewTestCases.BulkEditObjectsViewTestCase, + ViewTestCases.BulkDeleteObjectsViewTestCase, +): + # ViewTestCases.BulkImportObjectsViewTestCase, + model = OSPFArea + + @classmethod + def setUpTestData(cls): + areas = ( + cls.model(area_id='0.0.0.0'), + cls.model(area_id='1.1.1.1'), + cls.model(area_id='2.2.2.2'), ) - """ + cls.model.objects.bulk_create(areas) + + cls.form_data = { + 'area_id': '4.4.4.4', + } + + cls.bulk_edit_data = { + 'description': 'A test Area description' + } def _get_base_url(self): - return 'plugins:netbox_routing:staticroute_{}' + return 'plugins:netbox_routing:ospfarea_{}' + + +class OSPFInterfaceTestCase( + IPAddressFieldMixin, + ViewTestCases.GetObjectViewTestCase, + ViewTestCases.GetObjectChangelogViewTestCase, + ViewTestCases.CreateObjectViewTestCase, + ViewTestCases.EditObjectViewTestCase, + ViewTestCases.DeleteObjectViewTestCase, + ViewTestCases.ListObjectsViewTestCase, + ViewTestCases.BulkEditObjectsViewTestCase, + ViewTestCases.BulkDeleteObjectsViewTestCase, +): + # ViewTestCases.BulkImportObjectsViewTestCase, + model = OSPFInterface + + @classmethod + def setUpTestData(cls): + devices = [create_test_device(name='Device 1'), create_test_device(name='Device 2')] + interfaces = ( + Interface(name='Interface 1', device=devices[0], type='virtual'), + Interface(name='Interface 2', device=devices[0], type='virtual'), + Interface(name='Interface 3', device=devices[1], type='virtual'), + Interface(name='Interface 4', device=devices[1], type='virtual'), + ) + Interface.objects.bulk_create(interfaces) + + instances = ( + OSPFInstance(name="Instance 0", device=devices[0], router_id='0.0.0.0', process_id='0'), + OSPFInstance(name="Instance 1", device=devices[0], router_id='1.1.1.1', process_id='1'), + OSPFInstance(name="Instance 2", device=devices[1], router_id='2.2.2.2', process_id='2'), + OSPFInstance(name="Instance 3", device=devices[1], router_id='3.3.3.3', process_id='3'), + ) + OSPFInstance.objects.bulk_create(instances) + + areas = ( + OSPFArea(area_id='0.0.0.0'), + OSPFArea(area_id='1.1.1.1'), + OSPFArea(area_id='2.2.2.2'), + OSPFArea(area_id='3.3.3.3'), + ) + OSPFArea.objects.bulk_create(areas) + + ospfinterfaces = ( + cls.model(interface=interfaces[0], instance=instances[0], area=areas[0]), + cls.model(interface=interfaces[1], instance=instances[1], area=areas[1]), + cls.model(interface=interfaces[2], instance=instances[2], area=areas[2]), + ) + cls.model.objects.bulk_create(ospfinterfaces) + + cls.form_data = { + 'interface': interfaces[3].pk, + 'area': areas[3].pk, + 'instance': instances[3].pk, + } + + cls.bulk_edit_data = { + 'description': 'A test Interface description' + } + + def _get_base_url(self): + return 'plugins:netbox_routing:ospfinterface_{}' diff --git a/netbox_routing/urls.py b/netbox_routing/urls.py index f434d02..dfe26de 100644 --- a/netbox_routing/urls.py +++ b/netbox_routing/urls.py @@ -20,6 +20,8 @@ path('ospf/instance/', views.OSPFInstanceListView.as_view(), name='ospfinstance_list'), path('ospf/instance/add/', views.OSPFInstanceEditView.as_view(), name='ospfinstance_add'), + path('ospf/instance/edit/', views.OSPFInstanceBulkEditView.as_view(), name='ospfinstance_bulk_edit'), + path('ospf/instance/delete/', views.OSPFInstanceBulkDeleteView.as_view(), name='ospfinstance_bulk_delete'), path('ospf/instance/import/', views.OSPFInstanceListView.as_view(), name='ospfinstance_import'), path('ospf/instance//', views.OSPFInstanceView.as_view(), name='ospfinstance'), path('ospf/instance//edit/', views.OSPFInstanceEditView.as_view(), name='ospfinstance_edit'), @@ -29,6 +31,8 @@ path('ospf/area/', views.OSPFAreaListView.as_view(), name='ospfarea_list'), path('ospf/area/add/', views.OSPFAreaEditView.as_view(), name='ospfarea_add'), + path('ospf/area/edit/', views.OSPFAreaBulkEditView.as_view(), name='ospfarea_bulk_edit'), + path('ospf/area/delete/', views.OSPFAreaBulkDeleteView.as_view(), name='ospfarea_bulk_delete'), path('ospf/area/import/', views.OSPFAreaListView.as_view(), name='ospfarea_import'), path('ospf/area//', views.OSPFAreaView.as_view(), name='ospfarea'), path('ospf/area//edit/', views.OSPFAreaEditView.as_view(), name='ospfarea_edit'), diff --git a/netbox_routing/views/ospf.py b/netbox_routing/views/ospf.py index 29fcb7c..517cfdf 100644 --- a/netbox_routing/views/ospf.py +++ b/netbox_routing/views/ospf.py @@ -1,9 +1,7 @@ from netbox.views.generic import ObjectListView, ObjectEditView, ObjectView, ObjectDeleteView, ObjectChildrenView, \ BulkImportView, BulkEditView, BulkDeleteView from netbox_routing.filtersets.ospf import OSPFInterfaceFilterSet, OSPFAreaFilterSet, OSPFInstanceFilterSet -from netbox_routing.forms import OSPFInstanceFilterForm, OSPFInstanceForm, OSPFAreaFilterForm, OSPFAreaForm, \ - OSPFInterfaceFilterForm, OSPFInterfaceForm, OSPFInterfaceBulkEditForm, OSPFInterfaceImportForm -from netbox_routing.forms.bulk_import.ospf import OSPFAreaImportForm, OSPFInstanceImportForm +from netbox_routing.forms import * from netbox_routing.tables.ospf import OSPFAreaTable, OSPFInstanceTable, OSPFInterfaceTable from utilities.views import register_model_view, ViewTab @@ -15,13 +13,17 @@ 'OSPFInstanceView', 'OSPFInstanceInterfacesView', 'OSPFInstanceEditView', + 'OSPFInstanceBulkEditView', 'OSPFInstanceDeleteView', + 'OSPFInstanceBulkDeleteView', 'OSPFAreaListView', 'OSPFAreaView', 'OSPFAreaInterfacesView', 'OSPFAreaEditView', + 'OSPFAreaBulkEditView', 'OSPFAreaDeleteView', + 'OSPFAreaBulkDeleteView', 'OSPFInterfaceListView', 'OSPFInterfaceView', @@ -73,11 +75,11 @@ class OSPFInstanceEditView(ObjectEditView): @register_model_view(OSPFInstance, name='bulk_edit') -class OSPFInstanceBulkEditView(ObjectEditView): +class OSPFInstanceBulkEditView(BulkEditView): queryset = OSPFInstance.objects.all() - form = OSPFInstanceForm filterset = OSPFInstanceFilterSet - table = OSPFInterfaceTable + table = OSPFInstanceTable + form = OSPFInstanceBulkEditForm @register_model_view(OSPFInstance, name='delete') @@ -86,10 +88,10 @@ class OSPFInstanceDeleteView(ObjectDeleteView): @register_model_view(OSPFInstance, name='bulk_delete') -class OSPFInstanceBulkDeleteView(ObjectDeleteView): +class OSPFInstanceBulkDeleteView(BulkDeleteView): queryset = OSPFInstance.objects.all() filterset = OSPFInstanceFilterSet - table = OSPFInterfaceTable + table = OSPFInstanceTable class OSPFInstanceBulkImportView(BulkImportView): @@ -136,11 +138,26 @@ class OSPFAreaEditView(ObjectEditView): form = OSPFAreaForm +@register_model_view(OSPFArea, name='bulk_edit') +class OSPFAreaBulkEditView(BulkEditView): + queryset = OSPFArea.objects.all() + table = OSPFAreaTable + filterset = OSPFAreaFilterSet + form = OSPFAreaBulkEditForm + + @register_model_view(OSPFArea, name='delete') class OSPFAreaDeleteView(ObjectDeleteView): queryset = OSPFArea.objects.all() +@register_model_view(OSPFArea, name='delete') +class OSPFAreaBulkDeleteView(BulkDeleteView): + queryset = OSPFArea.objects.all() + table = OSPFAreaTable + filterset = OSPFAreaFilterSet + + class OSPFAreaBulkImportView(BulkImportView): queryset = OSPFArea.objects.all() model_form = OSPFAreaImportForm