mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-23 02:18:05 +01:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c0672550a | ||
|
|
199685d98b | ||
|
|
3ef2db81e8 | ||
|
|
3bacee16bd | ||
|
|
45c646dcec | ||
|
|
fedcbaf4c8 | ||
|
|
359c0cf3a0 | ||
|
|
46b933a5aa | ||
|
|
07da3f6d33 | ||
|
|
0613e8e95c | ||
|
|
113c60a44a | ||
|
|
8a237561ef | ||
|
|
cc0fc03ec3 | ||
|
|
b955751349 | ||
|
|
d6c8d1581c | ||
|
|
e6642b5f5b | ||
|
|
a67236fc3c | ||
|
|
634681a72e | ||
|
|
031b7540b3 | ||
|
|
43909ee33f | ||
|
|
99467e8f66 | ||
|
|
0d08205ab1 | ||
|
|
c289dda649 | ||
|
|
169207058f | ||
|
|
e5c565cbf4 | ||
|
|
f0b9008529 | ||
|
|
8dfec7e2b2 | ||
|
|
c1cf037eaf | ||
|
|
3f4a65cc5c | ||
|
|
12beac4f1a | ||
|
|
ec245b968f |
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -23,7 +23,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.6.7
|
||||
placeholder: v3.6.9
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.6.7
|
||||
placeholder: v3.6.9
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@@ -8,6 +8,9 @@ When entering a search query, the user can choose a specific lookup type: exact
|
||||
|
||||
Custom fields defined by NetBox administrators are also included in search results if configured with a search weight. Additionally, NetBox plugins can register their own custom models for inclusion alongside core models.
|
||||
|
||||
!!! note
|
||||
NetBox does not index any static choice field's (including custom fields of type "Selection" or "Multiple selection").
|
||||
|
||||
## Saved Filters
|
||||
|
||||
Each type of object in NetBox is accompanied by an extensive set of filters, each tied to a specific attribute, which enable the creation of complex queries. Often you'll find that certain queries are used routinely to apply some set of prescribed conditions to a query. Once a set of filters has been applied, NetBox offers the option to save it for future use.
|
||||
|
||||
@@ -19,7 +19,7 @@ The parent inventory item to which this item is assigned (optional).
|
||||
|
||||
### Name
|
||||
|
||||
The inventory item's name. Must be unique to the parent device.
|
||||
The inventory item's name. If the inventory item is assigned to a parent item, its name must be unique among its siblings (all items belonging to the same parent item).
|
||||
|
||||
### Label
|
||||
|
||||
|
||||
@@ -1,5 +1,45 @@
|
||||
# NetBox v3.6
|
||||
|
||||
## v3.6.9 (2023-12-28)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#14631](https://github.com/netbox-community/netbox/issues/14631) - All models can be filtered and searched by their description field (where applicable)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#14482](https://github.com/netbox-community/netbox/issues/14482) - Fix validation error when attempting to move a primary IP address to a new parent object
|
||||
* [#14620](https://github.com/netbox-community/netbox/issues/14620) - Permit setting device type U height to 0 during bulk edit
|
||||
* [#14621](https://github.com/netbox-community/netbox/issues/14621) - Fix error when using the device search filter
|
||||
|
||||
---
|
||||
|
||||
## v3.6.8 (2023-12-27)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#11039](https://github.com/netbox-community/netbox/issues/11039) - List parent prefixes under IP range view
|
||||
* [#14507](https://github.com/netbox-community/netbox/issues/14507) - Print new NetBox version when running upgrade script
|
||||
* [#14538](https://github.com/netbox-community/netbox/issues/14538) - Add the `available_at_site` filter for VLANs
|
||||
* [#14596](https://github.com/netbox-community/netbox/issues/14596) - Match against description field when searching for devices
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#11816](https://github.com/netbox-community/netbox/issues/11816) - Correct display of error message when attempting invalid VLAN site & group assignment
|
||||
* [#12731](https://github.com/netbox-community/netbox/issues/12731) - Fix custom validation for many-to-many fields
|
||||
* [#13606](https://github.com/netbox-community/netbox/issues/13606) - Fix filtering custom multi-choice fields by null
|
||||
* [#13649](https://github.com/netbox-community/netbox/issues/13649) - Correct calculation of absolute lengths for zero-length cables
|
||||
* [#13812](https://github.com/netbox-community/netbox/issues/13812) - Update status of remote data source when syncing fails via `syncdatasource` management command
|
||||
* [#13909](https://github.com/netbox-community/netbox/issues/13909) - Fix cloning of objects which have a multi-choice custom field
|
||||
* [#14517](https://github.com/netbox-community/netbox/issues/14517) - Ensure reservations tab is always displayed under rack view
|
||||
* [#14532](https://github.com/netbox-community/netbox/issues/14532) - Device/VM change record should accurately reflect when primary/OOB IP is deleted
|
||||
* [#14549](https://github.com/netbox-community/netbox/issues/14549) - Fix association of job results when executing scripts via `runscript` management command
|
||||
* [#14560](https://github.com/netbox-community/netbox/issues/14560) - Do not escape exclamation marks in custom link URLs
|
||||
* [#14575](https://github.com/netbox-community/netbox/issues/14575) - Fix display of the tags column under VDC table
|
||||
* [#14613](https://github.com/netbox-community/netbox/issues/14613) - Fix display of current configuration parameters in UI
|
||||
|
||||
---
|
||||
|
||||
## v3.6.7 (2023-12-15)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -67,13 +67,14 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'name', 'slug']
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(accounts__account__icontains=value) |
|
||||
Q(accounts__name__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
@@ -101,6 +102,7 @@ class ProviderAccountFilterSet(NetBoxModelFilterSet):
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(account__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
@@ -25,8 +25,8 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
ASN.objects.bulk_create(asns)
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 1', slug='provider-1', description='foobar1'),
|
||||
Provider(name='Provider 2', slug='provider-2', description='foobar2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
Provider(name='Provider 4', slug='provider-4'),
|
||||
Provider(name='Provider 5', slug='provider-5'),
|
||||
@@ -74,6 +74,10 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A'),
|
||||
))
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Provider 1', 'Provider 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -82,6 +86,10 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'slug': ['provider-1', 'provider-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_asn_id(self): # ASN object assignment
|
||||
asns = ASN.objects.all()[:2]
|
||||
params = {'asn_id': [asns[0].pk, asns[1].pk]}
|
||||
@@ -122,6 +130,10 @@ class CircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
|
||||
))
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Circuit Type 1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
@@ -227,6 +239,10 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_cid(self):
|
||||
params = {'cid': ['Test Circuit 1', 'Test Circuit 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -369,6 +385,10 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
|
||||
Cable(a_terminations=[circuit_terminations[0]], b_terminations=[circuit_terminations[1]]).save()
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_term_side(self):
|
||||
params = {'term_side': 'A'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
|
||||
@@ -440,6 +460,10 @@ class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Provider Network 1', 'Provider Network 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -477,6 +501,10 @@ class ProviderAccountTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Provider Account 1', 'Provider Account 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
@@ -26,7 +26,7 @@ class DataSourceFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = ('id', 'name', 'enabled')
|
||||
fields = ('id', 'name', 'enabled', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from core.choices import DataSourceStatusChoices
|
||||
from core.models import DataSource
|
||||
|
||||
|
||||
@@ -33,9 +34,13 @@ class Command(BaseCommand):
|
||||
for i, datasource in enumerate(datasources, start=1):
|
||||
self.stdout.write(f"[{i}] Syncing {datasource}... ", ending='')
|
||||
self.stdout.flush()
|
||||
datasource.sync()
|
||||
self.stdout.write(datasource.get_status_display())
|
||||
self.stdout.flush()
|
||||
try:
|
||||
datasource.sync()
|
||||
self.stdout.write(datasource.get_status_display())
|
||||
self.stdout.flush()
|
||||
except Exception as e:
|
||||
DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED)
|
||||
raise e
|
||||
|
||||
if len(options['name']) > 1:
|
||||
self.stdout.write(f"Finished.")
|
||||
|
||||
@@ -21,14 +21,16 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
type=DataSourceTypeChoices.LOCAL,
|
||||
source_url='file:///var/tmp/source1/',
|
||||
status=DataSourceStatusChoices.NEW,
|
||||
enabled=True
|
||||
enabled=True,
|
||||
description='foobar1'
|
||||
),
|
||||
DataSource(
|
||||
name='Data Source 2',
|
||||
type=DataSourceTypeChoices.LOCAL,
|
||||
source_url='file:///var/tmp/source2/',
|
||||
status=DataSourceStatusChoices.SYNCING,
|
||||
enabled=True
|
||||
enabled=True,
|
||||
description='foobar2'
|
||||
),
|
||||
DataSource(
|
||||
name='Data Source 3',
|
||||
@@ -40,10 +42,18 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
DataSource.objects.bulk_create(data_sources)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Data Source 1', 'Data Source 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_type(self):
|
||||
params = {'type': [DataSourceTypeChoices.LOCAL]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -97,6 +107,10 @@ class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
DataFile.objects.bulk_create(data_files)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'file1.txt'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_source(self):
|
||||
sources = DataSource.objects.all()
|
||||
params = {'source_id': [sources[0].pk, sources[1].pk]}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.contrib import messages
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
|
||||
from extras.models import ConfigRevision
|
||||
@@ -153,9 +154,11 @@ class ConfigView(generic.ObjectView):
|
||||
queryset = ConfigRevision.objects.all()
|
||||
|
||||
def get_object(self, **kwargs):
|
||||
if config := self.queryset.first():
|
||||
return config
|
||||
# Instantiate a dummy default config if none has been created yet
|
||||
return ConfigRevision(
|
||||
data=get_config().defaults
|
||||
)
|
||||
revision_id = cache.get('config_version')
|
||||
try:
|
||||
return ConfigRevision.objects.get(pk=revision_id)
|
||||
except ConfigRevision.DoesNotExist:
|
||||
# Fall back to using the active config data if no record is found
|
||||
return ConfigRevision(
|
||||
data=get_config()
|
||||
)
|
||||
|
||||
@@ -325,7 +325,7 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
||||
model = Rack
|
||||
fields = [
|
||||
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
|
||||
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit'
|
||||
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -336,6 +336,7 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
||||
Q(facility_id__icontains=value) |
|
||||
Q(serial__icontains=value.strip()) |
|
||||
Q(asset_tag__icontains=value.strip()) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
@@ -497,7 +498,8 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
|
||||
'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight',
|
||||
'weight_unit', 'description',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -507,6 +509,7 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
Q(manufacturer__name__icontains=value) |
|
||||
Q(model__icontains=value) |
|
||||
Q(part_number__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
@@ -591,7 +594,7 @@ class ModuleTypeFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
fields = ['id', 'model', 'part_number', 'weight', 'weight_unit']
|
||||
fields = ['id', 'model', 'part_number', 'weight', 'weight_unit', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -600,6 +603,7 @@ class ModuleTypeFilterSet(NetBoxModelFilterSet):
|
||||
Q(manufacturer__name__icontains=value) |
|
||||
Q(model__icontains=value) |
|
||||
Q(part_number__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
@@ -639,7 +643,10 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(name__icontains=value)
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet):
|
||||
@@ -654,21 +661,21 @@ class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['id', 'name', 'type']
|
||||
fields = ['id', 'name', 'type', 'description']
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['id', 'name', 'type']
|
||||
fields = ['id', 'name', 'type', 'description']
|
||||
|
||||
|
||||
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
|
||||
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description']
|
||||
|
||||
|
||||
class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
@@ -679,7 +686,7 @@ class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ['id', 'name', 'type', 'feed_leg']
|
||||
fields = ['id', 'name', 'type', 'feed_leg', 'description']
|
||||
|
||||
|
||||
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
@@ -703,7 +710,7 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = ['id', 'name', 'type', 'enabled', 'mgmt_only']
|
||||
fields = ['id', 'name', 'type', 'enabled', 'mgmt_only', 'description']
|
||||
|
||||
|
||||
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
@@ -714,7 +721,7 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = ['id', 'name', 'type', 'color']
|
||||
fields = ['id', 'name', 'type', 'color', 'description']
|
||||
|
||||
|
||||
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
@@ -725,21 +732,21 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = ['id', 'name', 'type', 'color', 'positions']
|
||||
fields = ['id', 'name', 'type', 'color', 'positions', 'description']
|
||||
|
||||
|
||||
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ModuleBayTemplate
|
||||
fields = ['id', 'name']
|
||||
fields = ['id', 'name', 'description']
|
||||
|
||||
|
||||
class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = ['id', 'name']
|
||||
fields = ['id', 'name', 'description']
|
||||
|
||||
|
||||
class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
@@ -772,7 +779,7 @@ class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompo
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemTemplate
|
||||
fields = ['id', 'name', 'label', 'part_id']
|
||||
fields = ['id', 'name', 'label', 'part_id', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -1008,7 +1015,10 @@ class DeviceFilterSet(
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'vc_position', 'vc_priority']
|
||||
fields = [
|
||||
'id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'vc_position', 'vc_priority',
|
||||
'description',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -1018,6 +1028,7 @@ class DeviceFilterSet(
|
||||
Q(serial__icontains=value.strip()) |
|
||||
Q(inventoryitems__serial__icontains=value.strip()) |
|
||||
Q(asset_tag__icontains=value.strip()) |
|
||||
Q(description__icontains=value.strip()) |
|
||||
Q(comments__icontains=value) |
|
||||
Q(primary_ip4__address__startswith=value) |
|
||||
Q(primary_ip6__address__startswith=value)
|
||||
@@ -1087,13 +1098,16 @@ class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet, Prim
|
||||
|
||||
class Meta:
|
||||
model = VirtualDeviceContext
|
||||
fields = ['id', 'device', 'name']
|
||||
fields = ['id', 'device', 'name', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
|
||||
qs_filter = Q(name__icontains=value)
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
try:
|
||||
qs_filter |= Q(identifier=int(value))
|
||||
except ValueError:
|
||||
@@ -1150,7 +1164,7 @@ class ModuleFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
fields = ['id', 'status', 'asset_tag']
|
||||
fields = ['id', 'status', 'asset_tag', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -1159,6 +1173,7 @@ class ModuleFilterSet(NetBoxModelFilterSet):
|
||||
Q(device__name__icontains=value.strip()) |
|
||||
Q(serial__icontains=value.strip()) |
|
||||
Q(asset_tag__icontains=value.strip()) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
@@ -1649,7 +1664,7 @@ class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemRole
|
||||
fields = ['id', 'name', 'slug', 'color']
|
||||
fields = ['id', 'name', 'slug', 'color', 'description']
|
||||
|
||||
|
||||
class VirtualChassisFilterSet(NetBoxModelFilterSet):
|
||||
@@ -1714,13 +1729,14 @@ class VirtualChassisFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'domain', 'name']
|
||||
fields = ['id', 'domain', 'name', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(members__name__icontains=value) |
|
||||
Q(domain__icontains=value)
|
||||
)
|
||||
@@ -1789,12 +1805,16 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = ['id', 'label', 'length', 'length_unit']
|
||||
fields = ['id', 'label', 'length', 'length_unit', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(label__icontains=value)
|
||||
qs_filter = (
|
||||
Q(label__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def filter_by_termination(self, queryset, name, value):
|
||||
# Filter by a related object cached on CableTermination. Note the underscore preceding the field name.
|
||||
@@ -1881,13 +1901,14 @@ class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = ['id', 'name']
|
||||
fields = ['id', 'name', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value)
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
@@ -1948,6 +1969,7 @@ class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpoi
|
||||
model = PowerFeed
|
||||
fields = [
|
||||
'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'cable_end',
|
||||
'description',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -1955,6 +1977,7 @@ class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpoi
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(power_panel__name__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
@@ -412,7 +412,7 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
)
|
||||
u_height = forms.IntegerField(
|
||||
label=_('U height'),
|
||||
min_value=1,
|
||||
min_value=0,
|
||||
required=False
|
||||
)
|
||||
is_full_depth = forms.NullBooleanField(
|
||||
|
||||
22
netbox/dcim/migrations/0182_zero_length_cable_fix.py
Normal file
22
netbox/dcim/migrations/0182_zero_length_cable_fix.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def update_cable_lengths(apps, schema_editor):
|
||||
Cable = apps.get_model('dcim', 'Cable')
|
||||
|
||||
# Set the absolute length for any zero-length Cables
|
||||
Cable.objects.filter(length=0).update(_abs_length=0)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0181_rename_device_role_device_role'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=update_cable_lengths,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
@@ -201,7 +201,7 @@ class Cable(PrimaryModel):
|
||||
_created = self.pk is None
|
||||
|
||||
# Store the given length (if any) in meters for use in database ordering
|
||||
if self.length and self.length_unit:
|
||||
if self.length is not None and self.length_unit:
|
||||
self._abs_length = to_meters(self.length, self.length_unit)
|
||||
else:
|
||||
self._abs_length = None
|
||||
|
||||
@@ -274,7 +274,7 @@ class CableTraceSVG:
|
||||
if cable.type:
|
||||
# Include the cable type in the tooltip
|
||||
description.append(cable.get_type_display())
|
||||
if cable.length and cable.length_unit:
|
||||
if cable.length is not None and cable.length_unit:
|
||||
# Include the cable length in the tooltip
|
||||
description.append(f'{cable.length} {cable.get_length_unit_display()}')
|
||||
else:
|
||||
@@ -285,7 +285,7 @@ class CableTraceSVG:
|
||||
description = []
|
||||
if cable.type:
|
||||
labels.append(cable.get_type_display())
|
||||
if cable.length and cable.length_unit:
|
||||
if cable.length is not None and cable.length_unit:
|
||||
# Include the cable length in the tooltip
|
||||
labels.append(f'{cable.length} {cable.get_length_unit_display()}')
|
||||
|
||||
|
||||
@@ -1078,7 +1078,7 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
|
||||
comments = columns.MarkdownColumn()
|
||||
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:vdc_list'
|
||||
url_name='dcim:virtualdevicecontext_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -695,8 +695,7 @@ class RackRackReservationsView(generic.ObjectChildrenView):
|
||||
label=_('Reservations'),
|
||||
badge=lambda obj: obj.reservations.count(),
|
||||
permission='dcim.view_rackreservation',
|
||||
weight=510,
|
||||
hide_if_empty=True
|
||||
weight=510
|
||||
)
|
||||
|
||||
def get_children(self, request, parent):
|
||||
|
||||
@@ -512,7 +512,7 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = ['id', 'name', 'is_active', 'data_synced']
|
||||
fields = ['id', 'name', 'is_active', 'data_synced', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
||||
@@ -114,7 +114,7 @@ class Command(BaseCommand):
|
||||
# Create the job
|
||||
job = Job.objects.create(
|
||||
object=module,
|
||||
name=script.name,
|
||||
name=script.class_name,
|
||||
user=User.objects.filter(is_superuser=True).order_by('pk')[0],
|
||||
job_id=uuid.uuid4()
|
||||
)
|
||||
|
||||
@@ -10,7 +10,6 @@ from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.validators import RegexValidator, ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -571,8 +570,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
|
||||
# Multiselect
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
||||
filter_class = filters.MultiValueCharFilter
|
||||
kwargs['lookup_expr'] = 'has_key'
|
||||
filter_class = filters.MultiValueArrayFilter
|
||||
|
||||
# Object
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
||||
|
||||
@@ -315,7 +315,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
text = clean_html(text, allowed_schemes)
|
||||
|
||||
# Sanitize link
|
||||
link = urllib.parse.quote(link, safe='/:?&=%+[]@#,;')
|
||||
link = urllib.parse.quote(link, safe='/:?&=%+[]@#,;!')
|
||||
|
||||
# Verify link scheme is allowed
|
||||
result = urllib.parse.urlparse(link)
|
||||
|
||||
@@ -62,21 +62,20 @@ def handle_changed_object(sender, instance, **kwargs):
|
||||
else:
|
||||
return
|
||||
|
||||
# Record an ObjectChange if applicable
|
||||
if hasattr(instance, 'to_objectchange'):
|
||||
if m2m_changed:
|
||||
ObjectChange.objects.filter(
|
||||
changed_object_type=ContentType.objects.get_for_model(instance),
|
||||
changed_object_id=instance.pk,
|
||||
request_id=request.id
|
||||
).update(
|
||||
postchange_data=instance.to_objectchange(action).postchange_data
|
||||
)
|
||||
else:
|
||||
objectchange = instance.to_objectchange(action)
|
||||
objectchange.user = request.user
|
||||
objectchange.request_id = request.id
|
||||
objectchange.save()
|
||||
# Record an ObjectChange
|
||||
if m2m_changed:
|
||||
ObjectChange.objects.filter(
|
||||
changed_object_type=ContentType.objects.get_for_model(instance),
|
||||
changed_object_id=instance.pk,
|
||||
request_id=request.id
|
||||
).update(
|
||||
postchange_data=instance.to_objectchange(action).postchange_data
|
||||
)
|
||||
else:
|
||||
objectchange = instance.to_objectchange(action)
|
||||
objectchange.user = request.user
|
||||
objectchange.request_id = request.id
|
||||
objectchange.save()
|
||||
|
||||
# If this is an M2M change, update the previously queued webhook (from post_save)
|
||||
queue = webhooks_queue.get()
|
||||
|
||||
265
netbox/extras/tests/test_custom_validation.py
Normal file
265
netbox/extras/tests/test_custom_validation.py
Normal file
@@ -0,0 +1,265 @@
|
||||
from django.test import TestCase
|
||||
from django.test import override_settings
|
||||
|
||||
from circuits.api.serializers import ProviderSerializer
|
||||
from circuits.forms import ProviderForm
|
||||
from circuits.models import Provider
|
||||
from ipam.models import ASN, RIR
|
||||
from utilities.choices import CSVDelimiterChoices, ImportFormatChoices
|
||||
from utilities.testing import APITestCase, ModelViewTestCase, create_tags, post_data
|
||||
|
||||
|
||||
class ModelFormCustomValidationTest(TestCase):
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={
|
||||
'circuits.provider': [
|
||||
{'tags': {'required': True}}
|
||||
]
|
||||
})
|
||||
def test_tags_validation(self):
|
||||
"""
|
||||
Check that custom validation rules work for tag assignment.
|
||||
"""
|
||||
data = {
|
||||
'name': 'Provider 1',
|
||||
'slug': 'provider-1',
|
||||
}
|
||||
form = ProviderForm(data)
|
||||
self.assertFalse(form.is_valid())
|
||||
|
||||
tags = create_tags('Tag1', 'Tag2', 'Tag3')
|
||||
data['tags'] = [tag.pk for tag in tags]
|
||||
form = ProviderForm(data)
|
||||
self.assertTrue(form.is_valid())
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={
|
||||
'circuits.provider': [
|
||||
{'asns': {'required': True}}
|
||||
]
|
||||
})
|
||||
def test_m2m_validation(self):
|
||||
"""
|
||||
Check that custom validation rules work for many-to-many fields.
|
||||
"""
|
||||
data = {
|
||||
'name': 'Provider 1',
|
||||
'slug': 'provider-1',
|
||||
}
|
||||
form = ProviderForm(data)
|
||||
self.assertFalse(form.is_valid())
|
||||
|
||||
rir = RIR.objects.create(name='RIR 1', slug='rir-1')
|
||||
asns = ASN.objects.bulk_create((
|
||||
ASN(rir=rir, asn=65001),
|
||||
ASN(rir=rir, asn=65002),
|
||||
ASN(rir=rir, asn=65003),
|
||||
))
|
||||
data['asns'] = [asn.pk for asn in asns]
|
||||
form = ProviderForm(data)
|
||||
self.assertTrue(form.is_valid())
|
||||
|
||||
|
||||
class BulkEditCustomValidationTest(ModelViewTestCase):
|
||||
model = Provider
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
rir = RIR.objects.create(name='RIR 1', slug='rir-1')
|
||||
asns = ASN.objects.bulk_create((
|
||||
ASN(rir=rir, asn=65001),
|
||||
ASN(rir=rir, asn=65002),
|
||||
ASN(rir=rir, asn=65003),
|
||||
))
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
for provider in providers:
|
||||
provider.asns.set(asns)
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={
|
||||
'circuits.provider': [
|
||||
{'asns': {'required': True}}
|
||||
]
|
||||
})
|
||||
def test_bulk_edit_without_m2m(self):
|
||||
"""
|
||||
Check that custom validation rules do not interfere with bulk editing.
|
||||
"""
|
||||
data = {
|
||||
'pk': list(Provider.objects.values_list('pk', flat=True)),
|
||||
'_apply': '',
|
||||
'description': 'New description',
|
||||
}
|
||||
self.add_permissions(
|
||||
'circuits.view_provider',
|
||||
'circuits.change_provider',
|
||||
)
|
||||
|
||||
# Bulk edit the description without changing ASN assignments
|
||||
request = {
|
||||
'path': self._get_url('bulk_edit'),
|
||||
'data': post_data(data),
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 302)
|
||||
self.assertEqual(
|
||||
Provider.objects.filter(description=data['description']).count(),
|
||||
len(data['pk'])
|
||||
)
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={
|
||||
'circuits.provider': [
|
||||
{'asns': {'required': True}}
|
||||
]
|
||||
})
|
||||
def test_bulk_edit_m2m(self):
|
||||
"""
|
||||
Test that custom validation rules are enforced during bulk editing.
|
||||
"""
|
||||
data = {
|
||||
'pk': list(Provider.objects.values_list('pk', flat=True)),
|
||||
'_apply': '',
|
||||
'description': 'New description',
|
||||
}
|
||||
self.add_permissions(
|
||||
'circuits.view_provider',
|
||||
'circuits.change_provider',
|
||||
'ipam.view_asn',
|
||||
)
|
||||
|
||||
# Change the ASN assignments
|
||||
asn = ASN.objects.first()
|
||||
data['asns'] = [asn.pk]
|
||||
request = {
|
||||
'path': self._get_url('bulk_edit'),
|
||||
'data': post_data(data),
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 302)
|
||||
for provider in Provider.objects.all():
|
||||
self.assertEqual(len(provider.asns.all()), 1)
|
||||
|
||||
# Attempt to remove the ASN assignments
|
||||
data.pop('asns')
|
||||
data['_nullify'] = 'asns'
|
||||
request = {
|
||||
'path': self._get_url('bulk_edit'),
|
||||
'data': post_data(data),
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
for provider in Provider.objects.all():
|
||||
self.assertTrue(provider.asns.exists())
|
||||
|
||||
|
||||
class BulkImportCustomValidationTest(ModelViewTestCase):
|
||||
model = Provider
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
create_tags('Tag1', 'Tag2', 'Tag3')
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={
|
||||
'circuits.provider': [
|
||||
{'tags': {'required': True}}
|
||||
]
|
||||
})
|
||||
def test_bulk_import_invalid(self):
|
||||
"""
|
||||
Test that custom validation rules are enforced during bulk import.
|
||||
"""
|
||||
csv_data = (
|
||||
"name,slug",
|
||||
"Provider 1,provider-1",
|
||||
"Provider 2,provider-2",
|
||||
"Provider 3,provider-3",
|
||||
)
|
||||
data = {
|
||||
'data': '\n'.join(csv_data),
|
||||
'format': ImportFormatChoices.CSV,
|
||||
'csv_delimiter': CSVDelimiterChoices.COMMA,
|
||||
}
|
||||
self.add_permissions(
|
||||
'circuits.view_provider',
|
||||
'circuits.add_provider',
|
||||
'extras.view_tag',
|
||||
)
|
||||
|
||||
# Attempt to import providers without tags
|
||||
request = {
|
||||
'path': self._get_url('import'),
|
||||
'data': post_data(data),
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertFalse(Provider.objects.exists())
|
||||
|
||||
# Import providers successfully with tag assignments
|
||||
csv_data = (
|
||||
"name,slug,tags",
|
||||
"Provider 1,provider-1,tag1",
|
||||
"Provider 2,provider-2,tag2",
|
||||
"Provider 3,provider-3,tag3",
|
||||
)
|
||||
data['data'] = '\n'.join(csv_data)
|
||||
request = {
|
||||
'path': self._get_url('import'),
|
||||
'data': post_data(data),
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 302)
|
||||
self.assertTrue(Provider.objects.exists())
|
||||
|
||||
|
||||
class APISerializerCustomValidationTest(APITestCase):
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={
|
||||
'circuits.provider': [
|
||||
{'tags': {'required': True}}
|
||||
]
|
||||
})
|
||||
def test_tags_validation(self):
|
||||
"""
|
||||
Check that custom validation rules work for tag assignment.
|
||||
"""
|
||||
data = {
|
||||
'name': 'Provider 1',
|
||||
'slug': 'provider-1',
|
||||
}
|
||||
serializer = ProviderSerializer(data=data)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
|
||||
tags = create_tags('Tag1', 'Tag2', 'Tag3')
|
||||
data['tags'] = [tag.pk for tag in tags]
|
||||
serializer = ProviderSerializer(data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={
|
||||
'circuits.provider': [
|
||||
{'asns': {'required': True}}
|
||||
]
|
||||
})
|
||||
def test_m2m_validation(self):
|
||||
"""
|
||||
Check that custom validation rules work for many-to-many fields.
|
||||
"""
|
||||
data = {
|
||||
'name': 'Provider 1',
|
||||
'slug': 'provider-1',
|
||||
}
|
||||
serializer = ProviderSerializer(data=data)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
|
||||
rir = RIR.objects.create(name='RIR 1', slug='rir-1')
|
||||
asns = ASN.objects.bulk_create((
|
||||
ASN(rir=rir, asn=65001),
|
||||
ASN(rir=rir, asn=65002),
|
||||
ASN(rir=rir, asn=65003),
|
||||
))
|
||||
data['asns'] = [asn.pk for asn in asns]
|
||||
serializer = ProviderSerializer(data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
@@ -1329,7 +1329,7 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
name='Custom Field Choice Set 1',
|
||||
extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'), ('x', 'X'))
|
||||
extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'))
|
||||
)
|
||||
|
||||
# Integer filtering
|
||||
@@ -1435,7 +1435,7 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
'cf7': 'http://a.example.com',
|
||||
'cf8': 'http://a.example.com',
|
||||
'cf9': 'A',
|
||||
'cf10': ['A', 'X'],
|
||||
'cf10': ['A', 'B'],
|
||||
'cf11': manufacturers[0].pk,
|
||||
'cf12': [manufacturers[0].pk, manufacturers[3].pk],
|
||||
}),
|
||||
@@ -1449,7 +1449,7 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
'cf7': 'http://b.example.com',
|
||||
'cf8': 'http://b.example.com',
|
||||
'cf9': 'B',
|
||||
'cf10': ['B', 'X'],
|
||||
'cf10': ['B', 'C'],
|
||||
'cf11': manufacturers[1].pk,
|
||||
'cf12': [manufacturers[1].pk, manufacturers[3].pk],
|
||||
}),
|
||||
@@ -1463,7 +1463,7 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
'cf7': 'http://c.example.com',
|
||||
'cf8': 'http://c.example.com',
|
||||
'cf9': 'C',
|
||||
'cf10': ['C', 'X'],
|
||||
'cf10': None,
|
||||
'cf11': manufacturers[2].pk,
|
||||
'cf12': [manufacturers[2].pk, manufacturers[3].pk],
|
||||
}),
|
||||
@@ -1531,8 +1531,9 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
self.assertEqual(self.filterset({'cf_cf9': ['A', 'B']}, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_filter_multiselect(self):
|
||||
self.assertEqual(self.filterset({'cf_cf10': ['A', 'B']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf10': ['X']}, self.queryset).qs.count(), 3)
|
||||
self.assertEqual(self.filterset({'cf_cf10': ['A']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf10': ['A', 'C']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf10': ['null']}, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_filter_object(self):
|
||||
manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
|
||||
|
||||
@@ -40,7 +40,8 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
|
||||
required=True,
|
||||
weight=100,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE,
|
||||
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE
|
||||
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
|
||||
description='foobar1'
|
||||
),
|
||||
CustomField(
|
||||
name='Custom Field 2',
|
||||
@@ -48,7 +49,8 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
|
||||
required=False,
|
||||
weight=200,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT,
|
||||
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY
|
||||
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY,
|
||||
description='foobar2'
|
||||
),
|
||||
CustomField(
|
||||
name='Custom Field 3',
|
||||
@@ -56,7 +58,8 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
|
||||
required=False,
|
||||
weight=300,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED,
|
||||
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
|
||||
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN,
|
||||
description='foobar3'
|
||||
),
|
||||
CustomField(
|
||||
name='Custom Field 4',
|
||||
@@ -84,6 +87,10 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
|
||||
custom_fields[3].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device'))
|
||||
custom_fields[4].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device'))
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Custom Field 1', 'Custom Field 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -116,6 +123,10 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
|
||||
params = {'choice_set_id': CustomFieldChoiceSet.objects.values_list('pk', flat=True)}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class CustomFieldChoiceSetTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = CustomFieldChoiceSet.objects.all()
|
||||
@@ -124,12 +135,16 @@ class CustomFieldChoiceSetTestCase(TestCase, BaseFilterSetTests):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
choice_sets = (
|
||||
CustomFieldChoiceSet(name='Choice Set 1', extra_choices=['A', 'B', 'C']),
|
||||
CustomFieldChoiceSet(name='Choice Set 2', extra_choices=['D', 'E', 'F']),
|
||||
CustomFieldChoiceSet(name='Choice Set 3', extra_choices=['G', 'H', 'I']),
|
||||
CustomFieldChoiceSet(name='Choice Set 1', extra_choices=['A', 'B', 'C'], description='foobar1'),
|
||||
CustomFieldChoiceSet(name='Choice Set 2', extra_choices=['D', 'E', 'F'], description='foobar2'),
|
||||
CustomFieldChoiceSet(name='Choice Set 3', extra_choices=['G', 'H', 'I'], description='foobar3'),
|
||||
)
|
||||
CustomFieldChoiceSet.objects.bulk_create(choice_sets)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Choice Set 1', 'Choice Set 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -138,6 +153,10 @@ class CustomFieldChoiceSetTestCase(TestCase, BaseFilterSetTests):
|
||||
params = {'choice': ['A', 'D']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class WebhookTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = Webhook.objects.all()
|
||||
@@ -216,6 +235,10 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
|
||||
webhooks[3].content_types.add(content_types[3])
|
||||
webhooks[4].content_types.add(content_types[4])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'Webhook 1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Webhook 1', 'Webhook 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -297,6 +320,10 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
||||
for i, custom_link in enumerate(custom_links):
|
||||
custom_link.content_types.set([content_types[i]])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'Custom Link 1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Custom Link 1', 'Custom Link 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -347,7 +374,8 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests):
|
||||
weight=100,
|
||||
enabled=True,
|
||||
shared=True,
|
||||
parameters={'status': ['active']}
|
||||
parameters={'status': ['active']},
|
||||
description='foobar1'
|
||||
),
|
||||
SavedFilter(
|
||||
name='Saved Filter 2',
|
||||
@@ -356,7 +384,8 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests):
|
||||
weight=200,
|
||||
enabled=True,
|
||||
shared=True,
|
||||
parameters={'status': ['planned']}
|
||||
parameters={'status': ['planned']},
|
||||
description='foobar2'
|
||||
),
|
||||
SavedFilter(
|
||||
name='Saved Filter 3',
|
||||
@@ -365,13 +394,18 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests):
|
||||
weight=300,
|
||||
enabled=False,
|
||||
shared=False,
|
||||
parameters={'status': ['retired']}
|
||||
parameters={'status': ['retired']},
|
||||
description='foobar3'
|
||||
),
|
||||
)
|
||||
SavedFilter.objects.bulk_create(saved_filters)
|
||||
for i, savedfilter in enumerate(saved_filters):
|
||||
savedfilter.content_types.set([content_types[i]])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Saved Filter 1', 'Saved Filter 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -380,6 +414,10 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests):
|
||||
params = {'slug': ['saved-filter-1', 'saved-filter-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_content_types(self):
|
||||
params = {'content_types': 'dcim.site'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
@@ -423,8 +461,6 @@ class BookmarkTestCase(TestCase, BaseFilterSetTests):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
|
||||
|
||||
users = (
|
||||
User(username='User 1'),
|
||||
User(username='User 2'),
|
||||
@@ -505,6 +541,10 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
|
||||
for i, et in enumerate(export_templates):
|
||||
et.content_types.set([content_types[i]])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Export Template 1', 'Export Template 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -578,6 +618,10 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
|
||||
)
|
||||
ImageAttachment.objects.bulk_create(image_attachments)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'Attachment 1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Image Attachment 1', 'Image Attachment 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -630,41 +674,45 @@ class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
assigned_object=sites[0],
|
||||
created_by=users[0],
|
||||
kind=JournalEntryKindChoices.KIND_INFO,
|
||||
comments='New journal entry'
|
||||
comments='foobar1'
|
||||
),
|
||||
JournalEntry(
|
||||
assigned_object=sites[0],
|
||||
created_by=users[1],
|
||||
kind=JournalEntryKindChoices.KIND_SUCCESS,
|
||||
comments='New journal entry'
|
||||
comments='foobar2'
|
||||
),
|
||||
JournalEntry(
|
||||
assigned_object=sites[1],
|
||||
created_by=users[2],
|
||||
kind=JournalEntryKindChoices.KIND_WARNING,
|
||||
comments='New journal entry'
|
||||
comments='foobar3'
|
||||
),
|
||||
JournalEntry(
|
||||
assigned_object=racks[0],
|
||||
created_by=users[0],
|
||||
kind=JournalEntryKindChoices.KIND_INFO,
|
||||
comments='New journal entry'
|
||||
comments='foobar4'
|
||||
),
|
||||
JournalEntry(
|
||||
assigned_object=racks[0],
|
||||
created_by=users[1],
|
||||
kind=JournalEntryKindChoices.KIND_SUCCESS,
|
||||
comments='New journal entry'
|
||||
comments='foobar5'
|
||||
),
|
||||
JournalEntry(
|
||||
assigned_object=racks[1],
|
||||
created_by=users[2],
|
||||
kind=JournalEntryKindChoices.KIND_WARNING,
|
||||
comments='New journal entry'
|
||||
comments='foobar6'
|
||||
),
|
||||
)
|
||||
JournalEntry.objects.bulk_create(journal_entries)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_created_by(self):
|
||||
users = User.objects.filter(username__in=['Alice', 'Bob'])
|
||||
params = {'created_by': [users[0].username, users[1].username]}
|
||||
@@ -800,9 +848,10 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
for i in range(0, 3):
|
||||
is_active = bool(i % 2)
|
||||
c = ConfigContext.objects.create(
|
||||
name='Config Context {}'.format(i + 1),
|
||||
name=f"Config Context {i + 1}",
|
||||
is_active=is_active,
|
||||
data='{"foo": 123}'
|
||||
data='{"foo": 123}',
|
||||
description=f"foobar{i + 1}"
|
||||
)
|
||||
c.regions.set([regions[i]])
|
||||
c.site_groups.set([site_groups[i]])
|
||||
@@ -818,6 +867,10 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
c.tenants.set([tenants[i]])
|
||||
c.tags.set([tags[i]])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Config Context 1', 'Config Context 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -828,6 +881,10 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'is_active': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
@@ -929,6 +986,10 @@ class ConfigTemplateTestCase(TestCase, BaseFilterSetTests):
|
||||
)
|
||||
ConfigTemplate.objects.bulk_create(config_templates)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Config Template 1', 'Config Template 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -965,6 +1026,10 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
site.tags.set([tags[0]])
|
||||
provider.tags.set([tags[1]])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Tag 1', 'Tag 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1076,6 +1141,10 @@ class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
|
||||
)
|
||||
ObjectChange.objects.bulk_create(object_changes)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'Site 1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_user(self):
|
||||
params = {'user_id': User.objects.filter(username__in=['user1', 'user2']).values_list('pk', flat=True)}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core import validators
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# NOTE: As this module may be imported by configuration.py, we cannot import
|
||||
# anything from NetBox itself.
|
||||
@@ -66,8 +67,7 @@ class CustomValidator:
|
||||
def __call__(self, instance):
|
||||
# Validate instance attributes per validation rules
|
||||
for attr_name, rules in self.validation_rules.items():
|
||||
assert hasattr(instance, attr_name), f"Invalid attribute '{attr_name}' for {instance.__class__.__name__}"
|
||||
attr = getattr(instance, attr_name)
|
||||
attr = self._getattr(instance, attr_name)
|
||||
for descriptor, value in rules.items():
|
||||
validator = self.get_validator(descriptor, value)
|
||||
try:
|
||||
@@ -79,6 +79,26 @@ class CustomValidator:
|
||||
# Execute custom validation logic (if any)
|
||||
self.validate(instance)
|
||||
|
||||
@staticmethod
|
||||
def _getattr(instance, name):
|
||||
# Attempt to resolve many-to-many fields to their stored values
|
||||
m2m_fields = [f.name for f in instance._meta.local_many_to_many]
|
||||
if name in m2m_fields:
|
||||
if name in getattr(instance, '_m2m_values', []):
|
||||
return instance._m2m_values[name]
|
||||
if instance.pk:
|
||||
return list(getattr(instance, name).all())
|
||||
return []
|
||||
|
||||
# Raise a ValidationError for unknown attributes
|
||||
if not hasattr(instance, name):
|
||||
raise ValidationError(_('Invalid attribute "{name}" for {model}').format(
|
||||
name=name,
|
||||
model=instance.__class__.__name__
|
||||
))
|
||||
|
||||
return getattr(instance, name)
|
||||
|
||||
def get_validator(self, descriptor, value):
|
||||
"""
|
||||
Instantiate and return the appropriate validator based on the descriptor given. For
|
||||
|
||||
@@ -759,7 +759,7 @@ class FHRPGroupFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = FHRPGroup
|
||||
fields = ['id', 'group_id', 'name', 'auth_key']
|
||||
fields = ['id', 'group_id', 'name', 'auth_key', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -950,6 +950,10 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
choices=VLANStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
available_at_site = django_filters.ModelChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
method='get_for_site'
|
||||
)
|
||||
available_on_device = django_filters.ModelChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
method='get_for_device'
|
||||
@@ -984,6 +988,10 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_for_site(self, queryset, name, value):
|
||||
return queryset.get_for_site(value)
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_for_device(self, queryset, name, value):
|
||||
return queryset.get_for_device(value)
|
||||
@@ -1001,12 +1009,15 @@ class ServiceTemplateFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ServiceTemplate
|
||||
fields = ['id', 'name', 'protocol']
|
||||
fields = ['id', 'name', 'protocol', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
|
||||
@@ -864,11 +864,9 @@ class IPAddress(PrimaryModel):
|
||||
is_primary = True
|
||||
|
||||
if is_primary and (parent != original_parent):
|
||||
raise ValidationError({
|
||||
'assigned_object': _(
|
||||
"Cannot reassign IP address while it is designated as the primary IP for the parent object"
|
||||
)
|
||||
})
|
||||
raise ValidationError(
|
||||
_("Cannot reassign IP address while it is designated as the primary IP for the parent object")
|
||||
)
|
||||
|
||||
# Validate IP status selection
|
||||
if self.status == IPAddressStatusChoices.STATUS_SLAAC and self.family != 6:
|
||||
|
||||
@@ -224,11 +224,11 @@ class VLAN(PrimaryModel):
|
||||
|
||||
# Validate VLAN group (if assigned)
|
||||
if self.group and self.site and self.group.scope != self.site:
|
||||
raise ValidationError({
|
||||
'group': _(
|
||||
raise ValidationError(
|
||||
_(
|
||||
"VLAN is assigned to group {group} (scope: {scope}); cannot also assign to site {site}."
|
||||
).format(group=self.group, scope=self.group.scope, site=self.site)
|
||||
})
|
||||
)
|
||||
|
||||
# Validate group min/max VIDs
|
||||
if self.group and not self.group.min_vid <= self.vid <= self.group.max_vid:
|
||||
|
||||
@@ -69,6 +69,35 @@ class VLANGroupQuerySet(RestrictedQuerySet):
|
||||
|
||||
class VLANQuerySet(RestrictedQuerySet):
|
||||
|
||||
def get_for_site(self, site):
|
||||
"""
|
||||
Return all VLANs in the specified site
|
||||
"""
|
||||
from .models import VLANGroup
|
||||
q = Q()
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'),
|
||||
scope_id=site.pk
|
||||
)
|
||||
|
||||
if site.region:
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'),
|
||||
scope_id__in=site.region.get_ancestors(include_self=True)
|
||||
)
|
||||
if site.group:
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'),
|
||||
scope_id__in=site.group.get_ancestors(include_self=True)
|
||||
)
|
||||
|
||||
return self.filter(
|
||||
Q(group__in=VLANGroup.objects.filter(q)) |
|
||||
Q(site=site) |
|
||||
Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs
|
||||
Q(group__isnull=True, site__isnull=True) # Global VLANs
|
||||
)
|
||||
|
||||
def get_for_device(self, device):
|
||||
"""
|
||||
Return all VLANs available to the specified Device.
|
||||
|
||||
@@ -56,8 +56,12 @@ def clear_primary_ip(instance, **kwargs):
|
||||
"""
|
||||
field_name = f'primary_ip{instance.family}'
|
||||
if device := Device.objects.filter(**{field_name: instance}).first():
|
||||
device.snapshot()
|
||||
setattr(device, field_name, None)
|
||||
device.save()
|
||||
if virtualmachine := VirtualMachine.objects.filter(**{field_name: instance}).first():
|
||||
virtualmachine.snapshot()
|
||||
setattr(virtualmachine, field_name, None)
|
||||
virtualmachine.save()
|
||||
|
||||
|
||||
@@ -67,4 +71,6 @@ def clear_oob_ip(instance, **kwargs):
|
||||
When an IPAddress is deleted, trigger save() on any Devices for which it was a OOB IP.
|
||||
"""
|
||||
if device := Device.objects.filter(oob_ip=instance).first():
|
||||
device.snapshot()
|
||||
device.oob_ip = None
|
||||
device.save()
|
||||
|
||||
@@ -39,7 +39,7 @@ class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
tenant=None,
|
||||
start=65000,
|
||||
end=65009,
|
||||
description='aaa'
|
||||
description='foobar1'
|
||||
),
|
||||
ASNRange(
|
||||
name='ASN Range 2',
|
||||
@@ -48,7 +48,7 @@ class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
tenant=tenants[0],
|
||||
start=65010,
|
||||
end=65019,
|
||||
description='bbb'
|
||||
description='foobar2'
|
||||
),
|
||||
ASNRange(
|
||||
name='ASN Range 3',
|
||||
@@ -57,11 +57,15 @@ class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
tenant=tenants[1],
|
||||
start=65020,
|
||||
end=65029,
|
||||
description='ccc'
|
||||
description='foobar3'
|
||||
),
|
||||
)
|
||||
ASNRange.objects.bulk_create(asn_ranges)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['ASN Range 1', 'ASN Range 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -89,7 +93,7 @@ class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['aaa', 'bbb']}
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
@@ -123,9 +127,9 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
asns = (
|
||||
ASN(asn=65001, rir=rirs[0], tenant=tenants[0], description='aaa'),
|
||||
ASN(asn=65002, rir=rirs[1], tenant=tenants[1], description='bbb'),
|
||||
ASN(asn=65003, rir=rirs[2], tenant=tenants[2], description='ccc'),
|
||||
ASN(asn=65001, rir=rirs[0], tenant=tenants[0], description='foobar1'),
|
||||
ASN(asn=65002, rir=rirs[1], tenant=tenants[1], description='foobar2'),
|
||||
ASN(asn=65003, rir=rirs[2], tenant=tenants[2], description='foobar3'),
|
||||
ASN(asn=4200000000, rir=rirs[0], tenant=tenants[0]),
|
||||
ASN(asn=4200000001, rir=rirs[1], tenant=tenants[1]),
|
||||
ASN(asn=4200000002, rir=rirs[2], tenant=tenants[2]),
|
||||
@@ -139,6 +143,10 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
asns[4].sites.set([sites[1]])
|
||||
asns[5].sites.set([sites[2]])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_asn(self):
|
||||
params = {'asn': [65001, 4200000000]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -165,7 +173,7 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['aaa', 'bbb']}
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
@@ -214,6 +222,10 @@ class VRFTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vrfs[2].import_targets.add(route_targets[2])
|
||||
vrfs[2].export_targets.add(route_targets[2])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['VRF 1', 'VRF 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -310,6 +322,10 @@ class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vrfs[1].import_targets.add(route_targets[4], route_targets[5])
|
||||
vrfs[1].export_targets.add(route_targets[6], route_targets[7])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['65000:1001', '65000:1002', '65000:1003']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
@@ -355,15 +371,19 @@ class RIRTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
def setUpTestData(cls):
|
||||
|
||||
rirs = (
|
||||
RIR(name='RIR 1', slug='rir-1', is_private=False, description='A'),
|
||||
RIR(name='RIR 2', slug='rir-2', is_private=False, description='B'),
|
||||
RIR(name='RIR 3', slug='rir-3', is_private=False, description='C'),
|
||||
RIR(name='RIR 4', slug='rir-4', is_private=True, description='D'),
|
||||
RIR(name='RIR 5', slug='rir-5', is_private=True, description='E'),
|
||||
RIR(name='RIR 6', slug='rir-6', is_private=True, description='F'),
|
||||
RIR(name='RIR 1', slug='rir-1', is_private=False, description='foobar1'),
|
||||
RIR(name='RIR 2', slug='rir-2', is_private=False, description='foobar2'),
|
||||
RIR(name='RIR 3', slug='rir-3', is_private=False, description='foobar3'),
|
||||
RIR(name='RIR 4', slug='rir-4', is_private=True),
|
||||
RIR(name='RIR 5', slug='rir-5', is_private=True),
|
||||
RIR(name='RIR 6', slug='rir-6', is_private=True),
|
||||
)
|
||||
RIR.objects.bulk_create(rirs)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['RIR 1', 'RIR 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -373,7 +393,7 @@ class RIRTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['A', 'B']}
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_is_private(self):
|
||||
@@ -422,6 +442,10 @@ class AggregateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
Aggregate.objects.bulk_create(aggregates)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_family(self):
|
||||
params = {'family': '4'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
@@ -475,6 +499,10 @@ class RoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
Role.objects.bulk_create(roles)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Role 1', 'Role 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -579,6 +607,10 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
for prefix in prefixes:
|
||||
prefix.save()
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_family(self):
|
||||
params = {'family': '6'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
@@ -745,17 +777,87 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
ip_ranges = (
|
||||
IPRange(start_address='10.0.1.100/24', end_address='10.0.1.199/24', size=100, vrf=None, tenant=None, role=None, status=IPRangeStatusChoices.STATUS_ACTIVE, description='foobar1'),
|
||||
IPRange(start_address='10.0.2.100/24', end_address='10.0.2.199/24', size=100, vrf=vrfs[0], tenant=tenants[0], role=roles[0], status=IPRangeStatusChoices.STATUS_ACTIVE, description='foobar2'),
|
||||
IPRange(start_address='10.0.3.100/24', end_address='10.0.3.199/24', size=100, vrf=vrfs[1], tenant=tenants[1], role=roles[1], status=IPRangeStatusChoices.STATUS_DEPRECATED),
|
||||
IPRange(start_address='10.0.4.100/24', end_address='10.0.4.199/24', size=100, vrf=vrfs[2], tenant=tenants[2], role=roles[2], status=IPRangeStatusChoices.STATUS_RESERVED),
|
||||
IPRange(start_address='2001:db8:0:1::1/64', end_address='2001:db8:0:1::100/64', size=100, vrf=None, tenant=None, role=None, status=IPRangeStatusChoices.STATUS_ACTIVE),
|
||||
IPRange(start_address='2001:db8:0:2::1/64', end_address='2001:db8:0:2::100/64', size=100, vrf=vrfs[0], tenant=tenants[0], role=roles[0], status=IPRangeStatusChoices.STATUS_ACTIVE),
|
||||
IPRange(start_address='2001:db8:0:3::1/64', end_address='2001:db8:0:3::100/64', size=100, vrf=vrfs[1], tenant=tenants[1], role=roles[1], status=IPRangeStatusChoices.STATUS_DEPRECATED),
|
||||
IPRange(start_address='2001:db8:0:4::1/64', end_address='2001:db8:0:4::100/64', size=100, vrf=vrfs[2], tenant=tenants[2], role=roles[2], status=IPRangeStatusChoices.STATUS_RESERVED),
|
||||
IPRange(
|
||||
start_address='10.0.1.100/24',
|
||||
end_address='10.0.1.199/24',
|
||||
size=100,
|
||||
vrf=None,
|
||||
tenant=None,
|
||||
role=None,
|
||||
status=IPRangeStatusChoices.STATUS_ACTIVE,
|
||||
description='foobar1'
|
||||
),
|
||||
IPRange(
|
||||
start_address='10.0.2.100/24',
|
||||
end_address='10.0.2.199/24',
|
||||
size=100,
|
||||
vrf=vrfs[0],
|
||||
tenant=tenants[0],
|
||||
role=roles[0],
|
||||
status=IPRangeStatusChoices.STATUS_ACTIVE,
|
||||
description='foobar2'
|
||||
),
|
||||
IPRange(
|
||||
start_address='10.0.3.100/24',
|
||||
end_address='10.0.3.199/24',
|
||||
size=100,
|
||||
vrf=vrfs[1],
|
||||
tenant=tenants[1],
|
||||
role=roles[1],
|
||||
status=IPRangeStatusChoices.STATUS_DEPRECATED
|
||||
),
|
||||
IPRange(
|
||||
start_address='10.0.4.100/24',
|
||||
end_address='10.0.4.199/24',
|
||||
size=100,
|
||||
vrf=vrfs[2],
|
||||
tenant=tenants[2],
|
||||
role=roles[2],
|
||||
status=IPRangeStatusChoices.STATUS_RESERVED
|
||||
),
|
||||
IPRange(
|
||||
start_address='2001:db8:0:1::1/64',
|
||||
end_address='2001:db8:0:1::100/64',
|
||||
size=100,
|
||||
vrf=None,
|
||||
tenant=None,
|
||||
role=None,
|
||||
status=IPRangeStatusChoices.STATUS_ACTIVE
|
||||
),
|
||||
IPRange(
|
||||
start_address='2001:db8:0:2::1/64',
|
||||
end_address='2001:db8:0:2::100/64',
|
||||
size=100,
|
||||
vrf=vrfs[0],
|
||||
tenant=tenants[0],
|
||||
role=roles[0],
|
||||
status=IPRangeStatusChoices.STATUS_ACTIVE
|
||||
),
|
||||
IPRange(
|
||||
start_address='2001:db8:0:3::1/64',
|
||||
end_address='2001:db8:0:3::100/64',
|
||||
size=100,
|
||||
vrf=vrfs[1],
|
||||
tenant=tenants[1],
|
||||
role=roles[1],
|
||||
status=IPRangeStatusChoices.STATUS_DEPRECATED
|
||||
),
|
||||
IPRange(
|
||||
start_address='2001:db8:0:4::1/64',
|
||||
end_address='2001:db8:0:4::100/64',
|
||||
size=100,
|
||||
vrf=vrfs[2],
|
||||
tenant=tenants[2],
|
||||
role=roles[2],
|
||||
status=IPRangeStatusChoices.STATUS_RESERVED
|
||||
),
|
||||
)
|
||||
IPRange.objects.bulk_create(ip_ranges)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_family(self):
|
||||
params = {'family': '6'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
@@ -889,21 +991,111 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
ipaddresses = (
|
||||
IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a', description='foobar1'),
|
||||
IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], assigned_object=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], assigned_object=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], assigned_object=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(address='10.0.0.5/24', tenant=None, vrf=None, assigned_object=fhrp_groups[0], status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a', description='foobar2'),
|
||||
IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], assigned_object=vminterfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], assigned_object=vminterfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], assigned_object=vminterfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(address='2001:db8::5/64', tenant=None, vrf=None, assigned_object=fhrp_groups[1], status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
IPAddress(address='2001:db8::1/65', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
IPAddress(
|
||||
address='10.0.0.1/24',
|
||||
tenant=None,
|
||||
vrf=None,
|
||||
assigned_object=None,
|
||||
status=IPAddressStatusChoices.STATUS_ACTIVE,
|
||||
dns_name='ipaddress-a',
|
||||
description='foobar1'
|
||||
),
|
||||
IPAddress(
|
||||
address='10.0.0.2/24',
|
||||
tenant=tenants[0],
|
||||
vrf=vrfs[0],
|
||||
assigned_object=interfaces[0],
|
||||
status=IPAddressStatusChoices.STATUS_ACTIVE,
|
||||
dns_name='ipaddress-b'
|
||||
),
|
||||
IPAddress(
|
||||
address='10.0.0.3/24',
|
||||
tenant=tenants[1],
|
||||
vrf=vrfs[1],
|
||||
assigned_object=interfaces[1],
|
||||
status=IPAddressStatusChoices.STATUS_RESERVED,
|
||||
role=IPAddressRoleChoices.ROLE_VIP,
|
||||
dns_name='ipaddress-c'
|
||||
),
|
||||
IPAddress(
|
||||
address='10.0.0.4/24',
|
||||
tenant=tenants[2],
|
||||
vrf=vrfs[2],
|
||||
assigned_object=interfaces[2],
|
||||
status=IPAddressStatusChoices.STATUS_DEPRECATED,
|
||||
role=IPAddressRoleChoices.ROLE_SECONDARY,
|
||||
dns_name='ipaddress-d'
|
||||
),
|
||||
IPAddress(
|
||||
address='10.0.0.5/24',
|
||||
tenant=None,
|
||||
vrf=None,
|
||||
assigned_object=fhrp_groups[0],
|
||||
status=IPAddressStatusChoices.STATUS_ACTIVE
|
||||
),
|
||||
IPAddress(
|
||||
address='10.0.0.1/25',
|
||||
tenant=None,
|
||||
vrf=None,
|
||||
assigned_object=None,
|
||||
status=IPAddressStatusChoices.STATUS_ACTIVE
|
||||
),
|
||||
IPAddress(
|
||||
address='2001:db8::1/64',
|
||||
tenant=None,
|
||||
vrf=None,
|
||||
assigned_object=None,
|
||||
status=IPAddressStatusChoices.STATUS_ACTIVE,
|
||||
dns_name='ipaddress-a',
|
||||
description='foobar2'
|
||||
),
|
||||
IPAddress(
|
||||
address='2001:db8::2/64',
|
||||
tenant=tenants[0],
|
||||
vrf=vrfs[0],
|
||||
assigned_object=vminterfaces[0],
|
||||
status=IPAddressStatusChoices.STATUS_ACTIVE,
|
||||
dns_name='ipaddress-b'
|
||||
),
|
||||
IPAddress(
|
||||
address='2001:db8::3/64',
|
||||
tenant=tenants[1],
|
||||
vrf=vrfs[1],
|
||||
assigned_object=vminterfaces[1],
|
||||
status=IPAddressStatusChoices.STATUS_RESERVED,
|
||||
role=IPAddressRoleChoices.ROLE_VIP,
|
||||
dns_name='ipaddress-c'
|
||||
),
|
||||
IPAddress(
|
||||
address='2001:db8::4/64',
|
||||
tenant=tenants[2],
|
||||
vrf=vrfs[2],
|
||||
assigned_object=vminterfaces[2],
|
||||
status=IPAddressStatusChoices.STATUS_DEPRECATED,
|
||||
role=IPAddressRoleChoices.ROLE_SECONDARY,
|
||||
dns_name='ipaddress-d'
|
||||
),
|
||||
IPAddress(
|
||||
address='2001:db8::5/64',
|
||||
tenant=None,
|
||||
vrf=None,
|
||||
assigned_object=fhrp_groups[1],
|
||||
status=IPAddressStatusChoices.STATUS_ACTIVE
|
||||
),
|
||||
IPAddress(
|
||||
address='2001:db8::1/65',
|
||||
tenant=None,
|
||||
vrf=None,
|
||||
assigned_object=None,
|
||||
status=IPAddressStatusChoices.STATUS_ACTIVE
|
||||
),
|
||||
)
|
||||
IPAddress.objects.bulk_create(ipaddresses)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_family(self):
|
||||
params = {'family': '4'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
@@ -1055,15 +1247,36 @@ class FHRPGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
IPAddress.objects.bulk_create(ip_addresses)
|
||||
|
||||
fhrp_groups = (
|
||||
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2, group_id=10, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_PLAINTEXT, auth_key='foo123'),
|
||||
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP3, group_id=20, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5, auth_key='bar456', name='bar123'),
|
||||
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP, group_id=30),
|
||||
FHRPGroup(
|
||||
protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2,
|
||||
group_id=10,
|
||||
auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_PLAINTEXT,
|
||||
auth_key='foo123',
|
||||
description='foobar1'
|
||||
),
|
||||
FHRPGroup(
|
||||
protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP3,
|
||||
group_id=20,
|
||||
auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5,
|
||||
auth_key='bar456',
|
||||
name='bar123',
|
||||
description='foobar2'
|
||||
),
|
||||
FHRPGroup(
|
||||
protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP,
|
||||
group_id=30,
|
||||
description='foobar3'
|
||||
),
|
||||
)
|
||||
FHRPGroup.objects.bulk_create(fhrp_groups)
|
||||
fhrp_groups[0].ip_addresses.set([ip_addresses[0]])
|
||||
fhrp_groups[1].ip_addresses.set([ip_addresses[1]])
|
||||
fhrp_groups[2].ip_addresses.set([ip_addresses[2]])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_protocol(self):
|
||||
params = {'protocol': [FHRPGroupProtocolChoices.PROTOCOL_VRRP2, FHRPGroupProtocolChoices.PROTOCOL_VRRP3]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1084,6 +1297,10 @@ class FHRPGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'name': ['bar123', ]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_related_ip(self):
|
||||
# Create some regular IPs to query for related IPs
|
||||
ipaddresses = (
|
||||
@@ -1199,17 +1416,21 @@ class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
cluster.save()
|
||||
|
||||
vlan_groups = (
|
||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', scope=region, description='A'),
|
||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', scope=sitegroup, description='B'),
|
||||
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=site, description='C'),
|
||||
VLANGroup(name='VLAN Group 4', slug='vlan-group-4', scope=location, description='D'),
|
||||
VLANGroup(name='VLAN Group 5', slug='vlan-group-5', scope=rack, description='E'),
|
||||
VLANGroup(name='VLAN Group 6', slug='vlan-group-6', scope=clustergroup, description='F'),
|
||||
VLANGroup(name='VLAN Group 7', slug='vlan-group-7', scope=cluster, description='G'),
|
||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', scope=region, description='foobar1'),
|
||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', scope=sitegroup, description='foobar2'),
|
||||
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=site, description='foobar3'),
|
||||
VLANGroup(name='VLAN Group 4', slug='vlan-group-4', scope=location),
|
||||
VLANGroup(name='VLAN Group 5', slug='vlan-group-5', scope=rack),
|
||||
VLANGroup(name='VLAN Group 6', slug='vlan-group-6', scope=clustergroup),
|
||||
VLANGroup(name='VLAN Group 7', slug='vlan-group-7', scope=cluster),
|
||||
VLANGroup(name='VLAN Group 8', slug='vlan-group-8'),
|
||||
)
|
||||
VLANGroup.objects.bulk_create(vlan_groups)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['VLAN Group 1', 'VLAN Group 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1219,7 +1440,7 @@ class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['A', 'B']}
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_region(self):
|
||||
@@ -1359,6 +1580,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1'),
|
||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2'),
|
||||
VLANGroup(name='VLAN Group 3', slug='vlan-group-3'),
|
||||
VLANGroup(name='VLAN Group 4', slug='vlan-group-4'),
|
||||
)
|
||||
VLANGroup.objects.bulk_create(groups)
|
||||
|
||||
@@ -1415,11 +1637,18 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
VLAN(vid=301, name='VLAN 301', site=sites[5], group=groups[23], role=roles[2], tenant=tenants[2], status=VLANStatusChoices.STATUS_RESERVED),
|
||||
VLAN(vid=302, name='VLAN 302', site=sites[5], group=groups[23], role=roles[2], tenant=tenants[2], status=VLANStatusChoices.STATUS_RESERVED),
|
||||
|
||||
# Create one globally available VLAN on a VLAN group
|
||||
VLAN(vid=500, name='VLAN Group 1', group=groups[24]),
|
||||
|
||||
# Create one globally available VLAN
|
||||
VLAN(vid=1000, name='Global VLAN'),
|
||||
)
|
||||
VLAN.objects.bulk_create(vlans)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['VLAN 101', 'VLAN 102']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1488,12 +1717,17 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
def test_available_on_device(self):
|
||||
device_id = Device.objects.first().pk
|
||||
params = {'available_on_device': device_id}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) # 5 scoped + 1 global
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7) # 5 scoped + 1 global group + 1 global
|
||||
|
||||
def test_available_on_virtualmachine(self):
|
||||
vm_id = VirtualMachine.objects.first().pk
|
||||
params = {'available_on_virtualmachine': vm_id}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) # 5 scoped + 1 global
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7) # 5 scoped + 1 global group + 1 global
|
||||
|
||||
def test_available_at_site(self):
|
||||
site_id = Site.objects.first().pk
|
||||
params = {'available_at_site': site_id}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) # 4 scoped + 1 global group + 1 global
|
||||
|
||||
|
||||
class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
@@ -1503,15 +1737,46 @@ class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
service_templates = (
|
||||
ServiceTemplate(name='Service Template 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1001]),
|
||||
ServiceTemplate(name='Service Template 2', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1002]),
|
||||
ServiceTemplate(name='Service Template 3', protocol=ServiceProtocolChoices.PROTOCOL_UDP, ports=[1003]),
|
||||
ServiceTemplate(name='Service Template 4', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[2001]),
|
||||
ServiceTemplate(name='Service Template 5', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[2002]),
|
||||
ServiceTemplate(name='Service Template 6', protocol=ServiceProtocolChoices.PROTOCOL_UDP, ports=[2003]),
|
||||
ServiceTemplate(
|
||||
name='Service Template 1',
|
||||
protocol=ServiceProtocolChoices.PROTOCOL_TCP,
|
||||
ports=[1001],
|
||||
description='foobar1'
|
||||
),
|
||||
ServiceTemplate(
|
||||
name='Service Template 2',
|
||||
protocol=ServiceProtocolChoices.PROTOCOL_TCP,
|
||||
ports=[1002],
|
||||
description='foobar2'
|
||||
),
|
||||
ServiceTemplate(
|
||||
name='Service Template 3',
|
||||
protocol=ServiceProtocolChoices.PROTOCOL_UDP,
|
||||
ports=[1003],
|
||||
description='foobar3'
|
||||
),
|
||||
ServiceTemplate(
|
||||
name='Service Template 4',
|
||||
protocol=ServiceProtocolChoices.PROTOCOL_TCP,
|
||||
ports=[2001]
|
||||
),
|
||||
ServiceTemplate(
|
||||
name='Service Template 5',
|
||||
protocol=ServiceProtocolChoices.PROTOCOL_TCP,
|
||||
ports=[2002]
|
||||
),
|
||||
ServiceTemplate(
|
||||
name='Service Template 6',
|
||||
protocol=ServiceProtocolChoices.PROTOCOL_UDP,
|
||||
ports=[2003]
|
||||
),
|
||||
)
|
||||
ServiceTemplate.objects.bulk_create(service_templates)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Service Template 1', 'Service Template 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1524,6 +1789,10 @@ class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'port': '1001'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Service.objects.all()
|
||||
@@ -1580,6 +1849,10 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
services[1].ipaddresses.add(ip_addresses[1])
|
||||
services[2].ipaddresses.add(ip_addresses[2])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Service 1', 'Service 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1636,9 +1909,26 @@ class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
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(
|
||||
name='L2VPN 1',
|
||||
slug='l2vpn-1',
|
||||
type=L2VPNTypeChoices.TYPE_VXLAN,
|
||||
identifier=65001,
|
||||
description='foobar1'
|
||||
),
|
||||
L2VPN(
|
||||
name='L2VPN 2',
|
||||
slug='l2vpn-2',
|
||||
type=L2VPNTypeChoices.TYPE_VPWS,
|
||||
identifier=65002,
|
||||
description='foobar2'
|
||||
),
|
||||
L2VPN(
|
||||
name='L2VPN 3',
|
||||
slug='l2vpn-3',
|
||||
type=L2VPNTypeChoices.TYPE_VPLS,
|
||||
description='foobar3'
|
||||
),
|
||||
)
|
||||
L2VPN.objects.bulk_create(l2vpns)
|
||||
l2vpns[0].import_targets.add(route_targets[0])
|
||||
@@ -1648,6 +1938,10 @@ class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
l2vpns[1].export_targets.add(route_targets[4])
|
||||
l2vpns[2].export_targets.add(route_targets[5])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['L2VPN 1', 'L2VPN 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1664,6 +1958,10 @@ class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'type': [L2VPNTypeChoices.TYPE_VXLAN, L2VPNTypeChoices.TYPE_VPWS]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
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]}
|
||||
|
||||
@@ -661,6 +661,26 @@ class IPRangeListView(generic.ObjectListView):
|
||||
class IPRangeView(generic.ObjectView):
|
||||
queryset = IPRange.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
|
||||
# Parent prefixes table
|
||||
parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
|
||||
Q(prefix__net_contains_or_equals=str(instance.start_address.ip)),
|
||||
Q(prefix__net_contains_or_equals=str(instance.end_address.ip)),
|
||||
vrf=instance.vrf
|
||||
).prefetch_related(
|
||||
'site', 'role', 'tenant', 'vlan', 'role'
|
||||
)
|
||||
parent_prefixes_table = tables.PrefixTable(
|
||||
list(parent_prefixes),
|
||||
exclude=('vrf', 'utilization'),
|
||||
orderable=False
|
||||
)
|
||||
|
||||
return {
|
||||
'parent_prefixes_table': parent_prefixes_table,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(IPRange, 'ipaddresses', path='ip-addresses')
|
||||
class IPRangeIPAddressesView(generic.ObjectChildrenView):
|
||||
|
||||
@@ -23,16 +23,16 @@ class ValidatedModelSerializer(BaseModelSerializer):
|
||||
validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
|
||||
"""
|
||||
def validate(self, data):
|
||||
|
||||
# Remove custom fields data and tags (if any) prior to model validation
|
||||
attrs = data.copy()
|
||||
|
||||
# Remove custom field data (if any) prior to model validation
|
||||
attrs.pop('custom_fields', None)
|
||||
attrs.pop('tags', None)
|
||||
|
||||
# Skip ManyToManyFields
|
||||
for field in self.Meta.model._meta.get_fields():
|
||||
if isinstance(field, ManyToManyField):
|
||||
attrs.pop(field.name, None)
|
||||
m2m_values = {}
|
||||
for field in self.Meta.model._meta.local_many_to_many:
|
||||
if field.name in attrs:
|
||||
m2m_values[field.name] = attrs.pop(field.name)
|
||||
|
||||
# Run clean() on an instance of the model
|
||||
if self.instance is None:
|
||||
@@ -41,6 +41,7 @@ class ValidatedModelSerializer(BaseModelSerializer):
|
||||
instance = self.instance
|
||||
for k, v in attrs.items():
|
||||
setattr(instance, k, v)
|
||||
instance._m2m_values = m2m_values
|
||||
instance.full_clean()
|
||||
|
||||
return data
|
||||
|
||||
@@ -315,5 +315,6 @@ class OrganizationalModelFilterSet(NetBoxModelFilterSet):
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
models.Q(name__icontains=value) |
|
||||
models.Q(slug__icontains=value)
|
||||
models.Q(slug__icontains=value) |
|
||||
models.Q(description__icontains=value)
|
||||
)
|
||||
|
||||
@@ -57,6 +57,17 @@ class NetBoxModelForm(BootstrapMixin, CheckLastUpdatedMixin, CustomFieldsMixin,
|
||||
|
||||
return super().clean()
|
||||
|
||||
def _post_clean(self):
|
||||
"""
|
||||
Override BaseModelForm's _post_clean() to store many-to-many field values on the model instance.
|
||||
"""
|
||||
self.instance._m2m_values = {}
|
||||
for field in self.instance._meta.local_many_to_many:
|
||||
if field.name in self.cleaned_data:
|
||||
self.instance._m2m_values[field.name] = list(self.cleaned_data[field.name])
|
||||
|
||||
return super()._post_clean()
|
||||
|
||||
|
||||
class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
|
||||
"""
|
||||
|
||||
@@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '3.6.7'
|
||||
VERSION = '3.6.9'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
|
||||
@@ -557,6 +557,14 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
elif name in form.changed_data:
|
||||
obj.custom_field_data[cf_name] = customfield.serialize(form.cleaned_data[name])
|
||||
|
||||
# Store M2M values for validation
|
||||
obj._m2m_values = {}
|
||||
for field in obj._meta.local_many_to_many:
|
||||
if value := form.cleaned_data.get(field.name):
|
||||
obj._m2m_values[field.name] = list(value)
|
||||
elif field.name in nullified_fields:
|
||||
obj._m2m_values[field.name] = []
|
||||
|
||||
obj.full_clean()
|
||||
obj.save()
|
||||
updated_objects.append(obj)
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<tr>
|
||||
<th scope="row">{% trans "Length" %}</th>
|
||||
<td>
|
||||
{% if object.length %}
|
||||
{% if object.length is not None %}
|
||||
{{ object.length|floatformat }} {{ object.get_length_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
|
||||
@@ -82,6 +82,11 @@
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
|
||||
@@ -65,7 +65,7 @@ class ContactFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Contact
|
||||
fields = ['id', 'name', 'title', 'phone', 'email', 'address', 'link']
|
||||
fields = ['id', 'name', 'title', 'phone', 'email', 'address', 'link', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -77,6 +77,7 @@ class ContactFilterSet(NetBoxModelFilterSet):
|
||||
Q(email__icontains=value) |
|
||||
Q(address__icontains=value) |
|
||||
Q(link__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
@@ -23,13 +23,32 @@ class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
tenantgroup.save()
|
||||
|
||||
tenant_groups = (
|
||||
TenantGroup(name='Tenant Group 1', slug='tenant-group-1', parent=parent_tenant_groups[0], description='A'),
|
||||
TenantGroup(name='Tenant Group 2', slug='tenant-group-2', parent=parent_tenant_groups[1], description='B'),
|
||||
TenantGroup(name='Tenant Group 3', slug='tenant-group-3', parent=parent_tenant_groups[2], description='C'),
|
||||
TenantGroup(
|
||||
name='Tenant Group 1',
|
||||
slug='tenant-group-1',
|
||||
parent=parent_tenant_groups[0],
|
||||
description='foobar1'
|
||||
),
|
||||
TenantGroup(
|
||||
name='Tenant Group 2',
|
||||
slug='tenant-group-2',
|
||||
parent=parent_tenant_groups[1],
|
||||
description='foobar2'
|
||||
),
|
||||
TenantGroup(
|
||||
name='Tenant Group 3',
|
||||
slug='tenant-group-3',
|
||||
parent=parent_tenant_groups[2],
|
||||
description='foobar3'
|
||||
),
|
||||
)
|
||||
for tenantgroup in tenant_groups:
|
||||
tenantgroup.save()
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Tenant Group 1', 'Tenant Group 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -39,7 +58,7 @@ class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['A', 'B']}
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_parent(self):
|
||||
@@ -68,10 +87,14 @@ class TenantTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0], description='foobar1'),
|
||||
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1], description='foobar2'),
|
||||
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
|
||||
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2], description='foobar3'),
|
||||
)
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Tenant 1', 'Tenant 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -108,13 +131,32 @@ class ContactGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
contactgroup.save()
|
||||
|
||||
contact_groups = (
|
||||
ContactGroup(name='Contact Group 1', slug='contact-group-1', parent=parent_contact_groups[0], description='A'),
|
||||
ContactGroup(name='Contact Group 2', slug='contact-group-2', parent=parent_contact_groups[1], description='B'),
|
||||
ContactGroup(name='Contact Group 3', slug='contact-group-3', parent=parent_contact_groups[2], description='C'),
|
||||
ContactGroup(
|
||||
name='Contact Group 1',
|
||||
slug='contact-group-1',
|
||||
parent=parent_contact_groups[0],
|
||||
description='foobar1'
|
||||
),
|
||||
ContactGroup(
|
||||
name='Contact Group 2',
|
||||
slug='contact-group-2',
|
||||
parent=parent_contact_groups[1],
|
||||
description='foobar2'
|
||||
),
|
||||
ContactGroup(
|
||||
name='Contact Group 3',
|
||||
slug='contact-group-3',
|
||||
parent=parent_contact_groups[2],
|
||||
description='foobar3'
|
||||
),
|
||||
)
|
||||
for contactgroup in contact_groups:
|
||||
contactgroup.save()
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Contact Group 1', 'Contact Group 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -124,7 +166,7 @@ class ContactGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['A', 'B']}
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_parent(self):
|
||||
@@ -145,10 +187,14 @@ class ContactRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
contact_roles = (
|
||||
ContactRole(name='Contact Role 1', slug='contact-role-1', description='foobar1'),
|
||||
ContactRole(name='Contact Role 2', slug='contact-role-2', description='foobar2'),
|
||||
ContactRole(name='Contact Role 3', slug='contact-role-3'),
|
||||
ContactRole(name='Contact Role 3', slug='contact-role-3', description='foobar3'),
|
||||
)
|
||||
ContactRole.objects.bulk_create(contact_roles)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Contact Role 1', 'Contact Role 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -178,16 +224,24 @@ class ContactTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
contactgroup.save()
|
||||
|
||||
contacts = (
|
||||
Contact(name='Contact 1', group=contact_groups[0]),
|
||||
Contact(name='Contact 2', group=contact_groups[1]),
|
||||
Contact(name='Contact 3', group=contact_groups[2]),
|
||||
Contact(name='Contact 1', group=contact_groups[0], description='foobar1'),
|
||||
Contact(name='Contact 2', group=contact_groups[1], description='foobar2'),
|
||||
Contact(name='Contact 3', group=contact_groups[2], description='foobar3'),
|
||||
)
|
||||
Contact.objects.bulk_create(contacts)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Contact 1', 'Contact 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_group(self):
|
||||
group = ContactGroup.objects.all()[:2]
|
||||
params = {'group_id': [group[0].pk, group[1].pk]}
|
||||
|
||||
@@ -67,6 +67,10 @@ class UserTestCase(TestCase, BaseFilterSetTests):
|
||||
users[1].groups.set([groups[1]])
|
||||
users[2].groups.set([groups[2]])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'user1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_username(self):
|
||||
params = {'username': ['User1', 'User2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -117,6 +121,10 @@ class GroupTestCase(TestCase, BaseFilterSetTests):
|
||||
)
|
||||
Group.objects.bulk_create(groups)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'group 1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Group 1', 'Group 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -164,6 +172,10 @@ class ObjectPermissionTestCase(TestCase, BaseFilterSetTests):
|
||||
permissions[i].users.set([users[i]])
|
||||
permissions[i].object_types.set([object_types[i]])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Permission 1', 'Permission 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -235,6 +247,10 @@ class TokenTestCase(TestCase, BaseFilterSetTests):
|
||||
)
|
||||
Token.objects.bulk_create(tokens)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_user(self):
|
||||
users = User.objects.order_by('id')[:2]
|
||||
params = {'user_id': [users[0].pk, users[1].pk]}
|
||||
|
||||
@@ -9,6 +9,7 @@ from drf_spectacular.types import OpenApiTypes
|
||||
__all__ = (
|
||||
'ContentTypeFilter',
|
||||
'MACAddressFilter',
|
||||
'MultiValueArrayFilter',
|
||||
'MultiValueCharFilter',
|
||||
'MultiValueDateFilter',
|
||||
'MultiValueDateTimeFilter',
|
||||
@@ -85,6 +86,21 @@ class MultiValueTimeFilter(django_filters.MultipleChoiceFilter):
|
||||
field_class = multivalue_field_factory(forms.TimeField)
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
class MultiValueArrayFilter(django_filters.MultipleChoiceFilter):
|
||||
field_class = multivalue_field_factory(forms.CharField)
|
||||
|
||||
def __init__(self, *args, lookup_expr='contains', **kwargs):
|
||||
# Set default lookup_expr to 'contains'
|
||||
super().__init__(*args, lookup_expr=lookup_expr, **kwargs)
|
||||
|
||||
def get_filter_predicate(self, v):
|
||||
# If filtering for null values, ignore lookup_expr
|
||||
if v is None:
|
||||
return {self.field_name: None}
|
||||
return super().get_filter_predicate(v)
|
||||
|
||||
|
||||
class MACAddressFilter(django_filters.CharFilter):
|
||||
pass
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class DynamicMultipleChoiceField(forms.MultipleChoiceField):
|
||||
|
||||
if data is not None:
|
||||
self.choices = [
|
||||
choice for choice in self.choices if choice[0] in data
|
||||
choice for choice in self.choices if choice[0] and choice[0] in data
|
||||
]
|
||||
|
||||
return bound_field
|
||||
|
||||
@@ -100,13 +100,14 @@ class ClusterFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
|
||||
class Meta:
|
||||
model = Cluster
|
||||
fields = ['id', 'name']
|
||||
fields = ['id', 'name', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
@@ -238,13 +239,14 @@ class VirtualMachineFilterSet(
|
||||
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
fields = ['id', 'cluster', 'vcpus', 'memory', 'disk']
|
||||
fields = ['id', 'cluster', 'vcpus', 'memory', 'disk', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value) |
|
||||
Q(primary_ip4__address__startswith=value) |
|
||||
Q(primary_ip6__address__startswith=value)
|
||||
|
||||
@@ -17,12 +17,16 @@ class ClusterTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
def setUpTestData(cls):
|
||||
|
||||
cluster_types = (
|
||||
ClusterType(name='Cluster Type 1', slug='cluster-type-1', description='A'),
|
||||
ClusterType(name='Cluster Type 2', slug='cluster-type-2', description='B'),
|
||||
ClusterType(name='Cluster Type 3', slug='cluster-type-3', description='C'),
|
||||
ClusterType(name='Cluster Type 1', slug='cluster-type-1', description='foobar1'),
|
||||
ClusterType(name='Cluster Type 2', slug='cluster-type-2', description='foobar2'),
|
||||
ClusterType(name='Cluster Type 3', slug='cluster-type-3', description='foobar3'),
|
||||
)
|
||||
ClusterType.objects.bulk_create(cluster_types)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Cluster Type 1', 'Cluster Type 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -32,7 +36,7 @@ class ClusterTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['A', 'B']}
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
@@ -44,12 +48,16 @@ class ClusterGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
def setUpTestData(cls):
|
||||
|
||||
cluster_groups = (
|
||||
ClusterGroup(name='Cluster Group 1', slug='cluster-group-1', description='A'),
|
||||
ClusterGroup(name='Cluster Group 2', slug='cluster-group-2', description='B'),
|
||||
ClusterGroup(name='Cluster Group 3', slug='cluster-group-3', description='C'),
|
||||
ClusterGroup(name='Cluster Group 1', slug='cluster-group-1', description='foobar1'),
|
||||
ClusterGroup(name='Cluster Group 2', slug='cluster-group-2', description='foobar2'),
|
||||
ClusterGroup(name='Cluster Group 3', slug='cluster-group-3', description='foobar3'),
|
||||
)
|
||||
ClusterGroup.objects.bulk_create(cluster_groups)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Cluster Group 1', 'Cluster Group 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -59,7 +67,7 @@ class ClusterGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['A', 'B']}
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
@@ -123,16 +131,48 @@ class ClusterTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
clusters = (
|
||||
Cluster(name='Cluster 1', type=cluster_types[0], group=cluster_groups[0], status=ClusterStatusChoices.STATUS_PLANNED, site=sites[0], tenant=tenants[0]),
|
||||
Cluster(name='Cluster 2', type=cluster_types[1], group=cluster_groups[1], status=ClusterStatusChoices.STATUS_STAGING, site=sites[1], tenant=tenants[1]),
|
||||
Cluster(name='Cluster 3', type=cluster_types[2], group=cluster_groups[2], status=ClusterStatusChoices.STATUS_ACTIVE, site=sites[2], tenant=tenants[2]),
|
||||
Cluster(
|
||||
name='Cluster 1',
|
||||
type=cluster_types[0],
|
||||
group=cluster_groups[0],
|
||||
status=ClusterStatusChoices.STATUS_PLANNED,
|
||||
site=sites[0],
|
||||
tenant=tenants[0],
|
||||
description='foobar1'
|
||||
),
|
||||
Cluster(
|
||||
name='Cluster 2',
|
||||
type=cluster_types[1],
|
||||
group=cluster_groups[1],
|
||||
status=ClusterStatusChoices.STATUS_STAGING,
|
||||
site=sites[1],
|
||||
tenant=tenants[1],
|
||||
description='foobar2'
|
||||
),
|
||||
Cluster(
|
||||
name='Cluster 3',
|
||||
type=cluster_types[2],
|
||||
group=cluster_groups[2],
|
||||
status=ClusterStatusChoices.STATUS_ACTIVE,
|
||||
site=sites[2],
|
||||
tenant=tenants[2],
|
||||
description='foobar3'
|
||||
),
|
||||
)
|
||||
Cluster.objects.bulk_create(clusters)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Cluster 1', 'Cluster 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
@@ -274,9 +314,49 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
vms = (
|
||||
VirtualMachine(name='Virtual Machine 1', site=sites[0], cluster=clusters[0], device=devices[0], platform=platforms[0], role=roles[0], tenant=tenants[0], status=VirtualMachineStatusChoices.STATUS_ACTIVE, vcpus=1, memory=1, disk=1, local_context_data={"foo": 123}),
|
||||
VirtualMachine(name='Virtual Machine 2', site=sites[1], cluster=clusters[1], device=devices[1], platform=platforms[1], role=roles[1], tenant=tenants[1], status=VirtualMachineStatusChoices.STATUS_STAGED, vcpus=2, memory=2, disk=2),
|
||||
VirtualMachine(name='Virtual Machine 3', site=sites[2], cluster=clusters[2], device=devices[2], platform=platforms[2], role=roles[2], tenant=tenants[2], status=VirtualMachineStatusChoices.STATUS_OFFLINE, vcpus=3, memory=3, disk=3),
|
||||
VirtualMachine(
|
||||
name='Virtual Machine 1',
|
||||
site=sites[0],
|
||||
cluster=clusters[0],
|
||||
device=devices[0],
|
||||
platform=platforms[0],
|
||||
role=roles[0],
|
||||
tenant=tenants[0],
|
||||
status=VirtualMachineStatusChoices.STATUS_ACTIVE,
|
||||
vcpus=1,
|
||||
memory=1,
|
||||
disk=1,
|
||||
description='foobar1',
|
||||
local_context_data={"foo": 123}
|
||||
),
|
||||
VirtualMachine(
|
||||
name='Virtual Machine 2',
|
||||
site=sites[1],
|
||||
cluster=clusters[1],
|
||||
device=devices[1],
|
||||
platform=platforms[1],
|
||||
role=roles[1],
|
||||
tenant=tenants[1],
|
||||
status=VirtualMachineStatusChoices.STATUS_STAGED,
|
||||
vcpus=2,
|
||||
memory=2,
|
||||
disk=2,
|
||||
description='foobar2'
|
||||
),
|
||||
VirtualMachine(
|
||||
name='Virtual Machine 3',
|
||||
site=sites[2],
|
||||
cluster=clusters[2],
|
||||
device=devices[2],
|
||||
platform=platforms[2],
|
||||
role=roles[2],
|
||||
tenant=tenants[2],
|
||||
status=VirtualMachineStatusChoices.STATUS_OFFLINE,
|
||||
vcpus=3,
|
||||
memory=3,
|
||||
disk=3,
|
||||
description='foobar3'
|
||||
),
|
||||
)
|
||||
VirtualMachine.objects.bulk_create(vms)
|
||||
|
||||
@@ -300,6 +380,10 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
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], primary_ip6=ipaddresses[4])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Virtual Machine 1', 'Virtual Machine 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -307,6 +391,10 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'name': ['VIRTUAL MACHINE 1', 'VIRTUAL MACHINE 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_vcpus(self):
|
||||
params = {'vcpus': [1, 2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -467,12 +555,40 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
VirtualMachine.objects.bulk_create(vms)
|
||||
|
||||
interfaces = (
|
||||
VMInterface(virtual_machine=vms[0], name='Interface 1', enabled=True, mtu=100, mac_address='00-00-00-00-00-01', vrf=vrfs[0], description='foobar1'),
|
||||
VMInterface(virtual_machine=vms[1], name='Interface 2', enabled=True, mtu=200, mac_address='00-00-00-00-00-02', vrf=vrfs[1], description='foobar2'),
|
||||
VMInterface(virtual_machine=vms[2], name='Interface 3', enabled=False, mtu=300, mac_address='00-00-00-00-00-03', vrf=vrfs[2]),
|
||||
VMInterface(
|
||||
virtual_machine=vms[0],
|
||||
name='Interface 1',
|
||||
enabled=True,
|
||||
mtu=100,
|
||||
mac_address='00-00-00-00-00-01',
|
||||
vrf=vrfs[0],
|
||||
description='foobar1'
|
||||
),
|
||||
VMInterface(
|
||||
virtual_machine=vms[1],
|
||||
name='Interface 2',
|
||||
enabled=True,
|
||||
mtu=200,
|
||||
mac_address='00-00-00-00-00-02',
|
||||
vrf=vrfs[1],
|
||||
description='foobar2'
|
||||
),
|
||||
VMInterface(
|
||||
virtual_machine=vms[2],
|
||||
name='Interface 3',
|
||||
enabled=False,
|
||||
mtu=300,
|
||||
mac_address='00-00-00-00-00-03',
|
||||
vrf=vrfs[2],
|
||||
description='foobar3'
|
||||
),
|
||||
)
|
||||
VMInterface.objects.bulk_create(interfaces)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Interface 1', 'Interface 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
@@ -36,6 +36,10 @@ class WirelessLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
for group in child_groups:
|
||||
group.save()
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Wireless LAN Group 1', 'Wireless LAN Group 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -103,7 +107,8 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
tenant=tenants[0],
|
||||
auth_type=WirelessAuthTypeChoices.TYPE_OPEN,
|
||||
auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO,
|
||||
auth_psk='PSK1'
|
||||
auth_psk='PSK1',
|
||||
description='foobar1'
|
||||
),
|
||||
WirelessLAN(
|
||||
ssid='WLAN2',
|
||||
@@ -113,7 +118,8 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
tenant=tenants[1],
|
||||
auth_type=WirelessAuthTypeChoices.TYPE_WEP,
|
||||
auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP,
|
||||
auth_psk='PSK2'
|
||||
auth_psk='PSK2',
|
||||
description='foobar2'
|
||||
),
|
||||
WirelessLAN(
|
||||
ssid='WLAN3',
|
||||
@@ -123,11 +129,16 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
tenant=tenants[2],
|
||||
auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL,
|
||||
auth_cipher=WirelessAuthCipherChoices.CIPHER_AES,
|
||||
auth_psk='PSK3'
|
||||
auth_psk='PSK3',
|
||||
description='foobar3'
|
||||
),
|
||||
)
|
||||
WirelessLAN.objects.bulk_create(wireless_lans)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_ssid(self):
|
||||
params = {'ssid': ['WLAN1', 'WLAN2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -160,6 +171,10 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'auth_psk': ['PSK1', 'PSK2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_tenant(self):
|
||||
tenants = Tenant.objects.all()[:2]
|
||||
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
|
||||
@@ -240,6 +255,10 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
ssid='LINK4'
|
||||
).save()
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_ssid(self):
|
||||
params = {'ssid': ['LINK1', 'LINK2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
@@ -9,7 +9,7 @@ django-pglocks==1.0.4
|
||||
django-prometheus==2.3.1
|
||||
django-redis==5.4.0
|
||||
django-rich==1.8.0
|
||||
django-rq==2.9.0
|
||||
django-rq==2.10.1
|
||||
django-tables2==2.7.0
|
||||
django-taggit==4.0.0
|
||||
django-timezone-field==6.1.0
|
||||
@@ -21,11 +21,11 @@ graphene-django==3.0.0
|
||||
gunicorn==21.2.0
|
||||
Jinja2==3.1.2
|
||||
Markdown==3.3.7
|
||||
mkdocs-material==9.5.2
|
||||
mkdocs-material==9.5.3
|
||||
mkdocstrings[python-legacy]==0.24.0
|
||||
netaddr==0.9.0
|
||||
Pillow==10.1.0
|
||||
psycopg[binary,pool]==3.1.15
|
||||
psycopg[binary,pool]==3.1.16
|
||||
PyYAML==6.0.1
|
||||
requests==2.31.0
|
||||
sentry-sdk==1.39.1
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
# Python 3.8 or later.
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
NETBOX_VERSION="$(grep ^VERSION netbox/netbox/settings.py | cut -d\' -f2)"
|
||||
echo "You are installing (or upgrading to) NetBox version ${NETBOX_VERSION}"
|
||||
|
||||
VIRTUALENV="$(pwd -P)/venv"
|
||||
PYTHON="${PYTHON:-python3}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user