mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-21 19:47:20 -06:00
commit
6ac25eeb65
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.4
|
placeholder: v3.6.5
|
||||||
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.4
|
placeholder: v3.6.5
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
37
.github/ISSUE_TEMPLATE/translation.yaml
vendored
Normal file
37
.github/ISSUE_TEMPLATE/translation.yaml
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: 🌍 Translation
|
||||||
|
description: Request support for a new language in the user interface
|
||||||
|
labels: ["type: translation"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: >
|
||||||
|
**NOTE:** This template is used only for proposing the addition of *new* languages. Please do
|
||||||
|
not use it to request changes to existing translations.
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Language
|
||||||
|
description: What is the name of the language in English?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: ISO 639-1 code
|
||||||
|
description: >
|
||||||
|
What is the two-letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
|
||||||
|
assigned to the language?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Volunteer
|
||||||
|
description: Are you a fluent speaker of this language **and** willing to contribute a translation map?
|
||||||
|
options:
|
||||||
|
- "Yes"
|
||||||
|
- "No"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Comments
|
||||||
|
description: Any other notes you would like to share
|
@ -53,7 +53,8 @@ django-tables2
|
|||||||
|
|
||||||
# User-defined tags for objects
|
# User-defined tags for objects
|
||||||
# https://github.com/jazzband/django-taggit/blob/master/CHANGELOG.rst
|
# https://github.com/jazzband/django-taggit/blob/master/CHANGELOG.rst
|
||||||
django-taggit
|
# TODO: Upgrade to v5.0 for NetBox v3.7 beta
|
||||||
|
django-taggit<5.0
|
||||||
|
|
||||||
# A Django field for representing time zones
|
# A Django field for representing time zones
|
||||||
# https://github.com/mfogel/django-timezone-field/
|
# https://github.com/mfogel/django-timezone-field/
|
||||||
|
@ -116,7 +116,7 @@ Multiple conditions can be combined into nested sets using AND or OR logic. This
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"attr": "tags",
|
"attr": "tags.slug",
|
||||||
"value": "exempt",
|
"value": "exempt",
|
||||||
"op": "contains"
|
"op": "contains"
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,35 @@
|
|||||||
# NetBox v3.6
|
# NetBox v3.6
|
||||||
|
|
||||||
|
## v3.6.5 (2023-11-09)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#12741](https://github.com/netbox-community/netbox/issues/12741) - Add selector widget to platform field on device & virtual machine forms
|
||||||
|
* [#13022](https://github.com/netbox-community/netbox/issues/13022) - Introduce support for assigning IP addresses when bulk importing services
|
||||||
|
* [#13587](https://github.com/netbox-community/netbox/issues/13587) - Annotate units of measurement on power port table columns
|
||||||
|
* [#13669](https://github.com/netbox-community/netbox/issues/13669) - Add bulk import button to contact assignments list view
|
||||||
|
* [#13723](https://github.com/netbox-community/netbox/issues/13723) - Add inventory items column to interfaces table
|
||||||
|
* [#13743](https://github.com/netbox-community/netbox/issues/13743) - Add site column to power feeds table
|
||||||
|
* [#13936](https://github.com/netbox-community/netbox/issues/13936) - Add primary IPv4 and IPv6 filters for virtual machines and VDCs
|
||||||
|
* [#13951](https://github.com/netbox-community/netbox/issues/13951) - Add device & virtual machine fields to service filter form
|
||||||
|
* [#14085](https://github.com/netbox-community/netbox/issues/14085) - Strip trailing port number from value returned by `get_client_ip()`
|
||||||
|
* [#14101](https://github.com/netbox-community/netbox/issues/14101) - Add greater/less than mask length filters for IP addresses
|
||||||
|
* [#14112](https://github.com/netbox-community/netbox/issues/14112) - Add tab listing child items under inventory item view
|
||||||
|
* [#14113](https://github.com/netbox-community/netbox/issues/14113) - Add optional parent column to inventory items table
|
||||||
|
* [#14220](https://github.com/netbox-community/netbox/issues/14220) - Order available columns alphabetically in table configuration form
|
||||||
|
* [#14221](https://github.com/netbox-community/netbox/issues/14221) - Add contact group column on contact assignments table
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#14033](https://github.com/netbox-community/netbox/issues/14033) - Avoid exception when attempting to connect both ends of a cable to the same object
|
||||||
|
* [#14117](https://github.com/netbox-community/netbox/issues/14117) - Check that enough rear port positions have been selected to accommodate the number of front ports being created
|
||||||
|
* [#14166](https://github.com/netbox-community/netbox/issues/14166) - Permit user login when maintenance mode is enabled
|
||||||
|
* [#14182](https://github.com/netbox-community/netbox/issues/14182) - Ensure the active configuration is restored upon clearing cache
|
||||||
|
* [#14195](https://github.com/netbox-community/netbox/issues/14195) - Correct permissions evaluation for ASN range child ASNs view
|
||||||
|
* [#14223](https://github.com/netbox-community/netbox/issues/14223) - Disable ordering of jobs by assigned object
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v3.6.4 (2023-10-17)
|
## v3.6.4 (2023-10-17)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
@ -1,11 +1,20 @@
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from extras.models import ConfigRevision
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""Command to clear the entire cache."""
|
"""Command to clear the entire cache."""
|
||||||
help = 'Clears the cache.'
|
help = 'Clears the cache.'
|
||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
|
# Fetch the current config revision from the cache
|
||||||
|
config_version = cache.get('config_version')
|
||||||
|
# Clear the cache
|
||||||
cache.clear()
|
cache.clear()
|
||||||
self.stdout.write('Cache has been cleared.', ending="\n")
|
self.stdout.write('Cache has been cleared.', ending="\n")
|
||||||
|
if config_version:
|
||||||
|
# Activate the current config revision
|
||||||
|
ConfigRevision.objects.get(id=config_version).activate()
|
||||||
|
self.stdout.write(f'Config revision ({config_version}) has been restored.', ending="\n")
|
||||||
|
@ -19,7 +19,8 @@ class JobTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
object = tables.Column(
|
object = tables.Column(
|
||||||
verbose_name=_('Object'),
|
verbose_name=_('Object'),
|
||||||
linkify=True
|
linkify=True,
|
||||||
|
orderable=False
|
||||||
)
|
)
|
||||||
status = columns.ChoiceFieldColumn(
|
status = columns.ChoiceFieldColumn(
|
||||||
verbose_name=_('Status'),
|
verbose_name=_('Status'),
|
||||||
|
@ -4,6 +4,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.models import ASN, L2VPN, IPAddress, VRF
|
from ipam.models import ASN, L2VPN, IPAddress, VRF
|
||||||
from netbox.filtersets import (
|
from netbox.filtersets import (
|
||||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
||||||
@ -817,7 +818,13 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
|
|||||||
fields = ['id', 'name', 'slug', 'description']
|
fields = ['id', 'name', 'slug', 'description']
|
||||||
|
|
||||||
|
|
||||||
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
|
class DeviceFilterSet(
|
||||||
|
NetBoxModelFilterSet,
|
||||||
|
TenancyFilterSet,
|
||||||
|
ContactModelFilterSet,
|
||||||
|
LocalConfigContextFilterSet,
|
||||||
|
PrimaryIPFilterSet,
|
||||||
|
):
|
||||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='device_type__manufacturer',
|
field_name='device_type__manufacturer',
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
@ -993,16 +1000,6 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
|
|||||||
method='_device_bays',
|
method='_device_bays',
|
||||||
label=_('Has device bays'),
|
label=_('Has device bays'),
|
||||||
)
|
)
|
||||||
primary_ip4_id = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='primary_ip4',
|
|
||||||
queryset=IPAddress.objects.all(),
|
|
||||||
label=_('Primary IPv4 (ID)'),
|
|
||||||
)
|
|
||||||
primary_ip6_id = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='primary_ip6',
|
|
||||||
queryset=IPAddress.objects.all(),
|
|
||||||
label=_('Primary IPv6 (ID)'),
|
|
||||||
)
|
|
||||||
oob_ip_id = django_filters.ModelMultipleChoiceFilter(
|
oob_ip_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='oob_ip',
|
field_name='oob_ip',
|
||||||
queryset=IPAddress.objects.all(),
|
queryset=IPAddress.objects.all(),
|
||||||
@ -1069,7 +1066,7 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
|
|||||||
return queryset.exclude(devicebays__isnull=value)
|
return queryset.exclude(devicebays__isnull=value)
|
||||||
|
|
||||||
|
|
||||||
class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet, PrimaryIPFilterSet):
|
||||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='device',
|
field_name='device',
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
|
@ -442,7 +442,8 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
|||||||
platform = DynamicModelChoiceField(
|
platform = DynamicModelChoiceField(
|
||||||
label=_('Platform'),
|
label=_('Platform'),
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
selector=True
|
||||||
)
|
)
|
||||||
cluster = DynamicModelChoiceField(
|
cluster = DynamicModelChoiceField(
|
||||||
label=_('Cluster'),
|
label=_('Cluster'),
|
||||||
|
@ -151,6 +151,23 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
|
|||||||
)
|
)
|
||||||
self.fields['rear_port'].choices = choices
|
self.fields['rear_port'].choices = choices
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
|
||||||
|
# positions
|
||||||
|
frontport_count = len(self.cleaned_data['name'])
|
||||||
|
rearport_count = len(self.cleaned_data['rear_port'])
|
||||||
|
if frontport_count != rearport_count:
|
||||||
|
raise forms.ValidationError({
|
||||||
|
'rear_port': _(
|
||||||
|
"The number of front port templates to be created ({frontport_count}) must match the selected "
|
||||||
|
"number of rear port positions ({rearport_count})."
|
||||||
|
).format(
|
||||||
|
frontport_count=frontport_count,
|
||||||
|
rearport_count=rearport_count
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
def get_iterative_data(self, iteration):
|
def get_iterative_data(self, iteration):
|
||||||
|
|
||||||
# Assign rear port and position from selected set
|
# Assign rear port and position from selected set
|
||||||
@ -291,6 +308,22 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
|||||||
)
|
)
|
||||||
self.fields['rear_port'].choices = choices
|
self.fields['rear_port'].choices = choices
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Check that the number of FrontPorts to be created matches the selected number of RearPort positions
|
||||||
|
frontport_count = len(self.cleaned_data['name'])
|
||||||
|
rearport_count = len(self.cleaned_data['rear_port'])
|
||||||
|
if frontport_count != rearport_count:
|
||||||
|
raise forms.ValidationError({
|
||||||
|
'rear_port': _(
|
||||||
|
"The number of front ports to be created ({frontport_count}) must match the selected number of "
|
||||||
|
"rear port positions ({rearport_count})."
|
||||||
|
).format(
|
||||||
|
frontport_count=frontport_count,
|
||||||
|
rearport_count=rearport_count
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
def get_iterative_data(self, iteration):
|
def get_iterative_data(self, iteration):
|
||||||
|
|
||||||
# Assign rear port and position from selected set
|
# Assign rear port and position from selected set
|
||||||
|
@ -180,6 +180,17 @@ class Cable(PrimaryModel):
|
|||||||
if b_type not in COMPATIBLE_TERMINATION_TYPES.get(a_type):
|
if b_type not in COMPATIBLE_TERMINATION_TYPES.get(a_type):
|
||||||
raise ValidationError(f"Incompatible termination types: {a_type} and {b_type}")
|
raise ValidationError(f"Incompatible termination types: {a_type} and {b_type}")
|
||||||
|
|
||||||
|
if a_type == b_type:
|
||||||
|
# can't directly use self.a_terminations here as possible they
|
||||||
|
# don't have pk yet
|
||||||
|
a_pks = set(obj.pk for obj in self.a_terminations if obj.pk)
|
||||||
|
b_pks = set(obj.pk for obj in self.b_terminations if obj.pk)
|
||||||
|
|
||||||
|
if (a_pks & b_pks):
|
||||||
|
raise ValidationError(
|
||||||
|
_("A and B terminations cannot connect to the same object.")
|
||||||
|
)
|
||||||
|
|
||||||
# Run clean() on any new CableTerminations
|
# Run clean() on any new CableTerminations
|
||||||
for termination in self.a_terminations:
|
for termination in self.a_terminations:
|
||||||
CableTermination(cable=self, cable_end='A', termination=termination).clean()
|
CableTermination(cable=self, cable_end='A', termination=termination).clean()
|
||||||
|
@ -466,6 +466,12 @@ class PowerPortTable(ModularDeviceComponentTable, PathEndpointTable):
|
|||||||
'args': [Accessor('device_id')],
|
'args': [Accessor('device_id')],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
maximum_draw = tables.Column(
|
||||||
|
verbose_name=_('Maximum draw (W)')
|
||||||
|
)
|
||||||
|
allocated_draw = tables.Column(
|
||||||
|
verbose_name=_('Allocated draw (W)')
|
||||||
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:powerport_list'
|
url_name='dcim:powerport_list'
|
||||||
)
|
)
|
||||||
@ -625,6 +631,10 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
|
|||||||
verbose_name=_('VRF'),
|
verbose_name=_('VRF'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
inventory_items = tables.ManyToManyColumn(
|
||||||
|
linkify_item=True,
|
||||||
|
verbose_name=_('Inventory Items'),
|
||||||
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:interface_list'
|
url_name='dcim:interface_list'
|
||||||
)
|
)
|
||||||
@ -636,7 +646,7 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
|
|||||||
'speed', 'speed_formatted', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
|
'speed', 'speed_formatted', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
|
||||||
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
|
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
|
||||||
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn',
|
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn',
|
||||||
'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
|
'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'inventory_items', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||||
|
|
||||||
@ -933,6 +943,10 @@ class InventoryItemTable(DeviceComponentTable):
|
|||||||
discovered = columns.BooleanColumn(
|
discovered = columns.BooleanColumn(
|
||||||
verbose_name=_('Discovered'),
|
verbose_name=_('Discovered'),
|
||||||
)
|
)
|
||||||
|
parent = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
verbose_name=_('Parent'),
|
||||||
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:inventoryitem_list'
|
url_name='dcim:inventoryitem_list'
|
||||||
)
|
)
|
||||||
@ -941,7 +955,7 @@ class InventoryItemTable(DeviceComponentTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = models.InventoryItem
|
model = models.InventoryItem
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
|
'pk', 'id', 'name', 'device', 'parent', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
|
||||||
'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated',
|
'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
|
@ -87,6 +87,11 @@ class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
|
|||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name=_('Tenant')
|
verbose_name=_('Tenant')
|
||||||
)
|
)
|
||||||
|
site = tables.Column(
|
||||||
|
accessor='rack__site',
|
||||||
|
linkify=True,
|
||||||
|
verbose_name=_('Site'),
|
||||||
|
)
|
||||||
comments = columns.MarkdownColumn(
|
comments = columns.MarkdownColumn(
|
||||||
verbose_name=_('Comments'),
|
verbose_name=_('Comments'),
|
||||||
)
|
)
|
||||||
@ -97,9 +102,9 @@ class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = PowerFeed
|
model = PowerFeed
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
'pk', 'id', 'name', 'power_panel', 'site', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage',
|
||||||
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power', 'tenant',
|
'phase', 'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power',
|
||||||
'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
|
'tenant', 'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
|
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
|
||||||
|
@ -4712,12 +4712,18 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
addresses = (
|
addresses = (
|
||||||
IPAddress(assigned_object=interfaces[0], address='10.1.1.1/24'),
|
IPAddress(assigned_object=interfaces[0], address='10.1.1.1/24'),
|
||||||
IPAddress(assigned_object=interfaces[1], address='10.1.1.2/24'),
|
IPAddress(assigned_object=interfaces[1], address='10.1.1.2/24'),
|
||||||
|
IPAddress(assigned_object=None, address='10.1.1.3/24'),
|
||||||
|
IPAddress(assigned_object=interfaces[0], address='2001:db8::1/64'),
|
||||||
|
IPAddress(assigned_object=interfaces[1], address='2001:db8::2/64'),
|
||||||
|
IPAddress(assigned_object=None, address='2001:db8::3/64'),
|
||||||
)
|
)
|
||||||
IPAddress.objects.bulk_create(addresses)
|
IPAddress.objects.bulk_create(addresses)
|
||||||
|
|
||||||
vdcs[0].primary_ip4 = addresses[0]
|
vdcs[0].primary_ip4 = addresses[0]
|
||||||
|
vdcs[0].primary_ip6 = addresses[3]
|
||||||
vdcs[0].save()
|
vdcs[0].save()
|
||||||
vdcs[1].primary_ip4 = addresses[1]
|
vdcs[1].primary_ip4 = addresses[1]
|
||||||
|
vdcs[1].primary_ip6 = addresses[4]
|
||||||
vdcs[1].save()
|
vdcs[1].save()
|
||||||
|
|
||||||
def test_device(self):
|
def test_device(self):
|
||||||
@ -4738,3 +4744,17 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
params = {'has_primary_ip': False}
|
params = {'has_primary_ip': False}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_primary_ip4(self):
|
||||||
|
addresses = IPAddress.objects.filter(address__family=4)
|
||||||
|
params = {'primary_ip4_id': [addresses[0].pk, addresses[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'primary_ip4_id': [addresses[2].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
|
||||||
|
|
||||||
|
def test_primary_ip6(self):
|
||||||
|
addresses = IPAddress.objects.filter(address__family=6)
|
||||||
|
params = {'primary_ip6_id': [addresses[0].pk, addresses[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'primary_ip6_id': [addresses[2].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
|
||||||
|
@ -2993,6 +2993,25 @@ class InventoryItemBulkDeleteView(generic.BulkDeleteView):
|
|||||||
template_name = 'dcim/inventoryitem_bulk_delete.html'
|
template_name = 'dcim/inventoryitem_bulk_delete.html'
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(InventoryItem, 'children')
|
||||||
|
class InventoryItemChildrenView(generic.ObjectChildrenView):
|
||||||
|
queryset = InventoryItem.objects.all()
|
||||||
|
child_model = InventoryItem
|
||||||
|
table = tables.InventoryItemTable
|
||||||
|
filterset = filtersets.InventoryItemFilterSet
|
||||||
|
template_name = 'generic/object_children.html'
|
||||||
|
tab = ViewTab(
|
||||||
|
label=_('Children'),
|
||||||
|
badge=lambda obj: obj.child_items.count(),
|
||||||
|
permission='dcim.view_inventoryitem',
|
||||||
|
hide_if_empty=True,
|
||||||
|
weight=5000
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_children(self, request, parent):
|
||||||
|
return parent.child_items.restrict(request.user, 'view')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Inventory item roles
|
# Inventory item roles
|
||||||
#
|
#
|
||||||
|
@ -457,7 +457,7 @@ class ConfigContextTestCase(
|
|||||||
'platforms': [],
|
'platforms': [],
|
||||||
'tenant_groups': [],
|
'tenant_groups': [],
|
||||||
'tenants': [],
|
'tenants': [],
|
||||||
'device_types': [devicetype.id,],
|
'device_types': [devicetype.id],
|
||||||
'tags': [],
|
'tags': [],
|
||||||
'data': '{"foo": 123}',
|
'data': '{"foo": 123}',
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ __all__ = (
|
|||||||
'L2VPNFilterSet',
|
'L2VPNFilterSet',
|
||||||
'L2VPNTerminationFilterSet',
|
'L2VPNTerminationFilterSet',
|
||||||
'PrefixFilterSet',
|
'PrefixFilterSet',
|
||||||
|
'PrimaryIPFilterSet',
|
||||||
'RIRFilterSet',
|
'RIRFilterSet',
|
||||||
'RoleFilterSet',
|
'RoleFilterSet',
|
||||||
'RouteTargetFilterSet',
|
'RouteTargetFilterSet',
|
||||||
@ -266,7 +267,8 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
)
|
)
|
||||||
mask_length = MultiValueNumberFilter(
|
mask_length = MultiValueNumberFilter(
|
||||||
field_name='prefix',
|
field_name='prefix',
|
||||||
lookup_expr='net_mask_length'
|
lookup_expr='net_mask_length',
|
||||||
|
label=_('Mask length')
|
||||||
)
|
)
|
||||||
mask_length__gte = django_filters.NumberFilter(
|
mask_length__gte = django_filters.NumberFilter(
|
||||||
field_name='prefix',
|
field_name='prefix',
|
||||||
@ -531,9 +533,18 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
method='filter_address',
|
method='filter_address',
|
||||||
label=_('Address'),
|
label=_('Address'),
|
||||||
)
|
)
|
||||||
mask_length = django_filters.NumberFilter(
|
mask_length = MultiValueNumberFilter(
|
||||||
method='filter_mask_length',
|
field_name='address',
|
||||||
label=_('Mask length'),
|
lookup_expr='net_mask_length',
|
||||||
|
label=_('Mask length')
|
||||||
|
)
|
||||||
|
mask_length__gte = django_filters.NumberFilter(
|
||||||
|
field_name='address',
|
||||||
|
lookup_expr='net_mask_length__gte'
|
||||||
|
)
|
||||||
|
mask_length__lte = django_filters.NumberFilter(
|
||||||
|
field_name='address',
|
||||||
|
lookup_expr='net_mask_length__lte'
|
||||||
)
|
)
|
||||||
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
@ -677,11 +688,6 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
except ValidationError:
|
except ValidationError:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
def filter_mask_length(self, queryset, name, value):
|
|
||||||
if not value:
|
|
||||||
return queryset
|
|
||||||
return queryset.filter(address__net_mask_length=value)
|
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.STR)
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def filter_present_in_vrf(self, queryset, name, vrf):
|
def filter_present_in_vrf(self, queryset, name, vrf):
|
||||||
if vrf is None:
|
if vrf is None:
|
||||||
@ -1227,3 +1233,19 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
class PrimaryIPFilterSet(django_filters.FilterSet):
|
||||||
|
"""
|
||||||
|
An inheritable FilterSet for models which support primary IP assignment.
|
||||||
|
"""
|
||||||
|
primary_ip4_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='primary_ip4',
|
||||||
|
queryset=IPAddress.objects.all(),
|
||||||
|
label=_('Primary IPv4 (ID)'),
|
||||||
|
)
|
||||||
|
primary_ip6_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='primary_ip6',
|
||||||
|
queryset=IPAddress.objects.all(),
|
||||||
|
label=_('Primary IPv6 (ID)'),
|
||||||
|
)
|
||||||
|
@ -507,10 +507,28 @@ class ServiceImportForm(NetBoxModelImportForm):
|
|||||||
choices=ServiceProtocolChoices,
|
choices=ServiceProtocolChoices,
|
||||||
help_text=_('IP protocol')
|
help_text=_('IP protocol')
|
||||||
)
|
)
|
||||||
|
ipaddresses = CSVModelMultipleChoiceField(
|
||||||
|
queryset=IPAddress.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='address',
|
||||||
|
help_text=_('IP Address'),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Service
|
model = Service
|
||||||
fields = ('device', 'virtual_machine', 'name', 'protocol', 'ports', 'description', 'comments', 'tags')
|
fields = (
|
||||||
|
'device', 'virtual_machine', 'ipaddresses', 'name', 'protocol', 'ports', 'description', 'comments', 'tags',
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_ipaddresses(self):
|
||||||
|
parent = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine')
|
||||||
|
for ip_address in self.cleaned_data['ipaddresses']:
|
||||||
|
if not ip_address.assigned_object or getattr(ip_address.assigned_object, 'parent_object') != parent:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_("{ip} is not assigned to this device/VM.").format(ip=ip_address)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.cleaned_data['ipaddresses']
|
||||||
|
|
||||||
|
|
||||||
class L2VPNImportForm(NetBoxModelImportForm):
|
class L2VPNImportForm(NetBoxModelImportForm):
|
||||||
|
@ -523,6 +523,21 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
|
|||||||
|
|
||||||
class ServiceFilterForm(ServiceTemplateFilterForm):
|
class ServiceFilterForm(ServiceTemplateFilterForm):
|
||||||
model = Service
|
model = Service
|
||||||
|
fieldsets = (
|
||||||
|
(None, ('q', 'filter_id', 'tag')),
|
||||||
|
(_('Attributes'), ('protocol', 'port')),
|
||||||
|
(_('Assignment'), ('device_id', 'virtual_machine_id')),
|
||||||
|
)
|
||||||
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Device'),
|
||||||
|
)
|
||||||
|
virtual_machine_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=VirtualMachine.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Virtual Machine'),
|
||||||
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
@ -627,8 +627,12 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_mask_length(self):
|
def test_mask_length(self):
|
||||||
params = {'mask_length': ['24']}
|
params = {'mask_length': [24]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
params = {'mask_length__gte': 32}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||||
|
params = {'mask_length__lte': 24}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||||
|
|
||||||
def test_vrf(self):
|
def test_vrf(self):
|
||||||
vrfs = VRF.objects.all()[:2]
|
vrfs = VRF.objects.all()[:2]
|
||||||
@ -954,8 +958,12 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_mask_length(self):
|
def test_mask_length(self):
|
||||||
params = {'mask_length': '24'}
|
params = {'mask_length': [24]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||||
|
params = {'mask_length__gte': 64}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
|
params = {'mask_length__lte': 25}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
|
|
||||||
def test_vrf(self):
|
def test_vrf(self):
|
||||||
vrfs = VRF.objects.all()[:2]
|
vrfs = VRF.objects.all()[:2]
|
||||||
|
@ -4,6 +4,7 @@ from django.test import override_settings
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from netaddr import IPNetwork
|
from netaddr import IPNetwork
|
||||||
|
|
||||||
|
from dcim.constants import InterfaceTypeChoices
|
||||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site, Interface
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site, Interface
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
@ -911,6 +912,7 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
|
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
|
||||||
role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||||
device = Device.objects.create(name='Device 1', site=site, device_type=devicetype, role=role)
|
device = Device.objects.create(name='Device 1', site=site, device_type=devicetype, role=role)
|
||||||
|
interface = Interface.objects.create(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL)
|
||||||
|
|
||||||
services = (
|
services = (
|
||||||
Service(device=device, name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[101]),
|
Service(device=device, name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[101]),
|
||||||
@ -919,6 +921,12 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
)
|
)
|
||||||
Service.objects.bulk_create(services)
|
Service.objects.bulk_create(services)
|
||||||
|
|
||||||
|
ip_addresses = (
|
||||||
|
IPAddress(assigned_object=interface, address='192.0.2.1/24'),
|
||||||
|
IPAddress(assigned_object=interface, address='192.0.2.2/24'),
|
||||||
|
)
|
||||||
|
IPAddress.objects.bulk_create(ip_addresses)
|
||||||
|
|
||||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
@ -933,10 +941,10 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"device,name,protocol,ports,description",
|
"device,name,protocol,ports,ipaddresses,description",
|
||||||
"Device 1,Service 1,tcp,1,First service",
|
"Device 1,Service 1,tcp,1,192.0.2.1/24,First service",
|
||||||
"Device 1,Service 2,tcp,2,Second service",
|
"Device 1,Service 2,tcp,2,192.0.2.2/24,Second service",
|
||||||
"Device 1,Service 3,udp,3,Third service",
|
"Device 1,Service 3,udp,3,,Third service",
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.csv_update_data = (
|
cls.csv_update_data = (
|
||||||
|
@ -220,7 +220,7 @@ class ASNRangeASNsView(generic.ObjectChildrenView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('ASNs'),
|
label=_('ASNs'),
|
||||||
badge=lambda x: x.get_child_asns().count(),
|
badge=lambda x: x.get_child_asns().count(),
|
||||||
permission='ipam.view_asns',
|
permission='ipam.view_asn',
|
||||||
weight=500
|
weight=500
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.6.4'
|
VERSION = '3.6.5'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@ -502,6 +502,9 @@ AUTH_EXEMPT_PATHS = (
|
|||||||
MAINTENANCE_EXEMPT_PATHS = (
|
MAINTENANCE_EXEMPT_PATHS = (
|
||||||
f'/{BASE_PATH}admin/',
|
f'/{BASE_PATH}admin/',
|
||||||
f'/{BASE_PATH}extras/config-revisions/', # Allow modifying the configuration
|
f'/{BASE_PATH}extras/config-revisions/', # Allow modifying the configuration
|
||||||
|
LOGIN_URL,
|
||||||
|
LOGIN_REDIRECT_URL,
|
||||||
|
LOGOUT_REDIRECT_URL
|
||||||
)
|
)
|
||||||
|
|
||||||
SERIALIZATION_MODULES = {
|
SERIALIZATION_MODULES = {
|
||||||
|
@ -119,7 +119,7 @@ class BaseTable(tables.Table):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def available_columns(self):
|
def available_columns(self):
|
||||||
return self._get_columns(visible=False)
|
return sorted(self._get_columns(visible=False))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def selected_columns(self):
|
def selected_columns(self):
|
||||||
|
@ -102,6 +102,11 @@ class ContactAssignmentTable(NetBoxTable):
|
|||||||
verbose_name=_('Role'),
|
verbose_name=_('Role'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
contact_group = tables.Column(
|
||||||
|
accessor=Accessor('contact__group'),
|
||||||
|
verbose_name=_('Group'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
contact_title = tables.Column(
|
contact_title = tables.Column(
|
||||||
accessor=Accessor('contact__title'),
|
accessor=Accessor('contact__title'),
|
||||||
verbose_name=_('Contact Title')
|
verbose_name=_('Contact Title')
|
||||||
@ -137,7 +142,8 @@ class ContactAssignmentTable(NetBoxTable):
|
|||||||
model = ContactAssignment
|
model = ContactAssignment
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'content_type', 'object', 'contact', 'role', 'priority', 'contact_title', 'contact_phone',
|
'pk', 'content_type', 'object', 'contact', 'role', 'priority', 'contact_title', 'contact_phone',
|
||||||
'contact_email', 'contact_address', 'contact_link', 'contact_description', 'tags', 'actions'
|
'contact_email', 'contact_address', 'contact_link', 'contact_description', 'contact_group', 'tags',
|
||||||
|
'actions'
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'content_type', 'object', 'contact', 'role', 'priority', 'contact_email', 'contact_phone'
|
'pk', 'content_type', 'object', 'contact', 'role', 'priority', 'contact_email', 'contact_phone'
|
||||||
|
@ -386,7 +386,7 @@ class ContactAssignmentListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.ContactAssignmentFilterSet
|
filterset = filtersets.ContactAssignmentFilterSet
|
||||||
filterset_form = forms.ContactAssignmentFilterForm
|
filterset_form = forms.ContactAssignmentFilterForm
|
||||||
table = tables.ContactAssignmentTable
|
table = tables.ContactAssignmentTable
|
||||||
actions = ('export', 'bulk_edit', 'bulk_delete')
|
actions = ('export', 'bulk_edit', 'bulk_delete', 'import')
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ContactAssignment, 'edit')
|
@register_model_view(ContactAssignment, 'edit')
|
||||||
|
@ -17,7 +17,7 @@ def get_client_ip(request, additional_headers=()):
|
|||||||
)
|
)
|
||||||
for header in HTTP_HEADERS:
|
for header in HTTP_HEADERS:
|
||||||
if header in request.META:
|
if header in request.META:
|
||||||
client_ip = request.META[header].split(',')[0]
|
client_ip = request.META[header].split(',')[0].partition(':')[0]
|
||||||
try:
|
try:
|
||||||
return IPAddress(client_ip)
|
return IPAddress(client_ip)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -6,6 +6,7 @@ from dcim.filtersets import CommonInterfaceFilterSet
|
|||||||
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||||
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 netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
|
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
|
||||||
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
||||||
from utilities.filters import MultiValueCharFilter, MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
|
from utilities.filters import MultiValueCharFilter, MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
|
||||||
@ -114,7 +115,8 @@ class VirtualMachineFilterSet(
|
|||||||
NetBoxModelFilterSet,
|
NetBoxModelFilterSet,
|
||||||
TenancyFilterSet,
|
TenancyFilterSet,
|
||||||
ContactModelFilterSet,
|
ContactModelFilterSet,
|
||||||
LocalConfigContextFilterSet
|
LocalConfigContextFilterSet,
|
||||||
|
PrimaryIPFilterSet,
|
||||||
):
|
):
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=VirtualMachineStatusChoices,
|
choices=VirtualMachineStatusChoices,
|
||||||
|
@ -200,7 +200,8 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
|
|||||||
platform = DynamicModelChoiceField(
|
platform = DynamicModelChoiceField(
|
||||||
label=_('Platform'),
|
label=_('Platform'),
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
selector=True
|
||||||
)
|
)
|
||||||
local_context_data = JSONField(
|
local_context_data = JSONField(
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -291,10 +291,14 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
ipaddresses = (
|
ipaddresses = (
|
||||||
IPAddress(address='192.0.2.1/24', assigned_object=interfaces[0]),
|
IPAddress(address='192.0.2.1/24', assigned_object=interfaces[0]),
|
||||||
IPAddress(address='192.0.2.2/24', assigned_object=interfaces[1]),
|
IPAddress(address='192.0.2.2/24', assigned_object=interfaces[1]),
|
||||||
|
IPAddress(address='192.0.2.3/24', assigned_object=None),
|
||||||
|
IPAddress(address='2001:db8::1/64', assigned_object=interfaces[0]),
|
||||||
|
IPAddress(address='2001:db8::2/64', assigned_object=interfaces[1]),
|
||||||
|
IPAddress(address='2001:db8::3/64', assigned_object=None),
|
||||||
)
|
)
|
||||||
IPAddress.objects.bulk_create(ipaddresses)
|
IPAddress.objects.bulk_create(ipaddresses)
|
||||||
VirtualMachine.objects.filter(pk=vms[0].pk).update(primary_ip4=ipaddresses[0])
|
VirtualMachine.objects.filter(pk=vms[0].pk).update(primary_ip4=ipaddresses[0], primary_ip6=ipaddresses[3])
|
||||||
VirtualMachine.objects.filter(pk=vms[1].pk).update(primary_ip4=ipaddresses[1])
|
VirtualMachine.objects.filter(pk=vms[1].pk).update(primary_ip4=ipaddresses[1], primary_ip6=ipaddresses[4])
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
params = {'name': ['Virtual Machine 1', 'Virtual Machine 2']}
|
params = {'name': ['Virtual Machine 1', 'Virtual Machine 2']}
|
||||||
@ -412,6 +416,20 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_primary_ip4(self):
|
||||||
|
addresses = IPAddress.objects.filter(address__family=4)
|
||||||
|
params = {'primary_ip4_id': [addresses[0].pk, addresses[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'primary_ip4_id': [addresses[2].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
|
||||||
|
|
||||||
|
def test_primary_ip6(self):
|
||||||
|
addresses = IPAddress.objects.filter(address__family=6)
|
||||||
|
params = {'primary_ip6_id': [addresses[0].pk, addresses[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'primary_ip6_id': [addresses[2].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = VMInterface.objects.all()
|
queryset = VMInterface.objects.all()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
bleach==6.1.0
|
bleach==6.1.0
|
||||||
Django==4.2.6
|
Django==4.2.7
|
||||||
django-cors-headers==4.3.0
|
django-cors-headers==4.3.0
|
||||||
django-debug-toolbar==4.2.0
|
django-debug-toolbar==4.2.0
|
||||||
django-filter==23.3
|
django-filter==23.3
|
||||||
@ -21,16 +21,16 @@ 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.6
|
mkdocs-material==9.4.8
|
||||||
mkdocstrings[python-legacy]==0.23.0
|
mkdocstrings[python-legacy]==0.23.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.12
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
sentry-sdk==1.32.0
|
sentry-sdk==1.34.0
|
||||||
social-auth-app-django==5.4.0
|
social-auth-app-django==5.4.0
|
||||||
social-auth-core[openidconnect]==4.4.2
|
social-auth-core[openidconnect]==4.5.0
|
||||||
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