diff --git a/netbox_routing/__init__.py b/netbox_routing/__init__.py index aa2d8ea..b181896 100644 --- a/netbox_routing/__init__.py +++ b/netbox_routing/__init__.py @@ -1,6 +1,5 @@ from extras.plugins import PluginConfig - try: from importlib.metadata import metadata except ModuleNotFoundError: @@ -16,11 +15,11 @@ class NetboxRouting(PluginConfig): version = plugin.get('Version') author = plugin.get('Author') author_email = plugin.get('Author-email') - base_url = 'netbox-plugin-extensions' - min_version = '3.2' + base_url = 'routing' + min_version = '3.2.0b1' required_settings = [] caching_config = {} default_settings = {} -config = NetboxPluginExtensions \ No newline at end of file +config = NetboxRouting diff --git a/netbox_routing/api/__init__.py b/netbox_routing/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netbox_routing/api/nested_serializers/__init__.py b/netbox_routing/api/nested_serializers/__init__.py new file mode 100644 index 0000000..3acd685 --- /dev/null +++ b/netbox_routing/api/nested_serializers/__init__.py @@ -0,0 +1,12 @@ +from .static import NestedStaticRouteSerializer +from .objects import NestedPrefixListSerializer, NestedPrefixListEntrySerializer, NestedRouteMapSerializer,\ + NestedRouteMapEntrySerializer + +__all__ = ( + 'NestedStaticRouteSerializer', + + 'NestedPrefixListSerializer', + 'NestedPrefixListEntrySerializer', + 'NestedRouteMapSerializer', + 'NestedRouteMapEntrySerializer', +) diff --git a/netbox_routing/api/nested_serializers/objects.py b/netbox_routing/api/nested_serializers/objects.py new file mode 100644 index 0000000..280dda8 --- /dev/null +++ b/netbox_routing/api/nested_serializers/objects.py @@ -0,0 +1,43 @@ +from rest_framework import serializers + +from netbox.api import WritableNestedSerializer +from netbox_routing.models import PrefixList, PrefixListEntry, RouteMap, RouteMapEntry + + +__all__ = ( + 'NestedStaticRouteSerializer' +) + + +class NestedPrefixListSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_routing-api:prefixlist-detail') + + class Meta: + model = PrefixList + fields = ('url', 'id', 'name') + + +class NestedPrefixListEntrySerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_routing-api:prefixlist-detail') + prefix_list = NestedPrefixListSerializer + + class Meta: + model = PrefixListEntry + fields = ('url', 'id', 'prefix_list') + + +class NestedRouteMapSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_routing-api:prefixlist-detail') + + class Meta: + model = RouteMap + fields = ('url', 'id', 'name') + + +class NestedRouteMapEntrySerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_routing-api:prefixlist-detail') + route_map = NestedRouteMapSerializer + + class Meta: + model = RouteMapEntry + fields = ('url', 'id', 'route_map') diff --git a/netbox_routing/api/nested_serializers/static.py b/netbox_routing/api/nested_serializers/static.py new file mode 100644 index 0000000..d1bb8e7 --- /dev/null +++ b/netbox_routing/api/nested_serializers/static.py @@ -0,0 +1,17 @@ +from rest_framework import serializers + +from netbox.api import WritableNestedSerializer +from netbox_routing.models import StaticRoute + + +__all__ = ( + 'NestedStaticRouteSerializer' +) + + +class NestedStaticRouteSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_routing-api:staticroute-detail') + + class Meta: + model = StaticRoute + fields = ('url', 'id', 'prefix', 'next_hop', 'name', 'metric', 'permanent') \ No newline at end of file diff --git a/netbox_routing/api/serializers/__init__.py b/netbox_routing/api/serializers/__init__.py new file mode 100644 index 0000000..d367121 --- /dev/null +++ b/netbox_routing/api/serializers/__init__.py @@ -0,0 +1,11 @@ +from .static import StaticRouteSerializer +from .objects import PrefixListSerializer, PrefixListEntrySerializer, RouteMapSerializer, RouteMapEntrySerializer + +__all__ = ( + 'StaticRouteSerializer', + + 'PrefixListSerializer', + 'PrefixListEntrySerializer', + 'RouteMapSerializer', + 'RouteMapEntrySerializer', +) diff --git a/netbox_routing/api/serializers/objects.py b/netbox_routing/api/serializers/objects.py new file mode 100644 index 0000000..83bb94e --- /dev/null +++ b/netbox_routing/api/serializers/objects.py @@ -0,0 +1,45 @@ +from rest_framework import serializers + +from netbox_routing.api.nested_serializers import NestedPrefixListSerializer, NestedRouteMapSerializer +from netbox.api.serializers import NetBoxModelSerializer +from netbox_routing.models import PrefixList, PrefixListEntry, RouteMap, RouteMapEntry + + +__all__ = ( + 'StaticRouteSerializer' +) + + +class PrefixListSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_routing-api:prefixlist-detail') + + class Meta: + model = PrefixList + fields = ('url', 'id', 'name') + + +class PrefixListEntrySerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_routing-api:prefixlistentry-detail') + prefix_list = NestedPrefixListSerializer() + + class Meta: + model = PrefixListEntry + fields = ('url', 'id', 'prefix_list', 'sequence', 'type', 'prefix', 'le', 'ge') + + +class RouteMapSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_routing-api:prefixlist-detail') + + class Meta: + model = RouteMap + fields = ('url', 'id', 'name') + + +class RouteMapEntrySerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_routing-api:prefixlistentry-detail') + route_map = NestedRouteMapSerializer() + + + class Meta: + model = RouteMapEntry + fields = ('url', 'id', 'route_map', 'sequence', 'type') diff --git a/netbox_routing/api/serializers/static.py b/netbox_routing/api/serializers/static.py new file mode 100644 index 0000000..ef00731 --- /dev/null +++ b/netbox_routing/api/serializers/static.py @@ -0,0 +1,21 @@ +from rest_framework import serializers + +from dcim.api.nested_serializers import NestedDeviceSerializer +from ipam.api.nested_serializers import NestedVRFSerializer +from netbox.api.serializers import NetBoxModelSerializer +from netbox_routing.models import StaticRoute + + +__all__ = ( + 'StaticRouteSerializer' +) + + +class StaticRouteSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_routing-api:staticroute-detail') + devices = NestedDeviceSerializer(many=True) + vrf = NestedVRFSerializer() + + class Meta: + model = StaticRoute + fields = ('url', 'id', 'devices', 'vrf', 'prefix', 'next_hop', 'name', 'metric', 'permanent') diff --git a/netbox_routing/api/urls.py b/netbox_routing/api/urls.py new file mode 100644 index 0000000..57afef6 --- /dev/null +++ b/netbox_routing/api/urls.py @@ -0,0 +1,10 @@ +from netbox.api.routers import NetBoxRouter +from .views import StaticRouteViewSet, PrefixListViewSet, RouteMapViewSet, PrefixListEntryViewSet, RouteMapEntryViewSet + +router = NetBoxRouter() +router.register('staticroute', StaticRouteViewSet) +router.register('prefix-list', PrefixListViewSet) +router.register('prefix-list-entry', PrefixListEntryViewSet) +router.register('route-map', RouteMapViewSet) +router.register('route-map-entry', RouteMapEntryViewSet) +urlpatterns = router.urls diff --git a/netbox_routing/api/views/__init__.py b/netbox_routing/api/views/__init__.py new file mode 100644 index 0000000..91cf2b6 --- /dev/null +++ b/netbox_routing/api/views/__init__.py @@ -0,0 +1,10 @@ +from .static import StaticRouteViewSet +from .objects import PrefixListViewSet, PrefixListEntryViewSet, RouteMapViewSet, RouteMapEntryViewSet + +__all__ = ( + 'StaticRouteViewSet', + 'PrefixListViewSet', + 'PrefixListEntryViewSet', + 'RouteMapViewSet', + 'RouteMapEntryViewSet', +) diff --git a/netbox_routing/api/views/objects.py b/netbox_routing/api/views/objects.py new file mode 100644 index 0000000..020a649 --- /dev/null +++ b/netbox_routing/api/views/objects.py @@ -0,0 +1,24 @@ +from netbox.api.viewsets import ModelViewSet +from netbox_routing.api.serializers import PrefixListSerializer, PrefixListEntrySerializer, RouteMapSerializer, \ + RouteMapEntrySerializer +from netbox_routing.models import PrefixList, PrefixListEntry, RouteMap, RouteMapEntry + + +class PrefixListViewSet(ModelViewSet): + queryset = PrefixList.objects.all() + serializer_class = PrefixListSerializer + + +class PrefixListEntryViewSet(ModelViewSet): + queryset = PrefixListEntry.objects.all() + serializer_class = PrefixListEntrySerializer + + +class RouteMapViewSet(ModelViewSet): + queryset = RouteMap.objects.all() + serializer_class = RouteMapSerializer + + +class RouteMapEntryViewSet(ModelViewSet): + queryset = RouteMapEntry.objects.all() + serializer_class = RouteMapEntrySerializer diff --git a/netbox_routing/api/views/static.py b/netbox_routing/api/views/static.py new file mode 100644 index 0000000..f06f3a7 --- /dev/null +++ b/netbox_routing/api/views/static.py @@ -0,0 +1,8 @@ +from netbox.api.viewsets import ModelViewSet +from netbox_routing.api.serializers import StaticRouteSerializer +from netbox_routing.models import StaticRoute + + +class StaticRouteViewSet(ModelViewSet): + queryset = StaticRoute.objects.all() + serializer_class = StaticRouteSerializer diff --git a/netbox_routing/choices/__init__.py b/netbox_routing/choices/__init__.py new file mode 100644 index 0000000..73b25b4 --- /dev/null +++ b/netbox_routing/choices/__init__.py @@ -0,0 +1 @@ +from .bgp import * diff --git a/netbox_routing/choices/bgp.py b/netbox_routing/choices/bgp.py new file mode 100644 index 0000000..5e6b828 --- /dev/null +++ b/netbox_routing/choices/bgp.py @@ -0,0 +1,65 @@ +from utilities.choices import ChoiceSet + + +class BGPAdditionalPathSelectChoices(ChoiceSet): + ALL = 'all' + BACKUP = 'backup' + BEST_EXTERNAL = 'best-external' + GROUP_BEST = 'group-best' + + CHOICES = [ + (ALL, 'All'), + (BACKUP, 'Backup'), + (BEST_EXTERNAL, 'Best External'), + (GROUP_BEST, 'Group Best') + ] + + +class BGPBestPathASPath(ChoiceSet): + IGNORE = 'ignore' + MULTIPATH = 'multipath-relax' + + CHOICES = [ + (IGNORE, 'Ignore'), + (MULTIPATH, 'Multipath Relax Comparison') + ] + + +class BGPAddressFamilies(ChoiceSet): + IPV4_UNICAST = 'ipv4-unicast' + IPV6_UNICAST = 'ipv6-unicast' + VPNV4_UNICAST = 'vpnv4-unicast' + VPNV6_UNICAST = 'vpnv6-unicast' + IPV4_MULTICAST = 'ipv4-multicast' + IPV6_MULTICAST = 'ipv6-multicast' + VPNV4_MULTICAST = 'vpnv4-multicast' + VPNV6_MULTICAST = 'vpnv6-multicast' + IPV4_FLOWSPEC = 'ipv4-flowspec' + IPV6_FLOWSPEC = 'ipv6-flowspec' + VPNV4_FLOWSPEC = 'vpnv4-flowspec' + VPNV6_FLOWSPEC = 'vpnv6-flowspec' + NSAP = 'nsap' + L2VPNVPLS = 'l2vpn-vpls' + L2VPSEVPN = 'l2vpn-evpn' + LINKSTATE = 'link-state' + RTFILTER_UNICAST = 'rtfilter-unicast' + + CHOICES = [ + (IPV4_UNICAST, 'IPv4 Unicast'), + (IPV6_UNICAST, 'IPv6 Unicast'), + (VPNV4_UNICAST, 'VPNv4 Unicast'), + (VPNV6_UNICAST, 'VPNv6 Unicast'), + (IPV4_MULTICAST, 'IPv4 Multicast'), + (IPV6_MULTICAST, 'IPv6 Multicast'), + (VPNV4_UNICAST, 'VPNv4 Multicast'), + (VPNV6_MULTICAST, 'VPNv6 Multicast'), + (IPV4_FLOWSPEC, 'IPv4 Flowspec'), + (IPV6_FLOWSPEC, 'IPv6 Flowspec'), + (VPNV4_FLOWSPEC, 'VPNv4 Flowspec'), + (VPNV6_FLOWSPEC, 'VPNv6 Flowspec'), + (NSAP, 'NSAP'), + (L2VPNVPLS, 'L2VPN VPLS'), + (L2VPSEVPN, 'L2VPN EVPN'), + (LINKSTATE, 'LINK-STATE'), + (RTFILTER_UNICAST, 'RTFILTER') + ] diff --git a/netbox_routing/choices/objects.py b/netbox_routing/choices/objects.py new file mode 100644 index 0000000..baa2ce1 --- /dev/null +++ b/netbox_routing/choices/objects.py @@ -0,0 +1,11 @@ +from utilities.choices import ChoiceSet + + +class PermitDenyChoices(ChoiceSet): + PERMIT = 'permit' + DENY = 'deny' + + CHOICES = [ + (PERMIT, 'Permit'), + (DENY, 'Deny') + ] diff --git a/netbox_routing/constants/__init__.py b/netbox_routing/constants/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netbox_routing/constants/bgp.py b/netbox_routing/constants/bgp.py new file mode 100644 index 0000000..b95719f --- /dev/null +++ b/netbox_routing/constants/bgp.py @@ -0,0 +1,26 @@ +from django.db.models import Q + +BGPSETTING_ASSIGNMENT_MODELS = Q( + Q(app_label='netbox_routing', model='bgprouter') | + Q(app_label='netbox_routing', model='bgpscope') +) + +BGPAF_ASSIGNMENT_MODELS = Q( + Q(app_label='netbox_routing', model='bgprouter') | + Q(app_label='netbox_routing', model='bgpscope') | + Q(app_label='netbox_routing', model='bgpneighbor') | + Q(app_label='ipam', model='VRF') +) + +BGPPEER_ASSIGNMENT_MODELS = Q( + Q(app_label='netbox_routing', model='bgprouter') | + Q(app_label='netbox_routing', model='bgpscope') | + Q(app_label='netbox_routing', model='bgpaddressfamily') +) + +BGPPEERAF_ASSIGNMENT_MODELS = Q( + Q(app_label='netbox_routing', model='bgppeer') | + Q(app_label='netbox_routing', model='bgppeergroup') | + Q(app_label='netbox_routing', model='bgptemplatepeer') | + Q(app_label='netbox_routing', model='bgptemplatepeerpolicy') +) \ No newline at end of file diff --git a/netbox_routing/fields/__init__.py b/netbox_routing/fields/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netbox_routing/fields/ip.py b/netbox_routing/fields/ip.py new file mode 100644 index 0000000..bd3bd05 --- /dev/null +++ b/netbox_routing/fields/ip.py @@ -0,0 +1,40 @@ +from django.core.exceptions import ValidationError +from django.db import models +from netaddr import AddrFormatError, IPAddress + +from ipam.formfields import IPAddressFormField + + +class IPAddressField(models.Field): + + def python_type(self): + return IPAddress + + def from_db_value(self, value, expression, connection): + return self.to_python(value) + + def to_python(self, value): + if not value: + return value + try: + # Always return a netaddr.IPNetwork object. (netaddr.IPAddress does not provide a mask.) + return IPAddress(value) + except AddrFormatError: + raise ValidationError("Invalid IP address format: {}".format(value)) + except (TypeError, ValueError) as e: + 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)) + + def form_class(self): + return IPAddressFormField + + def formfield(self, **kwargs): + defaults = {'form_class': self.form_class()} + defaults.update(kwargs) + return super().formfield(**defaults) \ No newline at end of file diff --git a/netbox_routing/filtersets/__init__.py b/netbox_routing/filtersets/__init__.py new file mode 100644 index 0000000..685e376 --- /dev/null +++ b/netbox_routing/filtersets/__init__.py @@ -0,0 +1,11 @@ +from .static import StaticRouteFilterSet +from .objects import PrefixListFilterSet, PrefixListEntryFilterSet, RouteMapFilterSet, RouteMapEntryFilterSet + +__all__ = ( + 'StaticRouteFilterSet', + + 'PrefixListFilterSet', + 'PrefixListEntryFilterSet', + 'RouteMapFilterSet', + 'RouteMapEntryFilterSet' +) diff --git a/netbox_routing/filtersets/objects.py b/netbox_routing/filtersets/objects.py new file mode 100644 index 0000000..46683ee --- /dev/null +++ b/netbox_routing/filtersets/objects.py @@ -0,0 +1,46 @@ +import django_filters +import netaddr + +from netbox.filtersets import NetBoxModelFilterSet +from netbox_routing.models import PrefixList, PrefixListEntry, RouteMapEntry, RouteMap + + +class PrefixListFilterSet(NetBoxModelFilterSet): + class Meta: + model = PrefixList + fields = () + + +class PrefixListEntryFilterSet(NetBoxModelFilterSet): + + prefix = django_filters.CharFilter( + method='filter_prefix', + label='Prefix', + ) + + class Meta: + model = PrefixListEntry + fields = ('prefix_list', 'prefix', 'sequence', 'type', 'le', 'ge') + + def filter_prefix(self, queryset, name, value): + if not value.strip(): + return queryset + try: + query = str(netaddr.IPNetwork(value).cidr) + return queryset.filter(prefix=query) + except (netaddr.AddrFormatError, ValueError): + return queryset.none() + + +class RouteMapFilterSet(NetBoxModelFilterSet): + + class Meta: + model = RouteMap + fields = () + + +class RouteMapEntryFilterSet(NetBoxModelFilterSet): + + class Meta: + model = RouteMapEntry + fields = ('route_map', 'sequence', 'type') diff --git a/netbox_routing/filtersets/static.py b/netbox_routing/filtersets/static.py new file mode 100644 index 0000000..01e5238 --- /dev/null +++ b/netbox_routing/filtersets/static.py @@ -0,0 +1,40 @@ +import django_filters +import netaddr + +from netbox.filtersets import NetBoxModelFilterSet +from netbox_routing.models import StaticRoute + + +class StaticRouteFilterSet(NetBoxModelFilterSet): + + prefix = django_filters.CharFilter( + method='filter_prefix', + label='Prefix', + ) + + next_hop = django_filters.CharFilter( + method='filter_address', + label='Prefix', + ) + + class Meta: + model = StaticRoute + fields = ('vrf', 'prefix', 'devices', 'metric', 'next_hop') + + def filter_prefix(self, queryset, name, value): + if not value.strip(): + return queryset + try: + query = str(netaddr.IPNetwork(value).cidr) + return queryset.filter(prefix=query) + except (netaddr.AddrFormatError, ValueError): + return queryset.none() + + def filter_address(self, queryset, name, value): + if not value.strip(): + return queryset + try: + query = str(netaddr.IPAddress(value)) + return queryset.filter(prefix=query) + except (netaddr.AddrFormatError, ValueError): + return queryset.none() diff --git a/netbox_routing/forms/__init__.py b/netbox_routing/forms/__init__.py new file mode 100644 index 0000000..cedec14 --- /dev/null +++ b/netbox_routing/forms/__init__.py @@ -0,0 +1,19 @@ +from .filtersets import * +from .objects import PrefixListForm, PrefixListEntryForm, RouteMapForm, RouteMapEntryForm +from .static import StaticRouteForm + +__all__ = ( + # Static Routes + 'StaticRouteForm', + 'StaticRouteFilterSetForm', + + # Objects + 'PrefixListForm', + 'PrefixListEntryForm', + 'RouteMapForm', + 'RouteMapEntryForm', + 'PrefixListFilterSetForm', + 'PrefixListEntryFilterSetForm', + 'RouteMapFilterSetForm', + 'RouteMapEntryFilterSetForm' +) diff --git a/netbox_routing/forms/fields.py b/netbox_routing/forms/fields.py new file mode 100644 index 0000000..e69de29 diff --git a/netbox_routing/forms/filtersets/__init__.py b/netbox_routing/forms/filtersets/__init__.py new file mode 100644 index 0000000..bdacec6 --- /dev/null +++ b/netbox_routing/forms/filtersets/__init__.py @@ -0,0 +1,11 @@ +from .static import StaticRouteFilterSetForm +from .objects import PrefixListFilterSetForm, PrefixListEntryFilterSetForm, RouteMapFilterSetForm,\ + RouteMapEntryFilterSetForm + +__all__ = ( + 'StaticRouteFilterSetForm', + 'PrefixListFilterSetForm', + 'PrefixListEntryFilterSetForm', + 'RouteMapFilterSetForm', + 'RouteMapEntryFilterSetForm' +) diff --git a/netbox_routing/forms/filtersets/objects.py b/netbox_routing/forms/filtersets/objects.py new file mode 100644 index 0000000..b2eee13 --- /dev/null +++ b/netbox_routing/forms/filtersets/objects.py @@ -0,0 +1,18 @@ +from netbox.forms import NetBoxModelFilterSetForm +from netbox_routing.models import PrefixList, PrefixListEntry, RouteMap, RouteMapEntry + + +class PrefixListFilterSetForm(NetBoxModelFilterSetForm): + model = PrefixList + + +class PrefixListEntryFilterSetForm(NetBoxModelFilterSetForm): + model = PrefixListEntry + + +class RouteMapFilterSetForm(NetBoxModelFilterSetForm): + model = RouteMap + + +class RouteMapEntryFilterSetForm(NetBoxModelFilterSetForm): + model = RouteMapEntry \ No newline at end of file diff --git a/netbox_routing/forms/filtersets/static.py b/netbox_routing/forms/filtersets/static.py new file mode 100644 index 0000000..cc3d34b --- /dev/null +++ b/netbox_routing/forms/filtersets/static.py @@ -0,0 +1,6 @@ +from netbox.forms import NetBoxModelFilterSetForm +from netbox_routing.models import StaticRoute + + +class StaticRouteFilterSetForm(NetBoxModelFilterSetForm): + model = StaticRoute \ No newline at end of file diff --git a/netbox_routing/forms/objects.py b/netbox_routing/forms/objects.py new file mode 100644 index 0000000..be0aaed --- /dev/null +++ b/netbox_routing/forms/objects.py @@ -0,0 +1,30 @@ +from netbox.forms import NetBoxModelForm +from netbox_routing.models import PrefixList, PrefixListEntry, RouteMap, RouteMapEntry + + +class PrefixListForm(NetBoxModelForm): + + class Meta: + model = PrefixList + fields = ('name',) + + +class PrefixListEntryForm(NetBoxModelForm): + + class Meta: + model = PrefixListEntry + fields = ('prefix_list', 'sequence', 'type', 'prefix', 'le', 'ge') + + +class RouteMapForm(NetBoxModelForm): + + class Meta: + model = RouteMap + fields = ('name',) + + +class RouteMapEntryForm(NetBoxModelForm): + + class Meta: + model = RouteMapEntry + fields = ('route_map', 'sequence', 'type') diff --git a/netbox_routing/forms/static.py b/netbox_routing/forms/static.py new file mode 100644 index 0000000..9ab5269 --- /dev/null +++ b/netbox_routing/forms/static.py @@ -0,0 +1,31 @@ +from dcim.models import Device +from ipam.models import VRF +from netbox.forms import NetBoxModelForm +from netbox_routing.models import StaticRoute +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField + + +class StaticRouteForm(NetBoxModelForm): + devices = DynamicModelMultipleChoiceField( + queryset=Device.objects.all() + ) + vrf = DynamicModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label='VRF' + ) + + class Meta: + model = StaticRoute + fields = ('devices', 'vrf', 'prefix', 'next_hop', 'name', 'metric', 'permanent') + + def __init__(self, data=None, instance=None, *args, **kwargs): + super().__init__(data=data, instance=instance, *args, **kwargs) + + if self.instance and self.instance.pk is not None: + self.fields['devices'].initial = self.instance.devices.all().values_list('id', flat=True) + + def save(self, *args, **kwargs): + instance = super().save(*args, **kwargs) + instance.devices.set(self.cleaned_data['devices']) + return instance diff --git a/netbox_routing/helpers/__init__.py b/netbox_routing/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netbox_routing/models/__init__.py b/netbox_routing/models/__init__.py index e69de29..2df28c2 100644 --- a/netbox_routing/models/__init__.py +++ b/netbox_routing/models/__init__.py @@ -0,0 +1,10 @@ +from .static import StaticRoute +from .objects import PrefixList, PrefixListEntry, RouteMap, RouteMapEntry + +__all__ = ( + 'StaticRoute', + 'PrefixList', + 'PrefixListEntry', + 'RouteMap', + 'RouteMapEntry' +) diff --git a/netbox_routing/models/bgp.py b/netbox_routing/models/bgp.py new file mode 100644 index 0000000..d944efd --- /dev/null +++ b/netbox_routing/models/bgp.py @@ -0,0 +1,307 @@ +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.db import models + +from netbox.models import NetBoxModel, NestedGroupModel +from netbox_routing import choices +from netbox_routing.constants.bgp import BGPSETTING_ASSIGNMENT_MODELS, BGPAF_ASSIGNMENT_MODELS, \ + BGPPEER_ASSIGNMENT_MODELS, BGPPEERAF_ASSIGNMENT_MODELS +from netbox_routing.fields.ip import IPAddressField + + +class BGPSettings(NetBoxModel): + assigned_object_type = models.ForeignKey( + to=ContentType, + limit_choices_to=BGPSETTING_ASSIGNMENT_MODELS, + on_delete=models.PROTECT, + related_name='+', + blank=True, + null=True + ) + assigned_object_id = models.PositiveBigIntegerField( + blank=True, + null=True + ) + assigned_object = GenericForeignKey( + ct_field='assigned_object_type', + fk_field='assigned_object_id' + ) + router_id = IPAddressField() + auto_summary = models.BooleanField() + bgp_additional_paths_install = models.BooleanField() + bgp_additional_paths_receive = models.BooleanField() + bgp_additional_paths_send = models.BooleanField() + bgp_asnotation_dot = models.BooleanField() + bgp_graceful_restart = models.BooleanField() + + default_information_originate = models.BooleanField(verbose_name='Default Information Originate') + default_metric = models.PositiveBigIntegerField(verbose_name='Default Metric') + distance_ebgp = models.PositiveSmallIntegerField(verbose_name='eBGP Distance') + distance_ibgp = models.PositiveSmallIntegerField(verbose_name='iBGP Distance') + distance_embgp = models.PositiveSmallIntegerField(verbose_name='eBGP Distance (MultiProtocol)') + distance_imbgp = models.PositiveSmallIntegerField(verbose_name='iBGP Distance (MultiProtocol)') + paths_maximum = models.PositiveSmallIntegerField(verbose_name='Maximum Paths') + paths_maximum_secondary = models.PositiveSmallIntegerField(verbose_name='Maximum Secondary Paths') + timers_keepalive = models.PositiveSmallIntegerField(verbose_name='Keepalive Timer') + timers_hold = models.PositiveSmallIntegerField(verbose_name='Hold Timer') + + +class BGPRouter(NetBoxModel): + asn = models.ForeignKey( + to='ipam.ASN', + on_delete=models.PROTECT, + related_name='router', + verbose_name='ASN' + ) + + +class BGPScope(NetBoxModel): + router = models.ForeignKey( + to=BGPRouter, + on_delete=models.PROTECT, + related_name='scopes', + blank=False, + null=False + ) + vrf = models.ForeignKey( + to='ipam.VRF', + on_delete=models.PROTECT, + related_name='scopes', + blank=False, + null=False + ) + + +class BGPAddressFamily(NetBoxModel): + assigned_object_type = models.ForeignKey( + to=ContentType, + limit_choices_to=BGPAF_ASSIGNMENT_MODELS, + on_delete=models.PROTECT, + related_name='+', + blank=True, + null=True + ) + assigned_object_id = models.PositiveBigIntegerField( + blank=True, + null=True + ) + assigned_object = GenericForeignKey( + ct_field='assigned_object_type', + fk_field='assigned_object_id' + ) + + +class BGPTemplateSession(NetBoxModel): + name = models.CharField( + verbose_name='Name', + max_length=255 + ) + router = models.ForeignKey( + to=BGPRouter, + on_delete=models.PROTECT, + related_name='session_templates', + blank=False, + null=False + ) + enabled = models.BooleanField( + blank=True, + null=True + ) + prefixlist_out = models.ForeignKey( + to='netbox_routing.PrefixList', + on_delete=models.PROTECT, + related_name='template_afs_out', + blank=True, + null=True + ) + prefixlist_in = models.ForeignKey( + to='netbox_routing.PrefixList', + on_delete=models.PROTECT, + related_name='template_afs_in', + blank=True, + null=True + ) + routemap_out = models.ForeignKey( + to='netbox_routing.RouteMap', + on_delete=models.PROTECT, + related_name='template_afs_out', + blank=True, + null=True + ) + routemap_in = models.ForeignKey( + to='netbox_routing.RouteMap', + on_delete=models.PROTECT, + related_name='template_afs_in', + blank=True, + null=True + ) + + +class BGPTemplatePolicy(NetBoxModel): + name = models.CharField( + verbose_name='Name', + max_length=255 + ) + router = models.ForeignKey( + to=BGPRouter, + on_delete=models.PROTECT, + related_name='session_templates', + blank=False, + null=False + ) + enabled = models.BooleanField( + blank=True, + null=True + ) + + +class BGPTemplatePeer(NetBoxModel): + name = models.CharField( + verbose_name='Name', + max_length=255 + ) + remote_as = models.ForeignKey( + to='ipam.ASN', + on_delete=models.PROTECT, + related_name='+', + blank=True, + null=True + ) + enabled = models.BooleanField( + blank=True, + null=True + ) + + +class BGPPeerGroup(NetBoxModel): + name = models.CharField( + verbose_name='Name', + max_length=255 + ) + remote_as = models.ForeignKey( + to='ipam.ASN', + on_delete=models.PROTECT, + related_name='+', + blank=True, + null=True + ) + enabled = models.BooleanField( + blank=True, + null=True + ) + + +class BGPPeer(NetBoxModel): + assigned_object_type = models.ForeignKey( + to=ContentType, + limit_choices_to=BGPPEER_ASSIGNMENT_MODELS, + on_delete=models.PROTECT, + related_name='+', + blank=True, + null=True + ) + assigned_object_id = models.PositiveBigIntegerField( + blank=True, + null=True + ) + assigned_object = GenericForeignKey( + ct_field='assigned_object_type', + fk_field='assigned_object_id' + ) + peer = IPAddressField() + peer_group = models.ForeignKey( + to=BGPPeerGroup, + on_delete=models.PROTECT, + related_name='peers', + blank=True, + null=True + ) + peer_template = models.ForeignKey( + to=BGPTemplatePeer, + on_delete=models.PROTECT, + related_name='peers', + blank=True, + null=True + ) + peer_policy = models.ForeignKey( + to=BGPTemplatePolicy, + on_delete=models.PROTECT, + related_name='peers', + blank=True, + null=True + ) + remote_as = models.ForeignKey( + to='ipam.ASN', + on_delete=models.PROTECT, + related_name='+', + blank=True, + null=True + ) + enabled = models.BooleanField( + blank=True, + null=True + ) + + +class BGPPeerAddressFamily(NetBoxModel): + assigned_object_type = models.ForeignKey( + to=ContentType, + limit_choices_to=BGPPEERAF_ASSIGNMENT_MODELS, + on_delete=models.PROTECT, + related_name='+', + blank=True, + null=True + ) + assigned_object_id = models.PositiveBigIntegerField( + blank=True, + null=True + ) + assigned_object = GenericForeignKey( + ct_field='assigned_object_type', + fk_field='assigned_object_id' + ) + + peer_session = models.ForeignKey( + to=BGPTemplateSession, + on_delete=models.PROTECT, + related_name='peer_afs', + blank=True, + null=True + ) + + address_family = models.CharField( + choices=choices.BGPAddressFamilies + ) + enabled = models.BooleanField( + blank=True, + null=True + ) + + prefixlist_out = models.ForeignKey( + to='netbox_routing.PrefixList', + on_delete=models.PROTECT, + related_name='peer_afs_out', + blank=True, + null=True + ) + prefixlist_in = models.ForeignKey( + to='netbox_routing.PrefixList', + on_delete=models.PROTECT, + related_name='peer_afs_in', + blank=True, + null=True + ) + routemap_out = models.ForeignKey( + to='netbox_routing.RouteMap', + on_delete=models.PROTECT, + related_name='peer_afs_out', + blank=True, + null=True + ) + routemap_in = models.ForeignKey( + to='netbox_routing.RouteMap', + on_delete=models.PROTECT, + related_name='peer_afs_in', + blank=True, + null=True + ) diff --git a/netbox_routing/models/objects.py b/netbox_routing/models/objects.py new file mode 100644 index 0000000..ecea428 --- /dev/null +++ b/netbox_routing/models/objects.py @@ -0,0 +1,100 @@ +from django.urls import reverse + +from django.db import models +from django.db.models import F, Q, CheckConstraint +from django.core.exceptions import ValidationError + +from ipam.fields import IPNetworkField +from netbox.models import NetBoxModel +from netbox_routing.choices.objects import PermitDenyChoices + + +__all__ = ( + 'RouteMap', + 'RouteMapEntry', + 'PrefixList', + 'PrefixListEntry' +) + + +class RouteMap(NetBoxModel): + name = models.CharField( + max_length=255 + ) + + def get_absolute_url(self): + return reverse('plugins:netbox_routing:routemap', args=[self.pk]) + +class RouteMapEntry(NetBoxModel): + route_map = models.ForeignKey( + to="netbox_routing.RouteMap", + on_delete=models.PROTECT, + related_name='entries', + verbose_name='Route Map' + ) + type = models.CharField(max_length=6, choices=PermitDenyChoices) + sequence = models.PositiveSmallIntegerField() + + def get_absolute_url(self): + return reverse('plugins:netbox_routing:routemapentry', args=[self.pk]) + + +class PrefixList(NetBoxModel): + name = models.CharField( + max_length=255 + ) + + def get_absolute_url(self): + return reverse('plugins:netbox_routing:prefixlist', args=[self.pk]) + + +class PrefixListEntry(NetBoxModel): + prefix_list = models.ForeignKey( + to="netbox_routing.PrefixList", + on_delete=models.PROTECT, + related_name='entries', + verbose_name='Prefix List' + ) + sequence = models.PositiveSmallIntegerField() + type = models.CharField(max_length=6, choices=PermitDenyChoices) + prefix = IPNetworkField(help_text='IPv4 or IPv6 network with mask') + ge = models.PositiveSmallIntegerField() + le = models.PositiveSmallIntegerField() + + def get_absolute_url(self): + return reverse('plugins:netbox_routing:prefixlistentry', args=[self.pk]) + + def clean(self): + super().clean() + + if self.prefix.version == 6: + if self.le is not None and self.le > 128: + raise ValidationError({ + 'le': 'LE value cannot be longer then 128' + }) + if self.ge is not None and self.ge > 128: + raise ValidationError({ + 'ge': 'GE value cannot be longer then 128' + }) + elif self.prefix.version == 4: + if self.le is not None and self.le > 32: + raise ValidationError({ + 'le': 'LE value cannot be longer then 32' + }) + if self.ge is not None and self.ge > 32: + raise ValidationError({ + 'ge': 'GE value cannot be longer then 32' + }) + + if self.ge and self.le and self.ge < self.le: + raise ValidationError({ + 'ge': 'GE cannot be more then LE', + 'le': 'LE cannot be less then GE' + }) + + if self.ge is not None and self.prefix.prefix.prefixlen >= self.ge: + raise ValidationError('Prefix\'s length cannot be longer then greater or equals value') + + if self.le is not None and self.prefix.prefix.prefixlen >= self.le: + raise ValidationError('Prefix\'s length cannot be longer then greater or equals value') + diff --git a/netbox_routing/models/static.py b/netbox_routing/models/static.py new file mode 100644 index 0000000..31daf87 --- /dev/null +++ b/netbox_routing/models/static.py @@ -0,0 +1,53 @@ +from django.db import models +from django.db.models import CheckConstraint, Q +from django.urls import reverse + +from ipam.fields import IPNetworkField +from netbox.models import NetBoxModel +from netbox_routing.fields.ip import IPAddressField + + +__all__ = ( + 'StaticRoute' +) + + +class StaticRoute(NetBoxModel): + devices = models.ManyToManyField( + to='dcim.Device', + related_name='static_routes' + ) + vrf = models.ForeignKey( + to='ipam.VRF', + on_delete=models.PROTECT, + related_name='staticroutes', + blank=True, + null=True, + verbose_name='VRF' + ) + prefix = IPNetworkField(help_text='IPv4 or IPv6 network with mask') + next_hop = IPAddressField() + name = models.CharField( + max_length=50, + verbose_name='Name', + blank=True, + null=True, + help_text='Optional name for this static route' + ) + metric = models.PositiveSmallIntegerField( + verbose_name='Metric' + ) + permanent = models.BooleanField() + + class Meta: + constraints = [ + CheckConstraint(check=Q(Q(metric__lte=255) & Q(metric__gte=0)), name='metric_gte_lte') + ] + + def __str__(self): + if self.vrf is None: + return f'{self.prefix} NH {self.next_hop}' + return f'{self.prefix} VRF {self.vrf} NH {self.next_hop}' + + def get_absolute_url(self): + return reverse('plugins:netbox_routing:staticroute', args=[self.pk]) diff --git a/netbox_routing/navigation.py b/netbox_routing/navigation.py new file mode 100644 index 0000000..3274ec1 --- /dev/null +++ b/netbox_routing/navigation.py @@ -0,0 +1,29 @@ +from extras.plugins import PluginMenuButton, PluginMenuItem +from utilities.choices import ButtonColorChoices + +menu_items = ( + PluginMenuItem( + link='plugins:netbox_routing:staticroute_list', + link_text='Static Route', + buttons=( + PluginMenuButton('plugins:netbox_routing:staticroute_add', 'Add', 'mdi mdi-plus', ButtonColorChoices.GREEN), + PluginMenuButton('plugins:netbox_routing:staticroute_import', 'Import', 'mdi mdi-upload', ButtonColorChoices.CYAN), + ) + ), + PluginMenuItem( + link='plugins:netbox_routing:prefixlist_list', + link_text='Prefix Lists', + buttons=( + PluginMenuButton('plugins:netbox_routing:prefixlist_add', 'Add', 'mdi mdi-plus', ButtonColorChoices.GREEN), + PluginMenuButton('plugins:netbox_routing:prefixlist_import', 'Import', 'mdi mdi-upload', ButtonColorChoices.CYAN), + ) + ), + PluginMenuItem( + link='plugins:netbox_routing:routemap_list', + link_text='Route Maps', + buttons=( + PluginMenuButton('plugins:netbox_routing:routemap_add', 'Add', 'mdi mdi-plus', ButtonColorChoices.GREEN), + PluginMenuButton('plugins:netbox_routing:routemap_import', 'Import', 'mdi mdi-upload', ButtonColorChoices.CYAN), + ) + ), +) \ No newline at end of file diff --git a/netbox_routing/tables/__init__.py b/netbox_routing/tables/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netbox_routing/tables/objects.py b/netbox_routing/tables/objects.py new file mode 100644 index 0000000..2a2f019 --- /dev/null +++ b/netbox_routing/tables/objects.py @@ -0,0 +1,30 @@ +from netbox.tables import NetBoxTable +from netbox_routing.models import PrefixList, PrefixListEntry, RouteMap, RouteMapEntry + + +class PrefixListTable(NetBoxTable): + class Meta(NetBoxTable.Meta): + model = PrefixList + fields = ('pk', 'id', 'name') + default_columns = ('pk', 'id', 'name') + + +class PrefixListEntryTable(NetBoxTable): + class Meta(NetBoxTable.Meta): + model = PrefixListEntry + fields = ('pk', 'id', 'prefix_list', 'sequence', 'type', 'prefix', 'le', 'ge') + default_columns = ('pk', 'id', 'prefix_list', 'sequence', 'type', 'prefix', 'le', 'ge') + + +class RouteMapTable(NetBoxTable): + class Meta(NetBoxTable.Meta): + model = RouteMap + fields = ('pk', 'id', 'name') + default_columns = ('pk', 'id', 'name') + + +class RouteMapEntryTable(NetBoxTable): + class Meta(NetBoxTable.Meta): + model = RouteMapEntry + fields = ('pk', 'id', 'route_map', 'sequence', 'type') + default_columns = ('pk', 'id', 'route_map', 'sequence', 'type') diff --git a/netbox_routing/tables/static.py b/netbox_routing/tables/static.py new file mode 100644 index 0000000..fb7a439 --- /dev/null +++ b/netbox_routing/tables/static.py @@ -0,0 +1,9 @@ +from netbox.tables import NetBoxTable +from netbox_routing.models import StaticRoute + + +class StaticRouteTable(NetBoxTable): + class Meta(NetBoxTable.Meta): + model = StaticRoute + fields = ('pk', 'id', 'vrf', 'prefix', 'next_hop', 'name') + default_columns = ('pk', 'id', 'vrf', 'prefix', 'next_hop', 'name') diff --git a/netbox_routing/templates/netbox_routing/staticroute.html b/netbox_routing/templates/netbox_routing/staticroute.html new file mode 100644 index 0000000..45ed6ba --- /dev/null +++ b/netbox_routing/templates/netbox_routing/staticroute.html @@ -0,0 +1,70 @@ +{% extends 'generic/object.html' %} +{% load humanize %} +{% load helpers %} +{% load plugins %} + +{% block content %} +
VRF | ++ {% if object.vrf %} + {{ object.vrf }} + {% else %} + Global + {% endif %} + | +
---|---|
Prefix | ++ {{ object.prefix }} + | +
Next Hop | ++ {{ object.next_hop }} + | +
Name | ++ {{ object.name|placeholder }} + | +
Metric | ++ {{ object.metric }} + | +
Permanent | ++ {{ object.permanent }} + | +