mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Introduce the RouteTarget model
This commit is contained in:
parent
9b16d6df2e
commit
dfb5a06d9d
@ -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
|
||||
#
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
netbox/ipam/migrations/0041_routetarget.py
Normal file
34
netbox/ipam/migrations/0041_routetarget.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
@ -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
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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'),
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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
netbox/templates/ipam/routetarget.html
Normal file
98
netbox/templates/ipam/routetarget.html
Normal 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 %}
|
Loading…
Reference in New Issue
Block a user