Skip to content

Commit 8db1093

Browse files
committed
#9816: Add TunnelGroup
1 parent 9f1283f commit 8db1093

26 files changed

+600
-91
lines changed

docs/features/vpn-tunnels.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Tunnels
22

3-
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.
3+
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.
44

55
```mermaid
66
flowchart TD

docs/models/vpn/tunnel.md

+7-5
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ A unique name assigned to the tunnel for identification.
1414

1515
The operational status of the tunnel. By default, the following statuses are available:
1616

17-
| Name |
18-
|----------------|
19-
| Planned |
20-
| Active |
21-
| Disabled |
17+
* Planned
18+
* Active
19+
* Disabled
2220

2321
!!! tip "Custom tunnel statuses"
2422
Additional tunnel statuses may be defined by setting `Tunnel.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
2523

24+
### Group
25+
26+
The [administrative group](./tunnelgroup.md) to which this tunnel is assigned (optional).
27+
2628
### Encapsulation
2729

2830
The encapsulation protocol or technique employed to effect the tunnel. NetBox supports GRE, IP-in-IP, and IPSec encapsulations.

docs/models/vpn/tunnelgroup.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Tunnel Group
2+
3+
[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.
4+
5+
## Fields
6+
7+
### Name
8+
9+
A unique human-friendly name.
10+
11+
### Slug
12+
13+
A unique URL-friendly identifier. (This value can be used for filtering.)

netbox/dcim/tables/template_code.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@
361361
{% endif %}
362362
{% elif record.type == 'virtual' %}
363363
{% if perms.vpn.add_tunnel and not record.tunnel_termination %}
364-
<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">
364+
<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">
365365
<i class="mdi mdi-tunnel-outline" aria-hidden="true"></i>
366366
</a>
367367
{% elif perms.vpn.delete_tunneltermination and record.tunnel_termination %}

netbox/netbox/navigation/menu.py

+1
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@
203203
label=_('Tunnels'),
204204
items=(
205205
get_model_item('vpn', 'tunnel', _('Tunnels')),
206+
get_model_item('vpn', 'tunnelgroup', _('Tunnel Groups')),
206207
get_model_item('vpn', 'tunneltermination', _('Tunnel Terminations')),
207208
),
208209
),

netbox/templates/vpn/tunnel.html

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ <h5 class="card-header">{% trans "Tunnel" %}</h5>
2626
<th scope="row">{% trans "Status" %}</th>
2727
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
2828
</tr>
29+
<tr>
30+
<th scope="row">{% trans "Group" %}</th>
31+
<td>{{ object.group|linkify|placeholder }}</td>
32+
</tr>
2933
<tr>
3034
<th scope="row">{% trans "Description" %}</th>
3135
<td>{{ object.description|placeholder }}</td>

netbox/templates/vpn/tunnelgroup.html

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{% extends 'generic/object.html' %}
2+
{% load helpers %}
3+
{% load plugins %}
4+
{% load render_table from django_tables2 %}
5+
{% load i18n %}
6+
7+
{% block breadcrumbs %}
8+
<li class="breadcrumb-item"><a href="{% url 'vpn:tunnelgroup_list' %}">{% trans "Tunnel Groups" %}</a></li>
9+
{% endblock %}
10+
11+
{% block extra_controls %}
12+
{% if perms.vpn.add_tunnel %}
13+
<a href="{% url 'vpn:tunnel_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
14+
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Tunnel" %}
15+
</a>
16+
{% endif %}
17+
{% endblock extra_controls %}
18+
19+
{% block content %}
20+
<div class="row mb-3">
21+
<div class="col col-md-6">
22+
<div class="card">
23+
<h5 class="card-header">
24+
{% trans "Tunnel Group" %}
25+
</h5>
26+
<div class="card-body">
27+
<table class="table table-hover attr-table">
28+
<tr>
29+
<th scope="row">{% trans "Name" %}</th>
30+
<td>{{ object.name }}</td>
31+
</tr>
32+
<tr>
33+
<th scope="row">{% trans "Description" %}</th>
34+
<td>{{ object.description|placeholder }}</td>
35+
</tr>
36+
</table>
37+
</div>
38+
</div>
39+
{% include 'inc/panels/tags.html' %}
40+
{% plugin_left_page object %}
41+
</div>
42+
<div class="col col-md-6">
43+
{% include 'inc/panels/related_objects.html' %}
44+
{% include 'inc/panels/custom_fields.html' %}
45+
{% plugin_right_page object %}
46+
</div>
47+
</div>
48+
<div class="row mb-3">
49+
<div class="col col-md-12">
50+
{% plugin_full_width_page object %}
51+
</div>
52+
</div>
53+
{% endblock %}

netbox/vpn/api/nested_serializers.py

+14
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from drf_spectacular.utils import extend_schema_serializer
12
from rest_framework import serializers
23

34
from netbox.api.serializers import WritableNestedSerializer
@@ -11,11 +12,24 @@
1112
'NestedIPSecProposalSerializer',
1213
'NestedL2VPNSerializer',
1314
'NestedL2VPNTerminationSerializer',
15+
'NestedTunnelGroupSerializer',
1416
'NestedTunnelSerializer',
1517
'NestedTunnelTerminationSerializer',
1618
)
1719

1820

21+
@extend_schema_serializer(
22+
exclude_fields=('tunnel_count',),
23+
)
24+
class NestedTunnelGroupSerializer(WritableNestedSerializer):
25+
url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
26+
tunnel_count = serializers.IntegerField(read_only=True)
27+
28+
class Meta:
29+
model = models.TunnelGroup
30+
fields = ['id', 'url', 'display', 'name', 'slug', 'tunnel_count']
31+
32+
1933
class NestedTunnelSerializer(WritableNestedSerializer):
2034
url = serializers.HyperlinkedIdentityField(
2135
view_name='vpn-api:tunnel-detail'

netbox/vpn/api/serializers.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,32 @@
2121
'IPSecProposalSerializer',
2222
'L2VPNSerializer',
2323
'L2VPNTerminationSerializer',
24+
'TunnelGroupSerializer',
2425
'TunnelSerializer',
2526
'TunnelTerminationSerializer',
2627
)
2728

2829

30+
class TunnelGroupSerializer(NetBoxModelSerializer):
31+
url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
32+
tunnel_count = serializers.IntegerField(read_only=True)
33+
34+
class Meta:
35+
model = TunnelGroup
36+
fields = [
37+
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
38+
'tunnel_count',
39+
]
40+
41+
2942
class TunnelSerializer(NetBoxModelSerializer):
3043
url = serializers.HyperlinkedIdentityField(
3144
view_name='vpn-api:tunnel-detail'
3245
)
3346
status = ChoiceField(
3447
choices=TunnelStatusChoices
3548
)
49+
group = NestedTunnelGroupSerializer()
3650
encapsulation = ChoiceField(
3751
choices=TunnelEncapsulationChoices
3852
)
@@ -48,7 +62,7 @@ class TunnelSerializer(NetBoxModelSerializer):
4862
class Meta:
4963
model = Tunnel
5064
fields = (
51-
'id', 'url', 'display', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id',
65+
'id', 'url', 'display', 'name', 'status', 'group', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id',
5266
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
5367
)
5468

netbox/vpn/api/urls.py

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
router.register('ipsec-policies', views.IPSecPolicyViewSet)
99
router.register('ipsec-proposals', views.IPSecProposalViewSet)
1010
router.register('ipsec-profiles', views.IPSecProfileViewSet)
11+
router.register('tunnel-groups', views.TunnelGroupViewSet)
1112
router.register('tunnels', views.TunnelViewSet)
1213
router.register('tunnel-terminations', views.TunnelTerminationViewSet)
1314
router.register('l2vpns', views.L2VPNViewSet)

netbox/vpn/api/views.py

+9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
'IPSecProposalViewSet',
1515
'L2VPNViewSet',
1616
'L2VPNTerminationViewSet',
17+
'TunnelGroupViewSet',
1718
'TunnelTerminationViewSet',
1819
'TunnelViewSet',
1920
'VPNRootView',
@@ -32,6 +33,14 @@ def get_view_name(self):
3233
# Viewsets
3334
#
3435

36+
class TunnelGroupViewSet(NetBoxModelViewSet):
37+
queryset = TunnelGroup.objects.annotate(
38+
tunnel_count=count_related(Tunnel, 'group')
39+
)
40+
serializer_class = serializers.TunnelGroupSerializer
41+
filterset_class = filtersets.TunnelGroupFilterSet
42+
43+
3544
class TunnelViewSet(NetBoxModelViewSet):
3645
queryset = Tunnel.objects.prefetch_related('ipsec_profile', 'tenant').annotate(
3746
terminations_count=count_related(TunnelTermination, 'tunnel')

netbox/vpn/filtersets.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from dcim.models import Device, Interface
66
from ipam.models import IPAddress, RouteTarget, VLAN
7-
from netbox.filtersets import NetBoxModelFilterSet
7+
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
88
from tenancy.filtersets import TenancyFilterSet
99
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
1010
from virtualization.models import VirtualMachine, VMInterface
@@ -20,14 +20,32 @@
2020
'L2VPNFilterSet',
2121
'L2VPNTerminationFilterSet',
2222
'TunnelFilterSet',
23+
'TunnelGroupFilterSet',
2324
'TunnelTerminationFilterSet',
2425
)
2526

2627

28+
class TunnelGroupFilterSet(OrganizationalModelFilterSet):
29+
30+
class Meta:
31+
model = TunnelGroup
32+
fields = ['id', 'name', 'slug', 'description']
33+
34+
2735
class TunnelFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
2836
status = django_filters.MultipleChoiceFilter(
2937
choices=TunnelStatusChoices
3038
)
39+
group_id = django_filters.ModelMultipleChoiceFilter(
40+
queryset=TunnelGroup.objects.all(),
41+
label=_('Tunnel group (ID)'),
42+
)
43+
group = django_filters.ModelMultipleChoiceFilter(
44+
field_name='group__slug',
45+
queryset=TunnelGroup.objects.all(),
46+
to_field_name='slug',
47+
label=_('Tunnel group (slug)'),
48+
)
3149
encapsulation = django_filters.MultipleChoiceFilter(
3250
choices=TunnelEncapsulationChoices
3351
)

netbox/vpn/forms/bulk_edit.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,33 @@
1717
'L2VPNBulkEditForm',
1818
'L2VPNTerminationBulkEditForm',
1919
'TunnelBulkEditForm',
20+
'TunnelGroupBulkEditForm',
2021
'TunnelTerminationBulkEditForm',
2122
)
2223

2324

25+
class TunnelGroupBulkEditForm(NetBoxModelBulkEditForm):
26+
description = forms.CharField(
27+
label=_('Description'),
28+
max_length=200,
29+
required=False
30+
)
31+
32+
model = TunnelGroup
33+
nullable_fields = ('description',)
34+
35+
2436
class TunnelBulkEditForm(NetBoxModelBulkEditForm):
2537
status = forms.ChoiceField(
2638
label=_('Status'),
2739
choices=add_blank_choice(TunnelStatusChoices),
2840
required=False
2941
)
42+
group = DynamicModelChoiceField(
43+
queryset=TunnelGroup.objects.all(),
44+
label=_('Tunnel group'),
45+
required=False
46+
)
3047
encapsulation = forms.ChoiceField(
3148
label=_('Encapsulation'),
3249
choices=add_blank_choice(TunnelEncapsulationChoices),
@@ -55,12 +72,12 @@ class TunnelBulkEditForm(NetBoxModelBulkEditForm):
5572

5673
model = Tunnel
5774
fieldsets = (
58-
(_('Tunnel'), ('status', 'encapsulation', 'tunnel_id', 'description')),
75+
(_('Tunnel'), ('status', 'group', 'encapsulation', 'tunnel_id', 'description')),
5976
(_('Security'), ('ipsec_profile',)),
6077
(_('Tenancy'), ('tenant',)),
6178
)
6279
nullable_fields = (
63-
'ipsec_profile', 'tunnel_id', 'tenant', 'description', 'comments',
80+
'group', 'ipsec_profile', 'tunnel_id', 'tenant', 'description', 'comments',
6481
)
6582

6683

netbox/vpn/forms/bulk_import.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from ipam.models import IPAddress, VLAN
66
from netbox.forms import NetBoxModelImportForm
77
from tenancy.models import Tenant
8-
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField
8+
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField
99
from virtualization.models import VirtualMachine, VMInterface
1010
from vpn.choices import *
1111
from vpn.models import *
@@ -19,16 +19,31 @@
1919
'L2VPNImportForm',
2020
'L2VPNTerminationImportForm',
2121
'TunnelImportForm',
22+
'TunnelGroupImportForm',
2223
'TunnelTerminationImportForm',
2324
)
2425

2526

27+
class TunnelGroupImportForm(NetBoxModelImportForm):
28+
slug = SlugField()
29+
30+
class Meta:
31+
model = TunnelGroup
32+
fields = ('name', 'slug', 'description', 'tags')
33+
34+
2635
class TunnelImportForm(NetBoxModelImportForm):
2736
status = CSVChoiceField(
2837
label=_('Status'),
2938
choices=TunnelStatusChoices,
3039
help_text=_('Operational status')
3140
)
41+
group = CSVModelChoiceField(
42+
label=_('Tunnel group'),
43+
queryset=TunnelGroup.objects.all(),
44+
required=False,
45+
to_field_name='name'
46+
)
3247
encapsulation = CSVChoiceField(
3348
label=_('Encapsulation'),
3449
choices=TunnelEncapsulationChoices,
@@ -51,8 +66,8 @@ class TunnelImportForm(NetBoxModelImportForm):
5166
class Meta:
5267
model = Tunnel
5368
fields = (
54-
'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id', 'description', 'comments',
55-
'tags',
69+
'name', 'status', 'group', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id', 'description',
70+
'comments', 'tags',
5671
)
5772

5873

0 commit comments

Comments
 (0)