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

This commit is contained in:
Jeremy Stretch 2023-11-29 20:38:02 -05:00
commit cdea1130eb
94 changed files with 1900 additions and 1553 deletions

View File

@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.6.5
placeholder: v3.6.6
validations:
required: true
- type: dropdown

View File

@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.6.5
placeholder: v3.6.6
validations:
required: true
- type: dropdown

View File

@ -0,0 +1,13 @@
# Virtual Disks
A virtual disk is used to model discrete virtual hard disks assigned to [virtual machines](./virtualmachine.md).
## Fields
### Name
A human-friendly name that is unique to the assigned virtual machine.
### Size
The allocated disk size, in gigabytes.

View File

@ -60,6 +60,10 @@ class MyModel(NetBoxModel):
This attribute specifies the URL at which the documentation for this model can be reached. By default, it will return `/static/docs/models/<app_label>/<model_name>/`. Plugin models can override this to return a custom URL. For example, you might direct the user to your plugin's documentation hosted on [ReadTheDocs](https://readthedocs.org/).
#### `_netbox_private`
By default, any model introduced by a plugin will appear in the list of available object types e.g. when creating a custom field or certain dashboard widgets. If your model is intended only for "behind the scenes use" and should not be exposed to end users, set `_netbox_private` to True. This will omit it from the list of general-purpose object types.
### Enabling Features Individually
If you prefer instead to enable only a subset of these features for a plugin model, NetBox provides a discrete "mix-in" class for each feature. You can subclass each of these individually when defining your model. (Your model will also need to inherit from Django's built-in `Model` class.)

View File

@ -1,6 +1,29 @@
# NetBox v3.6
## v3.6.6 (FUTURE)
## v3.6.7 (FUTURE)
---
## v3.6.6 (2023-11-29)
### Enhancements
* [#13735](https://github.com/netbox-community/netbox/issues/13735) - Show complete region hierarchy in UI for all relevant objects
### Bug Fixes
* [#14056](https://github.com/netbox-community/netbox/issues/14056) - Record a pre-change snapshot when bulk editing objects via CSV
* [#14187](https://github.com/netbox-community/netbox/issues/14187) - Raise a validation error when attempting to create a duplicate script or report
* [#14199](https://github.com/netbox-community/netbox/issues/14199) - Fix jobs list for reports with a custom name
* [#14239](https://github.com/netbox-community/netbox/issues/14239) - Fix CustomFieldChoiceSet search filter
* [#14242](https://github.com/netbox-community/netbox/issues/14242) - Enable export templates for contact assignments
* [#14299](https://github.com/netbox-community/netbox/issues/14299) - Webhook timestamps should be in proper ISO 8601 format
* [#14325](https://github.com/netbox-community/netbox/issues/14325) - Fix numeric ordering of service ports
* [#14339](https://github.com/netbox-community/netbox/issues/14339) - Correctly hash local user password when set via REST API
* [#14343](https://github.com/netbox-community/netbox/issues/14343) - Fix ordering ASN table by ASDOT column
* [#14346](https://github.com/netbox-community/netbox/issues/14346) - Fix running reports via REST API
* [#14349](https://github.com/netbox-community/netbox/issues/14349) - Fix custom validation support for remote data sources
* [#14363](https://github.com/netbox-community/netbox/issues/14363) - Fix bulk editing of interfaces assigned to VM with no cluster
---

View File

@ -0,0 +1,92 @@
## v3.7-beta1 (FUTURE)
### Breaking Changes
* The `ui_visibility` field on the [custom field model](../models/extras/customfield.md) has been replaced with two new fields: `ui_visible` and `ui_editable`. Existing values will be migrated automatically upon upgrade.
* The `FeatureQuery` class for querying content types by model feature has been removed. Plugins should now use the new `with_feature()` manager method on NetBox's proxy model for ContentType.
* The ConfigRevision model has been moved from `extras` to `core`. Configuration history will be retained throughout the upgrade process.
* The L2VPN and L2VPNTermination models have been moved from the `ipam` app to the new `vpn` app. All object data will be retained however please note that the relevant API endpoints have been moved to `/api/vpn/`.
* The `CustomFieldsMixin`, `SavedFiltersMixin`, and `TagsMixin` classes have moved from the `extras.forms.mixins` to `netbox.forms.mixins`.
### New Features
#### VPN Tunnels ([#9816](https://github.com/netbox-community/netbox/issues/9816))
Several new models have been introduced to enable [VPN tunnel management](../features/vpn-tunnels.md). Users can now define tunnels with two or more terminations to replicate peer-to-peer or hub-and-spoke topologies. Each termination is made to a virtual interface on a device or VM. Additionally, users can define IKE and IPSec policies which can be applied to tunnels to document encryption and authentication strategies.
#### Virtual Machine Disks ([#8356](https://github.com/netbox-community/netbox/issues/8356))
A new [VirtualDisk](../models/virtualization/virtualdisk.md) model has been introduced to enable tracking the assignment of discrete virtual disks to virtual machines. The original `size` field has been retained on the VirtualMachine model, and will be automatically updated with the aggregate size of all assigned virtual disks. (Users who opt to eschew the new model may continue using the VirtualMachine `size` attribute as before.)
#### Protection Rules ([#10244](https://github.com/netbox-community/netbox/issues/10244))
A new [`PROTECTION_RULES`](../configuration/data-validation.md#protection_rules) configuration parameter is now available. Similar to how [custom validation rules](../customization/custom-validation.md) can be used to enforce certain values for object attributes, protection rules guard against the deletion of objects which do not meet specified criteria. This enables an administrator to prevent, for example, the deletion of a site which has a status of "active."
#### Improved Custom Field Visibility Controls ([#13299](https://github.com/netbox-community/netbox/issues/13299))
The old `ui_visible` field on the custom field model](../models/extras/customfield.md) has been replaced by two new fields, `ui_visible` and `ui_editable`, which control how and whether a custom field is displayed when view and editing an object, respectively. Separating these two functions into discrete fields enables more control over how each custom field is presented to users. The values of these fields will be appropriately set automatically during the upgrade process depending on the value of the original field.
#### Extend Display of Global Search Results ([#14134](https://github.com/netbox-community/netbox/issues/14134))
Global search results now include additional context about each object, such as a description, status, and/or related objects. The set of attributes to be displayed is specific to each object type, and is defined by setting `display_attrs` under the object's [SearchIndex class](../plugins/development/search.md#netbox.search.SearchIndex).
#### Table Column Registration for Plugins ([#14173](https://github.com/netbox-community/netbox/issues/14173))
Plugins can now [register their own custom columns](../plugins/development/tables.md#extending-core-tables) for inclusion on core NetBox tables. For example, a plugin can register a new column on SiteTable using the new `register_table_column()` utility function, and it will become available for users to select for display.
#### Data Backend Registration for Plugins ([#13381](https://github.com/netbox-community/netbox/issues/13381))
Plugins can now [register their own data backends](../plugins/development/data-backends.md) for use with [synchronized data sources](../features/synchronized-data.md). This enables plugins to introduce new backends in addition to the git, S3, and local path backends provided natively.
### Enhancements
* [#12135](https://github.com/netbox-community/netbox/issues/12135) - Avoid orphaned interfaces by preventing the deletion of interfaces which have children assigned
* [#12216](https://github.com/netbox-community/netbox/issues/12216) - Add a `color` field for circuit types
* [#13230](https://github.com/netbox-community/netbox/issues/13230) - Allow device types to be excluded from consideration when calculating a rack's utilization
* [#13334](https://github.com/netbox-community/netbox/issues/13334) - Added an `error` field to the Job model to record any errors associated with its execution
* [#13427](https://github.com/netbox-community/netbox/issues/13427) - Introduced a mechanism for omitting models from general-purpose lists of object types
* [#13690](https://github.com/netbox-community/netbox/issues/13690) - Display any dependent objects to be deleted prior to deleting an object via the web UI
* [#13794](https://github.com/netbox-community/netbox/issues/13794) - Any models with a relationship to Tenant are now included automatically in the list of related objects under the tenant view
* [#13808](https://github.com/netbox-community/netbox/issues/13808) - Added a `/render-config` REST API endpoint for virtual machines
* [#14035](https://github.com/netbox-community/netbox/issues/14035) - Order objects of equivalent weight by value in global search results to improve readability
* [#14156](https://github.com/netbox-community/netbox/issues/14156) - Enable custom fields for contact assignments
### Other Changes
* [#13550](https://github.com/netbox-community/netbox/issues/13550) - Optimized the format for declaring view actions under `ActionsMixin` (backward compatibility has been retained)
* [#13645](https://github.com/netbox-community/netbox/issues/13645) - Installation of the `sentry-sdk` Python library is now required only if Sentry reporting is enabled
* [#14036](https://github.com/netbox-community/netbox/issues/14036) - Move plugin resources from the `extras` app into `netbox` (backward compatibility has been retained)
* [#14153](https://github.com/netbox-community/netbox/issues/14153) - Replace `FeatureQuery` with new `with_feature()` method on ContentType manager
* [#14311](https://github.com/netbox-community/netbox/issues/14311) - Move the L2VPN models from the `ipam` app to the new `vpn` app
* [#14312](https://github.com/netbox-community/netbox/issues/14312) - Move the ConfigRevision model from the `extras` app to `core`
* [#14326](https://github.com/netbox-community/netbox/issues/14326) - Form feature mixin classes have been moved from the `extras` app to `netbox`
### REST API Changes
* Introduced the following endpoints:
* `/api/virtualization/virtual-disks/`
* `/api/vpn/ike-policies/`
* `/api/vpn/ike-proposals/`
* `/api/vpn/ipsec-policies/`
* `/api/vpn/ipsec-profiles/`
* `/api/vpn/ipsec-proposals/`
* `/api/vpn/tunnels/`
* `/api/vpn/tunnel-terminations/`
* The following endpoints have been moved:
* `/api/ipam/l2vpns/` -> `/api/vpn/l2vpns/`
* `/api/ipam/l2vpn-terminations/` -> `/api/vpn/l2vpn-terminations/`
* circuits.CircuitType
* Added the optional `color` choice field
* core.Job
* Added the read-only `error` character field
* dcim.DeviceType
* Added the `exclude_from_utilization` boolean field
* extras.CustomField
* Removed the `ui_visibility` field
* Added the `ui_visible` and `ui_editable` choice fields
* tenancy.ContactAssignment
* Added support for custom fields
* virtualization.VirtualDisk
* Added the read-only `virtual_disk_count` integer field
* virtualization.VirtualMachine
* Added the `/render-config` endpoint

View File

@ -116,6 +116,7 @@ class DataSource(JobsMixin, PrimaryModel):
)
def clean(self):
super().clean()
# Validate data backend type
if self.type and self.type not in registry['data_backends']:

View File

@ -2,6 +2,7 @@ import logging
import os
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext as _
@ -85,6 +86,14 @@ class ManagedFile(SyncedDataMixin, models.Model):
self.file_path = os.path.basename(self.data_path)
self.data_file.write_to_disk(self.full_path, overwrite=True)
def clean(self):
super().clean()
# Ensure that the file root and path make a unique pair
if self._meta.model.objects.filter(file_root=self.file_root, file_path=self.file_path).exclude(pk=self.pk).exists():
raise ValidationError(
f"A {self._meta.verbose_name.lower()} with this file path already exists ({self.file_root}/{self.file_path}).")
def delete(self, *args, **kwargs):
# Delete file from disk
try:

View File

@ -2,8 +2,8 @@ import decimal
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from timezone_field.rest_framework import TimeZoneSerializerField
@ -12,8 +12,7 @@ from dcim.constants import *
from dcim.models import *
from extras.api.nested_serializers import NestedConfigTemplateSerializer
from ipam.api.nested_serializers import (
NestedASNSerializer, NestedIPAddressSerializer, NestedL2VPNTerminationSerializer, NestedVLANSerializer,
NestedVRFSerializer,
NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer,
)
from ipam.models import ASN, VLAN
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
@ -27,6 +26,7 @@ from tenancy.api.nested_serializers import NestedTenantSerializer
from users.api.nested_serializers import NestedUserSerializer
from utilities.api import get_serializer_for_model
from virtualization.api.nested_serializers import NestedClusterSerializer
from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
from wireless.api.nested_serializers import NestedWirelessLANSerializer, NestedWirelessLinkSerializer
from wireless.choices import *
from wireless.models import WirelessLAN

View File

@ -5,7 +5,7 @@ from django.utils.translation import gettext as _
from extras.filtersets import LocalConfigContextFilterSet
from extras.models import ConfigTemplate
from ipam.filtersets import PrimaryIPFilterSet
from ipam.models import ASN, L2VPN, IPAddress, VRF
from ipam.models import ASN, IPAddress, VRF
from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
)
@ -17,6 +17,7 @@ from utilities.filters import (
TreeNodeMultipleChoiceFilter,
)
from virtualization.models import Cluster
from vpn.models import L2VPN
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
from .choices import *
from .constants import *

View File

@ -7,12 +7,13 @@ from dcim.constants import *
from dcim.models import *
from extras.forms import LocalConfigContextFilterForm
from extras.models import ConfigTemplate
from ipam.models import ASN, L2VPN, VRF
from ipam.models import ASN, VRF
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.widgets import APISelectMultiple, NumberWithOptions
from vpn.models import L2VPN
from wireless.choices import *
__all__ = (

View File

@ -730,7 +730,7 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
related_query_name='interface'
)
l2vpn_terminations = GenericRelation(
to='ipam.L2VPNTermination',
to='vpn.L2VPNTermination',
content_type_field='assigned_object_type',
object_id_field='assigned_object_id',
related_query_name='interface',

View File

@ -316,8 +316,8 @@ INTERFACE_BUTTONS = """
{% if perms.dcim.add_interface %}
<li><a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ record.device_id }}&parent={{ record.pk }}&name={{ record.name }}.&type=virtual&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Child Interface</a></li>
{% endif %}
{% if perms.ipam.add_l2vpntermination %}
<li><a class="dropdown-item" href="{% url 'ipam:l2vpntermination_add' %}?device={{ object.pk }}&interface={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">L2VPN Termination</a></li>
{% if perms.vpn.add_l2vpntermination %}
<li><a class="dropdown-item" href="{% url 'vpn:l2vpntermination_add' %}?device={{ object.pk }}&interface={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">L2VPN Termination</a></li>
{% endif %}
{% if perms.ipam.add_fhrpgroupassignment %}
<li><a class="dropdown-item" href="{% url 'ipam:fhrpgroupassignment_add' %}?interface_type={{ record|content_type_id }}&interface_id={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Assign FHRP Group</a></li>

View File

@ -294,7 +294,7 @@ class ReportViewSet(ViewSet):
# Retrieve and run the Report. This will create a new Job.
module, report_cls = self._get_report(pk)
report = report_cls()
report = report_cls
input_serializer = serializers.ReportInputSerializer(
data=request.data,
context={'report': report}

View File

@ -92,7 +92,7 @@ def process_event_rules(event_rules, model_name, event, data, username, snapshot
"event": event,
"data": data,
"snapshots": snapshots,
"timestamp": str(timezone.now()),
"timestamp": timezone.now().isoformat(),
"username": username,
"retry": get_rq_retry()
}

View File

@ -153,8 +153,7 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet):
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value) |
Q(extra_choices__contains=value)
Q(description__icontains=value)
)
def filter_by_choice(self, queryset, name, value):

View File

@ -1138,7 +1138,7 @@ class ReportJobsView(ContentTypePermissionRequiredMixin, View):
jobs = Job.objects.filter(
object_type=object_type,
object_id=module.pk,
name=report.name
name=report.class_name
)
jobs_table = JobTable(

View File

@ -2,7 +2,6 @@ from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers
from ipam import models
from ipam.models.l2vpn import L2VPNTermination, L2VPN
from netbox.api.serializers import WritableNestedSerializer
from .field_serializers import IPAddressField
@ -14,8 +13,6 @@ __all__ = [
'NestedFHRPGroupAssignmentSerializer',
'NestedIPAddressSerializer',
'NestedIPRangeSerializer',
'NestedL2VPNSerializer',
'NestedL2VPNTerminationSerializer',
'NestedPrefixSerializer',
'NestedRIRSerializer',
'NestedRoleSerializer',
@ -223,28 +220,3 @@ class NestedServiceSerializer(WritableNestedSerializer):
class Meta:
model = models.Service
fields = ['id', 'url', 'display', 'name', 'protocol', 'ports']
#
# L2VPN
#
class NestedL2VPNSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:l2vpn-detail')
class Meta:
model = L2VPN
fields = [
'id', 'url', 'display', 'identifier', 'name', 'slug', 'type'
]
class NestedL2VPNTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:l2vpntermination-detail')
l2vpn = NestedL2VPNSerializer()
class Meta:
model = L2VPNTermination
fields = [
'id', 'url', 'display', 'l2vpn'
]

View File

@ -12,8 +12,9 @@ from netbox.constants import NESTED_SERIALIZER_PREFIX
from tenancy.api.nested_serializers import NestedTenantSerializer
from utilities.api import get_serializer_for_model
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
from .nested_serializers import *
from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
from .field_serializers import IPAddressField, IPNetworkField
from .nested_serializers import *
#
@ -479,54 +480,3 @@ class ServiceSerializer(NetBoxModelSerializer):
'id', 'url', 'display', 'device', 'virtual_machine', 'name', 'ports', 'protocol', 'ipaddresses',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
#
# L2VPN
#
class L2VPNSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:l2vpn-detail')
type = ChoiceField(choices=L2VPNTypeChoices, required=False)
import_targets = SerializedPKRelatedField(
queryset=RouteTarget.objects.all(),
serializer=NestedRouteTargetSerializer,
required=False,
many=True
)
export_targets = SerializedPKRelatedField(
queryset=RouteTarget.objects.all(),
serializer=NestedRouteTargetSerializer,
required=False,
many=True
)
tenant = NestedTenantSerializer(required=False, allow_null=True)
class Meta:
model = L2VPN
fields = [
'id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'import_targets', 'export_targets',
'description', 'comments', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated'
]
class L2VPNTerminationSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:l2vpntermination-detail')
l2vpn = NestedL2VPNSerializer()
assigned_object_type = ContentTypeField(
queryset=ContentType.objects.all()
)
assigned_object = serializers.SerializerMethodField(read_only=True)
class Meta:
model = L2VPNTermination
fields = [
'id', 'url', 'display', 'l2vpn', 'assigned_object_type', 'assigned_object_id',
'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated'
]
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_assigned_object(self, instance):
serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
context = {'request': self.context['request']}
return serializer(instance.assigned_object, context=context).data

View File

@ -23,8 +23,6 @@ router.register('vlan-groups', views.VLANGroupViewSet)
router.register('vlans', views.VLANViewSet)
router.register('service-templates', views.ServiceTemplateViewSet)
router.register('services', views.ServiceViewSet)
router.register('l2vpns', views.L2VPNViewSet)
router.register('l2vpn-terminations', views.L2VPNTerminationViewSet)
app_name = 'ipam-api'

View File

@ -14,7 +14,6 @@ from circuits.models import Provider
from dcim.models import Site
from ipam import filtersets
from ipam.models import *
from ipam.models import L2VPN, L2VPNTermination
from ipam.utils import get_next_available_prefix
from netbox.api.viewsets import NetBoxModelViewSet
from netbox.api.viewsets.mixins import ObjectValidationMixin
@ -178,18 +177,6 @@ class ServiceViewSet(NetBoxModelViewSet):
filterset_class = filtersets.ServiceFilterSet
class L2VPNViewSet(NetBoxModelViewSet):
queryset = L2VPN.objects.prefetch_related('import_targets', 'export_targets', 'tenant', 'tags')
serializer_class = serializers.L2VPNSerializer
filterset_class = filtersets.L2VPNFilterSet
class L2VPNTerminationViewSet(NetBoxModelViewSet):
queryset = L2VPNTermination.objects.prefetch_related('assigned_object')
serializer_class = serializers.L2VPNTerminationSerializer
filterset_class = filtersets.L2VPNTerminationFilterSet
#
# Views
#

View File

@ -172,52 +172,3 @@ class ServiceProtocolChoices(ChoiceSet):
(PROTOCOL_UDP, 'UDP'),
(PROTOCOL_SCTP, 'SCTP'),
)
class L2VPNTypeChoices(ChoiceSet):
TYPE_VPLS = 'vpls'
TYPE_VPWS = 'vpws'
TYPE_EPL = 'epl'
TYPE_EVPL = 'evpl'
TYPE_EPLAN = 'ep-lan'
TYPE_EVPLAN = 'evp-lan'
TYPE_EPTREE = 'ep-tree'
TYPE_EVPTREE = 'evp-tree'
TYPE_VXLAN = 'vxlan'
TYPE_VXLAN_EVPN = 'vxlan-evpn'
TYPE_MPLS_EVPN = 'mpls-evpn'
TYPE_PBB_EVPN = 'pbb-evpn'
CHOICES = (
('VPLS', (
(TYPE_VPWS, 'VPWS'),
(TYPE_VPLS, 'VPLS'),
)),
('VXLAN', (
(TYPE_VXLAN, 'VXLAN'),
(TYPE_VXLAN_EVPN, 'VXLAN-EVPN'),
)),
('L2VPN E-VPN', (
(TYPE_MPLS_EVPN, 'MPLS EVPN'),
(TYPE_PBB_EVPN, 'PBB EVPN'),
)),
('E-Line', (
(TYPE_EPL, 'EPL'),
(TYPE_EVPL, 'EVPL'),
)),
('E-LAN', (
(TYPE_EPLAN, 'Ethernet Private LAN'),
(TYPE_EVPLAN, 'Ethernet Virtual Private LAN'),
)),
('E-Tree', (
(TYPE_EPTREE, 'Ethernet Private Tree'),
(TYPE_EVPTREE, 'Ethernet Virtual Private Tree'),
)),
)
P2P = (
TYPE_VPWS,
TYPE_EPL,
TYPE_EPLAN,
TYPE_EPTREE
)

View File

@ -86,9 +86,3 @@ VLANGROUP_SCOPE_TYPES = (
# 16-bit port number
SERVICE_PORT_MIN = 1
SERVICE_PORT_MAX = 65535
L2VPN_ASSIGNMENT_MODELS = Q(
Q(app_label='dcim', model='interface') |
Q(app_label='ipam', model='vlan') |
Q(app_label='virtualization', model='vminterface')
)

View File

@ -4,8 +4,8 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.utils.translation import gettext as _
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from netaddr.core import AddrFormatError
from dcim.models import Device, Interface, Region, Site, SiteGroup
@ -15,6 +15,7 @@ from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
)
from virtualization.models import VirtualMachine, VMInterface
from vpn.models import L2VPN
from .choices import *
from .models import *
@ -26,8 +27,6 @@ __all__ = (
'FHRPGroupFilterSet',
'IPAddressFilterSet',
'IPRangeFilterSet',
'L2VPNFilterSet',
'L2VPNTerminationFilterSet',
'PrefixFilterSet',
'PrimaryIPFilterSet',
'RIRFilterSet',
@ -1059,182 +1058,6 @@ class ServiceFilterSet(NetBoxModelFilterSet):
return queryset.filter(qs_filter)
#
# L2VPN
#
class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=L2VPNTypeChoices,
null_value=None
)
import_target_id = django_filters.ModelMultipleChoiceFilter(
field_name='import_targets',
queryset=RouteTarget.objects.all(),
label=_('Import target'),
)
import_target = django_filters.ModelMultipleChoiceFilter(
field_name='import_targets__name',
queryset=RouteTarget.objects.all(),
to_field_name='name',
label=_('Import target (name)'),
)
export_target_id = django_filters.ModelMultipleChoiceFilter(
field_name='export_targets',
queryset=RouteTarget.objects.all(),
label=_('Export target'),
)
export_target = django_filters.ModelMultipleChoiceFilter(
field_name='export_targets__name',
queryset=RouteTarget.objects.all(),
to_field_name='name',
label=_('Export target (name)'),
)
class Meta:
model = L2VPN
fields = ['id', 'identifier', 'name', 'slug', 'type', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
try:
qs_filter |= Q(identifier=int(value))
except ValueError:
pass
return queryset.filter(qs_filter)
class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
queryset=L2VPN.objects.all(),
label=_('L2VPN (ID)'),
)
l2vpn = django_filters.ModelMultipleChoiceFilter(
field_name='l2vpn__slug',
queryset=L2VPN.objects.all(),
to_field_name='slug',
label=_('L2VPN (slug)'),
)
region = MultiValueCharFilter(
method='filter_region',
field_name='slug',
label=_('Region (slug)'),
)
region_id = MultiValueNumberFilter(
method='filter_region',
field_name='pk',
label=_('Region (ID)'),
)
site = MultiValueCharFilter(
method='filter_site',
field_name='slug',
label=_('Site (slug)'),
)
site_id = MultiValueNumberFilter(
method='filter_site',
field_name='pk',
label=_('Site (ID)'),
)
device = django_filters.ModelMultipleChoiceFilter(
field_name='interface__device__name',
queryset=Device.objects.all(),
to_field_name='name',
label=_('Device (name)'),
)
device_id = django_filters.ModelMultipleChoiceFilter(
field_name='interface__device',
queryset=Device.objects.all(),
label=_('Device (ID)'),
)
virtual_machine = django_filters.ModelMultipleChoiceFilter(
field_name='vminterface__virtual_machine__name',
queryset=VirtualMachine.objects.all(),
to_field_name='name',
label=_('Virtual machine (name)'),
)
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
field_name='vminterface__virtual_machine',
queryset=VirtualMachine.objects.all(),
label=_('Virtual machine (ID)'),
)
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)'),
)
vlan = django_filters.ModelMultipleChoiceFilter(
field_name='vlan__name',
queryset=VLAN.objects.all(),
to_field_name='name',
label=_('VLAN (name)'),
)
vlan_vid = django_filters.NumberFilter(
field_name='vlan__vid',
label=_('VLAN number (1-4094)'),
)
vlan_id = django_filters.ModelMultipleChoiceFilter(
field_name='vlan',
queryset=VLAN.objects.all(),
label=_('VLAN (ID)'),
)
assigned_object_type = ContentTypeFilter()
class Meta:
model = L2VPNTermination
fields = ('id', 'assigned_object_type_id')
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = Q(l2vpn__name__icontains=value)
return queryset.filter(qs_filter)
def filter_assigned_object(self, queryset, name, value):
qs = queryset.filter(
Q(**{'{}__in'.format(name): value})
)
return qs
def filter_site(self, queryset, name, value):
qs = queryset.filter(
Q(
Q(**{'vlan__site__{}__in'.format(name): value}) |
Q(**{'interface__device__site__{}__in'.format(name): value}) |
Q(**{'vminterface__virtual_machine__site__{}__in'.format(name): value})
)
)
return qs
def filter_region(self, queryset, name, value):
qs = queryset.filter(
Q(
Q(**{'vlan__site__region__{}__in'.format(name): value}) |
Q(**{'interface__device__site__region__{}__in'.format(name): value}) |
Q(**{'vminterface__virtual_machine__site__region__{}__in'.format(name): value})
)
)
return qs
class PrimaryIPFilterSet(django_filters.FilterSet):
"""
An inheritable FilterSet for models which support primary IP assignment.

View File

@ -23,8 +23,6 @@ __all__ = (
'FHRPGroupBulkEditForm',
'IPAddressBulkEditForm',
'IPRangeBulkEditForm',
'L2VPNBulkEditForm',
'L2VPNTerminationBulkEditForm',
'PrefixBulkEditForm',
'RIRBulkEditForm',
'RoleBulkEditForm',
@ -596,32 +594,3 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
class ServiceBulkEditForm(ServiceTemplateBulkEditForm):
model = Service
class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
type = forms.ChoiceField(
label=_('Type'),
choices=add_blank_choice(L2VPNTypeChoices),
required=False
)
tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField()
model = L2VPN
fieldsets = (
(None, ('type', 'tenant', 'description')),
)
nullable_fields = ('tenant', 'description', 'comments')
class L2VPNTerminationBulkEditForm(NetBoxModelBulkEditForm):
model = L2VPN

View File

@ -1,6 +1,5 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface, Site
@ -21,8 +20,6 @@ __all__ = (
'FHRPGroupImportForm',
'IPAddressImportForm',
'IPRangeImportForm',
'L2VPNImportForm',
'L2VPNTerminationImportForm',
'PrefixImportForm',
'RIRImportForm',
'RoleImportForm',
@ -529,92 +526,3 @@ class ServiceImportForm(NetBoxModelImportForm):
)
return self.cleaned_data['ipaddresses']
class L2VPNImportForm(NetBoxModelImportForm):
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
)
type = CSVChoiceField(
label=_('Type'),
choices=L2VPNTypeChoices,
help_text=_('L2VPN type')
)
class Meta:
model = L2VPN
fields = ('identifier', 'name', 'slug', 'tenant', 'type', 'description',
'comments', 'tags')
class L2VPNTerminationImportForm(NetBoxModelImportForm):
l2vpn = CSVModelChoiceField(
queryset=L2VPN.objects.all(),
required=True,
to_field_name='name',
label=_('L2VPN'),
)
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
required=False,
to_field_name='name',
help_text=_('Parent device (for interface)')
)
virtual_machine = CSVModelChoiceField(
label=_('Virtual machine'),
queryset=VirtualMachine.objects.all(),
required=False,
to_field_name='name',
help_text=_('Parent virtual machine (for interface)')
)
interface = CSVModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.none(), # Can also refer to VMInterface
required=False,
to_field_name='name',
help_text=_('Assigned interface (device or VM)')
)
vlan = CSVModelChoiceField(
label=_('VLAN'),
queryset=VLAN.objects.all(),
required=False,
to_field_name='name',
help_text=_('Assigned VLAN')
)
class Meta:
model = L2VPNTermination
fields = ('l2vpn', 'device', 'virtual_machine', 'interface', 'vlan', 'tags')
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
if data:
# Limit interface queryset by device or VM
if data.get('device'):
self.fields['interface'].queryset = Interface.objects.filter(
**{f"device__{self.fields['device'].to_field_name}": data['device']}
)
elif data.get('virtual_machine'):
self.fields['interface'].queryset = VMInterface.objects.filter(
**{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
)
def clean(self):
super().clean()
if self.cleaned_data.get('device') and self.cleaned_data.get('virtual_machine'):
raise ValidationError(_('Cannot import device and VM interface terminations simultaneously.'))
if not self.instance and not (self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')):
raise ValidationError(_('Each termination must specify either an interface or a VLAN.'))
if self.cleaned_data.get('interface') and self.cleaned_data.get('vlan'):
raise ValidationError(_('Cannot assign both an interface and a VLAN.'))
# if this is an update we might not have interface or vlan in the form data
if self.cleaned_data.get('interface') or self.cleaned_data.get('vlan'):
self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')

View File

@ -1,5 +1,4 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
@ -9,10 +8,9 @@ from ipam.models import *
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
from utilities.forms.fields import (
ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
)
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
from virtualization.models import VirtualMachine
from vpn.models import L2VPN
__all__ = (
'AggregateFilterForm',
@ -21,8 +19,6 @@ __all__ = (
'FHRPGroupFilterForm',
'IPAddressFilterForm',
'IPRangeFilterForm',
'L2VPNFilterForm',
'L2VPNTerminationFilterForm',
'PrefixFilterForm',
'RIRFilterForm',
'RoleFilterForm',
@ -539,90 +535,3 @@ class ServiceFilterForm(ServiceTemplateFilterForm):
label=_('Virtual Machine'),
)
tag = TagFilterField(model)
class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = L2VPN
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('type', 'import_target_id', 'export_target_id')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
)
type = forms.ChoiceField(
label=_('Type'),
choices=add_blank_choice(L2VPNTypeChoices),
required=False
)
import_target_id = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
required=False,
label=_('Import targets')
)
export_target_id = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
required=False,
label=_('Export targets')
)
tag = TagFilterField(model)
class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
model = L2VPNTermination
fieldsets = (
(None, ('filter_id', 'l2vpn_id',)),
(_('Assigned Object'), (
'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id',
)),
)
l2vpn_id = DynamicModelChoiceField(
queryset=L2VPN.objects.all(),
required=False,
label=_('L2VPN')
)
assigned_object_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.filter(L2VPN_ASSIGNMENT_MODELS),
required=False,
label=_('Assigned Object Type'),
limit_choices_to=L2VPN_ASSIGNMENT_MODELS
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
null_option='None',
query_params={
'region_id': '$region_id'
},
label=_('Site')
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
null_option='None',
query_params={
'site_id': '$site_id'
},
label=_('Device')
)
vlan_id = DynamicModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False,
null_option='None',
query_params={
'site_id': '$site_id'
},
label=_('VLAN')
)
virtual_machine_id = DynamicModelMultipleChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
null_option='None',
query_params={
'site_id': '$site_id'
},
label=_('Virtual Machine')
)

View File

@ -29,8 +29,6 @@ __all__ = (
'IPAddressBulkAddForm',
'IPAddressForm',
'IPRangeForm',
'L2VPNForm',
'L2VPNTerminationForm',
'PrefixForm',
'RIRForm',
'RoleForm',
@ -754,97 +752,3 @@ class ServiceCreateForm(ServiceForm):
self.cleaned_data['description'] = service_template.description
elif not all(self.cleaned_data[f] for f in ('name', 'protocol', 'ports')):
raise forms.ValidationError("Must specify name, protocol, and port(s) if not using a service template.")
#
# L2VPN
#
class L2VPNForm(TenancyForm, NetBoxModelForm):
slug = SlugField()
import_targets = DynamicModelMultipleChoiceField(
label=_('Import targets'),
queryset=RouteTarget.objects.all(),
required=False
)
export_targets = DynamicModelMultipleChoiceField(
label=_('Export targets'),
queryset=RouteTarget.objects.all(),
required=False
)
comments = CommentField()
fieldsets = (
(_('L2VPN'), ('name', 'slug', 'type', 'identifier', 'description', 'tags')),
(_('Route Targets'), ('import_targets', 'export_targets')),
(_('Tenancy'), ('tenant_group', 'tenant')),
)
class Meta:
model = L2VPN
fields = (
'name', 'slug', 'type', 'identifier', 'import_targets', 'export_targets', 'tenant', 'description',
'comments', 'tags'
)
class L2VPNTerminationForm(NetBoxModelForm):
l2vpn = DynamicModelChoiceField(
queryset=L2VPN.objects.all(),
required=True,
query_params={},
label=_('L2VPN'),
fetch_trigger='open'
)
vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
selector=True,
label=_('VLAN')
)
interface = DynamicModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.all(),
required=False,
selector=True
)
vminterface = DynamicModelChoiceField(
queryset=VMInterface.objects.all(),
required=False,
selector=True,
label=_('Interface')
)
class Meta:
model = L2VPNTermination
fields = ('l2vpn', )
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy()
if instance:
if type(instance.assigned_object) is Interface:
initial['interface'] = instance.assigned_object
elif type(instance.assigned_object) is VLAN:
initial['vlan'] = instance.assigned_object
elif type(instance.assigned_object) is VMInterface:
initial['vminterface'] = instance.assigned_object
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
def clean(self):
super().clean()
interface = self.cleaned_data.get('interface')
vminterface = self.cleaned_data.get('vminterface')
vlan = self.cleaned_data.get('vlan')
if not (interface or vminterface or vlan):
raise ValidationError(_('A termination must specify an interface or VLAN.'))
if len([x for x in (interface, vminterface, vlan) if x]) > 1:
raise ValidationError(_('A termination can only have one terminating object (an interface or VLAN).'))
self.instance.assigned_object = interface or vminterface or vlan

View File

@ -1,9 +1,8 @@
import graphene
from ipam import models
from utilities.graphql_optimizer import gql_query_optimizer
from netbox.graphql.fields import ObjectField, ObjectListField
from utilities.graphql_optimizer import gql_query_optimizer
from .types import *
@ -38,18 +37,6 @@ class IPAMQuery(graphene.ObjectType):
def resolve_ip_range_list(root, info, **kwargs):
return gql_query_optimizer(models.IPRange.objects.all(), info)
l2vpn = ObjectField(L2VPNType)
l2vpn_list = ObjectListField(L2VPNType)
def resolve_l2vpn_list(root, info, **kwargs):
return gql_query_optimizer(models.L2VPN.objects.all(), info)
l2vpn_termination = ObjectField(L2VPNTerminationType)
l2vpn_termination_list = ObjectListField(L2VPNTerminationType)
def resolve_l2vpn_termination_list(root, info, **kwargs):
return gql_query_optimizer(models.L2VPNTermination.objects.all(), info)
prefix = ObjectField(PrefixType)
prefix_list = ObjectListField(PrefixType)

View File

@ -1,6 +1,5 @@
import graphene
from extras.graphql.mixins import ContactsMixin
from ipam import filtersets, models
from netbox.graphql.scalars import BigInt
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
@ -13,8 +12,6 @@ __all__ = (
'FHRPGroupAssignmentType',
'IPAddressType',
'IPRangeType',
'L2VPNType',
'L2VPNTerminationType',
'PrefixType',
'RIRType',
'RoleType',
@ -188,19 +185,3 @@ class VRFType(NetBoxObjectType):
model = models.VRF
fields = '__all__'
filterset_class = filtersets.VRFFilterSet
class L2VPNType(ContactsMixin, NetBoxObjectType):
class Meta:
model = models.L2VPN
fields = '__all__'
filtersets_class = filtersets.L2VPNFilterSet
class L2VPNTerminationType(NetBoxObjectType):
assigned_object = graphene.Field('ipam.graphql.gfk_mixins.L2VPNAssignmentType')
class Meta:
model = models.L2VPNTermination
exclude = ('assigned_object_type', 'assigned_object_id')
filtersets_class = filtersets.L2VPNTerminationFilterSet

View File

@ -0,0 +1,64 @@
from django.db import migrations
def update_content_types(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType')
# Delete the new ContentTypes effected by the new models in the vpn app
ContentType.objects.filter(app_label='vpn', model='l2vpn').delete()
ContentType.objects.filter(app_label='vpn', model='l2vpntermination').delete()
# Update the app labels of the original ContentTypes for ipam.L2VPN and ipam.L2VPNTermination to ensure
# that any foreign key references are preserved
ContentType.objects.filter(app_label='ipam', model='l2vpn').update(app_label='vpn')
ContentType.objects.filter(app_label='ipam', model='l2vpntermination').update(app_label='vpn')
class Migration(migrations.Migration):
dependencies = [
('ipam', '0067_ipaddress_index_host'),
]
operations = [
migrations.RemoveConstraint(
model_name='l2vpntermination',
name='ipam_l2vpntermination_assigned_object',
),
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.RemoveField(
model_name='l2vpntermination',
name='assigned_object_type',
),
migrations.RemoveField(
model_name='l2vpntermination',
name='l2vpn',
),
migrations.RemoveField(
model_name='l2vpntermination',
name='tags',
),
migrations.DeleteModel(
name='L2VPN',
),
migrations.DeleteModel(
name='L2VPNTermination',
),
],
database_operations=[
migrations.AlterModelTable(
name='L2VPN',
table='vpn_l2vpn',
),
migrations.AlterModelTable(
name='L2VPNTermination',
table='vpn_l2vpntermination',
),
],
),
migrations.RunPython(
code=update_content_types,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -3,27 +3,5 @@ from .asns import *
from .fhrp import *
from .vrfs import *
from .ip import *
from .l2vpn import *
from .services import *
from .vlans import *
__all__ = (
'ASN',
'ASNRange',
'Aggregate',
'IPAddress',
'IPRange',
'FHRPGroup',
'FHRPGroupAssignment',
'L2VPN',
'L2VPNTermination',
'Prefix',
'RIR',
'Role',
'RouteTarget',
'Service',
'ServiceTemplate',
'VLAN',
'VLANGroup',
'VRF',
)

View File

@ -183,9 +183,8 @@ class VLAN(PrimaryModel):
null=True,
help_text=_("The primary function of this VLAN")
)
l2vpn_terminations = GenericRelation(
to='ipam.L2VPNTermination',
to='vpn.L2VPNTermination',
content_type_field='assigned_object_type',
object_id_field='assigned_object_id',
related_query_name='vlan'

View File

@ -1,5 +1,5 @@
from . import models
from netbox.search import SearchIndex, register_search
from . import models
@register_search
@ -69,18 +69,6 @@ class IPRangeIndex(SearchIndex):
display_attrs = ('vrf', 'tenant', 'status', 'role', 'description')
@register_search
class L2VPNIndex(SearchIndex):
model = models.L2VPN
fields = (
('name', 100),
('slug', 110),
('description', 500),
('comments', 5000),
)
display_attrs = ('type', 'identifier', 'tenant', 'description')
@register_search
class PrefixIndex(SearchIndex):
model = models.Prefix

View File

@ -1,7 +1,6 @@
from .asn import *
from .fhrp import *
from .ip import *
from .l2vpn import *
from .services import *
from .vlans import *
from .vrfs import *

View File

@ -48,6 +48,7 @@ class ASNTable(TenancyColumnsMixin, NetBoxTable):
asn_asdot = tables.Column(
accessor=tables.A('asn_asdot'),
linkify=True,
order_by=tables.A('asn'),
verbose_name=_('ASDOT')
)
site_count = columns.LinkedCountColumn(

View File

@ -1100,96 +1100,3 @@ class ServiceTest(APIViewTestCases.APIViewTestCase):
'ports': [6],
},
]
class L2VPNTest(APIViewTestCases.APIViewTestCase):
model = L2VPN
brief_fields = ['display', 'id', 'identifier', 'name', 'slug', 'type', 'url']
create_data = [
{
'name': 'L2VPN 4',
'slug': 'l2vpn-4',
'type': 'vxlan',
'identifier': 33343344
},
{
'name': 'L2VPN 5',
'slug': 'l2vpn-5',
'type': 'vxlan',
'identifier': 33343345
},
{
'name': 'L2VPN 6',
'slug': 'l2vpn-6',
'type': 'vpws',
'identifier': 33343346
},
]
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
l2vpns = (
L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
)
L2VPN.objects.bulk_create(l2vpns)
class L2VPNTerminationTest(APIViewTestCases.APIViewTestCase):
model = L2VPNTermination
brief_fields = ['display', 'id', 'l2vpn', 'url']
@classmethod
def setUpTestData(cls):
vlans = (
VLAN(name='VLAN 1', vid=651),
VLAN(name='VLAN 2', vid=652),
VLAN(name='VLAN 3', vid=653),
VLAN(name='VLAN 4', vid=654),
VLAN(name='VLAN 5', vid=655),
VLAN(name='VLAN 6', vid=656),
VLAN(name='VLAN 7', vid=657)
)
VLAN.objects.bulk_create(vlans)
l2vpns = (
L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
)
L2VPN.objects.bulk_create(l2vpns)
l2vpnterminations = (
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
)
L2VPNTermination.objects.bulk_create(l2vpnterminations)
cls.create_data = [
{
'l2vpn': l2vpns[0].pk,
'assigned_object_type': 'ipam.vlan',
'assigned_object_id': vlans[3].pk,
},
{
'l2vpn': l2vpns[0].pk,
'assigned_object_type': 'ipam.vlan',
'assigned_object_id': vlans[4].pk,
},
{
'l2vpn': l2vpns[0].pk,
'assigned_object_type': 'ipam.vlan',
'assigned_object_id': vlans[5].pk,
},
]
cls.bulk_update_data = {
'l2vpn': l2vpns[2].pk
}

View File

@ -7,9 +7,9 @@ from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Man
from ipam.choices import *
from ipam.filtersets import *
from ipam.models import *
from tenancy.models import Tenant, TenantGroup
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
from tenancy.models import Tenant, TenantGroup
class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
@ -1616,163 +1616,3 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'ipaddress': [str(ips[0].address), str(ips[1].address)]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = L2VPN.objects.all()
filterset = L2VPNFilterSet
@classmethod
def setUpTestData(cls):
route_targets = (
RouteTarget(name='1:1'),
RouteTarget(name='1:2'),
RouteTarget(name='1:3'),
RouteTarget(name='2:1'),
RouteTarget(name='2:2'),
RouteTarget(name='2:3'),
)
RouteTarget.objects.bulk_create(route_targets)
l2vpns = (
L2VPN(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=65001),
L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VPWS, identifier=65002),
L2VPN(name='L2VPN 3', slug='l2vpn-3', type=L2VPNTypeChoices.TYPE_VPLS),
)
L2VPN.objects.bulk_create(l2vpns)
l2vpns[0].import_targets.add(route_targets[0])
l2vpns[1].import_targets.add(route_targets[1])
l2vpns[2].import_targets.add(route_targets[2])
l2vpns[0].export_targets.add(route_targets[3])
l2vpns[1].export_targets.add(route_targets[4])
l2vpns[2].export_targets.add(route_targets[5])
def test_name(self):
params = {'name': ['L2VPN 1', 'L2VPN 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_slug(self):
params = {'slug': ['l2vpn-1', 'l2vpn-2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_identifier(self):
params = {'identifier': ['65001', '65002']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type(self):
params = {'type': [L2VPNTypeChoices.TYPE_VXLAN, L2VPNTypeChoices.TYPE_VPWS]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_import_targets(self):
route_targets = RouteTarget.objects.filter(name__in=['1:1', '1:2'])
params = {'import_target_id': [route_targets[0].pk, route_targets[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'import_target': [route_targets[0].name, route_targets[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_export_targets(self):
route_targets = RouteTarget.objects.filter(name__in=['2:1', '2:2'])
params = {'export_target_id': [route_targets[0].pk, route_targets[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'export_target': [route_targets[0].name, route_targets[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = L2VPNTermination.objects.all()
filterset = L2VPNTerminationFilterSet
@classmethod
def setUpTestData(cls):
device = create_test_device('Device 1')
interfaces = (
Interface(name='Interface 1', device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(name='Interface 2', device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(name='Interface 3', device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
)
Interface.objects.bulk_create(interfaces)
vm = create_test_virtualmachine('Virtual Machine 1')
vminterfaces = (
VMInterface(name='Interface 1', virtual_machine=vm),
VMInterface(name='Interface 2', virtual_machine=vm),
VMInterface(name='Interface 3', virtual_machine=vm),
)
VMInterface.objects.bulk_create(vminterfaces)
vlans = (
VLAN(name='VLAN 1', vid=101),
VLAN(name='VLAN 2', vid=102),
VLAN(name='VLAN 3', vid=103),
)
VLAN.objects.bulk_create(vlans)
l2vpns = (
L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=65001),
L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=65002),
L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD,
)
L2VPN.objects.bulk_create(l2vpns)
l2vpnterminations = (
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
L2VPNTermination(l2vpn=l2vpns[1], assigned_object=vlans[1]),
L2VPNTermination(l2vpn=l2vpns[2], assigned_object=vlans[2]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=interfaces[0]),
L2VPNTermination(l2vpn=l2vpns[1], assigned_object=interfaces[1]),
L2VPNTermination(l2vpn=l2vpns[2], assigned_object=interfaces[2]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vminterfaces[0]),
L2VPNTermination(l2vpn=l2vpns[1], assigned_object=vminterfaces[1]),
L2VPNTermination(l2vpn=l2vpns[2], assigned_object=vminterfaces[2]),
)
L2VPNTermination.objects.bulk_create(l2vpnterminations)
def test_l2vpn(self):
l2vpns = L2VPN.objects.all()[:2]
params = {'l2vpn_id': [l2vpns[0].pk, l2vpns[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'l2vpn': [l2vpns[0].slug, l2vpns[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_content_type(self):
params = {'assigned_object_type_id': ContentType.objects.get(model='vlan').pk}
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)
def test_vminterface(self):
vminterfaces = VMInterface.objects.all()[:2]
params = {'vminterface_id': [vminterfaces[0].pk, vminterfaces[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_vlan(self):
vlans = VLAN.objects.all()[:2]
params = {'vlan_id': [vlans[0].pk, vlans[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'vlan': ['VLAN 1', 'VLAN 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site(self):
site = Site.objects.all().first()
params = {'site_id': [site.pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'site': ['site-1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_device(self):
device = Device.objects.all().first()
params = {'device_id': [device.pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'device': ['Device 1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_virtual_machine(self):
virtual_machine = VirtualMachine.objects.all().first()
params = {'virtual_machine_id': [virtual_machine.pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'virtual_machine': ['Virtual Machine 1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)

View File

@ -1,10 +1,9 @@
from netaddr import IPNetwork, IPSet
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
from netaddr import IPNetwork, IPSet
from dcim.models import Interface, Device, DeviceRole, DeviceType, Manufacturer, Site
from ipam.choices import IPAddressRoleChoices, PrefixStatusChoices
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, RIR, VLAN, VLANGroup, VRF, L2VPN, L2VPNTermination
from ipam.choices import *
from ipam.models import *
class TestAggregate(TestCase):
@ -539,76 +538,3 @@ class TestVLANGroup(TestCase):
VLAN.objects.create(name='VLAN 104', vid=104, group=vlangroup)
self.assertEqual(vlangroup.get_next_available_vid(), 105)
class TestL2VPNTermination(TestCase):
@classmethod
def setUpTestData(cls):
site = Site.objects.create(name='Site 1')
manufacturer = Manufacturer.objects.create(name='Manufacturer 1')
device_type = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
role = DeviceRole.objects.create(name='Switch')
device = Device.objects.create(
name='Device 1',
site=site,
device_type=device_type,
role=role,
status='active'
)
interfaces = (
Interface(name='Interface 1', device=device, type='1000baset'),
Interface(name='Interface 2', device=device, type='1000baset'),
Interface(name='Interface 3', device=device, type='1000baset'),
Interface(name='Interface 4', device=device, type='1000baset'),
Interface(name='Interface 5', device=device, type='1000baset'),
)
Interface.objects.bulk_create(interfaces)
vlans = (
VLAN(name='VLAN 1', vid=651),
VLAN(name='VLAN 2', vid=652),
VLAN(name='VLAN 3', vid=653),
VLAN(name='VLAN 4', vid=654),
VLAN(name='VLAN 5', vid=655),
VLAN(name='VLAN 6', vid=656),
VLAN(name='VLAN 7', vid=657)
)
VLAN.objects.bulk_create(vlans)
l2vpns = (
L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
)
L2VPN.objects.bulk_create(l2vpns)
l2vpnterminations = (
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
)
L2VPNTermination.objects.bulk_create(l2vpnterminations)
def test_duplicate_interface_terminations(self):
device = Device.objects.first()
interface = Interface.objects.filter(device=device).first()
l2vpn = L2VPN.objects.first()
L2VPNTermination.objects.create(l2vpn=l2vpn, assigned_object=interface)
duplicate = L2VPNTermination(l2vpn=l2vpn, assigned_object=interface)
self.assertRaises(ValidationError, duplicate.clean)
def test_duplicate_vlan_terminations(self):
vlan = Interface.objects.first()
l2vpn = L2VPN.objects.first()
L2VPNTermination.objects.create(l2vpn=l2vpn, assigned_object=vlan)
duplicate = L2VPNTermination(l2vpn=l2vpn, assigned_object=vlan)
self.assertRaises(ValidationError, duplicate.clean)

View File

@ -9,7 +9,7 @@ from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site, Inte
from ipam.choices import *
from ipam.models import *
from tenancy.models import Tenant
from utilities.testing import ViewTestCases, create_test_device, create_tags
from utilities.testing import ViewTestCases, create_tags
class ASNRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@ -986,142 +986,3 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
self.assertEqual(instance.protocol, service_template.protocol)
self.assertEqual(instance.ports, service_template.ports)
self.assertEqual(instance.description, service_template.description)
class L2VPNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = L2VPN
@classmethod
def setUpTestData(cls):
rts = (
RouteTarget(name='64534:123'),
RouteTarget(name='64534:321')
)
RouteTarget.objects.bulk_create(rts)
l2vpns = (
L2VPN(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650001'),
L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650002'),
L2VPN(name='L2VPN 3', slug='l2vpn-3', type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650003')
)
L2VPN.objects.bulk_create(l2vpns)
cls.csv_data = (
'name,slug,type,identifier',
'L2VPN 5,l2vpn-5,vxlan,456',
'L2VPN 6,l2vpn-6,vxlan,444',
)
cls.csv_update_data = (
'id,name,description',
f'{l2vpns[0].pk},L2VPN 7,New description 7',
f'{l2vpns[1].pk},L2VPN 8,New description 8',
)
cls.bulk_edit_data = {
'description': 'New Description',
}
cls.form_data = {
'name': 'L2VPN 8',
'slug': 'l2vpn-8',
'type': L2VPNTypeChoices.TYPE_VXLAN,
'identifier': 123,
'description': 'Description',
'import_targets': [rts[0].pk],
'export_targets': [rts[1].pk]
}
class L2VPNTerminationTestCase(
ViewTestCases.GetObjectViewTestCase,
ViewTestCases.GetObjectChangelogViewTestCase,
ViewTestCases.CreateObjectViewTestCase,
ViewTestCases.EditObjectViewTestCase,
ViewTestCases.DeleteObjectViewTestCase,
ViewTestCases.ListObjectsViewTestCase,
ViewTestCases.BulkImportObjectsViewTestCase,
ViewTestCases.BulkDeleteObjectsViewTestCase,
):
model = L2VPNTermination
@classmethod
def setUpTestData(cls):
device = create_test_device('Device 1')
interface = Interface.objects.create(name='Interface 1', device=device, type='1000baset')
l2vpns = (
L2VPN(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=650001),
L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=650002),
)
L2VPN.objects.bulk_create(l2vpns)
vlans = (
VLAN(name='Vlan 1', vid=1001),
VLAN(name='Vlan 2', vid=1002),
VLAN(name='Vlan 3', vid=1003),
VLAN(name='Vlan 4', vid=1004),
VLAN(name='Vlan 5', vid=1005),
VLAN(name='Vlan 6', vid=1006)
)
VLAN.objects.bulk_create(vlans)
terminations = (
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
)
L2VPNTermination.objects.bulk_create(terminations)
cls.form_data = {
'l2vpn': l2vpns[0].pk,
'device': device.pk,
'interface': interface.pk,
}
cls.csv_data = (
"l2vpn,vlan",
"L2VPN 1,Vlan 4",
"L2VPN 1,Vlan 5",
"L2VPN 1,Vlan 6",
)
cls.csv_update_data = (
f"id,l2vpn",
f"{terminations[0].pk},{l2vpns[0].name}",
f"{terminations[1].pk},{l2vpns[0].name}",
f"{terminations[2].pk},{l2vpns[0].name}",
)
cls.bulk_edit_data = {}
# TODO: Fix L2VPNTerminationImportForm validation to support bulk updates
def test_bulk_update_objects_with_permission(self):
pass
#
# Custom assertions
#
# TODO: Remove this
def assertInstanceEqual(self, instance, data, exclude=None, api=False):
"""
Override parent
"""
if exclude is None:
exclude = []
fields = [k for k in data.keys() if k not in exclude]
model_dict = self.model_to_dict(instance, fields=fields, api=api)
# Omit any dictionary keys which are not instance attributes or have been excluded
relevant_data = {
k: v for k, v in data.items() if hasattr(instance, k) and k not in exclude
}
# Handle relations on the model
for k, v in model_dict.items():
if isinstance(v, object) and hasattr(v, 'first'):
model_dict[k] = v.first().pk
self.assertDictEqual(model_dict, relevant_data)

View File

@ -131,20 +131,4 @@ urlpatterns = [
path('services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
path('services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),
path('services/<int:pk>/', include(get_model_urls('ipam', 'service'))),
# L2VPN
path('l2vpns/', views.L2VPNListView.as_view(), name='l2vpn_list'),
path('l2vpns/add/', views.L2VPNEditView.as_view(), name='l2vpn_add'),
path('l2vpns/import/', views.L2VPNBulkImportView.as_view(), name='l2vpn_import'),
path('l2vpns/edit/', views.L2VPNBulkEditView.as_view(), name='l2vpn_bulk_edit'),
path('l2vpns/delete/', views.L2VPNBulkDeleteView.as_view(), name='l2vpn_bulk_delete'),
path('l2vpns/<int:pk>/', include(get_model_urls('ipam', 'l2vpn'))),
# L2VPN terminations
path('l2vpn-terminations/', views.L2VPNTerminationListView.as_view(), name='l2vpntermination_list'),
path('l2vpn-terminations/add/', views.L2VPNTerminationEditView.as_view(), name='l2vpntermination_add'),
path('l2vpn-terminations/import/', views.L2VPNTerminationBulkImportView.as_view(), name='l2vpntermination_import'),
path('l2vpn-terminations/edit/', views.L2VPNTerminationBulkEditView.as_view(), name='l2vpntermination_bulk_edit'),
path('l2vpn-terminations/delete/', views.L2VPNTerminationBulkDeleteView.as_view(), name='l2vpntermination_bulk_delete'),
path('l2vpn-terminations/<int:pk>/', include(get_model_urls('ipam', 'l2vpntermination'))),
]

View File

@ -1,5 +1,5 @@
from django.contrib.contenttypes.models import ContentType
from django.db.models import F, Prefetch
from django.db.models import Prefetch
from django.db.models.expressions import RawSQL
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
@ -9,7 +9,6 @@ from circuits.models import Provider
from dcim.filtersets import InterfaceFilterSet
from dcim.models import Interface, Site
from netbox.views import generic
from tenancy.views import ObjectContactsView
from utilities.tables import get_table_ordering
from utilities.utils import count_related
from utilities.views import ViewTab, register_model_view
@ -19,7 +18,6 @@ from . import filtersets, forms, tables
from .choices import PrefixStatusChoices
from .constants import *
from .models import *
from .tables.l2vpn import L2VPNTable, L2VPNTerminationTable
from .utils import add_requested_prefixes, add_available_ipaddresses, add_available_vlans
@ -1243,112 +1241,3 @@ class ServiceBulkDeleteView(generic.BulkDeleteView):
queryset = Service.objects.prefetch_related('device', 'virtual_machine')
filterset = filtersets.ServiceFilterSet
table = tables.ServiceTable
# L2VPN
class L2VPNListView(generic.ObjectListView):
queryset = L2VPN.objects.all()
table = L2VPNTable
filterset = filtersets.L2VPNFilterSet
filterset_form = forms.L2VPNFilterForm
@register_model_view(L2VPN)
class L2VPNView(generic.ObjectView):
queryset = L2VPN.objects.all()
def get_extra_context(self, request, instance):
import_targets_table = tables.RouteTargetTable(
instance.import_targets.prefetch_related('tenant'),
orderable=False
)
export_targets_table = tables.RouteTargetTable(
instance.export_targets.prefetch_related('tenant'),
orderable=False
)
return {
'import_targets_table': import_targets_table,
'export_targets_table': export_targets_table,
}
@register_model_view(L2VPN, 'edit')
class L2VPNEditView(generic.ObjectEditView):
queryset = L2VPN.objects.all()
form = forms.L2VPNForm
@register_model_view(L2VPN, 'delete')
class L2VPNDeleteView(generic.ObjectDeleteView):
queryset = L2VPN.objects.all()
class L2VPNBulkImportView(generic.BulkImportView):
queryset = L2VPN.objects.all()
model_form = forms.L2VPNImportForm
class L2VPNBulkEditView(generic.BulkEditView):
queryset = L2VPN.objects.all()
filterset = filtersets.L2VPNFilterSet
table = tables.L2VPNTable
form = forms.L2VPNBulkEditForm
class L2VPNBulkDeleteView(generic.BulkDeleteView):
queryset = L2VPN.objects.all()
filterset = filtersets.L2VPNFilterSet
table = tables.L2VPNTable
@register_model_view(L2VPN, 'contacts')
class L2VPNContactsView(ObjectContactsView):
queryset = L2VPN.objects.all()
#
# L2VPN terminations
#
class L2VPNTerminationListView(generic.ObjectListView):
queryset = L2VPNTermination.objects.all()
table = L2VPNTerminationTable
filterset = filtersets.L2VPNTerminationFilterSet
filterset_form = forms.L2VPNTerminationFilterForm
@register_model_view(L2VPNTermination)
class L2VPNTerminationView(generic.ObjectView):
queryset = L2VPNTermination.objects.all()
@register_model_view(L2VPNTermination, 'edit')
class L2VPNTerminationEditView(generic.ObjectEditView):
queryset = L2VPNTermination.objects.all()
form = forms.L2VPNTerminationForm
template_name = 'ipam/l2vpntermination_edit.html'
@register_model_view(L2VPNTermination, 'delete')
class L2VPNTerminationDeleteView(generic.ObjectDeleteView):
queryset = L2VPNTermination.objects.all()
class L2VPNTerminationBulkImportView(generic.BulkImportView):
queryset = L2VPNTermination.objects.all()
model_form = forms.L2VPNTerminationImportForm
class L2VPNTerminationBulkEditView(generic.BulkEditView):
queryset = L2VPNTermination.objects.all()
filterset = filtersets.L2VPNTerminationFilterSet
table = tables.L2VPNTerminationTable
form = forms.L2VPNTerminationBulkEditForm
class L2VPNTerminationBulkDeleteView(generic.BulkDeleteView):
queryset = L2VPNTermination.objects.all()
filterset = filtersets.L2VPNTerminationFilterSet
table = tables.L2VPNTerminationTable

View File

@ -209,8 +209,8 @@ VPN_MENU = Menu(
MenuGroup(
label=_('L2VPNs'),
items=(
get_model_item('ipam', 'l2vpn', _('L2VPNs')),
get_model_item('ipam', 'l2vpntermination', _('Terminations')),
get_model_item('vpn', 'l2vpn', _('L2VPNs')),
get_model_item('vpn', 'l2vpntermination', _('Terminations')),
),
),
MenuGroup(

View File

@ -27,7 +27,7 @@ from netbox.plugins import PluginConfig
# Environment setup
#
VERSION = '3.6.6-dev'
VERSION = '3.6.7-dev'
# Hostname
HOSTNAME = platform.node()

View File

@ -394,6 +394,10 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
form.add_error('data', f"Row {i}: Object with ID {object_id} does not exist")
raise ValidationError('')
# Take a snapshot for change logging
if instance.pk and hasattr(instance, 'snapshot'):
instance.snapshot()
# Instantiate the model form for the object
model_form_kwargs = {
'data': record,

View File

@ -5,6 +5,7 @@
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% load mptt %}
{% block content %}
<div class="row">
@ -15,16 +16,7 @@
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Region" %}</th>
<td>
{% if object.site.region %}
{% for region in object.site.region.get_ancestors %}
{{ region|linkify }} /
{% endfor %}
{{ object.site.region|linkify }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<td>{% nested_tree object.site.region %}</td>
</tr>
<tr>
<th scope="row">{% trans "Site" %}</th>
@ -32,16 +24,7 @@
</tr>
<tr>
<th scope="row">{% trans "Location" %}</th>
<td>
{% if object.location %}
{% for location in object.location.get_ancestors %}
{{ location|linkify }} /
{% endfor %}
{{ object.location|linkify }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<td>{% nested_tree object.location %}</td>
</tr>
<tr>
<th scope="row">{% trans "Rack" %}</th>

View File

@ -4,6 +4,7 @@
{% load static %}
{% load plugins %}
{% load i18n %}
{% load mptt %}
{% block content %}
<div class="row">
@ -15,26 +16,18 @@
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Site" %}</th>
<th scope="row">{% trans "Region" %}</th>
<td>
{% if object.site.region %}
{{ object.site.region|linkify }} /
{% endif %}
{{ object.site|linkify }}
{% nested_tree object.site.region %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Site" %}</th>
<td>{{ object.site|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Location" %}</th>
<td>
{% if object.location %}
{% for location in object.location.get_ancestors %}
{{ location|linkify }} /
{% endfor %}
{{ object.location|linkify }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<td>{% nested_tree object.location %}</td>
</tr>
<tr>
<th scope="row">{% trans "Facility ID" %}</th>

View File

@ -4,6 +4,7 @@
{% load static %}
{% load plugins %}
{% load i18n %}
{% load mptt %}
{% block breadcrumbs %}
{{ block.super }}
@ -20,25 +21,24 @@
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
{% with rack=object.rack %}
<tr>
<th scope="row">{% trans "Site" %}</th>
<td>
{% if rack.site.region %}
{{ rack.site.region|linkify }} /
{% endif %}
{{ rack.site|linkify }}
</td>
</tr>
<tr>
<th scope="row">{% trans "Location" %}</th>
<td>{{ rack.location|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Rack" %}</th>
<td>{{ rack|linkify }}</td>
</tr>
{% endwith %}
<tr>
<th scope="row">{% trans "Region" %}</th>
<td>
{% nested_tree object.rack.site.region %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Site" %}</th>
<td>{{ object.rack.site|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Location" %}</th>
<td>{{ object.rack.location|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Rack" %}</th>
<td>{{ object.rack|linkify }}</td>
</tr>
</table>
</div>
</div>

View File

@ -3,6 +3,7 @@
{% load plugins %}
{% load tz %}
{% load i18n %}
{% load mptt %}
{% block breadcrumbs %}
{{ block.super }}
@ -29,27 +30,13 @@
<tr>
<th scope="row">{% trans "Region" %}</th>
<td>
{% if object.region %}
{% for region in object.region.get_ancestors %}
{{ region|linkify }} /
{% endfor %}
{{ object.region|linkify }}
{% else %}
{{ ''|placeholder }}
{% endif %}
{% nested_tree object.region %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Group" %}</th>
<td>
{% if object.group %}
{% for group in object.group.get_ancestors %}
{{ group|linkify }} /
{% endfor %}
{{ object.group|linkify }}
{% else %}
{{ ''|placeholder }}
{% endif %}
{% nested_tree object.group %}
</td>
</tr>
<tr>

View File

@ -3,6 +3,7 @@
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% load mptt %}
{% block content %}
<div class="row">
@ -44,18 +45,17 @@
{% endif %}
</td>
</tr>
{% if object.site.region %}
<tr>
<th scope="row">{% trans "Region" %}</th>
<td>
{% nested_tree object.site.region %}
</td>
</tr>
{% endif %}
<tr>
<th scope="row">{% trans "Site" %}</th>
<td>
{% if object.site %}
{% if object.site.region %}
{{ object.site.region|linkify }} /
{% endif %}
{{ object.site|linkify }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<td>{{ object.site|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "VLAN" %}</th>

View File

@ -59,7 +59,7 @@
<div class="card">
<h5 class="card-header">{% trans "Importing L2VPNs" %}</h5>
<div class="card-body htmx-container table-responsive"
hx-get="{% url 'ipam:l2vpn_list' %}?import_target_id={{ object.pk }}"
hx-get="{% url 'vpn:l2vpn_list' %}?import_target_id={{ object.pk }}"
hx-trigger="load"
></div>
</div>
@ -68,7 +68,7 @@
<div class="card">
<h5 class="card-header">{% trans "Exporting L2VPNs" %}</h5>
<div class="card-body htmx-container table-responsive"
hx-get="{% url 'ipam:l2vpn_list' %}?export_target_id={{ object.pk }}"
hx-get="{% url 'vpn:l2vpn_list' %}?export_target_id={{ object.pk }}"
hx-trigger="load"
></div>
</div>

View File

@ -3,6 +3,7 @@
{% load render_table from django_tables2 %}
{% load plugins %}
{% load i18n %}
{% load mptt %}
{% block content %}
<div class="row">
@ -13,18 +14,17 @@
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
{% if object.site.region %}
<tr>
<th scope="row">{% trans "Region" %}</th>
<td>
{% nested_tree object.site.region %}
</td>
</tr>
{% endif %}
<tr>
<th scope="row">{% trans "Site" %}</th>
<td>
{% if object.site %}
{% if object.site.region %}
{{ object.site.region|linkify }} /
{% endif %}
{{ object.site|linkify }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<td>{{ object.site|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Group" %}</th>

View File

@ -34,7 +34,7 @@
</table>
</div>
</div>
{% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:l2vpn_list' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='vpn:l2vpn_list' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
@ -56,12 +56,12 @@
<div class="card">
<h5 class="card-header">{% trans "Terminations" %}</h5>
<div class="card-body htmx-container table-responsive"
hx-get="{% url 'ipam:l2vpntermination_list' %}?l2vpn_id={{ object.pk }}"
hx-get="{% url 'vpn:l2vpntermination_list' %}?l2vpn_id={{ object.pk }}"
hx-trigger="load"
></div>
{% if perms.ipam.add_l2vpntermination %}
{% if perms.vpn.add_l2vpntermination %}
<div class="card-footer text-end noprint">
<a href="{% url 'ipam:l2vpntermination_add' %}?l2vpn={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm{% if not object.can_add_termination %} disabled" aria-disabled="true{% endif %}">
<a href="{% url 'vpn:l2vpntermination_add' %}?l2vpn={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm{% if not object.can_add_termination %} disabled" aria-disabled="true{% endif %}">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Termination" %}
</a>
</div>

View File

@ -25,7 +25,7 @@
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:l2vpntermination_list' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='vpn:l2vpntermination_list' %}
</div>
</div>

View File

@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
from core.models import ContentType
from netbox.models import ChangeLoggedModel, NestedGroupModel, OrganizationalModel, PrimaryModel
from netbox.models.features import CustomFieldsMixin, TagsMixin
from netbox.models.features import CustomFieldsMixin, ExportTemplatesMixin, TagsMixin
from tenancy.choices import *
__all__ = (
@ -110,7 +110,7 @@ class Contact(PrimaryModel):
return reverse('tenancy:contact', args=[self.pk])
class ContactAssignment(CustomFieldsMixin, TagsMixin, ChangeLoggedModel):
class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
content_type = models.ForeignKey(
to='contenttypes.ContentType',
on_delete=models.CASCADE

View File

@ -52,6 +52,16 @@ class UserSerializer(ValidatedModelSerializer):
return user
def update(self, instance, validated_data):
"""
Ensure proper updated password hash generation.
"""
password = validated_data.pop('password', None)
if password is not None:
instance.set_password(password)
return super().update(instance, validated_data)
@extend_schema_field(OpenApiTypes.STR)
def get_display(self, obj):
if full_name := obj.get_full_name():

View File

@ -54,6 +54,38 @@ class UserTest(APIViewTestCases.APIViewTestCase):
)
User.objects.bulk_create(users)
def test_that_password_is_changed(self):
"""
Test that password is changed
"""
obj_perm = ObjectPermission(
name='Test permission',
actions=['change']
)
obj_perm.save()
obj_perm.users.add(self.user)
obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
user_credentials = {
'username': 'user1',
'password': 'abc123',
}
user = User.objects.create_user(**user_credentials)
data = {
'password': 'newpassword'
}
url = reverse('users-api:user-detail', kwargs={'pk': user.id})
response = self.client.patch(url, data, format='json', **self.header)
self.assertEqual(response.status_code, 200)
updated_user = User.objects.get(id=user.id)
self.assertTrue(updated_user.check_password(data['password']))
class GroupTest(APIViewTestCases.APIViewTestCase):
model = Group

View File

@ -40,7 +40,7 @@ def parse_numeric_range(string, base=10):
except ValueError:
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
values.extend(range(begin, end))
return list(set(values))
return sorted(set(values))
def parse_alphanumeric_range(string):

View File

@ -0,0 +1,20 @@
from django import template
from django.utils.safestring import mark_safe
register = template.Library()
@register.simple_tag()
def nested_tree(obj):
"""
Renders the entire hierarchy of a recursively-nested object (such as Region or SiteGroup).
"""
if not obj:
return mark_safe('&mdash;')
nodes = obj.get_ancestors(include_self=True)
return mark_safe(
' / '.join(
f'<a href="{node.get_absolute_url()}">{node}</a>' for node in nodes
)
)

View File

@ -6,15 +6,14 @@ from dcim.api.nested_serializers import (
)
from dcim.choices import InterfaceModeChoices
from extras.api.nested_serializers import NestedConfigTemplateSerializer
from ipam.api.nested_serializers import (
NestedIPAddressSerializer, NestedL2VPNTerminationSerializer, NestedVLANSerializer, NestedVRFSerializer,
)
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
from ipam.models import VLAN
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer
from virtualization.choices import *
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualDisk, VirtualMachine, VMInterface
from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
from .nested_serializers import *

View File

@ -296,9 +296,10 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
# Check interface sites. First interface should set site, further interfaces will either continue the
# loop or reset back to no site and break the loop.
for interface in interfaces:
vm_site = interface.virtual_machine.site or interface.virtual_machine.cluster.site
if site is None:
site = interface.virtual_machine.cluster.site
elif interface.virtual_machine.cluster.site is not site:
site = vm_site
elif vm_site is not site:
site = None
break

View File

@ -4,13 +4,14 @@ from django.utils.translation import gettext_lazy as _
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
from extras.forms import LocalConfigContextFilterForm
from extras.models import ConfigTemplate
from ipam.models import L2VPN, VRF
from ipam.models import VRF
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES
from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
from virtualization.choices import *
from virtualization.models import *
from vpn.models import L2VPN
__all__ = (
'ClusterFilterForm',

View File

@ -358,7 +358,7 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
related_query_name='vminterface',
)
l2vpn_terminations = GenericRelation(
to='ipam.L2VPNTermination',
to='vpn.L2VPNTermination',
content_type_field='assigned_object_type',
object_id_field='assigned_object_id',
related_query_name='vminterface',

View File

@ -24,8 +24,8 @@ VMINTERFACE_BUTTONS = """
{% if perms.ipam.add_ipaddress %}
<li><a class="dropdown-item" href="{% url 'ipam:ipaddress_add' %}?vminterface={{ record.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">IP Address</a></li>
{% endif %}
{% if perms.ipam.add_l2vpntermination %}
<li><a class="dropdown-item" href="{% url 'ipam:l2vpntermination_add' %}?virtual_machine={{ object.pk }}&vminterface={{ record.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">L2VPN Termination</a></li>
{% if perms.vpn.add_l2vpntermination %}
<li><a class="dropdown-item" href="{% url 'vpn:l2vpntermination_add' %}?virtual_machine={{ object.pk }}&vminterface={{ record.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">L2VPN Termination</a></li>
{% endif %}
{% if perms.ipam.add_fhrpgroupassignment %}
<li><a class="dropdown-item" href="{% url 'ipam:fhrpgroupassignment_add' %}?interface_type={{ record|content_type_id }}&interface_id={{ record.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">Assign FHRP Group</a></li>

View File

@ -9,6 +9,8 @@ __all__ = (
'NestedIPSecPolicySerializer',
'NestedIPSecProfileSerializer',
'NestedIPSecProposalSerializer',
'NestedL2VPNSerializer',
'NestedL2VPNTerminationSerializer',
'NestedTunnelSerializer',
'NestedTunnelTerminationSerializer',
)
@ -82,3 +84,28 @@ class NestedIPSecProfileSerializer(WritableNestedSerializer):
class Meta:
model = models.IPSecProfile
fields = ('id', 'url', 'display', 'name')
#
# L2VPN
#
class NestedL2VPNSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpn-detail')
class Meta:
model = models.L2VPN
fields = [
'id', 'url', 'display', 'identifier', 'name', 'slug', 'type'
]
class NestedL2VPNTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpntermination-detail')
l2vpn = NestedL2VPNSerializer()
class Meta:
model = models.L2VPNTermination
fields = [
'id', 'url', 'display', 'l2vpn'
]

View File

@ -2,7 +2,8 @@ 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 ipam.api.nested_serializers import NestedIPAddressSerializer, NestedRouteTargetSerializer
from ipam.models import RouteTarget
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer
from netbox.constants import NESTED_SERIALIZER_PREFIX
@ -18,6 +19,8 @@ __all__ = (
'IPSecPolicySerializer',
'IPSecProfileSerializer',
'IPSecProposalSerializer',
'L2VPNSerializer',
'L2VPNTerminationSerializer',
'TunnelSerializer',
'TunnelTerminationSerializer',
)
@ -191,3 +194,54 @@ class IPSecProfileSerializer(NetBoxModelSerializer):
'id', 'url', 'display', 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
)
#
# L2VPN
#
class L2VPNSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpn-detail')
type = ChoiceField(choices=L2VPNTypeChoices, required=False)
import_targets = SerializedPKRelatedField(
queryset=RouteTarget.objects.all(),
serializer=NestedRouteTargetSerializer,
required=False,
many=True
)
export_targets = SerializedPKRelatedField(
queryset=RouteTarget.objects.all(),
serializer=NestedRouteTargetSerializer,
required=False,
many=True
)
tenant = NestedTenantSerializer(required=False, allow_null=True)
class Meta:
model = L2VPN
fields = [
'id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'import_targets', 'export_targets',
'description', 'comments', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated'
]
class L2VPNTerminationSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpntermination-detail')
l2vpn = NestedL2VPNSerializer()
assigned_object_type = ContentTypeField(
queryset=ContentType.objects.all()
)
assigned_object = serializers.SerializerMethodField(read_only=True)
class Meta:
model = L2VPNTermination
fields = [
'id', 'url', 'display', 'l2vpn', 'assigned_object_type', 'assigned_object_id',
'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated'
]
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_assigned_object(self, instance):
serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
context = {'request': self.context['request']}
return serializer(instance.assigned_object, context=context).data

View File

@ -10,6 +10,8 @@ router.register('ipsec-proposals', views.IPSecProposalViewSet)
router.register('ipsec-profiles', views.IPSecProfileViewSet)
router.register('tunnels', views.TunnelViewSet)
router.register('tunnel-terminations', views.TunnelTerminationViewSet)
router.register('l2vpns', views.L2VPNViewSet)
router.register('l2vpn-terminations', views.L2VPNTerminationViewSet)
app_name = 'vpn-api'
urlpatterns = router.urls

View File

@ -12,6 +12,8 @@ __all__ = (
'IPSecPolicyViewSet',
'IPSecProfileViewSet',
'IPSecProposalViewSet',
'L2VPNViewSet',
'L2VPNTerminationViewSet',
'TunnelTerminationViewSet',
'TunnelViewSet',
'VPNRootView',
@ -72,3 +74,15 @@ class IPSecProfileViewSet(NetBoxModelViewSet):
queryset = IPSecProfile.objects.all()
serializer_class = serializers.IPSecProfileSerializer
filterset_class = filtersets.IPSecProfileFilterSet
class L2VPNViewSet(NetBoxModelViewSet):
queryset = L2VPN.objects.prefetch_related('import_targets', 'export_targets', 'tenant', 'tags')
serializer_class = serializers.L2VPNSerializer
filterset_class = filtersets.L2VPNFilterSet
class L2VPNTerminationViewSet(NetBoxModelViewSet):
queryset = L2VPNTermination.objects.prefetch_related('assigned_object')
serializer_class = serializers.L2VPNTerminationSerializer
filterset_class = filtersets.L2VPNTerminationFilterSet

View File

@ -199,3 +199,56 @@ class DHGroupChoices(ChoiceSet):
(GROUP_33, _('Group {n}').format(n=33)),
(GROUP_34, _('Group {n}').format(n=34)),
)
#
# L2VPN
#
class L2VPNTypeChoices(ChoiceSet):
TYPE_VPLS = 'vpls'
TYPE_VPWS = 'vpws'
TYPE_EPL = 'epl'
TYPE_EVPL = 'evpl'
TYPE_EPLAN = 'ep-lan'
TYPE_EVPLAN = 'evp-lan'
TYPE_EPTREE = 'ep-tree'
TYPE_EVPTREE = 'evp-tree'
TYPE_VXLAN = 'vxlan'
TYPE_VXLAN_EVPN = 'vxlan-evpn'
TYPE_MPLS_EVPN = 'mpls-evpn'
TYPE_PBB_EVPN = 'pbb-evpn'
CHOICES = (
('VPLS', (
(TYPE_VPWS, 'VPWS'),
(TYPE_VPLS, 'VPLS'),
)),
('VXLAN', (
(TYPE_VXLAN, 'VXLAN'),
(TYPE_VXLAN_EVPN, 'VXLAN-EVPN'),
)),
('L2VPN E-VPN', (
(TYPE_MPLS_EVPN, 'MPLS EVPN'),
(TYPE_PBB_EVPN, 'PBB EVPN'),
)),
('E-Line', (
(TYPE_EPL, 'EPL'),
(TYPE_EVPL, 'EVPL'),
)),
('E-LAN', (
(TYPE_EPLAN, _('Ethernet Private LAN')),
(TYPE_EVPLAN, _('Ethernet Virtual Private LAN')),
)),
('E-Tree', (
(TYPE_EPTREE, _('Ethernet Private Tree')),
(TYPE_EVPTREE, _('Ethernet Virtual Private Tree')),
)),
)
P2P = (
TYPE_VPWS,
TYPE_EPL,
TYPE_EPLAN,
TYPE_EPTREE
)

7
netbox/vpn/constants.py Normal file
View File

@ -0,0 +1,7 @@
from django.db.models import Q
L2VPN_ASSIGNMENT_MODELS = Q(
Q(app_label='dcim', model='interface') |
Q(app_label='ipam', model='vlan') |
Q(app_label='virtualization', model='vminterface')
)

View File

@ -2,12 +2,12 @@ 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 dcim.models import Device, Interface
from ipam.models import IPAddress, RouteTarget, VLAN
from netbox.filtersets import NetBoxModelFilterSet
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
from virtualization.models import VMInterface
from virtualization.models import VirtualMachine, VMInterface
from .choices import *
from .models import *
@ -17,6 +17,8 @@ __all__ = (
'IPSecPolicyFilterSet',
'IPSecProfileFilterSet',
'IPSecProposalFilterSet',
'L2VPNFilterSet',
'L2VPNTerminationFilterSet',
'TunnelFilterSet',
'TunnelTerminationFilterSet',
)
@ -239,3 +241,175 @@ class IPSecProfileFilterSet(NetBoxModelFilterSet):
Q(description__icontains=value) |
Q(comments__icontains=value)
)
class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=L2VPNTypeChoices,
null_value=None
)
import_target_id = django_filters.ModelMultipleChoiceFilter(
field_name='import_targets',
queryset=RouteTarget.objects.all(),
label=_('Import target'),
)
import_target = django_filters.ModelMultipleChoiceFilter(
field_name='import_targets__name',
queryset=RouteTarget.objects.all(),
to_field_name='name',
label=_('Import target (name)'),
)
export_target_id = django_filters.ModelMultipleChoiceFilter(
field_name='export_targets',
queryset=RouteTarget.objects.all(),
label=_('Export target'),
)
export_target = django_filters.ModelMultipleChoiceFilter(
field_name='export_targets__name',
queryset=RouteTarget.objects.all(),
to_field_name='name',
label=_('Export target (name)'),
)
class Meta:
model = L2VPN
fields = ['id', 'identifier', 'name', 'slug', 'type', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
try:
qs_filter |= Q(identifier=int(value))
except ValueError:
pass
return queryset.filter(qs_filter)
class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
queryset=L2VPN.objects.all(),
label=_('L2VPN (ID)'),
)
l2vpn = django_filters.ModelMultipleChoiceFilter(
field_name='l2vpn__slug',
queryset=L2VPN.objects.all(),
to_field_name='slug',
label=_('L2VPN (slug)'),
)
region = MultiValueCharFilter(
method='filter_region',
field_name='slug',
label=_('Region (slug)'),
)
region_id = MultiValueNumberFilter(
method='filter_region',
field_name='pk',
label=_('Region (ID)'),
)
site = MultiValueCharFilter(
method='filter_site',
field_name='slug',
label=_('Site (slug)'),
)
site_id = MultiValueNumberFilter(
method='filter_site',
field_name='pk',
label=_('Site (ID)'),
)
device = django_filters.ModelMultipleChoiceFilter(
field_name='interface__device__name',
queryset=Device.objects.all(),
to_field_name='name',
label=_('Device (name)'),
)
device_id = django_filters.ModelMultipleChoiceFilter(
field_name='interface__device',
queryset=Device.objects.all(),
label=_('Device (ID)'),
)
virtual_machine = django_filters.ModelMultipleChoiceFilter(
field_name='vminterface__virtual_machine__name',
queryset=VirtualMachine.objects.all(),
to_field_name='name',
label=_('Virtual machine (name)'),
)
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
field_name='vminterface__virtual_machine',
queryset=VirtualMachine.objects.all(),
label=_('Virtual machine (ID)'),
)
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)'),
)
vlan = django_filters.ModelMultipleChoiceFilter(
field_name='vlan__name',
queryset=VLAN.objects.all(),
to_field_name='name',
label=_('VLAN (name)'),
)
vlan_vid = django_filters.NumberFilter(
field_name='vlan__vid',
label=_('VLAN number (1-4094)'),
)
vlan_id = django_filters.ModelMultipleChoiceFilter(
field_name='vlan',
queryset=VLAN.objects.all(),
label=_('VLAN (ID)'),
)
assigned_object_type = ContentTypeFilter()
class Meta:
model = L2VPNTermination
fields = ('id', 'assigned_object_type_id')
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = Q(l2vpn__name__icontains=value)
return queryset.filter(qs_filter)
def filter_assigned_object(self, queryset, name, value):
qs = queryset.filter(
Q(**{'{}__in'.format(name): value})
)
return qs
def filter_site(self, queryset, name, value):
qs = queryset.filter(
Q(
Q(**{'vlan__site__{}__in'.format(name): value}) |
Q(**{'interface__device__site__{}__in'.format(name): value}) |
Q(**{'vminterface__virtual_machine__site__{}__in'.format(name): value})
)
)
return qs
def filter_region(self, queryset, name, value):
qs = queryset.filter(
Q(
Q(**{'vlan__site__region__{}__in'.format(name): value}) |
Q(**{'interface__device__site__region__{}__in'.format(name): value}) |
Q(**{'vminterface__virtual_machine__site__region__{}__in'.format(name): value})
)
)
return qs

View File

@ -14,6 +14,8 @@ __all__ = (
'IPSecPolicyBulkEditForm',
'IPSecProfileBulkEditForm',
'IPSecProposalBulkEditForm',
'L2VPNBulkEditForm',
'L2VPNTerminationBulkEditForm',
'TunnelBulkEditForm',
'TunnelTerminationBulkEditForm',
)
@ -241,3 +243,32 @@ class IPSecProfileBulkEditForm(NetBoxModelBulkEditForm):
nullable_fields = (
'description', 'comments',
)
class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
type = forms.ChoiceField(
label=_('Type'),
choices=add_blank_choice(L2VPNTypeChoices),
required=False
)
tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField()
model = L2VPN
fieldsets = (
(None, ('type', 'tenant', 'description')),
)
nullable_fields = ('tenant', 'description', 'comments')
class L2VPNTerminationBulkEditForm(NetBoxModelBulkEditForm):
model = L2VPN

View File

@ -1,7 +1,8 @@
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface
from ipam.models import IPAddress
from ipam.models import IPAddress, VLAN
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField
@ -15,6 +16,8 @@ __all__ = (
'IPSecPolicyImportForm',
'IPSecProfileImportForm',
'IPSecProposalImportForm',
'L2VPNImportForm',
'L2VPNTerminationImportForm',
'TunnelImportForm',
'TunnelTerminationImportForm',
)
@ -228,3 +231,92 @@ class IPSecProfileImportForm(NetBoxModelImportForm):
fields = (
'name', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags',
)
class L2VPNImportForm(NetBoxModelImportForm):
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
)
type = CSVChoiceField(
label=_('Type'),
choices=L2VPNTypeChoices,
help_text=_('L2VPN type')
)
class Meta:
model = L2VPN
fields = ('identifier', 'name', 'slug', 'tenant', 'type', 'description',
'comments', 'tags')
class L2VPNTerminationImportForm(NetBoxModelImportForm):
l2vpn = CSVModelChoiceField(
queryset=L2VPN.objects.all(),
required=True,
to_field_name='name',
label=_('L2VPN'),
)
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
required=False,
to_field_name='name',
help_text=_('Parent device (for interface)')
)
virtual_machine = CSVModelChoiceField(
label=_('Virtual machine'),
queryset=VirtualMachine.objects.all(),
required=False,
to_field_name='name',
help_text=_('Parent virtual machine (for interface)')
)
interface = CSVModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.none(), # Can also refer to VMInterface
required=False,
to_field_name='name',
help_text=_('Assigned interface (device or VM)')
)
vlan = CSVModelChoiceField(
label=_('VLAN'),
queryset=VLAN.objects.all(),
required=False,
to_field_name='name',
help_text=_('Assigned VLAN')
)
class Meta:
model = L2VPNTermination
fields = ('l2vpn', 'device', 'virtual_machine', 'interface', 'vlan', 'tags')
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
if data:
# Limit interface queryset by device or VM
if data.get('device'):
self.fields['interface'].queryset = Interface.objects.filter(
**{f"device__{self.fields['device'].to_field_name}": data['device']}
)
elif data.get('virtual_machine'):
self.fields['interface'].queryset = VMInterface.objects.filter(
**{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
)
def clean(self):
super().clean()
if self.cleaned_data.get('device') and self.cleaned_data.get('virtual_machine'):
raise ValidationError(_('Cannot import device and VM interface terminations simultaneously.'))
if not self.instance and not (self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')):
raise ValidationError(_('Each termination must specify either an interface or a VLAN.'))
if self.cleaned_data.get('interface') and self.cleaned_data.get('vlan'):
raise ValidationError(_('Cannot assign both an interface and a VLAN.'))
# if this is an update we might not have interface or vlan in the form data
if self.cleaned_data.get('interface') or self.cleaned_data.get('vlan'):
self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')

View File

@ -1,10 +1,18 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
from dcim.models import Device, Region, Site
from ipam.models import RouteTarget, VLAN
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm
from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.fields import (
ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
)
from utilities.forms.utils import add_blank_choice
from virtualization.models import VirtualMachine
from vpn.choices import *
from vpn.constants import L2VPN_ASSIGNMENT_MODELS
from vpn.models import *
__all__ = (
@ -13,6 +21,8 @@ __all__ = (
'IPSecPolicyFilterForm',
'IPSecProfileFilterForm',
'IPSecProposalFilterForm',
'L2VPNFilterForm',
'L2VPNTerminationFilterForm',
'TunnelFilterForm',
'TunnelTerminationFilterForm',
)
@ -180,3 +190,90 @@ class IPSecProfileFilterForm(NetBoxModelFilterSetForm):
label=_('IPSec policy')
)
tag = TagFilterField(model)
class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = L2VPN
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('type', 'import_target_id', 'export_target_id')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
)
type = forms.ChoiceField(
label=_('Type'),
choices=add_blank_choice(L2VPNTypeChoices),
required=False
)
import_target_id = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
required=False,
label=_('Import targets')
)
export_target_id = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
required=False,
label=_('Export targets')
)
tag = TagFilterField(model)
class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
model = L2VPNTermination
fieldsets = (
(None, ('filter_id', 'l2vpn_id',)),
(_('Assigned Object'), (
'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id',
)),
)
l2vpn_id = DynamicModelChoiceField(
queryset=L2VPN.objects.all(),
required=False,
label=_('L2VPN')
)
assigned_object_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.filter(L2VPN_ASSIGNMENT_MODELS),
required=False,
label=_('Assigned Object Type'),
limit_choices_to=L2VPN_ASSIGNMENT_MODELS
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
null_option='None',
query_params={
'region_id': '$region_id'
},
label=_('Site')
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
null_option='None',
query_params={
'site_id': '$site_id'
},
label=_('Device')
)
vlan_id = DynamicModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False,
null_option='None',
query_params={
'site_id': '$site_id'
},
label=_('VLAN')
)
virtual_machine_id = DynamicModelMultipleChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
null_option='None',
query_params={
'site_id': '$site_id'
},
label=_('Virtual Machine')
)

View File

@ -1,11 +1,12 @@
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface
from ipam.models import IPAddress
from ipam.models import IPAddress, RouteTarget, VLAN
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
from utilities.forms.utils import add_blank_choice
from utilities.forms.widgets import HTMXSelect
from virtualization.models import VirtualMachine, VMInterface
@ -18,6 +19,8 @@ __all__ = (
'IPSecPolicyForm',
'IPSecProfileForm',
'IPSecProposalForm',
'L2VPNForm',
'L2VPNTerminationForm',
'TunnelCreateForm',
'TunnelForm',
'TunnelTerminationForm',
@ -355,3 +358,96 @@ class IPSecProfileForm(NetBoxModelForm):
fields = [
'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags',
]
#
# L2VPN
#
class L2VPNForm(TenancyForm, NetBoxModelForm):
slug = SlugField()
import_targets = DynamicModelMultipleChoiceField(
label=_('Import targets'),
queryset=RouteTarget.objects.all(),
required=False
)
export_targets = DynamicModelMultipleChoiceField(
label=_('Export targets'),
queryset=RouteTarget.objects.all(),
required=False
)
comments = CommentField()
fieldsets = (
(_('L2VPN'), ('name', 'slug', 'type', 'identifier', 'description', 'tags')),
(_('Route Targets'), ('import_targets', 'export_targets')),
(_('Tenancy'), ('tenant_group', 'tenant')),
)
class Meta:
model = L2VPN
fields = (
'name', 'slug', 'type', 'identifier', 'import_targets', 'export_targets', 'tenant', 'description',
'comments', 'tags'
)
class L2VPNTerminationForm(NetBoxModelForm):
l2vpn = DynamicModelChoiceField(
queryset=L2VPN.objects.all(),
required=True,
query_params={},
label=_('L2VPN'),
fetch_trigger='open'
)
vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
selector=True,
label=_('VLAN')
)
interface = DynamicModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.all(),
required=False,
selector=True
)
vminterface = DynamicModelChoiceField(
queryset=VMInterface.objects.all(),
required=False,
selector=True,
label=_('Interface')
)
class Meta:
model = L2VPNTermination
fields = ('l2vpn', )
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy()
if instance:
if type(instance.assigned_object) is Interface:
initial['interface'] = instance.assigned_object
elif type(instance.assigned_object) is VLAN:
initial['vlan'] = instance.assigned_object
elif type(instance.assigned_object) is VMInterface:
initial['vminterface'] = instance.assigned_object
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
def clean(self):
super().clean()
interface = self.cleaned_data.get('interface')
vminterface = self.cleaned_data.get('vminterface')
vlan = self.cleaned_data.get('vlan')
if not (interface or vminterface or vlan):
raise ValidationError(_('A termination must specify an interface or VLAN.'))
if len([x for x in (interface, vminterface, vlan) if x]) > 1:
raise ValidationError(_('A termination can only have one terminating object (an interface or VLAN).'))
self.instance.assigned_object = interface or vminterface or vlan

View File

@ -0,0 +1,30 @@
import graphene
from dcim.graphql.types import InterfaceType
from dcim.models import Interface
from ipam.graphql.types import VLANType
from ipam.models import VLAN
from virtualization.graphql.types import VMInterfaceType
from virtualization.models import VMInterface
__all__ = (
'L2VPNAssignmentType',
)
class L2VPNAssignmentType(graphene.Union):
class Meta:
types = (
InterfaceType,
VLANType,
VMInterfaceType,
)
@classmethod
def resolve_type(cls, instance, info):
if type(instance) is Interface:
return InterfaceType
if type(instance) is VLAN:
return VLANType
if type(instance) is VMInterface:
return VMInterfaceType

View File

@ -38,6 +38,18 @@ class VPNQuery(graphene.ObjectType):
def resolve_ipsec_proposal_list(root, info, **kwargs):
return gql_query_optimizer(models.IPSecProposal.objects.all(), info)
l2vpn = ObjectField(L2VPNType)
l2vpn_list = ObjectListField(L2VPNType)
def resolve_l2vpn_list(root, info, **kwargs):
return gql_query_optimizer(models.L2VPN.objects.all(), info)
l2vpn_termination = ObjectField(L2VPNTerminationType)
l2vpn_termination_list = ObjectListField(L2VPNTerminationType)
def resolve_l2vpn_termination_list(root, info, **kwargs):
return gql_query_optimizer(models.L2VPNTermination.objects.all(), info)
tunnel = ObjectField(TunnelType)
tunnel_list = ObjectListField(TunnelType)

View File

@ -1,4 +1,6 @@
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
import graphene
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
from vpn import filtersets, models
@ -8,6 +10,8 @@ __all__ = (
'IPSecPolicyType',
'IPSecProfileType',
'IPSecProposalType',
'L2VPNType',
'L2VPNTerminationType',
'TunnelTerminationType',
'TunnelType',
)
@ -67,3 +71,19 @@ class IPSecProfileType(OrganizationalObjectType):
model = models.IPSecProfile
fields = '__all__'
filterset_class = filtersets.IPSecProfileFilterSet
class L2VPNType(ContactsMixin, NetBoxObjectType):
class Meta:
model = models.L2VPN
fields = '__all__'
filtersets_class = filtersets.L2VPNFilterSet
class L2VPNTerminationType(NetBoxObjectType):
assigned_object = graphene.Field('vpn.graphql.gfk_mixins.L2VPNAssignmentType')
class Meta:
model = models.L2VPNTermination
exclude = ('assigned_object_type', 'assigned_object_id')
filtersets_class = filtersets.L2VPNTerminationFilterSet

View File

@ -0,0 +1,73 @@
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
import utilities.json
class Migration(migrations.Migration):
dependencies = [
('extras', '0099_cachedvalue_ordering'),
('contenttypes', '0002_remove_content_type_name'),
('tenancy', '0012_contactassignment_custom_fields'),
('ipam', '0068_move_l2vpn'),
('vpn', '0001_initial'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.CreateModel(
name='L2VPN',
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)),
('slug', models.SlugField(max_length=100, unique=True)),
('type', models.CharField(max_length=50)),
('identifier', models.BigIntegerField(blank=True, null=True)),
('export_targets', models.ManyToManyField(blank=True, related_name='exporting_l2vpns', to='ipam.routetarget')),
('import_targets', models.ManyToManyField(blank=True, related_name='importing_l2vpns', to='ipam.routetarget')),
('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='l2vpns', to='tenancy.tenant')),
],
options={
'verbose_name': 'L2VPN',
'verbose_name_plural': 'L2VPNs',
'ordering': ('name', 'identifier'),
},
),
migrations.CreateModel(
name='L2VPNTermination',
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)),
('assigned_object_id', models.PositiveBigIntegerField()),
('assigned_object_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'ipam'), ('model', 'vlan')), models.Q(('app_label', 'virtualization'), ('model', 'vminterface')), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
('l2vpn', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='vpn.l2vpn')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'verbose_name': 'L2VPN termination',
'verbose_name_plural': 'L2VPN terminations',
'ordering': ('l2vpn',),
},
),
],
# Tables have been renamed from ipam
database_operations=[],
),
migrations.AddConstraint(
model_name='l2vpntermination',
constraint=models.UniqueConstraint(
fields=('assigned_object_type', 'assigned_object_id'),
name='vpn_l2vpntermination_assigned_object'
),
),
]

View File

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

View File

@ -6,10 +6,10 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from core.models import ContentType
from ipam.choices import L2VPNTypeChoices
from ipam.constants import L2VPN_ASSIGNMENT_MODELS
from netbox.models import NetBoxModel, PrimaryModel
from netbox.models.features import ContactsMixin
from vpn.choices import L2VPNTypeChoices
from vpn.constants import L2VPN_ASSIGNMENT_MODELS
__all__ = (
'L2VPN',
@ -69,7 +69,7 @@ class L2VPN(ContactsMixin, PrimaryModel):
return f'{self.name}'
def get_absolute_url(self):
return reverse('ipam:l2vpn', args=[self.pk])
return reverse('vpn:l2vpn', args=[self.pk])
@cached_property
def can_add_termination(self):
@ -81,7 +81,7 @@ class L2VPN(ContactsMixin, PrimaryModel):
class L2VPNTermination(NetBoxModel):
l2vpn = models.ForeignKey(
to='ipam.L2VPN',
to='vpn.L2VPN',
on_delete=models.CASCADE,
related_name='terminations'
)
@ -99,7 +99,7 @@ class L2VPNTermination(NetBoxModel):
clone_fields = ('l2vpn',)
prerequisite_models = (
'ipam.L2VPN',
'vpn.L2VPN',
)
class Meta:
@ -107,7 +107,7 @@ class L2VPNTermination(NetBoxModel):
constraints = (
models.UniqueConstraint(
fields=('assigned_object_type', 'assigned_object_id'),
name='ipam_l2vpntermination_assigned_object'
name='vpn_l2vpntermination_assigned_object'
),
)
verbose_name = _('L2VPN termination')
@ -119,7 +119,7 @@ class L2VPNTermination(NetBoxModel):
return super().__str__()
def get_absolute_url(self):
return reverse('ipam:l2vpntermination', args=[self.pk])
return reverse('vpn:l2vpntermination', args=[self.pk])
def clean(self):
# Only check is assigned_object is set. Required otherwise we have an Integrity Error thrown.

View File

@ -63,3 +63,15 @@ class IPSecProfileIndex(SearchIndex):
('comments', 5000),
)
display_attrs = ('description',)
@register_search
class L2VPNIndex(SearchIndex):
model = models.L2VPN
fields = (
('name', 100),
('slug', 110),
('description', 500),
('comments', 5000),
)
display_attrs = ('type', 'identifier', 'tenant', 'description')

View File

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

View File

@ -1,8 +1,6 @@
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 *
@ -12,88 +10,9 @@ __all__ = (
'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'),

View File

@ -1,9 +1,9 @@
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from ipam.models import L2VPN, L2VPNTermination
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
from vpn.models import L2VPN, L2VPNTermination
__all__ = (
'L2VPNTable',
@ -37,7 +37,7 @@ class L2VPNTable(TenancyColumnsMixin, NetBoxTable):
verbose_name=_('Comments'),
)
tags = columns.TagColumn(
url_name='ipam:l2vpn_list'
url_name='vpn:l2vpn_list'
)
class Meta(NetBoxTable.Meta):

View File

@ -0,0 +1,87 @@
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from django_tables2.utils import Accessor
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
from vpn.models import *
__all__ = (
'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', 'tenant', 'terminations_count')
class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable):
tunnel = tables.Column(
verbose_name=_('Tunnel'),
linkify=True
)
role = columns.ChoiceFieldColumn(
verbose_name=_('Role')
)
interface_parent = tables.Column(
accessor='interface__parent_object',
linkify=True,
orderable=False,
verbose_name=_('Host')
)
interface = tables.Column(
verbose_name=_('Interface'),
linkify=True
)
ip_addresses = tables.ManyToManyColumn(
accessor=tables.A('interface__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', 'interface_parent', 'interface', 'ip_addresses', 'outside_ip', 'tags',
'created', 'last_updated',
)
default_columns = ('pk', 'id', 'tunnel', 'role', 'interface_parent', 'interface', 'ip_addresses', 'outside_ip')

View File

@ -2,6 +2,7 @@ from django.urls import reverse
from dcim.choices import InterfaceTypeChoices
from dcim.models import Interface
from ipam.models import VLAN
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
from vpn.choices import *
from vpn.models import *
@ -471,3 +472,96 @@ class IPSecProfileTest(APIViewTestCases.APIViewTestCase):
'ipsec_policy': ipsec_policies[1].pk,
'description': 'New description',
}
class L2VPNTest(APIViewTestCases.APIViewTestCase):
model = L2VPN
brief_fields = ['display', 'id', 'identifier', 'name', 'slug', 'type', 'url']
create_data = [
{
'name': 'L2VPN 4',
'slug': 'l2vpn-4',
'type': 'vxlan',
'identifier': 33343344
},
{
'name': 'L2VPN 5',
'slug': 'l2vpn-5',
'type': 'vxlan',
'identifier': 33343345
},
{
'name': 'L2VPN 6',
'slug': 'l2vpn-6',
'type': 'vpws',
'identifier': 33343346
},
]
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
l2vpns = (
L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
)
L2VPN.objects.bulk_create(l2vpns)
class L2VPNTerminationTest(APIViewTestCases.APIViewTestCase):
model = L2VPNTermination
brief_fields = ['display', 'id', 'l2vpn', 'url']
@classmethod
def setUpTestData(cls):
vlans = (
VLAN(name='VLAN 1', vid=651),
VLAN(name='VLAN 2', vid=652),
VLAN(name='VLAN 3', vid=653),
VLAN(name='VLAN 4', vid=654),
VLAN(name='VLAN 5', vid=655),
VLAN(name='VLAN 6', vid=656),
VLAN(name='VLAN 7', vid=657)
)
VLAN.objects.bulk_create(vlans)
l2vpns = (
L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
)
L2VPN.objects.bulk_create(l2vpns)
l2vpnterminations = (
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
)
L2VPNTermination.objects.bulk_create(l2vpnterminations)
cls.create_data = [
{
'l2vpn': l2vpns[0].pk,
'assigned_object_type': 'ipam.vlan',
'assigned_object_id': vlans[3].pk,
},
{
'l2vpn': l2vpns[0].pk,
'assigned_object_type': 'ipam.vlan',
'assigned_object_id': vlans[4].pk,
},
{
'l2vpn': l2vpns[0].pk,
'assigned_object_type': 'ipam.vlan',
'assigned_object_id': vlans[5].pk,
},
]
cls.bulk_update_data = {
'l2vpn': l2vpns[2].pk
}

View File

@ -1,13 +1,14 @@
from django.contrib.contenttypes.models import ContentType
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 dcim.models import Device, Interface, Site
from ipam.models import IPAddress, VLAN, RouteTarget
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
from virtualization.models import VirtualMachine, 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):
@ -590,3 +591,163 @@ class IPSecProfileTestCase(TestCase, ChangeLoggedFilterSetTests):
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)
class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = L2VPN.objects.all()
filterset = L2VPNFilterSet
@classmethod
def setUpTestData(cls):
route_targets = (
RouteTarget(name='1:1'),
RouteTarget(name='1:2'),
RouteTarget(name='1:3'),
RouteTarget(name='2:1'),
RouteTarget(name='2:2'),
RouteTarget(name='2:3'),
)
RouteTarget.objects.bulk_create(route_targets)
l2vpns = (
L2VPN(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=65001),
L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VPWS, identifier=65002),
L2VPN(name='L2VPN 3', slug='l2vpn-3', type=L2VPNTypeChoices.TYPE_VPLS),
)
L2VPN.objects.bulk_create(l2vpns)
l2vpns[0].import_targets.add(route_targets[0])
l2vpns[1].import_targets.add(route_targets[1])
l2vpns[2].import_targets.add(route_targets[2])
l2vpns[0].export_targets.add(route_targets[3])
l2vpns[1].export_targets.add(route_targets[4])
l2vpns[2].export_targets.add(route_targets[5])
def test_name(self):
params = {'name': ['L2VPN 1', 'L2VPN 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_slug(self):
params = {'slug': ['l2vpn-1', 'l2vpn-2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_identifier(self):
params = {'identifier': ['65001', '65002']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type(self):
params = {'type': [L2VPNTypeChoices.TYPE_VXLAN, L2VPNTypeChoices.TYPE_VPWS]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_import_targets(self):
route_targets = RouteTarget.objects.filter(name__in=['1:1', '1:2'])
params = {'import_target_id': [route_targets[0].pk, route_targets[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'import_target': [route_targets[0].name, route_targets[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_export_targets(self):
route_targets = RouteTarget.objects.filter(name__in=['2:1', '2:2'])
params = {'export_target_id': [route_targets[0].pk, route_targets[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'export_target': [route_targets[0].name, route_targets[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = L2VPNTermination.objects.all()
filterset = L2VPNTerminationFilterSet
@classmethod
def setUpTestData(cls):
device = create_test_device('Device 1')
interfaces = (
Interface(name='Interface 1', device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(name='Interface 2', device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(name='Interface 3', device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
)
Interface.objects.bulk_create(interfaces)
vm = create_test_virtualmachine('Virtual Machine 1')
vminterfaces = (
VMInterface(name='Interface 1', virtual_machine=vm),
VMInterface(name='Interface 2', virtual_machine=vm),
VMInterface(name='Interface 3', virtual_machine=vm),
)
VMInterface.objects.bulk_create(vminterfaces)
vlans = (
VLAN(name='VLAN 1', vid=101),
VLAN(name='VLAN 2', vid=102),
VLAN(name='VLAN 3', vid=103),
)
VLAN.objects.bulk_create(vlans)
l2vpns = (
L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=65001),
L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=65002),
L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD,
)
L2VPN.objects.bulk_create(l2vpns)
l2vpnterminations = (
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
L2VPNTermination(l2vpn=l2vpns[1], assigned_object=vlans[1]),
L2VPNTermination(l2vpn=l2vpns[2], assigned_object=vlans[2]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=interfaces[0]),
L2VPNTermination(l2vpn=l2vpns[1], assigned_object=interfaces[1]),
L2VPNTermination(l2vpn=l2vpns[2], assigned_object=interfaces[2]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vminterfaces[0]),
L2VPNTermination(l2vpn=l2vpns[1], assigned_object=vminterfaces[1]),
L2VPNTermination(l2vpn=l2vpns[2], assigned_object=vminterfaces[2]),
)
L2VPNTermination.objects.bulk_create(l2vpnterminations)
def test_l2vpn(self):
l2vpns = L2VPN.objects.all()[:2]
params = {'l2vpn_id': [l2vpns[0].pk, l2vpns[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'l2vpn': [l2vpns[0].slug, l2vpns[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_content_type(self):
params = {'assigned_object_type_id': ContentType.objects.get(model='vlan').pk}
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)
def test_vminterface(self):
vminterfaces = VMInterface.objects.all()[:2]
params = {'vminterface_id': [vminterfaces[0].pk, vminterfaces[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_vlan(self):
vlans = VLAN.objects.all()[:2]
params = {'vlan_id': [vlans[0].pk, vlans[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'vlan': ['VLAN 1', 'VLAN 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site(self):
site = Site.objects.all().first()
params = {'site_id': [site.pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'site': ['site-1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_device(self):
device = Device.objects.all().first()
params = {'device_id': [device.pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'device': ['Device 1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_virtual_machine(self):
virtual_machine = VirtualMachine.objects.all().first()
params = {'virtual_machine_id': [virtual_machine.pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'virtual_machine': ['Virtual Machine 1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)

View File

@ -0,0 +1,79 @@
from django.core.exceptions import ValidationError
from django.test import TestCase
from dcim.models import Interface, Device, DeviceRole, DeviceType, Manufacturer, Site
from ipam.models import VLAN
from vpn.models import *
class TestL2VPNTermination(TestCase):
@classmethod
def setUpTestData(cls):
site = Site.objects.create(name='Site 1')
manufacturer = Manufacturer.objects.create(name='Manufacturer 1')
device_type = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
role = DeviceRole.objects.create(name='Switch')
device = Device.objects.create(
name='Device 1',
site=site,
device_type=device_type,
role=role,
status='active'
)
interfaces = (
Interface(name='Interface 1', device=device, type='1000baset'),
Interface(name='Interface 2', device=device, type='1000baset'),
Interface(name='Interface 3', device=device, type='1000baset'),
Interface(name='Interface 4', device=device, type='1000baset'),
Interface(name='Interface 5', device=device, type='1000baset'),
)
Interface.objects.bulk_create(interfaces)
vlans = (
VLAN(name='VLAN 1', vid=651),
VLAN(name='VLAN 2', vid=652),
VLAN(name='VLAN 3', vid=653),
VLAN(name='VLAN 4', vid=654),
VLAN(name='VLAN 5', vid=655),
VLAN(name='VLAN 6', vid=656),
VLAN(name='VLAN 7', vid=657)
)
VLAN.objects.bulk_create(vlans)
l2vpns = (
L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
)
L2VPN.objects.bulk_create(l2vpns)
l2vpnterminations = (
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
)
L2VPNTermination.objects.bulk_create(l2vpnterminations)
def test_duplicate_interface_terminations(self):
device = Device.objects.first()
interface = Interface.objects.filter(device=device).first()
l2vpn = L2VPN.objects.first()
L2VPNTermination.objects.create(l2vpn=l2vpn, assigned_object=interface)
duplicate = L2VPNTermination(l2vpn=l2vpn, assigned_object=interface)
self.assertRaises(ValidationError, duplicate.clean)
def test_duplicate_vlan_terminations(self):
vlan = Interface.objects.first()
l2vpn = L2VPN.objects.first()
L2VPNTermination.objects.create(l2vpn=l2vpn, assigned_object=vlan)
duplicate = L2VPNTermination(l2vpn=l2vpn, assigned_object=vlan)
self.assertRaises(ValidationError, duplicate.clean)

View File

@ -1,8 +1,9 @@
from dcim.choices import InterfaceTypeChoices
from dcim.models import Interface
from ipam.models import RouteTarget, VLAN
from utilities.testing import ViewTestCases, create_tags, create_test_device
from vpn.choices import *
from vpn.models import *
from utilities.testing import ViewTestCases, create_tags, create_test_device
class TunnelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@ -506,3 +507,142 @@ class IPSecProfileTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'ike_policy': ike_policies[1].pk,
'ipsec_policy': ipsec_policies[1].pk,
}
class L2VPNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = L2VPN
@classmethod
def setUpTestData(cls):
rts = (
RouteTarget(name='64534:123'),
RouteTarget(name='64534:321')
)
RouteTarget.objects.bulk_create(rts)
l2vpns = (
L2VPN(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650001'),
L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650002'),
L2VPN(name='L2VPN 3', slug='l2vpn-3', type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650003')
)
L2VPN.objects.bulk_create(l2vpns)
cls.csv_data = (
'name,slug,type,identifier',
'L2VPN 5,l2vpn-5,vxlan,456',
'L2VPN 6,l2vpn-6,vxlan,444',
)
cls.csv_update_data = (
'id,name,description',
f'{l2vpns[0].pk},L2VPN 7,New description 7',
f'{l2vpns[1].pk},L2VPN 8,New description 8',
)
cls.bulk_edit_data = {
'description': 'New Description',
}
cls.form_data = {
'name': 'L2VPN 8',
'slug': 'l2vpn-8',
'type': L2VPNTypeChoices.TYPE_VXLAN,
'identifier': 123,
'description': 'Description',
'import_targets': [rts[0].pk],
'export_targets': [rts[1].pk]
}
class L2VPNTerminationTestCase(
ViewTestCases.GetObjectViewTestCase,
ViewTestCases.GetObjectChangelogViewTestCase,
ViewTestCases.CreateObjectViewTestCase,
ViewTestCases.EditObjectViewTestCase,
ViewTestCases.DeleteObjectViewTestCase,
ViewTestCases.ListObjectsViewTestCase,
ViewTestCases.BulkImportObjectsViewTestCase,
ViewTestCases.BulkDeleteObjectsViewTestCase,
):
model = L2VPNTermination
@classmethod
def setUpTestData(cls):
device = create_test_device('Device 1')
interface = Interface.objects.create(name='Interface 1', device=device, type='1000baset')
l2vpns = (
L2VPN(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=650001),
L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=650002),
)
L2VPN.objects.bulk_create(l2vpns)
vlans = (
VLAN(name='Vlan 1', vid=1001),
VLAN(name='Vlan 2', vid=1002),
VLAN(name='Vlan 3', vid=1003),
VLAN(name='Vlan 4', vid=1004),
VLAN(name='Vlan 5', vid=1005),
VLAN(name='Vlan 6', vid=1006)
)
VLAN.objects.bulk_create(vlans)
terminations = (
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
)
L2VPNTermination.objects.bulk_create(terminations)
cls.form_data = {
'l2vpn': l2vpns[0].pk,
'device': device.pk,
'interface': interface.pk,
}
cls.csv_data = (
"l2vpn,vlan",
"L2VPN 1,Vlan 4",
"L2VPN 1,Vlan 5",
"L2VPN 1,Vlan 6",
)
cls.csv_update_data = (
f"id,l2vpn",
f"{terminations[0].pk},{l2vpns[0].name}",
f"{terminations[1].pk},{l2vpns[0].name}",
f"{terminations[2].pk},{l2vpns[0].name}",
)
cls.bulk_edit_data = {}
# TODO: Fix L2VPNTerminationImportForm validation to support bulk updates
def test_bulk_update_objects_with_permission(self):
pass
#
# Custom assertions
#
# TODO: Remove this
def assertInstanceEqual(self, instance, data, exclude=None, api=False):
"""
Override parent
"""
if exclude is None:
exclude = []
fields = [k for k in data.keys() if k not in exclude]
model_dict = self.model_to_dict(instance, fields=fields, api=api)
# Omit any dictionary keys which are not instance attributes or have been excluded
relevant_data = {
k: v for k, v in data.items() if hasattr(instance, k) and k not in exclude
}
# Handle relations on the model
for k, v in model_dict.items():
if isinstance(v, object) and hasattr(v, 'first'):
model_dict[k] = v.first().pk
self.assertDictEqual(model_dict, relevant_data)

View File

@ -62,4 +62,20 @@ urlpatterns = [
path('ipsec-profiles/delete/', views.IPSecProfileBulkDeleteView.as_view(), name='ipsecprofile_bulk_delete'),
path('ipsec-profiles/<int:pk>/', include(get_model_urls('vpn', 'ipsecprofile'))),
# L2VPN
path('l2vpns/', views.L2VPNListView.as_view(), name='l2vpn_list'),
path('l2vpns/add/', views.L2VPNEditView.as_view(), name='l2vpn_add'),
path('l2vpns/import/', views.L2VPNBulkImportView.as_view(), name='l2vpn_import'),
path('l2vpns/edit/', views.L2VPNBulkEditView.as_view(), name='l2vpn_bulk_edit'),
path('l2vpns/delete/', views.L2VPNBulkDeleteView.as_view(), name='l2vpn_bulk_delete'),
path('l2vpns/<int:pk>/', include(get_model_urls('vpn', 'l2vpn'))),
# L2VPN terminations
path('l2vpn-terminations/', views.L2VPNTerminationListView.as_view(), name='l2vpntermination_list'),
path('l2vpn-terminations/add/', views.L2VPNTerminationEditView.as_view(), name='l2vpntermination_add'),
path('l2vpn-terminations/import/', views.L2VPNTerminationBulkImportView.as_view(), name='l2vpntermination_import'),
path('l2vpn-terminations/edit/', views.L2VPNTerminationBulkEditView.as_view(), name='l2vpntermination_bulk_edit'),
path('l2vpn-terminations/delete/', views.L2VPNTerminationBulkDeleteView.as_view(), name='l2vpntermination_bulk_delete'),
path('l2vpn-terminations/<int:pk>/', include(get_model_urls('vpn', 'l2vpntermination'))),
]

View File

@ -1,4 +1,6 @@
from ipam.tables import RouteTargetTable
from netbox.views import generic
from tenancy.views import ObjectContactsView
from utilities.utils import count_related
from utilities.views import register_model_view
from . import filtersets, forms, tables
@ -332,3 +334,112 @@ class IPSecProfileBulkDeleteView(generic.BulkDeleteView):
queryset = IPSecProfile.objects.all()
filterset = filtersets.IPSecProfileFilterSet
table = tables.IPSecProfileTable
# L2VPN
class L2VPNListView(generic.ObjectListView):
queryset = L2VPN.objects.all()
table = tables.L2VPNTable
filterset = filtersets.L2VPNFilterSet
filterset_form = forms.L2VPNFilterForm
@register_model_view(L2VPN)
class L2VPNView(generic.ObjectView):
queryset = L2VPN.objects.all()
def get_extra_context(self, request, instance):
import_targets_table = RouteTargetTable(
instance.import_targets.prefetch_related('tenant'),
orderable=False
)
export_targets_table = RouteTargetTable(
instance.export_targets.prefetch_related('tenant'),
orderable=False
)
return {
'import_targets_table': import_targets_table,
'export_targets_table': export_targets_table,
}
@register_model_view(L2VPN, 'edit')
class L2VPNEditView(generic.ObjectEditView):
queryset = L2VPN.objects.all()
form = forms.L2VPNForm
@register_model_view(L2VPN, 'delete')
class L2VPNDeleteView(generic.ObjectDeleteView):
queryset = L2VPN.objects.all()
class L2VPNBulkImportView(generic.BulkImportView):
queryset = L2VPN.objects.all()
model_form = forms.L2VPNImportForm
class L2VPNBulkEditView(generic.BulkEditView):
queryset = L2VPN.objects.all()
filterset = filtersets.L2VPNFilterSet
table = tables.L2VPNTable
form = forms.L2VPNBulkEditForm
class L2VPNBulkDeleteView(generic.BulkDeleteView):
queryset = L2VPN.objects.all()
filterset = filtersets.L2VPNFilterSet
table = tables.L2VPNTable
@register_model_view(L2VPN, 'contacts')
class L2VPNContactsView(ObjectContactsView):
queryset = L2VPN.objects.all()
#
# L2VPN terminations
#
class L2VPNTerminationListView(generic.ObjectListView):
queryset = L2VPNTermination.objects.all()
table = tables.L2VPNTerminationTable
filterset = filtersets.L2VPNTerminationFilterSet
filterset_form = forms.L2VPNTerminationFilterForm
@register_model_view(L2VPNTermination)
class L2VPNTerminationView(generic.ObjectView):
queryset = L2VPNTermination.objects.all()
@register_model_view(L2VPNTermination, 'edit')
class L2VPNTerminationEditView(generic.ObjectEditView):
queryset = L2VPNTermination.objects.all()
form = forms.L2VPNTerminationForm
template_name = 'vpn/l2vpntermination_edit.html'
@register_model_view(L2VPNTermination, 'delete')
class L2VPNTerminationDeleteView(generic.ObjectDeleteView):
queryset = L2VPNTermination.objects.all()
class L2VPNTerminationBulkImportView(generic.BulkImportView):
queryset = L2VPNTermination.objects.all()
model_form = forms.L2VPNTerminationImportForm
class L2VPNTerminationBulkEditView(generic.BulkEditView):
queryset = L2VPNTermination.objects.all()
filterset = filtersets.L2VPNTerminationFilterSet
table = tables.L2VPNTerminationTable
form = forms.L2VPNTerminationBulkEditForm
class L2VPNTerminationBulkDeleteView(generic.BulkDeleteView):
queryset = L2VPNTermination.objects.all()
filterset = filtersets.L2VPNTerminationFilterSet
table = tables.L2VPNTerminationTable

View File

@ -1,18 +1,18 @@
bleach==6.1.0
Django==4.2.7
django-cors-headers==4.3.0
django-cors-headers==4.3.1
django-debug-toolbar==4.2.0
django-filter==23.3
django-filter==23.4
django-graphiql-debug-toolbar==0.2.0
django-mptt==0.14.0
django-pglocks==1.0.4
django-prometheus==2.3.1
django-redis==5.4.0
django-rich==1.8.0
django-rq==2.8.1
django-rq==2.9.0
django-tables2==2.6.0
django-taggit==4.0.0
django-timezone-field==6.0.1
django-timezone-field==6.1.0
djangorestframework==3.14.0
drf-spectacular==0.26.5
drf-spectacular-sidecar==2023.10.1
@ -21,15 +21,15 @@ graphene-django==3.0.0
gunicorn==21.2.0
Jinja2==3.1.2
Markdown==3.3.7
mkdocs-material==9.4.8
mkdocstrings[python-legacy]==0.23.0
mkdocs-material==9.4.14
mkdocstrings[python-legacy]==0.24.0
netaddr==0.9.0
Pillow==10.1.0
psycopg[binary,pool]==3.1.12
psycopg[binary,pool]==3.1.13
PyYAML==6.0.1
requests==2.31.0
social-auth-app-django==5.4.0
social-auth-core[openidconnect]==4.5.0
social-auth-core[openidconnect]==4.5.1
svgwrite==1.4.3
tablib==3.5.0
tzdata==2023.3