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.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})
### 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),
'users': reverse('users-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),
})

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
fields = (
'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))
def get_interface(self, obj):
serializer = get_serializer_for_model(obj.interface, prefix=NESTED_SERIALIZER_PREFIX)
context = {'request': self.context['request']}
return serializer(obj.assigned_object, context=context).data
return serializer(obj.interface, context=context).data
class IPSecProfileSerializer(NetBoxModelSerializer):

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from dcim.models import Interface
from ipam.models import IPAddress
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField
@ -17,7 +18,8 @@ __all__ = (
class TunnelForm(TenancyForm, NetBoxModelForm):
ipsec_profile = DynamicModelChoiceField(
queryset=IPSecProfile.objects.all()
queryset=IPSecProfile.objects.all(),
label=_('IPSec Profile')
)
comments = CommentField()
@ -51,6 +53,11 @@ class TunnelTerminationForm(NetBoxModelForm):
selector=True,
label=_('Interface'),
)
outside_ip = DynamicModelChoiceField(
queryset=IPAddress.objects.all(),
selector=True,
label=_('Outside IP'),
)
class Meta:
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,
help_text=_('Diffie-Hellman group')
)
phase1_sa_lifetime = models.PositiveSmallIntegerField(
phase1_sa_lifetime = models.PositiveIntegerField(
verbose_name=_('phase 1 SA lifetime'),
blank=True,
null=True,
@ -61,7 +61,7 @@ class IPSecProfile(PrimaryModel):
choices=DHGroupChoices,
help_text=_('Diffie-Hellman group')
)
phase2_sa_lifetime = models.PositiveSmallIntegerField(
phase2_sa_lifetime = models.PositiveIntegerField(
verbose_name=_('phase 2 SA lifetime'),
blank=True,
null=True,
@ -70,8 +70,8 @@ class IPSecProfile(PrimaryModel):
# TODO: Add PFS group?
clone_fields = (
'protocol', 'ike_version', 'phase1_encryption', 'phase1_authentication', 'phase1_group', 'phase1_as_lifetime',
'phase2_encryption', 'phase2_authentication', 'phase2_group', 'phase2_as_lifetime',
'protocol', 'ike_version', 'phase1_encryption', 'phase1_authentication', 'phase1_group', 'phase1_sa_lifetime',
'phase2_encryption', 'phase2_authentication', 'phase2_group', 'phase2_sa_lifetime',
)
class Meta:

View File

@ -50,7 +50,8 @@ class Tunnel(PrimaryModel):
)
tunnel_id = models.PositiveBigIntegerField(
verbose_name=_('tunnel ID'),
blank=True
blank=True,
null=True
)
clone_fields = (
@ -113,3 +114,6 @@ class TunnelTermination(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLo
def get_absolute_url(self):
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(
verbose_name=_('Status')
)
encapsulation = columns.ChoiceFieldColumn(
verbose_name=_('Encapsulation')
)
ipsec_profile = tables.Column(
verbose_name=_('IPSec profile'),
linkify=True
@ -47,7 +44,7 @@ class TunnelTable(TenancyColumnsMixin, NetBoxTable):
'pk', 'id', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tenant_group', 'preshared_key',
'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):

View File

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