Skip to content

Commit

Permalink
#9816: Add TunnelGroup
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremystretch committed Dec 4, 2023
1 parent 9f1283f commit 8db1093
Show file tree
Hide file tree
Showing 26 changed files with 600 additions and 91 deletions.
2 changes: 1 addition & 1 deletion docs/features/vpn-tunnels.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 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
flowchart TD
Expand Down
12 changes: 7 additions & 5 deletions docs/models/vpn/tunnel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

| Name |
|----------------|
| Planned |
| Active |
| Disabled |
* Planned
* Active
* Disabled

!!! 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.

### Group

The [administrative group](./tunnelgroup.md) to which this tunnel is assigned (optional).

### Encapsulation

The encapsulation protocol or technique employed to effect the tunnel. NetBox supports GRE, IP-in-IP, and IPSec encapsulations.
Expand Down
13 changes: 13 additions & 0 deletions docs/models/vpn/tunnelgroup.md
Original file line number Diff line number Diff line change
@@ -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.)
2 changes: 1 addition & 1 deletion netbox/dcim/tables/template_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@
{% endif %}
{% elif record.type == 'virtual' %}
{% 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>
</a>
{% elif perms.vpn.delete_tunneltermination and record.tunnel_termination %}
Expand Down
1 change: 1 addition & 0 deletions netbox/netbox/navigation/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@
label=_('Tunnels'),
items=(
get_model_item('vpn', 'tunnel', _('Tunnels')),
get_model_item('vpn', 'tunnelgroup', _('Tunnel Groups')),
get_model_item('vpn', 'tunneltermination', _('Tunnel Terminations')),
),
),
Expand Down
4 changes: 4 additions & 0 deletions netbox/templates/vpn/tunnel.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ <h5 class="card-header">{% trans "Tunnel" %}</h5>
<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 "Group" %}</th>
<td>{{ object.group|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
Expand Down
53 changes: 53 additions & 0 deletions netbox/templates/vpn/tunnelgroup.html
Original file line number Diff line number Diff line change
@@ -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 %}
14 changes: 14 additions & 0 deletions netbox/vpn/api/nested_serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers

from netbox.api.serializers import WritableNestedSerializer
Expand All @@ -11,11 +12,24 @@
'NestedIPSecProposalSerializer',
'NestedL2VPNSerializer',
'NestedL2VPNTerminationSerializer',
'NestedTunnelGroupSerializer',
'NestedTunnelSerializer',
'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):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:tunnel-detail'
Expand Down
16 changes: 15 additions & 1 deletion netbox/vpn/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,32 @@
'IPSecProposalSerializer',
'L2VPNSerializer',
'L2VPNTerminationSerializer',
'TunnelGroupSerializer',
'TunnelSerializer',
'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):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:tunnel-detail'
)
status = ChoiceField(
choices=TunnelStatusChoices
)
group = NestedTunnelGroupSerializer()
encapsulation = ChoiceField(
choices=TunnelEncapsulationChoices
)
Expand All @@ -48,7 +62,7 @@ class TunnelSerializer(NetBoxModelSerializer):
class Meta:
model = Tunnel
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',
)

Expand Down
1 change: 1 addition & 0 deletions netbox/vpn/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
router.register('ipsec-policies', views.IPSecPolicyViewSet)
router.register('ipsec-proposals', views.IPSecProposalViewSet)
router.register('ipsec-profiles', views.IPSecProfileViewSet)
router.register('tunnel-groups', views.TunnelGroupViewSet)
router.register('tunnels', views.TunnelViewSet)
router.register('tunnel-terminations', views.TunnelTerminationViewSet)
router.register('l2vpns', views.L2VPNViewSet)
Expand Down
9 changes: 9 additions & 0 deletions netbox/vpn/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
'IPSecProposalViewSet',
'L2VPNViewSet',
'L2VPNTerminationViewSet',
'TunnelGroupViewSet',
'TunnelTerminationViewSet',
'TunnelViewSet',
'VPNRootView',
Expand All @@ -32,6 +33,14 @@ def get_view_name(self):
# 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):
queryset = Tunnel.objects.prefetch_related('ipsec_profile', 'tenant').annotate(
terminations_count=count_related(TunnelTermination, 'tunnel')
Expand Down
20 changes: 19 additions & 1 deletion netbox/vpn/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from dcim.models import Device, Interface
from ipam.models import IPAddress, RouteTarget, VLAN
from netbox.filtersets import NetBoxModelFilterSet
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
from virtualization.models import VirtualMachine, VMInterface
Expand All @@ -20,14 +20,32 @@
'L2VPNFilterSet',
'L2VPNTerminationFilterSet',
'TunnelFilterSet',
'TunnelGroupFilterSet',
'TunnelTerminationFilterSet',
)


class TunnelGroupFilterSet(OrganizationalModelFilterSet):

class Meta:
model = TunnelGroup
fields = ['id', 'name', 'slug', 'description']


class TunnelFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
status = django_filters.MultipleChoiceFilter(
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(
choices=TunnelEncapsulationChoices
)
Expand Down
21 changes: 19 additions & 2 deletions netbox/vpn/forms/bulk_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,33 @@
'L2VPNBulkEditForm',
'L2VPNTerminationBulkEditForm',
'TunnelBulkEditForm',
'TunnelGroupBulkEditForm',
'TunnelTerminationBulkEditForm',
)


class TunnelGroupBulkEditForm(NetBoxModelBulkEditForm):
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)

model = TunnelGroup
nullable_fields = ('description',)


class TunnelBulkEditForm(NetBoxModelBulkEditForm):
status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(TunnelStatusChoices),
required=False
)
group = DynamicModelChoiceField(
queryset=TunnelGroup.objects.all(),
label=_('Tunnel group'),
required=False
)
encapsulation = forms.ChoiceField(
label=_('Encapsulation'),
choices=add_blank_choice(TunnelEncapsulationChoices),
Expand Down Expand Up @@ -55,12 +72,12 @@ class TunnelBulkEditForm(NetBoxModelBulkEditForm):

model = Tunnel
fieldsets = (
(_('Tunnel'), ('status', 'encapsulation', 'tunnel_id', 'description')),
(_('Tunnel'), ('status', 'group', 'encapsulation', 'tunnel_id', 'description')),
(_('Security'), ('ipsec_profile',)),
(_('Tenancy'), ('tenant',)),
)
nullable_fields = (
'ipsec_profile', 'tunnel_id', 'tenant', 'description', 'comments',
'group', 'ipsec_profile', 'tunnel_id', 'tenant', 'description', 'comments',
)


Expand Down
21 changes: 18 additions & 3 deletions netbox/vpn/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from ipam.models import IPAddress, VLAN
from netbox.forms import NetBoxModelImportForm
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 vpn.choices import *
from vpn.models import *
Expand All @@ -19,16 +19,31 @@
'L2VPNImportForm',
'L2VPNTerminationImportForm',
'TunnelImportForm',
'TunnelGroupImportForm',
'TunnelTerminationImportForm',
)


class TunnelGroupImportForm(NetBoxModelImportForm):
slug = SlugField()

class Meta:
model = TunnelGroup
fields = ('name', 'slug', 'description', 'tags')


class TunnelImportForm(NetBoxModelImportForm):
status = CSVChoiceField(
label=_('Status'),
choices=TunnelStatusChoices,
help_text=_('Operational status')
)
group = CSVModelChoiceField(
label=_('Tunnel group'),
queryset=TunnelGroup.objects.all(),
required=False,
to_field_name='name'
)
encapsulation = CSVChoiceField(
label=_('Encapsulation'),
choices=TunnelEncapsulationChoices,
Expand All @@ -51,8 +66,8 @@ class TunnelImportForm(NetBoxModelImportForm):
class Meta:
model = Tunnel
fields = (
'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id', 'description', 'comments',
'tags',
'name', 'status', 'group', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id', 'description',
'comments', 'tags',
)


Expand Down
Loading

0 comments on commit 8db1093

Please sign in to comment.