Przeglądaj źródła

Introduce the RouteTarget model

Jeremy Stretch 5 lat temu
rodzic
commit
dfb5a06d9d

+ 13 - 0
netbox/ipam/api/nested_serializers.py

@@ -9,6 +9,7 @@ __all__ = [
     'NestedPrefixSerializer',
     'NestedRIRSerializer',
     'NestedRoleSerializer',
+    'NestedRouteTargetSerializer',
     'NestedServiceSerializer',
     'NestedVLANGroupSerializer',
     'NestedVLANSerializer',
@@ -29,6 +30,18 @@ class NestedVRFSerializer(WritableNestedSerializer):
         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
 #

+ 17 - 5
netbox/ipam/api/serializers.py

@@ -3,20 +3,17 @@ from collections import OrderedDict
 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 *
@@ -40,6 +37,21 @@ class VRFSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
         ]
 
 
+#
+# 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',
+        ]
+
+
 #
 # RIRs/aggregates
 #

+ 3 - 0
netbox/ipam/api/urls.py

@@ -8,6 +8,9 @@ router.APIRootView = views.IPAMRootView
 # VRFs
 router.register('vrfs', views.VRFViewSet)
 
+# Route targets
+router.register('route-targets', views.RouteTargetViewSet)
+
 # RIRs
 router.register('rirs', views.RIRViewSet)
 

+ 11 - 1
netbox/ipam/api/views.py

@@ -10,7 +10,7 @@ from rest_framework.routers import APIRootView
 
 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
@@ -38,6 +38,16 @@ class VRFViewSet(CustomFieldModelViewSet):
     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
 #

+ 1 - 0
netbox/ipam/constants.py

@@ -16,6 +16,7 @@ BGP_ASN_MAX = 2**32 - 1
 #   * 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
 
 

+ 22 - 1
netbox/ipam/filters.py

@@ -13,7 +13,7 @@ from utilities.filters import (
 )
 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__ = (
@@ -22,6 +22,7 @@ __all__ = (
     'PrefixFilterSet',
     'RIRFilterSet',
     'RoleFilterSet',
+    'RouteTargetFilterSet',
     'ServiceFilterSet',
     'VLANFilterSet',
     'VLANGroupFilterSet',
@@ -50,6 +51,26 @@ class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Create
         fields = ['id', 'name', 'rd', 'enforce_unique']
 
 
+class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+    q = django_filters.CharFilter(
+        method='search',
+        label='Search',
+    )
+    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:

+ 61 - 2
netbox/ipam/forms.py

@@ -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 (
@@ -16,7 +15,7 @@ from utilities.forms import (
 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)
@@ -98,6 +97,66 @@ class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
     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']
+    q = forms.CharField(
+        required=False,
+        label='Search'
+    )
+    tag = TagFilterField(model)
+
+
 #
 # RIRs
 #

+ 34 - 0
netbox/ipam/migrations/0041_routetarget.py

@@ -0,0 +1,34 @@
+# Generated by Django 3.1 on 2020-09-24 15:19
+
+import django.core.serializers.json
+from django.db import migrations, models
+import django.db.models.deletion
+import taggit.managers
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('tenancy', '0010_custom_field_data'),
+        ('extras', '0052_delete_customfieldchoice_customfieldvalue'),
+        ('ipam', '0040_service_drop_port'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='RouteTarget',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
+                ('created', models.DateField(auto_now_add=True, null=True)),
+                ('last_updated', models.DateTimeField(auto_now=True, null=True)),
+                ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+                ('name', models.CharField(max_length=21, unique=True)),
+                ('description', models.CharField(blank=True, max_length=200)),
+                ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
+                ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='route_targets', to='tenancy.tenant')),
+            ],
+            options={
+                'ordering': ['name'],
+            },
+        ),
+    ]

+ 44 - 0
netbox/ipam/models.py

@@ -107,6 +107,50 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
         return self.name
 
 
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+class RouteTarget(ChangeLoggedModel, CustomFieldModel):
+    """
+    A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364.
+    """
+    name = models.CharField(
+        max_length=VRF_RD_MAX_LENGTH,  # Same format options as VRF RD (RFC 4360 section 4)
+        unique=True,
+        help_text='Route target value (formatted in accordance with RFC 4360)'
+    )
+    description = models.CharField(
+        max_length=200,
+        blank=True
+    )
+    tenant = models.ForeignKey(
+        to='tenancy.Tenant',
+        on_delete=models.PROTECT,
+        related_name='route_targets',
+        blank=True,
+        null=True
+    )
+    tags = TaggableManager(through=TaggedItem)
+
+    objects = RestrictedQuerySet.as_manager()
+
+    csv_headers = ['name', 'description', 'tenant']
+
+    class Meta:
+        ordering = ['name']
+
+    def __str__(self):
+        return self.name
+
+    def get_absolute_url(self):
+        return reverse('ipam:routetarget', args=[self.pk])
+
+    def to_csv(self):
+        return (
+            self.name,
+            self.description,
+            self.tenant.name if self.tenant else None,
+        )
+
+
 class RIR(ChangeLoggedModel):
     """
     A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address

+ 21 - 1
netbox/ipam/tables.py

@@ -5,7 +5,7 @@ from dcim.models import Interface
 from tenancy.tables import COL_TENANT
 from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, TagColumn, ToggleColumn
 from virtualization.models import VMInterface
-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
 
 RIR_UTILIZATION = """
 <div class="progress">
@@ -176,6 +176,26 @@ class VRFTable(BaseTable):
         default_columns = ('pk', 'name', 'rd', 'tenant', 'description')
 
 
+#
+# Route targets
+#
+
+class RouteTargetTable(BaseTable):
+    pk = ToggleColumn()
+    name = tables.LinkColumn()
+    tenant = tables.TemplateColumn(
+        template_code=COL_TENANT
+    )
+    tags = TagColumn(
+        url_name='ipam:vrf_list'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = RouteTarget
+        fields = ('pk', 'name', 'tenant', 'description', 'tags')
+        default_columns = ('pk', 'name', 'tenant', 'description')
+
+
 #
 # RIRs
 #

+ 12 - 1
netbox/ipam/urls.py

@@ -2,7 +2,7 @@ from django.urls import path
 
 from extras.views import ObjectChangeLogView
 from . import views
-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
 
 app_name = 'ipam'
 urlpatterns = [
@@ -18,6 +18,17 @@ urlpatterns = [
     path('vrfs/<int:pk>/delete/', views.VRFDeleteView.as_view(), name='vrf_delete'),
     path('vrfs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}),
 
+    # Route targets
+    path('route-targets/', views.RouteTargetListView.as_view(), name='routetarget_list'),
+    path('route-targets/add/', views.RouteTargetEditView.as_view(), name='routetarget_add'),
+    path('route-targets/import/', views.RouteTargetBulkImportView.as_view(), name='routetarget_import'),
+    path('route-targets/edit/', views.RouteTargetBulkEditView.as_view(), name='routetarget_bulk_edit'),
+    path('route-targets/delete/', views.RouteTargetBulkDeleteView.as_view(), name='routetarget_bulk_delete'),
+    path('route-targets/<int:pk>/', views.RouteTargetView.as_view(), name='routetarget'),
+    path('route-targets/<int:pk>/edit/', views.RouteTargetEditView.as_view(), name='routetarget_edit'),
+    path('route-targets/<int:pk>/delete/', views.RouteTargetDeleteView.as_view(), name='routetarget_delete'),
+    path('route-targets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='routetarget_changelog', kwargs={'model': RouteTarget}),
+
     # RIRs
     path('rirs/', views.RIRListView.as_view(), name='rir_list'),
     path('rirs/add/', views.RIREditView.as_view(), name='rir_add'),

+ 51 - 1
netbox/ipam/views.py

@@ -16,7 +16,7 @@ from virtualization.models import VirtualMachine, VMInterface
 from . import filters, forms, tables
 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
 from .utils import add_available_ipaddresses, add_available_prefixes, add_available_vlans
 
 
@@ -74,6 +74,56 @@ class VRFBulkDeleteView(BulkDeleteView):
     table = tables.VRFTable
 
 
+#
+# Route targets
+#
+
+class RouteTargetListView(ObjectListView):
+    queryset = RouteTarget.objects.prefetch_related('tenant')
+    filterset = filters.RouteTargetFilterSet
+    filterset_form = forms.RouteTargetFilterForm
+    table = tables.RouteTargetTable
+
+
+class RouteTargetView(ObjectView):
+    queryset = RouteTarget.objects.all()
+
+    def get(self, request, pk):
+        routetarget = get_object_or_404(self.queryset, pk=pk)
+
+        return render(request, 'ipam/routetarget.html', {
+            'routetarget': routetarget,
+        })
+
+
+class RouteTargetEditView(ObjectEditView):
+    queryset = RouteTarget.objects.all()
+    model_form = forms.RouteTargetForm
+
+
+class RouteTargetDeleteView(ObjectDeleteView):
+    queryset = RouteTarget.objects.all()
+
+
+class RouteTargetBulkImportView(BulkImportView):
+    queryset = RouteTarget.objects.all()
+    model_form = forms.RouteTargetCSVForm
+    table = tables.RouteTargetTable
+
+
+class RouteTargetBulkEditView(BulkEditView):
+    queryset = RouteTarget.objects.prefetch_related('tenant')
+    filterset = filters.RouteTargetFilterSet
+    table = tables.RouteTargetTable
+    form = forms.RouteTargetBulkEditForm
+
+
+class RouteTargetBulkDeleteView(BulkDeleteView):
+    queryset = RouteTarget.objects.prefetch_related('tenant')
+    filterset = filters.RouteTargetFilterSet
+    table = tables.RouteTargetTable
+
+
 #
 # RIRs
 #

+ 9 - 0
netbox/templates/inc/nav_menu.html

@@ -331,6 +331,15 @@
                             {% endif %}
                             <a href="{% url 'ipam:vrf_list' %}">VRFs</a>
                         </li>
+                        <li{% if not perms.ipam.view_routetarget %} class="disabled"{% endif %}>
+                            {% if perms.ipam.add_routetarget %}
+                                <div class="buttons pull-right">
+                                    <a href="{% url 'ipam:routetarget_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
+                                    <a href="{% url 'ipam:routetarget_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
+                                </div>
+                            {% endif %}
+                            <a href="{% url 'ipam:routetarget_list' %}">Route Targets</a>
+                        </li>
                         <li class="divider"></li>
                         <li class="dropdown-header">VLANs</li>
                         <li{% if not perms.ipam.view_vlan %} class="disabled"{% endif %}>

+ 98 - 0
netbox/templates/ipam/routetarget.html

@@ -0,0 +1,98 @@
+{% extends 'base.html' %}
+{% load buttons %}
+{% load custom_links %}
+{% load helpers %}
+{% load plugins %}
+
+{% block header %}
+    <div class="row noprint">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                <li><a href="{% url 'ipam:routetarget_list' %}">Route Targets</a></li>
+                <li>{{ routetarget }}</li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'ipam:routetarget_list' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" placeholder="Search roue targets" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="fa fa-search" aria-hidden="true"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
+    </div>
+    <div class="pull-right noprint">
+        {% plugin_buttons routetarget %}
+        {% if perms.ipam.add_routetarget %}
+            {% clone_button routetarget %}
+        {% endif %}
+        {% if perms.ipam.change_routetarget %}
+            {% edit_button routetarget %}
+        {% endif %}
+        {% if perms.ipam.delete_routetarget %}
+            {% delete_button routetarget %}
+        {% endif %}
+    </div>
+    <h1>{% block title %}Route target {{ routetarget }}{% endblock %}</h1>
+    {% include 'inc/created_updated.html' with obj=routetarget %}
+    <div class="pull-right noprint">
+        {% custom_links routetarget %}
+    </div>
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ routetarget.get_absolute_url }}">Route Target</a>
+        </li>
+        {% if perms.extras.view_objectchange %}
+            <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+                <a href="{% url 'ipam:routetarget_changelog' pk=routetarget.pk %}">Change Log</a>
+            </li>
+        {% endif %}
+    </ul>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-6">
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                <strong>Route Target</strong>
+            </div>
+            <table class="table table-hover panel-body attr-table">
+                <tr>
+                    <td>Name</td>
+                    <td>{{ routetarget.name }}</td>
+                </tr>
+                <tr>
+                    <td>Tenant</td>
+                    <td>
+                        {% if routetarget.tenant %}
+                            <a href="{{ routetarget.tenant.get_absolute_url }}">{{ routetarget.tenant }}</a>
+                        {% else %}
+                            <span class="text-muted">None</span>
+                        {% endif %}
+                    </td>
+                </tr>
+                <tr>
+                    <td>Description</td>
+                    <td>{{ vrf.description|placeholder }}</td>
+                </tr>
+		    </table>
+        </div>
+        {% include 'extras/inc/tags_panel.html' with tags=routetarget.tags.all url='ipam:routetarget_list' %}
+        {% plugin_left_page routetarget %}
+	</div>
+	<div class="col-md-6">
+        {% include 'inc/custom_fields_panel.html' with obj=routetarget %}
+        {% plugin_right_page routetarget %}
+    </div>
+</div>
+<div class="row">
+    <div class="col-md-12">
+        {% plugin_full_width_page routetarget %}
+    </div>
+</div>
+{% endblock %}