This commit is contained in:
Jeremy Stretch 2023-11-08 12:23:55 -05:00
parent 60fc28e37d
commit 4880111622
19 changed files with 409 additions and 28 deletions

View File

@ -9,7 +9,7 @@ from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'wireless') APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
BANNER_TEXT = """### NetBox interactive shell ({node}) BANNER_TEXT = """### NetBox interactive shell ({node})
### Python {python} | Django {django} | NetBox {netbox} ### Python {python} | Django {django} | NetBox {netbox}

View File

@ -39,6 +39,7 @@ class APIRootView(APIView):
'tenancy': reverse('tenancy-api:api-root', request=request, format=format), 'tenancy': reverse('tenancy-api:api-root', request=request, format=format),
'users': reverse('users-api:api-root', request=request, format=format), 'users': reverse('users-api:api-root', request=request, format=format),
'virtualization': reverse('virtualization-api:api-root', request=request, format=format), 'virtualization': reverse('virtualization-api:api-root', request=request, format=format),
'vpn': reverse('vpn-api:api-root', request=request, format=format),
'wireless': reverse('wireless-api:api-root', request=request, format=format), 'wireless': reverse('wireless-api:api-root', request=request, format=format),
}) })

View File

@ -0,0 +1,92 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">{% trans "IPSec Profile" %}</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Protocol" %}</th>
<td>{{ object.get_protocol_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "IKE Version" %}</th>
<td>{{ object.get_ike_version_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">{% trans "Phase 1 Parameters" %}</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Encryption" %}</th>
<td>{{ object.get_phase1_encryption_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Authentication" %}</th>
<td>{{ object.get_phase1_authentication_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "DH Group" %}</th>
<td>{{ object.get_phase1_group_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "SA Lifetime" %}</th>
<td>{{ object.phase1_sa_lifetime|placeholder }}</td>
</tr>
</table>
</div>
</div>
<div class="card">
<h5 class="card-header">{% trans "Phase 2 Parameters" %}</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Encryption" %}</th>
<td>{{ object.get_phase2_encryption_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Authentication" %}</th>
<td>{{ object.get_phase2_authentication_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "DH Group" %}</th>
<td>{{ object.get_phase2_group_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "SA Lifetime" %}</th>
<td>{{ object.phase2_sa_lifetime|placeholder }}</td>
</tr>
</table>
</div>
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,81 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">{% trans "Tunnel" %}</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr>
<tr>
<th scope="row">{% trans "Encapsulation" %}</th>
<td>{{ object.get_encapsulation_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "IPSec profile" %}</th>
<td>{{ object.ipsec_profile|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Tenant" %}</th>
<td>
{% if object.tenant.group %}
{{ object.tenant.group|linkify }} /
{% endif %}
{{ object.tenant|linkify|placeholder }}
</td>
</tr>
<tr>
<th scope="row">{% trans "Pre-shared key" %}</th>
<td>{{ object.preshared_key|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Tunnel ID" %}</th>
<td>{{ object.tunnel_id|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">{% trans "Terminations" %}</h5>
<div class="card-body htmx-container table-responsive"
hx-get="{% url 'vpn:tunneltermination_list' %}?tunnel_id={{ object.pk }}"
hx-trigger="load"
></div>
{% if perms.vpn.add_tunneltermination %}
<div class="card-footer text-end noprint">
<a href="{% url 'vpn:tunneltermination_add' %}?tunnel={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Termination" %}
</a>
</div>
{% endif %}
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -69,14 +69,14 @@ class TunnelTerminationSerializer(NetBoxModelSerializer):
model = TunnelTermination model = TunnelTermination
fields = ( fields = (
'id', 'url', 'display', 'tunnel', 'role', 'interface_type', 'interface_id', 'interface', 'outside_ip', 'id', 'url', 'display', 'tunnel', 'role', 'interface_type', 'interface_id', 'interface', 'outside_ip',
'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'tags', 'custom_fields', 'created', 'last_updated',
) )
@extend_schema_field(serializers.JSONField(allow_null=True)) @extend_schema_field(serializers.JSONField(allow_null=True))
def get_interface(self, obj): def get_interface(self, obj):
serializer = get_serializer_for_model(obj.interface, prefix=NESTED_SERIALIZER_PREFIX) serializer = get_serializer_for_model(obj.interface, prefix=NESTED_SERIALIZER_PREFIX)
context = {'request': self.context['request']} context = {'request': self.context['request']}
return serializer(obj.assigned_object, context=context).data return serializer(obj.interface, context=context).data
class IPSecProfileSerializer(NetBoxModelSerializer): class IPSecProfileSerializer(NetBoxModelSerializer):

View File

@ -35,7 +35,7 @@ class TunnelViewSet(NetBoxModelViewSet):
class TunnelTerminationViewSet(NetBoxModelViewSet): class TunnelTerminationViewSet(NetBoxModelViewSet):
queryset = Tunnel.objects.prefetch_related('tunnel') queryset = TunnelTermination.objects.prefetch_related('tunnel')
serializer_class = serializers.TunnelTerminationSerializer serializer_class = serializers.TunnelTerminationSerializer
filterset_class = filtersets.TunnelTerminationFilterSet filterset_class = filtersets.TunnelTerminationFilterSet

View File

@ -24,12 +24,14 @@ class TunnelStatusChoices(ChoiceSet):
class TunnelEncapsulationChoices(ChoiceSet): class TunnelEncapsulationChoices(ChoiceSet):
ENCAP_GRE = 'gre' ENCAP_GRE = 'gre'
ENCAP_IP_IP = 'ip-ip' ENCAP_IP_IP = 'ip-ip'
ENCAP_IPSEC = 'ipsec' ENCAP_IPSEC_TRANSPORT = 'ipsec-transport'
ENCAP_IPSEC_TUNNEL = 'ipsec-tunnel'
CHOICES = [ CHOICES = [
(ENCAP_IPSEC, _('IPsec')), (ENCAP_IPSEC_TRANSPORT, _('IPsec - Transport')),
(ENCAP_IP_IP, _('Active')), (ENCAP_IPSEC_TUNNEL, _('IPsec - Tunnel')),
(ENCAP_GRE, _('Disabled')), (ENCAP_IP_IP, _('IP-in-IP')),
(ENCAP_GRE, _('GRE')),
] ]
@ -39,9 +41,9 @@ class TunnelTerminationRoleChoices(ChoiceSet):
ROLE_SPOKE = 'spoke' ROLE_SPOKE = 'spoke'
CHOICES = [ CHOICES = [
(ROLE_PEER, _('Peer')), (ROLE_PEER, _('Peer'), 'green'),
(ROLE_HUB, _('Hub')), (ROLE_HUB, _('Hub'), 'blue'),
(ROLE_SPOKE, _('Spoke')), (ROLE_SPOKE, _('Spoke'), 'orange'),
] ]

View File

@ -2,6 +2,7 @@ from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from netbox.forms import NetBoxModelFilterSetForm from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm
from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
from vpn.choices import * from vpn.choices import *
from vpn.models import * from vpn.models import *
@ -13,13 +14,13 @@ __all__ = (
) )
class TunnelFilterForm(NetBoxModelFilterSetForm): class TunnelFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Tunnel model = Tunnel
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
(_('Tunnel'), ('status', 'encapsulation', 'tunnel_id')), (_('Tunnel'), ('status', 'encapsulation', 'tunnel_id')),
(_('Security'), ('ipsec_profile_id', 'preshared_key')), (_('Security'), ('ipsec_profile_id', 'preshared_key')),
(_('Tenancy'), ('tenant',)), (_('Tenancy'), ('tenant_group_id', 'tenant_id')),
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'), label=_('Status'),
@ -36,6 +37,14 @@ class TunnelFilterForm(NetBoxModelFilterSetForm):
required=False, required=False,
label=_('IPSec profile') label=_('IPSec profile')
) )
preshared_key = forms.CharField(
required=False,
label=_('Pre-shared key')
)
tunnel_id = forms.IntegerField(
required=False,
label=_('Tunnel ID')
)
tag = TagFilterField(model) tag = TagFilterField(model)

View File

@ -2,6 +2,7 @@ from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from dcim.models import Interface from dcim.models import Interface
from ipam.models import IPAddress
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField from utilities.forms.fields import CommentField, DynamicModelChoiceField
@ -17,7 +18,8 @@ __all__ = (
class TunnelForm(TenancyForm, NetBoxModelForm): class TunnelForm(TenancyForm, NetBoxModelForm):
ipsec_profile = DynamicModelChoiceField( ipsec_profile = DynamicModelChoiceField(
queryset=IPSecProfile.objects.all() queryset=IPSecProfile.objects.all(),
label=_('IPSec Profile')
) )
comments = CommentField() comments = CommentField()
@ -51,6 +53,11 @@ class TunnelTerminationForm(NetBoxModelForm):
selector=True, selector=True,
label=_('Interface'), label=_('Interface'),
) )
outside_ip = DynamicModelChoiceField(
queryset=IPAddress.objects.all(),
selector=True,
label=_('Outside IP'),
)
class Meta: class Meta:
model = TunnelTermination model = TunnelTermination

View File

View File

@ -0,0 +1,26 @@
import graphene
from netbox.graphql.fields import ObjectField, ObjectListField
from utilities.graphql_optimizer import gql_query_optimizer
from vpn import models
from .types import *
class VPNQuery(graphene.ObjectType):
ipsec_profile = ObjectField(IPSecProfileType)
ipsec_profile_list = ObjectListField(IPSecProfileType)
def resolve_ipsec_profile_list(root, info, **kwargs):
return gql_query_optimizer(models.IPSecProfile.objects.all(), info)
tunnel = ObjectField(TunnelType)
tunnel_list = ObjectListField(TunnelType)
def resolve_tunnel_list(root, info, **kwargs):
return gql_query_optimizer(models.Tunnel.objects.all(), info)
tunnel_termination = ObjectField(TunnelTerminationType)
tunnel_termination_list = ObjectListField(TunnelTerminationType)
def resolve_tunnel_termination_list(root, info, **kwargs):
return gql_query_optimizer(models.TunnelTermination.objects.all(), info)

View File

@ -0,0 +1,33 @@
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
from vpn import filtersets, models
__all__ = (
'IPSecProfileType',
'TunnelTerminationType',
'TunnelType',
)
class TunnelTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
class Meta:
model = models.TunnelTermination
fields = '__all__'
filterset_class = filtersets.TunnelTerminationFilterSet
class TunnelType(NetBoxObjectType):
class Meta:
model = models.Tunnel
fields = '__all__'
filterset_class = filtersets.TunnelFilterSet
class IPSecProfileType(OrganizationalObjectType):
class Meta:
model = models.IPSecProfile
fields = '__all__'
filterset_class = filtersets.IPSecProfileFilterSet

View File

@ -0,0 +1,93 @@
# Generated by Django 4.2.6 on 2023-11-07 21:49
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
import utilities.json
class Migration(migrations.Migration):
initial = True
dependencies = [
('tenancy', '0011_contactassignment_tags'),
('extras', '0099_cachedvalue_ordering'),
('contenttypes', '0002_remove_content_type_name'),
('ipam', '0067_ipaddress_index_host'),
]
operations = [
migrations.CreateModel(
name='IPSecProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(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=utilities.json.CustomFieldJSONEncoder)),
('description', models.CharField(blank=True, max_length=200)),
('comments', models.TextField(blank=True)),
('name', models.CharField(max_length=100, unique=True)),
('protocol', models.CharField()),
('ike_version', models.PositiveSmallIntegerField(default=2)),
('phase1_encryption', models.CharField()),
('phase1_authentication', models.CharField()),
('phase1_group', models.PositiveSmallIntegerField()),
('phase1_sa_lifetime', models.PositiveSmallIntegerField(blank=True, null=True)),
('phase2_encryption', models.CharField()),
('phase2_authentication', models.CharField()),
('phase2_group', models.PositiveSmallIntegerField()),
('phase2_sa_lifetime', models.PositiveSmallIntegerField(blank=True, null=True)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'verbose_name': 'tunnel',
'verbose_name_plural': 'tunnels',
'ordering': ('name',),
},
),
migrations.CreateModel(
name='Tunnel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(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=utilities.json.CustomFieldJSONEncoder)),
('description', models.CharField(blank=True, max_length=200)),
('comments', models.TextField(blank=True)),
('name', models.CharField(max_length=100, unique=True)),
('status', models.CharField(default='active', max_length=50)),
('encapsulation', models.CharField(max_length=50)),
('preshared_key', models.TextField(blank=True)),
('tunnel_id', models.PositiveBigIntegerField(blank=True)),
('ipsec_profile', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tunnels', to='vpn.ipsecprofile')),
('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='tunnels', to='tenancy.tenant')),
],
options={
'verbose_name': 'tunnel',
'verbose_name_plural': 'tunnels',
'ordering': ('name',),
},
),
migrations.CreateModel(
name='TunnelTermination',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(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=utilities.json.CustomFieldJSONEncoder)),
('role', models.CharField(default='peer', max_length=50)),
('interface_id', models.PositiveBigIntegerField(blank=True, null=True)),
('interface_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
('outside_ip', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='tunnel_termination', to='ipam.ipaddress')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
('tunnel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='vpn.tunnel')),
],
options={
'verbose_name': 'tunnel termination',
'verbose_name_plural': 'tunnel terminations',
'ordering': ('tunnel', 'pk'),
},
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.6 on 2023-11-08 16:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('vpn', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='ipsecprofile',
name='phase1_sa_lifetime',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='ipsecprofile',
name='phase2_sa_lifetime',
field=models.PositiveIntegerField(blank=True, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.6 on 2023-11-08 16:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('vpn', '0002_alter_ipsecprofile_phase1_sa_lifetime_and_more'),
]
operations = [
migrations.AlterField(
model_name='tunnel',
name='tunnel_id',
field=models.PositiveBigIntegerField(blank=True, null=True),
),
]

View File

@ -40,7 +40,7 @@ class IPSecProfile(PrimaryModel):
choices=DHGroupChoices, choices=DHGroupChoices,
help_text=_('Diffie-Hellman group') help_text=_('Diffie-Hellman group')
) )
phase1_sa_lifetime = models.PositiveSmallIntegerField( phase1_sa_lifetime = models.PositiveIntegerField(
verbose_name=_('phase 1 SA lifetime'), verbose_name=_('phase 1 SA lifetime'),
blank=True, blank=True,
null=True, null=True,
@ -61,7 +61,7 @@ class IPSecProfile(PrimaryModel):
choices=DHGroupChoices, choices=DHGroupChoices,
help_text=_('Diffie-Hellman group') help_text=_('Diffie-Hellman group')
) )
phase2_sa_lifetime = models.PositiveSmallIntegerField( phase2_sa_lifetime = models.PositiveIntegerField(
verbose_name=_('phase 2 SA lifetime'), verbose_name=_('phase 2 SA lifetime'),
blank=True, blank=True,
null=True, null=True,
@ -70,8 +70,8 @@ class IPSecProfile(PrimaryModel):
# TODO: Add PFS group? # TODO: Add PFS group?
clone_fields = ( clone_fields = (
'protocol', 'ike_version', 'phase1_encryption', 'phase1_authentication', 'phase1_group', 'phase1_as_lifetime', 'protocol', 'ike_version', 'phase1_encryption', 'phase1_authentication', 'phase1_group', 'phase1_sa_lifetime',
'phase2_encryption', 'phase2_authentication', 'phase2_group', 'phase2_as_lifetime', 'phase2_encryption', 'phase2_authentication', 'phase2_group', 'phase2_sa_lifetime',
) )
class Meta: class Meta:

View File

@ -50,7 +50,8 @@ class Tunnel(PrimaryModel):
) )
tunnel_id = models.PositiveBigIntegerField( tunnel_id = models.PositiveBigIntegerField(
verbose_name=_('tunnel ID'), verbose_name=_('tunnel ID'),
blank=True blank=True,
null=True
) )
clone_fields = ( clone_fields = (
@ -113,3 +114,6 @@ class TunnelTermination(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLo
def get_absolute_url(self): def get_absolute_url(self):
return self.tunnel.get_absolute_url() return self.tunnel.get_absolute_url()
def get_role_color(self):
return TunnelTerminationRoleChoices.colors.get(self.role)

View File

@ -21,9 +21,6 @@ class TunnelTable(TenancyColumnsMixin, NetBoxTable):
status = columns.ChoiceFieldColumn( status = columns.ChoiceFieldColumn(
verbose_name=_('Status') verbose_name=_('Status')
) )
encapsulation = columns.ChoiceFieldColumn(
verbose_name=_('Encapsulation')
)
ipsec_profile = tables.Column( ipsec_profile = tables.Column(
verbose_name=_('IPSec profile'), verbose_name=_('IPSec profile'),
linkify=True linkify=True
@ -47,7 +44,7 @@ class TunnelTable(TenancyColumnsMixin, NetBoxTable):
'pk', 'id', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tenant_group', 'preshared_key', 'pk', 'id', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tenant_group', 'preshared_key',
'tunnel_id', 'termination_count', 'description', 'comments', 'tags', 'created', 'last_updated', 'tunnel_id', 'termination_count', 'description', 'comments', 'tags', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'status', 'encapsulation', 'tenant', 'termination_count') default_columns = ('pk', 'name', 'status', 'encapsulation', 'tenant', 'terminations_count')
class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable): class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable):

View File

@ -67,11 +67,6 @@ class TunnelTerminationListView(generic.ObjectListView):
table = tables.TunnelTerminationTable table = tables.TunnelTerminationTable
@register_model_view(TunnelTermination)
class TunnelTerminationView(generic.ObjectView):
queryset = TunnelTermination.objects.all()
@register_model_view(TunnelTermination, 'edit') @register_model_view(TunnelTermination, 'edit')
class TunnelTerminationEditView(generic.ObjectEditView): class TunnelTerminationEditView(generic.ObjectEditView):
queryset = TunnelTermination.objects.all() queryset = TunnelTermination.objects.all()