mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-30 20:36:26 -06:00
Draft code for Wireguard support
This commit is contained in:
parent
9f7743e5da
commit
351ab1ecb0
@ -577,6 +577,10 @@ class BaseInterface(models.Model):
|
||||
def count_fhrp_groups(self):
|
||||
return self.fhrp_group_assignments.count()
|
||||
|
||||
@property
|
||||
def wireguard_config(self):
|
||||
return self.wireguard_configs.first()
|
||||
|
||||
|
||||
class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEndpoint, TrackingModelMixin):
|
||||
"""
|
||||
@ -734,6 +738,12 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
||||
object_id_field='assigned_object_id',
|
||||
related_query_name='interface',
|
||||
)
|
||||
wireguard_configs = GenericRelation(
|
||||
to='vpn.WireguardConfig',
|
||||
content_type_field='tunnel_interface_type',
|
||||
object_id_field='tunnel_interface_id',
|
||||
related_query_name='interface'
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'device', 'module', 'parent', 'bridge', 'lag', 'type', 'mgmt_only', 'mtu', 'mode', 'speed', 'duplex', 'rf_role',
|
||||
|
@ -234,6 +234,7 @@ VPN_MENU = Menu(
|
||||
get_model_item('vpn', 'ipsecproposal', _('IPSec Proposals')),
|
||||
get_model_item('vpn', 'ipsecpolicy', _('IPSec Policies')),
|
||||
get_model_item('vpn', 'ipsecprofile', _('IPSec Profiles')),
|
||||
get_model_item('vpn', 'wireguardconfig', _('Wireguard Configs')),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -37,10 +37,12 @@
|
||||
<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>
|
||||
{% if not object.is_wireguard %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "IPSec profile" %}</th>
|
||||
<td>{{ object.ipsec_profile|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tunnel ID" %}</th>
|
||||
<td>{{ object.tunnel_id|placeholder }}</td>
|
||||
|
32
netbox/templates/vpn/wireguardconfig.html
Normal file
32
netbox/templates/vpn/wireguardconfig.html
Normal file
@ -0,0 +1,32 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{# TODO: Fix this template.. #}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Tunnel" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Listen port" %}</th>
|
||||
<td>{{ object.listen_port }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Allowed ips" %}</th>
|
||||
<td>{{ object.allowed_ips }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</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>
|
||||
{% endblock %}
|
@ -372,6 +372,12 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
|
||||
object_id_field='assigned_object_id',
|
||||
related_query_name='vminterface',
|
||||
)
|
||||
wireguard_configs = GenericRelation(
|
||||
to='vpn.WireguardConfig',
|
||||
content_type_field='tunnel_interface_type',
|
||||
object_id_field='tunnel_interface_id',
|
||||
related_query_name='vminterface',
|
||||
)
|
||||
|
||||
class Meta(ComponentModel.Meta):
|
||||
verbose_name = _('interface')
|
||||
|
@ -1,7 +1,8 @@
|
||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from vpn.choices import *
|
||||
from vpn.models import IKEPolicy, IKEProposal, IPSecPolicy, IPSecProfile, IPSecProposal
|
||||
from vpn.models import IKEPolicy, IKEProposal, IPSecPolicy, IPSecProfile, IPSecProposal, \
|
||||
WireguardConfig
|
||||
|
||||
__all__ = (
|
||||
'IKEPolicySerializer',
|
||||
@ -9,6 +10,7 @@ __all__ = (
|
||||
'IPSecPolicySerializer',
|
||||
'IPSecProfileSerializer',
|
||||
'IPSecProposalSerializer',
|
||||
'WireguardConfigSerializer',
|
||||
)
|
||||
|
||||
|
||||
@ -119,3 +121,11 @@ class IPSecProfileSerializer(NetBoxModelSerializer):
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
)
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
# TODO: Fix missing stuff
|
||||
class WireguardConfigSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = WireguardConfig
|
||||
fields = ('id', 'tunnel_interface_type')
|
||||
brief_fields = ('id', 'tunnel_interface_type')
|
||||
|
@ -26,12 +26,14 @@ class TunnelEncapsulationChoices(ChoiceSet):
|
||||
ENCAP_IP_IP = 'ip-ip'
|
||||
ENCAP_IPSEC_TRANSPORT = 'ipsec-transport'
|
||||
ENCAP_IPSEC_TUNNEL = 'ipsec-tunnel'
|
||||
ENCAP_WIREGUARD = 'wireguard'
|
||||
|
||||
CHOICES = [
|
||||
(ENCAP_IPSEC_TRANSPORT, _('IPsec - Transport')),
|
||||
(ENCAP_IPSEC_TUNNEL, _('IPsec - Tunnel')),
|
||||
(ENCAP_IP_IP, _('IP-in-IP')),
|
||||
(ENCAP_GRE, _('GRE')),
|
||||
(ENCAP_WIREGUARD, _('Wireguard')),
|
||||
]
|
||||
|
||||
|
||||
|
@ -1,9 +1,13 @@
|
||||
from random import choices
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.choices import InterfaceTypeChoices
|
||||
from dcim.models import Device, Interface
|
||||
from ipam.models import IPAddress, RouteTarget, VLAN
|
||||
from netbox.api.fields import ChoiceField
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
||||
@ -20,6 +24,7 @@ __all__ = (
|
||||
'IPSecPolicyForm',
|
||||
'IPSecProfileForm',
|
||||
'IPSecProposalForm',
|
||||
'WireguardConfigForm',
|
||||
'L2VPNForm',
|
||||
'L2VPNTerminationForm',
|
||||
'TunnelCreateForm',
|
||||
@ -57,8 +62,7 @@ class TunnelForm(TenancyForm, NetBoxModelForm):
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags', name=_('Tunnel')),
|
||||
FieldSet('ipsec_profile', name=_('Security')),
|
||||
FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'tags', name=_('Tunnel')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
|
||||
@ -68,6 +72,15 @@ class TunnelForm(TenancyForm, NetBoxModelForm):
|
||||
'name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'tenant_group',
|
||||
'tenant', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'encapsulation': HTMXSelect(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, initial=None, **kwargs):
|
||||
super().__init__(*args, initial=initial, **kwargs)
|
||||
|
||||
if get_field_value(self, 'encapsulation') == TunnelEncapsulationChoices.ENCAP_WIREGUARD:
|
||||
del(self.fields['ipsec_profile'])
|
||||
|
||||
|
||||
class TunnelCreateForm(TunnelForm):
|
||||
@ -142,8 +155,7 @@ class TunnelCreateForm(TunnelForm):
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags', name=_('Tunnel')),
|
||||
FieldSet('ipsec_profile', name=_('Security')),
|
||||
FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'tags', name=_('Tunnel')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
FieldSet(
|
||||
'termination1_role', 'termination1_type', 'termination1_parent', 'termination1_termination',
|
||||
@ -264,12 +276,23 @@ class TunnelTerminationForm(NetBoxModelForm):
|
||||
def __init__(self, *args, initial=None, **kwargs):
|
||||
super().__init__(*args, initial=initial, **kwargs)
|
||||
|
||||
if (get_field_value(self, 'type') is None and
|
||||
self.instance.pk and isinstance(self.instance.termination.parent_object, VirtualMachine)):
|
||||
# Mimic HTMXSelect()
|
||||
self.fields['tunnel'].widget.attrs.update({
|
||||
'hx-get': '.',
|
||||
'hx-include': '#form_fields',
|
||||
'hx-target': '#form_fields',
|
||||
})
|
||||
|
||||
tunnel_id = get_field_value(self, 'tunnel')
|
||||
tunnel = Tunnel.objects.filter(id=tunnel_id).first() if tunnel_id else None
|
||||
|
||||
tt_type = get_field_value(self, 'type')
|
||||
|
||||
if tt_type is None and self.instance.pk and isinstance(self.instance.termination.parent_object, VirtualMachine):
|
||||
self.fields['type'].initial = TunnelTerminationTypeChoices.TYPE_VIRTUALMACHINE
|
||||
|
||||
# If initial or self.data is set and the type is a VIRTUALMACHINE type, swap the field querysets.
|
||||
if get_field_value(self, 'type') == TunnelTerminationTypeChoices.TYPE_VIRTUALMACHINE:
|
||||
if tt_type == TunnelTerminationTypeChoices.TYPE_VIRTUALMACHINE:
|
||||
self.fields['parent'].label = _('Virtual Machine')
|
||||
self.fields['parent'].queryset = VirtualMachine.objects.all()
|
||||
self.fields['parent'].widget.attrs['selector'] = 'virtualization.virtualmachine'
|
||||
@ -280,6 +303,10 @@ class TunnelTerminationForm(NetBoxModelForm):
|
||||
self.fields['outside_ip'].widget.add_query_params({
|
||||
'virtual_machine_id': '$parent',
|
||||
})
|
||||
elif tunnel and tunnel.is_wireguard:
|
||||
self.fields['termination'].help_text = _('As this is a Wireguard tunnel, only virtual interfaces are available for selection')
|
||||
if tt_type == TunnelTerminationTypeChoices.TYPE_VIRTUALMACHINE:
|
||||
self.fields['termination'].widget.add_query_params({'type': InterfaceTypeChoices.TYPE_VIRTUAL})
|
||||
|
||||
if self.instance.pk:
|
||||
self.fields['parent'].initial = self.instance.termination.parent_object
|
||||
@ -287,9 +314,15 @@ class TunnelTerminationForm(NetBoxModelForm):
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
termination = self.cleaned_data['termination']
|
||||
|
||||
# verify that interface is virtual
|
||||
is_virtual_interface = termination.type == InterfaceTypeChoices.TYPE_VIRTUAL if self.cleaned_data['type'] == TunnelTerminationTypeChoices.TYPE_DEVICE else True
|
||||
if self.cleaned_data['tunnel'].encapsulation == TunnelEncapsulationChoices.ENCAP_WIREGUARD and not is_virtual_interface:
|
||||
raise forms.ValidationError(_('Interface must be virtual for Wireguard tunnels'))
|
||||
|
||||
# Set the terminated object
|
||||
self.instance.termination = self.cleaned_data.get('termination')
|
||||
self.instance.termination = termination
|
||||
|
||||
|
||||
class IKEProposalForm(NetBoxModelForm):
|
||||
@ -387,6 +420,71 @@ class IPSecProfileForm(NetBoxModelForm):
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Wireguard Config
|
||||
#
|
||||
|
||||
|
||||
class WireguardConfigForm(NetBoxModelForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=TunnelTerminationTypeChoices,
|
||||
widget=HTMXSelect(),
|
||||
label=_('Type')
|
||||
)
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
selector=True,
|
||||
label=_('Device')
|
||||
)
|
||||
tunnel_interface = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.filter(type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||
label=_('Tunnel interface'),
|
||||
query_params={
|
||||
'device_id': '$parent',
|
||||
'type': InterfaceTypeChoices.TYPE_VIRTUAL,
|
||||
},
|
||||
help_text=_('Only virtual interfaces are shown'),
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('type', 'parent', 'tunnel_interface', 'tags', name=_('Wireguard config')),
|
||||
FieldSet('private_key', 'public_key', name=_('Keys')),
|
||||
FieldSet('listen_port', 'allowed_ips', 'fwmark', 'persistent_keepalive_interval', name=_('Parameters')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = WireguardConfig
|
||||
fields = [
|
||||
'type', 'parent', 'tunnel_interface', 'tags', 'private_key', 'public_key', 'listen_port', 'allowed_ips', 'fwmark', 'persistent_keepalive_interval',
|
||||
]
|
||||
|
||||
def __init__(self, *args, initial=None, **kwargs):
|
||||
super().__init__(*args, initial=initial, **kwargs)
|
||||
|
||||
if (get_field_value(self, 'type') is None and
|
||||
self.instance.pk and isinstance(self.instance.tunnel_interface.parent_object, VirtualMachine)):
|
||||
self.fields['type'].initial = TunnelTerminationTypeChoices.TYPE_VIRTUALMACHINE
|
||||
|
||||
# If initial or self.data is set and the type is a VIRTUALMACHINE type, swap the field querysets.
|
||||
if get_field_value(self, 'type') == TunnelTerminationTypeChoices.TYPE_VIRTUALMACHINE:
|
||||
self.fields['parent'].label = _('Virtual Machine')
|
||||
self.fields['parent'].queryset = VirtualMachine.objects.all()
|
||||
self.fields['parent'].widget.attrs['selector'] = 'virtualization.virtualmachine'
|
||||
self.fields['tunnel_interface'].queryset = VMInterface.objects.all()
|
||||
self.fields['tunnel_interface'].widget.add_query_params({
|
||||
'virtual_machine_id': '$parent',
|
||||
})
|
||||
|
||||
if self.instance.pk:
|
||||
self.fields['parent'].initial = self.instance.tunnel_interface.parent_object
|
||||
self.fields['tunnel_interface'].initial = self.instance.tunnel_interface
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Set the tunnel_interface object
|
||||
self.instance.tunnel_interface = self.cleaned_data.get('tunnel_interface')
|
||||
|
||||
#
|
||||
# L2VPN
|
||||
#
|
||||
|
49
netbox/vpn/migrations/0006_wireguardconfig_and_more.py
Normal file
49
netbox/vpn/migrations/0006_wireguardconfig_and_more.py
Normal file
@ -0,0 +1,49 @@
|
||||
# Generated by Django 5.0.7 on 2024-10-15 11:15
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import ipam.fields
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('extras', '0121_customfield_related_object_filter'),
|
||||
('vpn', '0005_rename_indexes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WireguardConfig',
|
||||
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)),
|
||||
('tunnel_interface_id', models.PositiveBigIntegerField(blank=True, null=True)),
|
||||
('private_key', models.TextField(blank=True)),
|
||||
('public_key', models.TextField(blank=True)),
|
||||
('listen_port', models.PositiveIntegerField(default=51820, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)])),
|
||||
('allowed_ips', django.contrib.postgres.fields.ArrayField(base_field=ipam.fields.IPNetworkField(), blank=True, null=True, size=None)),
|
||||
('fwmark', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('persistent_keepalive_interval', models.PositiveIntegerField(default=0)),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
('tunnel_interface_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'wireguard config',
|
||||
'verbose_name_plural': 'wireguard configs',
|
||||
'ordering': ('pk',),
|
||||
'indexes': [models.Index(fields=['tunnel_interface_type', 'tunnel_interface_id'], name='vpn_wiregua_tunnel__5cdbbe_idx')],
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='wireguardconfig',
|
||||
constraint=models.UniqueConstraint(fields=('tunnel_interface_type', 'tunnel_interface_id'), name='vpn_wireguardconfig_tunnel_interface', violation_error_message='An tunnel_interface may only have one wireguard configration.'),
|
||||
),
|
||||
]
|
@ -1,9 +1,15 @@
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.models import PrimaryModel
|
||||
from ipam.constants import SERVICE_PORT_MIN, SERVICE_PORT_MAX
|
||||
from ipam.fields import IPNetworkField
|
||||
from netbox.models import PrimaryModel, CustomFieldsMixin, CustomLinksMixin, TagsMixin, \
|
||||
ChangeLoggedModel
|
||||
from vpn.choices import *
|
||||
|
||||
__all__ = (
|
||||
@ -12,9 +18,13 @@ __all__ = (
|
||||
'IPSecPolicy',
|
||||
'IPSecProfile',
|
||||
'IPSecProposal',
|
||||
'WireguardConfig',
|
||||
)
|
||||
|
||||
|
||||
WIREGUARD_DEFAULT_PORT = 51820
|
||||
|
||||
|
||||
#
|
||||
# IKE
|
||||
#
|
||||
@ -255,3 +265,94 @@ class IPSecProfile(PrimaryModel):
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('vpn:ipsecprofile', args=[self.pk])
|
||||
|
||||
|
||||
class WireguardConfig(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLoggedModel):
|
||||
tunnel_interface_type = models.ForeignKey(
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+'
|
||||
)
|
||||
tunnel_interface_id = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
tunnel_interface = GenericForeignKey(
|
||||
ct_field='tunnel_interface_type',
|
||||
fk_field='tunnel_interface_id'
|
||||
)
|
||||
private_key = models.TextField(
|
||||
verbose_name=_('private key'),
|
||||
blank=True,
|
||||
)
|
||||
public_key = models.TextField(
|
||||
verbose_name=_('public key'),
|
||||
blank=True
|
||||
)
|
||||
listen_port = models.PositiveIntegerField(
|
||||
verbose_name=_('listen port'),
|
||||
default=WIREGUARD_DEFAULT_PORT,
|
||||
validators=[
|
||||
MinValueValidator(SERVICE_PORT_MIN),
|
||||
MaxValueValidator(SERVICE_PORT_MAX)
|
||||
]
|
||||
)
|
||||
allowed_ips = ArrayField(
|
||||
base_field=IPNetworkField(),
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('allowed ips'),
|
||||
help_text=_(
|
||||
"Represents the permissible IPv4/IPv6 networks for use by other peers in their "
|
||||
"'allowed_ips' configuration while creating a tunnel with this peer. "
|
||||
"Ex: '10.1.1.0/24, 192.168.10.16/32, 2001:DB8:1::/64'"
|
||||
),
|
||||
)
|
||||
fwmark = models.PositiveIntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_('fwmark'),
|
||||
help_text=_('Optional. Set a 32-bit integer firewall mark (fwmark) for outgoing packets'),
|
||||
)
|
||||
persistent_keepalive_interval = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_('persistent keepalive interval'),
|
||||
help_text=_('Persistant keepalive interval in seconds, 0 disables this feature'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
indexes = (
|
||||
models.Index(fields=('tunnel_interface_type', 'tunnel_interface_id')),
|
||||
)
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('tunnel_interface_type', 'tunnel_interface_id'),
|
||||
name='%(app_label)s_%(class)s_tunnel_interface',
|
||||
violation_error_message=_("An tunnel_interface may only have one wireguard configration.")
|
||||
),
|
||||
)
|
||||
verbose_name = _('wireguard config')
|
||||
verbose_name_plural = _('wireguard configs')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.tunnel_interface.name}: Wireguard config'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('vpn:wireguardconfig', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check that the selected termination object is not already
|
||||
if getattr(self.tunnel_interface, 'wireguard_config', None) and self.tunnel_interface.wireguard_config.pk != self.pk:
|
||||
raise ValidationError({
|
||||
'tunnel_interface': _("{name} already has a Wireguard config").format(
|
||||
name=self.tunnel_interface.name,
|
||||
)
|
||||
})
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
objectchange.related_object = self.tunnel_interface
|
||||
return objectchange
|
||||
|
@ -103,6 +103,10 @@ class Tunnel(PrimaryModel):
|
||||
def get_status_color(self):
|
||||
return TunnelStatusChoices.colors.get(self.status)
|
||||
|
||||
@property
|
||||
def is_wireguard(self):
|
||||
return self.encapsulation == TunnelEncapsulationChoices.ENCAP_WIREGUARD
|
||||
|
||||
|
||||
class TunnelTermination(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLoggedModel):
|
||||
tunnel = models.ForeignKey(
|
||||
|
@ -10,6 +10,7 @@ __all__ = (
|
||||
'IPSecPolicyTable',
|
||||
'IPSecProposalTable',
|
||||
'IPSecProfileTable',
|
||||
'WireguardConfigTable',
|
||||
)
|
||||
|
||||
|
||||
@ -183,3 +184,36 @@ class IPSecProfileTable(NetBoxTable):
|
||||
'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'mode', 'ike_policy', 'ipsec_policy', 'description')
|
||||
|
||||
|
||||
# TODO: Fix..
|
||||
class WireguardConfigTable(NetBoxTable):
|
||||
tunnel_interface_parent = tables.Column(
|
||||
accessor='tunnel_interface__parent_object',
|
||||
linkify=True,
|
||||
orderable=False,
|
||||
verbose_name=_('Host')
|
||||
)
|
||||
tunnel_interface = tables.Column(
|
||||
verbose_name=_('Tunnel interface'),
|
||||
linkify=True
|
||||
)
|
||||
ip_addresses = columns.ManyToManyColumn(
|
||||
accessor=tables.A('tunnel_interface__ip_addresses'),
|
||||
orderable=False,
|
||||
linkify_item=True,
|
||||
verbose_name=_('IP Addresses')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='vpn:tunneltermination_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = WireguardConfig
|
||||
fields = (
|
||||
'pk', 'id', 'tunnel', 'role', 'tunnel_interface_parent', 'tunnel_interface', 'ip_addresses', 'tags',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'id', 'tunnel_interface_parent', 'tunnel_interface', 'ip_addresses',
|
||||
)
|
||||
|
@ -70,6 +70,14 @@ urlpatterns = [
|
||||
path('ipsec-profiles/delete/', views.IPSecProfileBulkDeleteView.as_view(), name='ipsecprofile_bulk_delete'),
|
||||
path('ipsec-profiles/<int:pk>/', include(get_model_urls('vpn', 'ipsecprofile'))),
|
||||
|
||||
# Wireguard configs
|
||||
path('wireguard-configs/', views.WireguardConfigListView.as_view(), name='wireguardconfig_list'),
|
||||
path('wireguard-configs/add/', views.WireguardConfigEditView.as_view(), name='wireguardconfig_add'),
|
||||
path('wireguard-configs/import/', views.WireguardConfigBulkImportView.as_view(), name='wireguardconfig_import'),
|
||||
path('wireguard-configs/edit/', views.WireguardConfigBulkEditView.as_view(), name='wireguardconfig_bulk_edit'),
|
||||
path('wireguard-configs/delete/', views.WireguardConfigBulkDeleteView.as_view(), name='wireguardconfig_bulk_delete'),
|
||||
path('wireguard-configs/<int:pk>/', include(get_model_urls('vpn', 'wireguardconfig'))),
|
||||
|
||||
# L2VPN
|
||||
path('l2vpns/', views.L2VPNListView.as_view(), name='l2vpn_list'),
|
||||
path('l2vpns/add/', views.L2VPNEditView.as_view(), name='l2vpn_add'),
|
||||
|
@ -392,6 +392,56 @@ class IPSecProfileBulkDeleteView(generic.BulkDeleteView):
|
||||
table = tables.IPSecProfileTable
|
||||
|
||||
|
||||
#
|
||||
# Wireguard configs
|
||||
#
|
||||
|
||||
|
||||
# TODO: Fix..
|
||||
class WireguardConfigListView(generic.ObjectListView):
|
||||
queryset = WireguardConfig.objects.all()
|
||||
# filterset = filtersets.WireguardConfigFilterSet
|
||||
# filterset_form = forms.WireguardConfigFilterForm
|
||||
table = tables.WireguardConfigTable
|
||||
|
||||
|
||||
@register_model_view(WireguardConfig)
|
||||
class WireguardConfigView(generic.ObjectView):
|
||||
queryset = WireguardConfig.objects.all()
|
||||
|
||||
|
||||
@register_model_view(WireguardConfig, 'edit')
|
||||
class WireguardConfigEditView(generic.ObjectEditView):
|
||||
queryset = WireguardConfig.objects.all()
|
||||
form = forms.WireguardConfigForm
|
||||
|
||||
|
||||
@register_model_view(WireguardConfig, 'delete')
|
||||
class WireguardConfigDeleteView(generic.ObjectDeleteView):
|
||||
queryset = WireguardConfig.objects.all()
|
||||
|
||||
|
||||
# TODO: Fix..
|
||||
class WireguardConfigBulkImportView(generic.BulkImportView):
|
||||
queryset = WireguardConfig.objects.all()
|
||||
# model_form = forms.WireguardConfigImportForm
|
||||
|
||||
|
||||
# TODO: Fix..
|
||||
class WireguardConfigBulkEditView(generic.BulkEditView):
|
||||
queryset = WireguardConfig.objects.all()
|
||||
# filterset = filtersets.WireguardConfigFilterSet
|
||||
table = tables.WireguardConfigTable
|
||||
# form = forms.WireguardConfigBulkEditForm
|
||||
|
||||
|
||||
# TODO: Fix..
|
||||
class WireguardConfigBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = WireguardConfig.objects.all()
|
||||
# filterset = filtersets.WireguardConfigFilterSet
|
||||
table = tables.WireguardConfigTable
|
||||
|
||||
|
||||
# L2VPN
|
||||
|
||||
class L2VPNListView(generic.ObjectListView):
|
||||
|
Loading…
Reference in New Issue
Block a user