mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-18 05:28:16 -06:00
Merge branch 'feature' into 14132-event-refactor-2
This commit is contained in:
commit
cdea1130eb
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.6.5
|
placeholder: v3.6.6
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.6.5
|
placeholder: v3.6.6
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
13
docs/models/virtualization/virtualdisk.md
Normal file
13
docs/models/virtualization/virtualdisk.md
Normal 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.
|
@ -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/).
|
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
|
### 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.)
|
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.)
|
||||||
|
@ -1,6 +1,29 @@
|
|||||||
# NetBox v3.6
|
# 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
92
docs/release-notes/version-3.7.md
Normal file
92
docs/release-notes/version-3.7.md
Normal 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
|
@ -116,6 +116,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
# Validate data backend type
|
# Validate data backend type
|
||||||
if self.type and self.type not in registry['data_backends']:
|
if self.type and self.type not in registry['data_backends']:
|
||||||
|
@ -2,6 +2,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _
|
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.file_path = os.path.basename(self.data_path)
|
||||||
self.data_file.write_to_disk(self.full_path, overwrite=True)
|
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):
|
def delete(self, *args, **kwargs):
|
||||||
# Delete file from disk
|
# Delete file from disk
|
||||||
try:
|
try:
|
||||||
|
@ -2,8 +2,8 @@ import decimal
|
|||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from drf_spectacular.utils import extend_schema_field
|
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from timezone_field.rest_framework import TimeZoneSerializerField
|
from timezone_field.rest_framework import TimeZoneSerializerField
|
||||||
|
|
||||||
@ -12,8 +12,7 @@ from dcim.constants import *
|
|||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.api.nested_serializers import NestedConfigTemplateSerializer
|
from extras.api.nested_serializers import NestedConfigTemplateSerializer
|
||||||
from ipam.api.nested_serializers import (
|
from ipam.api.nested_serializers import (
|
||||||
NestedASNSerializer, NestedIPAddressSerializer, NestedL2VPNTerminationSerializer, NestedVLANSerializer,
|
NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer,
|
||||||
NestedVRFSerializer,
|
|
||||||
)
|
)
|
||||||
from ipam.models import ASN, VLAN
|
from ipam.models import ASN, VLAN
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
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 users.api.nested_serializers import NestedUserSerializer
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from virtualization.api.nested_serializers import NestedClusterSerializer
|
from virtualization.api.nested_serializers import NestedClusterSerializer
|
||||||
|
from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
|
||||||
from wireless.api.nested_serializers import NestedWirelessLANSerializer, NestedWirelessLinkSerializer
|
from wireless.api.nested_serializers import NestedWirelessLANSerializer, NestedWirelessLinkSerializer
|
||||||
from wireless.choices import *
|
from wireless.choices import *
|
||||||
from wireless.models import WirelessLAN
|
from wireless.models import WirelessLAN
|
||||||
|
@ -5,7 +5,7 @@ from django.utils.translation import gettext as _
|
|||||||
from extras.filtersets import LocalConfigContextFilterSet
|
from extras.filtersets import LocalConfigContextFilterSet
|
||||||
from extras.models import ConfigTemplate
|
from extras.models import ConfigTemplate
|
||||||
from ipam.filtersets import PrimaryIPFilterSet
|
from ipam.filtersets import PrimaryIPFilterSet
|
||||||
from ipam.models import ASN, L2VPN, IPAddress, VRF
|
from ipam.models import ASN, IPAddress, VRF
|
||||||
from netbox.filtersets import (
|
from netbox.filtersets import (
|
||||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
||||||
)
|
)
|
||||||
@ -17,6 +17,7 @@ from utilities.filters import (
|
|||||||
TreeNodeMultipleChoiceFilter,
|
TreeNodeMultipleChoiceFilter,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster
|
||||||
|
from vpn.models import L2VPN
|
||||||
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
|
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .constants import *
|
from .constants import *
|
||||||
|
@ -7,12 +7,13 @@ from dcim.constants import *
|
|||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.forms import LocalConfigContextFilterForm
|
from extras.forms import LocalConfigContextFilterForm
|
||||||
from extras.models import ConfigTemplate
|
from extras.models import ConfigTemplate
|
||||||
from ipam.models import ASN, L2VPN, VRF
|
from ipam.models import ASN, VRF
|
||||||
from netbox.forms import NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||||
from utilities.forms.widgets import APISelectMultiple, NumberWithOptions
|
from utilities.forms.widgets import APISelectMultiple, NumberWithOptions
|
||||||
|
from vpn.models import L2VPN
|
||||||
from wireless.choices import *
|
from wireless.choices import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
@ -730,7 +730,7 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
related_query_name='interface'
|
related_query_name='interface'
|
||||||
)
|
)
|
||||||
l2vpn_terminations = GenericRelation(
|
l2vpn_terminations = GenericRelation(
|
||||||
to='ipam.L2VPNTermination',
|
to='vpn.L2VPNTermination',
|
||||||
content_type_field='assigned_object_type',
|
content_type_field='assigned_object_type',
|
||||||
object_id_field='assigned_object_id',
|
object_id_field='assigned_object_id',
|
||||||
related_query_name='interface',
|
related_query_name='interface',
|
||||||
|
@ -316,8 +316,8 @@ INTERFACE_BUTTONS = """
|
|||||||
{% if perms.dcim.add_interface %}
|
{% 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>
|
<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 %}
|
{% endif %}
|
||||||
{% if perms.ipam.add_l2vpntermination %}
|
{% if perms.vpn.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>
|
<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 %}
|
{% endif %}
|
||||||
{% if perms.ipam.add_fhrpgroupassignment %}
|
{% 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>
|
<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>
|
||||||
|
@ -294,7 +294,7 @@ class ReportViewSet(ViewSet):
|
|||||||
|
|
||||||
# Retrieve and run the Report. This will create a new Job.
|
# Retrieve and run the Report. This will create a new Job.
|
||||||
module, report_cls = self._get_report(pk)
|
module, report_cls = self._get_report(pk)
|
||||||
report = report_cls()
|
report = report_cls
|
||||||
input_serializer = serializers.ReportInputSerializer(
|
input_serializer = serializers.ReportInputSerializer(
|
||||||
data=request.data,
|
data=request.data,
|
||||||
context={'report': report}
|
context={'report': report}
|
||||||
|
@ -92,7 +92,7 @@ def process_event_rules(event_rules, model_name, event, data, username, snapshot
|
|||||||
"event": event,
|
"event": event,
|
||||||
"data": data,
|
"data": data,
|
||||||
"snapshots": snapshots,
|
"snapshots": snapshots,
|
||||||
"timestamp": str(timezone.now()),
|
"timestamp": timezone.now().isoformat(),
|
||||||
"username": username,
|
"username": username,
|
||||||
"retry": get_rq_retry()
|
"retry": get_rq_retry()
|
||||||
}
|
}
|
||||||
|
@ -153,8 +153,7 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(name__icontains=value) |
|
Q(name__icontains=value) |
|
||||||
Q(description__icontains=value) |
|
Q(description__icontains=value)
|
||||||
Q(extra_choices__contains=value)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_by_choice(self, queryset, name, value):
|
def filter_by_choice(self, queryset, name, value):
|
||||||
|
@ -1138,7 +1138,7 @@ class ReportJobsView(ContentTypePermissionRequiredMixin, View):
|
|||||||
jobs = Job.objects.filter(
|
jobs = Job.objects.filter(
|
||||||
object_type=object_type,
|
object_type=object_type,
|
||||||
object_id=module.pk,
|
object_id=module.pk,
|
||||||
name=report.name
|
name=report.class_name
|
||||||
)
|
)
|
||||||
|
|
||||||
jobs_table = JobTable(
|
jobs_table = JobTable(
|
||||||
|
@ -2,7 +2,6 @@ from drf_spectacular.utils import extend_schema_serializer
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from ipam import models
|
from ipam import models
|
||||||
from ipam.models.l2vpn import L2VPNTermination, L2VPN
|
|
||||||
from netbox.api.serializers import WritableNestedSerializer
|
from netbox.api.serializers import WritableNestedSerializer
|
||||||
from .field_serializers import IPAddressField
|
from .field_serializers import IPAddressField
|
||||||
|
|
||||||
@ -14,8 +13,6 @@ __all__ = [
|
|||||||
'NestedFHRPGroupAssignmentSerializer',
|
'NestedFHRPGroupAssignmentSerializer',
|
||||||
'NestedIPAddressSerializer',
|
'NestedIPAddressSerializer',
|
||||||
'NestedIPRangeSerializer',
|
'NestedIPRangeSerializer',
|
||||||
'NestedL2VPNSerializer',
|
|
||||||
'NestedL2VPNTerminationSerializer',
|
|
||||||
'NestedPrefixSerializer',
|
'NestedPrefixSerializer',
|
||||||
'NestedRIRSerializer',
|
'NestedRIRSerializer',
|
||||||
'NestedRoleSerializer',
|
'NestedRoleSerializer',
|
||||||
@ -223,28 +220,3 @@ class NestedServiceSerializer(WritableNestedSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = models.Service
|
model = models.Service
|
||||||
fields = ['id', 'url', 'display', 'name', 'protocol', 'ports']
|
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'
|
|
||||||
]
|
|
||||||
|
@ -12,8 +12,9 @@ from netbox.constants import NESTED_SERIALIZER_PREFIX
|
|||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
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 .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',
|
'id', 'url', 'display', 'device', 'virtual_machine', 'name', 'ports', 'protocol', 'ipaddresses',
|
||||||
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
'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
|
|
||||||
|
@ -23,8 +23,6 @@ router.register('vlan-groups', views.VLANGroupViewSet)
|
|||||||
router.register('vlans', views.VLANViewSet)
|
router.register('vlans', views.VLANViewSet)
|
||||||
router.register('service-templates', views.ServiceTemplateViewSet)
|
router.register('service-templates', views.ServiceTemplateViewSet)
|
||||||
router.register('services', views.ServiceViewSet)
|
router.register('services', views.ServiceViewSet)
|
||||||
router.register('l2vpns', views.L2VPNViewSet)
|
|
||||||
router.register('l2vpn-terminations', views.L2VPNTerminationViewSet)
|
|
||||||
|
|
||||||
app_name = 'ipam-api'
|
app_name = 'ipam-api'
|
||||||
|
|
||||||
|
@ -14,7 +14,6 @@ from circuits.models import Provider
|
|||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from ipam import filtersets
|
from ipam import filtersets
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
from ipam.models import L2VPN, L2VPNTermination
|
|
||||||
from ipam.utils import get_next_available_prefix
|
from ipam.utils import get_next_available_prefix
|
||||||
from netbox.api.viewsets import NetBoxModelViewSet
|
from netbox.api.viewsets import NetBoxModelViewSet
|
||||||
from netbox.api.viewsets.mixins import ObjectValidationMixin
|
from netbox.api.viewsets.mixins import ObjectValidationMixin
|
||||||
@ -178,18 +177,6 @@ class ServiceViewSet(NetBoxModelViewSet):
|
|||||||
filterset_class = filtersets.ServiceFilterSet
|
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
|
# Views
|
||||||
#
|
#
|
||||||
|
@ -172,52 +172,3 @@ class ServiceProtocolChoices(ChoiceSet):
|
|||||||
(PROTOCOL_UDP, 'UDP'),
|
(PROTOCOL_UDP, 'UDP'),
|
||||||
(PROTOCOL_SCTP, 'SCTP'),
|
(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
|
|
||||||
)
|
|
||||||
|
@ -86,9 +86,3 @@ VLANGROUP_SCOPE_TYPES = (
|
|||||||
# 16-bit port number
|
# 16-bit port number
|
||||||
SERVICE_PORT_MIN = 1
|
SERVICE_PORT_MIN = 1
|
||||||
SERVICE_PORT_MAX = 65535
|
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')
|
|
||||||
)
|
|
||||||
|
@ -4,8 +4,8 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from drf_spectacular.utils import extend_schema_field
|
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
|
|
||||||
from dcim.models import Device, Interface, Region, Site, SiteGroup
|
from dcim.models import Device, Interface, Region, Site, SiteGroup
|
||||||
@ -15,6 +15,7 @@ from utilities.filters import (
|
|||||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine, VMInterface
|
from virtualization.models import VirtualMachine, VMInterface
|
||||||
|
from vpn.models import L2VPN
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
@ -26,8 +27,6 @@ __all__ = (
|
|||||||
'FHRPGroupFilterSet',
|
'FHRPGroupFilterSet',
|
||||||
'IPAddressFilterSet',
|
'IPAddressFilterSet',
|
||||||
'IPRangeFilterSet',
|
'IPRangeFilterSet',
|
||||||
'L2VPNFilterSet',
|
|
||||||
'L2VPNTerminationFilterSet',
|
|
||||||
'PrefixFilterSet',
|
'PrefixFilterSet',
|
||||||
'PrimaryIPFilterSet',
|
'PrimaryIPFilterSet',
|
||||||
'RIRFilterSet',
|
'RIRFilterSet',
|
||||||
@ -1059,182 +1058,6 @@ class ServiceFilterSet(NetBoxModelFilterSet):
|
|||||||
return queryset.filter(qs_filter)
|
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):
|
class PrimaryIPFilterSet(django_filters.FilterSet):
|
||||||
"""
|
"""
|
||||||
An inheritable FilterSet for models which support primary IP assignment.
|
An inheritable FilterSet for models which support primary IP assignment.
|
||||||
|
@ -23,8 +23,6 @@ __all__ = (
|
|||||||
'FHRPGroupBulkEditForm',
|
'FHRPGroupBulkEditForm',
|
||||||
'IPAddressBulkEditForm',
|
'IPAddressBulkEditForm',
|
||||||
'IPRangeBulkEditForm',
|
'IPRangeBulkEditForm',
|
||||||
'L2VPNBulkEditForm',
|
|
||||||
'L2VPNTerminationBulkEditForm',
|
|
||||||
'PrefixBulkEditForm',
|
'PrefixBulkEditForm',
|
||||||
'RIRBulkEditForm',
|
'RIRBulkEditForm',
|
||||||
'RoleBulkEditForm',
|
'RoleBulkEditForm',
|
||||||
@ -596,32 +594,3 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
|
|
||||||
class ServiceBulkEditForm(ServiceTemplateBulkEditForm):
|
class ServiceBulkEditForm(ServiceTemplateBulkEditForm):
|
||||||
model = Service
|
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
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.models import Device, Interface, Site
|
from dcim.models import Device, Interface, Site
|
||||||
@ -21,8 +20,6 @@ __all__ = (
|
|||||||
'FHRPGroupImportForm',
|
'FHRPGroupImportForm',
|
||||||
'IPAddressImportForm',
|
'IPAddressImportForm',
|
||||||
'IPRangeImportForm',
|
'IPRangeImportForm',
|
||||||
'L2VPNImportForm',
|
|
||||||
'L2VPNTerminationImportForm',
|
|
||||||
'PrefixImportForm',
|
'PrefixImportForm',
|
||||||
'RIRImportForm',
|
'RIRImportForm',
|
||||||
'RoleImportForm',
|
'RoleImportForm',
|
||||||
@ -529,92 +526,3 @@ class ServiceImportForm(NetBoxModelImportForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return self.cleaned_data['ipaddresses']
|
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')
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
|
from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
|
||||||
@ -9,10 +8,9 @@ from ipam.models import *
|
|||||||
from netbox.forms import NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
from tenancy.forms import TenancyFilterForm
|
from tenancy.forms import TenancyFilterForm
|
||||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
|
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
|
||||||
from utilities.forms.fields import (
|
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
|
||||||
ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
|
|
||||||
)
|
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
|
from vpn.models import L2VPN
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateFilterForm',
|
'AggregateFilterForm',
|
||||||
@ -21,8 +19,6 @@ __all__ = (
|
|||||||
'FHRPGroupFilterForm',
|
'FHRPGroupFilterForm',
|
||||||
'IPAddressFilterForm',
|
'IPAddressFilterForm',
|
||||||
'IPRangeFilterForm',
|
'IPRangeFilterForm',
|
||||||
'L2VPNFilterForm',
|
|
||||||
'L2VPNTerminationFilterForm',
|
|
||||||
'PrefixFilterForm',
|
'PrefixFilterForm',
|
||||||
'RIRFilterForm',
|
'RIRFilterForm',
|
||||||
'RoleFilterForm',
|
'RoleFilterForm',
|
||||||
@ -539,90 +535,3 @@ class ServiceFilterForm(ServiceTemplateFilterForm):
|
|||||||
label=_('Virtual Machine'),
|
label=_('Virtual Machine'),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
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')
|
|
||||||
)
|
|
||||||
|
@ -29,8 +29,6 @@ __all__ = (
|
|||||||
'IPAddressBulkAddForm',
|
'IPAddressBulkAddForm',
|
||||||
'IPAddressForm',
|
'IPAddressForm',
|
||||||
'IPRangeForm',
|
'IPRangeForm',
|
||||||
'L2VPNForm',
|
|
||||||
'L2VPNTerminationForm',
|
|
||||||
'PrefixForm',
|
'PrefixForm',
|
||||||
'RIRForm',
|
'RIRForm',
|
||||||
'RoleForm',
|
'RoleForm',
|
||||||
@ -754,97 +752,3 @@ class ServiceCreateForm(ServiceForm):
|
|||||||
self.cleaned_data['description'] = service_template.description
|
self.cleaned_data['description'] = service_template.description
|
||||||
elif not all(self.cleaned_data[f] for f in ('name', 'protocol', 'ports')):
|
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.")
|
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
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
from ipam import models
|
from ipam import models
|
||||||
from utilities.graphql_optimizer import gql_query_optimizer
|
|
||||||
|
|
||||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||||
|
from utilities.graphql_optimizer import gql_query_optimizer
|
||||||
from .types import *
|
from .types import *
|
||||||
|
|
||||||
|
|
||||||
@ -38,18 +37,6 @@ class IPAMQuery(graphene.ObjectType):
|
|||||||
def resolve_ip_range_list(root, info, **kwargs):
|
def resolve_ip_range_list(root, info, **kwargs):
|
||||||
return gql_query_optimizer(models.IPRange.objects.all(), info)
|
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 = ObjectField(PrefixType)
|
||||||
prefix_list = ObjectListField(PrefixType)
|
prefix_list = ObjectListField(PrefixType)
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
from extras.graphql.mixins import ContactsMixin
|
|
||||||
from ipam import filtersets, models
|
from ipam import filtersets, models
|
||||||
from netbox.graphql.scalars import BigInt
|
from netbox.graphql.scalars import BigInt
|
||||||
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
|
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
|
||||||
@ -13,8 +12,6 @@ __all__ = (
|
|||||||
'FHRPGroupAssignmentType',
|
'FHRPGroupAssignmentType',
|
||||||
'IPAddressType',
|
'IPAddressType',
|
||||||
'IPRangeType',
|
'IPRangeType',
|
||||||
'L2VPNType',
|
|
||||||
'L2VPNTerminationType',
|
|
||||||
'PrefixType',
|
'PrefixType',
|
||||||
'RIRType',
|
'RIRType',
|
||||||
'RoleType',
|
'RoleType',
|
||||||
@ -188,19 +185,3 @@ class VRFType(NetBoxObjectType):
|
|||||||
model = models.VRF
|
model = models.VRF
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
filterset_class = filtersets.VRFFilterSet
|
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
|
|
||||||
|
64
netbox/ipam/migrations/0068_move_l2vpn.py
Normal file
64
netbox/ipam/migrations/0068_move_l2vpn.py
Normal 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
|
||||||
|
),
|
||||||
|
]
|
@ -3,27 +3,5 @@ from .asns import *
|
|||||||
from .fhrp import *
|
from .fhrp import *
|
||||||
from .vrfs import *
|
from .vrfs import *
|
||||||
from .ip import *
|
from .ip import *
|
||||||
from .l2vpn import *
|
|
||||||
from .services import *
|
from .services import *
|
||||||
from .vlans import *
|
from .vlans import *
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
'ASN',
|
|
||||||
'ASNRange',
|
|
||||||
'Aggregate',
|
|
||||||
'IPAddress',
|
|
||||||
'IPRange',
|
|
||||||
'FHRPGroup',
|
|
||||||
'FHRPGroupAssignment',
|
|
||||||
'L2VPN',
|
|
||||||
'L2VPNTermination',
|
|
||||||
'Prefix',
|
|
||||||
'RIR',
|
|
||||||
'Role',
|
|
||||||
'RouteTarget',
|
|
||||||
'Service',
|
|
||||||
'ServiceTemplate',
|
|
||||||
'VLAN',
|
|
||||||
'VLANGroup',
|
|
||||||
'VRF',
|
|
||||||
)
|
|
||||||
|
@ -183,9 +183,8 @@ class VLAN(PrimaryModel):
|
|||||||
null=True,
|
null=True,
|
||||||
help_text=_("The primary function of this VLAN")
|
help_text=_("The primary function of this VLAN")
|
||||||
)
|
)
|
||||||
|
|
||||||
l2vpn_terminations = GenericRelation(
|
l2vpn_terminations = GenericRelation(
|
||||||
to='ipam.L2VPNTermination',
|
to='vpn.L2VPNTermination',
|
||||||
content_type_field='assigned_object_type',
|
content_type_field='assigned_object_type',
|
||||||
object_id_field='assigned_object_id',
|
object_id_field='assigned_object_id',
|
||||||
related_query_name='vlan'
|
related_query_name='vlan'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from . import models
|
|
||||||
from netbox.search import SearchIndex, register_search
|
from netbox.search import SearchIndex, register_search
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
@register_search
|
@register_search
|
||||||
@ -69,18 +69,6 @@ class IPRangeIndex(SearchIndex):
|
|||||||
display_attrs = ('vrf', 'tenant', 'status', 'role', 'description')
|
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
|
@register_search
|
||||||
class PrefixIndex(SearchIndex):
|
class PrefixIndex(SearchIndex):
|
||||||
model = models.Prefix
|
model = models.Prefix
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from .asn import *
|
from .asn import *
|
||||||
from .fhrp import *
|
from .fhrp import *
|
||||||
from .ip import *
|
from .ip import *
|
||||||
from .l2vpn import *
|
|
||||||
from .services import *
|
from .services import *
|
||||||
from .vlans import *
|
from .vlans import *
|
||||||
from .vrfs import *
|
from .vrfs import *
|
||||||
|
@ -48,6 +48,7 @@ class ASNTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
asn_asdot = tables.Column(
|
asn_asdot = tables.Column(
|
||||||
accessor=tables.A('asn_asdot'),
|
accessor=tables.A('asn_asdot'),
|
||||||
linkify=True,
|
linkify=True,
|
||||||
|
order_by=tables.A('asn'),
|
||||||
verbose_name=_('ASDOT')
|
verbose_name=_('ASDOT')
|
||||||
)
|
)
|
||||||
site_count = columns.LinkedCountColumn(
|
site_count = columns.LinkedCountColumn(
|
||||||
|
@ -1100,96 +1100,3 @@ class ServiceTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'ports': [6],
|
'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
|
|
||||||
}
|
|
||||||
|
@ -7,9 +7,9 @@ from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Man
|
|||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.filtersets import *
|
from ipam.filtersets import *
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
|
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
from tenancy.models import Tenant, TenantGroup
|
|
||||||
|
|
||||||
|
|
||||||
class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
@ -1616,163 +1616,3 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
params = {'ipaddress': [str(ips[0].address), str(ips[1].address)]}
|
params = {'ipaddress': [str(ips[0].address), str(ips[1].address)]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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)
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
from netaddr import IPNetwork, IPSet
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase, override_settings
|
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 *
|
||||||
from ipam.choices import IPAddressRoleChoices, PrefixStatusChoices
|
from ipam.models import *
|
||||||
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, RIR, VLAN, VLANGroup, VRF, L2VPN, L2VPNTermination
|
|
||||||
|
|
||||||
|
|
||||||
class TestAggregate(TestCase):
|
class TestAggregate(TestCase):
|
||||||
@ -539,76 +538,3 @@ class TestVLANGroup(TestCase):
|
|||||||
|
|
||||||
VLAN.objects.create(name='VLAN 104', vid=104, group=vlangroup)
|
VLAN.objects.create(name='VLAN 104', vid=104, group=vlangroup)
|
||||||
self.assertEqual(vlangroup.get_next_available_vid(), 105)
|
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)
|
|
||||||
|
@ -9,7 +9,7 @@ from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site, Inte
|
|||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
from tenancy.models import Tenant
|
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):
|
class ASNRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
@ -986,142 +986,3 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
self.assertEqual(instance.protocol, service_template.protocol)
|
self.assertEqual(instance.protocol, service_template.protocol)
|
||||||
self.assertEqual(instance.ports, service_template.ports)
|
self.assertEqual(instance.ports, service_template.ports)
|
||||||
self.assertEqual(instance.description, service_template.description)
|
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)
|
|
||||||
|
@ -131,20 +131,4 @@ urlpatterns = [
|
|||||||
path('services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
|
path('services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
|
||||||
path('services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),
|
path('services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),
|
||||||
path('services/<int:pk>/', include(get_model_urls('ipam', 'service'))),
|
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'))),
|
|
||||||
]
|
]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
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.db.models.expressions import RawSQL
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -9,7 +9,6 @@ from circuits.models import Provider
|
|||||||
from dcim.filtersets import InterfaceFilterSet
|
from dcim.filtersets import InterfaceFilterSet
|
||||||
from dcim.models import Interface, Site
|
from dcim.models import Interface, Site
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from tenancy.views import ObjectContactsView
|
|
||||||
from utilities.tables import get_table_ordering
|
from utilities.tables import get_table_ordering
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from utilities.views import ViewTab, register_model_view
|
from utilities.views import ViewTab, register_model_view
|
||||||
@ -19,7 +18,6 @@ from . import filtersets, forms, tables
|
|||||||
from .choices import PrefixStatusChoices
|
from .choices import PrefixStatusChoices
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .models import *
|
from .models import *
|
||||||
from .tables.l2vpn import L2VPNTable, L2VPNTerminationTable
|
|
||||||
from .utils import add_requested_prefixes, add_available_ipaddresses, add_available_vlans
|
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')
|
queryset = Service.objects.prefetch_related('device', 'virtual_machine')
|
||||||
filterset = filtersets.ServiceFilterSet
|
filterset = filtersets.ServiceFilterSet
|
||||||
table = tables.ServiceTable
|
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
|
|
||||||
|
@ -209,8 +209,8 @@ VPN_MENU = Menu(
|
|||||||
MenuGroup(
|
MenuGroup(
|
||||||
label=_('L2VPNs'),
|
label=_('L2VPNs'),
|
||||||
items=(
|
items=(
|
||||||
get_model_item('ipam', 'l2vpn', _('L2VPNs')),
|
get_model_item('vpn', 'l2vpn', _('L2VPNs')),
|
||||||
get_model_item('ipam', 'l2vpntermination', _('Terminations')),
|
get_model_item('vpn', 'l2vpntermination', _('Terminations')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MenuGroup(
|
MenuGroup(
|
||||||
|
@ -27,7 +27,7 @@ from netbox.plugins import PluginConfig
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.6.6-dev'
|
VERSION = '3.6.7-dev'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
|
@ -394,6 +394,10 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
form.add_error('data', f"Row {i}: Object with ID {object_id} does not exist")
|
form.add_error('data', f"Row {i}: Object with ID {object_id} does not exist")
|
||||||
raise ValidationError('')
|
raise ValidationError('')
|
||||||
|
|
||||||
|
# Take a snapshot for change logging
|
||||||
|
if instance.pk and hasattr(instance, 'snapshot'):
|
||||||
|
instance.snapshot()
|
||||||
|
|
||||||
# Instantiate the model form for the object
|
# Instantiate the model form for the object
|
||||||
model_form_kwargs = {
|
model_form_kwargs = {
|
||||||
'data': record,
|
'data': record,
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load mptt %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -15,16 +16,7 @@
|
|||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Region" %}</th>
|
<th scope="row">{% trans "Region" %}</th>
|
||||||
<td>
|
<td>{% nested_tree object.site.region %}</td>
|
||||||
{% if object.site.region %}
|
|
||||||
{% for region in object.site.region.get_ancestors %}
|
|
||||||
{{ region|linkify }} /
|
|
||||||
{% endfor %}
|
|
||||||
{{ object.site.region|linkify }}
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Site" %}</th>
|
<th scope="row">{% trans "Site" %}</th>
|
||||||
@ -32,16 +24,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Location" %}</th>
|
<th scope="row">{% trans "Location" %}</th>
|
||||||
<td>
|
<td>{% nested_tree object.location %}</td>
|
||||||
{% if object.location %}
|
|
||||||
{% for location in object.location.get_ancestors %}
|
|
||||||
{{ location|linkify }} /
|
|
||||||
{% endfor %}
|
|
||||||
{{ object.location|linkify }}
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Rack" %}</th>
|
<th scope="row">{% trans "Rack" %}</th>
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load mptt %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -15,26 +16,18 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Site" %}</th>
|
<th scope="row">{% trans "Region" %}</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.site.region %}
|
{% nested_tree object.site.region %}
|
||||||
{{ object.site.region|linkify }} /
|
|
||||||
{% endif %}
|
|
||||||
{{ object.site|linkify }}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Site" %}</th>
|
||||||
|
<td>{{ object.site|linkify }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Location" %}</th>
|
<th scope="row">{% trans "Location" %}</th>
|
||||||
<td>
|
<td>{% nested_tree object.location %}</td>
|
||||||
{% if object.location %}
|
|
||||||
{% for location in object.location.get_ancestors %}
|
|
||||||
{{ location|linkify }} /
|
|
||||||
{% endfor %}
|
|
||||||
{{ object.location|linkify }}
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Facility ID" %}</th>
|
<th scope="row">{% trans "Facility ID" %}</th>
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load mptt %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
@ -20,25 +21,24 @@
|
|||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
{% with rack=object.rack %}
|
<tr>
|
||||||
<tr>
|
<th scope="row">{% trans "Region" %}</th>
|
||||||
<th scope="row">{% trans "Site" %}</th>
|
<td>
|
||||||
<td>
|
{% nested_tree object.rack.site.region %}
|
||||||
{% if rack.site.region %}
|
</td>
|
||||||
{{ rack.site.region|linkify }} /
|
</tr>
|
||||||
{% endif %}
|
<tr>
|
||||||
{{ rack.site|linkify }}
|
<th scope="row">{% trans "Site" %}</th>
|
||||||
</td>
|
<td>{{ object.rack.site|linkify }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Location" %}</th>
|
<th scope="row">{% trans "Location" %}</th>
|
||||||
<td>{{ rack.location|linkify|placeholder }}</td>
|
<td>{{ object.rack.location|linkify|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Rack" %}</th>
|
<th scope="row">{% trans "Rack" %}</th>
|
||||||
<td>{{ rack|linkify }}</td>
|
<td>{{ object.rack|linkify }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endwith %}
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
{% load tz %}
|
{% load tz %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load mptt %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
@ -29,27 +30,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Region" %}</th>
|
<th scope="row">{% trans "Region" %}</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.region %}
|
{% nested_tree object.region %}
|
||||||
{% for region in object.region.get_ancestors %}
|
|
||||||
{{ region|linkify }} /
|
|
||||||
{% endfor %}
|
|
||||||
{{ object.region|linkify }}
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Group" %}</th>
|
<th scope="row">{% trans "Group" %}</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.group %}
|
{% nested_tree object.group %}
|
||||||
{% for group in object.group.get_ancestors %}
|
|
||||||
{{ group|linkify }} /
|
|
||||||
{% endfor %}
|
|
||||||
{{ object.group|linkify }}
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load mptt %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -44,18 +45,17 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if object.site.region %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Region" %}</th>
|
||||||
|
<td>
|
||||||
|
{% nested_tree object.site.region %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Site" %}</th>
|
<th scope="row">{% trans "Site" %}</th>
|
||||||
<td>
|
<td>{{ object.site|linkify|placeholder }}</td>
|
||||||
{% if object.site %}
|
|
||||||
{% if object.site.region %}
|
|
||||||
{{ object.site.region|linkify }} /
|
|
||||||
{% endif %}
|
|
||||||
{{ object.site|linkify }}
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "VLAN" %}</th>
|
<th scope="row">{% trans "VLAN" %}</th>
|
||||||
|
@ -59,7 +59,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{% trans "Importing L2VPNs" %}</h5>
|
<h5 class="card-header">{% trans "Importing L2VPNs" %}</h5>
|
||||||
<div class="card-body htmx-container table-responsive"
|
<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"
|
hx-trigger="load"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
@ -68,7 +68,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{% trans "Exporting L2VPNs" %}</h5>
|
<h5 class="card-header">{% trans "Exporting L2VPNs" %}</h5>
|
||||||
<div class="card-body htmx-container table-responsive"
|
<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"
|
hx-trigger="load"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
{% load render_table from django_tables2 %}
|
{% load render_table from django_tables2 %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load mptt %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -13,18 +14,17 @@
|
|||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<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>
|
<tr>
|
||||||
<th scope="row">{% trans "Site" %}</th>
|
<th scope="row">{% trans "Site" %}</th>
|
||||||
<td>
|
<td>{{ object.site|linkify|placeholder }}</td>
|
||||||
{% if object.site %}
|
|
||||||
{% if object.site.region %}
|
|
||||||
{{ object.site.region|linkify }} /
|
|
||||||
{% endif %}
|
|
||||||
{{ object.site|linkify }}
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Group" %}</th>
|
<th scope="row">{% trans "Group" %}</th>
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
@ -56,12 +56,12 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{% trans "Terminations" %}</h5>
|
<h5 class="card-header">{% trans "Terminations" %}</h5>
|
||||||
<div class="card-body htmx-container table-responsive"
|
<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"
|
hx-trigger="load"
|
||||||
></div>
|
></div>
|
||||||
{% if perms.ipam.add_l2vpntermination %}
|
{% if perms.vpn.add_l2vpntermination %}
|
||||||
<div class="card-footer text-end noprint">
|
<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" %}
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Termination" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ContentType
|
||||||
from netbox.models import ChangeLoggedModel, NestedGroupModel, OrganizationalModel, PrimaryModel
|
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 *
|
from tenancy.choices import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -110,7 +110,7 @@ class Contact(PrimaryModel):
|
|||||||
return reverse('tenancy:contact', args=[self.pk])
|
return reverse('tenancy:contact', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
class ContactAssignment(CustomFieldsMixin, TagsMixin, ChangeLoggedModel):
|
class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
||||||
content_type = models.ForeignKey(
|
content_type = models.ForeignKey(
|
||||||
to='contenttypes.ContentType',
|
to='contenttypes.ContentType',
|
||||||
on_delete=models.CASCADE
|
on_delete=models.CASCADE
|
||||||
|
@ -52,6 +52,16 @@ class UserSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
return user
|
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)
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def get_display(self, obj):
|
def get_display(self, obj):
|
||||||
if full_name := obj.get_full_name():
|
if full_name := obj.get_full_name():
|
||||||
|
@ -54,6 +54,38 @@ class UserTest(APIViewTestCases.APIViewTestCase):
|
|||||||
)
|
)
|
||||||
User.objects.bulk_create(users)
|
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):
|
class GroupTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Group
|
model = Group
|
||||||
|
@ -40,7 +40,7 @@ def parse_numeric_range(string, base=10):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
|
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
|
||||||
values.extend(range(begin, end))
|
values.extend(range(begin, end))
|
||||||
return list(set(values))
|
return sorted(set(values))
|
||||||
|
|
||||||
|
|
||||||
def parse_alphanumeric_range(string):
|
def parse_alphanumeric_range(string):
|
||||||
|
20
netbox/utilities/templatetags/mptt.py
Normal file
20
netbox/utilities/templatetags/mptt.py
Normal 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('—')
|
||||||
|
|
||||||
|
nodes = obj.get_ancestors(include_self=True)
|
||||||
|
return mark_safe(
|
||||||
|
' / '.join(
|
||||||
|
f'<a href="{node.get_absolute_url()}">{node}</a>' for node in nodes
|
||||||
|
)
|
||||||
|
)
|
@ -6,15 +6,14 @@ from dcim.api.nested_serializers import (
|
|||||||
)
|
)
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from extras.api.nested_serializers import NestedConfigTemplateSerializer
|
from extras.api.nested_serializers import NestedConfigTemplateSerializer
|
||||||
from ipam.api.nested_serializers import (
|
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
|
||||||
NestedIPAddressSerializer, NestedL2VPNTerminationSerializer, NestedVLANSerializer, NestedVRFSerializer,
|
|
||||||
)
|
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import NetBoxModelSerializer
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualDisk, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualDisk, VirtualMachine, VMInterface
|
||||||
|
from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
|
|
||||||
|
@ -296,9 +296,10 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
# Check interface sites. First interface should set site, further interfaces will either continue the
|
# 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.
|
# loop or reset back to no site and break the loop.
|
||||||
for interface in interfaces:
|
for interface in interfaces:
|
||||||
|
vm_site = interface.virtual_machine.site or interface.virtual_machine.cluster.site
|
||||||
if site is None:
|
if site is None:
|
||||||
site = interface.virtual_machine.cluster.site
|
site = vm_site
|
||||||
elif interface.virtual_machine.cluster.site is not site:
|
elif vm_site is not site:
|
||||||
site = None
|
site = None
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -4,13 +4,14 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||||
from extras.forms import LocalConfigContextFilterForm
|
from extras.forms import LocalConfigContextFilterForm
|
||||||
from extras.models import ConfigTemplate
|
from extras.models import ConfigTemplate
|
||||||
from ipam.models import L2VPN, VRF
|
from ipam.models import VRF
|
||||||
from netbox.forms import NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES
|
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES
|
||||||
from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
|
from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
from virtualization.models import *
|
from virtualization.models import *
|
||||||
|
from vpn.models import L2VPN
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ClusterFilterForm',
|
'ClusterFilterForm',
|
||||||
|
@ -358,7 +358,7 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
|
|||||||
related_query_name='vminterface',
|
related_query_name='vminterface',
|
||||||
)
|
)
|
||||||
l2vpn_terminations = GenericRelation(
|
l2vpn_terminations = GenericRelation(
|
||||||
to='ipam.L2VPNTermination',
|
to='vpn.L2VPNTermination',
|
||||||
content_type_field='assigned_object_type',
|
content_type_field='assigned_object_type',
|
||||||
object_id_field='assigned_object_id',
|
object_id_field='assigned_object_id',
|
||||||
related_query_name='vminterface',
|
related_query_name='vminterface',
|
||||||
|
@ -24,8 +24,8 @@ VMINTERFACE_BUTTONS = """
|
|||||||
{% if perms.ipam.add_ipaddress %}
|
{% 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>
|
<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 %}
|
{% endif %}
|
||||||
{% if perms.ipam.add_l2vpntermination %}
|
{% if perms.vpn.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>
|
<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 %}
|
{% endif %}
|
||||||
{% if perms.ipam.add_fhrpgroupassignment %}
|
{% 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>
|
<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>
|
||||||
|
@ -9,6 +9,8 @@ __all__ = (
|
|||||||
'NestedIPSecPolicySerializer',
|
'NestedIPSecPolicySerializer',
|
||||||
'NestedIPSecProfileSerializer',
|
'NestedIPSecProfileSerializer',
|
||||||
'NestedIPSecProposalSerializer',
|
'NestedIPSecProposalSerializer',
|
||||||
|
'NestedL2VPNSerializer',
|
||||||
|
'NestedL2VPNTerminationSerializer',
|
||||||
'NestedTunnelSerializer',
|
'NestedTunnelSerializer',
|
||||||
'NestedTunnelTerminationSerializer',
|
'NestedTunnelTerminationSerializer',
|
||||||
)
|
)
|
||||||
@ -82,3 +84,28 @@ class NestedIPSecProfileSerializer(WritableNestedSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = models.IPSecProfile
|
model = models.IPSecProfile
|
||||||
fields = ('id', 'url', 'display', 'name')
|
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'
|
||||||
|
]
|
||||||
|
@ -2,7 +2,8 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
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.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import NetBoxModelSerializer
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||||
@ -18,6 +19,8 @@ __all__ = (
|
|||||||
'IPSecPolicySerializer',
|
'IPSecPolicySerializer',
|
||||||
'IPSecProfileSerializer',
|
'IPSecProfileSerializer',
|
||||||
'IPSecProposalSerializer',
|
'IPSecProposalSerializer',
|
||||||
|
'L2VPNSerializer',
|
||||||
|
'L2VPNTerminationSerializer',
|
||||||
'TunnelSerializer',
|
'TunnelSerializer',
|
||||||
'TunnelTerminationSerializer',
|
'TunnelTerminationSerializer',
|
||||||
)
|
)
|
||||||
@ -191,3 +194,54 @@ class IPSecProfileSerializer(NetBoxModelSerializer):
|
|||||||
'id', 'url', 'display', 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'comments', 'tags',
|
'id', 'url', 'display', 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'comments', 'tags',
|
||||||
'custom_fields', 'created', 'last_updated',
|
'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
|
||||||
|
@ -10,6 +10,8 @@ router.register('ipsec-proposals', views.IPSecProposalViewSet)
|
|||||||
router.register('ipsec-profiles', views.IPSecProfileViewSet)
|
router.register('ipsec-profiles', views.IPSecProfileViewSet)
|
||||||
router.register('tunnels', views.TunnelViewSet)
|
router.register('tunnels', views.TunnelViewSet)
|
||||||
router.register('tunnel-terminations', views.TunnelTerminationViewSet)
|
router.register('tunnel-terminations', views.TunnelTerminationViewSet)
|
||||||
|
router.register('l2vpns', views.L2VPNViewSet)
|
||||||
|
router.register('l2vpn-terminations', views.L2VPNTerminationViewSet)
|
||||||
|
|
||||||
app_name = 'vpn-api'
|
app_name = 'vpn-api'
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls
|
||||||
|
@ -12,6 +12,8 @@ __all__ = (
|
|||||||
'IPSecPolicyViewSet',
|
'IPSecPolicyViewSet',
|
||||||
'IPSecProfileViewSet',
|
'IPSecProfileViewSet',
|
||||||
'IPSecProposalViewSet',
|
'IPSecProposalViewSet',
|
||||||
|
'L2VPNViewSet',
|
||||||
|
'L2VPNTerminationViewSet',
|
||||||
'TunnelTerminationViewSet',
|
'TunnelTerminationViewSet',
|
||||||
'TunnelViewSet',
|
'TunnelViewSet',
|
||||||
'VPNRootView',
|
'VPNRootView',
|
||||||
@ -72,3 +74,15 @@ class IPSecProfileViewSet(NetBoxModelViewSet):
|
|||||||
queryset = IPSecProfile.objects.all()
|
queryset = IPSecProfile.objects.all()
|
||||||
serializer_class = serializers.IPSecProfileSerializer
|
serializer_class = serializers.IPSecProfileSerializer
|
||||||
filterset_class = filtersets.IPSecProfileFilterSet
|
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
|
||||||
|
@ -199,3 +199,56 @@ class DHGroupChoices(ChoiceSet):
|
|||||||
(GROUP_33, _('Group {n}').format(n=33)),
|
(GROUP_33, _('Group {n}').format(n=33)),
|
||||||
(GROUP_34, _('Group {n}').format(n=34)),
|
(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
7
netbox/vpn/constants.py
Normal 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')
|
||||||
|
)
|
@ -2,12 +2,12 @@ import django_filters
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from dcim.models import Interface
|
from dcim.models import Device, Interface
|
||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress, RouteTarget, VLAN
|
||||||
from netbox.filtersets import NetBoxModelFilterSet
|
from netbox.filtersets import NetBoxModelFilterSet
|
||||||
from tenancy.filtersets import TenancyFilterSet
|
from tenancy.filtersets import TenancyFilterSet
|
||||||
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
|
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
|
||||||
from virtualization.models import VMInterface
|
from virtualization.models import VirtualMachine, VMInterface
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
@ -17,6 +17,8 @@ __all__ = (
|
|||||||
'IPSecPolicyFilterSet',
|
'IPSecPolicyFilterSet',
|
||||||
'IPSecProfileFilterSet',
|
'IPSecProfileFilterSet',
|
||||||
'IPSecProposalFilterSet',
|
'IPSecProposalFilterSet',
|
||||||
|
'L2VPNFilterSet',
|
||||||
|
'L2VPNTerminationFilterSet',
|
||||||
'TunnelFilterSet',
|
'TunnelFilterSet',
|
||||||
'TunnelTerminationFilterSet',
|
'TunnelTerminationFilterSet',
|
||||||
)
|
)
|
||||||
@ -239,3 +241,175 @@ class IPSecProfileFilterSet(NetBoxModelFilterSet):
|
|||||||
Q(description__icontains=value) |
|
Q(description__icontains=value) |
|
||||||
Q(comments__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
|
||||||
|
@ -14,6 +14,8 @@ __all__ = (
|
|||||||
'IPSecPolicyBulkEditForm',
|
'IPSecPolicyBulkEditForm',
|
||||||
'IPSecProfileBulkEditForm',
|
'IPSecProfileBulkEditForm',
|
||||||
'IPSecProposalBulkEditForm',
|
'IPSecProposalBulkEditForm',
|
||||||
|
'L2VPNBulkEditForm',
|
||||||
|
'L2VPNTerminationBulkEditForm',
|
||||||
'TunnelBulkEditForm',
|
'TunnelBulkEditForm',
|
||||||
'TunnelTerminationBulkEditForm',
|
'TunnelTerminationBulkEditForm',
|
||||||
)
|
)
|
||||||
@ -241,3 +243,32 @@ class IPSecProfileBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
nullable_fields = (
|
nullable_fields = (
|
||||||
'description', 'comments',
|
'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
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.models import Device, Interface
|
from dcim.models import Device, Interface
|
||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress, VLAN
|
||||||
from netbox.forms import NetBoxModelImportForm
|
from netbox.forms import NetBoxModelImportForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField
|
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField
|
||||||
@ -15,6 +16,8 @@ __all__ = (
|
|||||||
'IPSecPolicyImportForm',
|
'IPSecPolicyImportForm',
|
||||||
'IPSecProfileImportForm',
|
'IPSecProfileImportForm',
|
||||||
'IPSecProposalImportForm',
|
'IPSecProposalImportForm',
|
||||||
|
'L2VPNImportForm',
|
||||||
|
'L2VPNTerminationImportForm',
|
||||||
'TunnelImportForm',
|
'TunnelImportForm',
|
||||||
'TunnelTerminationImportForm',
|
'TunnelTerminationImportForm',
|
||||||
)
|
)
|
||||||
@ -228,3 +231,92 @@ class IPSecProfileImportForm(NetBoxModelImportForm):
|
|||||||
fields = (
|
fields = (
|
||||||
'name', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags',
|
'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')
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.translation import gettext as _
|
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 netbox.forms import NetBoxModelFilterSetForm
|
||||||
from tenancy.forms import TenancyFilterForm
|
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.choices import *
|
||||||
|
from vpn.constants import L2VPN_ASSIGNMENT_MODELS
|
||||||
from vpn.models import *
|
from vpn.models import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -13,6 +21,8 @@ __all__ = (
|
|||||||
'IPSecPolicyFilterForm',
|
'IPSecPolicyFilterForm',
|
||||||
'IPSecProfileFilterForm',
|
'IPSecProfileFilterForm',
|
||||||
'IPSecProposalFilterForm',
|
'IPSecProposalFilterForm',
|
||||||
|
'L2VPNFilterForm',
|
||||||
|
'L2VPNTerminationFilterForm',
|
||||||
'TunnelFilterForm',
|
'TunnelFilterForm',
|
||||||
'TunnelTerminationFilterForm',
|
'TunnelTerminationFilterForm',
|
||||||
)
|
)
|
||||||
@ -180,3 +190,90 @@ class IPSecProfileFilterForm(NetBoxModelFilterSetForm):
|
|||||||
label=_('IPSec policy')
|
label=_('IPSec policy')
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
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')
|
||||||
|
)
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.models import Device, Interface
|
from dcim.models import Device, Interface
|
||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress, RouteTarget, VLAN
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from tenancy.forms import TenancyForm
|
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.utils import add_blank_choice
|
||||||
from utilities.forms.widgets import HTMXSelect
|
from utilities.forms.widgets import HTMXSelect
|
||||||
from virtualization.models import VirtualMachine, VMInterface
|
from virtualization.models import VirtualMachine, VMInterface
|
||||||
@ -18,6 +19,8 @@ __all__ = (
|
|||||||
'IPSecPolicyForm',
|
'IPSecPolicyForm',
|
||||||
'IPSecProfileForm',
|
'IPSecProfileForm',
|
||||||
'IPSecProposalForm',
|
'IPSecProposalForm',
|
||||||
|
'L2VPNForm',
|
||||||
|
'L2VPNTerminationForm',
|
||||||
'TunnelCreateForm',
|
'TunnelCreateForm',
|
||||||
'TunnelForm',
|
'TunnelForm',
|
||||||
'TunnelTerminationForm',
|
'TunnelTerminationForm',
|
||||||
@ -355,3 +358,96 @@ class IPSecProfileForm(NetBoxModelForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags',
|
'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
|
||||||
|
30
netbox/vpn/graphql/gfk_mixins.py
Normal file
30
netbox/vpn/graphql/gfk_mixins.py
Normal 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
|
@ -38,6 +38,18 @@ class VPNQuery(graphene.ObjectType):
|
|||||||
def resolve_ipsec_proposal_list(root, info, **kwargs):
|
def resolve_ipsec_proposal_list(root, info, **kwargs):
|
||||||
return gql_query_optimizer(models.IPSecProposal.objects.all(), info)
|
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 = ObjectField(TunnelType)
|
||||||
tunnel_list = ObjectListField(TunnelType)
|
tunnel_list = ObjectListField(TunnelType)
|
||||||
|
|
||||||
|
@ -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 netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
|
||||||
from vpn import filtersets, models
|
from vpn import filtersets, models
|
||||||
|
|
||||||
@ -8,6 +10,8 @@ __all__ = (
|
|||||||
'IPSecPolicyType',
|
'IPSecPolicyType',
|
||||||
'IPSecProfileType',
|
'IPSecProfileType',
|
||||||
'IPSecProposalType',
|
'IPSecProposalType',
|
||||||
|
'L2VPNType',
|
||||||
|
'L2VPNTerminationType',
|
||||||
'TunnelTerminationType',
|
'TunnelTerminationType',
|
||||||
'TunnelType',
|
'TunnelType',
|
||||||
)
|
)
|
||||||
@ -67,3 +71,19 @@ class IPSecProfileType(OrganizationalObjectType):
|
|||||||
model = models.IPSecProfile
|
model = models.IPSecProfile
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
filterset_class = filtersets.IPSecProfileFilterSet
|
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
|
||||||
|
73
netbox/vpn/migrations/0002_move_l2vpn.py
Normal file
73
netbox/vpn/migrations/0002_move_l2vpn.py
Normal 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'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -1,2 +1,3 @@
|
|||||||
from .crypto import *
|
from .crypto import *
|
||||||
|
from .l2vpn import *
|
||||||
from .tunnels import *
|
from .tunnels import *
|
||||||
|
@ -6,10 +6,10 @@ from django.utils.functional import cached_property
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.models import ContentType
|
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 import NetBoxModel, PrimaryModel
|
||||||
from netbox.models.features import ContactsMixin
|
from netbox.models.features import ContactsMixin
|
||||||
|
from vpn.choices import L2VPNTypeChoices
|
||||||
|
from vpn.constants import L2VPN_ASSIGNMENT_MODELS
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'L2VPN',
|
'L2VPN',
|
||||||
@ -69,7 +69,7 @@ class L2VPN(ContactsMixin, PrimaryModel):
|
|||||||
return f'{self.name}'
|
return f'{self.name}'
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('ipam:l2vpn', args=[self.pk])
|
return reverse('vpn:l2vpn', args=[self.pk])
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def can_add_termination(self):
|
def can_add_termination(self):
|
||||||
@ -81,7 +81,7 @@ class L2VPN(ContactsMixin, PrimaryModel):
|
|||||||
|
|
||||||
class L2VPNTermination(NetBoxModel):
|
class L2VPNTermination(NetBoxModel):
|
||||||
l2vpn = models.ForeignKey(
|
l2vpn = models.ForeignKey(
|
||||||
to='ipam.L2VPN',
|
to='vpn.L2VPN',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='terminations'
|
related_name='terminations'
|
||||||
)
|
)
|
||||||
@ -99,7 +99,7 @@ class L2VPNTermination(NetBoxModel):
|
|||||||
|
|
||||||
clone_fields = ('l2vpn',)
|
clone_fields = ('l2vpn',)
|
||||||
prerequisite_models = (
|
prerequisite_models = (
|
||||||
'ipam.L2VPN',
|
'vpn.L2VPN',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -107,7 +107,7 @@ class L2VPNTermination(NetBoxModel):
|
|||||||
constraints = (
|
constraints = (
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=('assigned_object_type', 'assigned_object_id'),
|
fields=('assigned_object_type', 'assigned_object_id'),
|
||||||
name='ipam_l2vpntermination_assigned_object'
|
name='vpn_l2vpntermination_assigned_object'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
verbose_name = _('L2VPN termination')
|
verbose_name = _('L2VPN termination')
|
||||||
@ -119,7 +119,7 @@ class L2VPNTermination(NetBoxModel):
|
|||||||
return super().__str__()
|
return super().__str__()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('ipam:l2vpntermination', args=[self.pk])
|
return reverse('vpn:l2vpntermination', args=[self.pk])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
# Only check is assigned_object is set. Required otherwise we have an Integrity Error thrown.
|
# Only check is assigned_object is set. Required otherwise we have an Integrity Error thrown.
|
@ -63,3 +63,15 @@ class IPSecProfileIndex(SearchIndex):
|
|||||||
('comments', 5000),
|
('comments', 5000),
|
||||||
)
|
)
|
||||||
display_attrs = ('description',)
|
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')
|
||||||
|
3
netbox/vpn/tables/__init__.py
Normal file
3
netbox/vpn/tables/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from .crypto import *
|
||||||
|
from .l2vpn import *
|
||||||
|
from .tunnels import *
|
@ -1,8 +1,6 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django.utils.translation import gettext_lazy as _
|
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 netbox.tables import NetBoxTable, columns
|
||||||
from vpn.models import *
|
from vpn.models import *
|
||||||
|
|
||||||
@ -12,88 +10,9 @@ __all__ = (
|
|||||||
'IPSecPolicyTable',
|
'IPSecPolicyTable',
|
||||||
'IPSecProposalTable',
|
'IPSecProposalTable',
|
||||||
'IPSecProfileTable',
|
'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):
|
class IKEProposalTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
@ -1,9 +1,9 @@
|
|||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
import django_tables2 as tables
|
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 netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenancyColumnsMixin
|
from tenancy.tables import TenancyColumnsMixin
|
||||||
|
from vpn.models import L2VPN, L2VPNTermination
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'L2VPNTable',
|
'L2VPNTable',
|
||||||
@ -37,7 +37,7 @@ class L2VPNTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
verbose_name=_('Comments'),
|
verbose_name=_('Comments'),
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='ipam:l2vpn_list'
|
url_name='vpn:l2vpn_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
87
netbox/vpn/tables/tunnels.py
Normal file
87
netbox/vpn/tables/tunnels.py
Normal 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')
|
@ -2,6 +2,7 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from dcim.choices import InterfaceTypeChoices
|
from dcim.choices import InterfaceTypeChoices
|
||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
|
from ipam.models import VLAN
|
||||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
||||||
from vpn.choices import *
|
from vpn.choices import *
|
||||||
from vpn.models import *
|
from vpn.models import *
|
||||||
@ -471,3 +472,96 @@ class IPSecProfileTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'ipsec_policy': ipsec_policies[1].pk,
|
'ipsec_policy': ipsec_policies[1].pk,
|
||||||
'description': 'New description',
|
'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
|
||||||
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.choices import InterfaceTypeChoices
|
from dcim.choices import InterfaceTypeChoices
|
||||||
from dcim.models import Interface
|
from dcim.models import Device, Interface, Site
|
||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress, VLAN, RouteTarget
|
||||||
from virtualization.models import VMInterface
|
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
|
||||||
|
from virtualization.models import VirtualMachine, VMInterface
|
||||||
from vpn.choices import *
|
from vpn.choices import *
|
||||||
from vpn.filtersets import *
|
from vpn.filtersets import *
|
||||||
from vpn.models import *
|
from vpn.models import *
|
||||||
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
|
|
||||||
|
|
||||||
|
|
||||||
class TunnelTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class TunnelTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
@ -590,3 +591,163 @@ class IPSecProfileTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
params = {'ipsec_policy': [ipsec_policies[0].name, ipsec_policies[1].name]}
|
params = {'ipsec_policy': [ipsec_policies[0].name, ipsec_policies[1].name]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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)
|
||||||
|
79
netbox/vpn/tests/test_models.py
Normal file
79
netbox/vpn/tests/test_models.py
Normal 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)
|
@ -1,8 +1,9 @@
|
|||||||
from dcim.choices import InterfaceTypeChoices
|
from dcim.choices import InterfaceTypeChoices
|
||||||
from dcim.models import Interface
|
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.choices import *
|
||||||
from vpn.models import *
|
from vpn.models import *
|
||||||
from utilities.testing import ViewTestCases, create_tags, create_test_device
|
|
||||||
|
|
||||||
|
|
||||||
class TunnelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class TunnelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
@ -506,3 +507,142 @@ class IPSecProfileTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'ike_policy': ike_policies[1].pk,
|
'ike_policy': ike_policies[1].pk,
|
||||||
'ipsec_policy': ipsec_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)
|
||||||
|
@ -62,4 +62,20 @@ urlpatterns = [
|
|||||||
path('ipsec-profiles/delete/', views.IPSecProfileBulkDeleteView.as_view(), name='ipsecprofile_bulk_delete'),
|
path('ipsec-profiles/delete/', views.IPSecProfileBulkDeleteView.as_view(), name='ipsecprofile_bulk_delete'),
|
||||||
path('ipsec-profiles/<int:pk>/', include(get_model_urls('vpn', 'ipsecprofile'))),
|
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'))),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
from ipam.tables import RouteTargetTable
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
|
from tenancy.views import ObjectContactsView
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from utilities.views import register_model_view
|
from utilities.views import register_model_view
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
@ -332,3 +334,112 @@ class IPSecProfileBulkDeleteView(generic.BulkDeleteView):
|
|||||||
queryset = IPSecProfile.objects.all()
|
queryset = IPSecProfile.objects.all()
|
||||||
filterset = filtersets.IPSecProfileFilterSet
|
filterset = filtersets.IPSecProfileFilterSet
|
||||||
table = tables.IPSecProfileTable
|
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
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
bleach==6.1.0
|
bleach==6.1.0
|
||||||
Django==4.2.7
|
Django==4.2.7
|
||||||
django-cors-headers==4.3.0
|
django-cors-headers==4.3.1
|
||||||
django-debug-toolbar==4.2.0
|
django-debug-toolbar==4.2.0
|
||||||
django-filter==23.3
|
django-filter==23.4
|
||||||
django-graphiql-debug-toolbar==0.2.0
|
django-graphiql-debug-toolbar==0.2.0
|
||||||
django-mptt==0.14.0
|
django-mptt==0.14.0
|
||||||
django-pglocks==1.0.4
|
django-pglocks==1.0.4
|
||||||
django-prometheus==2.3.1
|
django-prometheus==2.3.1
|
||||||
django-redis==5.4.0
|
django-redis==5.4.0
|
||||||
django-rich==1.8.0
|
django-rich==1.8.0
|
||||||
django-rq==2.8.1
|
django-rq==2.9.0
|
||||||
django-tables2==2.6.0
|
django-tables2==2.6.0
|
||||||
django-taggit==4.0.0
|
django-taggit==4.0.0
|
||||||
django-timezone-field==6.0.1
|
django-timezone-field==6.1.0
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.14.0
|
||||||
drf-spectacular==0.26.5
|
drf-spectacular==0.26.5
|
||||||
drf-spectacular-sidecar==2023.10.1
|
drf-spectacular-sidecar==2023.10.1
|
||||||
@ -21,15 +21,15 @@ graphene-django==3.0.0
|
|||||||
gunicorn==21.2.0
|
gunicorn==21.2.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
Markdown==3.3.7
|
Markdown==3.3.7
|
||||||
mkdocs-material==9.4.8
|
mkdocs-material==9.4.14
|
||||||
mkdocstrings[python-legacy]==0.23.0
|
mkdocstrings[python-legacy]==0.24.0
|
||||||
netaddr==0.9.0
|
netaddr==0.9.0
|
||||||
Pillow==10.1.0
|
Pillow==10.1.0
|
||||||
psycopg[binary,pool]==3.1.12
|
psycopg[binary,pool]==3.1.13
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
social-auth-app-django==5.4.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
|
svgwrite==1.4.3
|
||||||
tablib==3.5.0
|
tablib==3.5.0
|
||||||
tzdata==2023.3
|
tzdata==2023.3
|
||||||
|
Loading…
Reference in New Issue
Block a user