1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

#9816: Add TunnelGroup

This commit is contained in:
Jeremy Stretch
2023-12-04 15:44:52 -05:00
parent 9f1283f0fa
commit 8db1093fdc
26 changed files with 669 additions and 160 deletions

View File

@ -1,6 +1,6 @@
# Tunnels # Tunnels
NetBox can model private tunnels formed among virtual termination points across your network. Typical tunnel implementations include GRE, IP-in-IP, and IPSec. A tunnel may be terminated to two or more device or virtual machine interfaces. NetBox can model private tunnels formed among virtual termination points across your network. Typical tunnel implementations include GRE, IP-in-IP, and IPSec. A tunnel may be terminated to two or more device or virtual machine interfaces. For convenient organization, tunnels may be assigned to user-defined groups.
```mermaid ```mermaid
flowchart TD flowchart TD

View File

@ -14,15 +14,17 @@ A unique name assigned to the tunnel for identification.
The operational status of the tunnel. By default, the following statuses are available: The operational status of the tunnel. By default, the following statuses are available:
| Name | * Planned
|----------------| * Active
| Planned | * Disabled
| Active |
| Disabled |
!!! tip "Custom tunnel statuses" !!! tip "Custom tunnel statuses"
Additional tunnel statuses may be defined by setting `Tunnel.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter. Additional tunnel statuses may be defined by setting `Tunnel.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Group
The [administrative group](./tunnelgroup.md) to which this tunnel is assigned (optional).
### Encapsulation ### Encapsulation
The encapsulation protocol or technique employed to effect the tunnel. NetBox supports GRE, IP-in-IP, and IPSec encapsulations. The encapsulation protocol or technique employed to effect the tunnel. NetBox supports GRE, IP-in-IP, and IPSec encapsulations.

View File

@ -0,0 +1,13 @@
# Tunnel Group
[Tunnels](./tunnel.md) can be arranged into administrative groups for organization. For example, you might crete a group to manage all peer-to-peer tunnels inside a mesh network. The assignment of a tunnel to a group is optional.
## Fields
### Name
A unique human-friendly name.
### Slug
A unique URL-friendly identifier. (This value can be used for filtering.)

View File

@ -361,7 +361,7 @@ INTERFACE_BUTTONS = """
{% endif %} {% endif %}
{% elif record.type == 'virtual' %} {% elif record.type == 'virtual' %}
{% if perms.vpn.add_tunnel and not record.tunnel_termination %} {% if perms.vpn.add_tunnel and not record.tunnel_termination %}
<a href="{% url 'vpn:tunnel_add' %}?termination1_type=dcim.device&termination1_parent={{ record.device.pk }}&termination1_interface={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}" title="Create a tunnel" class="btn btn-success btn-sm"> <a href="{% url 'vpn:tunnel_add' %}?termination1_type=dcim.device&termination1_parent={{ record.device.pk }}&termination1_termination={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}" title="Create a tunnel" class="btn btn-success btn-sm">
<i class="mdi mdi-tunnel-outline" aria-hidden="true"></i> <i class="mdi mdi-tunnel-outline" aria-hidden="true"></i>
</a> </a>
{% elif perms.vpn.delete_tunneltermination and record.tunnel_termination %} {% elif perms.vpn.delete_tunneltermination and record.tunnel_termination %}

View File

@ -203,6 +203,7 @@ VPN_MENU = Menu(
label=_('Tunnels'), label=_('Tunnels'),
items=( items=(
get_model_item('vpn', 'tunnel', _('Tunnels')), get_model_item('vpn', 'tunnel', _('Tunnels')),
get_model_item('vpn', 'tunnelgroup', _('Tunnel Groups')),
get_model_item('vpn', 'tunneltermination', _('Tunnel Terminations')), get_model_item('vpn', 'tunneltermination', _('Tunnel Terminations')),
), ),
), ),

View File

@ -26,6 +26,10 @@
<th scope="row">{% trans "Status" %}</th> <th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td> <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr> </tr>
<tr>
<th scope="row">{% trans "Group" %}</th>
<td>{{ object.group|linkify|placeholder }}</td>
</tr>
<tr> <tr>
<th scope="row">{% trans "Description" %}</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>

View File

@ -0,0 +1,53 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'vpn:tunnelgroup_list' %}">{% trans "Tunnel Groups" %}</a></li>
{% endblock %}
{% block extra_controls %}
{% if perms.vpn.add_tunnel %}
<a href="{% url 'vpn:tunnel_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Tunnel" %}
</a>
{% endif %}
{% endblock extra_controls %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
{% trans "Tunnel Group" %}
</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 "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -1,3 +1,4 @@
from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers from rest_framework import serializers
from netbox.api.serializers import WritableNestedSerializer from netbox.api.serializers import WritableNestedSerializer
@ -11,11 +12,24 @@ __all__ = (
'NestedIPSecProposalSerializer', 'NestedIPSecProposalSerializer',
'NestedL2VPNSerializer', 'NestedL2VPNSerializer',
'NestedL2VPNTerminationSerializer', 'NestedL2VPNTerminationSerializer',
'NestedTunnelGroupSerializer',
'NestedTunnelSerializer', 'NestedTunnelSerializer',
'NestedTunnelTerminationSerializer', 'NestedTunnelTerminationSerializer',
) )
@extend_schema_serializer(
exclude_fields=('tunnel_count',),
)
class NestedTunnelGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
tunnel_count = serializers.IntegerField(read_only=True)
class Meta:
model = models.TunnelGroup
fields = ['id', 'url', 'display', 'name', 'slug', 'tunnel_count']
class NestedTunnelSerializer(WritableNestedSerializer): class NestedTunnelSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField( url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:tunnel-detail' view_name='vpn-api:tunnel-detail'

View File

@ -21,11 +21,24 @@ __all__ = (
'IPSecProposalSerializer', 'IPSecProposalSerializer',
'L2VPNSerializer', 'L2VPNSerializer',
'L2VPNTerminationSerializer', 'L2VPNTerminationSerializer',
'TunnelGroupSerializer',
'TunnelSerializer', 'TunnelSerializer',
'TunnelTerminationSerializer', 'TunnelTerminationSerializer',
) )
class TunnelGroupSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
tunnel_count = serializers.IntegerField(read_only=True)
class Meta:
model = TunnelGroup
fields = [
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
'tunnel_count',
]
class TunnelSerializer(NetBoxModelSerializer): class TunnelSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField( url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:tunnel-detail' view_name='vpn-api:tunnel-detail'
@ -33,6 +46,7 @@ class TunnelSerializer(NetBoxModelSerializer):
status = ChoiceField( status = ChoiceField(
choices=TunnelStatusChoices choices=TunnelStatusChoices
) )
group = NestedTunnelGroupSerializer()
encapsulation = ChoiceField( encapsulation = ChoiceField(
choices=TunnelEncapsulationChoices choices=TunnelEncapsulationChoices
) )
@ -48,7 +62,7 @@ class TunnelSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Tunnel model = Tunnel
fields = ( fields = (
'id', 'url', 'display', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id', 'id', 'url', 'display', 'name', 'status', 'group', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
) )

View File

@ -8,6 +8,7 @@ router.register('ike-proposals', views.IKEProposalViewSet)
router.register('ipsec-policies', views.IPSecPolicyViewSet) router.register('ipsec-policies', views.IPSecPolicyViewSet)
router.register('ipsec-proposals', views.IPSecProposalViewSet) router.register('ipsec-proposals', views.IPSecProposalViewSet)
router.register('ipsec-profiles', views.IPSecProfileViewSet) router.register('ipsec-profiles', views.IPSecProfileViewSet)
router.register('tunnel-groups', views.TunnelGroupViewSet)
router.register('tunnels', views.TunnelViewSet) router.register('tunnels', views.TunnelViewSet)
router.register('tunnel-terminations', views.TunnelTerminationViewSet) router.register('tunnel-terminations', views.TunnelTerminationViewSet)
router.register('l2vpns', views.L2VPNViewSet) router.register('l2vpns', views.L2VPNViewSet)

View File

@ -14,6 +14,7 @@ __all__ = (
'IPSecProposalViewSet', 'IPSecProposalViewSet',
'L2VPNViewSet', 'L2VPNViewSet',
'L2VPNTerminationViewSet', 'L2VPNTerminationViewSet',
'TunnelGroupViewSet',
'TunnelTerminationViewSet', 'TunnelTerminationViewSet',
'TunnelViewSet', 'TunnelViewSet',
'VPNRootView', 'VPNRootView',
@ -32,6 +33,14 @@ class VPNRootView(APIRootView):
# Viewsets # Viewsets
# #
class TunnelGroupViewSet(NetBoxModelViewSet):
queryset = TunnelGroup.objects.annotate(
tunnel_count=count_related(Tunnel, 'group')
)
serializer_class = serializers.TunnelGroupSerializer
filterset_class = filtersets.TunnelGroupFilterSet
class TunnelViewSet(NetBoxModelViewSet): class TunnelViewSet(NetBoxModelViewSet):
queryset = Tunnel.objects.prefetch_related('ipsec_profile', 'tenant').annotate( queryset = Tunnel.objects.prefetch_related('ipsec_profile', 'tenant').annotate(
terminations_count=count_related(TunnelTermination, 'tunnel') terminations_count=count_related(TunnelTermination, 'tunnel')

View File

@ -4,7 +4,7 @@ from django.utils.translation import gettext as _
from dcim.models import Device, Interface from dcim.models import Device, Interface
from ipam.models import IPAddress, RouteTarget, VLAN from ipam.models import IPAddress, RouteTarget, VLAN
from netbox.filtersets import NetBoxModelFilterSet from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
from virtualization.models import VirtualMachine, VMInterface from virtualization.models import VirtualMachine, VMInterface
@ -20,14 +20,32 @@ __all__ = (
'L2VPNFilterSet', 'L2VPNFilterSet',
'L2VPNTerminationFilterSet', 'L2VPNTerminationFilterSet',
'TunnelFilterSet', 'TunnelFilterSet',
'TunnelGroupFilterSet',
'TunnelTerminationFilterSet', 'TunnelTerminationFilterSet',
) )
class TunnelGroupFilterSet(OrganizationalModelFilterSet):
class Meta:
model = TunnelGroup
fields = ['id', 'name', 'slug', 'description']
class TunnelFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class TunnelFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=TunnelStatusChoices choices=TunnelStatusChoices
) )
group_id = django_filters.ModelMultipleChoiceFilter(
queryset=TunnelGroup.objects.all(),
label=_('Tunnel group (ID)'),
)
group = django_filters.ModelMultipleChoiceFilter(
field_name='group__slug',
queryset=TunnelGroup.objects.all(),
to_field_name='slug',
label=_('Tunnel group (slug)'),
)
encapsulation = django_filters.MultipleChoiceFilter( encapsulation = django_filters.MultipleChoiceFilter(
choices=TunnelEncapsulationChoices choices=TunnelEncapsulationChoices
) )

View File

@ -17,16 +17,33 @@ __all__ = (
'L2VPNBulkEditForm', 'L2VPNBulkEditForm',
'L2VPNTerminationBulkEditForm', 'L2VPNTerminationBulkEditForm',
'TunnelBulkEditForm', 'TunnelBulkEditForm',
'TunnelGroupBulkEditForm',
'TunnelTerminationBulkEditForm', 'TunnelTerminationBulkEditForm',
) )
class TunnelGroupBulkEditForm(NetBoxModelBulkEditForm):
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
model = TunnelGroup
nullable_fields = ('description',)
class TunnelBulkEditForm(NetBoxModelBulkEditForm): class TunnelBulkEditForm(NetBoxModelBulkEditForm):
status = forms.ChoiceField( status = forms.ChoiceField(
label=_('Status'), label=_('Status'),
choices=add_blank_choice(TunnelStatusChoices), choices=add_blank_choice(TunnelStatusChoices),
required=False required=False
) )
group = DynamicModelChoiceField(
queryset=TunnelGroup.objects.all(),
label=_('Tunnel group'),
required=False
)
encapsulation = forms.ChoiceField( encapsulation = forms.ChoiceField(
label=_('Encapsulation'), label=_('Encapsulation'),
choices=add_blank_choice(TunnelEncapsulationChoices), choices=add_blank_choice(TunnelEncapsulationChoices),
@ -55,12 +72,12 @@ class TunnelBulkEditForm(NetBoxModelBulkEditForm):
model = Tunnel model = Tunnel
fieldsets = ( fieldsets = (
(_('Tunnel'), ('status', 'encapsulation', 'tunnel_id', 'description')), (_('Tunnel'), ('status', 'group', 'encapsulation', 'tunnel_id', 'description')),
(_('Security'), ('ipsec_profile',)), (_('Security'), ('ipsec_profile',)),
(_('Tenancy'), ('tenant',)), (_('Tenancy'), ('tenant',)),
) )
nullable_fields = ( nullable_fields = (
'ipsec_profile', 'tunnel_id', 'tenant', 'description', 'comments', 'group', 'ipsec_profile', 'tunnel_id', 'tenant', 'description', 'comments',
) )

View File

@ -5,7 +5,7 @@ from dcim.models import Device, Interface
from ipam.models import IPAddress, VLAN from ipam.models import IPAddress, VLAN
from netbox.forms import NetBoxModelImportForm from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField
from virtualization.models import VirtualMachine, VMInterface from virtualization.models import VirtualMachine, VMInterface
from vpn.choices import * from vpn.choices import *
from vpn.models import * from vpn.models import *
@ -19,16 +19,31 @@ __all__ = (
'L2VPNImportForm', 'L2VPNImportForm',
'L2VPNTerminationImportForm', 'L2VPNTerminationImportForm',
'TunnelImportForm', 'TunnelImportForm',
'TunnelGroupImportForm',
'TunnelTerminationImportForm', 'TunnelTerminationImportForm',
) )
class TunnelGroupImportForm(NetBoxModelImportForm):
slug = SlugField()
class Meta:
model = TunnelGroup
fields = ('name', 'slug', 'description', 'tags')
class TunnelImportForm(NetBoxModelImportForm): class TunnelImportForm(NetBoxModelImportForm):
status = CSVChoiceField( status = CSVChoiceField(
label=_('Status'), label=_('Status'),
choices=TunnelStatusChoices, choices=TunnelStatusChoices,
help_text=_('Operational status') help_text=_('Operational status')
) )
group = CSVModelChoiceField(
label=_('Tunnel group'),
queryset=TunnelGroup.objects.all(),
required=False,
to_field_name='name'
)
encapsulation = CSVChoiceField( encapsulation = CSVChoiceField(
label=_('Encapsulation'), label=_('Encapsulation'),
choices=TunnelEncapsulationChoices, choices=TunnelEncapsulationChoices,
@ -51,8 +66,8 @@ class TunnelImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = Tunnel model = Tunnel
fields = ( fields = (
'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id', 'description', 'comments', 'name', 'status', 'group', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id', 'description',
'tags', 'comments', 'tags',
) )

View File

@ -24,10 +24,16 @@ __all__ = (
'L2VPNFilterForm', 'L2VPNFilterForm',
'L2VPNTerminationFilterForm', 'L2VPNTerminationFilterForm',
'TunnelFilterForm', 'TunnelFilterForm',
'TunnelGroupFilterForm',
'TunnelTerminationFilterForm', 'TunnelTerminationFilterForm',
) )
class TunnelGroupFilterForm(NetBoxModelFilterSetForm):
model = TunnelGroup
tag = TagFilterField(model)
class TunnelFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class TunnelFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Tunnel model = Tunnel
fieldsets = ( fieldsets = (
@ -41,6 +47,11 @@ class TunnelFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
choices=TunnelStatusChoices, choices=TunnelStatusChoices,
required=False required=False
) )
group_id = DynamicModelMultipleChoiceField(
queryset=TunnelGroup.objects.all(),
required=False,
label=_('Tunnel group')
)
encapsulation = forms.MultipleChoiceField( encapsulation = forms.MultipleChoiceField(
label=_('Encapsulation'), label=_('Encapsulation'),
choices=TunnelEncapsulationChoices, choices=TunnelEncapsulationChoices,

View File

@ -23,11 +23,31 @@ __all__ = (
'L2VPNTerminationForm', 'L2VPNTerminationForm',
'TunnelCreateForm', 'TunnelCreateForm',
'TunnelForm', 'TunnelForm',
'TunnelGroupForm',
'TunnelTerminationForm', 'TunnelTerminationForm',
) )
class TunnelGroupForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
(_('Tunnel Group'), ('name', 'slug', 'description', 'tags')),
)
class Meta:
model = TunnelGroup
fields = [
'name', 'slug', 'description', 'tags',
]
class TunnelForm(TenancyForm, NetBoxModelForm): class TunnelForm(TenancyForm, NetBoxModelForm):
group = DynamicModelChoiceField(
queryset=TunnelGroup.objects.all(),
label=_('Tunnel Group'),
required=False
)
ipsec_profile = DynamicModelChoiceField( ipsec_profile = DynamicModelChoiceField(
queryset=IPSecProfile.objects.all(), queryset=IPSecProfile.objects.all(),
label=_('IPSec Profile'), label=_('IPSec Profile'),
@ -36,7 +56,7 @@ class TunnelForm(TenancyForm, NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('Tunnel'), ('name', 'status', 'encapsulation', 'description', 'tunnel_id', 'tags')), (_('Tunnel'), ('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags')),
(_('Security'), ('ipsec_profile',)), (_('Security'), ('ipsec_profile',)),
(_('Tenancy'), ('tenant_group', 'tenant')), (_('Tenancy'), ('tenant_group', 'tenant')),
) )
@ -44,8 +64,8 @@ class TunnelForm(TenancyForm, NetBoxModelForm):
class Meta: class Meta:
model = Tunnel model = Tunnel
fields = [ fields = [
'name', 'status', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'tenant_group', 'tenant', 'name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'tenant_group',
'comments', 'tags', 'tenant', 'comments', 'tags',
] ]

View File

@ -56,6 +56,12 @@ class VPNQuery(graphene.ObjectType):
def resolve_tunnel_list(root, info, **kwargs): def resolve_tunnel_list(root, info, **kwargs):
return gql_query_optimizer(models.Tunnel.objects.all(), info) return gql_query_optimizer(models.Tunnel.objects.all(), info)
tunnel_group = ObjectField(TunnelGroupType)
tunnel_group_list = ObjectListField(TunnelGroupType)
def resolve_tunnel_group_list(root, info, **kwargs):
return gql_query_optimizer(models.TunnelGroup.objects.all(), info)
tunnel_termination = ObjectField(TunnelTerminationType) tunnel_termination = ObjectField(TunnelTerminationType)
tunnel_termination_list = ObjectListField(TunnelTerminationType) tunnel_termination_list = ObjectListField(TunnelTerminationType)

View File

@ -12,11 +12,20 @@ __all__ = (
'IPSecProposalType', 'IPSecProposalType',
'L2VPNType', 'L2VPNType',
'L2VPNTerminationType', 'L2VPNTerminationType',
'TunnelGroupType',
'TunnelTerminationType', 'TunnelTerminationType',
'TunnelType', 'TunnelType',
) )
class TunnelGroupType(OrganizationalObjectType):
class Meta:
model = models.TunnelGroup
fields = '__all__'
filterset_class = filtersets.TunnelGroupFilterSet
class TunnelTerminationType(CustomFieldsMixin, TagsMixin, ObjectType): class TunnelTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
class Meta: class Meta:

View File

@ -16,140 +16,7 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.CreateModel( # IKE
name='IKEPolicy',
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)),
('version', models.PositiveSmallIntegerField(default=2)),
('mode', models.CharField()),
('preshared_key', models.TextField(blank=True)),
],
options={
'verbose_name': 'IKE policy',
'verbose_name_plural': 'IKE policies',
'ordering': ('name',),
},
),
migrations.CreateModel(
name='IPSecPolicy',
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)),
('pfs_group', models.PositiveSmallIntegerField(blank=True, null=True)),
],
options={
'verbose_name': 'IPSec policy',
'verbose_name_plural': 'IPSec policies',
'ordering': ('name',),
},
),
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)),
('mode', models.CharField()),
('ike_policy', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='ipsec_profiles', to='vpn.ikepolicy')),
('ipsec_policy', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='ipsec_profiles', to='vpn.ipsecpolicy')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'verbose_name': 'IPSec profile',
'verbose_name_plural': 'IPSec profiles',
'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)),
('tunnel_id', models.PositiveBigIntegerField(blank=True, null=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)),
('termination_id', models.PositiveBigIntegerField(blank=True, null=True)),
('termination_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
('outside_ip', models.OneToOneField(blank=True, null=True, 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', 'role', 'pk'),
},
),
migrations.CreateModel(
name='IPSecProposal',
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)),
('encryption_algorithm', models.CharField()),
('authentication_algorithm', models.CharField()),
('sa_lifetime_seconds', models.PositiveIntegerField(blank=True, null=True)),
('sa_lifetime_data', models.PositiveIntegerField(blank=True, null=True)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'verbose_name': 'IPSec proposal',
'verbose_name_plural': 'IPSec proposals',
'ordering': ('name',),
},
),
migrations.AddField(
model_name='ipsecpolicy',
name='proposals',
field=models.ManyToManyField(related_name='ipsec_policies', to='vpn.ipsecproposal'),
),
migrations.AddField(
model_name='ipsecpolicy',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.CreateModel( migrations.CreateModel(
name='IKEProposal', name='IKEProposal',
fields=[ fields=[
@ -173,6 +40,26 @@ class Migration(migrations.Migration):
'ordering': ('name',), 'ordering': ('name',),
}, },
), ),
migrations.CreateModel(
name='IKEPolicy',
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)),
('version', models.PositiveSmallIntegerField(default=2)),
('mode', models.CharField()),
('preshared_key', models.TextField(blank=True)),
],
options={
'verbose_name': 'IKE policy',
'verbose_name_plural': 'IKE policies',
'ordering': ('name',),
},
),
migrations.AddField( migrations.AddField(
model_name='ikepolicy', model_name='ikepolicy',
name='proposals', name='proposals',
@ -183,6 +70,155 @@ class Migration(migrations.Migration):
name='tags', name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
), ),
# IPSec
migrations.CreateModel(
name='IPSecProposal',
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)),
('encryption_algorithm', models.CharField()),
('authentication_algorithm', models.CharField()),
('sa_lifetime_seconds', models.PositiveIntegerField(blank=True, null=True)),
('sa_lifetime_data', models.PositiveIntegerField(blank=True, null=True)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'verbose_name': 'IPSec proposal',
'verbose_name_plural': 'IPSec proposals',
'ordering': ('name',),
},
),
migrations.CreateModel(
name='IPSecPolicy',
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)),
('pfs_group', models.PositiveSmallIntegerField(blank=True, null=True)),
],
options={
'verbose_name': 'IPSec policy',
'verbose_name_plural': 'IPSec policies',
'ordering': ('name',),
},
),
migrations.AddField(
model_name='ipsecpolicy',
name='proposals',
field=models.ManyToManyField(related_name='ipsec_policies', to='vpn.ipsecproposal'),
),
migrations.AddField(
model_name='ipsecpolicy',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
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)),
('mode', models.CharField()),
('ike_policy', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='ipsec_profiles', to='vpn.ikepolicy')),
('ipsec_policy', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='ipsec_profiles', to='vpn.ipsecpolicy')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'verbose_name': 'IPSec profile',
'verbose_name_plural': 'IPSec profiles',
'ordering': ('name',),
},
),
# Tunnels
migrations.CreateModel(
name='TunnelGroup',
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)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
('description', models.CharField(blank=True, max_length=200)),
],
options={
'verbose_name': 'tunnel group',
'verbose_name_plural': 'tunnel groups',
'ordering': ('name',),
},
),
migrations.AddField(
model_name='tunnelgroup',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
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)),
('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tunnels', to='vpn.tunnelgroup')),
('encapsulation', models.CharField(max_length=50)),
('tunnel_id', models.PositiveBigIntegerField(blank=True, null=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.AddConstraint(
model_name='tunnel',
constraint=models.UniqueConstraint(fields=('group', 'name'), name='vpn_tunnel_group_name'),
),
migrations.AddConstraint(
model_name='tunnel',
constraint=models.UniqueConstraint(condition=models.Q(('group__isnull', True)), fields=('name',), name='vpn_tunnel_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)),
('termination_id', models.PositiveBigIntegerField(blank=True, null=True)),
('termination_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
('outside_ip', models.OneToOneField(blank=True, null=True, 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', 'role', 'pk'),
},
),
migrations.AddConstraint( migrations.AddConstraint(
model_name='tunneltermination', model_name='tunneltermination',
constraint=models.UniqueConstraint(fields=('termination_type', 'termination_id'), name='vpn_tunneltermination_termination', violation_error_message='An object may be terminated to only one tunnel at a time.'), constraint=models.UniqueConstraint(fields=('termination_type', 'termination_id'), name='vpn_tunneltermination_termination', violation_error_message='An object may be terminated to only one tunnel at a time.'),

View File

@ -1,19 +1,35 @@
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from netbox.models import ChangeLoggedModel, PrimaryModel from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin
from vpn.choices import * from vpn.choices import *
__all__ = ( __all__ = (
'Tunnel', 'Tunnel',
'TunnelGroup',
'TunnelTermination', 'TunnelTermination',
) )
class TunnelGroup(OrganizationalModel):
"""
An administrative grouping of Tunnels. This can be used to correlate peer-to-peer tunnels which form a mesh,
for example.
"""
class Meta:
ordering = ('name',)
verbose_name = _('tunnel group')
verbose_name_plural = _('tunnel groups')
def get_absolute_url(self):
return reverse('vpn:tunnelgroup', args=[self.pk])
class Tunnel(PrimaryModel): class Tunnel(PrimaryModel):
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
@ -26,6 +42,13 @@ class Tunnel(PrimaryModel):
choices=TunnelStatusChoices, choices=TunnelStatusChoices,
default=TunnelStatusChoices.STATUS_ACTIVE default=TunnelStatusChoices.STATUS_ACTIVE
) )
group = models.ForeignKey(
to='vpn.TunnelGroup',
on_delete=models.PROTECT,
related_name='tunnels',
blank=True,
null=True
)
encapsulation = models.CharField( encapsulation = models.CharField(
verbose_name=_('encapsulation'), verbose_name=_('encapsulation'),
max_length=50, max_length=50,
@ -57,6 +80,17 @@ class Tunnel(PrimaryModel):
class Meta: class Meta:
ordering = ('name',) ordering = ('name',)
constraints = (
models.UniqueConstraint(
fields=('group', 'name'),
name='%(app_label)s_%(class)s_group_name'
),
models.UniqueConstraint(
fields=('name',),
name='%(app_label)s_%(class)s_name',
condition=Q(group__isnull=True)
),
)
verbose_name = _('tunnel') verbose_name = _('tunnel')
verbose_name_plural = _('tunnels') verbose_name_plural = _('tunnels')

View File

@ -8,10 +8,33 @@ from vpn.models import *
__all__ = ( __all__ = (
'TunnelTable', 'TunnelTable',
'TunnelGroupTable',
'TunnelTerminationTable', 'TunnelTerminationTable',
) )
class TunnelGroupTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
tunnel_count = columns.LinkedCountColumn(
viewname='vpn:tunnel_list',
url_params={'group_id': 'pk'},
verbose_name=_('Tunnels')
)
tags = columns.TagColumn(
url_name='vpn:tunnelgroup_list'
)
class Meta(NetBoxTable.Meta):
model = TunnelGroup
fields = (
'pk', 'id', 'name', 'tunnel_count', 'description', 'slug', 'tags', 'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'tunnel_count', 'description')
class TunnelTable(TenancyColumnsMixin, NetBoxTable): class TunnelTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
verbose_name=_('Name'), verbose_name=_('Name'),

View File

@ -17,6 +17,38 @@ class AppTest(APITestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
class TunnelGroupTest(APIViewTestCases.APIViewTestCase):
model = TunnelGroup
brief_fields = ['display', 'id', 'name', 'slug', 'tunnel_count', 'url']
create_data = (
{
'name': 'Tunnel Group 4',
'slug': 'tunnel-group-4',
},
{
'name': 'Tunnel Group 5',
'slug': 'tunnel-group-5',
},
{
'name': 'Tunnel Group 6',
'slug': 'tunnel-group-6',
},
)
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
tunnel_groups = (
TunnelGroup(name='Tunnel Group 1', slug='tunnel-group-1'),
TunnelGroup(name='Tunnel Group 2', slug='tunnel-group-2'),
TunnelGroup(name='Tunnel Group 3', slug='tunnel-group-3'),
)
TunnelGroup.objects.bulk_create(tunnel_groups)
class TunnelTest(APIViewTestCases.APIViewTestCase): class TunnelTest(APIViewTestCases.APIViewTestCase):
model = Tunnel model = Tunnel
brief_fields = ['display', 'id', 'name', 'url'] brief_fields = ['display', 'id', 'name', 'url']
@ -29,20 +61,29 @@ class TunnelTest(APIViewTestCases.APIViewTestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
tunnel_groups = (
TunnelGroup(name='Tunnel Group 1', slug='tunnel-group-1'),
TunnelGroup(name='Tunnel Group 2', slug='tunnel-group-2'),
)
TunnelGroup.objects.bulk_create(tunnel_groups)
tunnels = ( tunnels = (
Tunnel( Tunnel(
name='Tunnel 1', name='Tunnel 1',
status=TunnelStatusChoices.STATUS_ACTIVE, status=TunnelStatusChoices.STATUS_ACTIVE,
group=tunnel_groups[0],
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
), ),
Tunnel( Tunnel(
name='Tunnel 2', name='Tunnel 2',
status=TunnelStatusChoices.STATUS_ACTIVE, status=TunnelStatusChoices.STATUS_ACTIVE,
group=tunnel_groups[0],
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
), ),
Tunnel( Tunnel(
name='Tunnel 3', name='Tunnel 3',
status=TunnelStatusChoices.STATUS_ACTIVE, status=TunnelStatusChoices.STATUS_ACTIVE,
group=tunnel_groups[0],
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
), ),
) )
@ -52,16 +93,19 @@ class TunnelTest(APIViewTestCases.APIViewTestCase):
{ {
'name': 'Tunnel 4', 'name': 'Tunnel 4',
'status': TunnelStatusChoices.STATUS_DISABLED, 'status': TunnelStatusChoices.STATUS_DISABLED,
'group': tunnel_groups[1].pk,
'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE, 'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
}, },
{ {
'name': 'Tunnel 5', 'name': 'Tunnel 5',
'status': TunnelStatusChoices.STATUS_DISABLED, 'status': TunnelStatusChoices.STATUS_DISABLED,
'group': tunnel_groups[1].pk,
'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE, 'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
}, },
{ {
'name': 'Tunnel 6', 'name': 'Tunnel 6',
'status': TunnelStatusChoices.STATUS_DISABLED, 'status': TunnelStatusChoices.STATUS_DISABLED,
'group': tunnel_groups[1].pk,
'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE, 'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
}, },
] ]

View File

@ -11,6 +11,32 @@ from vpn.filtersets import *
from vpn.models import * from vpn.models import *
class TunnelGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = TunnelGroup.objects.all()
filterset = TunnelGroupFilterSet
@classmethod
def setUpTestData(cls):
TunnelGroup.objects.bulk_create((
TunnelGroup(name='Tunnel Group 1', slug='tunnel-group-1', description='foobar1'),
TunnelGroup(name='Tunnel Group 2', slug='tunnel-group-2', description='foobar2'),
TunnelGroup(name='Tunnel Group 3', slug='tunnel-group-3'),
))
def test_name(self):
params = {'name': ['Tunnel Group 1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_slug(self):
params = {'slug': ['tunnel-group-1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class TunnelTestCase(TestCase, ChangeLoggedFilterSetTests): class TunnelTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Tunnel.objects.all() queryset = Tunnel.objects.all()
filterset = TunnelFilterSet filterset = TunnelFilterSet
@ -56,10 +82,18 @@ class TunnelTestCase(TestCase, ChangeLoggedFilterSetTests):
) )
IPSecProfile.objects.bulk_create(ipsec_profiles) IPSecProfile.objects.bulk_create(ipsec_profiles)
tunnel_groups = (
TunnelGroup(name='Tunnel Group 1', slug='tunnel-group-1'),
TunnelGroup(name='Tunnel Group 2', slug='tunnel-group-2'),
TunnelGroup(name='Tunnel Group 3', slug='tunnel-group-3'),
)
TunnelGroup.objects.bulk_create(tunnel_groups)
tunnels = ( tunnels = (
Tunnel( Tunnel(
name='Tunnel 1', name='Tunnel 1',
status=TunnelStatusChoices.STATUS_ACTIVE, status=TunnelStatusChoices.STATUS_ACTIVE,
group=tunnel_groups[0],
encapsulation=TunnelEncapsulationChoices.ENCAP_GRE, encapsulation=TunnelEncapsulationChoices.ENCAP_GRE,
ipsec_profile=ipsec_profiles[0], ipsec_profile=ipsec_profiles[0],
tunnel_id=100 tunnel_id=100
@ -67,6 +101,7 @@ class TunnelTestCase(TestCase, ChangeLoggedFilterSetTests):
Tunnel( Tunnel(
name='Tunnel 2', name='Tunnel 2',
status=TunnelStatusChoices.STATUS_PLANNED, status=TunnelStatusChoices.STATUS_PLANNED,
group=tunnel_groups[1],
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP, encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP,
ipsec_profile=ipsec_profiles[0], ipsec_profile=ipsec_profiles[0],
tunnel_id=200 tunnel_id=200
@ -74,6 +109,7 @@ class TunnelTestCase(TestCase, ChangeLoggedFilterSetTests):
Tunnel( Tunnel(
name='Tunnel 3', name='Tunnel 3',
status=TunnelStatusChoices.STATUS_DISABLED, status=TunnelStatusChoices.STATUS_DISABLED,
group=tunnel_groups[2],
encapsulation=TunnelEncapsulationChoices.ENCAP_IPSEC_TUNNEL, encapsulation=TunnelEncapsulationChoices.ENCAP_IPSEC_TUNNEL,
ipsec_profile=None, ipsec_profile=None,
tunnel_id=300 tunnel_id=300
@ -89,6 +125,13 @@ class TunnelTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'status': [TunnelStatusChoices.STATUS_ACTIVE, TunnelStatusChoices.STATUS_PLANNED]} params = {'status': [TunnelStatusChoices.STATUS_ACTIVE, TunnelStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_group(self):
tunnel_groups = TunnelGroup.objects.all()[:2]
params = {'group_id': [tunnel_groups[0].pk, tunnel_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'group': [tunnel_groups[0].slug, tunnel_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_encapsulation(self): def test_encapsulation(self):
params = {'encapsulation': [TunnelEncapsulationChoices.ENCAP_GRE, TunnelEncapsulationChoices.ENCAP_IP_IP]} params = {'encapsulation': [TunnelEncapsulationChoices.ENCAP_GRE, TunnelEncapsulationChoices.ENCAP_IP_IP]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@ -6,26 +6,78 @@ from vpn.choices import *
from vpn.models import * from vpn.models import *
class TunnelGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = TunnelGroup
@classmethod
def setUpTestData(cls):
tunnel_groups = (
TunnelGroup(name='Tunnel Group 1', slug='tunnel-group-1'),
TunnelGroup(name='Tunnel Group 2', slug='tunnel-group-2'),
TunnelGroup(name='Tunnel Group 3', slug='tunnel-group-3'),
)
TunnelGroup.objects.bulk_create(tunnel_groups)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Tunnel Group X',
'slug': 'tunnel-group-x',
'description': 'A new Tunnel Group',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"name,slug",
"Tunnel Group 4,tunnel-group-4",
"Tunnel Group 5,tunnel-group-5",
"Tunnel Group 6,tunnel-group-6",
)
cls.csv_update_data = (
"id,name,description",
f"{tunnel_groups[0].pk},Tunnel Group 7,New description7",
f"{tunnel_groups[1].pk},Tunnel Group 8,New description8",
f"{tunnel_groups[2].pk},Tunnel Group 9,New description9",
)
cls.bulk_edit_data = {
'description': 'Foo',
}
class TunnelTestCase(ViewTestCases.PrimaryObjectViewTestCase): class TunnelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = Tunnel model = Tunnel
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
tunnel_groups = (
TunnelGroup(name='Tunnel Group 1', slug='tunnel-group-1'),
TunnelGroup(name='Tunnel Group 2', slug='tunnel-group-2'),
TunnelGroup(name='Tunnel Group 3', slug='tunnel-group-3'),
TunnelGroup(name='Tunnel Group 4', slug='tunnel-group-4'),
)
TunnelGroup.objects.bulk_create(tunnel_groups)
tunnels = ( tunnels = (
Tunnel( Tunnel(
name='Tunnel 1', name='Tunnel 1',
status=TunnelStatusChoices.STATUS_ACTIVE, status=TunnelStatusChoices.STATUS_ACTIVE,
group=tunnel_groups[0],
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
), ),
Tunnel( Tunnel(
name='Tunnel 2', name='Tunnel 2',
status=TunnelStatusChoices.STATUS_ACTIVE, status=TunnelStatusChoices.STATUS_ACTIVE,
group=tunnel_groups[1],
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
), ),
Tunnel( Tunnel(
name='Tunnel 3', name='Tunnel 3',
status=TunnelStatusChoices.STATUS_ACTIVE, status=TunnelStatusChoices.STATUS_ACTIVE,
group=tunnel_groups[2],
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
), ),
) )
@ -37,26 +89,28 @@ class TunnelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'name': 'Tunnel X', 'name': 'Tunnel X',
'description': 'New tunnel', 'description': 'New tunnel',
'status': TunnelStatusChoices.STATUS_PLANNED, 'status': TunnelStatusChoices.STATUS_PLANNED,
'group': tunnel_groups[3].pk,
'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE, 'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (
"name,status,encapsulation", "name,status,group,encapsulation",
"Tunnel 4,planned,gre", "Tunnel 4,planned,Tunnel Group 1,gre",
"Tunnel 5,planned,gre", "Tunnel 5,planned,Tunnel Group 2,gre",
"Tunnel 6,planned,gre", "Tunnel 6,planned,Tunnel Group 3,gre",
) )
cls.csv_update_data = ( cls.csv_update_data = (
"id,status,encapsulation", "id,status,group,encapsulation",
f"{tunnels[0].pk},active,ip-ip", f"{tunnels[0].pk},active,Tunnel Group 4,ip-ip",
f"{tunnels[1].pk},active,ip-ip", f"{tunnels[1].pk},active,Tunnel Group 4,ip-ip",
f"{tunnels[2].pk},active,ip-ip", f"{tunnels[2].pk},active,Tunnel Group 4,ip-ip",
) )
cls.bulk_edit_data = { cls.bulk_edit_data = {
'description': 'New description', 'description': 'New description',
'group': tunnel_groups[3].pk,
'status': TunnelStatusChoices.STATUS_DISABLED, 'status': TunnelStatusChoices.STATUS_DISABLED,
'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE, 'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
} }

View File

@ -6,6 +6,14 @@ from . import views
app_name = 'vpn' app_name = 'vpn'
urlpatterns = [ urlpatterns = [
# Tunnel groups
path('tunnel-groups/', views.TunnelGroupListView.as_view(), name='tunnelgroup_list'),
path('tunnel-groups/add/', views.TunnelGroupEditView.as_view(), name='tunnelgroup_add'),
path('tunnel-groups/import/', views.TunnelGroupBulkImportView.as_view(), name='tunnelgroup_import'),
path('tunnel-groups/edit/', views.TunnelGroupBulkEditView.as_view(), name='tunnelgroup_bulk_edit'),
path('tunnel-groups/delete/', views.TunnelGroupBulkDeleteView.as_view(), name='tunnelgroup_bulk_delete'),
path('tunnel-groups/<int:pk>/', include(get_model_urls('vpn', 'tunnelgroup'))),
# Tunnels # Tunnels
path('tunnels/', views.TunnelListView.as_view(), name='tunnel_list'), path('tunnels/', views.TunnelListView.as_view(), name='tunnel_list'),
path('tunnels/add/', views.TunnelEditView.as_view(), name='tunnel_add'), path('tunnels/add/', views.TunnelEditView.as_view(), name='tunnel_add'),

View File

@ -7,6 +7,66 @@ from . import filtersets, forms, tables
from .models import * from .models import *
#
# Tunnel groups
#
class TunnelGroupListView(generic.ObjectListView):
queryset = TunnelGroup.objects.annotate(
tunnel_count=count_related(Tunnel, 'group')
)
filterset = filtersets.TunnelGroupFilterSet
filterset_form = forms.TunnelGroupFilterForm
table = tables.TunnelGroupTable
@register_model_view(TunnelGroup)
class TunnelGroupView(generic.ObjectView):
queryset = TunnelGroup.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(Tunnel.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'),
)
return {
'related_models': related_models,
}
@register_model_view(TunnelGroup, 'edit')
class TunnelGroupEditView(generic.ObjectEditView):
queryset = TunnelGroup.objects.all()
form = forms.TunnelGroupForm
@register_model_view(TunnelGroup, 'delete')
class TunnelGroupDeleteView(generic.ObjectDeleteView):
queryset = TunnelGroup.objects.all()
class TunnelGroupBulkImportView(generic.BulkImportView):
queryset = TunnelGroup.objects.all()
model_form = forms.TunnelGroupImportForm
class TunnelGroupBulkEditView(generic.BulkEditView):
queryset = TunnelGroup.objects.annotate(
tunnel_count=count_related(Tunnel, 'group')
)
filterset = filtersets.TunnelGroupFilterSet
table = tables.TunnelGroupTable
form = forms.TunnelGroupBulkEditForm
class TunnelGroupBulkDeleteView(generic.BulkDeleteView):
queryset = TunnelGroup.objects.annotate(
tunnel_count=count_related(Tunnel, 'group')
)
filterset = filtersets.TunnelGroupFilterSet
table = tables.TunnelGroupTable
# #
# Tunnels # Tunnels
# #