Introduce the RouteTarget model

This commit is contained in:
Jeremy Stretch 2020-09-24 11:25:52 -04:00
parent 9b16d6df2e
commit dfb5a06d9d
14 changed files with 397 additions and 12 deletions

View File

@ -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
#

View File

@ -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
#

View File

@ -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)

View File

@ -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
#

View File

@ -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

View File

@ -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:

View File

@ -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
#

View File

@ -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'],
},
),
]

View File

@ -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

View File

@ -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
#

View File

@ -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'),

View File

@ -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
#

View File

@ -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 %}>

View File

@ -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 %}