Merge branch 'feature' into 14132-event-refactor-2

This commit is contained in:
Jeremy Stretch 2023-11-28 08:51:59 -05:00
commit cd8eb7b92c
89 changed files with 6081 additions and 362 deletions

View File

@ -0,0 +1,49 @@
# 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.
```mermaid
flowchart TD
Termination1[TunnelTermination]
Termination2[TunnelTermination]
Interface1[Interface]
Interface2[Interface]
Tunnel --> Termination1 & Termination2
Termination1 --> Interface1
Termination2 --> Interface2
Interface1 --> Device
Interface2 --> VirtualMachine
click Tunnel "../../models/vpn/tunnel/"
click TunnelTermination1 "../../models/vpn/tunneltermination/"
click TunnelTermination2 "../../models/vpn/tunneltermination/"
```
# IPSec & IKE
NetBox includes robust support for modeling IPSec & IKE policies. These are used to define encryption and authentication parameters for IPSec tunnels.
```mermaid
flowchart TD
subgraph IKEProposals[Proposals]
IKEProposal1[IKEProposal]
IKEProposal2[IKEProposal]
end
subgraph IPSecProposals[Proposals]
IPSecProposal1[IPSecProposal]
IPSecProposal2[IPSecProposal]
end
IKEProposals --> IKEPolicy
IPSecProposals --> IPSecPolicy
IKEPolicy & IPSecPolicy--> IPSecProfile
IPSecProfile --> Tunnel
click IKEProposal1 "../../models/vpn/ikeproposal/"
click IKEProposal2 "../../models/vpn/ikeproposal/"
click IKEPolicy "../../models/vpn/ikepolicy/"
click IPSecProposal1 "../../models/vpn/ipsecproposal/"
click IPSecProposal2 "../../models/vpn/ipsecproposal/"
click IPSecPolicy "../../models/vpn/ipsecpolicy/"
click IPSecProfile "../../models/vpn/ipsecprofile/"
click Tunnel "../../models/vpn/tunnel/"
```

View File

@ -0,0 +1,25 @@
# IKE Policies
An [Internet Key Exhcnage (IKE)](https://en.wikipedia.org/wiki/Internet_Key_Exchange) policy defines an IKE version, mode, and set of [proposals](./ikeproposal.md) to be used in IKE negotiation. These policies are referenced by [IPSec profiles](./ipsecprofile.md).
## Fields
### Name
The unique user-assigned name for the policy.
### Version
The IKE version employed (v1 or v2).
### Mode
The IKE mode employed (main or aggressive).
### Proposals
One or more [IKE proposals](./ikeproposal.md) supported for use by this policy.
### Pre-shared Key
A pre-shared secret key associated with this policy (optional).

View File

@ -0,0 +1,39 @@
# IKE Proposals
An [Internet Key Exhcnage (IKE)](https://en.wikipedia.org/wiki/Internet_Key_Exchange) proposal defines a set of parameters used to establish a secure bidirectional connection across an untrusted medium, such as the Internet. IKE proposals defined in NetBox can be referenced by [IKE policies](./ikepolicy.md), which are in turn employed by [IPSec profiles](./ipsecprofile.md).
!!! note
Some platforms refer to IKE proposals as [ISAKMP](https://en.wikipedia.org/wiki/Internet_Security_Association_and_Key_Management_Protocol), which is a framework for authentication and key exchange which employs IKE.
## Fields
### Name
The unique user-assigned name for the proposal.
### Authentication Method
The strategy employed for authenticating the IKE peer. Available options are listed below.
| Name |
|----------------|
| Pre-shared key |
| Certificate |
| RSA signature |
| DSA signature |
### Encryption Algorithm
The protocol employed for data encryption. Options include DES, 3DES, and various flavors of AES.
### Authentication Algorithm
The mechanism employed to ensure data integrity. Options include MD5 and SHA HMAC implementations.
### Group
The [Diffie-Hellman group](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange) supported by the proposal. Group IDs are [managed by IANA](https://www.iana.org/assignments/ikev2-parameters/ikev2-parameters.xhtml#ikev2-parameters-8).
### SA Lifetime
The maximum lifetime for the IKE security association (SA), in seconds.

View File

@ -0,0 +1,17 @@
# IPSec Policy
An [IPSec](https://en.wikipedia.org/wiki/IPsec) policy defines a set of [proposals](./ikeproposal.md) to be used in the formation of IPSec tunnels. A perfect forward secrecy (PFS) group may optionally also be defined. These policies are referenced by [IPSec profiles](./ipsecprofile.md).
## Fields
### Name
The unique user-assigned name for the policy.
### Proposals
One or more [IPSec proposals](./ipsecproposal.md) supported for use by this policy.
### PFS Group
The [perfect forward secrecy (PFS)](https://en.wikipedia.org/wiki/Forward_secrecy) group supported by this policy (optional).

View File

@ -0,0 +1,21 @@
# IPSec Profile
An [IPSec](https://en.wikipedia.org/wiki/IPsec) profile defines an [IKE policy](./ikepolicy.md), [IPSec policy](./ipsecpolicy.md), and IPSec mode used for establishing an IPSec tunnel.
## Fields
### Name
The unique user-assigned name for the profile.
### Mode
The IPSec mode employed by the profile: Encapsulating Security Payload (ESP) or Authentication Header (AH).
### IKE Policy
The [IKE policy](./ikepolicy.md) associated with the profile.
### IPSec Policy
The [IPSec policy](./ipsecpolicy.md) associated with the profile.

View File

@ -0,0 +1,25 @@
# IPSec Proposal
An [IPSec](https://en.wikipedia.org/wiki/IPsec) proposal defines a set of parameters used in negotiating security associations for IPSec tunnels. IPSec proposals defined in NetBox can be referenced by [IPSec policies](./ipsecpolicy.md), which are in turn employed by [IPSec profiles](./ipsecprofile.md).
## Fields
### Name
The unique user-assigned name for the proposal.
### Encryption Algorithm
The protocol employed for data encryption. Options include DES, 3DES, and various flavors of AES.
### Authentication Algorithm
The mechanism employed to ensure data integrity. Options include MD5 and SHA HMAC implementations.
### SA Lifetime (Seconds)
The maximum amount of time for which the security association (SA) may be active, in seconds.
### SA Lifetime (Data)
The maximum amount of data which can be transferred within the security association (SA) before it must be rebuilt, in kilobytes.

36
docs/models/vpn/tunnel.md Normal file
View File

@ -0,0 +1,36 @@
# Tunnels
A tunnel represents a private virtual connection established among two or more endpoints across a shared infrastructure by employing protocol encapsulation. Common encapsulation techniques include [Generic Routing Encapsulation (GRE)](https://en.wikipedia.org/wiki/Generic_Routing_Encapsulation), [IP-in-IP](https://en.wikipedia.org/wiki/IP_in_IP), and [IPSec](https://en.wikipedia.org/wiki/IPsec). NetBox supports modeling both peer-to-peer and hub-and-spoke tunnel topologies.
Device and virtual machine interfaces are associated to tunnels by creating [tunnel terminations](./tunneltermination.md).
## Fields
### Name
A unique name assigned to the tunnel for identification.
### Status
The operational status of the tunnel. By default, the following statuses are available:
| Name |
|----------------|
| 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.
### Encapsulation
The encapsulation protocol or technique employed to effect the tunnel. NetBox supports GRE, IP-in-IP, and IPSec encapsulations.
### Tunnel ID
An optional numeric identifier for the tunnel.
### IPSec Profile
For IPSec tunnels, this is the [IPSec Profile](./ipsecprofile.md) employed to negotiate security associations.

View File

@ -0,0 +1,30 @@
# Tunnel Terminations
A tunnel termination connects a device or virtual machine interface to a [tunnel](./tunnel.md). The tunnel must be created before any terminations may be added.
## Fields
### Tunnel
The [tunnel](./tunnel.md) to which this termination is made.
### Role
The functional role of the attached interface. The following options are available:
| Name | Description |
|-------|--------------------------------------------------|
| Peer | An endpoint in a point-to-point or mesh topology |
| Hub | A central point in a hub-and-spoke topology |
| Spoke | An edge point in a hub-and-spoke topology |
!!! note
Multiple hub terminations may be attached to a tunnel.
### Termination
The device or virtual machine interface terminated to the tunnel.
### Outside IP
The public or underlay IP address with which this termination is associated. This is the IP to which peers will route tunneled traffic.

View File

@ -74,6 +74,7 @@ nav:
- Circuits: 'features/circuits.md'
- Wireless: 'features/wireless.md'
- Virtualization: 'features/virtualization.md'
- VPN Tunnels: 'features/vpn-tunnels.md'
- Tenancy: 'features/tenancy.md'
- Contacts: 'features/contacts.md'
- Search: 'features/search.md'
@ -254,6 +255,14 @@ nav:
- ClusterType: 'models/virtualization/clustertype.md'
- VMInterface: 'models/virtualization/vminterface.md'
- VirtualMachine: 'models/virtualization/virtualmachine.md'
- VPN:
- IKEPolicy: 'models/vpn/ikepolicy.md'
- IKEProposal: 'models/vpn/ikeproposal.md'
- IPSecPolicy: 'models/vpn/ipsecpolicy.md'
- IPSecProfile: 'models/vpn/ipsecprofile.md'
- IPSecProposal: 'models/vpn/ipsecproposal.md'
- Tunnel: 'models/vpn/tunnel.md'
- TunnelTermination: 'models/vpn/tunneltermination.md'
- Wireless:
- WirelessLAN: 'models/wireless/wirelesslan.md'
- WirelessLANGroup: 'models/wireless/wirelesslangroup.md'

View File

@ -9,6 +9,7 @@ from .choices import *
from .models import *
__all__ = (
'ConfigRevisionFilterSet',
'DataFileFilterSet',
'DataSourceFilterSet',
'JobFilterSet',
@ -123,3 +124,23 @@ class JobFilterSet(BaseFilterSet):
Q(user__username__icontains=value) |
Q(name__icontains=value)
)
class ConfigRevisionFilterSet(BaseFilterSet):
q = django_filters.CharFilter(
method='search',
label=_('Search'),
)
class Meta:
model = ConfigRevision
fields = [
'id',
]
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(comment__icontains=value)
)

View File

@ -4,14 +4,15 @@ from django.utils.translation import gettext_lazy as _
from core.choices import *
from core.models import *
from extras.forms.mixins import SavedFiltersMixin
from netbox.forms import NetBoxModelFilterSetForm
from netbox.forms.mixins import SavedFiltersMixin
from netbox.utils import get_data_backend_choices
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
from utilities.forms.fields import ContentTypeChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.widgets import APISelectMultiple, DateTimePicker
__all__ = (
'ConfigRevisionFilterForm',
'DataFileFilterForm',
'DataSourceFilterForm',
'JobFilterForm',
@ -123,3 +124,9 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
api_url='/api/users/users/',
)
)
class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
)

View File

@ -1,22 +1,28 @@
import copy
import json
from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from core.forms.mixins import SyncedDataMixin
from core.models import *
from netbox.config import get_config, PARAMS
from netbox.forms import NetBoxModelForm
from netbox.registry import registry
from netbox.utils import get_data_backend_choices
from utilities.forms import get_field_value
from utilities.forms import BootstrapMixin, get_field_value
from utilities.forms.fields import CommentField
from utilities.forms.widgets import HTMXSelect
__all__ = (
'ConfigRevisionForm',
'DataSourceForm',
'ManagedFileForm',
)
EMPTY_VALUES = ('', None, [], ())
class DataSourceForm(NetBoxModelForm):
type = forms.ChoiceField(
@ -111,3 +117,113 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
new_file.write(self.cleaned_data['upload_file'].read())
return super().save(*args, **kwargs)
class ConfigFormMetaclass(forms.models.ModelFormMetaclass):
def __new__(mcs, name, bases, attrs):
# Emulate a declared field for each supported configuration parameter
param_fields = {}
for param in PARAMS:
field_kwargs = {
'required': False,
'label': param.label,
'help_text': param.description,
}
field_kwargs.update(**param.field_kwargs)
param_fields[param.name] = param.field(**field_kwargs)
attrs.update(param_fields)
return super().__new__(mcs, name, bases, attrs)
class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMetaclass):
"""
Form for creating a new ConfigRevision.
"""
fieldsets = (
(_('Rack Elevations'), ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')),
(_('Power'), ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')),
(_('IPAM'), ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')),
(_('Security'), ('ALLOWED_URL_SCHEMES',)),
(_('Banners'), ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')),
(_('Pagination'), ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')),
(_('Validation'), ('CUSTOM_VALIDATORS', 'PROTECTION_RULES')),
(_('User Preferences'), ('DEFAULT_USER_PREFERENCES',)),
(_('Miscellaneous'), (
'MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL',
)),
(_('Config Revision'), ('comment',))
)
class Meta:
model = ConfigRevision
fields = '__all__'
widgets = {
'BANNER_LOGIN': forms.Textarea(attrs={'class': 'font-monospace'}),
'BANNER_MAINTENANCE': forms.Textarea(attrs={'class': 'font-monospace'}),
'BANNER_TOP': forms.Textarea(attrs={'class': 'font-monospace'}),
'BANNER_BOTTOM': forms.Textarea(attrs={'class': 'font-monospace'}),
'CUSTOM_VALIDATORS': forms.Textarea(attrs={'class': 'font-monospace'}),
'PROTECTION_RULES': forms.Textarea(attrs={'class': 'font-monospace'}),
'comment': forms.Textarea(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Append current parameter values to form field help texts and check for static configurations
config = get_config()
for param in PARAMS:
value = getattr(config, param.name)
# Set the field's initial value, if it can be serialized. (This may not be the case e.g. for
# CUSTOM_VALIDATORS, which may reference Python objects.)
try:
json.dumps(value)
if type(value) in (tuple, list):
self.fields[param.name].initial = ', '.join(value)
else:
self.fields[param.name].initial = value
except TypeError:
pass
# Check whether this parameter is statically configured (e.g. in configuration.py)
if hasattr(settings, param.name):
self.fields[param.name].disabled = True
self.fields[param.name].help_text = _(
'This parameter has been defined statically and cannot be modified.'
)
continue
# Set the field's help text
help_text = self.fields[param.name].help_text
if help_text:
help_text += '<br />' # Line break
help_text += _('Current value: <strong>{value}</strong>').format(value=value or '&mdash;')
if value == param.default:
help_text += _(' (default)')
self.fields[param.name].help_text = help_text
def save(self, commit=True):
instance = super().save(commit=False)
# Populate JSON data on the instance
instance.data = self.render_json()
if commit:
instance.save()
return instance
def render_json(self):
json = {}
# Iterate through each field and populate non-empty values
for field_name in self.declared_fields:
if field_name in self.cleaned_data and self.cleaned_data[field_name] not in EMPTY_VALUES:
json[field_name] = self.cleaned_data[field_name]
return json

View File

@ -1,7 +1,7 @@
from django.core.cache import cache
from django.core.management.base import BaseCommand
from extras.models import ConfigRevision
from core.models import ConfigRevision
class Command(BaseCommand):

View File

@ -9,7 +9,7 @@ from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'wireless')
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
BANNER_TEXT = """### NetBox interactive shell ({node})
### Python {python} | Django {django} | NetBox {netbox}

View File

@ -0,0 +1,31 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0008_contenttype_proxy'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.CreateModel(
name='ConfigRevision',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('comment', models.CharField(blank=True, max_length=200)),
('data', models.JSONField(blank=True, null=True)),
],
options={
'verbose_name': 'config revision',
'verbose_name_plural': 'config revisions',
'ordering': ['-created'],
},
),
],
# Table will be renamed from extras_configrevision in extras/0101_move_configrevision
database_operations=[],
),
]

View File

@ -1,3 +1,4 @@
from .config import *
from .contenttypes import *
from .data import *
from .files import *

View File

@ -0,0 +1,66 @@
from django.core.cache import cache
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext, gettext_lazy as _
from utilities.querysets import RestrictedQuerySet
__all__ = (
'ConfigRevision',
)
class ConfigRevision(models.Model):
"""
An atomic revision of NetBox's configuration.
"""
created = models.DateTimeField(
verbose_name=_('created'),
auto_now_add=True
)
comment = models.CharField(
verbose_name=_('comment'),
max_length=200,
blank=True
)
data = models.JSONField(
blank=True,
null=True,
verbose_name=_('configuration data')
)
objects = RestrictedQuerySet.as_manager()
class Meta:
ordering = ['-created']
verbose_name = _('config revision')
verbose_name_plural = _('config revisions')
def __str__(self):
if not self.pk:
return gettext('Default configuration')
if self.is_active:
return gettext('Current configuration')
return gettext('Config revision #{id}').format(id=self.pk)
def __getattr__(self, item):
if item in self.data:
return self.data[item]
return super().__getattribute__(item)
def get_absolute_url(self):
if not self.pk:
return reverse('core:config') # Default config view
return reverse('core:configrevision', args=[self.pk])
def activate(self):
"""
Cache the configuration data.
"""
cache.set('config', self.data, None)
cache.set('config_version', self.pk, None)
activate.alters_data = True
@property
def is_active(self):
return cache.get('config_version') == self.pk

View File

@ -1,5 +1,8 @@
from django.db.models.signals import post_save
from django.dispatch import Signal, receiver
from .models import ConfigRevision
__all__ = (
'post_sync',
'pre_sync',
@ -19,3 +22,11 @@ def auto_sync(instance, **kwargs):
for autosync in AutoSyncRecord.objects.filter(datafile__source=instance).prefetch_related('object'):
autosync.object.sync(save=True)
@receiver(post_save, sender=ConfigRevision)
def update_config(sender, instance, **kwargs):
"""
Update the cached NetBox configuration when a new ConfigRevision is created.
"""
instance.activate()

View File

@ -1,2 +1,3 @@
from .config import *
from .data import *
from .jobs import *

View File

@ -0,0 +1,33 @@
from django.utils.translation import gettext_lazy as _
from core.models import ConfigRevision
from netbox.tables import NetBoxTable, columns
__all__ = (
'ConfigRevisionTable',
)
REVISION_BUTTONS = """
{% if not record.is_active %}
<a href="{% url 'core:configrevision_restore' pk=record.pk %}" class="btn btn-sm btn-primary" title="Restore config">
<i class="mdi mdi-file-restore"></i>
</a>
{% endif %}
"""
class ConfigRevisionTable(NetBoxTable):
is_active = columns.BooleanColumn(
verbose_name=_('Is Active'),
)
actions = columns.ActionsColumn(
actions=('delete',),
extra_buttons=REVISION_BUTTONS
)
class Meta(NetBoxTable.Meta):
model = ConfigRevision
fields = (
'pk', 'id', 'is_active', 'created', 'comment',
)
default_columns = ('pk', 'id', 'is_active', 'created', 'comment')

View File

@ -25,6 +25,13 @@ urlpatterns = (
path('jobs/<int:pk>/', views.JobView.as_view(), name='job'),
path('jobs/<int:pk>/delete/', views.JobDeleteView.as_view(), name='job_delete'),
# Config revisions
path('config-revisions/', views.ConfigRevisionListView.as_view(), name='configrevision_list'),
path('config-revisions/add/', views.ConfigRevisionEditView.as_view(), name='configrevision_add'),
path('config-revisions/delete/', views.ConfigRevisionBulkDeleteView.as_view(), name='configrevision_bulk_delete'),
path('config-revisions/<int:pk>/restore/', views.ConfigRevisionRestoreView.as_view(), name='configrevision_restore'),
path('config-revisions/<int:pk>/', include(get_model_urls('core', 'configrevision'))),
# Configuration
path('config/', views.ConfigView.as_view(), name='config'),

View File

@ -1,12 +1,13 @@
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect
from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import View
from extras.models import ConfigRevision
from netbox.config import get_config
from netbox.config import get_config, PARAMS
from netbox.views import generic
from netbox.views.generic.base import BaseObjectView
from utilities.utils import count_related
from utilities.views import register_model_view
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
from . import filtersets, forms, tables
from .models import *
@ -164,3 +165,67 @@ class ConfigView(generic.ObjectView):
return ConfigRevision(
data=get_config().defaults
)
class ConfigRevisionListView(generic.ObjectListView):
queryset = ConfigRevision.objects.all()
filterset = filtersets.ConfigRevisionFilterSet
filterset_form = forms.ConfigRevisionFilterForm
table = tables.ConfigRevisionTable
@register_model_view(ConfigRevision)
class ConfigRevisionView(generic.ObjectView):
queryset = ConfigRevision.objects.all()
class ConfigRevisionEditView(generic.ObjectEditView):
queryset = ConfigRevision.objects.all()
form = forms.ConfigRevisionForm
@register_model_view(ConfigRevision, 'delete')
class ConfigRevisionDeleteView(generic.ObjectDeleteView):
queryset = ConfigRevision.objects.all()
class ConfigRevisionBulkDeleteView(generic.BulkDeleteView):
queryset = ConfigRevision.objects.all()
filterset = filtersets.ConfigRevisionFilterSet
table = tables.ConfigRevisionTable
class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View):
def get_required_permission(self):
return 'core.configrevision_edit'
def get(self, request, pk):
candidate_config = get_object_or_404(ConfigRevision, pk=pk)
# Get the current ConfigRevision
config_version = get_config().version
current_config = ConfigRevision.objects.filter(pk=config_version).first()
params = []
for param in PARAMS:
params.append((
param.name,
current_config.data.get(param.name, None),
candidate_config.data.get(param.name, None)
))
return render(request, 'core/configrevision_restore.html', {
'object': candidate_config,
'params': params,
})
def post(self, request, pk):
if not request.user.has_perm('core.configrevision_edit'):
return HttpResponseForbidden()
candidate_config = get_object_or_404(ConfigRevision, pk=pk)
candidate_config.activate()
messages.success(request, f"Restored configuration revision #{pk}")
return redirect(candidate_config.get_absolute_url())

View File

@ -1,9 +1,9 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from dcim.models import *
from django.utils.translation import gettext_lazy as _
from extras.forms import CustomFieldsMixin
from extras.models import Tag
from netbox.forms.mixins import CustomFieldsMixin
from utilities.forms import BootstrapMixin, form_from_model
from utilities.forms.fields import DynamicModelMultipleChoiceField, ExpandableNameField
from .object_create import ComponentCreateForm

View File

@ -566,6 +566,10 @@ class BaseInterface(models.Model):
return super().save(*args, **kwargs)
@property
def tunnel_termination(self):
return self.tunnel_terminations.first()
@property
def count_ipaddresses(self):
return self.ip_addresses.count()
@ -719,6 +723,12 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
object_id_field='interface_id',
related_query_name='+'
)
tunnel_terminations = GenericRelation(
to='vpn.TunnelTermination',
content_type_field='termination_type',
object_id_field='termination_id',
related_query_name='interface'
)
l2vpn_terminations = GenericRelation(
to='ipam.L2VPNTermination',
content_type_field='assigned_object_type',

View File

@ -584,6 +584,12 @@ class BaseInterfaceTable(NetBoxTable):
orderable=False,
verbose_name=_('L2VPN')
)
tunnel = tables.Column(
accessor=tables.A('tunnel_termination__tunnel'),
linkify=True,
orderable=False,
verbose_name=_('Tunnel')
)
untagged_vlan = tables.Column(
verbose_name=_('Untagged VLAN'),
linkify=True
@ -646,7 +652,8 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
'speed', 'speed_formatted', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn',
'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'inventory_items', 'created', 'last_updated',
'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'inventory_items', 'created',
'last_updated',
)
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
@ -682,8 +689,8 @@ class DeviceInterfaceTable(InterfaceTable):
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag',
'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups',
'untagged_vlan', 'tagged_vlans', 'actions',
'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses',
'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',

View File

@ -359,6 +359,16 @@ INTERFACE_BUTTONS = """
<i class="mdi mdi-wifi-off" aria-hidden="true"></i>
</a>
{% 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">
<i class="mdi mdi-tunnel-outline" aria-hidden="true"></i>
</a>
{% elif perms.vpn.delete_tunneltermination and record.tunnel_termination %}
<a href="{% url 'vpn:tunneltermination_delete' pk=record.tunnel_termination.pk %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" title="Remove tunnel" class="btn btn-danger btn-sm">
<i class="mdi mdi-tunnel-outline" aria-hidden="true"></i>
</a>
{% endif %}
{% elif record.is_wired and perms.dcim.add_cable %}
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>

View File

@ -1,2 +0,0 @@
# TODO: Removing this import triggers an import loop due to how form mixins are currently organized
from .forms import ConfigRevisionForm

View File

@ -17,7 +17,6 @@ from .models import *
__all__ = (
'BookmarkFilterSet',
'ConfigContextFilterSet',
'ConfigRevisionFilterSet',
'ConfigTemplateFilterSet',
'ContentTypeFilterSet',
'CustomFieldChoiceSetFilterSet',
@ -657,27 +656,3 @@ class ContentTypeFilterSet(django_filters.FilterSet):
Q(app_label__icontains=value) |
Q(model__icontains=value)
)
#
# ConfigRevisions
#
class ConfigRevisionFilterSet(BaseFilterSet):
q = django_filters.CharFilter(
method='search',
label=_('Search'),
)
class Meta:
model = ConfigRevision
fields = [
'id',
]
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(comment__icontains=value)
)

View File

@ -3,5 +3,4 @@ from .filtersets import *
from .bulk_edit import *
from .bulk_import import *
from .misc import *
from .mixins import *
from .scripts import *

View File

@ -7,6 +7,7 @@ from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site
from extras.choices import *
from extras.models import *
from netbox.forms.base import NetBoxModelFilterSetForm
from netbox.forms.mixins import SavedFiltersMixin
from tenancy.models import Tenant, TenantGroup
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import (
@ -14,11 +15,9 @@ from utilities.forms.fields import (
)
from utilities.forms.widgets import APISelectMultiple, DateTimePicker
from virtualization.models import Cluster, ClusterGroup, ClusterType
from .mixins import *
__all__ = (
'ConfigContextFilterForm',
'ConfigRevisionFilterForm',
'ConfigTemplateFilterForm',
'CustomFieldChoiceSetFilterForm',
'CustomFieldFilterForm',
@ -522,9 +521,3 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
api_url='/api/extras/content-types/',
)
)
class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
)

View File

@ -1,7 +1,6 @@
import json
from django import forms
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
@ -11,7 +10,6 @@ from core.models import ContentType
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import *
from extras.models import *
from netbox.config import get_config, PARAMS
from netbox.forms import NetBoxModelForm
from tenancy.models import Tenant, TenantGroup
from utilities.forms import BootstrapMixin, add_blank_choice, get_field_value
@ -25,7 +23,6 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType
__all__ = (
'BookmarkForm',
'ConfigContextForm',
'ConfigRevisionForm',
'ConfigTemplateForm',
'CustomFieldChoiceSetForm',
'CustomFieldForm',
@ -542,116 +539,3 @@ class JournalEntryForm(NetBoxModelForm):
'assigned_object_type': forms.HiddenInput,
'assigned_object_id': forms.HiddenInput,
}
EMPTY_VALUES = ('', None, [], ())
class ConfigFormMetaclass(forms.models.ModelFormMetaclass):
def __new__(mcs, name, bases, attrs):
# Emulate a declared field for each supported configuration parameter
param_fields = {}
for param in PARAMS:
field_kwargs = {
'required': False,
'label': param.label,
'help_text': param.description,
}
field_kwargs.update(**param.field_kwargs)
param_fields[param.name] = param.field(**field_kwargs)
attrs.update(param_fields)
return super().__new__(mcs, name, bases, attrs)
class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMetaclass):
"""
Form for creating a new ConfigRevision.
"""
fieldsets = (
(_('Rack Elevations'), ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')),
(_('Power'), ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')),
(_('IPAM'), ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')),
(_('Security'), ('ALLOWED_URL_SCHEMES',)),
(_('Banners'), ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')),
(_('Pagination'), ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')),
(_('Validation'), ('CUSTOM_VALIDATORS', 'PROTECTION_RULES')),
(_('User Preferences'), ('DEFAULT_USER_PREFERENCES',)),
(_('Miscellaneous'), (
'MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL',
)),
(_('Config Revision'), ('comment',))
)
class Meta:
model = ConfigRevision
fields = '__all__'
widgets = {
'BANNER_LOGIN': forms.Textarea(attrs={'class': 'font-monospace'}),
'BANNER_MAINTENANCE': forms.Textarea(attrs={'class': 'font-monospace'}),
'BANNER_TOP': forms.Textarea(attrs={'class': 'font-monospace'}),
'BANNER_BOTTOM': forms.Textarea(attrs={'class': 'font-monospace'}),
'CUSTOM_VALIDATORS': forms.Textarea(attrs={'class': 'font-monospace'}),
'PROTECTION_RULES': forms.Textarea(attrs={'class': 'font-monospace'}),
'comment': forms.Textarea(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Append current parameter values to form field help texts and check for static configurations
config = get_config()
for param in PARAMS:
value = getattr(config, param.name)
# Set the field's initial value, if it can be serialized. (This may not be the case e.g. for
# CUSTOM_VALIDATORS, which may reference Python objects.)
try:
json.dumps(value)
if type(value) in (tuple, list):
self.fields[param.name].initial = ', '.join(value)
else:
self.fields[param.name].initial = value
except TypeError:
pass
# Check whether this parameter is statically configured (e.g. in configuration.py)
if hasattr(settings, param.name):
self.fields[param.name].disabled = True
self.fields[param.name].help_text = _(
'This parameter has been defined statically and cannot be modified.'
)
continue
# Set the field's help text
help_text = self.fields[param.name].help_text
if help_text:
help_text += '<br />' # Line break
help_text += _('Current value: <strong>{value}</strong>').format(value=value or '&mdash;')
if value == param.default:
help_text += _(' (default)')
self.fields[param.name].help_text = help_text
def save(self, commit=True):
instance = super().save(commit=False)
# Populate JSON data on the instance
instance.data = self.render_json()
if commit:
instance.save()
return instance
def render_json(self):
json = {}
# Iterate through each field and populate non-empty values
for field_name in self.declared_fields:
if field_name in self.cleaned_data and self.cleaned_data[field_name] not in EMPTY_VALUES:
json[field_name] = self.cleaned_data[field_name]
return json

View File

@ -0,0 +1,39 @@
from django.db import migrations
def update_content_type(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType')
# Delete the new ContentType effected by the introduction of core.ConfigRevision
ContentType.objects.filter(app_label='core', model='configrevision').delete()
# Update the app label of the original ContentType for extras.ConfigRevision to ensure any foreign key
# references are preserved
ContentType.objects.filter(app_label='extras', model='configrevision').update(app_label='core')
class Migration(migrations.Migration):
dependencies = [
('extras', '0101_eventrule'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.DeleteModel(
name='ConfigRevision',
),
],
database_operations=[
migrations.AlterModelTable(
name='ConfigRevision',
table='core_configrevision',
),
],
),
migrations.RunPython(
code=update_content_type,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -3,14 +3,13 @@ import urllib.parse
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.core.cache import cache
from django.core.validators import ValidationError
from django.db import models
from django.http import HttpResponse
from django.urls import reverse
from django.utils import timezone
from django.utils.formats import date_format
from django.utils.translation import gettext, gettext_lazy as _
from django.utils.translation import gettext_lazy as _
from rest_framework.utils.encoders import JSONEncoder
from core.models import ContentType
@ -28,7 +27,6 @@ from utilities.utils import clean_html, dict_to_querydict, render_jinja2
__all__ = (
'Bookmark',
'ConfigRevision',
'CustomLink',
'EventRule',
'ExportTemplate',
@ -789,59 +787,3 @@ class Bookmark(models.Model):
raise ValidationError(
_("Bookmarks cannot be assigned to this object type ({type}).").format(type=self.object_type)
)
class ConfigRevision(models.Model):
"""
An atomic revision of NetBox's configuration.
"""
created = models.DateTimeField(
verbose_name=_('created'),
auto_now_add=True
)
comment = models.CharField(
verbose_name=_('comment'),
max_length=200,
blank=True
)
data = models.JSONField(
blank=True,
null=True,
verbose_name=_('configuration data')
)
objects = RestrictedQuerySet.as_manager()
class Meta:
ordering = ['-created']
verbose_name = _('config revision')
verbose_name_plural = _('config revisions')
def __str__(self):
if not self.pk:
return gettext('Default configuration')
if self.is_active:
return gettext('Current configuration')
return gettext('Config revision #{id}').format(id=self.pk)
def __getattr__(self, item):
if item in self.data:
return self.data[item]
return super().__getattribute__(item)
def get_absolute_url(self):
if not self.pk:
return reverse('core:config') # Default config view
return reverse('extras:configrevision', args=[self.pk])
def activate(self):
"""
Cache the configuration data.
"""
cache.set('config', self.data, None)
cache.set('config_version', self.pk, None)
activate.alters_data = True
@property
def is_active(self):
return cache.get('config_version') == self.pk

View File

@ -15,7 +15,7 @@ from netbox.signals import post_clean
from utilities.exceptions import AbortRequest
from .choices import ObjectChangeActionChoices
from .events import enqueue_object, get_snapshots, serialize_for_event
from .models import ConfigRevision, CustomField, ObjectChange, TaggedItem
from .models import CustomField, ObjectChange, TaggedItem
#
# Change logging/webhooks
@ -219,18 +219,6 @@ def run_delete_validators(sender, instance, **kwargs):
)
#
# Dynamic configuration
#
@receiver(post_save, sender=ConfigRevision)
def update_config(sender, instance, **kwargs):
"""
Update the cached NetBox configuration when a new ConfigRevision is created.
"""
instance.activate()
#
# Tags
#

View File

@ -11,7 +11,6 @@ from .template_code import *
__all__ = (
'BookmarkTable',
'ConfigContextTable',
'ConfigRevisionTable',
'ConfigTemplateTable',
'CustomFieldChoiceSetTable',
'CustomFieldTable',
@ -35,31 +34,6 @@ IMAGEATTACHMENT_IMAGE = '''
{% endif %}
'''
REVISION_BUTTONS = """
{% if not record.is_active %}
<a href="{% url 'extras:configrevision_restore' pk=record.pk %}" class="btn btn-sm btn-primary" title="Restore config">
<i class="mdi mdi-file-restore"></i>
</a>
{% endif %}
"""
class ConfigRevisionTable(NetBoxTable):
is_active = columns.BooleanColumn(
verbose_name=_('Is Active'),
)
actions = columns.ActionsColumn(
actions=('delete',),
extra_buttons=REVISION_BUTTONS
)
class Meta(NetBoxTable.Meta):
model = ConfigRevision
fields = (
'pk', 'id', 'is_active', 'created', 'comment',
)
default_columns = ('pk', 'id', 'is_active', 'created', 'comment')
class CustomFieldTable(NetBoxTable):
name = tables.Column(

View File

@ -106,13 +106,6 @@ urlpatterns = [
path('journal-entries/import/', views.JournalEntryBulkImportView.as_view(), name='journalentry_import'),
path('journal-entries/<int:pk>/', include(get_model_urls('extras', 'journalentry'))),
# Config revisions
path('config-revisions/', views.ConfigRevisionListView.as_view(), name='configrevision_list'),
path('config-revisions/add/', views.ConfigRevisionEditView.as_view(), name='configrevision_add'),
path('config-revisions/delete/', views.ConfigRevisionBulkDeleteView.as_view(), name='configrevision_bulk_delete'),
path('config-revisions/<int:pk>/restore/', views.ConfigRevisionRestoreView.as_view(), name='configrevision_restore'),
path('config-revisions/<int:pk>/', include(get_model_urls('extras', 'configrevision'))),
# Change logging
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
path('changelog/<int:pk>/', include(get_model_urls('extras', 'objectchange'))),

View File

@ -15,7 +15,6 @@ from core.models import Job
from core.tables import JobTable
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
from extras.dashboard.utils import get_widget_class
from netbox.config import get_config, PARAMS
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic
from utilities.forms import ConfirmationForm, get_field_value
@ -1361,74 +1360,6 @@ class ScriptResultView(ContentTypePermissionRequiredMixin, View):
})
#
# Config Revisions
#
class ConfigRevisionListView(generic.ObjectListView):
queryset = ConfigRevision.objects.all()
filterset = filtersets.ConfigRevisionFilterSet
filterset_form = forms.ConfigRevisionFilterForm
table = tables.ConfigRevisionTable
@register_model_view(ConfigRevision)
class ConfigRevisionView(generic.ObjectView):
queryset = ConfigRevision.objects.all()
class ConfigRevisionEditView(generic.ObjectEditView):
queryset = ConfigRevision.objects.all()
form = forms.ConfigRevisionForm
@register_model_view(ConfigRevision, 'delete')
class ConfigRevisionDeleteView(generic.ObjectDeleteView):
queryset = ConfigRevision.objects.all()
class ConfigRevisionBulkDeleteView(generic.BulkDeleteView):
queryset = ConfigRevision.objects.all()
filterset = filtersets.ConfigRevisionFilterSet
table = tables.ConfigRevisionTable
class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View):
def get_required_permission(self):
return 'extras.configrevision_edit'
def get(self, request, pk):
candidate_config = get_object_or_404(ConfigRevision, pk=pk)
# Get the current ConfigRevision
config_version = get_config().version
current_config = ConfigRevision.objects.filter(pk=config_version).first()
params = []
for param in PARAMS:
params.append((
param.name,
current_config.data.get(param.name, None),
candidate_config.data.get(param.name, None)
))
return render(request, 'extras/configrevision_restore.html', {
'object': candidate_config,
'params': params,
})
def post(self, request, pk):
if not request.user.has_perm('extras.configrevision_edit'):
return HttpResponseForbidden()
candidate_config = get_object_or_404(ConfigRevision, pk=pk)
candidate_config.activate()
messages.success(request, f"Restored configuration revision #{pk}")
return redirect(candidate_config.get_absolute_url())
#
# Markdown
#

View File

@ -39,6 +39,7 @@ class APIRootView(APIView):
'tenancy': reverse('tenancy-api:api-root', request=request, format=format),
'users': reverse('users-api:api-root', request=request, format=format),
'virtualization': reverse('virtualization-api:api-root', request=request, format=format),
'vpn': reverse('vpn-api:api-root', request=request, format=format),
'wireless': reverse('wireless-api:api-root', request=request, format=format),
})

View File

@ -74,7 +74,7 @@ class Config:
def _populate_from_db(self):
"""Cache data from latest ConfigRevision, then populate from cache"""
from extras.models import ConfigRevision
from core.models import ConfigRevision
try:
revision = ConfigRevision.objects.last()

View File

@ -4,11 +4,11 @@ from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from extras.choices import *
from extras.forms.mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin
from extras.models import CustomField, Tag
from utilities.forms import CSVModelForm
from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.mixins import BootstrapMixin, CheckLastUpdatedMixin
from .mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin
__all__ = (
'NetBoxModelForm',

View File

@ -9,6 +9,7 @@ from netbox.registry import registry
from tenancy.graphql.schema import TenancyQuery
from users.graphql.schema import UsersQuery
from virtualization.graphql.schema import VirtualizationQuery
from vpn.graphql.schema import VPNQuery
from wireless.graphql.schema import WirelessQuery
@ -21,6 +22,7 @@ class Query(
IPAMQuery,
TenancyQuery,
VirtualizationQuery,
VPNQuery,
WirelessQuery,
*registry['plugins']['graphql_schemas'], # Append plugin schemas
graphene.ObjectType

View File

@ -195,17 +195,34 @@ IPAM_MENU = Menu(
),
)
OVERLAY_MENU = Menu(
label=_('Overlay'),
VPN_MENU = Menu(
label=_('VPN'),
icon_class='mdi mdi-graph-outline',
groups=(
MenuGroup(
label='L2VPNs',
label=_('Tunnels'),
items=(
get_model_item('vpn', 'tunnel', _('Tunnels')),
get_model_item('vpn', 'tunneltermination', _('Tunnel Terminations')),
),
),
MenuGroup(
label=_('L2VPNs'),
items=(
get_model_item('ipam', 'l2vpn', _('L2VPNs')),
get_model_item('ipam', 'l2vpntermination', _('Terminations')),
),
),
MenuGroup(
label=_('Security'),
items=(
get_model_item('vpn', 'ikeproposal', _('IKE Proposals')),
get_model_item('vpn', 'ikepolicy', _('IKE Policies')),
get_model_item('vpn', 'ipsecproposal', _('IPSec Proposals')),
get_model_item('vpn', 'ipsecpolicy', _('IPSec Policies')),
get_model_item('vpn', 'ipsecprofile', _('IPSec Profiles')),
),
),
),
)
@ -425,13 +442,13 @@ ADMIN_MENU = Menu(
MenuItem(
link='core:config',
link_text=_('Current Config'),
permissions=['extras.view_configrevision'],
permissions=['core.view_configrevision'],
staff_only=True
),
MenuItem(
link='extras:configrevision_list',
link='core:configrevision_list',
link_text=_('Config Revisions'),
permissions=['extras.view_configrevision'],
permissions=['core.view_configrevision'],
staff_only=True
),
),
@ -445,7 +462,7 @@ MENUS = [
CONNECTIONS_MENU,
WIRELESS_MENU,
IPAM_MENU,
OVERLAY_MENU,
VPN_MENU,
VIRTUALIZATION_MENU,
CIRCUITS_MENU,
POWER_MENU,

View File

@ -382,6 +382,7 @@ INSTALLED_APPS = [
'users',
'utilities',
'virtualization',
'vpn',
'wireless',
'django_rq', # Must come after extras to allow overriding management commands
'drf_spectacular',

View File

@ -2,7 +2,7 @@ from django.conf import settings
from django.core.cache import cache
from django.test import override_settings, TestCase
from extras.models import ConfigRevision
from core.models import ConfigRevision
from netbox.config import clear_config, get_config

View File

@ -33,6 +33,7 @@ _patterns = [
path('tenancy/', include('tenancy.urls')),
path('users/', include('users.urls')),
path('virtualization/', include('virtualization.urls')),
path('vpn/', include('vpn.urls')),
path('wireless/', include('wireless.urls')),
# Current user views
@ -51,6 +52,7 @@ _patterns = [
path('api/tenancy/', include('tenancy.api.urls')),
path('api/users/', include('users.api.urls')),
path('api/virtualization/', include('virtualization.api.urls')),
path('api/vpn/', include('vpn.api.urls')),
path('api/wireless/', include('wireless.api.urls')),
path('api/status/', StatusView.as_view(), name='api-status'),

View File

@ -14,11 +14,11 @@
<div class="controls">
<div class="control-group">
{% plugin_buttons object %}
{% if not object.pk or object.is_active and perms.extras.add_configrevision %}
{% url 'extras:configrevision_add' as edit_url %}
{% if not object.pk or object.is_active and perms.core.add_configrevision %}
{% url 'core:configrevision_add' as edit_url %}
{% include "buttons/edit.html" with url=edit_url %}
{% endif %}
{% if object.pk and not object.is_active and perms.extras.delete_configrevision %}
{% if object.pk and not object.is_active and perms.core.delete_configrevision %}
{% delete_button object %}
{% endif %}
</div>

View File

@ -18,8 +18,8 @@
<div class="col col-md-12">
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'extras:configrevision_list' %}">{% trans "Config revisions" %}</a></li>
<li class="breadcrumb-item"><a href="{% url 'extras:configrevision' pk=object.pk %}">{{ object }}</a></li>
<li class="breadcrumb-item"><a href="{% url 'core:configrevision_list' %}">{% trans "Config revisions" %}</a></li>
<li class="breadcrumb-item"><a href="{% url 'core:configrevision' pk=object.pk %}">{{ object }}</a></li>
</ol>
</nav>
</div>
@ -77,7 +77,7 @@
<div class="controls">
<div class="control-group">
<button type="submit" name="restore" class="btn btn-primary">{% trans "Restore" %}</button>
<a href="{% url 'extras:configrevision_list' %}" id="cancel" name="cancel" class="btn btn-outline-danger">{% trans "Cancel" %}</a>
<a href="{% url 'core:configrevision_list' %}" id="cancel" name="cancel" class="btn btn-outline-danger">{% trans "Cancel" %}</a>
</div>
</div>
</div>

View File

@ -0,0 +1,67 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">{% trans "IKE Policy" %}</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>
<tr>
<th scope="row">{% trans "IKE Version" %}</th>
<td>{{ object.get_version_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Mode" %}</th>
<td>{{ object.get_mode_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Pre-Shared Key" %}</th>
<td>
<span id="secret" class="font-monospace" data-secret="{{ object.preshared_key }}">{{ object.preshared_key|placeholder }}</span>
{% if object.preshared_key %}
<button type="button" class="btn btn-sm btn-primary toggle-secret float-end" data-bs-toggle="button">{% trans "Show Secret" %}</button>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "IPSec Profiles" %}</th>
<td>
<a href="{% url 'vpn:ipsecprofile_list' %}?ike_policy_id={{ object.pk }}">{{ object.ipsec_profiles.count }}</a>
</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">{% trans "Proposals" %}</h5>
<div class="card-body htmx-container table-responsive"
hx-get="{% url 'vpn:ikeproposal_list' %}?ike_policy_id={{ object.pk }}"
hx-trigger="load"
></div>
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,63 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">{% trans "IKE Proposal" %}</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>
<tr>
<th scope="row">{% trans "Authentication method" %}</th>
<td>{{ object.get_authentication_method_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Encryption algorithm" %}</th>
<td>{{ object.get_encryption_algorithm_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Authentication algorithm" %}</th>
<td>{{ object.get_authentication_algorithm_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "DH group" %}</th>
<td>{{ object.get_group_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "SA lifetime (seconds)" %}</th>
<td>{{ object.sa_lifetime|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "IKE Policies" %}</th>
<td>
<a href="{% url 'vpn:ikepolicy_list' %}?proposal_id={{ object.pk }}">{{ object.ike_policies.count }}</a>
</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,55 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">{% trans "IPSec Policy" %}</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>
<tr>
<th scope="row">{% trans "PFS group" %}</th>
<td>{{ object.get_pfs_group_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "IPSec Profiles" %}</th>
<td>
<a href="{% url 'vpn:ipsecprofile_list' %}?ipsec_policy_id={{ object.pk }}">{{ object.ipsec_profiles.count }}</a>
</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">{% trans "Proposals" %}</h5>
<div class="card-body htmx-container table-responsive"
hx-get="{% url 'vpn:ipsecproposal_list' %}?ipsec_policy_id={{ object.pk }}"
hx-trigger="load"
></div>
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,112 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">{% trans "IPSec Profile" %}</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Mode" %}</th>
<td>{{ object.get_mode_display }}</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">{% trans "IKE Policy" %}</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.ike_policy|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.ike_policy.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Version" %}</th>
<td>{{ object.ike_policy.get_version_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Mode" %}</th>
<td>{{ object.ike_policy.get_mode_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Proposals" %}</th>
<td>
<ul class="list-unstyled mb-0">
{% for proposal in object.ike_policy.proposals.all %}
<li>
<a href="{{ proposal.get_absolute_url }}">{{ proposal }}</a>
</li>
{% endfor %}
</ul>
</td>
</tr>
<tr>
<th scope="row">{% trans "Pre-Shared Key" %}</th>
<td>{% checkmark object.ike_policy.preshared_key %}</td>
</tr>
</table>
</div>
</div>
<div class="card">
<h5 class="card-header">{% trans "IPSec Policy" %}</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.ipsec_policy|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.ipsec_policy.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Proposals" %}</th>
<td>
<ul class="list-unstyled mb-0">
{% for proposal in object.ipsec_policy.proposals.all %}
<li>
<a href="{{ proposal.get_absolute_url }}">{{ proposal }}</a>
</li>
{% endfor %}
</ul>
</td>
</tr>
<tr>
<th scope="row">{% trans "PFS Group" %}</th>
<td>{{ object.ipsec_policy.get_pfs_group_display }}</td>
</tr>
</table>
</div>
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,59 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">{% trans "IPSec Proposal" %}</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>
<tr>
<th scope="row">{% trans "Encryption algorithm" %}</th>
<td>{{ object.get_encryption_algorithm_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Authentication algorithm" %}</th>
<td>{{ object.get_authentication_algorithm_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "SA lifetime (seconds)" %}</th>
<td>{{ object.sa_lifetime_seconds|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "SA lifetime (KB)" %}</th>
<td>{{ object.sa_lifetime_data|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "IPSec Policies" %}</th>
<td>
<a href="{% url 'vpn:ipsecpolicy_list' %}?proposal_id={{ object.pk }}">{{ object.ipsec_policies.count }}</a>
</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,85 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block extra_controls %}
{% if perms.vpn.add_tunneltermination %}
<a href="{% url 'vpn:tunneltermination_add' %}?tunnel={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-primary">
<i class="mdi mdi-plus-thick"></i> {% trans "Add Termination" %}
</a>
{% endif %}
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">{% trans "Tunnel" %}</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Encapsulation" %}</th>
<td>{{ object.get_encapsulation_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "IPSec profile" %}</th>
<td>{{ object.ipsec_profile|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Tunnel ID" %}</th>
<td>{{ object.tunnel_id|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Tenant" %}</th>
<td>
{% if object.tenant.group %}
{{ object.tenant.group|linkify }} /
{% endif %}
{{ object.tenant|linkify|placeholder }}
</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">{% trans "Terminations" %}</h5>
<div class="card-body htmx-container table-responsive"
hx-get="{% url 'vpn:tunneltermination_list' %}?tunnel_id={{ object.pk }}"
hx-trigger="load"
></div>
{% if perms.vpn.add_tunneltermination %}
<div class="card-footer text-end noprint">
<a href="{% url 'vpn:tunneltermination_add' %}?tunnel={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Termination" %}
</a>
</div>
{% endif %}
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,62 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">{% trans "Tunnel Termination" %}</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Tunnel" %}</th>
<td>{{ object.tunnel|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Role" %}</th>
<td>{% badge object.get_role_display bg_color=object.get_role_color %}</td>
</tr>
<tr>
<th scope="row">
{% if object.termination.device %}
{% trans "Device" %}
{% elif object.termination.virtual_machine %}
{% trans "Virtual Machine" %}
{% endif %}
</th>
<td>{{ object.termination.parent_object|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Interface" %}</th>
<td>{{ object.termination|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Outside IP" %}</th>
<td>{{ object.outside_ip|linkify|placeholder }}</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">{% trans "Peer Terminations" %}</h5>
<div class="card-body htmx-container table-responsive"
hx-get="{% url 'vpn:tunneltermination_list' %}?tunnel_id={{ object.tunnel.pk }}&id__n={{ object.pk }}"
hx-trigger="load"
></div>
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -1,14 +1,12 @@
from django import forms
from extras.forms.mixins import SavedFiltersMixin
from utilities.forms import FilterForm
from users.models import Token
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelFilterSetForm
from users.models import NetBoxGroup, NetBoxUser, ObjectPermission
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES
from netbox.forms.mixins import SavedFiltersMixin
from users.models import NetBoxGroup, NetBoxUser, ObjectPermission, Token
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
from utilities.forms.fields import DynamicModelMultipleChoiceField
from utilities.forms.widgets import DateTimePicker

View File

@ -351,6 +351,12 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
object_id_field='interface_id',
related_query_name='+'
)
tunnel_terminations = GenericRelation(
to='vpn.TunnelTermination',
content_type_field='termination_type',
object_id_field='termination_id',
related_query_name='vminterface',
)
l2vpn_terminations = GenericRelation(
to='ipam.L2VPNTermination',
content_type_field='assigned_object_type',

View File

@ -131,7 +131,8 @@ class VMInterfaceTable(BaseInterfaceTable):
model = VMInterface
fields = (
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created',
'last_updated',
)
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
@ -154,7 +155,7 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable):
model = VMInterface
fields = (
'pk', 'id', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags',
'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
)
default_columns = ('pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses')
row_attrs = {

0
netbox/vpn/__init__.py Normal file
View File

3
netbox/vpn/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

View File

@ -0,0 +1,84 @@
from rest_framework import serializers
from netbox.api.serializers import WritableNestedSerializer
from vpn import models
__all__ = (
'NestedIKEPolicySerializer',
'NestedIKEProposalSerializer',
'NestedIPSecPolicySerializer',
'NestedIPSecProfileSerializer',
'NestedIPSecProposalSerializer',
'NestedTunnelSerializer',
'NestedTunnelTerminationSerializer',
)
class NestedTunnelSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:tunnel-detail'
)
class Meta:
model = models.Tunnel
fields = ('id', 'url', 'display', 'name')
class NestedTunnelTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:tunneltermination-detail'
)
class Meta:
model = models.TunnelTermination
fields = ('id', 'url', 'display')
class NestedIKEProposalSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:ikeproposal-detail'
)
class Meta:
model = models.IKEProposal
fields = ('id', 'url', 'display', 'name')
class NestedIKEPolicySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:ikepolicy-detail'
)
class Meta:
model = models.IKEPolicy
fields = ('id', 'url', 'display', 'name')
class NestedIPSecProposalSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:ipsecproposal-detail'
)
class Meta:
model = models.IPSecProposal
fields = ('id', 'url', 'display', 'name')
class NestedIPSecPolicySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:ipsecpolicy-detail'
)
class Meta:
model = models.IPSecPolicy
fields = ('id', 'url', 'display', 'name')
class NestedIPSecProfileSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:ipsecprofile-detail'
)
class Meta:
model = models.IPSecProfile
fields = ('id', 'url', 'display', 'name')

View File

@ -0,0 +1,193 @@
from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from ipam.api.nested_serializers import NestedIPAddressSerializer
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer
from netbox.constants import NESTED_SERIALIZER_PREFIX
from tenancy.api.nested_serializers import NestedTenantSerializer
from utilities.api import get_serializer_for_model
from vpn.choices import *
from vpn.models import *
from .nested_serializers import *
__all__ = (
'IKEPolicySerializer',
'IKEProposalSerializer',
'IPSecPolicySerializer',
'IPSecProfileSerializer',
'IPSecProposalSerializer',
'TunnelSerializer',
'TunnelTerminationSerializer',
)
class TunnelSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:tunnel-detail'
)
status = ChoiceField(
choices=TunnelStatusChoices
)
encapsulation = ChoiceField(
choices=TunnelEncapsulationChoices
)
ipsec_profile = NestedIPSecProfileSerializer(
required=False,
allow_null=True
)
tenant = NestedTenantSerializer(
required=False,
allow_null=True
)
class Meta:
model = Tunnel
fields = (
'id', 'url', 'display', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
)
class TunnelTerminationSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:tunneltermination-detail'
)
tunnel = NestedTunnelSerializer()
role = ChoiceField(
choices=TunnelTerminationRoleChoices
)
termination_type = ContentTypeField(
queryset=ContentType.objects.all()
)
termination = serializers.SerializerMethodField(
read_only=True
)
outside_ip = NestedIPAddressSerializer(
required=False,
allow_null=True
)
class Meta:
model = TunnelTermination
fields = (
'id', 'url', 'display', 'tunnel', 'role', 'termination_type', 'termination_id', 'termination', 'outside_ip',
'tags', 'custom_fields', 'created', 'last_updated',
)
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_termination(self, obj):
serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX)
context = {'request': self.context['request']}
return serializer(obj.termination, context=context).data
class IKEProposalSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:ikeproposal-detail'
)
authentication_method = ChoiceField(
choices=AuthenticationMethodChoices
)
encryption_algorithm = ChoiceField(
choices=EncryptionAlgorithmChoices
)
authentication_algorithm = ChoiceField(
choices=AuthenticationAlgorithmChoices
)
group = ChoiceField(
choices=DHGroupChoices
)
class Meta:
model = IKEProposal
fields = (
'id', 'url', 'display', 'name', 'description', 'authentication_method', 'encryption_algorithm',
'authentication_algorithm', 'group', 'sa_lifetime', 'tags', 'custom_fields', 'created', 'last_updated',
)
class IKEPolicySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:ikepolicy-detail'
)
version = ChoiceField(
choices=IKEVersionChoices
)
mode = ChoiceField(
choices=IKEModeChoices
)
proposals = SerializedPKRelatedField(
queryset=IKEProposal.objects.all(),
serializer=NestedIKEProposalSerializer,
required=False,
many=True
)
class Meta:
model = IKEPolicy
fields = (
'id', 'url', 'display', 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'tags',
'custom_fields', 'created', 'last_updated',
)
class IPSecProposalSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:ipsecproposal-detail'
)
encryption_algorithm = ChoiceField(
choices=EncryptionAlgorithmChoices
)
authentication_algorithm = ChoiceField(
choices=AuthenticationAlgorithmChoices
)
class Meta:
model = IPSecProposal
fields = (
'id', 'url', 'display', 'name', 'description', 'encryption_algorithm', 'authentication_algorithm',
'sa_lifetime_seconds', 'sa_lifetime_data', 'tags', 'custom_fields', 'created', 'last_updated',
)
class IPSecPolicySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:ipsecpolicy-detail'
)
proposals = SerializedPKRelatedField(
queryset=IPSecProposal.objects.all(),
serializer=NestedIPSecProposalSerializer,
required=False,
many=True
)
pfs_group = ChoiceField(
choices=DHGroupChoices,
required=False
)
class Meta:
model = IPSecPolicy
fields = (
'id', 'url', 'display', 'name', 'description', 'proposals', 'pfs_group', 'tags', 'custom_fields', 'created',
'last_updated',
)
class IPSecProfileSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='vpn-api:ipsecprofile-detail'
)
mode = ChoiceField(
choices=IPSecModeChoices
)
ike_policy = NestedIKEPolicySerializer()
ipsec_policy = NestedIPSecPolicySerializer()
class Meta:
model = IPSecProfile
fields = (
'id', 'url', 'display', 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
)

15
netbox/vpn/api/urls.py Normal file
View File

@ -0,0 +1,15 @@
from netbox.api.routers import NetBoxRouter
from . import views
router = NetBoxRouter()
router.APIRootView = views.VPNRootView
router.register('ike-policies', views.IKEPolicyViewSet)
router.register('ike-proposals', views.IKEProposalViewSet)
router.register('ipsec-policies', views.IPSecPolicyViewSet)
router.register('ipsec-proposals', views.IPSecProposalViewSet)
router.register('ipsec-profiles', views.IPSecProfileViewSet)
router.register('tunnels', views.TunnelViewSet)
router.register('tunnel-terminations', views.TunnelTerminationViewSet)
app_name = 'vpn-api'
urlpatterns = router.urls

74
netbox/vpn/api/views.py Normal file
View File

@ -0,0 +1,74 @@
from rest_framework.routers import APIRootView
from netbox.api.viewsets import NetBoxModelViewSet
from utilities.utils import count_related
from vpn import filtersets
from vpn.models import *
from . import serializers
__all__ = (
'IKEPolicyViewSet',
'IKEProposalViewSet',
'IPSecPolicyViewSet',
'IPSecProfileViewSet',
'IPSecProposalViewSet',
'TunnelTerminationViewSet',
'TunnelViewSet',
'VPNRootView',
)
class VPNRootView(APIRootView):
"""
VPN API root view
"""
def get_view_name(self):
return 'VPN'
#
# Viewsets
#
class TunnelViewSet(NetBoxModelViewSet):
queryset = Tunnel.objects.prefetch_related('ipsec_profile', 'tenant').annotate(
terminations_count=count_related(TunnelTermination, 'tunnel')
)
serializer_class = serializers.TunnelSerializer
filterset_class = filtersets.TunnelFilterSet
class TunnelTerminationViewSet(NetBoxModelViewSet):
queryset = TunnelTermination.objects.prefetch_related('tunnel')
serializer_class = serializers.TunnelTerminationSerializer
filterset_class = filtersets.TunnelTerminationFilterSet
class IKEProposalViewSet(NetBoxModelViewSet):
queryset = IKEProposal.objects.all()
serializer_class = serializers.IKEProposalSerializer
filterset_class = filtersets.IKEProposalFilterSet
class IKEPolicyViewSet(NetBoxModelViewSet):
queryset = IKEPolicy.objects.all()
serializer_class = serializers.IKEPolicySerializer
filterset_class = filtersets.IKEPolicyFilterSet
class IPSecProposalViewSet(NetBoxModelViewSet):
queryset = IPSecProposal.objects.all()
serializer_class = serializers.IPSecProposalSerializer
filterset_class = filtersets.IPSecProposalFilterSet
class IPSecPolicyViewSet(NetBoxModelViewSet):
queryset = IPSecPolicy.objects.all()
serializer_class = serializers.IPSecPolicySerializer
filterset_class = filtersets.IPSecPolicyFilterSet
class IPSecProfileViewSet(NetBoxModelViewSet):
queryset = IPSecProfile.objects.all()
serializer_class = serializers.IPSecProfileSerializer
filterset_class = filtersets.IPSecProfileFilterSet

9
netbox/vpn/apps.py Normal file
View File

@ -0,0 +1,9 @@
from django.apps import AppConfig
class VPNConfig(AppConfig):
name = 'vpn'
verbose_name = 'VPN'
def ready(self):
from . import search

201
netbox/vpn/choices.py Normal file
View File

@ -0,0 +1,201 @@
from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
#
# Tunnels
#
class TunnelStatusChoices(ChoiceSet):
key = 'Tunnel.status'
STATUS_PLANNED = 'planned'
STATUS_ACTIVE = 'active'
STATUS_DISABLED = 'disabled'
CHOICES = [
(STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_DISABLED, _('Disabled'), 'red'),
]
class TunnelEncapsulationChoices(ChoiceSet):
ENCAP_GRE = 'gre'
ENCAP_IP_IP = 'ip-ip'
ENCAP_IPSEC_TRANSPORT = 'ipsec-transport'
ENCAP_IPSEC_TUNNEL = 'ipsec-tunnel'
CHOICES = [
(ENCAP_IPSEC_TRANSPORT, _('IPsec - Transport')),
(ENCAP_IPSEC_TUNNEL, _('IPsec - Tunnel')),
(ENCAP_IP_IP, _('IP-in-IP')),
(ENCAP_GRE, _('GRE')),
]
class TunnelTerminationTypeChoices(ChoiceSet):
# For TunnelCreateForm
TYPE_DEVICE = 'dcim.device'
TYPE_VIRUTALMACHINE = 'virtualization.virtualmachine'
CHOICES = (
(TYPE_DEVICE, _('Device')),
(TYPE_VIRUTALMACHINE, _('Virtual Machine')),
)
class TunnelTerminationRoleChoices(ChoiceSet):
ROLE_PEER = 'peer'
ROLE_HUB = 'hub'
ROLE_SPOKE = 'spoke'
CHOICES = [
(ROLE_PEER, _('Peer'), 'green'),
(ROLE_HUB, _('Hub'), 'blue'),
(ROLE_SPOKE, _('Spoke'), 'orange'),
]
#
# Crypto
#
class IKEVersionChoices(ChoiceSet):
VERSION_1 = 1
VERSION_2 = 2
CHOICES = (
(VERSION_1, 'IKEv1'),
(VERSION_2, 'IKEv2'),
)
class IKEModeChoices(ChoiceSet):
AGGRESSIVE = 'aggressive'
MAIN = 'main'
CHOICES = (
(AGGRESSIVE, _('Aggressive')),
(MAIN, _('Main')),
)
class AuthenticationMethodChoices(ChoiceSet):
PRESHARED_KEYS = 'preshared-keys'
CERTIFICATES = 'certificates'
RSA_SIGNATURES = 'rsa-signatures'
DSA_SIGNATURES = 'dsa-signatures'
CHOICES = (
(PRESHARED_KEYS, _('Pre-shared keys')),
(CERTIFICATES, _('Certificates')),
(RSA_SIGNATURES, _('RSA signatures')),
(DSA_SIGNATURES, _('DSA signatures')),
)
class IPSecModeChoices(ChoiceSet):
ESP = 'esp'
AH = 'ah'
CHOICES = (
(ESP, 'ESP'),
(AH, 'AH'),
)
class EncryptionAlgorithmChoices(ChoiceSet):
ENCRYPTION_AES128_CBC = 'aes-128-cbc'
ENCRYPTION_AES128_GCM = 'aes-128-gcm'
ENCRYPTION_AES192_CBC = 'aes-192-cbc'
ENCRYPTION_AES192_GCM = 'aes-192-gcm'
ENCRYPTION_AES256_CBC = 'aes-256-cbc'
ENCRYPTION_AES256_GCM = 'aes-256-gcm'
ENCRYPTION_3DES = '3des-cbc'
ENCRYPTION_DES = 'des-cbc'
CHOICES = (
(ENCRYPTION_AES128_CBC, '128-bit AES (CBC)'),
(ENCRYPTION_AES128_GCM, '128-bit AES (GCM)'),
(ENCRYPTION_AES192_CBC, '192-bit AES (CBC)'),
(ENCRYPTION_AES192_GCM, '192-bit AES (GCM)'),
(ENCRYPTION_AES256_CBC, '256-bit AES (CBC)'),
(ENCRYPTION_AES256_GCM, '256-bit AES (GCM)'),
(ENCRYPTION_3DES, '3DES'),
(ENCRYPTION_3DES, 'DES'),
)
class AuthenticationAlgorithmChoices(ChoiceSet):
AUTH_HMAC_SHA1 = 'hmac-sha1'
AUTH_HMAC_SHA256 = 'hmac-sha256'
AUTH_HMAC_SHA384 = 'hmac-sha384'
AUTH_HMAC_SHA512 = 'hmac-sha512'
AUTH_HMAC_MD5 = 'hmac-md5'
CHOICES = (
(AUTH_HMAC_SHA1, 'SHA-1 HMAC'),
(AUTH_HMAC_SHA256, 'SHA-256 HMAC'),
(AUTH_HMAC_SHA384, 'SHA-384 HMAC'),
(AUTH_HMAC_SHA512, 'SHA-512 HMAC'),
(AUTH_HMAC_MD5, 'MD5 HMAC'),
)
class DHGroupChoices(ChoiceSet):
# https://www.iana.org/assignments/ikev2-parameters/ikev2-parameters.xhtml#ikev2-parameters-8
GROUP_1 = 1 # 768-bit MODP
GROUP_2 = 2 # 1024-but MODP
# Groups 3-4 reserved
GROUP_5 = 5 # 1536-bit MODP
# Groups 6-13 unassigned
GROUP_14 = 14 # 2048-bit MODP
GROUP_15 = 15 # 3072-bit MODP
GROUP_16 = 16 # 4096-bit MODP
GROUP_17 = 17 # 6144-bit MODP
GROUP_18 = 18 # 8192-bit MODP
GROUP_19 = 19 # 256-bit random ECP
GROUP_20 = 20 # 384-bit random ECP
GROUP_21 = 21 # 521-bit random ECP (521 is not a typo)
GROUP_22 = 22 # 1024-bit MODP w/160-bit prime
GROUP_23 = 23 # 2048-bit MODP w/224-bit prime
GROUP_24 = 24 # 2048-bit MODP w/256-bit prime
GROUP_25 = 25 # 192-bit ECP
GROUP_26 = 26 # 224-bit ECP
GROUP_27 = 27 # brainpoolP224r1
GROUP_28 = 28 # brainpoolP256r1
GROUP_29 = 29 # brainpoolP384r1
GROUP_30 = 30 # brainpoolP512r1
GROUP_31 = 31 # Curve25519
GROUP_32 = 32 # Curve448
GROUP_33 = 33 # GOST3410_2012_256
GROUP_34 = 34 # GOST3410_2012_512
CHOICES = (
# Strings are formatted in this manner to optimize translations
(GROUP_1, _('Group {n}').format(n=1)),
(GROUP_2, _('Group {n}').format(n=2)),
(GROUP_5, _('Group {n}').format(n=5)),
(GROUP_14, _('Group {n}').format(n=14)),
(GROUP_16, _('Group {n}').format(n=16)),
(GROUP_17, _('Group {n}').format(n=17)),
(GROUP_18, _('Group {n}').format(n=18)),
(GROUP_19, _('Group {n}').format(n=19)),
(GROUP_20, _('Group {n}').format(n=20)),
(GROUP_21, _('Group {n}').format(n=21)),
(GROUP_22, _('Group {n}').format(n=22)),
(GROUP_23, _('Group {n}').format(n=23)),
(GROUP_24, _('Group {n}').format(n=24)),
(GROUP_25, _('Group {n}').format(n=25)),
(GROUP_26, _('Group {n}').format(n=26)),
(GROUP_27, _('Group {n}').format(n=27)),
(GROUP_28, _('Group {n}').format(n=28)),
(GROUP_29, _('Group {n}').format(n=29)),
(GROUP_30, _('Group {n}').format(n=30)),
(GROUP_31, _('Group {n}').format(n=31)),
(GROUP_32, _('Group {n}').format(n=32)),
(GROUP_33, _('Group {n}').format(n=33)),
(GROUP_34, _('Group {n}').format(n=34)),
)

241
netbox/vpn/filtersets.py Normal file
View File

@ -0,0 +1,241 @@
import django_filters
from django.db.models import Q
from django.utils.translation import gettext as _
from dcim.models import Interface
from ipam.models import IPAddress
from netbox.filtersets import NetBoxModelFilterSet
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
from virtualization.models import VMInterface
from .choices import *
from .models import *
__all__ = (
'IKEPolicyFilterSet',
'IKEProposalFilterSet',
'IPSecPolicyFilterSet',
'IPSecProfileFilterSet',
'IPSecProposalFilterSet',
'TunnelFilterSet',
'TunnelTerminationFilterSet',
)
class TunnelFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
status = django_filters.MultipleChoiceFilter(
choices=TunnelStatusChoices
)
encapsulation = django_filters.MultipleChoiceFilter(
choices=TunnelEncapsulationChoices
)
ipsec_profile_id = django_filters.ModelMultipleChoiceFilter(
queryset=IPSecProfile.objects.all(),
label=_('IPSec profile (ID)'),
)
ipsec_profile = django_filters.ModelMultipleChoiceFilter(
field_name='ipsec_profile__name',
queryset=IPSecProfile.objects.all(),
to_field_name='name',
label=_('IPSec profile (name)'),
)
class Meta:
model = Tunnel
fields = ['id', 'name', 'tunnel_id']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value) |
Q(comments__icontains=value)
)
class TunnelTerminationFilterSet(NetBoxModelFilterSet):
tunnel_id = django_filters.ModelMultipleChoiceFilter(
field_name='tunnel',
queryset=Tunnel.objects.all(),
label=_('Tunnel (ID)'),
)
tunnel = django_filters.ModelMultipleChoiceFilter(
field_name='tunnel__name',
queryset=Tunnel.objects.all(),
to_field_name='name',
label=_('Tunnel (name)'),
)
role = django_filters.MultipleChoiceFilter(
choices=TunnelTerminationRoleChoices
)
termination_type = ContentTypeFilter()
interface = django_filters.ModelMultipleChoiceFilter(
field_name='interface__name',
queryset=Interface.objects.all(),
to_field_name='name',
label=_('Interface (name)'),
)
interface_id = django_filters.ModelMultipleChoiceFilter(
field_name='interface',
queryset=Interface.objects.all(),
label=_('Interface (ID)'),
)
vminterface = django_filters.ModelMultipleChoiceFilter(
field_name='vminterface__name',
queryset=VMInterface.objects.all(),
to_field_name='name',
label=_('VM interface (name)'),
)
vminterface_id = django_filters.ModelMultipleChoiceFilter(
field_name='vminterface',
queryset=VMInterface.objects.all(),
label=_('VM interface (ID)'),
)
outside_ip_id = django_filters.ModelMultipleChoiceFilter(
field_name='outside_ip',
queryset=IPAddress.objects.all(),
label=_('Outside IP (ID)'),
)
class Meta:
model = TunnelTermination
fields = ['id']
class IKEProposalFilterSet(NetBoxModelFilterSet):
authentication_method = django_filters.MultipleChoiceFilter(
choices=AuthenticationMethodChoices
)
encryption_algorithm = django_filters.MultipleChoiceFilter(
choices=EncryptionAlgorithmChoices
)
authentication_algorithm = django_filters.MultipleChoiceFilter(
choices=AuthenticationAlgorithmChoices
)
group = django_filters.MultipleChoiceFilter(
choices=DHGroupChoices
)
class Meta:
model = IKEProposal
fields = ['id', 'name', 'sa_lifetime']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
)
class IKEPolicyFilterSet(NetBoxModelFilterSet):
version = django_filters.MultipleChoiceFilter(
choices=IKEVersionChoices
)
mode = django_filters.MultipleChoiceFilter(
choices=IKEModeChoices
)
proposal_id = MultiValueNumberFilter(
field_name='proposals__id'
)
proposal = MultiValueCharFilter(
field_name='proposals__name'
)
class Meta:
model = IKEPolicy
fields = ['id', 'name', 'preshared_key']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
)
class IPSecProposalFilterSet(NetBoxModelFilterSet):
encryption_algorithm = django_filters.MultipleChoiceFilter(
choices=EncryptionAlgorithmChoices
)
authentication_algorithm = django_filters.MultipleChoiceFilter(
choices=AuthenticationAlgorithmChoices
)
class Meta:
model = IPSecProposal
fields = ['id', 'name', 'sa_lifetime_seconds', 'sa_lifetime_data']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
)
class IPSecPolicyFilterSet(NetBoxModelFilterSet):
pfs_group = django_filters.MultipleChoiceFilter(
choices=DHGroupChoices
)
proposal_id = MultiValueNumberFilter(
field_name='proposals__id'
)
proposal = MultiValueCharFilter(
field_name='proposals__name'
)
class Meta:
model = IPSecPolicy
fields = ['id', 'name']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
)
class IPSecProfileFilterSet(NetBoxModelFilterSet):
mode = django_filters.MultipleChoiceFilter(
choices=IPSecModeChoices
)
ike_policy_id = django_filters.ModelMultipleChoiceFilter(
queryset=IKEPolicy.objects.all(),
label=_('IKE policy (ID)'),
)
ike_policy = django_filters.ModelMultipleChoiceFilter(
field_name='ike_policy__name',
queryset=IKEPolicy.objects.all(),
to_field_name='name',
label=_('IKE policy (name)'),
)
ipsec_policy_id = django_filters.ModelMultipleChoiceFilter(
queryset=IPSecPolicy.objects.all(),
label=_('IPSec policy (ID)'),
)
ipsec_policy = django_filters.ModelMultipleChoiceFilter(
field_name='ipsec_policy__name',
queryset=IPSecPolicy.objects.all(),
to_field_name='name',
label=_('IPSec policy (name)'),
)
class Meta:
model = IPSecProfile
fields = ['id', 'name']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value) |
Q(comments__icontains=value)
)

View File

@ -0,0 +1,4 @@
from .bulk_edit import *
from .bulk_import import *
from .filtersets import *
from .model_forms import *

View File

@ -0,0 +1,243 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import add_blank_choice
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from vpn.choices import *
from vpn.models import *
__all__ = (
'IKEPolicyBulkEditForm',
'IKEProposalBulkEditForm',
'IPSecPolicyBulkEditForm',
'IPSecProfileBulkEditForm',
'IPSecProposalBulkEditForm',
'TunnelBulkEditForm',
'TunnelTerminationBulkEditForm',
)
class TunnelBulkEditForm(NetBoxModelBulkEditForm):
status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(TunnelStatusChoices),
required=False
)
encapsulation = forms.ChoiceField(
label=_('Encapsulation'),
choices=add_blank_choice(TunnelEncapsulationChoices),
required=False
)
ipsec_profile = DynamicModelMultipleChoiceField(
queryset=IPSecProfile.objects.all(),
label=_('IPSec profile'),
required=False
)
tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
tunnel_id = forms.IntegerField(
label=_('Tunnel ID'),
required=False
)
comments = CommentField()
model = Tunnel
fieldsets = (
(_('Tunnel'), ('status', 'encapsulation', 'tunnel_id', 'description')),
(_('Security'), ('ipsec_profile',)),
(_('Tenancy'), ('tenant',)),
)
nullable_fields = (
'ipsec_profile', 'tunnel_id', 'tenant', 'description', 'comments',
)
class TunnelTerminationBulkEditForm(NetBoxModelBulkEditForm):
role = forms.ChoiceField(
label=_('Role'),
choices=add_blank_choice(TunnelTerminationRoleChoices),
required=False
)
model = TunnelTermination
class IKEProposalBulkEditForm(NetBoxModelBulkEditForm):
authentication_method = forms.ChoiceField(
label=_('Authentication method'),
choices=add_blank_choice(AuthenticationMethodChoices),
required=False
)
encryption_algorithm = forms.ChoiceField(
label=_('Encryption algorithm'),
choices=add_blank_choice(EncryptionAlgorithmChoices),
required=False
)
authentication_algorithm = forms.ChoiceField(
label=_('Authentication algorithm'),
choices=add_blank_choice(AuthenticationAlgorithmChoices),
required=False
)
group = forms.ChoiceField(
label=_('Group'),
choices=add_blank_choice(DHGroupChoices),
required=False
)
sa_lifetime = forms.IntegerField(
label=_('SA lifetime'),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField()
model = IKEProposal
fieldsets = (
(None, (
'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime',
'description',
)),
)
nullable_fields = (
'sa_lifetime', 'description', 'comments',
)
class IKEPolicyBulkEditForm(NetBoxModelBulkEditForm):
version = forms.ChoiceField(
label=_('Version'),
choices=add_blank_choice(IKEVersionChoices),
required=False
)
mode = forms.ChoiceField(
label=_('Mode'),
choices=add_blank_choice(IKEModeChoices),
required=False
)
preshared_key = forms.CharField(
label=_('Pre-shared key'),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField()
model = IKEPolicy
fieldsets = (
(None, (
'version', 'mode', 'preshared_key', 'description',
)),
)
nullable_fields = (
'preshared_key', 'description', 'comments',
)
class IPSecProposalBulkEditForm(NetBoxModelBulkEditForm):
encryption_algorithm = forms.ChoiceField(
label=_('Encryption algorithm'),
choices=add_blank_choice(EncryptionAlgorithmChoices),
required=False
)
authentication_algorithm = forms.ChoiceField(
label=_('Authentication algorithm'),
choices=add_blank_choice(AuthenticationAlgorithmChoices),
required=False
)
sa_lifetime_seconds = forms.IntegerField(
label=_('SA lifetime (seconds)'),
required=False
)
sa_lifetime_data = forms.IntegerField(
label=_('SA lifetime (KB)'),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField()
model = IPSecProposal
fieldsets = (
(None, (
'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data',
'description',
)),
)
nullable_fields = (
'sa_lifetime_seconds', 'sa_lifetime_data', 'description', 'comments',
)
class IPSecPolicyBulkEditForm(NetBoxModelBulkEditForm):
pfs_group = forms.ChoiceField(
label=_('PFS group'),
choices=add_blank_choice(DHGroupChoices),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField()
model = IPSecPolicy
fieldsets = (
(None, ('pfs_group', 'description',)),
)
nullable_fields = (
'pfs_group', 'description', 'comments',
)
class IPSecProfileBulkEditForm(NetBoxModelBulkEditForm):
mode = forms.ChoiceField(
label=_('Mode'),
choices=add_blank_choice(IPSecModeChoices),
required=False
)
ike_policy = DynamicModelChoiceField(
label=_('IKE policy'),
queryset=IKEPolicy.objects.all(),
required=False
)
ipsec_policy = DynamicModelChoiceField(
label=_('IPSec policy'),
queryset=IPSecPolicy.objects.all(),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField()
model = IPSecProfile
fieldsets = (
(_('Profile'), (
'mode', 'ike_policy', 'ipsec_policy', 'description',
)),
)
nullable_fields = (
'description', 'comments',
)

View File

@ -0,0 +1,230 @@
from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface
from ipam.models import IPAddress
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField
from virtualization.models import VirtualMachine, VMInterface
from vpn.choices import *
from vpn.models import *
__all__ = (
'IKEPolicyImportForm',
'IKEProposalImportForm',
'IPSecPolicyImportForm',
'IPSecProfileImportForm',
'IPSecProposalImportForm',
'TunnelImportForm',
'TunnelTerminationImportForm',
)
class TunnelImportForm(NetBoxModelImportForm):
status = CSVChoiceField(
label=_('Status'),
choices=TunnelStatusChoices,
help_text=_('Operational status')
)
encapsulation = CSVChoiceField(
label=_('Encapsulation'),
choices=TunnelEncapsulationChoices,
help_text=_('Tunnel encapsulation')
)
ipsec_profile = CSVModelChoiceField(
label=_('IPSec profile'),
queryset=IPSecProfile.objects.all(),
required=False,
to_field_name='name'
)
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text=_('Assigned tenant')
)
class Meta:
model = Tunnel
fields = (
'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id', 'description', 'comments',
'tags',
)
class TunnelTerminationImportForm(NetBoxModelImportForm):
tunnel = CSVModelChoiceField(
label=_('Tunnel'),
queryset=Tunnel.objects.all(),
to_field_name='name'
)
role = CSVChoiceField(
label=_('Role'),
choices=TunnelTerminationRoleChoices,
help_text=_('Operational role')
)
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
required=False,
to_field_name='name',
help_text=_('Parent device of assigned interface')
)
virtual_machine = CSVModelChoiceField(
label=_('Virtual machine'),
queryset=VirtualMachine.objects.all(),
required=False,
to_field_name='name',
help_text=_('Parent VM of assigned interface')
)
termination = CSVModelChoiceField(
label=_('Termination'),
queryset=Interface.objects.none(), # Can also refer to VMInterface
required=False,
to_field_name='name',
help_text=_('Device or virtual machine interface')
)
outside_ip = CSVModelChoiceField(
label=_('Outside IP'),
queryset=IPAddress.objects.all(),
required=False,
to_field_name='name'
)
class Meta:
model = TunnelTermination
fields = (
'tunnel', 'role', 'outside_ip', 'tags',
)
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
if data:
# Limit termination queryset by assigned device/VM
if data.get('device'):
self.fields['termination'].queryset = Interface.objects.filter(
**{f"device__{self.fields['device'].to_field_name}": data['device']}
)
elif data.get('virtual_machine'):
self.fields['termination'].queryset = VMInterface.objects.filter(
**{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
)
def save(self, *args, **kwargs):
# Assign termination object
if self.cleaned_data.get('termination'):
self.instance.termination = self.cleaned_data['termination']
return super().save(*args, **kwargs)
class IKEProposalImportForm(NetBoxModelImportForm):
authentication_method = CSVChoiceField(
label=_('Authentication method'),
choices=AuthenticationMethodChoices
)
encryption_algorithm = CSVChoiceField(
label=_('Encryption algorithm'),
choices=EncryptionAlgorithmChoices
)
authentication_algorithm = CSVChoiceField(
label=_('Authentication algorithm'),
choices=AuthenticationAlgorithmChoices
)
group = CSVChoiceField(
label=_('Group'),
choices=DHGroupChoices
)
class Meta:
model = IKEProposal
fields = (
'name', 'description', 'authentication_method', 'encryption_algorithm', 'authentication_algorithm',
'group', 'sa_lifetime', 'tags',
)
class IKEPolicyImportForm(NetBoxModelImportForm):
version = CSVChoiceField(
label=_('Version'),
choices=IKEVersionChoices
)
mode = CSVChoiceField(
label=_('Mode'),
choices=IKEModeChoices
)
proposals = CSVModelMultipleChoiceField(
queryset=IKEProposal.objects.all(),
to_field_name='name',
help_text=_('IKE proposal(s)'),
)
class Meta:
model = IKEPolicy
fields = (
'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'tags',
)
class IPSecProposalImportForm(NetBoxModelImportForm):
encryption_algorithm = CSVChoiceField(
label=_('Encryption algorithm'),
choices=EncryptionAlgorithmChoices
)
authentication_algorithm = CSVChoiceField(
label=_('Authentication algorithm'),
choices=AuthenticationAlgorithmChoices
)
class Meta:
model = IPSecProposal
fields = (
'name', 'description', 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds',
'sa_lifetime_data', 'tags',
)
class IPSecPolicyImportForm(NetBoxModelImportForm):
pfs_group = CSVChoiceField(
label=_('Diffie-Hellman group for Perfect Forward Secrecy'),
choices=DHGroupChoices
)
proposals = CSVModelMultipleChoiceField(
queryset=IPSecProposal.objects.all(),
to_field_name='name',
help_text=_('IPSec proposal(s)'),
)
class Meta:
model = IPSecPolicy
fields = (
'name', 'description', 'proposals', 'pfs_group', 'tags',
)
class IPSecProfileImportForm(NetBoxModelImportForm):
mode = CSVChoiceField(
label=_('Mode'),
choices=IPSecModeChoices,
help_text=_('IPSec protocol')
)
ike_policy = CSVModelChoiceField(
label=_('IKE policy'),
queryset=IKEPolicy.objects.all(),
to_field_name='name'
)
ipsec_policy = CSVModelChoiceField(
label=_('IPSec policy'),
queryset=IPSecPolicy.objects.all(),
to_field_name='name'
)
class Meta:
model = IPSecProfile
fields = (
'name', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags',
)

View File

@ -0,0 +1,182 @@
from django import forms
from django.utils.translation import gettext as _
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm
from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
from vpn.choices import *
from vpn.models import *
__all__ = (
'IKEPolicyFilterForm',
'IKEProposalFilterForm',
'IPSecPolicyFilterForm',
'IPSecProfileFilterForm',
'IPSecProposalFilterForm',
'TunnelFilterForm',
'TunnelTerminationFilterForm',
)
class TunnelFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Tunnel
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Tunnel'), ('status', 'encapsulation', 'tunnel_id')),
(_('Security'), ('ipsec_profile_id',)),
(_('Tenancy'), ('tenant_group_id', 'tenant_id')),
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=TunnelStatusChoices,
required=False
)
encapsulation = forms.MultipleChoiceField(
label=_('Encapsulation'),
choices=TunnelEncapsulationChoices,
required=False
)
ipsec_profile_id = DynamicModelMultipleChoiceField(
queryset=IPSecProfile.objects.all(),
required=False,
label=_('IPSec profile')
)
tunnel_id = forms.IntegerField(
required=False,
label=_('Tunnel ID')
)
tag = TagFilterField(model)
class TunnelTerminationFilterForm(NetBoxModelFilterSetForm):
model = TunnelTermination
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Termination'), ('tunnel_id', 'role')),
)
tunnel_id = DynamicModelMultipleChoiceField(
queryset=Tunnel.objects.all(),
required=False,
label=_('Tunnel')
)
role = forms.MultipleChoiceField(
label=_('Role'),
choices=TunnelTerminationRoleChoices,
required=False
)
tag = TagFilterField(model)
class IKEProposalFilterForm(NetBoxModelFilterSetForm):
model = IKEProposal
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Parameters'), ('authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group')),
)
authentication_method = forms.MultipleChoiceField(
label=_('Authentication method'),
choices=AuthenticationMethodChoices,
required=False
)
encryption_algorithm = forms.MultipleChoiceField(
label=_('Encryption algorithm'),
choices=EncryptionAlgorithmChoices,
required=False
)
authentication_algorithm = forms.MultipleChoiceField(
label=_('Authentication algorithm'),
choices=AuthenticationAlgorithmChoices,
required=False
)
group = forms.MultipleChoiceField(
label=_('Group'),
choices=DHGroupChoices,
required=False
)
tag = TagFilterField(model)
class IKEPolicyFilterForm(NetBoxModelFilterSetForm):
model = IKEPolicy
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Parameters'), ('version', 'mode', 'proposal_id')),
)
version = forms.MultipleChoiceField(
label=_('IKE version'),
choices=IKEVersionChoices,
required=False
)
mode = forms.MultipleChoiceField(
label=_('Mode'),
choices=IKEModeChoices,
required=False
)
proposal_id = DynamicModelMultipleChoiceField(
queryset=IKEProposal.objects.all(),
required=False,
label=_('Proposal')
)
tag = TagFilterField(model)
class IPSecProposalFilterForm(NetBoxModelFilterSetForm):
model = IPSecProposal
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Parameters'), ('encryption_algorithm', 'authentication_algorithm')),
)
encryption_algorithm = forms.MultipleChoiceField(
label=_('Encryption algorithm'),
choices=EncryptionAlgorithmChoices,
required=False
)
authentication_algorithm = forms.MultipleChoiceField(
label=_('Authentication algorithm'),
choices=AuthenticationAlgorithmChoices,
required=False
)
tag = TagFilterField(model)
class IPSecPolicyFilterForm(NetBoxModelFilterSetForm):
model = IPSecPolicy
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Parameters'), ('proposal_id', 'pfs_group')),
)
proposal_id = DynamicModelMultipleChoiceField(
queryset=IKEProposal.objects.all(),
required=False,
label=_('Proposal')
)
pfs_group = forms.MultipleChoiceField(
label=_('Mode'),
choices=DHGroupChoices,
required=False
)
tag = TagFilterField(model)
class IPSecProfileFilterForm(NetBoxModelFilterSetForm):
model = IPSecProfile
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Profile'), ('mode', 'ike_policy_id', 'ipsec_policy_id')),
)
mode = forms.MultipleChoiceField(
label=_('Mode'),
choices=IPSecModeChoices,
required=False
)
ike_policy_id = DynamicModelMultipleChoiceField(
queryset=IKEPolicy.objects.all(),
required=False,
label=_('IKE policy')
)
ipsec_policy_id = DynamicModelMultipleChoiceField(
queryset=IPSecPolicy.objects.all(),
required=False,
label=_('IPSec policy')
)
tag = TagFilterField(model)

View File

@ -0,0 +1,357 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface
from ipam.models import IPAddress
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.utils import add_blank_choice
from utilities.forms.widgets import HTMXSelect
from virtualization.models import VirtualMachine, VMInterface
from vpn.choices import *
from vpn.models import *
__all__ = (
'IKEPolicyForm',
'IKEProposalForm',
'IPSecPolicyForm',
'IPSecProfileForm',
'IPSecProposalForm',
'TunnelCreateForm',
'TunnelForm',
'TunnelTerminationForm',
)
class TunnelForm(TenancyForm, NetBoxModelForm):
ipsec_profile = DynamicModelChoiceField(
queryset=IPSecProfile.objects.all(),
label=_('IPSec Profile'),
required=False
)
comments = CommentField()
fieldsets = (
(_('Tunnel'), ('name', 'status', 'encapsulation', 'description', 'tunnel_id', 'tags')),
(_('Security'), ('ipsec_profile',)),
(_('Tenancy'), ('tenant_group', 'tenant')),
)
class Meta:
model = Tunnel
fields = [
'name', 'status', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'tenant_group', 'tenant',
'comments', 'tags',
]
class TunnelCreateForm(TunnelForm):
# First termination
termination1_role = forms.ChoiceField(
choices=add_blank_choice(TunnelTerminationRoleChoices),
required=False,
label=_('Role')
)
termination1_type = forms.ChoiceField(
choices=TunnelTerminationTypeChoices,
required=False,
widget=HTMXSelect(),
label=_('Type')
)
termination1_parent = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False,
selector=True,
label=_('Device')
)
termination1_termination = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
label=_('Interface'),
query_params={
'device_id': '$termination1_parent',
}
)
termination1_outside_ip = DynamicModelChoiceField(
queryset=IPAddress.objects.all(),
label=_('Outside IP'),
required=False,
query_params={
'device_id': '$termination1_parent',
}
)
# Second termination
termination2_role = forms.ChoiceField(
choices=add_blank_choice(TunnelTerminationRoleChoices),
required=False,
label=_('Role')
)
termination2_type = forms.ChoiceField(
choices=TunnelTerminationTypeChoices,
required=False,
widget=HTMXSelect(),
label=_('Type')
)
termination2_parent = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False,
selector=True,
label=_('Device')
)
termination2_termination = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
label=_('Interface'),
query_params={
'device_id': '$termination2_parent',
}
)
termination2_outside_ip = DynamicModelChoiceField(
queryset=IPAddress.objects.all(),
required=False,
label=_('Outside IP'),
query_params={
'device_id': '$termination2_parent',
}
)
fieldsets = (
(_('Tunnel'), ('name', 'status', 'encapsulation', 'description', 'tunnel_id', 'tags')),
(_('Security'), ('ipsec_profile',)),
(_('Tenancy'), ('tenant_group', 'tenant')),
(_('First Termination'), (
'termination1_role', 'termination1_type', 'termination1_parent', 'termination1_termination',
'termination1_outside_ip',
)),
(_('Second Termination'), (
'termination2_role', 'termination2_type', 'termination2_parent', 'termination2_termination',
'termination2_outside_ip',
)),
)
def __init__(self, *args, initial=None, **kwargs):
super().__init__(*args, initial=initial, **kwargs)
if initial and initial.get('termination1_type') == TunnelTerminationTypeChoices.TYPE_VIRUTALMACHINE:
self.fields['termination1_parent'].label = _('Virtual Machine')
self.fields['termination1_parent'].queryset = VirtualMachine.objects.all()
self.fields['termination1_termination'].queryset = VMInterface.objects.all()
self.fields['termination1_termination'].widget.add_query_params({
'virtual_machine_id': '$termination1_parent',
})
self.fields['termination1_outside_ip'].widget.add_query_params({
'virtual_machine_id': '$termination1_parent',
})
if initial and initial.get('termination2_type') == TunnelTerminationTypeChoices.TYPE_VIRUTALMACHINE:
self.fields['termination2_parent'].label = _('Virtual Machine')
self.fields['termination2_parent'].queryset = VirtualMachine.objects.all()
self.fields['termination2_termination'].queryset = VMInterface.objects.all()
self.fields['termination2_termination'].widget.add_query_params({
'virtual_machine_id': '$termination2_parent',
})
self.fields['termination2_outside_ip'].widget.add_query_params({
'virtual_machine_id': '$termination2_parent',
})
def clean(self):
super().clean()
# Validate attributes for each termination (if any)
for term in ('termination1', 'termination2'):
required_parameters = (
f'{term}_role', f'{term}_parent', f'{term}_termination',
)
parameters = (
*required_parameters,
f'{term}_outside_ip',
)
if any([self.cleaned_data[param] for param in parameters]):
for param in required_parameters:
if not self.cleaned_data[param]:
raise forms.ValidationError({
param: _("This parameter is required when defining a termination.")
})
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
# Create first termination
if self.cleaned_data['termination1_termination']:
TunnelTermination.objects.create(
tunnel=instance,
role=self.cleaned_data['termination1_role'],
termination=self.cleaned_data['termination1_termination'],
outside_ip=self.cleaned_data['termination1_outside_ip'],
)
# Create second termination, if defined
if self.cleaned_data['termination2_termination']:
TunnelTermination.objects.create(
tunnel=instance,
role=self.cleaned_data['termination2_role'],
termination=self.cleaned_data['termination2_termination'],
outside_ip=self.cleaned_data.get('termination1_outside_ip'),
)
return instance
class TunnelTerminationForm(NetBoxModelForm):
tunnel = DynamicModelChoiceField(
queryset=Tunnel.objects.all()
)
type = forms.ChoiceField(
choices=TunnelTerminationTypeChoices,
widget=HTMXSelect(),
label=_('Type')
)
parent = DynamicModelChoiceField(
queryset=Device.objects.all(),
selector=True,
label=_('Device')
)
termination = DynamicModelChoiceField(
queryset=Interface.objects.all(),
label=_('Interface'),
query_params={
'device_id': '$parent',
}
)
outside_ip = DynamicModelChoiceField(
queryset=IPAddress.objects.all(),
label=_('Outside IP'),
required=False,
query_params={
'device_id': '$parent',
}
)
fieldsets = (
(None, ('tunnel', 'role', 'type', 'parent', 'termination', 'outside_ip', 'tags')),
)
class Meta:
model = TunnelTermination
fields = [
'tunnel', 'role', 'termination', 'outside_ip', 'tags',
]
def __init__(self, *args, initial=None, **kwargs):
super().__init__(*args, initial=initial, **kwargs)
if initial and initial.get('type') == TunnelTerminationTypeChoices.TYPE_VIRUTALMACHINE:
self.fields['parent'].label = _('Virtual Machine')
self.fields['parent'].queryset = VirtualMachine.objects.all()
self.fields['termination'].queryset = VMInterface.objects.all()
self.fields['termination'].widget.add_query_params({
'virtual_machine_id': '$parent',
})
self.fields['outside_ip'].widget.add_query_params({
'virtual_machine_id': '$parent',
})
if self.instance.pk:
self.fields['parent'].initial = self.instance.termination.parent_object
self.fields['termination'].initial = self.instance.termination
def clean(self):
super().clean()
# Set the terminated object
self.instance.termination = self.cleaned_data.get('termination')
class IKEProposalForm(NetBoxModelForm):
fieldsets = (
(_('Proposal'), ('name', 'description', 'tags')),
(_('Parameters'), (
'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime',
)),
)
class Meta:
model = IKEProposal
fields = [
'name', 'description', 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group',
'sa_lifetime', 'tags',
]
class IKEPolicyForm(NetBoxModelForm):
proposals = DynamicModelMultipleChoiceField(
queryset=IKEProposal.objects.all(),
label=_('Proposals')
)
fieldsets = (
(_('Policy'), ('name', 'description', 'tags')),
(_('Parameters'), ('version', 'mode', 'proposals', 'preshared_key')),
)
class Meta:
model = IKEPolicy
fields = [
'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'tags',
]
class IPSecProposalForm(NetBoxModelForm):
fieldsets = (
(_('Proposal'), ('name', 'description', 'tags')),
(_('Parameters'), (
'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data',
)),
)
class Meta:
model = IPSecProposal
fields = [
'name', 'description', 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds',
'sa_lifetime_data', 'tags',
]
class IPSecPolicyForm(NetBoxModelForm):
proposals = DynamicModelMultipleChoiceField(
queryset=IPSecProposal.objects.all(),
label=_('Proposals')
)
fieldsets = (
(_('Policy'), ('name', 'description', 'tags')),
(_('Parameters'), ('proposals', 'pfs_group')),
)
class Meta:
model = IPSecPolicy
fields = [
'name', 'description', 'proposals', 'pfs_group', 'tags',
]
class IPSecProfileForm(NetBoxModelForm):
ike_policy = DynamicModelChoiceField(
queryset=IKEPolicy.objects.all(),
label=_('IKE policy')
)
ipsec_policy = DynamicModelChoiceField(
queryset=IPSecPolicy.objects.all(),
label=_('IPSec policy')
)
comments = CommentField()
fieldsets = (
(_('Profile'), ('name', 'description', 'tags')),
(_('Parameters'), ('mode', 'ike_policy', 'ipsec_policy')),
)
class Meta:
model = IPSecProfile
fields = [
'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags',
]

View File

View File

@ -0,0 +1,51 @@
import graphene
from netbox.graphql.fields import ObjectField, ObjectListField
from utilities.graphql_optimizer import gql_query_optimizer
from vpn import models
from .types import *
class VPNQuery(graphene.ObjectType):
ike_policy = ObjectField(IKEPolicyType)
ike_policy_list = ObjectListField(IKEPolicyType)
def resolve_ike_policy_list(root, info, **kwargs):
return gql_query_optimizer(models.IKEPolicy.objects.all(), info)
ike_proposal = ObjectField(IKEProposalType)
ike_proposal_list = ObjectListField(IKEProposalType)
def resolve_ike_proposal_list(root, info, **kwargs):
return gql_query_optimizer(models.IKEProposal.objects.all(), info)
ipsec_policy = ObjectField(IPSecPolicyType)
ipsec_policy_list = ObjectListField(IPSecPolicyType)
def resolve_ipsec_policy_list(root, info, **kwargs):
return gql_query_optimizer(models.IPSecPolicy.objects.all(), info)
ipsec_profile = ObjectField(IPSecProfileType)
ipsec_profile_list = ObjectListField(IPSecProfileType)
def resolve_ipsec_profile_list(root, info, **kwargs):
return gql_query_optimizer(models.IPSecProfile.objects.all(), info)
ipsec_proposal = ObjectField(IPSecProposalType)
ipsec_proposal_list = ObjectListField(IPSecProposalType)
def resolve_ipsec_proposal_list(root, info, **kwargs):
return gql_query_optimizer(models.IPSecProposal.objects.all(), info)
tunnel = ObjectField(TunnelType)
tunnel_list = ObjectListField(TunnelType)
def resolve_tunnel_list(root, info, **kwargs):
return gql_query_optimizer(models.Tunnel.objects.all(), info)
tunnel_termination = ObjectField(TunnelTerminationType)
tunnel_termination_list = ObjectListField(TunnelTerminationType)
def resolve_tunnel_termination_list(root, info, **kwargs):
return gql_query_optimizer(models.TunnelTermination.objects.all(), info)

View File

@ -0,0 +1,69 @@
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
from vpn import filtersets, models
__all__ = (
'IKEPolicyType',
'IKEProposalType',
'IPSecPolicyType',
'IPSecProfileType',
'IPSecProposalType',
'TunnelTerminationType',
'TunnelType',
)
class TunnelTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
class Meta:
model = models.TunnelTermination
fields = '__all__'
filterset_class = filtersets.TunnelTerminationFilterSet
class TunnelType(NetBoxObjectType):
class Meta:
model = models.Tunnel
fields = '__all__'
filterset_class = filtersets.TunnelFilterSet
class IKEProposalType(OrganizationalObjectType):
class Meta:
model = models.IKEProposal
fields = '__all__'
filterset_class = filtersets.IKEProposalFilterSet
class IKEPolicyType(OrganizationalObjectType):
class Meta:
model = models.IKEPolicy
fields = '__all__'
filterset_class = filtersets.IKEPolicyFilterSet
class IPSecProposalType(OrganizationalObjectType):
class Meta:
model = models.IPSecProposal
fields = '__all__'
filterset_class = filtersets.IPSecProposalFilterSet
class IPSecPolicyType(OrganizationalObjectType):
class Meta:
model = models.IPSecPolicy
fields = '__all__'
filterset_class = filtersets.IPSecPolicyFilterSet
class IPSecProfileType(OrganizationalObjectType):
class Meta:
model = models.IPSecProfile
fields = '__all__'
filterset_class = filtersets.IPSecProfileFilterSet

View File

@ -0,0 +1,186 @@
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
import utilities.json
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('extras', '0099_cachedvalue_ordering'),
('ipam', '0067_ipaddress_index_host'),
('tenancy', '0012_contactassignment_custom_fields'),
]
operations = [
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)),
('name', models.CharField(max_length=100, unique=True)),
('description', models.CharField(blank=True, max_length=200)),
('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)),
('name', models.CharField(max_length=100, unique=True)),
('description', models.CharField(blank=True, max_length=200)),
('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)),
('name', models.CharField(max_length=100, unique=True)),
('description', models.CharField(blank=True, max_length=200)),
('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(
name='IKEProposal',
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)),
('description', models.CharField(blank=True, max_length=200)),
('authentication_method', models.CharField()),
('encryption_algorithm', models.CharField()),
('authentication_algorithm', models.CharField()),
('group', models.PositiveSmallIntegerField()),
('sa_lifetime', models.PositiveIntegerField(blank=True, null=True)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'verbose_name': 'IKE proposal',
'verbose_name_plural': 'IKE proposals',
'ordering': ('name',),
},
),
migrations.AddField(
model_name='ikepolicy',
name='proposals',
field=models.ManyToManyField(related_name='ike_policies', to='vpn.ikeproposal'),
),
migrations.AddField(
model_name='ikepolicy',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AddConstraint(
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.'),
),
]

View File

View File

@ -0,0 +1,2 @@
from .crypto import *
from .tunnels import *

254
netbox/vpn/models/crypto.py Normal file
View File

@ -0,0 +1,254 @@
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from netbox.models import NetBoxModel, PrimaryModel
from vpn.choices import *
__all__ = (
'IKEPolicy',
'IKEProposal',
'IPSecPolicy',
'IPSecProfile',
'IPSecProposal',
)
#
# IKE
#
class IKEProposal(NetBoxModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
)
description = models.CharField(
verbose_name=_('description'),
max_length=200,
blank=True
)
authentication_method = models.CharField(
verbose_name=('authentication method'),
choices=AuthenticationMethodChoices
)
encryption_algorithm = models.CharField(
verbose_name=_('encryption algorithm'),
choices=EncryptionAlgorithmChoices
)
authentication_algorithm = models.CharField(
verbose_name=_('authentication algorithm'),
choices=AuthenticationAlgorithmChoices
)
group = models.PositiveSmallIntegerField(
verbose_name=_('group'),
choices=DHGroupChoices,
help_text=_('Diffie-Hellman group ID')
)
sa_lifetime = models.PositiveIntegerField(
verbose_name=_('SA lifetime'),
blank=True,
null=True,
help_text=_('Security association lifetime (in seconds)')
)
clone_fields = (
'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime',
)
class Meta:
ordering = ('name',)
verbose_name = _('IKE proposal')
verbose_name_plural = _('IKE proposals')
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('vpn:ikeproposal', args=[self.pk])
class IKEPolicy(NetBoxModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
)
description = models.CharField(
verbose_name=_('description'),
max_length=200,
blank=True
)
version = models.PositiveSmallIntegerField(
verbose_name=_('version'),
choices=IKEVersionChoices,
default=IKEVersionChoices.VERSION_2
)
mode = models.CharField(
verbose_name=_('mode'),
choices=IKEModeChoices
)
proposals = models.ManyToManyField(
to='vpn.IKEProposal',
related_name='ike_policies',
verbose_name=_('proposals')
)
preshared_key = models.TextField(
verbose_name=_('pre-shared key'),
blank=True
)
clone_fields = (
'version', 'mode', 'proposals',
)
prerequisite_models = (
'vpn.IKEProposal',
)
class Meta:
ordering = ('name',)
verbose_name = _('IKE policy')
verbose_name_plural = _('IKE policies')
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('vpn:ikepolicy', args=[self.pk])
#
# IPSec
#
class IPSecProposal(NetBoxModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
)
description = models.CharField(
verbose_name=_('description'),
max_length=200,
blank=True
)
encryption_algorithm = models.CharField(
verbose_name=_('encryption'),
choices=EncryptionAlgorithmChoices
)
authentication_algorithm = models.CharField(
verbose_name=_('authentication'),
choices=AuthenticationAlgorithmChoices
)
sa_lifetime_seconds = models.PositiveIntegerField(
verbose_name=_('SA lifetime (seconds)'),
blank=True,
null=True,
help_text=_('Security association lifetime (seconds)')
)
sa_lifetime_data = models.PositiveIntegerField(
verbose_name=_('SA lifetime (KB)'),
blank=True,
null=True,
help_text=_('Security association lifetime (in kilobytes)')
)
clone_fields = (
'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data',
)
class Meta:
ordering = ('name',)
verbose_name = _('IPSec proposal')
verbose_name_plural = _('IPSec proposals')
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('vpn:ipsecproposal', args=[self.pk])
class IPSecPolicy(NetBoxModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
)
description = models.CharField(
verbose_name=_('description'),
max_length=200,
blank=True
)
proposals = models.ManyToManyField(
to='vpn.IPSecProposal',
related_name='ipsec_policies',
verbose_name=_('proposals')
)
pfs_group = models.PositiveSmallIntegerField(
verbose_name=_('PFS group'),
choices=DHGroupChoices,
blank=True,
null=True,
help_text=_('Diffie-Hellman group for Perfect Forward Secrecy')
)
clone_fields = (
'proposals', 'pfs_group',
)
prerequisite_models = (
'vpn.IPSecProposal',
)
class Meta:
ordering = ('name',)
verbose_name = _('IPSec policy')
verbose_name_plural = _('IPSec policies')
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('vpn:ipsecpolicy', args=[self.pk])
class IPSecProfile(PrimaryModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
)
mode = models.CharField(
verbose_name=_('mode'),
choices=IPSecModeChoices
)
ike_policy = models.ForeignKey(
to='vpn.IKEPolicy',
on_delete=models.PROTECT,
related_name='ipsec_profiles'
)
ipsec_policy = models.ForeignKey(
to='vpn.IPSecPolicy',
on_delete=models.PROTECT,
related_name='ipsec_profiles'
)
clone_fields = (
'mode', 'ike_policy', 'ipsec_policy',
)
prerequisite_models = (
'vpn.IKEPolicy',
'vpn.IPSecPolicy',
)
class Meta:
ordering = ('name',)
verbose_name = _('IPSec profile')
verbose_name_plural = _('IPSec profiles')
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('vpn:ipsecprofile', args=[self.pk])

View File

@ -0,0 +1,146 @@
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from netbox.models import ChangeLoggedModel, PrimaryModel
from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin
from vpn.choices import *
__all__ = (
'Tunnel',
'TunnelTermination',
)
class Tunnel(PrimaryModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=TunnelStatusChoices,
default=TunnelStatusChoices.STATUS_ACTIVE
)
encapsulation = models.CharField(
verbose_name=_('encapsulation'),
max_length=50,
choices=TunnelEncapsulationChoices
)
ipsec_profile = models.ForeignKey(
to='vpn.IPSecProfile',
on_delete=models.PROTECT,
related_name='tunnels',
blank=True,
null=True
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='tunnels',
blank=True,
null=True
)
tunnel_id = models.PositiveBigIntegerField(
verbose_name=_('tunnel ID'),
blank=True,
null=True
)
clone_fields = (
'status', 'encapsulation', 'ipsec_profile', 'tenant',
)
class Meta:
ordering = ('name',)
verbose_name = _('tunnel')
verbose_name_plural = _('tunnels')
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('vpn:tunnel', args=[self.pk])
def get_status_color(self):
return TunnelStatusChoices.colors.get(self.status)
class TunnelTermination(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLoggedModel):
tunnel = models.ForeignKey(
to='vpn.Tunnel',
on_delete=models.CASCADE,
related_name='terminations'
)
role = models.CharField(
verbose_name=_('role'),
max_length=50,
choices=TunnelTerminationRoleChoices,
default=TunnelTerminationRoleChoices.ROLE_PEER
)
termination_type = models.ForeignKey(
to='contenttypes.ContentType',
on_delete=models.PROTECT,
related_name='+'
)
termination_id = models.PositiveBigIntegerField(
blank=True,
null=True
)
termination = GenericForeignKey(
ct_field='termination_type',
fk_field='termination_id'
)
outside_ip = models.OneToOneField(
to='ipam.IPAddress',
on_delete=models.PROTECT,
related_name='tunnel_termination',
blank=True,
null=True
)
prerequisite_models = (
'vpn.Tunnel',
)
class Meta:
ordering = ('tunnel', 'role', 'pk')
constraints = (
models.UniqueConstraint(
fields=('termination_type', 'termination_id'),
name='%(app_label)s_%(class)s_termination',
violation_error_message=_("An object may be terminated to only one tunnel at a time.")
),
)
verbose_name = _('tunnel termination')
verbose_name_plural = _('tunnel terminations')
def __str__(self):
return f'{self.tunnel}: Termination {self.pk}'
def get_absolute_url(self):
return reverse('vpn:tunneltermination', args=[self.pk])
def get_role_color(self):
return TunnelTerminationRoleChoices.colors.get(self.role)
def clean(self):
super().clean()
# Check that the selected termination object is not already attached to a Tunnel
if getattr(self.termination, 'tunnel_termination', None) and self.termination.tunnel_termination.pk != self.pk:
raise ValidationError({
'termination': _("{name} is already attached to a tunnel ({tunnel}).").format(
name=self.termination.name,
tunnel=self.termination.tunnel_termination.tunnel
)
})
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
objectchange.related_object = self.tunnel
return objectchange

65
netbox/vpn/search.py Normal file
View File

@ -0,0 +1,65 @@
from netbox.search import SearchIndex, register_search
from . import models
@register_search
class TunnelIndex(SearchIndex):
model = models.Tunnel
fields = (
('name', 100),
('tunnel_id', 300),
('description', 500),
('comments', 5000),
)
display_attrs = ('status', 'encapsulation', 'tenant', 'description')
@register_search
class IKEProposalIndex(SearchIndex):
model = models.IKEProposal
fields = (
('name', 100),
('description', 500),
)
display_attrs = ('description',)
@register_search
class IKEPolicyIndex(SearchIndex):
model = models.IKEPolicy
fields = (
('name', 100),
('description', 500),
)
display_attrs = ('description',)
@register_search
class IPSecProposalIndex(SearchIndex):
model = models.IPSecProposal
fields = (
('name', 100),
('description', 500),
)
display_attrs = ('description',)
@register_search
class IPSecPolicyIndex(SearchIndex):
model = models.IPSecPolicy
fields = (
('name', 100),
('description', 500),
)
display_attrs = ('description',)
@register_search
class IPSecProfileIndex(SearchIndex):
model = models.IPSecProfile
fields = (
('name', 100),
('description', 500),
('comments', 5000),
)
display_attrs = ('description',)

254
netbox/vpn/tables.py Normal file
View File

@ -0,0 +1,254 @@
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from django_tables2.utils import Accessor
from tenancy.tables import TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns
from vpn.models import *
__all__ = (
'IKEPolicyTable',
'IKEProposalTable',
'IPSecPolicyTable',
'IPSecProposalTable',
'IPSecProfileTable',
'TunnelTable',
'TunnelTerminationTable',
)
class TunnelTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
status = columns.ChoiceFieldColumn(
verbose_name=_('Status')
)
ipsec_profile = tables.Column(
verbose_name=_('IPSec profile'),
linkify=True
)
terminations_count = columns.LinkedCountColumn(
accessor=Accessor('count_terminations'),
viewname='vpn:tunneltermination_list',
url_params={'tunnel_id': 'pk'},
verbose_name=_('Terminations')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
tags = columns.TagColumn(
url_name='vpn:tunnel_list'
)
class Meta(NetBoxTable.Meta):
model = Tunnel
fields = (
'pk', 'id', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tenant_group', 'tunnel_id',
'termination_count', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'terminations_count')
class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable):
tunnel = tables.Column(
verbose_name=_('Tunnel'),
linkify=True
)
role = columns.ChoiceFieldColumn(
verbose_name=_('Role')
)
termination_parent = tables.Column(
accessor='termination__parent_object',
linkify=True,
orderable=False,
verbose_name=_('Host')
)
termination = tables.Column(
verbose_name=_('Termination'),
linkify=True
)
ip_addresses = tables.ManyToManyColumn(
accessor=tables.A('termination__ip_addresses'),
orderable=False,
linkify_item=True,
verbose_name=_('IP Addresses')
)
outside_ip = tables.Column(
verbose_name=_('Outside IP'),
linkify=True
)
tags = columns.TagColumn(
url_name='vpn:tunneltermination_list'
)
class Meta(NetBoxTable.Meta):
model = TunnelTermination
fields = (
'pk', 'id', 'tunnel', 'role', 'termination_parent', 'termination', 'ip_addresses', 'outside_ip', 'tags',
'created', 'last_updated',
)
default_columns = (
'pk', 'id', 'tunnel', 'role', 'termination_parent', 'termination', 'ip_addresses', 'outside_ip',
)
class IKEProposalTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
authentication_method = tables.Column(
verbose_name=_('Authentication Method')
)
encryption_algorithm = tables.Column(
verbose_name=_('Encryption Algorithm')
)
authentication_algorithm = tables.Column(
verbose_name=_('Authentication Algorithm')
)
group = tables.Column(
verbose_name=_('Group')
)
sa_lifetime = tables.Column(
verbose_name=_('SA Lifetime')
)
tags = columns.TagColumn(
url_name='vpn:ikeproposal_list'
)
class Meta(NetBoxTable.Meta):
model = IKEProposal
fields = (
'pk', 'id', 'name', 'authentication_method', 'encryption_algorithm', 'authentication_algorithm',
'group', 'sa_lifetime', 'description', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group',
'sa_lifetime', 'description',
)
class IKEPolicyTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
version = tables.Column(
verbose_name=_('Version')
)
mode = tables.Column(
verbose_name=_('Mode')
)
proposals = tables.ManyToManyColumn(
linkify_item=True,
verbose_name=_('Proposals')
)
preshared_key = tables.Column(
verbose_name=_('Pre-shared Key')
)
tags = columns.TagColumn(
url_name='vpn:ikepolicy_list'
)
class Meta(NetBoxTable.Meta):
model = IKEPolicy
fields = (
'pk', 'id', 'name', 'version', 'mode', 'proposals', 'preshared_key', 'description', 'tags', 'created',
'last_updated',
)
default_columns = (
'pk', 'name', 'version', 'mode', 'proposals', 'description',
)
class IPSecProposalTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
encryption_algorithm = tables.Column(
verbose_name=_('Encryption Algorithm')
)
authentication_algorithm = tables.Column(
verbose_name=_('Authentication Algorithm')
)
sa_lifetime_seconds = tables.Column(
verbose_name=_('SA Lifetime (Seconds)')
)
sa_lifetime_data = tables.Column(
verbose_name=_('SA Lifetime (KB)')
)
tags = columns.TagColumn(
url_name='vpn:ipsecproposal_list'
)
class Meta(NetBoxTable.Meta):
model = IPSecProposal
fields = (
'pk', 'id', 'name', 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds',
'sa_lifetime_data', 'description', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds',
'sa_lifetime_data', 'description',
)
class IPSecPolicyTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
proposals = tables.ManyToManyColumn(
linkify_item=True,
verbose_name=_('Proposals')
)
pfs_group = tables.Column(
verbose_name=_('PFS Group')
)
tags = columns.TagColumn(
url_name='vpn:ipsecpolicy_list'
)
class Meta(NetBoxTable.Meta):
model = IPSecPolicy
fields = (
'pk', 'id', 'name', 'proposals', 'pfs_group', 'description', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'proposals', 'pfs_group', 'description',
)
class IPSecProfileTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
mode = tables.Column(
verbose_name=_('Mode')
)
ike_policy = tables.Column(
linkify=True,
verbose_name=_('IKE Policy')
)
ipsec_policy = tables.Column(
linkify=True,
verbose_name=_('IPSec Policy')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
tags = columns.TagColumn(
url_name='vpn:ipsecprofile_list'
)
class Meta(NetBoxTable.Meta):
model = IPSecProfile
fields = (
'pk', 'id', 'name', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags', 'created',
'last_updated',
)
default_columns = ('pk', 'name', 'mode', 'ike_policy', 'ipsec_policy', 'description')

View File

View File

@ -0,0 +1,473 @@
from django.urls import reverse
from dcim.choices import InterfaceTypeChoices
from dcim.models import Interface
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
from vpn.choices import *
from vpn.models import *
class AppTest(APITestCase):
def test_root(self):
url = reverse('vpn-api:api-root')
response = self.client.get('{}?format=api'.format(url), **self.header)
self.assertEqual(response.status_code, 200)
class TunnelTest(APIViewTestCases.APIViewTestCase):
model = Tunnel
brief_fields = ['display', 'id', 'name', 'url']
bulk_update_data = {
'status': TunnelStatusChoices.STATUS_PLANNED,
'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
tunnels = (
Tunnel(
name='Tunnel 1',
status=TunnelStatusChoices.STATUS_ACTIVE,
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
),
Tunnel(
name='Tunnel 2',
status=TunnelStatusChoices.STATUS_ACTIVE,
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
),
Tunnel(
name='Tunnel 3',
status=TunnelStatusChoices.STATUS_ACTIVE,
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
),
)
Tunnel.objects.bulk_create(tunnels)
cls.create_data = [
{
'name': 'Tunnel 4',
'status': TunnelStatusChoices.STATUS_DISABLED,
'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
},
{
'name': 'Tunnel 5',
'status': TunnelStatusChoices.STATUS_DISABLED,
'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
},
{
'name': 'Tunnel 6',
'status': TunnelStatusChoices.STATUS_DISABLED,
'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
},
]
class TunnelTerminationTest(APIViewTestCases.APIViewTestCase):
model = TunnelTermination
brief_fields = ['display', 'id', 'url']
bulk_update_data = {
'role': TunnelTerminationRoleChoices.ROLE_PEER,
}
@classmethod
def setUpTestData(cls):
device = create_test_device('Device 1')
interfaces = (
Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 4', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 5', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 6', type=InterfaceTypeChoices.TYPE_VIRTUAL),
)
Interface.objects.bulk_create(interfaces)
tunnel = Tunnel.objects.create(
name='Tunnel 1',
status=TunnelStatusChoices.STATUS_ACTIVE,
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
)
tunnel_terminations = (
TunnelTermination(
tunnel=tunnel,
role=TunnelTerminationRoleChoices.ROLE_HUB,
termination=interfaces[0]
),
TunnelTermination(
tunnel=tunnel,
role=TunnelTerminationRoleChoices.ROLE_HUB,
termination=interfaces[1]
),
TunnelTermination(
tunnel=tunnel,
role=TunnelTerminationRoleChoices.ROLE_HUB,
termination=interfaces[2]
),
)
TunnelTermination.objects.bulk_create(tunnel_terminations)
cls.create_data = [
{
'tunnel': tunnel.pk,
'role': TunnelTerminationRoleChoices.ROLE_PEER,
'termination_type': 'dcim.interface',
'termination_id': interfaces[3].pk,
},
{
'tunnel': tunnel.pk,
'role': TunnelTerminationRoleChoices.ROLE_PEER,
'termination_type': 'dcim.interface',
'termination_id': interfaces[4].pk,
},
{
'tunnel': tunnel.pk,
'role': TunnelTerminationRoleChoices.ROLE_PEER,
'termination_type': 'dcim.interface',
'termination_id': interfaces[5].pk,
},
]
class IKEProposalTest(APIViewTestCases.APIViewTestCase):
model = IKEProposal
brief_fields = ['display', 'id', 'name', 'url']
bulk_update_data = {
'authentication_method': AuthenticationMethodChoices.CERTIFICATES,
'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_MD5,
'group': DHGroupChoices.GROUP_19,
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
ike_proposals = (
IKEProposal(
name='IKE Proposal 1',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
),
IKEProposal(
name='IKE Proposal 2',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
),
IKEProposal(
name='IKE Proposal 3',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
),
)
IKEProposal.objects.bulk_create(ike_proposals)
cls.create_data = [
{
'name': 'IKE Proposal 4',
'authentication_method': AuthenticationMethodChoices.CERTIFICATES,
'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
'group': DHGroupChoices.GROUP_19,
},
{
'name': 'IKE Proposal 5',
'authentication_method': AuthenticationMethodChoices.CERTIFICATES,
'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
'group': DHGroupChoices.GROUP_19,
},
{
'name': 'IKE Proposal 6',
'authentication_method': AuthenticationMethodChoices.CERTIFICATES,
'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
'group': DHGroupChoices.GROUP_19,
},
]
class IKEPolicyTest(APIViewTestCases.APIViewTestCase):
model = IKEPolicy
brief_fields = ['display', 'id', 'name', 'url']
bulk_update_data = {
'version': IKEVersionChoices.VERSION_1,
'mode': IKEModeChoices.AGGRESSIVE,
'description': 'New description',
'preshared_key': 'New key',
}
@classmethod
def setUpTestData(cls):
ike_proposals = (
IKEProposal(
name='IKE Proposal 1',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
),
IKEProposal(
name='IKE Proposal 2',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
),
)
IKEProposal.objects.bulk_create(ike_proposals)
ike_policies = (
IKEPolicy(
name='IKE Policy 1',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
),
IKEPolicy(
name='IKE Policy 2',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
),
IKEPolicy(
name='IKE Policy 3',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
),
)
IKEPolicy.objects.bulk_create(ike_policies)
for ike_policy in ike_policies:
ike_policy.proposals.set(ike_proposals)
cls.create_data = [
{
'name': 'IKE Policy 4',
'version': IKEVersionChoices.VERSION_1,
'mode': IKEModeChoices.MAIN,
'proposals': [ike_proposals[0].pk, ike_proposals[1].pk],
},
{
'name': 'IKE Policy 5',
'version': IKEVersionChoices.VERSION_1,
'mode': IKEModeChoices.MAIN,
'proposals': [ike_proposals[0].pk, ike_proposals[1].pk],
},
{
'name': 'IKE Policy 6',
'version': IKEVersionChoices.VERSION_1,
'mode': IKEModeChoices.MAIN,
'proposals': [ike_proposals[0].pk, ike_proposals[1].pk],
},
]
class IPSecProposalTest(APIViewTestCases.APIViewTestCase):
model = IPSecProposal
brief_fields = ['display', 'id', 'name', 'url']
bulk_update_data = {
'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_MD5,
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
ipsec_proposals = (
IPSecProposal(
name='IPSec Proposal 1',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
),
IPSecProposal(
name='IPSec Proposal 2',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
),
IPSecProposal(
name='IPSec Proposal 3',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
),
)
IPSecProposal.objects.bulk_create(ipsec_proposals)
cls.create_data = [
{
'name': 'IPSec Proposal 4',
'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
},
{
'name': 'IPSec Proposal 5',
'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
},
{
'name': 'IPSec Proposal 6',
'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
},
]
class IPSecPolicyTest(APIViewTestCases.APIViewTestCase):
model = IPSecPolicy
brief_fields = ['display', 'id', 'name', 'url']
bulk_update_data = {
'pfs_group': DHGroupChoices.GROUP_5,
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
ipsec_proposals = (
IPSecProposal(
name='IPSec Policy 1',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
),
IPSecProposal(
name='IPSec Proposal 2',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
),
)
IPSecProposal.objects.bulk_create(ipsec_proposals)
ipsec_policies = (
IPSecPolicy(
name='IPSec Policy 1',
pfs_group=DHGroupChoices.GROUP_14
),
IPSecPolicy(
name='IPSec Policy 2',
pfs_group=DHGroupChoices.GROUP_14
),
IPSecPolicy(
name='IPSec Policy 3',
pfs_group=DHGroupChoices.GROUP_14
),
)
IPSecPolicy.objects.bulk_create(ipsec_policies)
for ipsec_policy in ipsec_policies:
ipsec_policy.proposals.set(ipsec_proposals)
cls.create_data = [
{
'name': 'IPSec Policy 4',
'pfs_group': DHGroupChoices.GROUP_16,
'proposals': [ipsec_proposals[0].pk, ipsec_proposals[1].pk],
},
{
'name': 'IPSec Policy 5',
'pfs_group': DHGroupChoices.GROUP_16,
'proposals': [ipsec_proposals[0].pk, ipsec_proposals[1].pk],
},
{
'name': 'IPSec Policy 6',
'pfs_group': DHGroupChoices.GROUP_16,
'proposals': [ipsec_proposals[0].pk, ipsec_proposals[1].pk],
},
]
class IPSecProfileTest(APIViewTestCases.APIViewTestCase):
model = IPSecProfile
brief_fields = ['display', 'id', 'name', 'url']
@classmethod
def setUpTestData(cls):
ike_proposal = IKEProposal.objects.create(
name='IKE Proposal 1',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
)
ipsec_proposal = IPSecProposal.objects.create(
name='IPSec Proposal 1',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
)
ike_policies = (
IKEPolicy(
name='IKE Policy 1',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
),
IKEPolicy(
name='IKE Policy 2',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
),
)
IKEPolicy.objects.bulk_create(ike_policies)
for ike_policy in ike_policies:
ike_policy.proposals.add(ike_proposal)
ipsec_policies = (
IPSecPolicy(
name='IPSec Policy 1',
pfs_group=DHGroupChoices.GROUP_14
),
IPSecPolicy(
name='IPSec Policy 2',
pfs_group=DHGroupChoices.GROUP_14
),
)
IPSecPolicy.objects.bulk_create(ipsec_policies)
for ipsec_policy in ipsec_policies:
ipsec_policy.proposals.add(ipsec_proposal)
ipsec_profiles = (
IPSecProfile(
name='IPSec Profile 1',
mode=IPSecModeChoices.ESP,
ike_policy=ike_policies[0],
ipsec_policy=ipsec_policies[0]
),
IPSecProfile(
name='IPSec Profile 2',
mode=IPSecModeChoices.ESP,
ike_policy=ike_policies[0],
ipsec_policy=ipsec_policies[0]
),
IPSecProfile(
name='IPSec Profile 3',
mode=IPSecModeChoices.ESP,
ike_policy=ike_policies[0],
ipsec_policy=ipsec_policies[0]
),
)
IPSecProfile.objects.bulk_create(ipsec_profiles)
cls.create_data = [
{
'name': 'IPSec Profile 4',
'mode': IPSecModeChoices.AH,
'ike_policy': ike_policies[1].pk,
'ipsec_policy': ipsec_policies[1].pk,
},
]
cls.bulk_update_data = {
'mode': IPSecModeChoices.AH,
'ike_policy': ike_policies[1].pk,
'ipsec_policy': ipsec_policies[1].pk,
'description': 'New description',
}

View File

@ -0,0 +1,592 @@
from django.test import TestCase
from dcim.choices import InterfaceTypeChoices
from dcim.models import Interface
from ipam.models import IPAddress
from virtualization.models import VMInterface
from vpn.choices import *
from vpn.filtersets import *
from vpn.models import *
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
class TunnelTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Tunnel.objects.all()
filterset = TunnelFilterSet
@classmethod
def setUpTestData(cls):
ike_proposal = IKEProposal.objects.create(
name='IKE Proposal 1',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
)
ike_policy = IKEPolicy.objects.create(
name='IKE Policy 1',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
)
ike_policy.proposals.add(ike_proposal)
ipsec_proposal = IPSecProposal.objects.create(
name='IPSec Proposal 1',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
)
ipsec_policy = IPSecPolicy.objects.create(
name='IPSec Policy 1',
pfs_group=DHGroupChoices.GROUP_14
)
ipsec_policy.proposals.add(ipsec_proposal)
ipsec_profiles = (
IPSecProfile(
name='IPSec Profile 1',
mode=IPSecModeChoices.ESP,
ike_policy=ike_policy,
ipsec_policy=ipsec_policy
),
IPSecProfile(
name='IPSec Profile 2',
mode=IPSecModeChoices.ESP,
ike_policy=ike_policy,
ipsec_policy=ipsec_policy
),
)
IPSecProfile.objects.bulk_create(ipsec_profiles)
tunnels = (
Tunnel(
name='Tunnel 1',
status=TunnelStatusChoices.STATUS_ACTIVE,
encapsulation=TunnelEncapsulationChoices.ENCAP_GRE,
ipsec_profile=ipsec_profiles[0],
tunnel_id=100
),
Tunnel(
name='Tunnel 2',
status=TunnelStatusChoices.STATUS_PLANNED,
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP,
ipsec_profile=ipsec_profiles[0],
tunnel_id=200
),
Tunnel(
name='Tunnel 3',
status=TunnelStatusChoices.STATUS_DISABLED,
encapsulation=TunnelEncapsulationChoices.ENCAP_IPSEC_TUNNEL,
ipsec_profile=None,
tunnel_id=300
),
)
Tunnel.objects.bulk_create(tunnels)
def test_name(self):
params = {'name': ['Tunnel 1', 'Tunnel 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_status(self):
params = {'status': [TunnelStatusChoices.STATUS_ACTIVE, TunnelStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_encapsulation(self):
params = {'encapsulation': [TunnelEncapsulationChoices.ENCAP_GRE, TunnelEncapsulationChoices.ENCAP_IP_IP]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_ipsec_profile(self):
ipsec_profiles = IPSecProfile.objects.all()[:2]
params = {'ipsec_profile_id': [ipsec_profiles[0].pk, ipsec_profiles[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'ipsec_profile': [ipsec_profiles[0].name, ipsec_profiles[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tunnel_id(self):
params = {'tunnel_id': [100, 200]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class TunnelTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = TunnelTermination.objects.all()
filterset = TunnelTerminationFilterSet
@classmethod
def setUpTestData(cls):
device = create_test_device('Device 1')
interfaces = (
Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
)
Interface.objects.bulk_create(interfaces)
virtual_machine = create_test_virtualmachine('Virtual Machine 1')
vm_interfaces = (
VMInterface(virtual_machine=virtual_machine, name='Interface 1'),
VMInterface(virtual_machine=virtual_machine, name='Interface 2'),
VMInterface(virtual_machine=virtual_machine, name='Interface 3'),
)
VMInterface.objects.bulk_create(vm_interfaces)
ip_addresses = (
IPAddress(address='192.168.0.1/32'),
IPAddress(address='192.168.0.2/32'),
IPAddress(address='192.168.0.3/32'),
IPAddress(address='192.168.0.4/32'),
IPAddress(address='192.168.0.5/32'),
IPAddress(address='192.168.0.6/32'),
)
IPAddress.objects.bulk_create(ip_addresses)
tunnels = (
Tunnel(
name='Tunnel 1',
status=TunnelStatusChoices.STATUS_ACTIVE,
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
),
Tunnel(
name='Tunnel 2',
status=TunnelStatusChoices.STATUS_ACTIVE,
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
),
Tunnel(
name='Tunnel 3',
status=TunnelStatusChoices.STATUS_ACTIVE,
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
),
)
Tunnel.objects.bulk_create(tunnels)
tunnel_terminations = (
# Tunnel 1
TunnelTermination(
tunnel=tunnels[0],
role=TunnelTerminationRoleChoices.ROLE_HUB,
termination=interfaces[0],
outside_ip=ip_addresses[0]
),
TunnelTermination(
tunnel=tunnels[0],
role=TunnelTerminationRoleChoices.ROLE_SPOKE,
termination=vm_interfaces[0],
outside_ip=ip_addresses[1]
),
# Tunnel 2
TunnelTermination(
tunnel=tunnels[1],
role=TunnelTerminationRoleChoices.ROLE_HUB,
termination=interfaces[1],
outside_ip=ip_addresses[2]
),
TunnelTermination(
tunnel=tunnels[1],
role=TunnelTerminationRoleChoices.ROLE_SPOKE,
termination=vm_interfaces[1],
outside_ip=ip_addresses[3]
),
# Tunnel 3
TunnelTermination(
tunnel=tunnels[2],
role=TunnelTerminationRoleChoices.ROLE_PEER,
termination=interfaces[2],
outside_ip=ip_addresses[4]
),
TunnelTermination(
tunnel=tunnels[2],
role=TunnelTerminationRoleChoices.ROLE_PEER,
termination=vm_interfaces[2],
outside_ip=ip_addresses[5]
),
)
TunnelTermination.objects.bulk_create(tunnel_terminations)
def test_tunnel(self):
tunnels = Tunnel.objects.all()[:2]
params = {'tunnel_id': [tunnels[0].pk, tunnels[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'tunnel': [tunnels[0].name, tunnels[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_role(self):
params = {'role': [TunnelTerminationRoleChoices.ROLE_HUB, TunnelTerminationRoleChoices.ROLE_SPOKE]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_termination_type(self):
params = {'termination_type': 'dcim.interface'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'termination_type': 'virtualization.vminterface'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_interface(self):
interfaces = Interface.objects.all()[:2]
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'interface': [interfaces[0].name, interfaces[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_vminterface(self):
vm_interfaces = VMInterface.objects.all()[:2]
params = {'vminterface_id': [vm_interfaces[0].pk, vm_interfaces[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'vminterface': [vm_interfaces[0].name, vm_interfaces[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_outside_ip(self):
ip_addresses = IPAddress.objects.all()[:2]
params = {'outside_ip_id': [ip_addresses[0].pk, ip_addresses[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class IKEProposalTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = IKEProposal.objects.all()
filterset = IKEProposalFilterSet
@classmethod
def setUpTestData(cls):
ike_proposals = (
IKEProposal(
name='IKE Proposal 1',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_1,
sa_lifetime=1000
),
IKEProposal(
name='IKE Proposal 2',
authentication_method=AuthenticationMethodChoices.CERTIFICATES,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
group=DHGroupChoices.GROUP_2,
sa_lifetime=2000
),
IKEProposal(
name='IKE Proposal 3',
authentication_method=AuthenticationMethodChoices.RSA_SIGNATURES,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA512,
group=DHGroupChoices.GROUP_5,
sa_lifetime=3000
),
)
IKEProposal.objects.bulk_create(ike_proposals)
def test_name(self):
params = {'name': ['IKE Proposal 1', 'IKE Proposal 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_authentication_method(self):
params = {'authentication_method': [
AuthenticationMethodChoices.PRESHARED_KEYS, AuthenticationMethodChoices.CERTIFICATES
]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_encryption_algorithm(self):
params = {'encryption_algorithm': [
EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC, EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC
]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_authentication_algorithm(self):
params = {'authentication_algorithm': [
AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1, AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256
]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_group(self):
params = {'group': [DHGroupChoices.GROUP_1, DHGroupChoices.GROUP_2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_sa_lifetime(self):
params = {'sa_lifetime': [1000, 2000]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class IKEPolicyTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = IKEPolicy.objects.all()
filterset = IKEPolicyFilterSet
@classmethod
def setUpTestData(cls):
ike_proposals = (
IKEProposal(
name='IKE Proposal 1',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
),
IKEProposal(
name='IKE Proposal 2',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
),
IKEProposal(
name='IKE Proposal 3',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
),
)
IKEProposal.objects.bulk_create(ike_proposals)
ike_policies = (
IKEPolicy(
name='IKE Policy 1',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
),
IKEPolicy(
name='IKE Policy 2',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
),
IKEPolicy(
name='IKE Policy 3',
version=IKEVersionChoices.VERSION_2,
mode=IKEModeChoices.AGGRESSIVE,
),
)
IKEPolicy.objects.bulk_create(ike_policies)
ike_policies[0].proposals.add(ike_proposals[0])
ike_policies[1].proposals.add(ike_proposals[1])
ike_policies[2].proposals.add(ike_proposals[2])
def test_name(self):
params = {'name': ['IKE Policy 1', 'IKE Policy 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_version(self):
params = {'version': [IKEVersionChoices.VERSION_1]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_mode(self):
params = {'mode': [IKEModeChoices.MAIN]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_proposal(self):
proposals = IKEProposal.objects.all()[:2]
params = {'proposal_id': [proposals[0].pk, proposals[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'proposal': [proposals[0].name, proposals[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class IPSecProposalTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = IPSecProposal.objects.all()
filterset = IPSecProposalFilterSet
@classmethod
def setUpTestData(cls):
ipsec_proposals = (
IPSecProposal(
name='IPSec Proposal 1',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
sa_lifetime_seconds=1000,
sa_lifetime_data=1000
),
IPSecProposal(
name='IPSec Proposal 2',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
sa_lifetime_seconds=2000,
sa_lifetime_data=2000
),
IPSecProposal(
name='IPSec Proposal 3',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES256_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA512,
sa_lifetime_seconds=3000,
sa_lifetime_data=3000
),
)
IPSecProposal.objects.bulk_create(ipsec_proposals)
def test_name(self):
params = {'name': ['IPSec Proposal 1', 'IPSec Proposal 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_encryption_algorithm(self):
params = {'encryption_algorithm': [
EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC, EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC
]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_authentication_algorithm(self):
params = {'authentication_algorithm': [
AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1, AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256
]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_sa_lifetime_seconds(self):
params = {'sa_lifetime_seconds': [1000, 2000]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_sa_lifetime_data(self):
params = {'sa_lifetime_data': [1000, 2000]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class IPSecPolicyTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = IPSecPolicy.objects.all()
filterset = IPSecPolicyFilterSet
@classmethod
def setUpTestData(cls):
ipsec_proposals = (
IPSecProposal(
name='IPSec Policy 1',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
),
IPSecProposal(
name='IPSec Proposal 2',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
),
IPSecProposal(
name='IPSec Proposal 3',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
),
)
IPSecProposal.objects.bulk_create(ipsec_proposals)
ipsec_policies = (
IPSecPolicy(
name='IPSec Policy 1',
pfs_group=DHGroupChoices.GROUP_1
),
IPSecPolicy(
name='IPSec Policy 2',
pfs_group=DHGroupChoices.GROUP_2
),
IPSecPolicy(
name='IPSec Policy 3',
pfs_group=DHGroupChoices.GROUP_5
),
)
IPSecPolicy.objects.bulk_create(ipsec_policies)
ipsec_policies[0].proposals.add(ipsec_proposals[0])
ipsec_policies[1].proposals.add(ipsec_proposals[1])
ipsec_policies[2].proposals.add(ipsec_proposals[2])
def test_name(self):
params = {'name': ['IPSec Policy 1', 'IPSec Policy 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_pfs_group(self):
params = {'pfs_group': [DHGroupChoices.GROUP_1, DHGroupChoices.GROUP_2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_proposal(self):
proposals = IPSecProposal.objects.all()[:2]
params = {'proposal_id': [proposals[0].pk, proposals[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'proposal': [proposals[0].name, proposals[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class IPSecProfileTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = IPSecProfile.objects.all()
filterset = IPSecProfileFilterSet
@classmethod
def setUpTestData(cls):
ike_proposal = IKEProposal.objects.create(
name='IKE Proposal 1',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
)
ipsec_proposal = IPSecProposal.objects.create(
name='IPSec Proposal 1',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
)
ike_policies = (
IKEPolicy(
name='IKE Policy 1',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
),
IKEPolicy(
name='IKE Policy 2',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
),
IKEPolicy(
name='IKE Policy 3',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
),
)
IKEPolicy.objects.bulk_create(ike_policies)
for ike_policy in ike_policies:
ike_policy.proposals.add(ike_proposal)
ipsec_policies = (
IPSecPolicy(
name='IPSec Policy 1',
pfs_group=DHGroupChoices.GROUP_14
),
IPSecPolicy(
name='IPSec Policy 2',
pfs_group=DHGroupChoices.GROUP_14
),
IPSecPolicy(
name='IPSec Policy 3',
pfs_group=DHGroupChoices.GROUP_14
),
)
IPSecPolicy.objects.bulk_create(ipsec_policies)
for ipsec_policy in ipsec_policies:
ipsec_policy.proposals.add(ipsec_proposal)
ipsec_profiles = (
IPSecProfile(
name='IPSec Profile 1',
mode=IPSecModeChoices.ESP,
ike_policy=ike_policies[0],
ipsec_policy=ipsec_policies[0]
),
IPSecProfile(
name='IPSec Profile 2',
mode=IPSecModeChoices.ESP,
ike_policy=ike_policies[1],
ipsec_policy=ipsec_policies[1]
),
IPSecProfile(
name='IPSec Profile 3',
mode=IPSecModeChoices.AH,
ike_policy=ike_policies[2],
ipsec_policy=ipsec_policies[2]
),
)
IPSecProfile.objects.bulk_create(ipsec_profiles)
def test_name(self):
params = {'name': ['IPSec Profile 1', 'IPSec Profile 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_mode(self):
params = {'mode': [IPSecModeChoices.ESP]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_ike_policy(self):
ike_policies = IKEPolicy.objects.all()[:2]
params = {'ike_policy_id': [ike_policies[0].pk, ike_policies[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'ike_policy': [ike_policies[0].name, ike_policies[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_ipsec_policy(self):
ipsec_policies = IPSecPolicy.objects.all()[:2]
params = {'ipsec_policy_id': [ipsec_policies[0].pk, ipsec_policies[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'ipsec_policy': [ipsec_policies[0].name, ipsec_policies[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@ -0,0 +1,508 @@
from dcim.choices import InterfaceTypeChoices
from dcim.models import Interface
from vpn.choices import *
from vpn.models import *
from utilities.testing import ViewTestCases, create_tags, create_test_device
class TunnelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = Tunnel
@classmethod
def setUpTestData(cls):
tunnels = (
Tunnel(
name='Tunnel 1',
status=TunnelStatusChoices.STATUS_ACTIVE,
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
),
Tunnel(
name='Tunnel 2',
status=TunnelStatusChoices.STATUS_ACTIVE,
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
),
Tunnel(
name='Tunnel 3',
status=TunnelStatusChoices.STATUS_ACTIVE,
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
),
)
Tunnel.objects.bulk_create(tunnels)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Tunnel X',
'description': 'New tunnel',
'status': TunnelStatusChoices.STATUS_PLANNED,
'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"name,status,encapsulation",
"Tunnel 4,planned,gre",
"Tunnel 5,planned,gre",
"Tunnel 6,planned,gre",
)
cls.csv_update_data = (
"id,status,encapsulation",
f"{tunnels[0].pk},active,ip-ip",
f"{tunnels[1].pk},active,ip-ip",
f"{tunnels[2].pk},active,ip-ip",
)
cls.bulk_edit_data = {
'description': 'New description',
'status': TunnelStatusChoices.STATUS_DISABLED,
'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
}
class TunnelTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = TunnelTermination
# TODO: Workaround for conflict between form field and GFK
validation_excluded_fields = ('termination',)
@classmethod
def setUpTestData(cls):
device = create_test_device('Device 1')
interfaces = (
Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 4', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 5', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 6', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 7', type=InterfaceTypeChoices.TYPE_VIRTUAL),
)
Interface.objects.bulk_create(interfaces)
tunnel = Tunnel.objects.create(
name='Tunnel 1',
status=TunnelStatusChoices.STATUS_ACTIVE,
encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
)
tunnel_terminations = (
TunnelTermination(
tunnel=tunnel,
role=TunnelTerminationRoleChoices.ROLE_HUB,
termination=interfaces[0]
),
TunnelTermination(
tunnel=tunnel,
role=TunnelTerminationRoleChoices.ROLE_SPOKE,
termination=interfaces[1]
),
TunnelTermination(
tunnel=tunnel,
role=TunnelTerminationRoleChoices.ROLE_SPOKE,
termination=interfaces[2]
),
)
TunnelTermination.objects.bulk_create(tunnel_terminations)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'tunnel': tunnel.pk,
'role': TunnelTerminationRoleChoices.ROLE_PEER,
'type': TunnelTerminationTypeChoices.TYPE_DEVICE,
'parent': device.pk,
'termination': interfaces[6].pk,
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"tunnel,role,device,termination",
"Tunnel 1,peer,Device 1,Interface 4",
"Tunnel 1,peer,Device 1,Interface 5",
"Tunnel 1,peer,Device 1,Interface 6",
)
cls.csv_update_data = (
"id,role",
f"{tunnel_terminations[0].pk},peer",
f"{tunnel_terminations[1].pk},peer",
f"{tunnel_terminations[2].pk},peer",
)
cls.bulk_edit_data = {
'role': TunnelTerminationRoleChoices.ROLE_PEER,
}
class IKEProposalTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = IKEProposal
@classmethod
def setUpTestData(cls):
ike_proposals = (
IKEProposal(
name='IKE Proposal 1',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
),
IKEProposal(
name='IKE Proposal 2',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
),
IKEProposal(
name='IKE Proposal 3',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
),
)
IKEProposal.objects.bulk_create(ike_proposals)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'IKE Proposal X',
'authentication_method': AuthenticationMethodChoices.CERTIFICATES,
'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
'group': DHGroupChoices.GROUP_19,
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"name,authentication_method,encryption_algorithm,authentication_algorithm,group",
"IKE Proposal 4,preshared-keys,aes-128-cbc,hmac-sha1,14",
"IKE Proposal 5,preshared-keys,aes-128-cbc,hmac-sha1,14",
"IKE Proposal 6,preshared-keys,aes-128-cbc,hmac-sha1,14",
)
cls.csv_update_data = (
"id,description",
f"{ike_proposals[0].pk},New description",
f"{ike_proposals[1].pk},New description",
f"{ike_proposals[2].pk},New description",
)
cls.bulk_edit_data = {
'description': 'New description',
'authentication_method': AuthenticationMethodChoices.CERTIFICATES,
'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
'group': DHGroupChoices.GROUP_19
}
class IKEPolicyTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = IKEPolicy
@classmethod
def setUpTestData(cls):
ike_proposals = (
IKEProposal(
name='IKE Proposal 1',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
),
IKEProposal(
name='IKE Proposal 2',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
),
)
IKEProposal.objects.bulk_create(ike_proposals)
ike_policies = (
IKEPolicy(
name='IKE Policy 1',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
),
IKEPolicy(
name='IKE Policy 2',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
),
IKEPolicy(
name='IKE Policy 3',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
),
)
IKEPolicy.objects.bulk_create(ike_policies)
for ike_policy in ike_policies:
ike_policy.proposals.set(ike_proposals)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'IKE Policy X',
'version': IKEVersionChoices.VERSION_2,
'mode': IKEModeChoices.AGGRESSIVE,
'proposals': [p.pk for p in ike_proposals],
'tags': [t.pk for t in tags],
}
ike_proposal_names = ','.join([p.name for p in ike_proposals])
cls.csv_data = (
"name,version,mode,proposals",
f"IKE Proposal 4,2,aggressive,\"{ike_proposal_names}\"",
f"IKE Proposal 5,2,aggressive,\"{ike_proposal_names}\"",
f"IKE Proposal 6,2,aggressive,\"{ike_proposal_names}\"",
)
cls.csv_update_data = (
"id,description",
f"{ike_policies[0].pk},New description",
f"{ike_policies[1].pk},New description",
f"{ike_policies[2].pk},New description",
)
cls.bulk_edit_data = {
'description': 'New description',
'version': IKEVersionChoices.VERSION_2,
'mode': IKEModeChoices.AGGRESSIVE,
}
class IPSecProposalTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = IPSecProposal
@classmethod
def setUpTestData(cls):
ipsec_proposals = (
IPSecProposal(
name='IPSec Proposal 1',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
),
IPSecProposal(
name='IPSec Proposal 2',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
),
IPSecProposal(
name='IPSec Proposal 3',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
),
)
IPSecProposal.objects.bulk_create(ipsec_proposals)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'IPSec Proposal X',
'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
'sa_lifetime_seconds': 3600,
'sa_lifetime_data': 1000000,
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"name,encryption_algorithm,authentication_algorithm,sa_lifetime_seconds,sa_lifetime_data",
"IKE Proposal 4,aes-128-cbc,hmac-sha1,3600,1000000",
"IKE Proposal 5,aes-128-cbc,hmac-sha1,3600,1000000",
"IKE Proposal 6,aes-128-cbc,hmac-sha1,3600,1000000",
)
cls.csv_update_data = (
"id,description",
f"{ipsec_proposals[0].pk},New description",
f"{ipsec_proposals[1].pk},New description",
f"{ipsec_proposals[2].pk},New description",
)
cls.bulk_edit_data = {
'description': 'New description',
'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
'sa_lifetime_seconds': 3600,
'sa_lifetime_data': 1000000,
}
class IPSecPolicyTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = IPSecPolicy
@classmethod
def setUpTestData(cls):
ipsec_proposals = (
IPSecProposal(
name='IPSec Policy 1',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
),
IPSecProposal(
name='IPSec Proposal 2',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
),
)
IPSecProposal.objects.bulk_create(ipsec_proposals)
ipsec_policies = (
IPSecPolicy(
name='IPSec Policy 1',
pfs_group=DHGroupChoices.GROUP_14
),
IPSecPolicy(
name='IPSec Policy 2',
pfs_group=DHGroupChoices.GROUP_14
),
IPSecPolicy(
name='IPSec Policy 3',
pfs_group=DHGroupChoices.GROUP_14
),
)
IPSecPolicy.objects.bulk_create(ipsec_policies)
for ipsec_policy in ipsec_policies:
ipsec_policy.proposals.set(ipsec_proposals)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'IPSec Policy X',
'pfs_group': DHGroupChoices.GROUP_5,
'proposals': [p.pk for p in ipsec_proposals],
'tags': [t.pk for t in tags],
}
ipsec_proposal_names = ','.join([p.name for p in ipsec_proposals])
cls.csv_data = (
"name,pfs_group,proposals",
f"IKE Proposal 4,19,\"{ipsec_proposal_names}\"",
f"IKE Proposal 5,19,\"{ipsec_proposal_names}\"",
f"IKE Proposal 6,19,\"{ipsec_proposal_names}\"",
)
cls.csv_update_data = (
"id,description",
f"{ipsec_policies[0].pk},New description",
f"{ipsec_policies[1].pk},New description",
f"{ipsec_policies[2].pk},New description",
)
cls.bulk_edit_data = {
'description': 'New description',
'pfs_group': DHGroupChoices.GROUP_5,
}
class IPSecProfileTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = IPSecProfile
@classmethod
def setUpTestData(cls):
ike_proposal = IKEProposal.objects.create(
name='IKE Proposal 1',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14
)
ipsec_proposal = IPSecProposal.objects.create(
name='IPSec Proposal 1',
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
)
ike_policies = (
IKEPolicy(
name='IKE Policy 1',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
),
IKEPolicy(
name='IKE Policy 2',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
),
)
IKEPolicy.objects.bulk_create(ike_policies)
for ike_policy in ike_policies:
ike_policy.proposals.add(ike_proposal)
ipsec_policies = (
IPSecPolicy(
name='IPSec Policy 1',
pfs_group=DHGroupChoices.GROUP_14
),
IPSecPolicy(
name='IPSec Policy 2',
pfs_group=DHGroupChoices.GROUP_14
),
)
IPSecPolicy.objects.bulk_create(ipsec_policies)
for ipsec_policy in ipsec_policies:
ipsec_policy.proposals.add(ipsec_proposal)
ipsec_profiles = (
IPSecProfile(
name='IPSec Profile 1',
mode=IPSecModeChoices.ESP,
ike_policy=ike_policies[0],
ipsec_policy=ipsec_policies[0]
),
IPSecProfile(
name='IPSec Profile 2',
mode=IPSecModeChoices.ESP,
ike_policy=ike_policies[0],
ipsec_policy=ipsec_policies[0]
),
IPSecProfile(
name='IPSec Profile 3',
mode=IPSecModeChoices.ESP,
ike_policy=ike_policies[0],
ipsec_policy=ipsec_policies[0]
),
)
IPSecProfile.objects.bulk_create(ipsec_profiles)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'IPSec Profile X',
'mode': IPSecModeChoices.AH,
'ike_policy': ike_policies[1].pk,
'ipsec_policy': ipsec_policies[1].pk,
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"name,mode,ike_policy,ipsec_policy",
f"IKE Proposal 4,ah,IKE Policy 2,IPSec Policy 2",
f"IKE Proposal 5,ah,IKE Policy 2,IPSec Policy 2",
f"IKE Proposal 6,ah,IKE Policy 2,IPSec Policy 2",
)
cls.csv_update_data = (
"id,description",
f"{ipsec_profiles[0].pk},New description",
f"{ipsec_profiles[1].pk},New description",
f"{ipsec_profiles[2].pk},New description",
)
cls.bulk_edit_data = {
'description': 'New description',
'mode': IPSecModeChoices.AH,
'ike_policy': ike_policies[1].pk,
'ipsec_policy': ipsec_policies[1].pk,
}

65
netbox/vpn/urls.py Normal file
View File

@ -0,0 +1,65 @@
from django.urls import include, path
from utilities.urls import get_model_urls
from . import views
app_name = 'vpn'
urlpatterns = [
# Tunnels
path('tunnels/', views.TunnelListView.as_view(), name='tunnel_list'),
path('tunnels/add/', views.TunnelEditView.as_view(), name='tunnel_add'),
path('tunnels/import/', views.TunnelBulkImportView.as_view(), name='tunnel_import'),
path('tunnels/edit/', views.TunnelBulkEditView.as_view(), name='tunnel_bulk_edit'),
path('tunnels/delete/', views.TunnelBulkDeleteView.as_view(), name='tunnel_bulk_delete'),
path('tunnels/<int:pk>/', include(get_model_urls('vpn', 'tunnel'))),
# Tunnel terminations
path('tunnel-terminations/', views.TunnelTerminationListView.as_view(), name='tunneltermination_list'),
path('tunnel-terminations/add/', views.TunnelTerminationEditView.as_view(), name='tunneltermination_add'),
path('tunnel-terminations/import/', views.TunnelTerminationBulkImportView.as_view(), name='tunneltermination_import'),
path('tunnel-terminations/edit/', views.TunnelTerminationBulkEditView.as_view(), name='tunneltermination_bulk_edit'),
path('tunnel-terminations/delete/', views.TunnelTerminationBulkDeleteView.as_view(), name='tunneltermination_bulk_delete'),
path('tunnel-terminations/<int:pk>/', include(get_model_urls('vpn', 'tunneltermination'))),
# IKE proposals
path('ike-proposals/', views.IKEProposalListView.as_view(), name='ikeproposal_list'),
path('ike-proposals/add/', views.IKEProposalEditView.as_view(), name='ikeproposal_add'),
path('ike-proposals/import/', views.IKEProposalBulkImportView.as_view(), name='ikeproposal_import'),
path('ike-proposals/edit/', views.IKEProposalBulkEditView.as_view(), name='ikeproposal_bulk_edit'),
path('ike-proposals/delete/', views.IKEProposalBulkDeleteView.as_view(), name='ikeproposal_bulk_delete'),
path('ike-proposals/<int:pk>/', include(get_model_urls('vpn', 'ikeproposal'))),
# IKE policies
path('ike-policys/', views.IKEPolicyListView.as_view(), name='ikepolicy_list'),
path('ike-policys/add/', views.IKEPolicyEditView.as_view(), name='ikepolicy_add'),
path('ike-policys/import/', views.IKEPolicyBulkImportView.as_view(), name='ikepolicy_import'),
path('ike-policys/edit/', views.IKEPolicyBulkEditView.as_view(), name='ikepolicy_bulk_edit'),
path('ike-policys/delete/', views.IKEPolicyBulkDeleteView.as_view(), name='ikepolicy_bulk_delete'),
path('ike-policys/<int:pk>/', include(get_model_urls('vpn', 'ikepolicy'))),
# IPSec proposals
path('ipsec-proposals/', views.IPSecProposalListView.as_view(), name='ipsecproposal_list'),
path('ipsec-proposals/add/', views.IPSecProposalEditView.as_view(), name='ipsecproposal_add'),
path('ipsec-proposals/import/', views.IPSecProposalBulkImportView.as_view(), name='ipsecproposal_import'),
path('ipsec-proposals/edit/', views.IPSecProposalBulkEditView.as_view(), name='ipsecproposal_bulk_edit'),
path('ipsec-proposals/delete/', views.IPSecProposalBulkDeleteView.as_view(), name='ipsecproposal_bulk_delete'),
path('ipsec-proposals/<int:pk>/', include(get_model_urls('vpn', 'ipsecproposal'))),
# IPSec policies
path('ipsec-policys/', views.IPSecPolicyListView.as_view(), name='ipsecpolicy_list'),
path('ipsec-policys/add/', views.IPSecPolicyEditView.as_view(), name='ipsecpolicy_add'),
path('ipsec-policys/import/', views.IPSecPolicyBulkImportView.as_view(), name='ipsecpolicy_import'),
path('ipsec-policys/edit/', views.IPSecPolicyBulkEditView.as_view(), name='ipsecpolicy_bulk_edit'),
path('ipsec-policys/delete/', views.IPSecPolicyBulkDeleteView.as_view(), name='ipsecpolicy_bulk_delete'),
path('ipsec-policys/<int:pk>/', include(get_model_urls('vpn', 'ipsecpolicy'))),
# IPSec profiles
path('ipsec-profiles/', views.IPSecProfileListView.as_view(), name='ipsecprofile_list'),
path('ipsec-profiles/add/', views.IPSecProfileEditView.as_view(), name='ipsecprofile_add'),
path('ipsec-profiles/import/', views.IPSecProfileBulkImportView.as_view(), name='ipsecprofile_import'),
path('ipsec-profiles/edit/', views.IPSecProfileBulkEditView.as_view(), name='ipsecprofile_bulk_edit'),
path('ipsec-profiles/delete/', views.IPSecProfileBulkDeleteView.as_view(), name='ipsecprofile_bulk_delete'),
path('ipsec-profiles/<int:pk>/', include(get_model_urls('vpn', 'ipsecprofile'))),
]

334
netbox/vpn/views.py Normal file
View File

@ -0,0 +1,334 @@
from netbox.views import generic
from utilities.utils import count_related
from utilities.views import register_model_view
from . import filtersets, forms, tables
from .models import *
#
# Tunnels
#
class TunnelListView(generic.ObjectListView):
queryset = Tunnel.objects.annotate(
count_terminations=count_related(TunnelTermination, 'tunnel')
)
filterset = filtersets.TunnelFilterSet
filterset_form = forms.TunnelFilterForm
table = tables.TunnelTable
@register_model_view(Tunnel)
class TunnelView(generic.ObjectView):
queryset = Tunnel.objects.all()
@register_model_view(Tunnel, 'edit')
class TunnelEditView(generic.ObjectEditView):
queryset = Tunnel.objects.all()
form = forms.TunnelForm
def dispatch(self, request, *args, **kwargs):
# If creating a new Tunnel, use the creation form
if 'pk' not in kwargs:
self.form = forms.TunnelCreateForm
return super().dispatch(request, *args, **kwargs)
@register_model_view(Tunnel, 'delete')
class TunnelDeleteView(generic.ObjectDeleteView):
queryset = Tunnel.objects.all()
class TunnelBulkImportView(generic.BulkImportView):
queryset = Tunnel.objects.all()
model_form = forms.TunnelImportForm
class TunnelBulkEditView(generic.BulkEditView):
queryset = Tunnel.objects.annotate(
count_terminations=count_related(TunnelTermination, 'tunnel')
)
filterset = filtersets.TunnelFilterSet
table = tables.TunnelTable
form = forms.TunnelBulkEditForm
class TunnelBulkDeleteView(generic.BulkDeleteView):
queryset = Tunnel.objects.annotate(
count_terminations=count_related(TunnelTermination, 'tunnel')
)
filterset = filtersets.TunnelFilterSet
table = tables.TunnelTable
#
# Tunnel terminations
#
class TunnelTerminationListView(generic.ObjectListView):
queryset = TunnelTermination.objects.all()
filterset = filtersets.TunnelTerminationFilterSet
filterset_form = forms.TunnelTerminationFilterForm
table = tables.TunnelTerminationTable
@register_model_view(TunnelTermination)
class TunnelTerminationView(generic.ObjectView):
queryset = TunnelTermination.objects.all()
@register_model_view(TunnelTermination, 'edit')
class TunnelTerminationEditView(generic.ObjectEditView):
queryset = TunnelTermination.objects.all()
form = forms.TunnelTerminationForm
@register_model_view(TunnelTermination, 'delete')
class TunnelTerminationDeleteView(generic.ObjectDeleteView):
queryset = TunnelTermination.objects.all()
class TunnelTerminationBulkImportView(generic.BulkImportView):
queryset = TunnelTermination.objects.all()
model_form = forms.TunnelTerminationImportForm
class TunnelTerminationBulkEditView(generic.BulkEditView):
queryset = TunnelTermination.objects.all()
filterset = filtersets.TunnelTerminationFilterSet
table = tables.TunnelTerminationTable
form = forms.TunnelTerminationBulkEditForm
class TunnelTerminationBulkDeleteView(generic.BulkDeleteView):
queryset = TunnelTermination.objects.all()
filterset = filtersets.TunnelTerminationFilterSet
table = tables.TunnelTerminationTable
#
# IKE proposals
#
class IKEProposalListView(generic.ObjectListView):
queryset = IKEProposal.objects.all()
filterset = filtersets.IKEProposalFilterSet
filterset_form = forms.IKEProposalFilterForm
table = tables.IKEProposalTable
@register_model_view(IKEProposal)
class IKEProposalView(generic.ObjectView):
queryset = IKEProposal.objects.all()
@register_model_view(IKEProposal, 'edit')
class IKEProposalEditView(generic.ObjectEditView):
queryset = IKEProposal.objects.all()
form = forms.IKEProposalForm
@register_model_view(IKEProposal, 'delete')
class IKEProposalDeleteView(generic.ObjectDeleteView):
queryset = IKEProposal.objects.all()
class IKEProposalBulkImportView(generic.BulkImportView):
queryset = IKEProposal.objects.all()
model_form = forms.IKEProposalImportForm
class IKEProposalBulkEditView(generic.BulkEditView):
queryset = IKEProposal.objects.all()
filterset = filtersets.IKEProposalFilterSet
table = tables.IKEProposalTable
form = forms.IKEProposalBulkEditForm
class IKEProposalBulkDeleteView(generic.BulkDeleteView):
queryset = IKEProposal.objects.all()
filterset = filtersets.IKEProposalFilterSet
table = tables.IKEProposalTable
#
# IKE policies
#
class IKEPolicyListView(generic.ObjectListView):
queryset = IKEPolicy.objects.all()
filterset = filtersets.IKEPolicyFilterSet
filterset_form = forms.IKEPolicyFilterForm
table = tables.IKEPolicyTable
@register_model_view(IKEPolicy)
class IKEPolicyView(generic.ObjectView):
queryset = IKEPolicy.objects.all()
@register_model_view(IKEPolicy, 'edit')
class IKEPolicyEditView(generic.ObjectEditView):
queryset = IKEPolicy.objects.all()
form = forms.IKEPolicyForm
@register_model_view(IKEPolicy, 'delete')
class IKEPolicyDeleteView(generic.ObjectDeleteView):
queryset = IKEPolicy.objects.all()
class IKEPolicyBulkImportView(generic.BulkImportView):
queryset = IKEPolicy.objects.all()
model_form = forms.IKEPolicyImportForm
class IKEPolicyBulkEditView(generic.BulkEditView):
queryset = IKEPolicy.objects.all()
filterset = filtersets.IKEPolicyFilterSet
table = tables.IKEPolicyTable
form = forms.IKEPolicyBulkEditForm
class IKEPolicyBulkDeleteView(generic.BulkDeleteView):
queryset = IKEPolicy.objects.all()
filterset = filtersets.IKEPolicyFilterSet
table = tables.IKEPolicyTable
#
# IPSec proposals
#
class IPSecProposalListView(generic.ObjectListView):
queryset = IPSecProposal.objects.all()
filterset = filtersets.IPSecProposalFilterSet
filterset_form = forms.IPSecProposalFilterForm
table = tables.IPSecProposalTable
@register_model_view(IPSecProposal)
class IPSecProposalView(generic.ObjectView):
queryset = IPSecProposal.objects.all()
@register_model_view(IPSecProposal, 'edit')
class IPSecProposalEditView(generic.ObjectEditView):
queryset = IPSecProposal.objects.all()
form = forms.IPSecProposalForm
@register_model_view(IPSecProposal, 'delete')
class IPSecProposalDeleteView(generic.ObjectDeleteView):
queryset = IPSecProposal.objects.all()
class IPSecProposalBulkImportView(generic.BulkImportView):
queryset = IPSecProposal.objects.all()
model_form = forms.IPSecProposalImportForm
class IPSecProposalBulkEditView(generic.BulkEditView):
queryset = IPSecProposal.objects.all()
filterset = filtersets.IPSecProposalFilterSet
table = tables.IPSecProposalTable
form = forms.IPSecProposalBulkEditForm
class IPSecProposalBulkDeleteView(generic.BulkDeleteView):
queryset = IPSecProposal.objects.all()
filterset = filtersets.IPSecProposalFilterSet
table = tables.IPSecProposalTable
#
# IPSec policies
#
class IPSecPolicyListView(generic.ObjectListView):
queryset = IPSecPolicy.objects.all()
filterset = filtersets.IPSecPolicyFilterSet
filterset_form = forms.IPSecPolicyFilterForm
table = tables.IPSecPolicyTable
@register_model_view(IPSecPolicy)
class IPSecPolicyView(generic.ObjectView):
queryset = IPSecPolicy.objects.all()
@register_model_view(IPSecPolicy, 'edit')
class IPSecPolicyEditView(generic.ObjectEditView):
queryset = IPSecPolicy.objects.all()
form = forms.IPSecPolicyForm
@register_model_view(IPSecPolicy, 'delete')
class IPSecPolicyDeleteView(generic.ObjectDeleteView):
queryset = IPSecPolicy.objects.all()
class IPSecPolicyBulkImportView(generic.BulkImportView):
queryset = IPSecPolicy.objects.all()
model_form = forms.IPSecPolicyImportForm
class IPSecPolicyBulkEditView(generic.BulkEditView):
queryset = IPSecPolicy.objects.all()
filterset = filtersets.IPSecPolicyFilterSet
table = tables.IPSecPolicyTable
form = forms.IPSecPolicyBulkEditForm
class IPSecPolicyBulkDeleteView(generic.BulkDeleteView):
queryset = IPSecPolicy.objects.all()
filterset = filtersets.IPSecPolicyFilterSet
table = tables.IPSecPolicyTable
#
# IPSec profiles
#
class IPSecProfileListView(generic.ObjectListView):
queryset = IPSecProfile.objects.all()
filterset = filtersets.IPSecProfileFilterSet
filterset_form = forms.IPSecProfileFilterForm
table = tables.IPSecProfileTable
@register_model_view(IPSecProfile)
class IPSecProfileView(generic.ObjectView):
queryset = IPSecProfile.objects.all()
@register_model_view(IPSecProfile, 'edit')
class IPSecProfileEditView(generic.ObjectEditView):
queryset = IPSecProfile.objects.all()
form = forms.IPSecProfileForm
@register_model_view(IPSecProfile, 'delete')
class IPSecProfileDeleteView(generic.ObjectDeleteView):
queryset = IPSecProfile.objects.all()
class IPSecProfileBulkImportView(generic.BulkImportView):
queryset = IPSecProfile.objects.all()
model_form = forms.IPSecProfileImportForm
class IPSecProfileBulkEditView(generic.BulkEditView):
queryset = IPSecProfile.objects.all()
filterset = filtersets.IPSecProfileFilterSet
table = tables.IPSecProfileTable
form = forms.IPSecProfileBulkEditForm
class IPSecProfileBulkDeleteView(generic.BulkDeleteView):
queryset = IPSecProfile.objects.all()
filterset = filtersets.IPSecProfileFilterSet
table = tables.IPSecProfileTable