Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/core-functionality/ipam.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
---

{!docs/models/ipam/vrf.md!}
{!docs/models/ipam/routetarget.md!}
5 changes: 5 additions & 0 deletions docs/models/ipam/routetarget.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Route Targets

A route target is a particular type of [extended BGP community](https://tools.ietf.org/html/rfc4360#section-4) used to control the redistribution of routes among VRF tables in a network. Route targets can be assigned to individual VRFs in NetBox as import or export targets (or both) to model this exchange in an L3VPN. Each route target must be given a unique name, which should be in a format prescribed by [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), similar to a VR route distinguisher.

Each route target can optionally be assigned to a tenant, and may have tags assigned to it.
2 changes: 2 additions & 0 deletions docs/models/ipam/vrf.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ By default, NetBox will allow duplicate prefixes to be assigned to a VRF. This b

!!! note
Enforcement of unique IP space can be toggled for global table (non-VRF prefixes) using the `ENFORCE_GLOBAL_UNIQUE` configuration setting.

Each VRF may have one or more import and/or export route targets applied to it. Route targets are used to control the exchange of routes (prefixes) among VRFs in L3VPNs.
4 changes: 4 additions & 0 deletions docs/release-notes/version-2.10.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

### New Features

#### Route Targets ([#259](https://github.com/netbox-community/netbox/issues/259))

This release introduces support for model L3VPN route targets, which can be used to control the redistribution of routing information among VRFs. Each VRF may be assigned one or more route targets in the import or export direction (or both). Like VRFs, route targets may be assigned to tenants and may have tags applied to them.

#### REST API Bulk Deletion ([#3436](https://github.com/netbox-community/netbox/issues/3436))

The REST API now supports the bulk deletion of objects of the same type in a single request. Send a `DELETE` HTTP request to the list to the model's list endpoint (e.g. `/api/dcim/sites/`) with a list of JSON objects specifying the numeric ID of each object to be deleted. For example, to delete sites with IDs 10, 11, and 12, issue the following request:
Expand Down
13 changes: 13 additions & 0 deletions netbox/ipam/api/nested_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
'NestedPrefixSerializer',
'NestedRIRSerializer',
'NestedRoleSerializer',
'NestedRouteTargetSerializer',
'NestedServiceSerializer',
'NestedVLANGroupSerializer',
'NestedVLANSerializer',
Expand All @@ -29,6 +30,18 @@ class Meta:
fields = ['id', 'url', 'name', 'rd', 'display_name', 'prefix_count']


#
# Route targets
#

class NestedRouteTargetSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')

class Meta:
model = models.RouteTarget
fields = ['id', 'url', 'name']


#
# RIRs/aggregates
#
Expand Down
28 changes: 21 additions & 7 deletions netbox/ipam/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,17 @@
from django.contrib.contenttypes.models import ContentType
from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers
from rest_framework.reverse import reverse
from rest_framework.validators import UniqueTogetherValidator

from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
from dcim.models import Interface
from extras.api.customfields import CustomFieldModelSerializer
from extras.api.serializers import TaggedObjectSerializer
from ipam.choices import *
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
from tenancy.api.nested_serializers import NestedTenantSerializer
from utilities.api import (
ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer,
get_serializer_for_model,
ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer, get_serializer_for_model,
)
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
from .nested_serializers import *
Expand All @@ -29,14 +26,31 @@
class VRFSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
tenant = NestedTenantSerializer(required=False, allow_null=True)
import_targets = NestedRouteTargetSerializer(required=False, allow_null=True, many=True)
export_targets = NestedRouteTargetSerializer(required=False, allow_null=True, many=True)
ipaddress_count = serializers.IntegerField(read_only=True)
prefix_count = serializers.IntegerField(read_only=True)

class Meta:
model = VRF
fields = [
'id', 'url', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'tags', 'display_name',
'custom_fields', 'created', 'last_updated', 'ipaddress_count', 'prefix_count',
'id', 'url', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets',
'tags', 'display_name', 'custom_fields', 'created', 'last_updated', 'ipaddress_count', 'prefix_count',
]


#
# Route targets
#

class RouteTargetSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
tenant = NestedTenantSerializer(required=False, allow_null=True)

class Meta:
model = RouteTarget
fields = [
'id', 'url', 'name', 'tenant', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
]


Expand Down
3 changes: 3 additions & 0 deletions netbox/ipam/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
# VRFs
router.register('vrfs', views.VRFViewSet)

# Route targets
router.register('route-targets', views.RouteTargetViewSet)

# RIRs
router.register('rirs', views.RIRViewSet)

Expand Down
16 changes: 14 additions & 2 deletions netbox/ipam/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from extras.api.views import CustomFieldModelViewSet
from ipam import filters
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
from utilities.api import ModelViewSet
from utilities.constants import ADVISORY_LOCK_KEYS
from utilities.utils import get_subquery
Expand All @@ -30,14 +30,26 @@ def get_view_name(self):
#

class VRFViewSet(CustomFieldModelViewSet):
queryset = VRF.objects.prefetch_related('tenant').prefetch_related('tags').annotate(
queryset = VRF.objects.prefetch_related('tenant').prefetch_related(
'import_targets', 'export_targets', 'tags'
).annotate(
ipaddress_count=get_subquery(IPAddress, 'vrf'),
prefix_count=get_subquery(Prefix, 'vrf')
).order_by(*VRF._meta.ordering)
serializer_class = serializers.VRFSerializer
filterset_class = filters.VRFFilterSet


#
# Route targets
#

class RouteTargetViewSet(CustomFieldModelViewSet):
queryset = RouteTarget.objects.prefetch_related('tenant').prefetch_related('tags')
serializer_class = serializers.RouteTargetSerializer
filterset_class = filters.RouteTargetFilterSet


#
# RIRs
#
Expand Down
1 change: 1 addition & 0 deletions netbox/ipam/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# * Type 1 (32-bit IPv4 address : 16-bit integer)
# * Type 2 (32-bit AS number : 16-bit integer)
# 21 characters are sufficient to convey the longest possible string value (255.255.255.255:65535)
# Also used for RouteTargets
VRF_RD_MAX_LENGTH = 21


Expand Down
67 changes: 66 additions & 1 deletion netbox/ipam/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
)
from virtualization.models import VirtualMachine, VMInterface
from .choices import *
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF


__all__ = (
Expand All @@ -22,6 +22,7 @@
'PrefixFilterSet',
'RIRFilterSet',
'RoleFilterSet',
'RouteTargetFilterSet',
'ServiceFilterSet',
'VLANFilterSet',
'VLANGroupFilterSet',
Expand All @@ -34,6 +35,28 @@ class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Create
method='search',
label='Search',
)
import_target_id = django_filters.ModelMultipleChoiceFilter(
field_name='import_targets',
queryset=RouteTarget.objects.all(),
label='Import target',
)
import_target = django_filters.ModelMultipleChoiceFilter(
field_name='import_targets__name',
queryset=RouteTarget.objects.all(),
to_field_name='name',
label='Import target (name)',
)
export_target_id = django_filters.ModelMultipleChoiceFilter(
field_name='export_targets',
queryset=RouteTarget.objects.all(),
label='Export target',
)
export_target = django_filters.ModelMultipleChoiceFilter(
field_name='export_targets__name',
queryset=RouteTarget.objects.all(),
to_field_name='name',
label='Export target (name)',
)
tag = TagFilter()

def search(self, queryset, name, value):
Expand All @@ -50,6 +73,48 @@ class Meta:
fields = ['id', 'name', 'rd', 'enforce_unique']


class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
importing_vrf_id = django_filters.ModelMultipleChoiceFilter(
field_name='importing_vrfs',
queryset=VRF.objects.all(),
label='Importing VRF',
)
importing_vrf = django_filters.ModelMultipleChoiceFilter(
field_name='importing_vrfs__rd',
queryset=VRF.objects.all(),
to_field_name='rd',
label='Import VRF (RD)',
)
exporting_vrf_id = django_filters.ModelMultipleChoiceFilter(
field_name='exporting_vrfs',
queryset=VRF.objects.all(),
label='Exporting VRF',
)
exporting_vrf = django_filters.ModelMultipleChoiceFilter(
field_name='exporting_vrfs__rd',
queryset=VRF.objects.all(),
to_field_name='rd',
label='Export VRF (RD)',
)
tag = TagFilter()

def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
)

class Meta:
model = RouteTarget
fields = ['id', 'name']


class RIRFilterSet(BaseFilterSet, NameSlugSearchFilterSet):

class Meta:
Expand Down
96 changes: 92 additions & 4 deletions netbox/ipam/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from django import forms
from django.core.validators import MaxValueValidator, MinValueValidator

from dcim.models import Device, Interface, Rack, Region, Site
from extras.forms import (
Expand All @@ -16,7 +15,7 @@
from virtualization.models import Cluster, VirtualMachine, VMInterface
from .choices import *
from .constants import *
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF

PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([
(i, i) for i in range(PREFIX_LENGTH_MIN, PREFIX_LENGTH_MAX + 1)
Expand All @@ -32,6 +31,14 @@
#

class VRFForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
import_targets = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
required=False
)
export_targets = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
required=False
)
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
Expand All @@ -40,7 +47,8 @@ class VRFForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class Meta:
model = VRF
fields = [
'name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant', 'tags',
'name', 'rd', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'tenant_group', 'tenant',
'tags',
]
labels = {
'rd': "RD",
Expand Down Expand Up @@ -90,11 +98,91 @@ class Meta:

class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = VRF
field_order = ['q', 'tenant_group', 'tenant']
field_order = ['q', 'import_target', 'export_target', 'tenant_group', 'tenant']
q = forms.CharField(
required=False,
label='Search'
)
import_target = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
to_field_name='name',
required=False
)
export_target = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
to_field_name='name',
required=False
)
tag = TagFilterField(model)


#
# Route targets
#

class RouteTargetForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)

class Meta:
model = RouteTarget
fields = [
'name', 'description', 'tenant_group', 'tenant', 'tags',
]


class RouteTargetCSVForm(CustomFieldModelCSVForm):
tenant = CSVModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text='Assigned tenant'
)

class Meta:
model = RouteTarget
fields = RouteTarget.csv_headers


class RouteTargetBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
widget=forms.MultipleHiddenInput()
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
required=False
)
description = forms.CharField(
max_length=200,
required=False
)

class Meta:
nullable_fields = [
'tenant', 'description',
]


class RouteTargetFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = RouteTarget
field_order = ['q', 'name', 'tenant_group', 'tenant', 'importing_vrfs', 'exporting_vrfs']
q = forms.CharField(
required=False,
label='Search'
)
importing_vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(),
required=False,
label='Imported by VRF'
)
exporting_vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(),
required=False,
label='Exported by VRF'
)
tag = TagFilterField(model)


Expand Down
Loading