mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-21 04:42:22 -06:00
Merge branch 'feature' of https://github.com/netbox-community/netbox into feature
# Conflicts: # netbox/dcim/tables/template_code.py # netbox/netbox/forms.py # netbox/templates/dcim/cable_connect.html # netbox/templates/dcim/consoleport.html # netbox/templates/dcim/consoleserverport.html # netbox/templates/dcim/device.html # netbox/templates/dcim/device/base.html # netbox/templates/dcim/device_edit.html # netbox/templates/dcim/interface.html # netbox/templates/dcim/rack.html # netbox/templates/dcim/rack_edit.html # netbox/templates/dcim/site.html # netbox/templates/extras/configcontext.html # netbox/templates/extras/objectchange.html # netbox/templates/generic/object.html # netbox/templates/inc/nav_menu.html # netbox/templates/ipam/ipaddress_edit.html # netbox/templates/ipam/vrf.html # netbox/utilities/templates/buttons/export.html
This commit is contained in:
@@ -21,7 +21,7 @@ class NestedProviderSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'url', 'name', 'slug', 'circuit_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
#
|
||||
@@ -34,7 +34,7 @@ class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'url', 'name', 'slug', 'circuit_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
class NestedCircuitSerializer(WritableNestedSerializer):
|
||||
@@ -42,7 +42,7 @@ class NestedCircuitSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'url', 'cid']
|
||||
fields = ['id', 'url', 'display', 'cid']
|
||||
|
||||
|
||||
class NestedCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
@@ -51,4 +51,4 @@ class NestedCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['id', 'url', 'circuit', 'term_side', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display', 'circuit', 'term_side', 'cable', '_occupied']
|
||||
|
||||
@@ -4,10 +4,10 @@ from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
||||
from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer
|
||||
from netbox.api.serializers import CustomFieldModelSerializer
|
||||
from extras.api.serializers import TaggedObjectSerializer
|
||||
from netbox.api import ChoiceField
|
||||
from netbox.api.serializers import OrganizationalModelSerializer, WritableNestedSerializer
|
||||
from netbox.api.serializers import (
|
||||
BaseModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer
|
||||
)
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from .nested_serializers import *
|
||||
|
||||
@@ -16,15 +16,15 @@ from .nested_serializers import *
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class ProviderSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
]
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ class CircuitTypeSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'circuit_count',
|
||||
]
|
||||
|
||||
|
||||
@@ -50,12 +51,12 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEnd
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'connected_endpoint',
|
||||
'id', 'url', 'display', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'connected_endpoint',
|
||||
'connected_endpoint_type', 'connected_endpoint_reachable',
|
||||
]
|
||||
|
||||
|
||||
class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class CircuitSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
provider = NestedProviderSerializer()
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
@@ -67,12 +68,13 @@ class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'id', 'url', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
|
||||
'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate',
|
||||
'description', 'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class CircuitTerminationSerializer(CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = NestedCircuitSerializer()
|
||||
site = NestedSiteSerializer()
|
||||
@@ -81,7 +83,7 @@ class CircuitTerminationSerializer(CableTerminationSerializer, ConnectedEndpoint
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
||||
'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint',
|
||||
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info', 'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint',
|
||||
'connected_endpoint_type', 'connected_endpoint_reachable', '_occupied',
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@ import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.filters import CableTerminationFilterSet, PathEndpointFilterSet
|
||||
from dcim.models import Region, Site
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from utilities.filters import (
|
||||
@@ -37,6 +37,19 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdated
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='circuits__terminations__site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='circuits__terminations__site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuits__terminations__site',
|
||||
queryset=Site.objects.all(),
|
||||
@@ -102,17 +115,6 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
|
||||
choices=CircuitStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='terminations__site__region',
|
||||
@@ -126,6 +128,30 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='terminations__site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='terminations__site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.models import Region, Site
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from extras.forms import (
|
||||
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
|
||||
)
|
||||
@@ -142,6 +142,20 @@ class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||
]
|
||||
|
||||
|
||||
class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
class CircuitTypeCSVForm(CustomFieldModelCSVForm):
|
||||
slug = SlugField()
|
||||
|
||||
@@ -320,18 +334,26 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
query_params={
|
||||
'region_id': '$region'
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'term_side', 'region', 'site', 'mark_connected', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
||||
'description',
|
||||
'term_side', 'region', 'site_group', 'site', 'mark_connected', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description',
|
||||
]
|
||||
help_texts = {
|
||||
'port_speed': "Physical circuit speed",
|
||||
|
||||
@@ -34,4 +34,14 @@ class Migration(migrations.Migration):
|
||||
name='id',
|
||||
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.fields import ASNField
|
||||
from dcim.models import CableTermination, PathEndpoint
|
||||
from extras.models import ObjectChange, TaggedItem
|
||||
from extras.models import ObjectChange
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import BigIDModel, OrganizationalModel, PrimaryModel
|
||||
from netbox.models import BigIDModel, ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import serialize_object
|
||||
from .choices import *
|
||||
from .querysets import CircuitQuerySet
|
||||
|
||||
@@ -61,7 +59,6 @@ class Provider(PrimaryModel):
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
@@ -185,7 +182,6 @@ class Circuit(PrimaryModel):
|
||||
)
|
||||
|
||||
objects = CircuitQuerySet.as_manager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||
@@ -235,7 +231,8 @@ class Circuit(PrimaryModel):
|
||||
return self._get_termination('Z')
|
||||
|
||||
|
||||
class CircuitTermination(BigIDModel, PathEndpoint, CableTermination):
|
||||
@extras_features('webhooks')
|
||||
class CircuitTermination(ChangeLoggedModel, PathEndpoint, CableTermination):
|
||||
circuit = models.ForeignKey(
|
||||
to='circuits.Circuit',
|
||||
on_delete=models.CASCADE,
|
||||
@@ -289,21 +286,14 @@ class CircuitTermination(BigIDModel, PathEndpoint, CableTermination):
|
||||
def to_objectchange(self, action):
|
||||
# Annotate the parent Circuit
|
||||
try:
|
||||
related_object = self.circuit
|
||||
circuit = self.circuit
|
||||
except Circuit.DoesNotExist:
|
||||
# Parent circuit has been deleted
|
||||
related_object = None
|
||||
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
related_object=related_object,
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
circuit = None
|
||||
return super().to_objectchange(action, related_object=circuit)
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
def parent_object(self):
|
||||
return self.circuit
|
||||
|
||||
def get_peer_termination(self):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from tenancy.tables import COL_TENANT
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
|
||||
@@ -60,9 +60,7 @@ class CircuitTable(BaseTable):
|
||||
linkify=True
|
||||
)
|
||||
status = ChoiceFieldColumn()
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
a_side = tables.Column(
|
||||
verbose_name='A Side'
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ class AppTest(APITestCase):
|
||||
|
||||
class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Provider
|
||||
brief_fields = ['circuit_count', 'id', 'name', 'slug', 'url']
|
||||
brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Provider 4',
|
||||
@@ -49,7 +49,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitType
|
||||
brief_fields = ['circuit_count', 'id', 'name', 'slug', 'url']
|
||||
brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
|
||||
create_data = (
|
||||
{
|
||||
'name': 'Circuit Type 4',
|
||||
@@ -81,7 +81,7 @@ class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class CircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Circuit
|
||||
brief_fields = ['cid', 'id', 'url']
|
||||
brief_fields = ['cid', 'display', 'id', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
@@ -129,7 +129,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitTermination
|
||||
brief_fields = ['_occupied', 'cable', 'circuit', 'id', 'term_side', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'circuit', 'display', 'id', 'term_side', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.test import TestCase
|
||||
from circuits.choices import *
|
||||
from circuits.filters import *
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from dcim.models import Cable, Region, Site
|
||||
from dcim.models import Cable, Region, Site, SiteGroup
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
|
||||
|
||||
@@ -27,13 +27,20 @@ class ProviderTestCase(TestCase):
|
||||
Region(name='Test Region 1', slug='test-region-1'),
|
||||
Region(name='Test Region 2', slug='test-region-2'),
|
||||
)
|
||||
# Can't use bulk_create for models with MPTT fields
|
||||
for r in regions:
|
||||
r.save()
|
||||
|
||||
site_groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for site_group in site_groups:
|
||||
site_group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0]),
|
||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1]),
|
||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
|
||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@@ -74,13 +81,6 @@ class ProviderTestCase(TestCase):
|
||||
params = {'account': ['1234', '2345']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
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]}
|
||||
@@ -88,6 +88,20 @@ class ProviderTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class CircuitTypeTestCase(TestCase):
|
||||
queryset = CircuitType.objects.all()
|
||||
@@ -127,14 +141,21 @@ class CircuitTestCase(TestCase):
|
||||
Region(name='Test Region 2', slug='test-region-2'),
|
||||
Region(name='Test Region 3', slug='test-region-3'),
|
||||
)
|
||||
# Can't use bulk_create for models with MPTT fields
|
||||
for r in regions:
|
||||
r.save()
|
||||
|
||||
site_groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for site_group in site_groups:
|
||||
site_group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0]),
|
||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1]),
|
||||
Site(name='Test Site 3', slug='test-site-3', region=regions[2]),
|
||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
|
||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
|
||||
Site(name='Test Site 3', slug='test-site-3', region=regions[2], group=site_groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@@ -223,6 +244,13 @@ class CircuitTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
|
||||
@@ -73,6 +73,10 @@ class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
"Circuit Type 6,circuit-type-6",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'Foo',
|
||||
}
|
||||
|
||||
|
||||
class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Circuit
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from dcim.views import CableCreateView, PathTraceView
|
||||
from extras.views import ObjectChangeLogView
|
||||
from extras.views import ObjectChangeLogView, ObjectJournalView
|
||||
from . import views
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
|
||||
@@ -18,11 +18,13 @@ urlpatterns = [
|
||||
path('providers/<int:pk>/edit/', views.ProviderEditView.as_view(), name='provider_edit'),
|
||||
path('providers/<int:pk>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'),
|
||||
path('providers/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
|
||||
path('providers/<int:pk>/journal/', ObjectJournalView.as_view(), name='provider_journal', kwargs={'model': Provider}),
|
||||
|
||||
# Circuit types
|
||||
path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
|
||||
path('circuit-types/add/', views.CircuitTypeEditView.as_view(), name='circuittype_add'),
|
||||
path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
|
||||
path('circuit-types/edit/', views.CircuitTypeBulkEditView.as_view(), name='circuittype_bulk_edit'),
|
||||
path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
|
||||
path('circuit-types/<int:pk>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
|
||||
path('circuit-types/<int:pk>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'),
|
||||
@@ -38,6 +40,7 @@ urlpatterns = [
|
||||
path('circuits/<int:pk>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'),
|
||||
path('circuits/<int:pk>/delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'),
|
||||
path('circuits/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
|
||||
path('circuits/<int:pk>/journal/', ObjectJournalView.as_view(), name='circuit_journal', kwargs={'model': Circuit}),
|
||||
path('circuits/<int:pk>/terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'),
|
||||
|
||||
# Circuit terminations
|
||||
|
||||
@@ -107,6 +107,15 @@ class CircuitTypeBulkImportView(generic.BulkImportView):
|
||||
table = tables.CircuitTypeTable
|
||||
|
||||
|
||||
class CircuitTypeBulkEditView(generic.BulkEditView):
|
||||
queryset = CircuitType.objects.annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
)
|
||||
filterset = filters.CircuitTypeFilterSet
|
||||
table = tables.CircuitTypeTable
|
||||
form = forms.CircuitTypeBulkEditForm
|
||||
|
||||
|
||||
class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CircuitType.objects.annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim import models
|
||||
from netbox.api import WritableNestedSerializer
|
||||
from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedCableSerializer',
|
||||
@@ -27,7 +27,7 @@ __all__ = [
|
||||
'NestedPowerPanelSerializer',
|
||||
'NestedPowerPortSerializer',
|
||||
'NestedPowerPortTemplateSerializer',
|
||||
'NestedRackGroupSerializer',
|
||||
'NestedLocationSerializer',
|
||||
'NestedRackReservationSerializer',
|
||||
'NestedRackRoleSerializer',
|
||||
'NestedRackSerializer',
|
||||
@@ -35,6 +35,7 @@ __all__ = [
|
||||
'NestedRearPortTemplateSerializer',
|
||||
'NestedRegionSerializer',
|
||||
'NestedSiteSerializer',
|
||||
'NestedSiteGroupSerializer',
|
||||
'NestedVirtualChassisSerializer',
|
||||
]
|
||||
|
||||
@@ -50,7 +51,17 @@ class NestedRegionSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Region
|
||||
fields = ['id', 'url', 'name', 'slug', 'site_count', '_depth']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||
|
||||
|
||||
class NestedSiteGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.SiteGroup
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||
|
||||
|
||||
class NestedSiteSerializer(WritableNestedSerializer):
|
||||
@@ -58,21 +69,21 @@ class NestedSiteSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Site
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
class NestedRackGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
|
||||
class NestedLocationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.RackGroup
|
||||
fields = ['id', 'url', 'name', 'slug', 'rack_count', '_depth']
|
||||
model = models.Location
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count', '_depth']
|
||||
|
||||
|
||||
class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||
@@ -81,7 +92,7 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.RackRole
|
||||
fields = ['id', 'url', 'name', 'slug', 'rack_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count']
|
||||
|
||||
|
||||
class NestedRackSerializer(WritableNestedSerializer):
|
||||
@@ -90,7 +101,7 @@ class NestedRackSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Rack
|
||||
fields = ['id', 'url', 'name', 'display_name', 'device_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'display_name', 'device_count']
|
||||
|
||||
|
||||
class NestedRackReservationSerializer(WritableNestedSerializer):
|
||||
@@ -99,7 +110,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.RackReservation
|
||||
fields = ['id', 'url', 'user', 'units']
|
||||
fields = ['id', 'url', 'display', 'user', 'units']
|
||||
|
||||
def get_user(self, obj):
|
||||
return obj.user.username
|
||||
@@ -115,7 +126,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Manufacturer
|
||||
fields = ['id', 'url', 'name', 'slug', 'devicetype_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'devicetype_count']
|
||||
|
||||
|
||||
class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
@@ -125,7 +136,7 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceType
|
||||
fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'device_count']
|
||||
fields = ['id', 'url', 'display', 'manufacturer', 'model', 'slug', 'display_name', 'device_count']
|
||||
|
||||
|
||||
class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
|
||||
@@ -133,7 +144,7 @@ class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.ConsolePortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedConsoleServerPortTemplateSerializer(WritableNestedSerializer):
|
||||
@@ -141,7 +152,7 @@ class NestedConsoleServerPortTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.ConsoleServerPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedPowerPortTemplateSerializer(WritableNestedSerializer):
|
||||
@@ -149,7 +160,7 @@ class NestedPowerPortTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedPowerOutletTemplateSerializer(WritableNestedSerializer):
|
||||
@@ -157,7 +168,7 @@ class NestedPowerOutletTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerOutletTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
|
||||
@@ -165,7 +176,7 @@ class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.InterfaceTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedRearPortTemplateSerializer(WritableNestedSerializer):
|
||||
@@ -173,7 +184,7 @@ class NestedRearPortTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.RearPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
|
||||
@@ -181,7 +192,7 @@ class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.FrontPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
|
||||
@@ -189,7 +200,7 @@ class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceBayTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
#
|
||||
@@ -203,7 +214,7 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceRole
|
||||
fields = ['id', 'url', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
|
||||
|
||||
class NestedPlatformSerializer(WritableNestedSerializer):
|
||||
@@ -213,7 +224,7 @@ class NestedPlatformSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Platform
|
||||
fields = ['id', 'url', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
|
||||
|
||||
class NestedDeviceSerializer(WritableNestedSerializer):
|
||||
@@ -221,7 +232,7 @@ class NestedDeviceSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Device
|
||||
fields = ['id', 'url', 'name', 'display_name']
|
||||
fields = ['id', 'url', 'display', 'name', 'display_name']
|
||||
|
||||
|
||||
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
@@ -230,7 +241,7 @@ class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.ConsoleServerPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedConsolePortSerializer(WritableNestedSerializer):
|
||||
@@ -239,7 +250,7 @@ class NestedConsolePortSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.ConsolePort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
@@ -248,7 +259,7 @@ class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerOutlet
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedPowerPortSerializer(WritableNestedSerializer):
|
||||
@@ -257,7 +268,7 @@ class NestedPowerPortSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
@@ -266,7 +277,7 @@ class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Interface
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedRearPortSerializer(WritableNestedSerializer):
|
||||
@@ -275,7 +286,7 @@ class NestedRearPortSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.RearPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedFrontPortSerializer(WritableNestedSerializer):
|
||||
@@ -284,7 +295,7 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.FrontPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
@@ -293,7 +304,7 @@ class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceBay
|
||||
fields = ['id', 'url', 'device', 'name']
|
||||
fields = ['id', 'url', 'display', 'device', 'name']
|
||||
|
||||
|
||||
class NestedInventoryItemSerializer(WritableNestedSerializer):
|
||||
@@ -303,19 +314,19 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItem
|
||||
fields = ['id', 'url', 'device', 'name', '_depth']
|
||||
fields = ['id', 'url', 'display', 'device', 'name', '_depth']
|
||||
|
||||
|
||||
#
|
||||
# Cables
|
||||
#
|
||||
|
||||
class NestedCableSerializer(serializers.ModelSerializer):
|
||||
class NestedCableSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Cable
|
||||
fields = ['id', 'url', 'label']
|
||||
fields = ['id', 'url', 'display', 'label']
|
||||
|
||||
|
||||
#
|
||||
@@ -342,7 +353,7 @@ class NestedPowerPanelSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPanel
|
||||
fields = ['id', 'url', 'name', 'powerfeed_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'powerfeed_count']
|
||||
|
||||
|
||||
class NestedPowerFeedSerializer(WritableNestedSerializer):
|
||||
@@ -350,4 +361,4 @@ class NestedPowerFeedSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerFeed
|
||||
fields = ['id', 'url', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display', 'name', 'cable', '_occupied']
|
||||
|
||||
@@ -6,20 +6,13 @@ from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from netbox.api.serializers import CustomFieldModelSerializer
|
||||
from extras.api.serializers import TaggedObjectSerializer
|
||||
from dcim.models import *
|
||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
||||
from ipam.models import VLAN
|
||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField
|
||||
from netbox.api.serializers import (
|
||||
NestedGroupModelSerializer, OrganizationalModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
|
||||
NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer,
|
||||
WritableNestedSerializer,
|
||||
)
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
@@ -49,7 +42,7 @@ class CableTerminationSerializer(serializers.ModelSerializer):
|
||||
return None
|
||||
|
||||
|
||||
class ConnectedEndpointSerializer(CustomFieldModelSerializer):
|
||||
class ConnectedEndpointSerializer(serializers.ModelSerializer):
|
||||
connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
|
||||
connected_endpoint = serializers.SerializerMethodField(read_only=True)
|
||||
connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
|
||||
@@ -89,15 +82,29 @@ class RegionSerializer(NestedGroupModelSerializer):
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'site_count', '_depth',
|
||||
]
|
||||
|
||||
|
||||
class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class SiteGroupSerializer(NestedGroupModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
||||
parent = NestedRegionSerializer(required=False, allow_null=True)
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'site_count', '_depth',
|
||||
]
|
||||
|
||||
|
||||
class SiteSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
|
||||
status = ChoiceField(choices=SiteStatusChoices, required=False)
|
||||
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||
group = NestedSiteGroupSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
time_zone = TimeZoneField(required=False)
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
@@ -110,10 +117,10 @@ class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn',
|
||||
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
|
||||
'contact_phone', 'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'circuit_count', 'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count',
|
||||
]
|
||||
|
||||
|
||||
@@ -121,17 +128,17 @@ class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackGroupSerializer(NestedGroupModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
|
||||
class LocationSerializer(NestedGroupModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
||||
site = NestedSiteSerializer()
|
||||
parent = NestedRackGroupSerializer(required=False, allow_null=True)
|
||||
parent = NestedLocationSerializer(required=False, allow_null=True)
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
model = Location
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'site', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'rack_count', '_depth',
|
||||
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'description', 'custom_fields', 'created',
|
||||
'last_updated', 'rack_count', '_depth',
|
||||
]
|
||||
|
||||
|
||||
@@ -142,15 +149,15 @@ class RackRoleSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'color', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'rack_count',
|
||||
]
|
||||
|
||||
|
||||
class RackSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class RackSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||
site = NestedSiteSerializer()
|
||||
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
|
||||
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=RackStatusChoices, required=False)
|
||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||
@@ -163,21 +170,22 @@ class RackSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = [
|
||||
'id', 'url', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'status', 'role', 'serial',
|
||||
'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
|
||||
'id', 'url', 'display', 'name', 'facility_id', 'display_name', 'site', 'location', 'tenant', 'status',
|
||||
'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
|
||||
'outer_unit', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
|
||||
'powerfeed_count',
|
||||
]
|
||||
# Omit the UniqueTogetherValidator that would be automatically added to validate (group, facility_id). This
|
||||
# Omit the UniqueTogetherValidator that would be automatically added to validate (location, facility_id). This
|
||||
# prevents facility_id from being interpreted as a required field.
|
||||
validators = [
|
||||
UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'name'))
|
||||
UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'name'))
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
# Validate uniqueness of (group, facility_id) since we omitted the automatically-created validator from Meta.
|
||||
# Validate uniqueness of (location, facility_id) since we omitted the automatically-created validator from Meta.
|
||||
if data.get('facility_id', None):
|
||||
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'facility_id'))
|
||||
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'facility_id'))
|
||||
validator(data, self)
|
||||
|
||||
# Enforce model validation
|
||||
@@ -197,7 +205,7 @@ class RackUnitSerializer(serializers.Serializer):
|
||||
occupied = serializers.BooleanField(read_only=True)
|
||||
|
||||
|
||||
class RackReservationSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class RackReservationSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
|
||||
rack = NestedRackSerializer()
|
||||
user = NestedUserSerializer()
|
||||
@@ -205,7 +213,10 @@ class RackReservationSerializer(TaggedObjectSerializer, CustomFieldModelSerializ
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ['id', 'url', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags', 'custom_fields']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags',
|
||||
'custom_fields',
|
||||
]
|
||||
|
||||
|
||||
class RackElevationDetailFilterSerializer(serializers.Serializer):
|
||||
@@ -257,12 +268,12 @@ class ManufacturerSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated', 'devicetype_count',
|
||||
'inventoryitem_count', 'platform_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'devicetype_count', 'inventoryitem_count', 'platform_count',
|
||||
]
|
||||
|
||||
|
||||
class DeviceTypeSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class DeviceTypeSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||
manufacturer = NestedManufacturerSerializer()
|
||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
||||
@@ -271,9 +282,9 @@ class DeviceTypeSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height', 'is_full_depth',
|
||||
'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'device_count',
|
||||
'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height',
|
||||
'is_full_depth', 'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'device_count',
|
||||
]
|
||||
|
||||
|
||||
@@ -288,7 +299,9 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'description', 'created', 'last_updated']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'description', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@@ -302,7 +315,9 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'description', 'created', 'last_updated']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'description', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@@ -317,8 +332,8 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = [
|
||||
'id', 'url', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
|
||||
'description', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@@ -342,8 +357,8 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = [
|
||||
'id', 'url', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'created',
|
||||
'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@@ -355,7 +370,8 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = [
|
||||
'id', 'url', 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'created',
|
||||
'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@@ -367,7 +383,8 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = [
|
||||
'id', 'url', 'device_type', 'name', 'label', 'type', 'positions', 'description', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'positions', 'description', 'created',
|
||||
'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@@ -380,8 +397,8 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
'id', 'url', 'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position',
|
||||
'description', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@@ -391,7 +408,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = ['id', 'url', 'device_type', 'name', 'label', 'description', 'created', 'last_updated']
|
||||
fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated']
|
||||
|
||||
|
||||
#
|
||||
@@ -406,8 +423,8 @@ class DeviceRoleSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'color', 'vm_role', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'device_count', 'virtualmachine_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'custom_fields', 'created',
|
||||
'last_updated', 'device_count', 'virtualmachine_count',
|
||||
]
|
||||
|
||||
|
||||
@@ -420,18 +437,19 @@ class PlatformSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'custom_fields',
|
||||
'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
|
||||
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||
]
|
||||
|
||||
|
||||
class DeviceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class DeviceSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
device_role = NestedDeviceRoleSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||
site = NestedSiteSerializer()
|
||||
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
||||
rack = NestedRackSerializer(required=False, allow_null=True)
|
||||
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, required=False)
|
||||
status = ChoiceField(choices=DeviceStatusChoices, required=False)
|
||||
@@ -445,10 +463,10 @@ class DeviceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = [
|
||||
'id', 'url', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial',
|
||||
'asset_tag', 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
|
||||
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data',
|
||||
'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform',
|
||||
'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status',
|
||||
'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority',
|
||||
'comments', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
validators = []
|
||||
|
||||
@@ -481,10 +499,10 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
|
||||
class Meta(DeviceSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'url', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial',
|
||||
'asset_tag', 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
|
||||
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data',
|
||||
'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform',
|
||||
'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status',
|
||||
'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority',
|
||||
'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@@ -496,7 +514,11 @@ class DeviceNAPALMSerializer(serializers.Serializer):
|
||||
method = serializers.DictField()
|
||||
|
||||
|
||||
class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsoleServerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@@ -504,18 +526,23 @@ class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerial
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
speed = ChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'description', 'mark_connected', 'cable', 'cable_peer',
|
||||
'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
|
||||
'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class ConsolePortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@@ -523,18 +550,23 @@ class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
speed = ChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'description', 'mark_connected', 'cable', 'cable_peer',
|
||||
'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
|
||||
'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class PowerOutletSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@@ -557,13 +589,13 @@ class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer,
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'mark_connected',
|
||||
'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||
'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class PowerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@@ -576,16 +608,17 @@ class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||
'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
@@ -601,10 +634,11 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
|
||||
'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_peer',
|
||||
'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'count_ipaddresses', '_occupied',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
|
||||
'mgmt_only', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable',
|
||||
'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
|
||||
'_occupied',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
@@ -621,7 +655,7 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
|
||||
return super().validate(data)
|
||||
|
||||
|
||||
class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, CustomFieldModelSerializer):
|
||||
class RearPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
@@ -630,8 +664,8 @@ class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Cus
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable',
|
||||
'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'positions', 'description', 'mark_connected',
|
||||
'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
@@ -643,10 +677,10 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['id', 'url', 'name', 'label']
|
||||
fields = ['id', 'url', 'display', 'name', 'label']
|
||||
|
||||
|
||||
class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, CustomFieldModelSerializer):
|
||||
class FrontPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
@@ -656,13 +690,13 @@ class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Cu
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description',
|
||||
'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
class DeviceBaySerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class DeviceBaySerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
installed_device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||
@@ -670,8 +704,8 @@ class DeviceBaySerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'description', 'installed_device', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@@ -679,7 +713,7 @@ class DeviceBaySerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
class InventoryItemSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class InventoryItemSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
# Provide a default value to satisfy UniqueTogetherValidator
|
||||
@@ -690,8 +724,8 @@ class InventoryItemSerializer(TaggedObjectSerializer, CustomFieldModelSerializer
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = [
|
||||
'id', 'url', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||
'discovered', 'description', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
|
||||
'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial',
|
||||
'asset_tag', 'discovered', 'description', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
|
||||
]
|
||||
|
||||
|
||||
@@ -699,7 +733,7 @@ class InventoryItemSerializer(TaggedObjectSerializer, CustomFieldModelSerializer
|
||||
# Cables
|
||||
#
|
||||
|
||||
class CableSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class CableSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||
termination_a_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
|
||||
@@ -715,7 +749,7 @@ class CableSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'id', 'url', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type',
|
||||
'id', 'url', 'display', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type',
|
||||
'termination_b_id', 'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
|
||||
'custom_fields',
|
||||
]
|
||||
@@ -829,24 +863,24 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer):
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class VirtualChassisSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||
master = NestedDeviceSerializer(required=False)
|
||||
member_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'url', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count']
|
||||
|
||||
|
||||
#
|
||||
# Power panels
|
||||
#
|
||||
|
||||
class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class PowerPanelSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
||||
site = NestedSiteSerializer()
|
||||
rack_group = NestedRackGroupSerializer(
|
||||
location = NestedLocationSerializer(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
default=None
|
||||
@@ -855,15 +889,10 @@ class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = ['id', 'url', 'site', 'rack_group', 'name', 'tags', 'custom_fields', 'powerfeed_count']
|
||||
fields = ['id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count']
|
||||
|
||||
|
||||
class PowerFeedSerializer(
|
||||
TaggedObjectSerializer,
|
||||
CableTerminationSerializer,
|
||||
ConnectedEndpointSerializer,
|
||||
CustomFieldModelSerializer
|
||||
):
|
||||
class PowerFeedSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
||||
power_panel = NestedPowerPanelSerializer()
|
||||
rack = NestedRackSerializer(
|
||||
@@ -892,8 +921,8 @@ class PowerFeedSerializer(
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = [
|
||||
'id', 'url', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
|
||||
'max_utilization', 'comments', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
|
||||
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
||||
'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
|
||||
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
@@ -7,10 +7,11 @@ router.APIRootView = views.DCIMRootView
|
||||
|
||||
# Sites
|
||||
router.register('regions', views.RegionViewSet)
|
||||
router.register('site-groups', views.SiteGroupViewSet)
|
||||
router.register('sites', views.SiteViewSet)
|
||||
|
||||
# Racks
|
||||
router.register('rack-groups', views.RackGroupViewSet)
|
||||
router.register('locations', views.LocationViewSet)
|
||||
router.register('rack-roles', views.RackRoleViewSet)
|
||||
router.register('racks', views.RackViewSet)
|
||||
router.register('rack-reservations', views.RackReservationViewSet)
|
||||
|
||||
@@ -16,13 +16,7 @@ from rest_framework.viewsets import GenericViewSet, ViewSet
|
||||
|
||||
from circuits.models import Circuit
|
||||
from dcim import filters
|
||||
from dcim.models import (
|
||||
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from dcim.models import *
|
||||
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
|
||||
from ipam.models import Prefix, VLAN
|
||||
from netbox.api.views import ModelViewSet
|
||||
@@ -111,6 +105,22 @@ class RegionViewSet(CustomFieldModelViewSet):
|
||||
filterset_class = filters.RegionFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Site groups
|
||||
#
|
||||
|
||||
class SiteGroupViewSet(CustomFieldModelViewSet):
|
||||
queryset = SiteGroup.objects.add_related_count(
|
||||
SiteGroup.objects.all(),
|
||||
Site,
|
||||
'group',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
serializer_class = serializers.SiteGroupSerializer
|
||||
filterset_class = filters.SiteGroupFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
@@ -134,16 +144,16 @@ class SiteViewSet(CustomFieldModelViewSet):
|
||||
# Rack groups
|
||||
#
|
||||
|
||||
class RackGroupViewSet(CustomFieldModelViewSet):
|
||||
queryset = RackGroup.objects.add_related_count(
|
||||
RackGroup.objects.all(),
|
||||
class LocationViewSet(CustomFieldModelViewSet):
|
||||
queryset = Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Rack,
|
||||
'group',
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site')
|
||||
serializer_class = serializers.RackGroupSerializer
|
||||
filterset_class = filters.RackGroupFilterSet
|
||||
serializer_class = serializers.LocationSerializer
|
||||
filterset_class = filters.LocationFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -164,7 +174,7 @@ class RackRoleViewSet(CustomFieldModelViewSet):
|
||||
|
||||
class RackViewSet(CustomFieldModelViewSet):
|
||||
queryset = Rack.objects.prefetch_related(
|
||||
'site', 'group__site', 'role', 'tenant', 'tags'
|
||||
'site', 'location__site', 'role', 'tenant', 'tags'
|
||||
).annotate(
|
||||
device_count=count_related(Device, 'rack'),
|
||||
powerfeed_count=count_related(PowerFeed, 'rack')
|
||||
@@ -345,7 +355,7 @@ class PlatformViewSet(CustomFieldModelViewSet):
|
||||
|
||||
class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
||||
queryset = Device.objects.prefetch_related(
|
||||
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
|
||||
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
|
||||
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
|
||||
)
|
||||
filterset_class = filters.DeviceFilterSet
|
||||
@@ -522,7 +532,7 @@ class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
|
||||
|
||||
class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = Interface.objects.prefetch_related(
|
||||
'device', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
|
||||
'device', 'parent', 'lag', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
|
||||
)
|
||||
serializer_class = serializers.InterfaceSerializer
|
||||
filterset_class = filters.InterfaceFilterSet
|
||||
@@ -619,7 +629,7 @@ class VirtualChassisViewSet(ModelViewSet):
|
||||
|
||||
class PowerPanelViewSet(ModelViewSet):
|
||||
queryset = PowerPanel.objects.prefetch_related(
|
||||
'site', 'rack_group'
|
||||
'site', 'location'
|
||||
).annotate(
|
||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||
)
|
||||
|
||||
@@ -217,6 +217,29 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortSpeedChoices(ChoiceSet):
|
||||
|
||||
SPEED_1200 = 1200
|
||||
SPEED_2400 = 2400
|
||||
SPEED_4800 = 4800
|
||||
SPEED_9600 = 9600
|
||||
SPEED_19200 = 19200
|
||||
SPEED_38400 = 38400
|
||||
SPEED_57600 = 57600
|
||||
SPEED_115200 = 115200
|
||||
|
||||
CHOICES = (
|
||||
(SPEED_1200, '1200 bps'),
|
||||
(SPEED_2400, '2400 bps'),
|
||||
(SPEED_4800, '4800 bps'),
|
||||
(SPEED_9600, '9600 bps'),
|
||||
(SPEED_19200, '19.2 kbps'),
|
||||
(SPEED_38400, '38.4 kbps'),
|
||||
(SPEED_57600, '57.6 kbps'),
|
||||
(SPEED_115200, '115.2 kbps'),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# PowerPorts
|
||||
#
|
||||
@@ -686,6 +709,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_8GFC_SFP_PLUS = '8gfc-sfpp'
|
||||
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
|
||||
TYPE_32GFC_SFP28 = '32gfc-sfp28'
|
||||
TYPE_64GFC_QSFP_PLUS = '64gfc-qsfpp'
|
||||
TYPE_128GFC_QSFP28 = '128gfc-sfp28'
|
||||
|
||||
# InfiniBand
|
||||
@@ -801,6 +825,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'),
|
||||
(TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'),
|
||||
(TYPE_32GFC_SFP28, 'SFP28 (32GFC)'),
|
||||
(TYPE_64GFC_QSFP_PLUS, 'QSFP+ (64GFC)'),
|
||||
(TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'),
|
||||
)
|
||||
),
|
||||
|
||||
@@ -12,13 +12,7 @@ from utilities.filters import (
|
||||
from virtualization.models import Cluster
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from .models import *
|
||||
|
||||
|
||||
__all__ = (
|
||||
@@ -40,6 +34,7 @@ __all__ = (
|
||||
'InterfaceFilterSet',
|
||||
'InterfaceTemplateFilterSet',
|
||||
'InventoryItemFilterSet',
|
||||
'LocationFilterSet',
|
||||
'ManufacturerFilterSet',
|
||||
'PathEndpointFilterSet',
|
||||
'PlatformFilterSet',
|
||||
@@ -51,13 +46,13 @@ __all__ = (
|
||||
'PowerPortFilterSet',
|
||||
'PowerPortTemplateFilterSet',
|
||||
'RackFilterSet',
|
||||
'RackGroupFilterSet',
|
||||
'RackReservationFilterSet',
|
||||
'RackRoleFilterSet',
|
||||
'RearPortFilterSet',
|
||||
'RearPortTemplateFilterSet',
|
||||
'RegionFilterSet',
|
||||
'SiteFilterSet',
|
||||
'SiteGroupFilterSet',
|
||||
'VirtualChassisFilterSet',
|
||||
)
|
||||
|
||||
@@ -79,6 +74,23 @@ class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class SiteGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Parent site group (ID)',
|
||||
)
|
||||
parent = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent__slug',
|
||||
queryset=SiteGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Parent site group (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -96,11 +108,22 @@ class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='group',
|
||||
lookup_expr='in',
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Group (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
@@ -131,7 +154,7 @@ class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region',
|
||||
@@ -145,6 +168,19 @@ class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@@ -156,18 +192,18 @@ class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
label='Site (slug)',
|
||||
)
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
queryset=Location.objects.all(),
|
||||
label='Rack group (ID)',
|
||||
)
|
||||
parent = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent__slug',
|
||||
queryset=RackGroup.objects.all(),
|
||||
queryset=Location.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Rack group (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
model = Location
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
@@ -196,6 +232,19 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@@ -206,18 +255,18 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='group',
|
||||
location_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='location',
|
||||
lookup_expr='in',
|
||||
label='Rack group (ID)',
|
||||
label='Location (ID)',
|
||||
)
|
||||
group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='group',
|
||||
location = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='location',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Rack group (slug)',
|
||||
label='Location (slug)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=RackStatusChoices,
|
||||
@@ -283,18 +332,18 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModel
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='rack__group',
|
||||
location_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='rack__location',
|
||||
lookup_expr='in',
|
||||
label='Rack group (ID)',
|
||||
label='Location (ID)',
|
||||
)
|
||||
group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='rack__group',
|
||||
location = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='rack__location',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Rack group (slug)',
|
||||
label='Location (slug)',
|
||||
)
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
@@ -565,6 +614,19 @@ class DeviceFilterSet(
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@@ -575,11 +637,11 @@ class DeviceFilterSet(
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
rack_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='rack__group',
|
||||
location_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='location',
|
||||
lookup_expr='in',
|
||||
label='Rack group (ID)',
|
||||
label='Location (ID)',
|
||||
)
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='rack',
|
||||
@@ -721,6 +783,19 @@ class DeviceComponentFilterSet(CustomFieldModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='device__site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='device__site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__site',
|
||||
queryset=Site.objects.all(),
|
||||
@@ -844,6 +919,11 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
|
||||
method='filter_kind',
|
||||
label='Kind of interface',
|
||||
)
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent',
|
||||
queryset=Interface.objects.all(),
|
||||
label='Parent interface (ID)',
|
||||
)
|
||||
lag_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='lag',
|
||||
queryset=Interface.objects.all(),
|
||||
@@ -1038,6 +1118,19 @@ class VirtualChassisFilterSet(BaseFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='master__site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='master__site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='master__site',
|
||||
queryset=Site.objects.all(),
|
||||
@@ -1226,6 +1319,19 @@ class PowerPanelFilterSet(BaseFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@@ -1236,9 +1342,9 @@ class PowerPanelFilterSet(BaseFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
rack_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='rack_group',
|
||||
location_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='location',
|
||||
lookup_expr='in',
|
||||
label='Rack group (ID)',
|
||||
)
|
||||
@@ -1281,6 +1387,19 @@ class PowerFeedFilterSet(
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='power_panel__site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='power_panel__site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='power_panel__site',
|
||||
queryset=Site.objects.all(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
21
netbox/dcim/migrations/0125_console_port_speed.py
Normal file
21
netbox/dcim/migrations/0125_console_port_speed.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0124_mark_connected'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='speed',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='speed',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
39
netbox/dcim/migrations/0126_rename_rackgroup_location.py
Normal file
39
netbox/dcim/migrations/0126_rename_rackgroup_location.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0125_console_port_speed'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='RackGroup',
|
||||
new_name='Location',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='rack',
|
||||
options={'ordering': ('site', 'location', '_name', 'pk')},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='site',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations', to='dcim.site'),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='powerpanel',
|
||||
old_name='rack_group',
|
||||
new_name='location',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='rack',
|
||||
old_name='group',
|
||||
new_name='location',
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rack',
|
||||
unique_together={('location', 'facility_id'), ('location', 'name')},
|
||||
),
|
||||
]
|
||||
17
netbox/dcim/migrations/0127_device_location.py
Normal file
17
netbox/dcim/migrations/0127_device_location.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0126_rename_rackgroup_location'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='location',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.location'),
|
||||
),
|
||||
]
|
||||
24
netbox/dcim/migrations/0128_device_location_populate.py
Normal file
24
netbox/dcim/migrations/0128_device_location_populate.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.db import migrations
|
||||
from django.db.models import Subquery, OuterRef
|
||||
|
||||
|
||||
def populate_device_location(apps, schema_editor):
|
||||
Device = apps.get_model('dcim', 'Device')
|
||||
Device.objects.filter(rack__isnull=False).update(
|
||||
location_id=Subquery(
|
||||
Device.objects.filter(pk=OuterRef('pk')).values('rack__location_id')[:1]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0127_device_location'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=populate_device_location
|
||||
),
|
||||
]
|
||||
17
netbox/dcim/migrations/0129_interface_parent.py
Normal file
17
netbox/dcim/migrations/0129_interface_parent.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0128_device_location_populate'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_interfaces', to='dcim.interface'),
|
||||
),
|
||||
]
|
||||
39
netbox/dcim/migrations/0130_sitegroup.py
Normal file
39
netbox/dcim/migrations/0130_sitegroup.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import django.core.serializers.json
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import mptt.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0129_interface_parent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SiteGroup',
|
||||
fields=[
|
||||
('created', models.DateField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('lft', models.PositiveIntegerField(editable=False)),
|
||||
('rght', models.PositiveIntegerField(editable=False)),
|
||||
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('level', models.PositiveIntegerField(editable=False)),
|
||||
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.sitegroup')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='group',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='dcim.sitegroup'),
|
||||
),
|
||||
]
|
||||
@@ -34,12 +34,13 @@ __all__ = (
|
||||
'PowerPort',
|
||||
'PowerPortTemplate',
|
||||
'Rack',
|
||||
'RackGroup',
|
||||
'Location',
|
||||
'RackReservation',
|
||||
'RackRole',
|
||||
'RearPort',
|
||||
'RearPortTemplate',
|
||||
'Region',
|
||||
'Site',
|
||||
'SiteGroup',
|
||||
'VirtualChassis',
|
||||
)
|
||||
|
||||
@@ -6,13 +6,11 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.fields import PathField
|
||||
from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object
|
||||
from extras.models import TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import BigIDModel, PrimaryModel
|
||||
from utilities.fields import ColorField
|
||||
@@ -108,7 +106,6 @@ class Cable(PrimaryModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
|
||||
@@ -4,13 +4,11 @@ from django.db import models
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from extras.models import ObjectChange
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import BigIDModel, ChangeLoggingMixin
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from utilities.fields import NaturalOrderingField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.ordering import naturalize_interface
|
||||
from utilities.utils import serialize_object
|
||||
from .device_components import (
|
||||
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort,
|
||||
)
|
||||
@@ -28,7 +26,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ComponentTemplateModel(ChangeLoggingMixin, BigIDModel):
|
||||
class ComponentTemplateModel(ChangeLoggedModel):
|
||||
device_type = models.ForeignKey(
|
||||
to='dcim.DeviceType',
|
||||
on_delete=models.CASCADE,
|
||||
@@ -75,16 +73,10 @@ class ComponentTemplateModel(ChangeLoggingMixin, BigIDModel):
|
||||
except ObjectDoesNotExist:
|
||||
# The parent DeviceType has already been deleted
|
||||
device_type = None
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
related_object=device_type,
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
return super().to_objectchange(action, related_object=device_type)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('webhooks')
|
||||
class ConsolePortTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
A template for a ConsolePort to be created for a new Device.
|
||||
@@ -108,7 +100,7 @@ class ConsolePortTemplate(ComponentTemplateModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('webhooks')
|
||||
class ConsoleServerPortTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
A template for a ConsoleServerPort to be created for a new Device.
|
||||
@@ -132,7 +124,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('webhooks')
|
||||
class PowerPortTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
A template for a PowerPort to be created for a new Device.
|
||||
@@ -179,7 +171,7 @@ class PowerPortTemplate(ComponentTemplateModel):
|
||||
})
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('webhooks')
|
||||
class PowerOutletTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
A template for a PowerOutlet to be created for a new Device.
|
||||
@@ -231,7 +223,7 @@ class PowerOutletTemplate(ComponentTemplateModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('webhooks')
|
||||
class InterfaceTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
A template for a physical data interface on a new Device.
|
||||
@@ -266,7 +258,7 @@ class InterfaceTemplate(ComponentTemplateModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('webhooks')
|
||||
class FrontPortTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
Template for a pass-through port on the front of a new Device.
|
||||
@@ -327,7 +319,7 @@ class FrontPortTemplate(ComponentTemplateModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('webhooks')
|
||||
class RearPortTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
Template for a pass-through port on the rear of a new Device.
|
||||
@@ -358,7 +350,7 @@ class RearPortTemplate(ComponentTemplateModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('webhooks')
|
||||
class DeviceBayTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
A template for a DeviceBay to be created for a new parent Device.
|
||||
|
||||
@@ -6,12 +6,10 @@ from django.db import models
|
||||
from django.db.models import Sum
|
||||
from django.urls import reverse
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.fields import MACAddressField
|
||||
from extras.models import ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import PrimaryModel
|
||||
from utilities.fields import NaturalOrderingField
|
||||
@@ -19,7 +17,6 @@ from utilities.mptt import TreeManager
|
||||
from utilities.ordering import naturalize_interface
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.query_functions import CollateAsChar
|
||||
from utilities.utils import serialize_object
|
||||
|
||||
|
||||
__all__ = (
|
||||
@@ -82,17 +79,11 @@ class ComponentModel(PrimaryModel):
|
||||
except ObjectDoesNotExist:
|
||||
# The parent Device has already been deleted
|
||||
device = None
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
related_object=device,
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
return super().to_objectchange(action, related_object=device)
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return getattr(self, 'device', None)
|
||||
def parent_object(self):
|
||||
return self.device
|
||||
|
||||
|
||||
class CableTermination(models.Model):
|
||||
@@ -159,6 +150,10 @@ class CableTermination(models.Model):
|
||||
def _occupied(self):
|
||||
return bool(self.mark_connected or self.cable_id)
|
||||
|
||||
@property
|
||||
def parent_object(self):
|
||||
raise NotImplementedError("CableTermination models must implement parent_object()")
|
||||
|
||||
|
||||
class PathEndpoint(models.Model):
|
||||
"""
|
||||
@@ -213,8 +208,8 @@ class PathEndpoint(models.Model):
|
||||
# Console ports
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||
"""
|
||||
@@ -224,9 +219,14 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
|
||||
blank=True,
|
||||
help_text='Physical port type'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
speed = models.PositiveSmallIntegerField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Port speed in bits per second'
|
||||
)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'mark_connected', 'description']
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
@@ -241,6 +241,7 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
|
||||
self.name,
|
||||
self.label,
|
||||
self.type,
|
||||
self.speed,
|
||||
self.mark_connected,
|
||||
self.description,
|
||||
)
|
||||
@@ -250,8 +251,8 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||
"""
|
||||
@@ -261,9 +262,14 @@ class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
blank=True,
|
||||
help_text='Physical port type'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
speed = models.PositiveSmallIntegerField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Port speed in bits per second'
|
||||
)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'mark_connected', 'description']
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
@@ -278,6 +284,7 @@ class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
self.name,
|
||||
self.label,
|
||||
self.type,
|
||||
self.speed,
|
||||
self.mark_connected,
|
||||
self.description,
|
||||
)
|
||||
@@ -287,8 +294,8 @@ class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
# Power ports
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class PowerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||
"""
|
||||
@@ -310,7 +317,6 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
validators=[MinValueValidator(1)],
|
||||
help_text="Allocated power draw (watts)"
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description',
|
||||
@@ -399,8 +405,8 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel):
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class PowerOutlet(CableTermination, PathEndpoint, ComponentModel):
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||
"""
|
||||
@@ -423,7 +429,6 @@ class PowerOutlet(CableTermination, PathEndpoint, ComponentModel):
|
||||
blank=True,
|
||||
help_text="Phase (for three-phase feeds)"
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description']
|
||||
|
||||
@@ -500,8 +505,8 @@ class BaseInterface(models.Model):
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
||||
"""
|
||||
@@ -512,6 +517,14 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
parent = models.ForeignKey(
|
||||
to='self',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='child_interfaces',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='Parent interface'
|
||||
)
|
||||
lag = models.ForeignKey(
|
||||
to='self',
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -549,11 +562,10 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
object_id_field='assigned_object_id',
|
||||
related_query_name='interface'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
'device', 'name', 'label', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu', 'mgmt_only',
|
||||
'description', 'mode',
|
||||
'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu',
|
||||
'mgmt_only', 'description', 'mode',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@@ -568,6 +580,7 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
self.device.identifier if self.device else None,
|
||||
self.name,
|
||||
self.label,
|
||||
self.parent.name if self.parent else None,
|
||||
self.lag.name if self.lag else None,
|
||||
self.get_type_display(),
|
||||
self.enabled,
|
||||
@@ -591,6 +604,27 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
"Disconnect the interface or choose a suitable type."
|
||||
})
|
||||
|
||||
# An interface's parent must belong to the same device or virtual chassis
|
||||
if self.parent and self.parent.device != self.device:
|
||||
if self.device.virtual_chassis is None:
|
||||
raise ValidationError({
|
||||
'parent': f"The selected parent interface ({self.parent}) belongs to a different device "
|
||||
f"({self.parent.device})."
|
||||
})
|
||||
elif self.parent.device.virtual_chassis != self.parent.virtual_chassis:
|
||||
raise ValidationError({
|
||||
'parent': f"The selected parent interface ({self.parent}) belongs to {self.parent.device}, which "
|
||||
f"is not part of virtual chassis {self.device.virtual_chassis}."
|
||||
})
|
||||
|
||||
# A physical interface cannot have a parent interface
|
||||
if self.type != InterfaceTypeChoices.TYPE_VIRTUAL and self.parent is not None:
|
||||
raise ValidationError({'parent': "Only virtual interfaces may be assigned to a parent interface."})
|
||||
|
||||
# A virtual interface cannot be a parent interface
|
||||
if self.parent is not None and self.parent.type == InterfaceTypeChoices.TYPE_VIRTUAL:
|
||||
raise ValidationError({'parent': "Virtual interfaces may not be parents of other interfaces."})
|
||||
|
||||
# An interface's LAG must belong to the same device or virtual chassis
|
||||
if self.lag and self.lag.device != self.device:
|
||||
if self.device.virtual_chassis is None:
|
||||
@@ -612,16 +646,12 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
raise ValidationError({'lag': "A LAG interface cannot be its own parent."})
|
||||
|
||||
# Validate untagged VLAN
|
||||
if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]:
|
||||
if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]:
|
||||
raise ValidationError({
|
||||
'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent "
|
||||
"device, or it must be global".format(self.untagged_vlan)
|
||||
})
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return self.device
|
||||
|
||||
@property
|
||||
def is_connectable(self):
|
||||
return self.type not in NONCONNECTABLE_IFACE_TYPES
|
||||
@@ -647,8 +677,8 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
||||
# Pass-through ports
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class FrontPort(CableTermination, ComponentModel):
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class FrontPort(ComponentModel, CableTermination):
|
||||
"""
|
||||
A pass-through port on the front of a Device.
|
||||
"""
|
||||
@@ -668,7 +698,6 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
]
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
'device', 'name', 'label', 'type', 'mark_connected', 'rear_port', 'rear_port_position', 'description',
|
||||
@@ -713,8 +742,8 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
})
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class RearPort(CableTermination, ComponentModel):
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class RearPort(ComponentModel, CableTermination):
|
||||
"""
|
||||
A pass-through port on the rear of a Device.
|
||||
"""
|
||||
@@ -729,7 +758,6 @@ class RearPort(CableTermination, ComponentModel):
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
]
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'mark_connected', 'positions', 'description']
|
||||
|
||||
@@ -767,7 +795,7 @@ class RearPort(CableTermination, ComponentModel):
|
||||
# Device bays
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class DeviceBay(ComponentModel):
|
||||
"""
|
||||
An empty space within a Device which can house a child device
|
||||
@@ -779,7 +807,6 @@ class DeviceBay(ComponentModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'installed_device', 'description']
|
||||
|
||||
@@ -827,7 +854,7 @@ class DeviceBay(ComponentModel):
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class InventoryItem(MPTTModel, ComponentModel):
|
||||
"""
|
||||
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
||||
@@ -872,8 +899,6 @@ class InventoryItem(MPTTModel, ComponentModel):
|
||||
help_text='This item was automatically discovered'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = TreeManager()
|
||||
|
||||
csv_headers = [
|
||||
|
||||
@@ -9,11 +9,10 @@ from django.db import models
|
||||
from django.db.models import F, ProtectedError
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from extras.models import ConfigContextModel, TaggedItem
|
||||
from extras.models import ConfigContextModel
|
||||
from extras.querysets import ConfigContextModelQuerySet
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
@@ -136,7 +135,6 @@ class DeviceType(PrimaryModel):
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
@@ -517,6 +515,13 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
on_delete=models.PROTECT,
|
||||
related_name='devices'
|
||||
)
|
||||
location = models.ForeignKey(
|
||||
to='dcim.Location',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='devices',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
rack = models.ForeignKey(
|
||||
to='dcim.Rack',
|
||||
on_delete=models.PROTECT,
|
||||
@@ -594,16 +599,15 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
object_id_field='assigned_object_id',
|
||||
related_query_name='device'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = ConfigContextModelQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
|
||||
'site', 'location', 'rack_name', 'position', 'face', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'device_type', 'device_role', 'tenant', 'platform', 'site', 'rack', 'status', 'cluster',
|
||||
'device_type', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'status', 'cluster',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@@ -640,11 +644,17 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate site/rack combination
|
||||
# Validate site/location/rack combination
|
||||
if self.rack and self.site != self.rack.site:
|
||||
raise ValidationError({
|
||||
'rack': f"Rack {self.rack} does not belong to site {self.site}.",
|
||||
})
|
||||
if self.rack and self.location and self.rack.location != self.location:
|
||||
raise ValidationError({
|
||||
'rack': f"Rack {self.rack} does not belong to location {self.location}.",
|
||||
})
|
||||
elif self.rack:
|
||||
self.location = self.rack.location
|
||||
|
||||
if self.rack is None:
|
||||
if self.face:
|
||||
@@ -799,7 +809,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
self.asset_tag,
|
||||
self.get_status_display(),
|
||||
self.site.name,
|
||||
self.rack.group.name if self.rack and self.rack.group else None,
|
||||
self.rack.location.name if self.rack and self.rack.location else None,
|
||||
self.rack.name if self.rack else None,
|
||||
self.position,
|
||||
self.get_face_display(),
|
||||
@@ -903,7 +913,6 @@ class VirtualChassis(PrimaryModel):
|
||||
max_length=30,
|
||||
blank=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from extras.models import TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import PrimaryModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
@@ -32,8 +30,8 @@ class PowerPanel(PrimaryModel):
|
||||
to='Site',
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
rack_group = models.ForeignKey(
|
||||
to='RackGroup',
|
||||
location = models.ForeignKey(
|
||||
to='dcim.Location',
|
||||
on_delete=models.PROTECT,
|
||||
blank=True,
|
||||
null=True
|
||||
@@ -41,11 +39,10 @@ class PowerPanel(PrimaryModel):
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['site', 'rack_group', 'name']
|
||||
csv_headers = ['site', 'location', 'name']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
@@ -60,17 +57,17 @@ class PowerPanel(PrimaryModel):
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.site.name,
|
||||
self.rack_group.name if self.rack_group else None,
|
||||
self.location.name if self.location else None,
|
||||
self.name,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# RackGroup must belong to assigned Site
|
||||
if self.rack_group and self.rack_group.site != self.site:
|
||||
# Location must belong to assigned Site
|
||||
if self.location and self.location.site != self.site:
|
||||
raise ValidationError("Rack group {} ({}) is in a different site than {}".format(
|
||||
self.rack_group, self.rack_group.site, self.site
|
||||
self.location, self.location.site, self.site
|
||||
))
|
||||
|
||||
|
||||
@@ -133,12 +130,11 @@ class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'site', 'power_panel', 'rack_group', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
|
||||
'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
|
||||
'voltage', 'amperage', 'max_utilization', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
@@ -160,7 +156,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
|
||||
return (
|
||||
self.power_panel.site.name,
|
||||
self.power_panel.name,
|
||||
self.rack.group.name if self.rack and self.rack.group else None,
|
||||
self.rack.location.name if self.rack and self.rack.location else None,
|
||||
self.rack.name if self.rack else None,
|
||||
self.name,
|
||||
self.get_status_display(),
|
||||
@@ -201,7 +197,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
def parent_object(self):
|
||||
return self.power_panel
|
||||
|
||||
def get_type_class(self):
|
||||
|
||||
@@ -10,27 +10,22 @@ from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Count, Sum
|
||||
from django.urls import reverse
|
||||
from mptt.models import TreeForeignKey
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.elevations import RackElevationSVG
|
||||
from extras.models import ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.mptt import TreeManager
|
||||
from utilities.utils import array_to_string, serialize_object
|
||||
from utilities.utils import array_to_string
|
||||
from .device_components import PowerOutlet, PowerPort
|
||||
from .devices import Device
|
||||
from .power import PowerFeed
|
||||
|
||||
__all__ = (
|
||||
'Rack',
|
||||
'RackGroup',
|
||||
'RackReservation',
|
||||
'RackRole',
|
||||
)
|
||||
@@ -40,66 +35,6 @@ __all__ = (
|
||||
# Racks
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class RackGroup(NestedGroupModel):
|
||||
"""
|
||||
Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
|
||||
example, if a Site spans a corporate campus, a RackGroup might be defined to represent each building within that
|
||||
campus. If a Site instead represents a single building, a RackGroup might represent a single room or floor.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
slug = models.SlugField(
|
||||
max_length=100
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='rack_groups'
|
||||
)
|
||||
parent = TreeForeignKey(
|
||||
to='self',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='children',
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
csv_headers = ['site', 'parent', 'name', 'slug', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = [
|
||||
['site', 'name'],
|
||||
['site', 'slug'],
|
||||
]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.site,
|
||||
self.parent.name if self.parent else '',
|
||||
self.name,
|
||||
self.slug,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Parent RackGroup (if any) must belong to the same Site
|
||||
if self.parent and self.parent.site != self.site:
|
||||
raise ValidationError(f"Parent rack group ({self.parent}) must belong to the same site ({self.site})")
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class RackRole(OrganizationalModel):
|
||||
"""
|
||||
@@ -147,7 +82,7 @@ class RackRole(OrganizationalModel):
|
||||
class Rack(PrimaryModel):
|
||||
"""
|
||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||
Each Rack is assigned to a Site and (optionally) a RackGroup.
|
||||
Each Rack is assigned to a Site and (optionally) a Location.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
@@ -169,13 +104,12 @@ class Rack(PrimaryModel):
|
||||
on_delete=models.PROTECT,
|
||||
related_name='racks'
|
||||
)
|
||||
group = models.ForeignKey(
|
||||
to='dcim.RackGroup',
|
||||
location = models.ForeignKey(
|
||||
to='dcim.Location',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='racks',
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Assigned group'
|
||||
null=True
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
@@ -254,25 +188,24 @@ class Rack(PrimaryModel):
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'site', 'group', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
|
||||
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
|
||||
'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'site', 'group', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
|
||||
'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
|
||||
'outer_depth', 'outer_unit',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ('site', 'group', '_name', 'pk') # (site, group, name) may be non-unique
|
||||
ordering = ('site', 'location', '_name', 'pk') # (site, location, name) may be non-unique
|
||||
unique_together = (
|
||||
# Name and facility_id must be unique *only* within a RackGroup
|
||||
('group', 'name'),
|
||||
('group', 'facility_id'),
|
||||
# Name and facility_id must be unique *only* within a Location
|
||||
('location', 'name'),
|
||||
('location', 'facility_id'),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
@@ -284,9 +217,9 @@ class Rack(PrimaryModel):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate group/site assignment
|
||||
if self.site and self.group and self.group.site != self.site:
|
||||
raise ValidationError(f"Assigned rack group must belong to parent site ({self.site}).")
|
||||
# Validate location/site assignment
|
||||
if self.site and self.location and self.location.site != self.site:
|
||||
raise ValidationError(f"Assigned location must belong to parent site ({self.site}).")
|
||||
|
||||
# Validate outer dimensions and unit
|
||||
if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
|
||||
@@ -309,17 +242,17 @@ class Rack(PrimaryModel):
|
||||
min_height
|
||||
)
|
||||
})
|
||||
# Validate that Rack was assigned a group of its same site, if applicable
|
||||
if self.group:
|
||||
if self.group.site != self.site:
|
||||
# Validate that Rack was assigned a Location of its same site, if applicable
|
||||
if self.location:
|
||||
if self.location.site != self.site:
|
||||
raise ValidationError({
|
||||
'group': "Rack group must be from the same site, {}.".format(self.site)
|
||||
'location': f"Location must be from the same site, {self.site}."
|
||||
})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.site.name,
|
||||
self.group.name if self.group else None,
|
||||
self.location.name if self.location else None,
|
||||
self.name,
|
||||
self.facility_id,
|
||||
self.tenant.name if self.tenant else None,
|
||||
@@ -561,11 +494,10 @@ class RackReservation(PrimaryModel):
|
||||
description = models.CharField(
|
||||
max_length=200
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description']
|
||||
csv_headers = ['site', 'location', 'rack', 'units', 'tenant', 'user', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['created', 'pk']
|
||||
@@ -606,7 +538,7 @@ class RackReservation(PrimaryModel):
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.rack.site.name,
|
||||
self.rack.group if self.rack.group else None,
|
||||
self.rack.location if self.rack.location else None,
|
||||
self.rack.name,
|
||||
','.join([str(u) for u in self.units]),
|
||||
self.tenant.name if self.tenant else None,
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from mptt.models import TreeForeignKey
|
||||
from taggit.managers import TaggableManager
|
||||
from timezone_field import TimeZoneField
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.fields import ASNField
|
||||
from extras.models import ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import NestedGroupModel, PrimaryModel
|
||||
from utilities.fields import NaturalOrderingField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.mptt import TreeManager
|
||||
from utilities.utils import serialize_object
|
||||
|
||||
__all__ = (
|
||||
'Location',
|
||||
'Region',
|
||||
'Site',
|
||||
'SiteGroup',
|
||||
)
|
||||
|
||||
|
||||
@@ -29,7 +28,9 @@ __all__ = (
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class Region(NestedGroupModel):
|
||||
"""
|
||||
Sites can be grouped within geographic Regions.
|
||||
A region represents a geographic collection of sites. For example, you might create regions representing countries,
|
||||
states, and/or cities. Regions are recursively nested into a hierarchy: all sites belonging to a child region are
|
||||
also considered to be members of its parent and ancestor region(s).
|
||||
"""
|
||||
parent = TreeForeignKey(
|
||||
to='self',
|
||||
@@ -72,6 +73,58 @@ class Region(NestedGroupModel):
|
||||
).count()
|
||||
|
||||
|
||||
#
|
||||
# Site groups
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class SiteGroup(NestedGroupModel):
|
||||
"""
|
||||
A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
|
||||
within corporate sites you might distinguish between offices and data centers. Like regions, site groups can be
|
||||
nested recursively to form a hierarchy.
|
||||
"""
|
||||
parent = TreeForeignKey(
|
||||
to='self',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='children',
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
slug = models.SlugField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
csv_headers = ['name', 'slug', 'parent', 'description']
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?group={}".format(reverse('dcim:site_list'), self.slug)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.parent.name if self.parent else None,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def get_site_count(self):
|
||||
return Site.objects.filter(
|
||||
Q(group=self) |
|
||||
Q(group__in=self.get_descendants())
|
||||
).count()
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
@@ -107,6 +160,13 @@ class Site(PrimaryModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
group = models.ForeignKey(
|
||||
to='dcim.SiteGroup',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='sites',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
on_delete=models.PROTECT,
|
||||
@@ -172,16 +232,16 @@ class Site(PrimaryModel):
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
||||
'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||
'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
||||
'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
||||
'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email',
|
||||
]
|
||||
|
||||
@@ -200,6 +260,7 @@ class Site(PrimaryModel):
|
||||
self.slug,
|
||||
self.get_status_display(),
|
||||
self.region.name if self.region else None,
|
||||
self.group.name if self.group else None,
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.facility,
|
||||
self.asn,
|
||||
@@ -217,3 +278,66 @@ class Site(PrimaryModel):
|
||||
|
||||
def get_status_class(self):
|
||||
return SiteStatusChoices.CSS_CLASSES.get(self.status)
|
||||
|
||||
|
||||
#
|
||||
# Locations
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class Location(NestedGroupModel):
|
||||
"""
|
||||
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
|
||||
site, or a room within a building, for example.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
slug = models.SlugField(
|
||||
max_length=100
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='locations'
|
||||
)
|
||||
parent = TreeForeignKey(
|
||||
to='self',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='children',
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
csv_headers = ['site', 'parent', 'name', 'slug', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = [
|
||||
['site', 'name'],
|
||||
['site', 'slug'],
|
||||
]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?location_id={}".format(reverse('dcim:rack_list'), self.pk)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.site,
|
||||
self.parent.name if self.parent else '',
|
||||
self.name,
|
||||
self.slug,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Parent Location (if any) must belong to the same Site
|
||||
if self.parent and self.parent.site != self.site:
|
||||
raise ValidationError(f"Parent location ({self.parent}) must belong to the same site ({self.site})")
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.db import transaction
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .choices import CableStatusChoices
|
||||
from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, RackGroup, VirtualChassis
|
||||
from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis
|
||||
|
||||
|
||||
def create_cablepath(node):
|
||||
@@ -37,36 +37,30 @@ def rebuild_paths(obj):
|
||||
|
||||
|
||||
#
|
||||
# Site/rack/device assignment
|
||||
# Location/rack/device assignment
|
||||
#
|
||||
|
||||
@receiver(post_save, sender=RackGroup)
|
||||
def handle_rackgroup_site_change(instance, created, **kwargs):
|
||||
@receiver(post_save, sender=Location)
|
||||
def handle_location_site_change(instance, created, **kwargs):
|
||||
"""
|
||||
Update child RackGroups and Racks if Site assignment has changed. We intentionally recurse through each child
|
||||
Update child objects if Site assignment has changed. We intentionally recurse through each child
|
||||
object instead of calling update() on the QuerySet to ensure the proper change records get created for each.
|
||||
"""
|
||||
if not created:
|
||||
for rackgroup in instance.get_children():
|
||||
rackgroup.site = instance.site
|
||||
rackgroup.save()
|
||||
for rack in Rack.objects.filter(group=instance).exclude(site=instance.site):
|
||||
rack.site = instance.site
|
||||
rack.save()
|
||||
for powerpanel in PowerPanel.objects.filter(rack_group=instance).exclude(site=instance.site):
|
||||
powerpanel.site = instance.site
|
||||
powerpanel.save()
|
||||
instance.get_descendants().update(site=instance.site)
|
||||
locations = instance.get_descendants(include_self=True).values_list('pk', flat=True)
|
||||
Rack.objects.filter(location__in=locations).update(site=instance.site)
|
||||
Device.objects.filter(location__in=locations).update(site=instance.site)
|
||||
PowerPanel.objects.filter(location__in=locations).update(site=instance.site)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Rack)
|
||||
def handle_rack_site_change(instance, created, **kwargs):
|
||||
"""
|
||||
Update child Devices if Site assignment has changed.
|
||||
Update child Devices if Site or Location assignment has changed.
|
||||
"""
|
||||
if not created:
|
||||
for device in Device.objects.filter(rack=instance).exclude(site=instance.site):
|
||||
device.site = instance.site
|
||||
device.save()
|
||||
Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
from django.conf import settings
|
||||
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform,
|
||||
PowerOutlet, PowerPort, RearPort, VirtualChassis,
|
||||
)
|
||||
from tenancy.tables import COL_TENANT
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn,
|
||||
TagColumn, ToggleColumn,
|
||||
@@ -109,12 +110,13 @@ class DeviceTable(BaseTable):
|
||||
template_code=DEVICE_LINK
|
||||
)
|
||||
status = ChoiceFieldColumn()
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
location = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
rack = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@@ -127,11 +129,18 @@ class DeviceTable(BaseTable):
|
||||
verbose_name='Type',
|
||||
text=lambda record: record.device_type.display_name
|
||||
)
|
||||
primary_ip = tables.Column(
|
||||
linkify=True,
|
||||
order_by=('primary_ip6', 'primary_ip4'),
|
||||
verbose_name='IP Address'
|
||||
)
|
||||
if settings.PREFER_IPV4:
|
||||
primary_ip = tables.Column(
|
||||
linkify=True,
|
||||
order_by=('primary_ip4', 'primary_ip6'),
|
||||
verbose_name='IP Address'
|
||||
)
|
||||
else:
|
||||
primary_ip = tables.Column(
|
||||
linkify=True,
|
||||
order_by=('primary_ip6', 'primary_ip4'),
|
||||
verbose_name='IP Address'
|
||||
)
|
||||
primary_ip4 = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='IPv4 Address'
|
||||
@@ -162,11 +171,11 @@ class DeviceTable(BaseTable):
|
||||
model = Device
|
||||
fields = (
|
||||
'pk', 'name', 'status', 'tenant', 'device_role', 'device_type', 'platform', 'serial', 'asset_tag', 'site',
|
||||
'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis',
|
||||
'vc_position', 'vc_priority', 'tags',
|
||||
'location', 'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster',
|
||||
'virtual_chassis', 'vc_position', 'vc_priority', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip',
|
||||
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'device_type', 'primary_ip',
|
||||
)
|
||||
|
||||
|
||||
@@ -175,9 +184,7 @@ class DeviceImportTable(BaseTable):
|
||||
template_code=DEVICE_LINK
|
||||
)
|
||||
status = ChoiceFieldColumn()
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@@ -249,10 +256,10 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'type', 'description', 'mark_connected', 'cable', 'cable_peer',
|
||||
'pk', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_peer',
|
||||
'connection', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'speed', 'description')
|
||||
|
||||
|
||||
class DeviceConsolePortTable(ConsolePortTable):
|
||||
@@ -269,10 +276,10 @@ class DeviceConsolePortTable(ConsolePortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'description', 'mark_connected', 'cable', 'cable_peer', 'connection',
|
||||
'tags', 'actions'
|
||||
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_peer',
|
||||
'connection', 'tags', 'actions'
|
||||
)
|
||||
default_columns = ('pk', 'name', 'label', 'type', 'description', 'cable', 'connection', 'actions')
|
||||
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
|
||||
row_attrs = {
|
||||
'class': lambda record: record.cable.get_status_class() if record.cable else ''
|
||||
}
|
||||
@@ -286,10 +293,10 @@ class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'type', 'description', 'mark_connected', 'cable', 'cable_peer',
|
||||
'pk', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_peer',
|
||||
'connection', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'speed', 'description')
|
||||
|
||||
|
||||
class DeviceConsoleServerPortTable(ConsoleServerPortTable):
|
||||
@@ -307,10 +314,10 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'description', 'mark_connected', 'cable', 'cable_peer', 'connection', 'tags',
|
||||
'actions',
|
||||
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_peer',
|
||||
'connection', 'tags', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'label', 'type', 'description', 'cable', 'connection', 'actions')
|
||||
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
|
||||
row_attrs = {
|
||||
'class': lambda record: record.cable.get_status_class() if record.cable else ''
|
||||
}
|
||||
@@ -437,6 +444,10 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
'{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||
attrs={'td': {'class': 'text-nowrap'}}
|
||||
)
|
||||
parent = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Parent'
|
||||
)
|
||||
lag = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='LAG'
|
||||
@@ -450,13 +461,13 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = Interface
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'description',
|
||||
'mark_connected', 'cable', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan',
|
||||
'tagged_vlans', 'actions',
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||
'description', 'mark_connected', 'cable', 'cable_peer', 'connection', 'tags', 'ip_addresses',
|
||||
'untagged_vlan', 'tagged_vlans', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'lag', 'mtu', 'mode', 'description', 'ip_addresses', 'cable',
|
||||
'connection', 'actions',
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
|
||||
'cable', 'connection', 'actions',
|
||||
)
|
||||
row_attrs = {
|
||||
'class': lambda record: record.cable.get_status_class() if record.cable else '',
|
||||
|
||||
@@ -33,8 +33,8 @@ class PowerPanelTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPanel
|
||||
fields = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count')
|
||||
fields = ('pk', 'name', 'site', 'location', 'powerfeed_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import Rack, RackGroup, RackReservation, RackRole
|
||||
from tenancy.tables import COL_TENANT
|
||||
from dcim.models import Rack, Location, RackReservation, RackRole
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, TagColumn,
|
||||
ToggleColumn,
|
||||
BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, MPTTColumn,
|
||||
TagColumn, ToggleColumn, UtilizationColumn,
|
||||
)
|
||||
from .template_code import MPTT_LINK, RACKGROUP_ELEVATIONS, UTILIZATION_GRAPH
|
||||
from .template_code import LOCATION_ELEVATIONS
|
||||
|
||||
__all__ = (
|
||||
'RackTable',
|
||||
'RackDetailTable',
|
||||
'RackGroupTable',
|
||||
'LocationTable',
|
||||
'RackReservationTable',
|
||||
'RackRoleTable',
|
||||
)
|
||||
@@ -22,13 +22,9 @@ __all__ = (
|
||||
# Rack groups
|
||||
#
|
||||
|
||||
class RackGroupTable(BaseTable):
|
||||
class LocationTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.TemplateColumn(
|
||||
template_code=MPTT_LINK,
|
||||
orderable=False,
|
||||
attrs={'td': {'class': 'text-nowrap'}}
|
||||
)
|
||||
name = MPTTColumn()
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@@ -36,12 +32,12 @@ class RackGroupTable(BaseTable):
|
||||
verbose_name='Racks'
|
||||
)
|
||||
actions = ButtonsColumn(
|
||||
model=RackGroup,
|
||||
prepend_template=RACKGROUP_ELEVATIONS
|
||||
model=Location,
|
||||
prepend_template=LOCATION_ELEVATIONS
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackGroup
|
||||
model = Location
|
||||
fields = ('pk', 'name', 'site', 'rack_count', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'site', 'rack_count', 'description', 'actions')
|
||||
|
||||
@@ -79,9 +75,7 @@ class RackTable(BaseTable):
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
status = ChoiceFieldColumn()
|
||||
role = ColoredLabelColumn()
|
||||
u_height = tables.TemplateColumn(
|
||||
@@ -104,13 +98,10 @@ class RackDetailTable(RackTable):
|
||||
url_params={'rack_id': 'pk'},
|
||||
verbose_name='Devices'
|
||||
)
|
||||
get_utilization = tables.TemplateColumn(
|
||||
template_code=UTILIZATION_GRAPH,
|
||||
orderable=False,
|
||||
get_utilization = UtilizationColumn(
|
||||
verbose_name='Space'
|
||||
)
|
||||
get_power_utilization = tables.TemplateColumn(
|
||||
template_code=UTILIZATION_GRAPH,
|
||||
get_power_utilization = UtilizationColumn(
|
||||
orderable=False,
|
||||
verbose_name='Power'
|
||||
)
|
||||
@@ -143,9 +134,7 @@ class RackReservationTable(BaseTable):
|
||||
accessor=Accessor('rack__site'),
|
||||
linkify=True
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
rack = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from dcim.models import Region, Site
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn
|
||||
from .template_code import MPTT_LINK
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, MPTTColumn, TagColumn, ToggleColumn
|
||||
|
||||
__all__ = (
|
||||
'RegionTable',
|
||||
'SiteTable',
|
||||
'SiteGroupTable',
|
||||
)
|
||||
|
||||
|
||||
@@ -17,11 +17,7 @@ __all__ = (
|
||||
|
||||
class RegionTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.TemplateColumn(
|
||||
template_code=MPTT_LINK,
|
||||
orderable=False,
|
||||
attrs={'td': {'class': 'text-nowrap'}}
|
||||
)
|
||||
name = MPTTColumn()
|
||||
site_count = tables.Column(
|
||||
verbose_name='Sites'
|
||||
)
|
||||
@@ -33,6 +29,24 @@ class RegionTable(BaseTable):
|
||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
||||
|
||||
|
||||
#
|
||||
# Site groups
|
||||
#
|
||||
|
||||
class SiteGroupTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = MPTTColumn()
|
||||
site_count = tables.Column(
|
||||
verbose_name='Sites'
|
||||
)
|
||||
actions = ButtonsColumn(SiteGroup)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SiteGroup
|
||||
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
@@ -46,9 +60,10 @@ class SiteTable(BaseTable):
|
||||
region = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
group = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
tags = TagColumn(
|
||||
url_name='dcim:site_list'
|
||||
)
|
||||
@@ -56,8 +71,8 @@ class SiteTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Site
|
||||
fields = (
|
||||
'pk', 'name', 'slug', 'status', 'facility', 'region', 'tenant', 'asn', 'time_zone', 'description',
|
||||
'pk', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description')
|
||||
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'description')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
CABLETERMINATION = """
|
||||
{% if value %}
|
||||
<a href="{{ value.parent.get_absolute_url }}">{{ value.parent }}</a>
|
||||
<a href="{{ value.parent_object.get_absolute_url }}">{{ value.parent_object }}</a>
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||
{% else %}
|
||||
@@ -56,13 +56,6 @@ INTERFACE_TAGGED_VLANS = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
MPTT_LINK = """
|
||||
{% for i in record.get_ancestors %}
|
||||
<i class="mdi mdi-circle-small"></i>
|
||||
{% endfor %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.name }}</a>
|
||||
"""
|
||||
|
||||
POWERFEED_CABLE = """
|
||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||
<a href="{% url 'dcim:powerfeed_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace">
|
||||
@@ -71,22 +64,17 @@ POWERFEED_CABLE = """
|
||||
"""
|
||||
|
||||
POWERFEED_CABLETERMINATION = """
|
||||
<a href="{{ value.parent.get_absolute_url }}">{{ value.parent }}</a>
|
||||
<a href="{{ value.parent_object.get_absolute_url }}">{{ value.parent_object }}</a>
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
RACKGROUP_ELEVATIONS = """
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&group_id={{ record.pk }}" class="btn btn-sm btn-primary" title="View elevations">
|
||||
LOCATION_ELEVATIONS = """
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&location_id={{ record.pk }}" class="btn btn-sm btn-primary" title="View elevations">
|
||||
<i class="mdi mdi-server"></i>
|
||||
</a>
|
||||
"""
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% utilization_graph value %}
|
||||
"""
|
||||
|
||||
#
|
||||
# Device component buttons
|
||||
#
|
||||
|
||||
@@ -4,12 +4,7 @@ from rest_framework import status
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerFeed, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, PowerPanel,
|
||||
Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
)
|
||||
from dcim.models import *
|
||||
from ipam.models import VLAN
|
||||
from utilities.testing import APITestCase, APIViewTestCases
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
@@ -64,7 +59,7 @@ class Mixins:
|
||||
|
||||
class RegionTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Region
|
||||
brief_fields = ['_depth', 'id', 'name', 'site_count', 'slug', 'url']
|
||||
brief_fields = ['_depth', 'display', 'id', 'name', 'site_count', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Region 4',
|
||||
@@ -93,7 +88,7 @@ class RegionTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class SiteTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Site
|
||||
brief_fields = ['id', 'name', 'slug', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'slug', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
@@ -102,14 +97,19 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
|
||||
def setUpTestData(cls):
|
||||
|
||||
regions = (
|
||||
Region.objects.create(name='Test Region 1', slug='test-region-1'),
|
||||
Region.objects.create(name='Test Region 2', slug='test-region-2'),
|
||||
Region.objects.create(name='Region 1', slug='region-1'),
|
||||
Region.objects.create(name='Region 2', slug='region-2'),
|
||||
)
|
||||
|
||||
groups = (
|
||||
SiteGroup.objects.create(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup.objects.create(name='Site Group 2', slug='site-group-2'),
|
||||
)
|
||||
|
||||
sites = (
|
||||
Site(region=regions[0], name='Site 1', slug='site-1'),
|
||||
Site(region=regions[0], name='Site 2', slug='site-2'),
|
||||
Site(region=regions[0], name='Site 3', slug='site-3'),
|
||||
Site(region=regions[0], group=groups[0], name='Site 1', slug='site-1'),
|
||||
Site(region=regions[0], group=groups[0], name='Site 2', slug='site-2'),
|
||||
Site(region=regions[0], group=groups[0], name='Site 3', slug='site-3'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@@ -118,26 +118,29 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
|
||||
'name': 'Site 4',
|
||||
'slug': 'site-4',
|
||||
'region': regions[1].pk,
|
||||
'group': groups[1].pk,
|
||||
'status': SiteStatusChoices.STATUS_ACTIVE,
|
||||
},
|
||||
{
|
||||
'name': 'Site 5',
|
||||
'slug': 'site-5',
|
||||
'region': regions[1].pk,
|
||||
'group': groups[1].pk,
|
||||
'status': SiteStatusChoices.STATUS_ACTIVE,
|
||||
},
|
||||
{
|
||||
'name': 'Site 6',
|
||||
'slug': 'site-6',
|
||||
'region': regions[1].pk,
|
||||
'group': groups[1].pk,
|
||||
'status': SiteStatusChoices.STATUS_ACTIVE,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class RackGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RackGroup
|
||||
brief_fields = ['_depth', 'id', 'name', 'rack_count', 'slug', 'url']
|
||||
class LocationTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Location
|
||||
brief_fields = ['_depth', 'display', 'id', 'name', 'rack_count', 'slug', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -151,40 +154,40 @@ class RackGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
parent_rack_groups = (
|
||||
RackGroup.objects.create(site=sites[0], name='Parent Rack Group 1', slug='parent-rack-group-1'),
|
||||
RackGroup.objects.create(site=sites[1], name='Parent Rack Group 2', slug='parent-rack-group-2'),
|
||||
parent_locations = (
|
||||
Location.objects.create(site=sites[0], name='Parent Location 1', slug='parent-location-1'),
|
||||
Location.objects.create(site=sites[1], name='Parent Location 2', slug='parent-location-2'),
|
||||
)
|
||||
|
||||
RackGroup.objects.create(site=sites[0], name='Rack Group 1', slug='rack-group-1', parent=parent_rack_groups[0])
|
||||
RackGroup.objects.create(site=sites[0], name='Rack Group 2', slug='rack-group-2', parent=parent_rack_groups[0])
|
||||
RackGroup.objects.create(site=sites[0], name='Rack Group 3', slug='rack-group-3', parent=parent_rack_groups[0])
|
||||
Location.objects.create(site=sites[0], name='Location 1', slug='location-1', parent=parent_locations[0])
|
||||
Location.objects.create(site=sites[0], name='Location 2', slug='location-2', parent=parent_locations[0])
|
||||
Location.objects.create(site=sites[0], name='Location 3', slug='location-3', parent=parent_locations[0])
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Test Rack Group 4',
|
||||
'slug': 'test-rack-group-4',
|
||||
'name': 'Test Location 4',
|
||||
'slug': 'test-location-4',
|
||||
'site': sites[1].pk,
|
||||
'parent': parent_rack_groups[1].pk,
|
||||
'parent': parent_locations[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Test Rack Group 5',
|
||||
'slug': 'test-rack-group-5',
|
||||
'name': 'Test Location 5',
|
||||
'slug': 'test-location-5',
|
||||
'site': sites[1].pk,
|
||||
'parent': parent_rack_groups[1].pk,
|
||||
'parent': parent_locations[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Test Rack Group 6',
|
||||
'slug': 'test-rack-group-6',
|
||||
'name': 'Test Location 6',
|
||||
'slug': 'test-location-6',
|
||||
'site': sites[1].pk,
|
||||
'parent': parent_rack_groups[1].pk,
|
||||
'parent': parent_locations[1].pk,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class RackRoleTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RackRole
|
||||
brief_fields = ['id', 'name', 'rack_count', 'slug', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'rack_count', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Rack Role 4',
|
||||
@@ -219,7 +222,7 @@ class RackRoleTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Rack
|
||||
brief_fields = ['device_count', 'display_name', 'id', 'name', 'url']
|
||||
brief_fields = ['device_count', 'display', 'display_name', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
@@ -233,9 +236,9 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rack_groups = (
|
||||
RackGroup.objects.create(site=sites[0], name='Rack Group 1', slug='rack-group-1'),
|
||||
RackGroup.objects.create(site=sites[1], name='Rack Group 2', slug='rack-group-2'),
|
||||
locations = (
|
||||
Location.objects.create(site=sites[0], name='Location 1', slug='location-1'),
|
||||
Location.objects.create(site=sites[1], name='Location 2', slug='location-2'),
|
||||
)
|
||||
|
||||
rack_roles = (
|
||||
@@ -245,9 +248,9 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
RackRole.objects.bulk_create(rack_roles)
|
||||
|
||||
racks = (
|
||||
Rack(site=sites[0], group=rack_groups[0], role=rack_roles[0], name='Rack 1'),
|
||||
Rack(site=sites[0], group=rack_groups[0], role=rack_roles[0], name='Rack 2'),
|
||||
Rack(site=sites[0], group=rack_groups[0], role=rack_roles[0], name='Rack 3'),
|
||||
Rack(site=sites[0], location=locations[0], role=rack_roles[0], name='Rack 1'),
|
||||
Rack(site=sites[0], location=locations[0], role=rack_roles[0], name='Rack 2'),
|
||||
Rack(site=sites[0], location=locations[0], role=rack_roles[0], name='Rack 3'),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
@@ -255,19 +258,19 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
{
|
||||
'name': 'Test Rack 4',
|
||||
'site': sites[1].pk,
|
||||
'group': rack_groups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'role': rack_roles[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Test Rack 5',
|
||||
'site': sites[1].pk,
|
||||
'group': rack_groups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'role': rack_roles[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Test Rack 6',
|
||||
'site': sites[1].pk,
|
||||
'group': rack_groups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'role': rack_roles[1].pk,
|
||||
},
|
||||
]
|
||||
@@ -307,7 +310,7 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class RackReservationTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RackReservation
|
||||
brief_fields = ['id', 'units', 'url', 'user']
|
||||
brief_fields = ['display', 'id', 'units', 'url', 'user']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -358,7 +361,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class ManufacturerTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Manufacturer
|
||||
brief_fields = ['devicetype_count', 'id', 'name', 'slug', 'url']
|
||||
brief_fields = ['devicetype_count', 'display', 'id', 'name', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Manufacturer 4',
|
||||
@@ -390,7 +393,7 @@ class ManufacturerTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
model = DeviceType
|
||||
brief_fields = ['device_count', 'display_name', 'id', 'manufacturer', 'model', 'slug', 'url']
|
||||
brief_fields = ['device_count', 'display', 'display_name', 'id', 'manufacturer', 'model', 'slug', 'url']
|
||||
bulk_update_data = {
|
||||
'part_number': 'ABC123',
|
||||
}
|
||||
@@ -432,7 +435,7 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ConsolePortTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -469,7 +472,7 @@ class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ConsoleServerPortTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -506,7 +509,7 @@ class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = PowerPortTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -543,7 +546,7 @@ class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = PowerOutletTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -580,7 +583,7 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = InterfaceTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -620,7 +623,7 @@ class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = FrontPortTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -691,7 +694,7 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RearPortTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -731,7 +734,7 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = DeviceBayTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -771,7 +774,7 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class DeviceRoleTest(APIViewTestCases.APIViewTestCase):
|
||||
model = DeviceRole
|
||||
brief_fields = ['device_count', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
|
||||
brief_fields = ['device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Device Role 4',
|
||||
@@ -806,7 +809,7 @@ class DeviceRoleTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class PlatformTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Platform
|
||||
brief_fields = ['device_count', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
|
||||
brief_fields = ['device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Platform 4',
|
||||
@@ -838,7 +841,7 @@ class PlatformTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class DeviceTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Device
|
||||
brief_fields = ['display_name', 'id', 'name', 'url']
|
||||
brief_fields = ['display', 'display_name', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'failed',
|
||||
}
|
||||
@@ -979,7 +982,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = ConsolePort
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -1018,7 +1021,7 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
||||
|
||||
class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = ConsoleServerPort
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -1057,7 +1060,7 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView
|
||||
|
||||
class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = PowerPort
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -1096,7 +1099,7 @@ class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
|
||||
class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = PowerOutlet
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -1135,7 +1138,7 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
||||
|
||||
class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = Interface
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -1193,7 +1196,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
|
||||
class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
||||
model = FrontPort
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -1251,7 +1254,7 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class RearPortTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RearPort
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -1293,7 +1296,7 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class DeviceBayTest(APIViewTestCases.APIViewTestCase):
|
||||
model = DeviceBay
|
||||
brief_fields = ['device', 'id', 'name', 'url']
|
||||
brief_fields = ['device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -1356,7 +1359,7 @@ class DeviceBayTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class InventoryItemTest(APIViewTestCases.APIViewTestCase):
|
||||
model = InventoryItem
|
||||
brief_fields = ['_depth', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['_depth', 'device', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -1394,7 +1397,7 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class CableTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Cable
|
||||
brief_fields = ['id', 'label', 'url']
|
||||
brief_fields = ['display', 'id', 'label', 'url']
|
||||
bulk_update_data = {
|
||||
'length': 100,
|
||||
'length_unit': 'm',
|
||||
@@ -1579,7 +1582,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class PowerPanelTest(APIViewTestCases.APIViewTestCase):
|
||||
model = PowerPanel
|
||||
brief_fields = ['id', 'name', 'powerfeed_count', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'powerfeed_count', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1588,17 +1591,17 @@ class PowerPanelTest(APIViewTestCases.APIViewTestCase):
|
||||
Site.objects.create(name='Site 2', slug='site-2'),
|
||||
)
|
||||
|
||||
rack_groups = (
|
||||
RackGroup.objects.create(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup.objects.create(name='Rack Group 2', slug='rack-group-2', site=sites[0]),
|
||||
RackGroup.objects.create(name='Rack Group 3', slug='rack-group-3', site=sites[0]),
|
||||
RackGroup.objects.create(name='Rack Group 4', slug='rack-group-3', site=sites[1]),
|
||||
locations = (
|
||||
Location.objects.create(name='Location 1', slug='location-1', site=sites[0]),
|
||||
Location.objects.create(name='Location 2', slug='location-2', site=sites[0]),
|
||||
Location.objects.create(name='Location 3', slug='location-3', site=sites[0]),
|
||||
Location.objects.create(name='Location 4', slug='location-3', site=sites[1]),
|
||||
)
|
||||
|
||||
power_panels = (
|
||||
PowerPanel(site=sites[0], rack_group=rack_groups[0], name='Power Panel 1'),
|
||||
PowerPanel(site=sites[0], rack_group=rack_groups[1], name='Power Panel 2'),
|
||||
PowerPanel(site=sites[0], rack_group=rack_groups[2], name='Power Panel 3'),
|
||||
PowerPanel(site=sites[0], location=locations[0], name='Power Panel 1'),
|
||||
PowerPanel(site=sites[0], location=locations[1], name='Power Panel 2'),
|
||||
PowerPanel(site=sites[0], location=locations[2], name='Power Panel 3'),
|
||||
)
|
||||
PowerPanel.objects.bulk_create(power_panels)
|
||||
|
||||
@@ -1606,29 +1609,29 @@ class PowerPanelTest(APIViewTestCases.APIViewTestCase):
|
||||
{
|
||||
'name': 'Power Panel 4',
|
||||
'site': sites[0].pk,
|
||||
'rack_group': rack_groups[0].pk,
|
||||
'location': locations[0].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Power Panel 5',
|
||||
'site': sites[0].pk,
|
||||
'rack_group': rack_groups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
},
|
||||
{
|
||||
'name': 'Power Panel 6',
|
||||
'site': sites[0].pk,
|
||||
'rack_group': rack_groups[2].pk,
|
||||
'location': locations[2].pk,
|
||||
},
|
||||
]
|
||||
|
||||
cls.bulk_update_data = {
|
||||
'site': sites[1].pk,
|
||||
'rack_group': rack_groups[3].pk
|
||||
'location': locations[3].pk
|
||||
}
|
||||
|
||||
|
||||
class PowerFeedTest(APIViewTestCases.APIViewTestCase):
|
||||
model = PowerFeed
|
||||
brief_fields = ['_occupied', 'cable', 'id', 'name', 'url']
|
||||
brief_fields = ['_occupied', 'cable', 'display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
@@ -1636,20 +1639,20 @@ class PowerFeedTest(APIViewTestCases.APIViewTestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
rackgroup = RackGroup.objects.create(site=site, name='Rack Group 1', slug='rack-group-1')
|
||||
location = Location.objects.create(site=site, name='Location 1', slug='location-1')
|
||||
rackrole = RackRole.objects.create(name='Rack Role 1', slug='rack-role-1', color='ff0000')
|
||||
|
||||
racks = (
|
||||
Rack(site=site, group=rackgroup, role=rackrole, name='Rack 1'),
|
||||
Rack(site=site, group=rackgroup, role=rackrole, name='Rack 2'),
|
||||
Rack(site=site, group=rackgroup, role=rackrole, name='Rack 3'),
|
||||
Rack(site=site, group=rackgroup, role=rackrole, name='Rack 4'),
|
||||
Rack(site=site, location=location, role=rackrole, name='Rack 1'),
|
||||
Rack(site=site, location=location, role=rackrole, name='Rack 2'),
|
||||
Rack(site=site, location=location, role=rackrole, name='Rack 3'),
|
||||
Rack(site=site, location=location, role=rackrole, name='Rack 4'),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
power_panels = (
|
||||
PowerPanel(site=site, rack_group=rackgroup, name='Power Panel 1'),
|
||||
PowerPanel(site=site, rack_group=rackgroup, name='Power Panel 2'),
|
||||
PowerPanel(site=site, location=location, name='Power Panel 1'),
|
||||
PowerPanel(site=site, location=location, name='Power Panel 2'),
|
||||
)
|
||||
PowerPanel.objects.bulk_create(power_panels)
|
||||
|
||||
|
||||
@@ -3,13 +3,7 @@ from django.test import TestCase
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.filters import *
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
InventoryItem, Manufacturer, Platform, PowerFeed, PowerPanel, PowerPort, PowerPortTemplate, PowerOutlet,
|
||||
PowerOutletTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from dcim.models import *
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
@@ -65,6 +59,56 @@ class RegionTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class SiteGroupTestCase(TestCase):
|
||||
queryset = SiteGroup.objects.all()
|
||||
filterset = SiteGroupFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
sitegroups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1', description='A'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2', description='B'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3', description='C'),
|
||||
)
|
||||
for sitegroup in sitegroups:
|
||||
sitegroup.save()
|
||||
|
||||
child_sitegroups = (
|
||||
SiteGroup(name='Site Group 1A', slug='site-group-1a', parent=sitegroups[0]),
|
||||
SiteGroup(name='Site Group 1B', slug='site-group-1b', parent=sitegroups[0]),
|
||||
SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=sitegroups[1]),
|
||||
SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=sitegroups[1]),
|
||||
SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=sitegroups[2]),
|
||||
SiteGroup(name='Site Group 3B', slug='site-group-3b', parent=sitegroups[2]),
|
||||
)
|
||||
for sitegroup in child_sitegroups:
|
||||
sitegroup.save()
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Site Group 1', 'Site Group 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['site-group-1', 'site-group-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_parent(self):
|
||||
parent_sitegroups = SiteGroup.objects.filter(parent__isnull=True)[:2]
|
||||
params = {'parent_id': [parent_sitegroups[0].pk, parent_sitegroups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'parent': [parent_sitegroups[0].slug, parent_sitegroups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class SiteTestCase(TestCase):
|
||||
queryset = Site.objects.all()
|
||||
filterset = SiteFilterSet
|
||||
@@ -80,6 +124,14 @@ class SiteTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
tenant_groups = (
|
||||
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
||||
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
||||
@@ -96,9 +148,9 @@ class SiteTestCase(TestCase):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], tenant=tenants[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com'),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com'),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], tenant=tenants[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30, contact_name='Contact 3', contact_phone='123-555-0003', contact_email='contact3@example.com'),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0], tenant=tenants[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com'),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com'),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2], tenant=tenants[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30, contact_name='Contact 3', contact_phone='123-555-0003', contact_email='contact3@example.com'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@@ -153,6 +205,13 @@ class SiteTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
groups = SiteGroup.objects.all()[:2]
|
||||
params = {'group_id': [groups[0].pk, groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'group': [groups[0].slug, groups[1].slug]}
|
||||
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]}
|
||||
@@ -168,9 +227,9 @@ class SiteTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class RackGroupTestCase(TestCase):
|
||||
queryset = RackGroup.objects.all()
|
||||
filterset = RackGroupFilterSet
|
||||
class LocationTestCase(TestCase):
|
||||
queryset = Location.objects.all()
|
||||
filterset = LocationFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -183,39 +242,47 @@ class RackGroupTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
parent_rack_groups = (
|
||||
RackGroup(name='Parent Rack Group 1', slug='parent-rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Parent Rack Group 2', slug='parent-rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Parent Rack Group 3', slug='parent-rack-group-3', site=sites[2]),
|
||||
parent_locations = (
|
||||
Location(name='Parent Location 1', slug='parent-location-1', site=sites[0]),
|
||||
Location(name='Parent Location 2', slug='parent-location-2', site=sites[1]),
|
||||
Location(name='Parent Location 3', slug='parent-location-3', site=sites[2]),
|
||||
)
|
||||
for rackgroup in parent_rack_groups:
|
||||
rackgroup.save()
|
||||
for location in parent_locations:
|
||||
location.save()
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0], parent=parent_rack_groups[0], description='A'),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1], parent=parent_rack_groups[1], description='B'),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2], parent=parent_rack_groups[2], description='C'),
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0], parent=parent_locations[0], description='A'),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1], parent=parent_locations[1], description='B'),
|
||||
Location(name='Location 3', slug='location-3', site=sites[2], parent=parent_locations[2], description='C'),
|
||||
)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Rack Group 1', 'Rack Group 2']}
|
||||
params = {'name': ['Location 1', 'Location 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['rack-group-1', 'rack-group-2']}
|
||||
params = {'slug': ['location-1', 'location-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
@@ -229,6 +296,13 @@ class RackGroupTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -237,7 +311,7 @@ class RackGroupTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_parent(self):
|
||||
parent_groups = RackGroup.objects.filter(name__startswith='Parent')[:2]
|
||||
parent_groups = Location.objects.filter(name__startswith='Parent')[:2]
|
||||
params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]}
|
||||
@@ -290,20 +364,28 @@ class RackTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1]),
|
||||
Location(name='Location 3', slug='location-3', site=sites[2]),
|
||||
)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
rack_roles = (
|
||||
RackRole(name='Rack Role 1', slug='rack-role-1'),
|
||||
@@ -328,9 +410,9 @@ class RackTestCase(TestCase):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', facility_id='rack-1', site=sites[0], group=rack_groups[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
|
||||
Rack(name='Rack 2', facility_id='rack-2', site=sites[1], group=rack_groups[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
|
||||
Rack(name='Rack 3', facility_id='rack-3', site=sites[2], group=rack_groups[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH),
|
||||
Rack(name='Rack 1', facility_id='rack-1', site=sites[0], location=locations[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
|
||||
Rack(name='Rack 2', facility_id='rack-2', site=sites[1], location=locations[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
|
||||
Rack(name='Rack 3', facility_id='rack-3', site=sites[2], location=locations[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
@@ -388,6 +470,13 @@ class RackTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -395,11 +484,11 @@ class RackTestCase(TestCase):
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_group(self):
|
||||
groups = RackGroup.objects.all()[:2]
|
||||
params = {'group_id': [groups[0].pk, groups[1].pk]}
|
||||
def test_location(self):
|
||||
locations = Location.objects.all()[:2]
|
||||
params = {'location_id': [locations[0].pk, locations[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'group': [groups[0].slug, groups[1].slug]}
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_status(self):
|
||||
@@ -448,18 +537,18 @@ class RackReservationTestCase(TestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1]),
|
||||
Location(name='Location 3', slug='location-3', site=sites[2]),
|
||||
)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0], group=rack_groups[0]),
|
||||
Rack(name='Rack 2', site=sites[1], group=rack_groups[1]),
|
||||
Rack(name='Rack 3', site=sites[2], group=rack_groups[2]),
|
||||
Rack(name='Rack 1', site=sites[0], location=locations[0]),
|
||||
Rack(name='Rack 2', site=sites[1], location=locations[1]),
|
||||
Rack(name='Rack 3', site=sites[2], location=locations[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
@@ -503,11 +592,11 @@ class RackReservationTestCase(TestCase):
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_group(self):
|
||||
groups = RackGroup.objects.all()[:2]
|
||||
params = {'group_id': [groups[0].pk, groups[1].pk]}
|
||||
def test_location(self):
|
||||
locations = Location.objects.all()[:2]
|
||||
params = {'location_id': [locations[0].pk, locations[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'group': [groups[0].slug, groups[1].slug]}
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_user(self):
|
||||
@@ -1161,25 +1250,33 @@ class DeviceTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1]),
|
||||
Location(name='Location 3', slug='location-3', site=sites[2]),
|
||||
)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0], group=rack_groups[0]),
|
||||
Rack(name='Rack 2', site=sites[1], group=rack_groups[1]),
|
||||
Rack(name='Rack 3', site=sites[2], group=rack_groups[2]),
|
||||
Rack(name='Rack 1', site=sites[0], location=locations[0]),
|
||||
Rack(name='Rack 2', site=sites[1], location=locations[1]),
|
||||
Rack(name='Rack 3', site=sites[2], location=locations[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
@@ -1207,9 +1304,9 @@ class DeviceTestCase(TestCase):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], tenant=tenants[0], serial='ABC', asset_tag='1001', site=sites[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}),
|
||||
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], tenant=tenants[1], serial='DEF', asset_tag='1002', site=sites[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED, cluster=clusters[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], tenant=tenants[2], serial='GHI', asset_tag='1003', site=sites[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED, cluster=clusters[2]),
|
||||
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], tenant=tenants[0], serial='ABC', asset_tag='1001', site=sites[0], location=locations[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}),
|
||||
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], tenant=tenants[1], serial='DEF', asset_tag='1002', site=sites[1], location=locations[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED, cluster=clusters[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], tenant=tenants[2], serial='GHI', asset_tag='1003', site=sites[2], location=locations[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED, cluster=clusters[2]),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -1324,6 +1421,13 @@ class DeviceTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -1331,9 +1435,9 @@ class DeviceTestCase(TestCase):
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rackgroup(self):
|
||||
rack_groups = RackGroup.objects.all()[:2]
|
||||
params = {'rack_group_id': [rack_groups[0].pk, rack_groups[1].pk]}
|
||||
def test_location(self):
|
||||
locations = Location.objects.all()[:2]
|
||||
params = {'location_id': [locations[0].pk, locations[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rack(self):
|
||||
@@ -1463,10 +1567,19 @@ class ConsolePortTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@@ -1524,6 +1637,13 @@ class ConsolePortTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -1559,10 +1679,19 @@ class ConsoleServerPortTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@@ -1620,6 +1749,13 @@ class ConsoleServerPortTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -1655,10 +1791,19 @@ class PowerPortTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@@ -1724,6 +1869,13 @@ class PowerPortTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -1759,10 +1911,19 @@ class PowerOutletTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@@ -1825,6 +1986,13 @@ class PowerOutletTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -1860,10 +2028,19 @@ class InterfaceTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@@ -1931,6 +2108,34 @@ class InterfaceTestCase(TestCase):
|
||||
params = {'description': ['First', 'Second']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_parent(self):
|
||||
# Create child interfaces
|
||||
parent_interface = Interface.objects.first()
|
||||
child_interfaces = (
|
||||
Interface(device=parent_interface.device, name='Child 1', parent=parent_interface, type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||
Interface(device=parent_interface.device, name='Child 2', parent=parent_interface, type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||
Interface(device=parent_interface.device, name='Child 3', parent=parent_interface, type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||
)
|
||||
Interface.objects.bulk_create(child_interfaces)
|
||||
|
||||
params = {'parent_id': [parent_interface.pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_lag(self):
|
||||
# Create LAG members
|
||||
device = Device.objects.first()
|
||||
lag_interface = Interface(device=device, name='LAG', type=InterfaceTypeChoices.TYPE_LAG)
|
||||
lag_interface.save()
|
||||
lag_members = (
|
||||
Interface(device=device, name='Member 1', lag=lag_interface, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=device, name='Member 2', lag=lag_interface, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=device, name='Member 3', lag=lag_interface, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
)
|
||||
Interface.objects.bulk_create(lag_members)
|
||||
|
||||
params = {'lag_id': [lag_interface.pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
@@ -1938,6 +2143,13 @@ class InterfaceTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -1987,10 +2199,19 @@ class FrontPortTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@@ -2054,6 +2275,13 @@ class FrontPortTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -2089,10 +2317,19 @@ class RearPortTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@@ -2150,6 +2387,13 @@ class RearPortTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -2185,10 +2429,19 @@ class DeviceBayTestCase(TestCase):
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = Site.objects.bulk_create((
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
Site(name='Site X', slug='site-x'),
|
||||
))
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
@@ -2228,6 +2481,13 @@ class DeviceBayTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -2268,10 +2528,18 @@ class InventoryItemTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@@ -2328,6 +2596,13 @@ class InventoryItemTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -2381,10 +2656,18 @@ class VirtualChassisTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@@ -2435,6 +2718,13 @@ class VirtualChassisTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -2582,25 +2872,33 @@ class PowerPanelTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1]),
|
||||
Location(name='Location 3', slug='location-3', site=sites[2]),
|
||||
)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
power_panels = (
|
||||
PowerPanel(name='Power Panel 1', site=sites[0], rack_group=rack_groups[0]),
|
||||
PowerPanel(name='Power Panel 2', site=sites[1], rack_group=rack_groups[1]),
|
||||
PowerPanel(name='Power Panel 3', site=sites[2], rack_group=rack_groups[2]),
|
||||
PowerPanel(name='Power Panel 1', site=sites[0], location=locations[0]),
|
||||
PowerPanel(name='Power Panel 2', site=sites[1], location=locations[1]),
|
||||
PowerPanel(name='Power Panel 3', site=sites[2], location=locations[2]),
|
||||
)
|
||||
PowerPanel.objects.bulk_create(power_panels)
|
||||
|
||||
@@ -2619,6 +2917,13 @@ class PowerPanelTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -2626,9 +2931,9 @@ class PowerPanelTestCase(TestCase):
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rack_group(self):
|
||||
rack_groups = RackGroup.objects.all()[:2]
|
||||
params = {'rack_group_id': [rack_groups[0].pk, rack_groups[1].pk]}
|
||||
def test_location(self):
|
||||
locations = Location.objects.all()[:2]
|
||||
params = {'location_id': [locations[0].pk, locations[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
@@ -2647,10 +2952,18 @@ class PowerFeedTestCase(TestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@@ -2731,6 +3044,13 @@ class PowerFeedTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
|
||||
@@ -7,39 +7,66 @@ from dcim.models import *
|
||||
from tenancy.models import Tenant
|
||||
|
||||
|
||||
class RackGroupTestCase(TestCase):
|
||||
class LocationTestCase(TestCase):
|
||||
|
||||
def test_change_rackgroup_site(self):
|
||||
def test_change_location_site(self):
|
||||
"""
|
||||
Check that all child RackGroups and Racks get updated when a RackGroup is moved to a new Site. Topology:
|
||||
Check that all child Locations and Racks get updated when a Location is moved to a new Site. Topology:
|
||||
Site A
|
||||
- RackGroup A1
|
||||
- RackGroup A2
|
||||
- Location A1
|
||||
- Location A2
|
||||
- Rack 2
|
||||
- Device 2
|
||||
- Rack 1
|
||||
- Device 1
|
||||
"""
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||
)
|
||||
device_role = DeviceRole.objects.create(
|
||||
name='Device Role 1', slug='device-role-1', color='ff0000'
|
||||
)
|
||||
|
||||
site_a = Site.objects.create(name='Site A', slug='site-a')
|
||||
site_b = Site.objects.create(name='Site B', slug='site-b')
|
||||
|
||||
rackgroup_a1 = RackGroup(site=site_a, name='RackGroup A1', slug='rackgroup-a1')
|
||||
rackgroup_a1.save()
|
||||
rackgroup_a2 = RackGroup(site=site_a, parent=rackgroup_a1, name='RackGroup A2', slug='rackgroup-a2')
|
||||
rackgroup_a2.save()
|
||||
location_a1 = Location(site=site_a, name='Location A1', slug='location-a1')
|
||||
location_a1.save()
|
||||
location_a2 = Location(site=site_a, parent=location_a1, name='Location A2', slug='location-a2')
|
||||
location_a2.save()
|
||||
|
||||
rack1 = Rack.objects.create(site=site_a, group=rackgroup_a1, name='Rack 1')
|
||||
rack2 = Rack.objects.create(site=site_a, group=rackgroup_a2, name='Rack 2')
|
||||
rack1 = Rack.objects.create(site=site_a, location=location_a1, name='Rack 1')
|
||||
rack2 = Rack.objects.create(site=site_a, location=location_a2, name='Rack 2')
|
||||
|
||||
powerpanel1 = PowerPanel.objects.create(site=site_a, rack_group=rackgroup_a1, name='Power Panel 1')
|
||||
device1 = Device.objects.create(
|
||||
site=site_a,
|
||||
location=location_a1,
|
||||
name='Device 1',
|
||||
device_type=device_type,
|
||||
device_role=device_role
|
||||
)
|
||||
device2 = Device.objects.create(
|
||||
site=site_a,
|
||||
location=location_a2,
|
||||
name='Device 2',
|
||||
device_type=device_type,
|
||||
device_role=device_role
|
||||
)
|
||||
|
||||
# Move RackGroup A1 to Site B
|
||||
rackgroup_a1.site = site_b
|
||||
rackgroup_a1.save()
|
||||
powerpanel1 = PowerPanel.objects.create(site=site_a, location=location_a1, name='Power Panel 1')
|
||||
|
||||
# Check that all objects within RackGroup A1 now belong to Site B
|
||||
self.assertEqual(RackGroup.objects.get(pk=rackgroup_a1.pk).site, site_b)
|
||||
self.assertEqual(RackGroup.objects.get(pk=rackgroup_a2.pk).site, site_b)
|
||||
# Move Location A1 to Site B
|
||||
location_a1.site = site_b
|
||||
location_a1.save()
|
||||
|
||||
# Check that all objects within Location A1 now belong to Site B
|
||||
self.assertEqual(Location.objects.get(pk=location_a1.pk).site, site_b)
|
||||
self.assertEqual(Location.objects.get(pk=location_a2.pk).site, site_b)
|
||||
self.assertEqual(Rack.objects.get(pk=rack1.pk).site, site_b)
|
||||
self.assertEqual(Rack.objects.get(pk=rack2.pk).site, site_b)
|
||||
self.assertEqual(Device.objects.get(pk=device1.pk).site, site_b)
|
||||
self.assertEqual(Device.objects.get(pk=device2.pk).site, site_b)
|
||||
self.assertEqual(PowerPanel.objects.get(pk=powerpanel1.pk).site, site_b)
|
||||
|
||||
|
||||
@@ -55,12 +82,12 @@ class RackTestCase(TestCase):
|
||||
name='TestSite2',
|
||||
slug='test-site-2'
|
||||
)
|
||||
self.group1 = RackGroup.objects.create(
|
||||
self.location1 = Location.objects.create(
|
||||
name='TestGroup1',
|
||||
slug='test-group-1',
|
||||
site=self.site1
|
||||
)
|
||||
self.group2 = RackGroup.objects.create(
|
||||
self.location2 = Location.objects.create(
|
||||
name='TestGroup2',
|
||||
slug='test-group-2',
|
||||
site=self.site2
|
||||
@@ -69,7 +96,7 @@ class RackTestCase(TestCase):
|
||||
name='TestRack1',
|
||||
facility_id='A101',
|
||||
site=self.site1,
|
||||
group=self.group1,
|
||||
location=self.location1,
|
||||
u_height=42
|
||||
)
|
||||
self.manufacturer = Manufacturer.objects.create(
|
||||
@@ -134,19 +161,19 @@ class RackTestCase(TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
rack1.clean()
|
||||
|
||||
def test_rack_group_site(self):
|
||||
def test_location_site(self):
|
||||
|
||||
rack_invalid_group = Rack(
|
||||
rack_invalid_location = Rack(
|
||||
name='TestRack2',
|
||||
facility_id='A102',
|
||||
site=self.site1,
|
||||
u_height=42,
|
||||
group=self.group2
|
||||
location=self.location2
|
||||
)
|
||||
rack_invalid_group.save()
|
||||
rack_invalid_location.save()
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
rack_invalid_group.clean()
|
||||
rack_invalid_location.clean()
|
||||
|
||||
def test_mount_single_device(self):
|
||||
|
||||
|
||||
@@ -57,6 +57,44 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
"Region 6,region-6,Sixth region",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = SiteGroup
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
# Create three SiteGroups
|
||||
sitegroups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for sitegroup in sitegroups:
|
||||
sitegroup.save()
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Site Group X',
|
||||
'slug': 'site-group-x',
|
||||
'parent': sitegroups[2].pk,
|
||||
'description': 'A new site group',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug,description",
|
||||
"Site Group 4,site-group-4,Fourth site group",
|
||||
"Site Group 5,site-group-5,Fifth site group",
|
||||
"Site Group 6,site-group-6,Sixth site group",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Site
|
||||
@@ -71,10 +109,17 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
)
|
||||
for group in groups:
|
||||
group.save()
|
||||
|
||||
Site.objects.bulk_create([
|
||||
Site(name='Site 1', slug='site-1', region=regions[0]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[0]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[0]),
|
||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[1]),
|
||||
Site(name='Site 2', slug='site-2', region=regions[0], group=groups[1]),
|
||||
Site(name='Site 3', slug='site-3', region=regions[0], group=groups[1]),
|
||||
])
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
@@ -84,6 +129,7 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'slug': 'site-x',
|
||||
'status': SiteStatusChoices.STATUS_PLANNED,
|
||||
'region': regions[1].pk,
|
||||
'group': groups[1].pk,
|
||||
'tenant': None,
|
||||
'facility': 'Facility X',
|
||||
'asn': 65001,
|
||||
@@ -110,6 +156,7 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
cls.bulk_edit_data = {
|
||||
'status': SiteStatusChoices.STATUS_PLANNED,
|
||||
'region': regions[1].pk,
|
||||
'group': groups[1].pk,
|
||||
'tenant': None,
|
||||
'asn': 65009,
|
||||
'time_zone': pytz.timezone('US/Eastern'),
|
||||
@@ -117,8 +164,8 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class RackGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = RackGroup
|
||||
class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = Location
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -126,28 +173,32 @@ class RackGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=site),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=site),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=site),
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=site),
|
||||
Location(name='Location 2', slug='location-2', site=site),
|
||||
Location(name='Location 3', slug='location-3', site=site),
|
||||
)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Rack Group X',
|
||||
'slug': 'rack-group-x',
|
||||
'name': 'Location X',
|
||||
'slug': 'location-x',
|
||||
'site': site.pk,
|
||||
'description': 'A new rack group',
|
||||
'description': 'A new location',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"site,name,slug,description",
|
||||
"Site 1,Rack Group 4,rack-group-4,Fourth rack group",
|
||||
"Site 1,Rack Group 5,rack-group-5,Fifth rack group",
|
||||
"Site 1,Rack Group 6,rack-group-6,Sixth rack group",
|
||||
"Site 1,Location 4,location-4,Fourth location",
|
||||
"Site 1,Location 5,location-5,Fifth location",
|
||||
"Site 1,Location 6,location-6,Sixth location",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = RackRole
|
||||
@@ -175,6 +226,11 @@ class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
"Rack Role 6,rack-role-6,0000ff",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'color': '00ff00',
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = RackReservation
|
||||
@@ -187,10 +243,10 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
|
||||
rack_group = RackGroup(name='Rack Group 1', slug='rack-group-1', site=site)
|
||||
rack_group.save()
|
||||
location = Location(name='Location 1', slug='location-1', site=site)
|
||||
location.save()
|
||||
|
||||
rack = Rack(name='Rack 1', site=site, group=rack_group)
|
||||
rack = Rack(name='Rack 1', site=site, location=location)
|
||||
rack.save()
|
||||
|
||||
RackReservation.objects.bulk_create([
|
||||
@@ -211,10 +267,10 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
'site,rack_group,rack,units,description',
|
||||
'Site 1,Rack Group 1,Rack 1,"10,11,12",Reservation 1',
|
||||
'Site 1,Rack Group 1,Rack 1,"13,14,15",Reservation 2',
|
||||
'Site 1,Rack Group 1,Rack 1,"16,17,18",Reservation 3',
|
||||
'site,location,rack,units,description',
|
||||
'Site 1,Location 1,Rack 1,"10,11,12",Reservation 1',
|
||||
'Site 1,Location 1,Rack 1,"13,14,15",Reservation 2',
|
||||
'Site 1,Location 1,Rack 1,"16,17,18",Reservation 3',
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@@ -236,12 +292,12 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rackgroups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1])
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1])
|
||||
)
|
||||
for rackgroup in rackgroups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
rackroles = (
|
||||
RackRole(name='Rack Role 1', slug='rack-role-1'),
|
||||
@@ -261,7 +317,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'name': 'Rack X',
|
||||
'facility_id': 'Facility X',
|
||||
'site': sites[1].pk,
|
||||
'group': rackgroups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'tenant': None,
|
||||
'status': RackStatusChoices.STATUS_PLANNED,
|
||||
'role': rackroles[1].pk,
|
||||
@@ -279,15 +335,15 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"site,group,name,width,u_height",
|
||||
"site,location,name,width,u_height",
|
||||
"Site 1,,Rack 4,19,42",
|
||||
"Site 1,Rack Group 1,Rack 5,19,42",
|
||||
"Site 2,Rack Group 2,Rack 6,19,42",
|
||||
"Site 1,Location 1,Rack 5,19,42",
|
||||
"Site 2,Location 2,Rack 6,19,42",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'site': sites[1].pk,
|
||||
'group': rackgroups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'tenant': None,
|
||||
'status': RackStatusChoices.STATUS_DEPRECATED,
|
||||
'role': rackroles[1].pk,
|
||||
@@ -336,6 +392,10 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
"Manufacturer 6,manufacturer-6,Sixth manufacturer",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
# TODO: Change base class to PrimaryObjectViewTestCase
|
||||
# Blocked by absence of bulk import view for DeviceTypes
|
||||
@@ -885,6 +945,11 @@ class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
"Device Role 6,device-role-6,0000ff",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'color': '00ff00',
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = Platform
|
||||
@@ -916,6 +981,11 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
"Platform 6,platform-6,Sixth platform",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'napalm_driver': 'ios',
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Device
|
||||
@@ -929,11 +999,11 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rack_group = RackGroup(site=sites[0], name='Rack Group 1', slug='rack-group-1')
|
||||
rack_group.save()
|
||||
location = Location(site=sites[0], name='Location 1', slug='location-1')
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0], group=rack_group),
|
||||
Rack(name='Rack 1', site=sites[0], location=location),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
@@ -991,10 +1061,10 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"device_role,manufacturer,device_type,status,name,site,rack_group,rack,position,face",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 4,Site 1,Rack Group 1,Rack 1,10,front",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 5,Site 1,Rack Group 1,Rack 1,20,front",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 6,Site 1,Rack Group 1,Rack 1,30,front",
|
||||
"device_role,manufacturer,device_type,status,name,site,location,rack,position,face",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 4,Site 1,Location 1,Rack 1,10,front",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 5,Site 1,Location 1,Rack 1,20,front",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 6,Site 1,Location 1,Rack 1,30,front",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@@ -1771,38 +1841,38 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rackgroups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1]),
|
||||
)
|
||||
for rackgroup in rackgroups:
|
||||
rackgroup.save()
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
PowerPanel.objects.bulk_create((
|
||||
PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 1'),
|
||||
PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 2'),
|
||||
PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 3'),
|
||||
PowerPanel(site=sites[0], location=locations[0], name='Power Panel 1'),
|
||||
PowerPanel(site=sites[0], location=locations[0], name='Power Panel 2'),
|
||||
PowerPanel(site=sites[0], location=locations[0], name='Power Panel 3'),
|
||||
))
|
||||
|
||||
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'site': sites[1].pk,
|
||||
'rack_group': rackgroups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
'name': 'Power Panel X',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"site,rack_group,name",
|
||||
"Site 1,Rack Group 1,Power Panel 4",
|
||||
"Site 1,Rack Group 1,Power Panel 5",
|
||||
"Site 1,Rack Group 1,Power Panel 6",
|
||||
"site,location,name",
|
||||
"Site 1,Location 1,Power Panel 4",
|
||||
"Site 1,Location 1,Power Panel 5",
|
||||
"Site 1,Location 1,Power Panel 6",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'site': sites[1].pk,
|
||||
'rack_group': rackgroups[1].pk,
|
||||
'location': locations[1].pk,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from extras.views import ObjectChangeLogView, ImageAttachmentEditView
|
||||
from extras.views import ImageAttachmentEditView, ObjectChangeLogView, ObjectJournalView
|
||||
from ipam.views import ServiceEditView
|
||||
from . import views
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, FrontPort, Interface,
|
||||
InventoryItem, Manufacturer, Platform, PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup,
|
||||
RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
|
||||
)
|
||||
from .models import *
|
||||
|
||||
app_name = 'dcim'
|
||||
urlpatterns = [
|
||||
@@ -16,11 +12,22 @@ urlpatterns = [
|
||||
path('regions/', views.RegionListView.as_view(), name='region_list'),
|
||||
path('regions/add/', views.RegionEditView.as_view(), name='region_add'),
|
||||
path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'),
|
||||
path('regions/edit/', views.RegionBulkEditView.as_view(), name='region_bulk_edit'),
|
||||
path('regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
||||
path('regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'),
|
||||
path('regions/<int:pk>/delete/', views.RegionDeleteView.as_view(), name='region_delete'),
|
||||
path('regions/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
|
||||
|
||||
# Site groups
|
||||
path('site-groups/', views.SiteGroupListView.as_view(), name='sitegroup_list'),
|
||||
path('site-groups/add/', views.SiteGroupEditView.as_view(), name='sitegroup_add'),
|
||||
path('site-groups/import/', views.SiteGroupBulkImportView.as_view(), name='sitegroup_import'),
|
||||
path('site-groups/edit/', views.SiteGroupBulkEditView.as_view(), name='sitegroup_bulk_edit'),
|
||||
path('site-groups/delete/', views.SiteGroupBulkDeleteView.as_view(), name='sitegroup_bulk_delete'),
|
||||
path('site-groups/<int:pk>/edit/', views.SiteGroupEditView.as_view(), name='sitegroup_edit'),
|
||||
path('site-groups/<int:pk>/delete/', views.SiteGroupDeleteView.as_view(), name='sitegroup_delete'),
|
||||
path('site-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='sitegroup_changelog', kwargs={'model': SiteGroup}),
|
||||
|
||||
# Sites
|
||||
path('sites/', views.SiteListView.as_view(), name='site_list'),
|
||||
path('sites/add/', views.SiteEditView.as_view(), name='site_add'),
|
||||
@@ -31,21 +38,24 @@ urlpatterns = [
|
||||
path('sites/<int:pk>/edit/', views.SiteEditView.as_view(), name='site_edit'),
|
||||
path('sites/<int:pk>/delete/', views.SiteDeleteView.as_view(), name='site_delete'),
|
||||
path('sites/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
|
||||
path('sites/<int:pk>/journal/', ObjectJournalView.as_view(), name='site_journal', kwargs={'model': Site}),
|
||||
path('sites/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
|
||||
|
||||
# Rack groups
|
||||
path('rack-groups/', views.RackGroupListView.as_view(), name='rackgroup_list'),
|
||||
path('rack-groups/add/', views.RackGroupEditView.as_view(), name='rackgroup_add'),
|
||||
path('rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
|
||||
path('rack-groups/delete/', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
|
||||
path('rack-groups/<int:pk>/edit/', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
|
||||
path('rack-groups/<int:pk>/delete/', views.RackGroupDeleteView.as_view(), name='rackgroup_delete'),
|
||||
path('rack-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
|
||||
# Locations
|
||||
path('locations/', views.LocationListView.as_view(), name='location_list'),
|
||||
path('locations/add/', views.LocationEditView.as_view(), name='location_add'),
|
||||
path('locations/import/', views.LocationBulkImportView.as_view(), name='location_import'),
|
||||
path('locations/edit/', views.LocationBulkEditView.as_view(), name='location_bulk_edit'),
|
||||
path('locations/delete/', views.LocationBulkDeleteView.as_view(), name='location_bulk_delete'),
|
||||
path('locations/<int:pk>/edit/', views.LocationEditView.as_view(), name='location_edit'),
|
||||
path('locations/<int:pk>/delete/', views.LocationDeleteView.as_view(), name='location_delete'),
|
||||
path('locations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='location_changelog', kwargs={'model': Location}),
|
||||
|
||||
# Rack roles
|
||||
path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'),
|
||||
path('rack-roles/add/', views.RackRoleEditView.as_view(), name='rackrole_add'),
|
||||
path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
|
||||
path('rack-roles/edit/', views.RackRoleBulkEditView.as_view(), name='rackrole_bulk_edit'),
|
||||
path('rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
||||
path('rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||
path('rack-roles/<int:pk>/delete/', views.RackRoleDeleteView.as_view(), name='rackrole_delete'),
|
||||
@@ -61,6 +71,7 @@ urlpatterns = [
|
||||
path('rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||
path('rack-reservations/<int:pk>/delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||
path('rack-reservations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
|
||||
path('rack-reservations/<int:pk>/journal/', ObjectJournalView.as_view(), name='rackreservation_journal', kwargs={'model': RackReservation}),
|
||||
|
||||
# Racks
|
||||
path('racks/', views.RackListView.as_view(), name='rack_list'),
|
||||
@@ -73,12 +84,14 @@ urlpatterns = [
|
||||
path('racks/<int:pk>/edit/', views.RackEditView.as_view(), name='rack_edit'),
|
||||
path('racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||
path('racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
|
||||
path('racks/<int:pk>/journal/', ObjectJournalView.as_view(), name='rack_journal', kwargs={'model': Rack}),
|
||||
path('racks/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
|
||||
|
||||
# Manufacturers
|
||||
path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||
path('manufacturers/add/', views.ManufacturerEditView.as_view(), name='manufacturer_add'),
|
||||
path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
|
||||
path('manufacturers/edit/', views.ManufacturerBulkEditView.as_view(), name='manufacturer_bulk_edit'),
|
||||
path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
|
||||
path('manufacturers/<int:pk>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
|
||||
path('manufacturers/<int:pk>/delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'),
|
||||
@@ -94,6 +107,7 @@ urlpatterns = [
|
||||
path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
|
||||
path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
||||
path('device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
|
||||
path('device-types/<int:pk>/journal/', ObjectJournalView.as_view(), name='devicetype_journal', kwargs={'model': DeviceType}),
|
||||
|
||||
# Console port templates
|
||||
path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'),
|
||||
@@ -163,6 +177,7 @@ urlpatterns = [
|
||||
path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||
path('device-roles/add/', views.DeviceRoleEditView.as_view(), name='devicerole_add'),
|
||||
path('device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
|
||||
path('device-roles/edit/', views.DeviceRoleBulkEditView.as_view(), name='devicerole_bulk_edit'),
|
||||
path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
|
||||
path('device-roles/<int:pk>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
|
||||
path('device-roles/<int:pk>/delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'),
|
||||
@@ -172,6 +187,7 @@ urlpatterns = [
|
||||
path('platforms/', views.PlatformListView.as_view(), name='platform_list'),
|
||||
path('platforms/add/', views.PlatformEditView.as_view(), name='platform_add'),
|
||||
path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
|
||||
path('platforms/edit/', views.PlatformBulkEditView.as_view(), name='platform_bulk_edit'),
|
||||
path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
|
||||
path('platforms/<int:pk>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
|
||||
path('platforms/<int:pk>/delete/', views.PlatformDeleteView.as_view(), name='platform_delete'),
|
||||
@@ -198,6 +214,7 @@ urlpatterns = [
|
||||
path('devices/<int:pk>/inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'),
|
||||
path('devices/<int:pk>/config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
|
||||
path('devices/<int:pk>/changelog/', views.DeviceChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
|
||||
path('devices/<int:pk>/journal/', views.DeviceJournalView.as_view(), name='device_journal', kwargs={'model': Device}),
|
||||
path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
|
||||
path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||
path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
|
||||
@@ -353,6 +370,7 @@ urlpatterns = [
|
||||
path('cables/<int:pk>/edit/', views.CableEditView.as_view(), name='cable_edit'),
|
||||
path('cables/<int:pk>/delete/', views.CableDeleteView.as_view(), name='cable_delete'),
|
||||
path('cables/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}),
|
||||
path('cables/<int:pk>/journal/', ObjectJournalView.as_view(), name='cable_journal', kwargs={'model': Cable}),
|
||||
|
||||
# Console/power/interface connections (read-only)
|
||||
path('console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
|
||||
@@ -369,6 +387,7 @@ urlpatterns = [
|
||||
path('virtual-chassis/<int:pk>/edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
|
||||
path('virtual-chassis/<int:pk>/delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
|
||||
path('virtual-chassis/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
|
||||
path('virtual-chassis/<int:pk>/journal/', ObjectJournalView.as_view(), name='virtualchassis_journal', kwargs={'model': VirtualChassis}),
|
||||
path('virtual-chassis/<int:pk>/add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
|
||||
path('virtual-chassis-members/<int:pk>/delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
|
||||
|
||||
@@ -382,6 +401,7 @@ urlpatterns = [
|
||||
path('power-panels/<int:pk>/edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'),
|
||||
path('power-panels/<int:pk>/delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'),
|
||||
path('power-panels/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}),
|
||||
path('power-panels/<int:pk>/journal/', ObjectJournalView.as_view(), name='powerpanel_journal', kwargs={'model': PowerPanel}),
|
||||
|
||||
# Power feeds
|
||||
path('power-feeds/', views.PowerFeedListView.as_view(), name='powerfeed_list'),
|
||||
@@ -394,6 +414,7 @@ urlpatterns = [
|
||||
path('power-feeds/<int:pk>/delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'),
|
||||
path('power-feeds/<int:pk>/trace/', views.PathTraceView.as_view(), name='powerfeed_trace', kwargs={'model': PowerFeed}),
|
||||
path('power-feeds/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}),
|
||||
path('power-feeds/<int:pk>/journal/', ObjectJournalView.as_view(), name='powerfeed_journal', kwargs={'model': PowerFeed}),
|
||||
path('power-feeds/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerfeed_connect', kwargs={'termination_a_type': PowerFeed}),
|
||||
|
||||
]
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
|
||||
from circuits.models import Circuit
|
||||
from extras.views import ObjectChangeLogView, ObjectConfigContextView
|
||||
from extras.views import ObjectChangeLogView, ObjectConfigContextView, ObjectJournalView
|
||||
from ipam.models import IPAddress, Prefix, Service, VLAN
|
||||
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
|
||||
from netbox.views import generic
|
||||
@@ -30,8 +30,8 @@ from .models import (
|
||||
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
InventoryItem, Manufacturer, PathEndpoint, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel,
|
||||
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
PowerPort, PowerPortTemplate, Rack, Location, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
SiteGroup, VirtualChassis,
|
||||
)
|
||||
|
||||
|
||||
@@ -126,6 +126,19 @@ class RegionBulkImportView(generic.BulkImportView):
|
||||
table = tables.RegionTable
|
||||
|
||||
|
||||
class RegionBulkEditView(generic.BulkEditView):
|
||||
queryset = Region.objects.add_related_count(
|
||||
Region.objects.all(),
|
||||
Site,
|
||||
'region',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.RegionFilterSet
|
||||
table = tables.RegionTable
|
||||
form = forms.RegionBulkEditForm
|
||||
|
||||
|
||||
class RegionBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Region.objects.add_related_count(
|
||||
Region.objects.all(),
|
||||
@@ -138,6 +151,63 @@ class RegionBulkDeleteView(generic.BulkDeleteView):
|
||||
table = tables.RegionTable
|
||||
|
||||
|
||||
#
|
||||
# Site groups
|
||||
#
|
||||
|
||||
class SiteGroupListView(generic.ObjectListView):
|
||||
queryset = SiteGroup.objects.add_related_count(
|
||||
SiteGroup.objects.all(),
|
||||
Site,
|
||||
'group',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.SiteGroupFilterSet
|
||||
filterset_form = forms.SiteGroupFilterForm
|
||||
table = tables.SiteGroupTable
|
||||
|
||||
|
||||
class SiteGroupEditView(generic.ObjectEditView):
|
||||
queryset = SiteGroup.objects.all()
|
||||
model_form = forms.SiteGroupForm
|
||||
|
||||
|
||||
class SiteGroupDeleteView(generic.ObjectDeleteView):
|
||||
queryset = SiteGroup.objects.all()
|
||||
|
||||
|
||||
class SiteGroupBulkImportView(generic.BulkImportView):
|
||||
queryset = SiteGroup.objects.all()
|
||||
model_form = forms.SiteGroupCSVForm
|
||||
table = tables.SiteGroupTable
|
||||
|
||||
|
||||
class SiteGroupBulkEditView(generic.BulkEditView):
|
||||
queryset = SiteGroup.objects.add_related_count(
|
||||
SiteGroup.objects.all(),
|
||||
Site,
|
||||
'group',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.SiteGroupFilterSet
|
||||
table = tables.SiteGroupTable
|
||||
form = forms.SiteGroupBulkEditForm
|
||||
|
||||
|
||||
class SiteGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SiteGroup.objects.add_related_count(
|
||||
SiteGroup.objects.all(),
|
||||
Site,
|
||||
'group',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.SiteGroupFilterSet
|
||||
table = tables.SiteGroupTable
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
@@ -161,17 +231,17 @@ class SiteView(generic.ObjectView):
|
||||
'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).count(),
|
||||
'vm_count': VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=instance).count(),
|
||||
}
|
||||
rack_groups = RackGroup.objects.add_related_count(
|
||||
RackGroup.objects.all(),
|
||||
locations = Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Rack,
|
||||
'group',
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).restrict(request.user, 'view').filter(site=instance)
|
||||
|
||||
return {
|
||||
'stats': stats,
|
||||
'rack_groups': rack_groups,
|
||||
'locations': locations,
|
||||
}
|
||||
|
||||
|
||||
@@ -207,44 +277,57 @@ class SiteBulkDeleteView(generic.BulkDeleteView):
|
||||
# Rack groups
|
||||
#
|
||||
|
||||
class RackGroupListView(generic.ObjectListView):
|
||||
queryset = RackGroup.objects.add_related_count(
|
||||
RackGroup.objects.all(),
|
||||
class LocationListView(generic.ObjectListView):
|
||||
queryset = Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Rack,
|
||||
'group',
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.RackGroupFilterSet
|
||||
filterset_form = forms.RackGroupFilterForm
|
||||
table = tables.RackGroupTable
|
||||
filterset = filters.LocationFilterSet
|
||||
filterset_form = forms.LocationFilterForm
|
||||
table = tables.LocationTable
|
||||
|
||||
|
||||
class RackGroupEditView(generic.ObjectEditView):
|
||||
queryset = RackGroup.objects.all()
|
||||
model_form = forms.RackGroupForm
|
||||
class LocationEditView(generic.ObjectEditView):
|
||||
queryset = Location.objects.all()
|
||||
model_form = forms.LocationForm
|
||||
|
||||
|
||||
class RackGroupDeleteView(generic.ObjectDeleteView):
|
||||
queryset = RackGroup.objects.all()
|
||||
class LocationDeleteView(generic.ObjectDeleteView):
|
||||
queryset = Location.objects.all()
|
||||
|
||||
|
||||
class RackGroupBulkImportView(generic.BulkImportView):
|
||||
queryset = RackGroup.objects.all()
|
||||
model_form = forms.RackGroupCSVForm
|
||||
table = tables.RackGroupTable
|
||||
class LocationBulkImportView(generic.BulkImportView):
|
||||
queryset = Location.objects.all()
|
||||
model_form = forms.LocationCSVForm
|
||||
table = tables.LocationTable
|
||||
|
||||
|
||||
class RackGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RackGroup.objects.add_related_count(
|
||||
RackGroup.objects.all(),
|
||||
class LocationBulkEditView(generic.BulkEditView):
|
||||
queryset = Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Rack,
|
||||
'group',
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site')
|
||||
filterset = filters.RackGroupFilterSet
|
||||
table = tables.RackGroupTable
|
||||
filterset = filters.LocationFilterSet
|
||||
table = tables.LocationTable
|
||||
form = forms.LocationBulkEditForm
|
||||
|
||||
|
||||
class LocationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Rack,
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site')
|
||||
filterset = filters.LocationFilterSet
|
||||
table = tables.LocationTable
|
||||
|
||||
|
||||
#
|
||||
@@ -273,6 +356,15 @@ class RackRoleBulkImportView(generic.BulkImportView):
|
||||
table = tables.RackRoleTable
|
||||
|
||||
|
||||
class RackRoleBulkEditView(generic.BulkEditView):
|
||||
queryset = RackRole.objects.annotate(
|
||||
rack_count=count_related(Rack, 'role')
|
||||
)
|
||||
filterset = filters.RackRoleFilterSet
|
||||
table = tables.RackRoleTable
|
||||
form = forms.RackRoleBulkEditForm
|
||||
|
||||
|
||||
class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RackRole.objects.annotate(
|
||||
rack_count=count_related(Rack, 'role')
|
||||
@@ -286,7 +378,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class RackListView(generic.ObjectListView):
|
||||
queryset = Rack.objects.prefetch_related(
|
||||
'site', 'group', 'tenant', 'role', 'devices__device_type'
|
||||
'site', 'location', 'tenant', 'role', 'devices__device_type'
|
||||
).annotate(
|
||||
device_count=count_related(Device, 'rack')
|
||||
)
|
||||
@@ -338,7 +430,7 @@ class RackElevationListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class RackView(generic.ObjectView):
|
||||
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'group', 'role')
|
||||
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role')
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
# Get 0U and child devices located within the rack
|
||||
@@ -349,10 +441,10 @@ class RackView(generic.ObjectView):
|
||||
|
||||
peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
|
||||
|
||||
if instance.group:
|
||||
peer_racks = peer_racks.filter(group=instance.group)
|
||||
if instance.location:
|
||||
peer_racks = peer_racks.filter(location=instance.location)
|
||||
else:
|
||||
peer_racks = peer_racks.filter(group__isnull=True)
|
||||
peer_racks = peer_racks.filter(location__isnull=True)
|
||||
next_rack = peer_racks.filter(name__gt=instance.name).order_by('name').first()
|
||||
prev_rack = peer_racks.filter(name__lt=instance.name).order_by('-name').first()
|
||||
|
||||
@@ -390,14 +482,14 @@ class RackBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class RackBulkEditView(generic.BulkEditView):
|
||||
queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role')
|
||||
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role')
|
||||
filterset = filters.RackFilterSet
|
||||
table = tables.RackTable
|
||||
form = forms.RackBulkEditForm
|
||||
|
||||
|
||||
class RackBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role')
|
||||
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role')
|
||||
filterset = filters.RackFilterSet
|
||||
table = tables.RackTable
|
||||
|
||||
@@ -490,6 +582,15 @@ class ManufacturerBulkImportView(generic.BulkImportView):
|
||||
table = tables.ManufacturerTable
|
||||
|
||||
|
||||
class ManufacturerBulkEditView(generic.BulkEditView):
|
||||
queryset = Manufacturer.objects.annotate(
|
||||
devicetype_count=count_related(DeviceType, 'manufacturer')
|
||||
)
|
||||
filterset = filters.ManufacturerFilterSet
|
||||
table = tables.ManufacturerTable
|
||||
form = forms.ManufacturerBulkEditForm
|
||||
|
||||
|
||||
class ManufacturerBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Manufacturer.objects.annotate(
|
||||
devicetype_count=count_related(DeviceType, 'manufacturer')
|
||||
@@ -931,6 +1032,13 @@ class DeviceRoleBulkImportView(generic.BulkImportView):
|
||||
table = tables.DeviceRoleTable
|
||||
|
||||
|
||||
class DeviceRoleBulkEditView(generic.BulkEditView):
|
||||
queryset = DeviceRole.objects.all()
|
||||
filterset = filters.DeviceRoleFilterSet
|
||||
table = tables.DeviceRoleTable
|
||||
form = forms.DeviceRoleBulkEditForm
|
||||
|
||||
|
||||
class DeviceRoleBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = DeviceRole.objects.all()
|
||||
table = tables.DeviceRoleTable
|
||||
@@ -963,6 +1071,13 @@ class PlatformBulkImportView(generic.BulkImportView):
|
||||
table = tables.PlatformTable
|
||||
|
||||
|
||||
class PlatformBulkEditView(generic.BulkEditView):
|
||||
queryset = Platform.objects.all()
|
||||
filterset = filters.PlatformFilterSet
|
||||
table = tables.PlatformTable
|
||||
form = forms.PlatformBulkEditForm
|
||||
|
||||
|
||||
class PlatformBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Platform.objects.all()
|
||||
table = tables.PlatformTable
|
||||
@@ -982,7 +1097,7 @@ class DeviceListView(generic.ObjectListView):
|
||||
|
||||
class DeviceView(generic.ObjectView):
|
||||
queryset = Device.objects.prefetch_related(
|
||||
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform', 'primary_ip4', 'primary_ip6'
|
||||
'site__region', 'location', 'rack', 'tenant__group', 'device_role', 'platform', 'primary_ip4', 'primary_ip6'
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
@@ -1268,6 +1383,10 @@ class DeviceChangeLogView(ObjectChangeLogView):
|
||||
base_template = 'dcim/device/base.html'
|
||||
|
||||
|
||||
class DeviceJournalView(ObjectJournalView):
|
||||
base_template = 'dcim/device/base.html'
|
||||
|
||||
|
||||
class DeviceEditView(generic.ObjectEditView):
|
||||
queryset = Device.objects.all()
|
||||
model_form = forms.DeviceForm
|
||||
@@ -2178,13 +2297,15 @@ class CableCreateView(generic.ObjectEditView):
|
||||
initial_data = {k: request.GET[k] for k in request.GET}
|
||||
|
||||
# Set initial site and rack based on side A termination (if not already set)
|
||||
termination_a_site = getattr(obj.termination_a.parent, 'site', None)
|
||||
termination_a_site = getattr(obj.termination_a.parent_object, 'site', None)
|
||||
if termination_a_site and 'termination_b_region' not in initial_data:
|
||||
initial_data['termination_b_region'] = termination_a_site.region
|
||||
if termination_a_site and 'termination_b_site_group' not in initial_data:
|
||||
initial_data['termination_b_site_group'] = termination_a_site.group
|
||||
if 'termination_b_site' not in initial_data:
|
||||
initial_data['termination_b_site'] = termination_a_site
|
||||
if 'termination_b_rack' not in initial_data:
|
||||
initial_data['termination_b_rack'] = getattr(obj.termination_a.parent, 'rack', None)
|
||||
initial_data['termination_b_rack'] = getattr(obj.termination_a.parent_object, 'rack', None)
|
||||
|
||||
form = self.model_form(instance=obj, initial=initial_data)
|
||||
|
||||
@@ -2560,7 +2681,7 @@ class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class PowerPanelListView(generic.ObjectListView):
|
||||
queryset = PowerPanel.objects.prefetch_related(
|
||||
'site', 'rack_group'
|
||||
'site', 'location'
|
||||
).annotate(
|
||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||
)
|
||||
@@ -2570,7 +2691,7 @@ class PowerPanelListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class PowerPanelView(generic.ObjectView):
|
||||
queryset = PowerPanel.objects.prefetch_related('site', 'rack_group')
|
||||
queryset = PowerPanel.objects.prefetch_related('site', 'location')
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
power_feeds = PowerFeed.objects.restrict(request.user).filter(power_panel=instance).prefetch_related('rack')
|
||||
@@ -2601,7 +2722,7 @@ class PowerPanelBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class PowerPanelBulkEditView(generic.BulkEditView):
|
||||
queryset = PowerPanel.objects.prefetch_related('site', 'rack_group')
|
||||
queryset = PowerPanel.objects.prefetch_related('site', 'location')
|
||||
filterset = filters.PowerPanelFilterSet
|
||||
table = tables.PowerPanelTable
|
||||
form = forms.PowerPanelBulkEditForm
|
||||
@@ -2609,7 +2730,7 @@ class PowerPanelBulkEditView(generic.BulkEditView):
|
||||
|
||||
class PowerPanelBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = PowerPanel.objects.prefetch_related(
|
||||
'site', 'rack_group'
|
||||
'site', 'location'
|
||||
).annotate(
|
||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||
)
|
||||
|
||||
@@ -132,15 +132,15 @@ class CustomLinkForm(forms.ModelForm):
|
||||
model = CustomLink
|
||||
exclude = []
|
||||
widgets = {
|
||||
'text': forms.Textarea,
|
||||
'url': forms.Textarea,
|
||||
'link_text': forms.Textarea,
|
||||
'link_url': forms.Textarea,
|
||||
}
|
||||
help_texts = {
|
||||
'weight': 'A numeric weight to influence the ordering of this link among its peers. Lower weights appear '
|
||||
'first in a list.',
|
||||
'text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>. Links '
|
||||
'which render as empty text will not be displayed.',
|
||||
'url': 'Jinja2 template code for the link URL. Reference the object as <code>{{ obj }}</code>.',
|
||||
'link_text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>. '
|
||||
'Links which render as empty text will not be displayed.',
|
||||
'link_url': 'Jinja2 template code for the link URL. Reference the object as <code>{{ obj }}</code>.',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -158,7 +158,7 @@ class CustomLinkAdmin(admin.ModelAdmin):
|
||||
'fields': ('content_type', 'name', 'group_name', 'weight', 'button_class', 'new_window')
|
||||
}),
|
||||
('Templates', {
|
||||
'fields': ('text', 'url'),
|
||||
'fields': ('link_text', 'link_url'),
|
||||
'classes': ('monospace',)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -2,24 +2,44 @@ from rest_framework import serializers
|
||||
|
||||
from extras import choices, models
|
||||
from netbox.api import ChoiceField, WritableNestedSerializer
|
||||
from netbox.api.serializers import NestedTagSerializer
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedConfigContextSerializer',
|
||||
'NestedCustomFieldSerializer',
|
||||
'NestedCustomLinkSerializer',
|
||||
'NestedExportTemplateSerializer',
|
||||
'NestedImageAttachmentSerializer',
|
||||
'NestedJobResultSerializer',
|
||||
'NestedTagSerializer',
|
||||
'NestedJournalEntrySerializer',
|
||||
'NestedTagSerializer', # Defined in netbox.api.serializers
|
||||
'NestedWebhookSerializer',
|
||||
]
|
||||
|
||||
|
||||
class NestedWebhookSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Webhook
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedCustomFieldSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.CustomField
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedCustomLinkSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.CustomLink
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedConfigContextSerializer(WritableNestedSerializer):
|
||||
@@ -27,7 +47,7 @@ class NestedConfigContextSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.ConfigContext
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedExportTemplateSerializer(WritableNestedSerializer):
|
||||
@@ -35,7 +55,7 @@ class NestedExportTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.ExportTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedImageAttachmentSerializer(WritableNestedSerializer):
|
||||
@@ -43,15 +63,15 @@ class NestedImageAttachmentSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.ImageAttachment
|
||||
fields = ['id', 'url', 'name', 'image']
|
||||
fields = ['id', 'url', 'display', 'name', 'image']
|
||||
|
||||
|
||||
class NestedTagSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
|
||||
class NestedJournalEntrySerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Tag
|
||||
fields = ['id', 'url', 'name', 'slug', 'color']
|
||||
model = models.JournalEntry
|
||||
fields = ['id', 'url', 'display', 'created']
|
||||
|
||||
|
||||
class NestedJobResultSerializer(serializers.ModelSerializer):
|
||||
|
||||
@@ -5,16 +5,15 @@ from rest_framework import serializers
|
||||
|
||||
from dcim.api.nested_serializers import (
|
||||
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
|
||||
NestedRegionSerializer, NestedSiteSerializer,
|
||||
NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
|
||||
)
|
||||
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
|
||||
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
|
||||
from extras.choices import *
|
||||
from extras.models import (
|
||||
ConfigContext, CustomField, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag,
|
||||
)
|
||||
from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer
|
||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||
from netbox.api.exceptions import SerializerNotFound
|
||||
from netbox.api.serializers import BaseModelSerializer, ValidatedModelSerializer
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
@@ -23,6 +22,46 @@ from virtualization.api.nested_serializers import NestedClusterGroupSerializer,
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .nested_serializers import *
|
||||
|
||||
__all__ = (
|
||||
'ConfigContextSerializer',
|
||||
'ContentTypeSerializer',
|
||||
'CustomFieldSerializer',
|
||||
'CustomLinkSerializer',
|
||||
'ExportTemplateSerializer',
|
||||
'ImageAttachmentSerializer',
|
||||
'JobResultSerializer',
|
||||
'ObjectChangeSerializer',
|
||||
'ReportDetailSerializer',
|
||||
'ReportSerializer',
|
||||
'ScriptDetailSerializer',
|
||||
'ScriptInputSerializer',
|
||||
'ScriptLogMessageSerializer',
|
||||
'ScriptOutputSerializer',
|
||||
'ScriptSerializer',
|
||||
'TagSerializer',
|
||||
'WebhookSerializer',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Webhooks
|
||||
#
|
||||
|
||||
class WebhookSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
|
||||
content_types = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()),
|
||||
many=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
fields = [
|
||||
'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url',
|
||||
'enabled', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
|
||||
'ssl_verification', 'ca_file_path',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Custom fields
|
||||
@@ -40,11 +79,29 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = [
|
||||
'id', 'url', 'content_types', 'type', 'name', 'label', 'description', 'required', 'filter_logic',
|
||||
'id', 'url', 'display', 'content_types', 'type', 'name', 'label', 'description', 'required', 'filter_logic',
|
||||
'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Custom links
|
||||
#
|
||||
|
||||
class CustomLinkSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
||||
content_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query())
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
fields = [
|
||||
'id', 'url', 'display', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name',
|
||||
'button_class', 'new_window',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Export templates
|
||||
#
|
||||
@@ -57,7 +114,10 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
fields = ['id', 'url', 'content_type', 'name', 'description', 'template_code', 'mime_type', 'file_extension']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'content_type', 'name', 'description', 'template_code', 'mime_type',
|
||||
'file_extension',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
@@ -70,39 +130,7 @@ class TagSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ['id', 'url', 'name', 'slug', 'color', 'description', 'tagged_items']
|
||||
|
||||
|
||||
class TaggedObjectSerializer(serializers.Serializer):
|
||||
tags = NestedTagSerializer(many=True, required=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
tags = validated_data.pop('tags', None)
|
||||
instance = super().create(validated_data)
|
||||
|
||||
if tags is not None:
|
||||
return self._save_tags(instance, tags)
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
tags = validated_data.pop('tags', None)
|
||||
|
||||
# Cache tags on instance for change logging
|
||||
instance._tags = tags or []
|
||||
|
||||
instance = super().update(instance, validated_data)
|
||||
|
||||
if tags is not None:
|
||||
return self._save_tags(instance, tags)
|
||||
return instance
|
||||
|
||||
def _save_tags(self, instance, tags):
|
||||
if tags:
|
||||
instance.tags.set(*[t.name for t in tags])
|
||||
else:
|
||||
instance.tags.clear()
|
||||
|
||||
return instance
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tagged_items']
|
||||
|
||||
|
||||
#
|
||||
@@ -119,8 +147,8 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
fields = [
|
||||
'id', 'url', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height', 'image_width',
|
||||
'created',
|
||||
'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height',
|
||||
'image_width', 'created',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
@@ -154,6 +182,51 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
return serializer(obj.parent, context={'request': self.context['request']}).data
|
||||
|
||||
|
||||
#
|
||||
# Journal entries
|
||||
#
|
||||
|
||||
class JournalEntrySerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
|
||||
assigned_object_type = ContentTypeField(
|
||||
queryset=ContentType.objects.all()
|
||||
)
|
||||
assigned_object = serializers.SerializerMethodField(read_only=True)
|
||||
kind = ChoiceField(
|
||||
choices=JournalEntryKindChoices,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = JournalEntry
|
||||
fields = [
|
||||
'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
|
||||
'created_by', 'kind', 'comments',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
# Validate that the parent object exists
|
||||
if 'assigned_object_type' in data and 'assigned_object_id' in data:
|
||||
try:
|
||||
data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id'])
|
||||
except ObjectDoesNotExist:
|
||||
raise serializers.ValidationError(
|
||||
f"Invalid assigned_object: {data['assigned_object_type']} ID {data['assigned_object_id']}"
|
||||
)
|
||||
|
||||
# Enforce model validation
|
||||
super().validate(data)
|
||||
|
||||
return data
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
def get_assigned_object(self, instance):
|
||||
serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix='Nested')
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(instance.assigned_object, context=context).data
|
||||
|
||||
|
||||
#
|
||||
# Config contexts
|
||||
#
|
||||
@@ -166,6 +239,12 @@ class ConfigContextSerializer(ValidatedModelSerializer):
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
site_groups = SerializedPKRelatedField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
serializer=NestedSiteGroupSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
sites = SerializedPKRelatedField(
|
||||
queryset=Site.objects.all(),
|
||||
serializer=NestedSiteSerializer,
|
||||
@@ -218,8 +297,9 @@ class ConfigContextSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = [
|
||||
'id', 'url', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms',
|
||||
'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
|
||||
'roles', 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', 'created',
|
||||
'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@@ -227,7 +307,7 @@ class ConfigContextSerializer(ValidatedModelSerializer):
|
||||
# Job Results
|
||||
#
|
||||
|
||||
class JobResultSerializer(serializers.ModelSerializer):
|
||||
class JobResultSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:jobresult-detail')
|
||||
user = NestedUserSerializer(
|
||||
read_only=True
|
||||
@@ -240,7 +320,7 @@ class JobResultSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = JobResult
|
||||
fields = [
|
||||
'id', 'url', 'created', 'completed', 'name', 'obj_type', 'status', 'user', 'data', 'job_id',
|
||||
'id', 'url', 'display', 'created', 'completed', 'name', 'obj_type', 'status', 'user', 'data', 'job_id',
|
||||
]
|
||||
|
||||
|
||||
@@ -318,7 +398,7 @@ class ScriptOutputSerializer(serializers.Serializer):
|
||||
# Change logging
|
||||
#
|
||||
|
||||
class ObjectChangeSerializer(serializers.ModelSerializer):
|
||||
class ObjectChangeSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail')
|
||||
user = NestedUserSerializer(
|
||||
read_only=True
|
||||
@@ -337,8 +417,8 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
fields = [
|
||||
'id', 'url', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type',
|
||||
'changed_object_id', 'changed_object', 'object_data',
|
||||
'id', 'url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type',
|
||||
'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@@ -365,13 +445,13 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
|
||||
# ContentTypes
|
||||
#
|
||||
|
||||
class ContentTypeSerializer(serializers.ModelSerializer):
|
||||
class ContentTypeSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail')
|
||||
display_name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ContentType
|
||||
fields = ['id', 'url', 'app_label', 'model', 'display_name']
|
||||
fields = ['id', 'url', 'display', 'app_label', 'model', 'display_name']
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.CharField)
|
||||
def get_display_name(self, obj):
|
||||
|
||||
@@ -5,9 +5,15 @@ from . import views
|
||||
router = OrderedDefaultRouter()
|
||||
router.APIRootView = views.ExtrasRootView
|
||||
|
||||
# Webhooks
|
||||
router.register('webhooks', views.WebhookViewSet)
|
||||
|
||||
# Custom fields
|
||||
router.register('custom-fields', views.CustomFieldViewSet)
|
||||
|
||||
# Custom links
|
||||
router.register('custom-links', views.CustomLinkViewSet)
|
||||
|
||||
# Export templates
|
||||
router.register('export-templates', views.ExportTemplateViewSet)
|
||||
|
||||
@@ -17,6 +23,9 @@ router.register('tags', views.TagViewSet)
|
||||
# Image attachments
|
||||
router.register('image-attachments', views.ImageAttachmentViewSet)
|
||||
|
||||
# Journal entries
|
||||
router.register('journal-entries', views.JournalEntryViewSet)
|
||||
|
||||
# Config contexts
|
||||
router.register('config-contexts', views.ConfigContextViewSet)
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@ from rq import Worker
|
||||
|
||||
from extras import filters
|
||||
from extras.choices import JobResultStatusChoices
|
||||
from extras.models import (
|
||||
ConfigContext, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag, TaggedItem,
|
||||
)
|
||||
from extras.models import *
|
||||
from extras.models import CustomField
|
||||
from extras.reports import get_report, get_reports, run_report
|
||||
from extras.scripts import get_script, get_scripts, run_script
|
||||
@@ -55,6 +53,17 @@ class ConfigContextQuerySetMixin:
|
||||
return queryset.annotate_config_context_data()
|
||||
|
||||
|
||||
#
|
||||
# Webhooks
|
||||
#
|
||||
|
||||
class WebhookViewSet(ModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = Webhook.objects.all()
|
||||
serializer_class = serializers.WebhookSerializer
|
||||
filterset_class = filters.WebhookFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Custom fields
|
||||
#
|
||||
@@ -84,6 +93,17 @@ class CustomFieldModelViewSet(ModelViewSet):
|
||||
return context
|
||||
|
||||
|
||||
#
|
||||
# Custom links
|
||||
#
|
||||
|
||||
class CustomLinkViewSet(ModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = CustomLink.objects.all()
|
||||
serializer_class = serializers.CustomLinkSerializer
|
||||
filterset_class = filters.CustomLinkFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Export templates
|
||||
#
|
||||
@@ -118,13 +138,24 @@ class ImageAttachmentViewSet(ModelViewSet):
|
||||
filterset_class = filters.ImageAttachmentFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Journal entries
|
||||
#
|
||||
|
||||
class JournalEntryViewSet(ModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = JournalEntry.objects.all()
|
||||
serializer_class = serializers.JournalEntrySerializer
|
||||
filterset_class = filters.JournalEntryFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Config contexts
|
||||
#
|
||||
|
||||
class ConfigContextViewSet(ModelViewSet):
|
||||
queryset = ConfigContext.objects.prefetch_related(
|
||||
'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
|
||||
'regions', 'site_groups', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
|
||||
)
|
||||
serializer_class = serializers.ConfigContextSerializer
|
||||
filterset_class = filters.ConfigContextFilterSet
|
||||
|
||||
@@ -87,6 +87,32 @@ class ObjectChangeActionChoices(ChoiceSet):
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Jounral entries
|
||||
#
|
||||
|
||||
class JournalEntryKindChoices(ChoiceSet):
|
||||
|
||||
KIND_INFO = 'info'
|
||||
KIND_SUCCESS = 'success'
|
||||
KIND_WARNING = 'warning'
|
||||
KIND_DANGER = 'danger'
|
||||
|
||||
CHOICES = (
|
||||
(KIND_INFO, 'Info'),
|
||||
(KIND_SUCCESS, 'Success'),
|
||||
(KIND_WARNING, 'Warning'),
|
||||
(KIND_DANGER, 'Danger'),
|
||||
)
|
||||
|
||||
CSS_CLASSES = {
|
||||
KIND_INFO: 'default',
|
||||
KIND_SUCCESS: 'success',
|
||||
KIND_WARNING: 'warning',
|
||||
KIND_DANGER: 'danger',
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Log Levels for Reports and Scripts
|
||||
#
|
||||
|
||||
@@ -4,12 +4,12 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.forms import DateField, IntegerField, NullBooleanField
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.filters import BaseFilterSet, ContentTypeFilter
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .choices import *
|
||||
from .models import ConfigContext, CustomField, ExportTemplate, ImageAttachment, JobResult, ObjectChange, Tag
|
||||
from .models import *
|
||||
|
||||
|
||||
__all__ = (
|
||||
@@ -17,12 +17,15 @@ __all__ = (
|
||||
'ContentTypeFilterSet',
|
||||
'CreatedUpdatedFilterSet',
|
||||
'CustomFieldFilter',
|
||||
'CustomLinkFilterSet',
|
||||
'CustomFieldModelFilterSet',
|
||||
'ExportTemplateFilterSet',
|
||||
'ImageAttachmentFilterSet',
|
||||
'JournalEntryFilterSet',
|
||||
'LocalConfigContextFilterSet',
|
||||
'ObjectChangeFilterSet',
|
||||
'TagFilterSet',
|
||||
'WebhookFilterSet',
|
||||
)
|
||||
|
||||
EXACT_FILTER_TYPES = (
|
||||
@@ -33,6 +36,20 @@ EXACT_FILTER_TYPES = (
|
||||
)
|
||||
|
||||
|
||||
class WebhookFilterSet(BaseFilterSet):
|
||||
content_types = ContentTypeFilter()
|
||||
http_method = django_filters.MultipleChoiceFilter(
|
||||
choices=WebhookHttpMethodChoices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
fields = [
|
||||
'id', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled',
|
||||
'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
|
||||
]
|
||||
|
||||
|
||||
class CustomFieldFilter(django_filters.Filter):
|
||||
"""
|
||||
Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name.
|
||||
@@ -79,6 +96,13 @@ class CustomFieldFilterSet(django_filters.FilterSet):
|
||||
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight']
|
||||
|
||||
|
||||
class CustomLinkFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
fields = ['id', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name', 'new_window']
|
||||
|
||||
|
||||
class ExportTemplateFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -94,6 +118,37 @@ class ImageAttachmentFilterSet(BaseFilterSet):
|
||||
fields = ['id', 'content_type_id', 'object_id', 'name']
|
||||
|
||||
|
||||
class JournalEntryFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
created = django_filters.DateTimeFromToRangeFilter()
|
||||
assigned_object_type = ContentTypeFilter()
|
||||
created_by_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
label='User (ID)',
|
||||
)
|
||||
created_by = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='created_by__username',
|
||||
queryset=User.objects.all(),
|
||||
to_field_name='username',
|
||||
label='User (name)',
|
||||
)
|
||||
kind = django_filters.MultipleChoiceFilter(
|
||||
choices=JournalEntryKindChoices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = JournalEntry
|
||||
fields = ['id', 'assigned_object_type_id', 'assigned_object_id', 'created', 'kind']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(comments__icontains=value)
|
||||
|
||||
|
||||
class TagFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -129,6 +184,17 @@ class ConfigContextFilterSet(BaseFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site_groups__slug',
|
||||
queryset=SiteGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site_groups',
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='sites',
|
||||
queryset=Site.objects.all(),
|
||||
|
||||
@@ -4,16 +4,16 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
|
||||
CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
|
||||
CommentField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
|
||||
BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .choices import *
|
||||
from .models import ConfigContext, CustomField, ImageAttachment, ObjectChange, Tag
|
||||
from .models import ConfigContext, CustomField, ImageAttachment, JournalEntry, ObjectChange, Tag
|
||||
|
||||
|
||||
#
|
||||
@@ -210,6 +210,10 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
queryset=Region.objects.all(),
|
||||
required=False
|
||||
)
|
||||
site_groups = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
sites = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False
|
||||
@@ -249,8 +253,8 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = (
|
||||
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'cluster_groups',
|
||||
'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
|
||||
'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'platforms',
|
||||
'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
|
||||
)
|
||||
|
||||
|
||||
@@ -280,8 +284,8 @@ class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
|
||||
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
field_order = [
|
||||
'q', 'region_id', 'site_id', 'role_id', 'platform_id', 'cluster_group_id', 'cluster_id', 'tenant_group_id',
|
||||
'tenant_id',
|
||||
'q', 'region_id', 'site_group_id', 'site_id', 'role_id', 'platform_id', 'cluster_group_id', 'cluster_id',
|
||||
'tenant_group_id', 'tenant_id',
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
@@ -292,6 +296,11 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
required=False,
|
||||
label=_('Regions')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site groups')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
@@ -362,6 +371,78 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Journal entries
|
||||
#
|
||||
|
||||
class JournalEntryForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = JournalEntry
|
||||
fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'comments']
|
||||
widgets = {
|
||||
'assigned_object_type': forms.HiddenInput,
|
||||
'assigned_object_id': forms.HiddenInput,
|
||||
}
|
||||
|
||||
|
||||
class JournalEntryBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=JournalEntry.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
kind = forms.ChoiceField(
|
||||
choices=JournalEntryKindChoices,
|
||||
required=False
|
||||
)
|
||||
comments = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.Textarea()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class JournalEntryFilterForm(BootstrapMixin, forms.Form):
|
||||
model = JournalEntry
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
created_after = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('After'),
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
created_before = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('Before'),
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
created_by_id = DynamicModelMultipleChoiceField(
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
label=_('User'),
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/users/users/',
|
||||
)
|
||||
)
|
||||
assigned_object_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
required=False,
|
||||
label=_('Object Type'),
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/extras/content-types/',
|
||||
)
|
||||
)
|
||||
kind = forms.ChoiceField(
|
||||
choices=add_blank_choice(JournalEntryKindChoices),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
@@ -390,7 +471,6 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
user_id = DynamicModelMultipleChoiceField(
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
display_field='username',
|
||||
label=_('User'),
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/users/users/',
|
||||
@@ -399,7 +479,6 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
changed_object_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
required=False,
|
||||
display_field='display_name',
|
||||
label=_('Object Type'),
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/extras/content-types/',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from cacheops import invalidate_model
|
||||
from django.apps import apps
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
@@ -27,7 +28,7 @@ class Command(BaseCommand):
|
||||
app_label, model_name = name.split('.')
|
||||
except ValueError:
|
||||
raise CommandError(
|
||||
"Invalid format: {}. Models must be specified in the form app_label.ModelName.".format(name)
|
||||
f"Invalid format: {name}. Models must be specified in the form app_label.ModelName."
|
||||
)
|
||||
try:
|
||||
app_config = apps.get_app_config(app_label)
|
||||
@@ -36,13 +37,13 @@ class Command(BaseCommand):
|
||||
try:
|
||||
model = app_config.get_model(model_name)
|
||||
except LookupError:
|
||||
raise CommandError("Unknown model: {}.{}".format(app_label, model_name))
|
||||
raise CommandError(f"Unknown model: {app_label}.{model_name}")
|
||||
fields = [
|
||||
field for field in model._meta.concrete_fields if type(field) is NaturalOrderingField
|
||||
]
|
||||
if not fields:
|
||||
raise CommandError(
|
||||
"Invalid model: {}.{} does not employ natural ordering".format(app_label, model_name)
|
||||
f"Invalid model: {app_label}.{model_name} does not employ natural ordering"
|
||||
)
|
||||
models.append(
|
||||
(model, fields)
|
||||
@@ -67,7 +68,7 @@ class Command(BaseCommand):
|
||||
models = self._get_models(args)
|
||||
|
||||
if options['verbosity']:
|
||||
self.stdout.write("Renaturalizing {} models.".format(len(models)))
|
||||
self.stdout.write(f"Renaturalizing {len(models)} models.")
|
||||
|
||||
for model, fields in models:
|
||||
for field in fields:
|
||||
@@ -78,7 +79,7 @@ class Command(BaseCommand):
|
||||
# Print the model and field name
|
||||
if options['verbosity']:
|
||||
self.stdout.write(
|
||||
"{}.{} ({})... ".format(model._meta.label, field.target_field, field.name),
|
||||
f"{model._meta.label}.{field.target_field} ({field.name})... ",
|
||||
ending='\n' if options['verbosity'] >= 2 else ''
|
||||
)
|
||||
self.stdout.flush()
|
||||
@@ -89,23 +90,26 @@ class Command(BaseCommand):
|
||||
naturalized_value = naturalize(value, max_length=field.max_length)
|
||||
|
||||
if options['verbosity'] >= 2:
|
||||
self.stdout.write(" {} -> {}".format(value, naturalized_value), ending='')
|
||||
self.stdout.write(f" {value} -> {naturalized_value}", ending='')
|
||||
self.stdout.flush()
|
||||
|
||||
# Update each unique field value in bulk
|
||||
changed = model.objects.filter(name=value).update(**{field.name: naturalized_value})
|
||||
|
||||
if options['verbosity'] >= 2:
|
||||
self.stdout.write(" ({})".format(changed))
|
||||
self.stdout.write(f" ({changed})")
|
||||
count += changed
|
||||
|
||||
# Print the total count of alterations for the field
|
||||
if options['verbosity'] >= 2:
|
||||
self.stdout.write(self.style.SUCCESS("{} {} updated ({} unique values)".format(
|
||||
count, model._meta.verbose_name_plural, queryset.count()
|
||||
)))
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f"{count} {model._meta.verbose_name_plural} updated ({queryset.count()} unique values)"
|
||||
))
|
||||
elif options['verbosity']:
|
||||
self.stdout.write(self.style.SUCCESS(str(count)))
|
||||
|
||||
# Invalidate cached queries
|
||||
invalidate_model(model)
|
||||
|
||||
if options['verbosity']:
|
||||
self.stdout.write(self.style.SUCCESS("Done."))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import sys
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
@@ -47,8 +48,10 @@ class WebhookHandler(BaseHTTPRequestHandler):
|
||||
# Print the request body (if any)
|
||||
content_length = self.headers.get('Content-Length')
|
||||
if content_length is not None:
|
||||
body = self.rfile.read(int(content_length))
|
||||
print(body.decode('utf-8'))
|
||||
body = self.rfile.read(int(content_length)).decode('utf-8')
|
||||
if self.headers.get('Content-Type') == 'application/json':
|
||||
body = json.loads(body)
|
||||
print(json.dumps(body, indent=4))
|
||||
else:
|
||||
print('(No body)')
|
||||
|
||||
|
||||
28
netbox/extras/migrations/0055_objectchange_data.py
Normal file
28
netbox/extras/migrations/0055_objectchange_data.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.2b1 on 2021-03-03 20:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0054_standardize_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='objectchange',
|
||||
old_name='object_data',
|
||||
new_name='postchange_data',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objectchange',
|
||||
name='postchange_data',
|
||||
field=models.JSONField(blank=True, editable=False, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='objectchange',
|
||||
name='prechange_data',
|
||||
field=models.JSONField(blank=True, editable=False, null=True),
|
||||
),
|
||||
]
|
||||
17
netbox/extras/migrations/0056_sitegroup.py
Normal file
17
netbox/extras/migrations/0056_sitegroup.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0130_sitegroup'),
|
||||
('extras', '0055_objectchange_data'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='configcontext',
|
||||
name='site_groups',
|
||||
field=models.ManyToManyField(blank=True, related_name='_extras_configcontext_site_groups_+', to='dcim.SiteGroup'),
|
||||
),
|
||||
]
|
||||
28
netbox/extras/migrations/0057_customlink_rename_fields.py
Normal file
28
netbox/extras/migrations/0057_customlink_rename_fields.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.2b1 on 2021-03-09 01:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0056_sitegroup'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='customlink',
|
||||
old_name='text',
|
||||
new_name='link_text',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='customlink',
|
||||
old_name='url',
|
||||
new_name='link_url',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customlink',
|
||||
name='new_window',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
31
netbox/extras/migrations/0058_journalentry.py
Normal file
31
netbox/extras/migrations/0058_journalentry.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('extras', '0057_customlink_rename_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='JournalEntry',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('assigned_object_id', models.PositiveIntegerField()),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('kind', models.CharField(default='info', max_length=30)),
|
||||
('comments', models.TextField()),
|
||||
('assigned_object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'journal entries',
|
||||
'ordering': ('-created',),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,9 +1,7 @@
|
||||
from .change_logging import ObjectChange
|
||||
from .configcontexts import ConfigContext, ConfigContextModel
|
||||
from .customfields import CustomField
|
||||
from .models import (
|
||||
ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, ImageAttachment, JobResult, Report, Script,
|
||||
Webhook,
|
||||
)
|
||||
from .models import CustomLink, ExportTemplate, ImageAttachment, JobResult, JournalEntry, Report, Script, Webhook
|
||||
from .tags import Tag, TaggedItem
|
||||
|
||||
__all__ = (
|
||||
@@ -14,6 +12,7 @@ __all__ = (
|
||||
'ExportTemplate',
|
||||
'ImageAttachment',
|
||||
'JobResult',
|
||||
'JournalEntry',
|
||||
'ObjectChange',
|
||||
'Report',
|
||||
'Script',
|
||||
|
||||
@@ -67,15 +67,22 @@ class ObjectChange(BigIDModel):
|
||||
max_length=200,
|
||||
editable=False
|
||||
)
|
||||
object_data = models.JSONField(
|
||||
editable=False
|
||||
prechange_data = models.JSONField(
|
||||
editable=False,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
postchange_data = models.JSONField(
|
||||
editable=False,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id',
|
||||
'related_object_type', 'related_object_id', 'object_repr', 'object_data',
|
||||
'related_object_type', 'related_object_id', 'object_repr', 'prechange_data', 'postchange_data',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@@ -114,7 +121,8 @@ class ObjectChange(BigIDModel):
|
||||
self.related_object_type,
|
||||
self.related_object_id,
|
||||
self.object_repr,
|
||||
self.object_data,
|
||||
self.prechange_data,
|
||||
self.postchange_data,
|
||||
)
|
||||
|
||||
def get_action_class(self):
|
||||
|
||||
161
netbox/extras/models/configcontexts.py
Normal file
161
netbox/extras/models/configcontexts.py
Normal file
@@ -0,0 +1,161 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from extras.querysets import ConfigContextQuerySet
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from utilities.utils import deepmerge
|
||||
|
||||
|
||||
__all__ = (
|
||||
'ConfigContext',
|
||||
'ConfigContextModel',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Config contexts
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
class ConfigContext(ChangeLoggedModel):
|
||||
"""
|
||||
A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
|
||||
qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
|
||||
will be available to a Device in site A assigned to tenant B. Data is stored in JSON format.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
default=1000
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
)
|
||||
regions = models.ManyToManyField(
|
||||
to='dcim.Region',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
site_groups = models.ManyToManyField(
|
||||
to='dcim.SiteGroup',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
sites = models.ManyToManyField(
|
||||
to='dcim.Site',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
roles = models.ManyToManyField(
|
||||
to='dcim.DeviceRole',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
platforms = models.ManyToManyField(
|
||||
to='dcim.Platform',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
cluster_groups = models.ManyToManyField(
|
||||
to='virtualization.ClusterGroup',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
clusters = models.ManyToManyField(
|
||||
to='virtualization.Cluster',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
tenant_groups = models.ManyToManyField(
|
||||
to='tenancy.TenantGroup',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
tenants = models.ManyToManyField(
|
||||
to='tenancy.Tenant',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
tags = models.ManyToManyField(
|
||||
to='extras.Tag',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
data = models.JSONField()
|
||||
|
||||
objects = ConfigContextQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['weight', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:configcontext', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Verify that JSON data is provided as an object
|
||||
if type(self.data) is not dict:
|
||||
raise ValidationError(
|
||||
{'data': 'JSON data must be in object form. Example: {"foo": 123}'}
|
||||
)
|
||||
|
||||
|
||||
class ConfigContextModel(models.Model):
|
||||
"""
|
||||
A model which includes local configuration context data. This local data will override any inherited data from
|
||||
ConfigContexts.
|
||||
"""
|
||||
local_context_data = models.JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get_config_context(self):
|
||||
"""
|
||||
Return the rendered configuration context for a device or VM.
|
||||
"""
|
||||
|
||||
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
|
||||
data = OrderedDict()
|
||||
|
||||
if not hasattr(self, 'config_context_data'):
|
||||
# The annotation is not available, so we fall back to manually querying for the config context objects
|
||||
config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True)
|
||||
else:
|
||||
# The attribute may exist, but the annotated value could be None if there is no config context data
|
||||
config_context_data = self.config_context_data or []
|
||||
|
||||
for context in config_context_data:
|
||||
data = deepmerge(data, context)
|
||||
|
||||
# If the object has local config context data defined, merge it last
|
||||
if self.local_context_data:
|
||||
data = deepmerge(data, self.local_context_data)
|
||||
|
||||
return data
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Verify that JSON data is provided as an object
|
||||
if self.local_context_data and type(self.local_context_data) is not dict:
|
||||
raise ValidationError(
|
||||
{'local_context_data': 'JSON data must be in object form. Example: {"foo": 123}'}
|
||||
)
|
||||
@@ -1,6 +1,5 @@
|
||||
import json
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
@@ -8,17 +7,27 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
|
||||
from extras.choices import *
|
||||
from extras.constants import *
|
||||
from extras.querysets import ConfigContextQuerySet
|
||||
from extras.utils import extras_features, FeatureQuery, image_upload
|
||||
from netbox.models import BigIDModel, ChangeLoggingMixin
|
||||
from netbox.models import BigIDModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import deepmerge, render_jinja2
|
||||
from utilities.utils import render_jinja2
|
||||
|
||||
|
||||
__all__ = (
|
||||
'CustomLink',
|
||||
'ExportTemplate',
|
||||
'ImageAttachment',
|
||||
'JobResult',
|
||||
'JournalEntry',
|
||||
'Report',
|
||||
'Script',
|
||||
'Webhook',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -109,6 +118,8 @@ class Webhook(BigIDModel):
|
||||
'Leave blank to use the system defaults.'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
unique_together = ('payload_url', 'type_create', 'type_update', 'type_delete',)
|
||||
@@ -172,13 +183,13 @@ class CustomLink(BigIDModel):
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
text = models.CharField(
|
||||
link_text = models.CharField(
|
||||
max_length=500,
|
||||
help_text="Jinja2 template code for link text"
|
||||
)
|
||||
url = models.CharField(
|
||||
link_url = models.CharField(
|
||||
max_length=500,
|
||||
verbose_name='URL',
|
||||
verbose_name='Link URL',
|
||||
help_text="Jinja2 template code for link URL"
|
||||
)
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
@@ -196,9 +207,12 @@ class CustomLink(BigIDModel):
|
||||
help_text="The class of the first link in a group will be used for the dropdown button"
|
||||
)
|
||||
new_window = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Force link to open in a new window"
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['group_name', 'weight', 'name']
|
||||
|
||||
@@ -358,142 +372,51 @@ class ImageAttachment(BigIDModel):
|
||||
|
||||
|
||||
#
|
||||
# Config contexts
|
||||
# Journal entries
|
||||
#
|
||||
|
||||
class ConfigContext(ChangeLoggingMixin, BigIDModel):
|
||||
class JournalEntry(BigIDModel):
|
||||
"""
|
||||
A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
|
||||
qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
|
||||
will be available to a Device in site A assigned to tenant B. Data is stored in JSON format.
|
||||
A historical remark concerning an object; collectively, these form an object's journal. The journal is used to
|
||||
preserve historical context around an object, and complements NetBox's built-in change logging. For example, you
|
||||
might record a new journal entry when a device undergoes maintenance, or when a prefix is expanded.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
assigned_object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
default=1000
|
||||
assigned_object_id = models.PositiveIntegerField()
|
||||
assigned_object = GenericForeignKey(
|
||||
ct_field='assigned_object_type',
|
||||
fk_field='assigned_object_id'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
created = models.DateTimeField(
|
||||
auto_now_add=True
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
created_by = models.ForeignKey(
|
||||
to=User,
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
regions = models.ManyToManyField(
|
||||
to='dcim.Region',
|
||||
related_name='+',
|
||||
blank=True
|
||||
kind = models.CharField(
|
||||
max_length=30,
|
||||
choices=JournalEntryKindChoices,
|
||||
default=JournalEntryKindChoices.KIND_INFO
|
||||
)
|
||||
sites = models.ManyToManyField(
|
||||
to='dcim.Site',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
roles = models.ManyToManyField(
|
||||
to='dcim.DeviceRole',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
platforms = models.ManyToManyField(
|
||||
to='dcim.Platform',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
cluster_groups = models.ManyToManyField(
|
||||
to='virtualization.ClusterGroup',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
clusters = models.ManyToManyField(
|
||||
to='virtualization.Cluster',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
tenant_groups = models.ManyToManyField(
|
||||
to='tenancy.TenantGroup',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
tenants = models.ManyToManyField(
|
||||
to='tenancy.Tenant',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
tags = models.ManyToManyField(
|
||||
to='extras.Tag',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
data = models.JSONField()
|
||||
comments = models.TextField()
|
||||
|
||||
objects = ConfigContextQuerySet.as_manager()
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['weight', 'name']
|
||||
ordering = ('-created',)
|
||||
verbose_name_plural = 'journal entries'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return f"{self.created} - {self.get_kind_display()}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:configcontext', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Verify that JSON data is provided as an object
|
||||
if type(self.data) is not dict:
|
||||
raise ValidationError(
|
||||
{'data': 'JSON data must be in object form. Example: {"foo": 123}'}
|
||||
)
|
||||
|
||||
|
||||
class ConfigContextModel(models.Model):
|
||||
"""
|
||||
A model which includes local configuration context data. This local data will override any inherited data from
|
||||
ConfigContexts.
|
||||
"""
|
||||
local_context_data = models.JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get_config_context(self):
|
||||
"""
|
||||
Return the rendered configuration context for a device or VM.
|
||||
"""
|
||||
|
||||
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
|
||||
data = OrderedDict()
|
||||
|
||||
if not hasattr(self, 'config_context_data'):
|
||||
# The annotation is not available, so we fall back to manually querying for the config context objects
|
||||
config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True)
|
||||
else:
|
||||
# The attribute may exist, but the annotated value could be None if there is no config context data
|
||||
config_context_data = self.config_context_data or []
|
||||
|
||||
for context in config_context_data:
|
||||
data = deepmerge(data, context)
|
||||
|
||||
# If the object has local config context data defined, merge it last
|
||||
if self.local_context_data:
|
||||
data = deepmerge(data, self.local_context_data)
|
||||
|
||||
return data
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Verify that JSON data is provided as an object
|
||||
if self.local_context_data and type(self.local_context_data) is not dict:
|
||||
raise ValidationError(
|
||||
{'local_context_data': 'JSON data must be in object form. Example: {"foo": 123}'}
|
||||
)
|
||||
def get_kind_class(self):
|
||||
return JournalEntryKindChoices.CSS_CLASSES.get(self.kind)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -2,7 +2,8 @@ from django.db import models
|
||||
from django.utils.text import slugify
|
||||
from taggit.models import TagBase, GenericTaggedItemBase
|
||||
|
||||
from netbox.models import BigIDModel, ChangeLoggingMixin
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import BigIDModel, ChangeLoggedModel
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
@@ -12,7 +13,8 @@ from utilities.querysets import RestrictedQuerySet
|
||||
# Tags
|
||||
#
|
||||
|
||||
class Tag(ChangeLoggingMixin, BigIDModel, TagBase):
|
||||
@extras_features('webhooks')
|
||||
class Tag(ChangeLoggedModel, TagBase):
|
||||
color = ColorField(
|
||||
default=ColorChoices.COLOR_GREY
|
||||
)
|
||||
|
||||
@@ -180,28 +180,26 @@ class ObjectVar(ScriptVariable):
|
||||
A single object within NetBox.
|
||||
|
||||
:param model: The NetBox model being referenced
|
||||
:param display_field: The attribute of the returned object to display in the selection list (default: 'name')
|
||||
:param display_field: The attribute of the returned object to display in the selection list (DEPRECATED)
|
||||
:param query_params: A dictionary of additional query parameters to attach when making REST API requests (optional)
|
||||
:param null_option: The label to use as a "null" selection option (optional)
|
||||
"""
|
||||
form_field = DynamicModelChoiceField
|
||||
|
||||
def __init__(self, model=None, queryset=None, display_field='name', query_params=None, null_option=None, *args,
|
||||
**kwargs):
|
||||
def __init__(self, model, query_params=None, null_option=None, *args, **kwargs):
|
||||
|
||||
# TODO: Remove display_field in v2.12
|
||||
if 'display_field' in kwargs:
|
||||
warnings.warn(
|
||||
"The 'display_field' parameter has been deprecated, and will be removed in NetBox v2.12. Object "
|
||||
"variables will now reference the 'display' attribute available on all model serializers by default."
|
||||
)
|
||||
display_field = kwargs.pop('display_field', 'display')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Set the form field's queryset. Support backward compatibility for the "queryset" argument for now.
|
||||
if model is not None:
|
||||
self.field_attrs['queryset'] = model.objects.all()
|
||||
elif queryset is not None:
|
||||
warnings.warn(
|
||||
f'{self}: Specifying a queryset for ObjectVar is no longer supported. Please use "model" instead.'
|
||||
)
|
||||
self.field_attrs['queryset'] = queryset
|
||||
else:
|
||||
raise TypeError('ObjectVar must specify a model')
|
||||
|
||||
self.field_attrs.update({
|
||||
'queryset': model.objects.all(),
|
||||
'display_field': display_field,
|
||||
'query_params': query_params,
|
||||
'null_option': null_option,
|
||||
|
||||
@@ -36,6 +36,9 @@ def _handle_changed_object(request, sender, instance, **kwargs):
|
||||
# Record an ObjectChange if applicable
|
||||
if hasattr(instance, 'to_objectchange'):
|
||||
objectchange = instance.to_objectchange(action)
|
||||
# TODO: Move this to to_objectchange()
|
||||
if hasattr(instance, '_prechange_snapshot'):
|
||||
objectchange.prechange_data = instance._prechange_snapshot
|
||||
objectchange.user = request.user
|
||||
objectchange.request_id = request.id
|
||||
objectchange.save()
|
||||
@@ -62,6 +65,9 @@ def _handle_deleted_object(request, sender, instance, **kwargs):
|
||||
# Record an ObjectChange if applicable
|
||||
if hasattr(instance, 'to_objectchange'):
|
||||
objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)
|
||||
# TODO: Move this to to_objectchange()
|
||||
if hasattr(instance, '_prechange_snapshot'):
|
||||
objectchange.prechange_data = instance._prechange_snapshot
|
||||
objectchange.user = request.user
|
||||
objectchange.request_id = request.id
|
||||
objectchange.save()
|
||||
|
||||
@@ -2,7 +2,7 @@ import django_tables2 as tables
|
||||
from django.conf import settings
|
||||
|
||||
from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ToggleColumn
|
||||
from .models import ConfigContext, ObjectChange, Tag, TaggedItem
|
||||
from .models import ConfigContext, JournalEntry, ObjectChange, Tag, TaggedItem
|
||||
|
||||
TAGGED_ITEM = """
|
||||
{% if value.get_absolute_url %}
|
||||
@@ -96,3 +96,47 @@ class ObjectChangeTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ObjectChange
|
||||
fields = ('time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'request_id')
|
||||
|
||||
|
||||
class JournalEntryTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
created = tables.DateTimeColumn(
|
||||
format=settings.SHORT_DATETIME_FORMAT
|
||||
)
|
||||
assigned_object_type = tables.Column(
|
||||
verbose_name='Object type'
|
||||
)
|
||||
assigned_object = tables.Column(
|
||||
linkify=True,
|
||||
orderable=False,
|
||||
verbose_name='Object'
|
||||
)
|
||||
kind = ChoiceFieldColumn()
|
||||
actions = ButtonsColumn(
|
||||
model=JournalEntry,
|
||||
buttons=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = JournalEntry
|
||||
fields = (
|
||||
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments', 'actions'
|
||||
)
|
||||
|
||||
|
||||
class ObjectJournalTable(BaseTable):
|
||||
"""
|
||||
Used for displaying a set of JournalEntries within the context of a single object.
|
||||
"""
|
||||
created = tables.DateTimeColumn(
|
||||
format=settings.SHORT_DATETIME_FORMAT
|
||||
)
|
||||
kind = ChoiceFieldColumn()
|
||||
actions = ButtonsColumn(
|
||||
model=JournalEntry,
|
||||
buttons=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = JournalEntry
|
||||
fields = ('created', 'created_by', 'kind', 'comments', 'actions')
|
||||
|
||||
@@ -52,9 +52,9 @@ def custom_links(context, obj):
|
||||
# Add non-grouped links
|
||||
else:
|
||||
try:
|
||||
text_rendered = render_jinja2(cl.text, link_context)
|
||||
text_rendered = render_jinja2(cl.link_text, link_context)
|
||||
if text_rendered:
|
||||
link_rendered = render_jinja2(cl.url, link_context)
|
||||
link_rendered = render_jinja2(cl.link_url, link_context)
|
||||
link_target = ' target="_blank"' if cl.new_window else ''
|
||||
template_code += LINK_BUTTON.format(
|
||||
link_rendered, link_target, cl.button_class, text_rendered
|
||||
@@ -70,10 +70,10 @@ def custom_links(context, obj):
|
||||
|
||||
for cl in links:
|
||||
try:
|
||||
text_rendered = render_jinja2(cl.text, link_context)
|
||||
text_rendered = render_jinja2(cl.link_text, link_context)
|
||||
if text_rendered:
|
||||
link_target = ' target="_blank"' if cl.new_window else ''
|
||||
link_rendered = render_jinja2(cl.url, link_context)
|
||||
link_rendered = render_jinja2(cl.link_url, link_context)
|
||||
links_rendered.append(
|
||||
GROUP_LINK.format(link_rendered, link_target, text_rendered)
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import datetime
|
||||
from unittest import skipIf
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
@@ -9,9 +10,9 @@ from django_rq.queues import get_connection
|
||||
from rest_framework import status
|
||||
from rq import Worker
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, RackGroup, RackRole, Site
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
|
||||
from extras.api.views import ReportViewSet, ScriptViewSet
|
||||
from extras.models import ConfigContext, CustomField, ExportTemplate, ImageAttachment, Tag
|
||||
from extras.models import *
|
||||
from extras.reports import Report
|
||||
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
|
||||
from utilities.testing import APITestCase, APIViewTestCases
|
||||
@@ -30,9 +31,63 @@ class AppTest(APITestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class WebhookTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Webhook
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'content_types': ['dcim.device', 'dcim.devicetype'],
|
||||
'name': 'Webhook 4',
|
||||
'type_create': True,
|
||||
'payload_url': 'http://example.com/?4',
|
||||
},
|
||||
{
|
||||
'content_types': ['dcim.device', 'dcim.devicetype'],
|
||||
'name': 'Webhook 5',
|
||||
'type_update': True,
|
||||
'payload_url': 'http://example.com/?5',
|
||||
},
|
||||
{
|
||||
'content_types': ['dcim.device', 'dcim.devicetype'],
|
||||
'name': 'Webhook 6',
|
||||
'type_delete': True,
|
||||
'payload_url': 'http://example.com/?6',
|
||||
},
|
||||
]
|
||||
bulk_update_data = {
|
||||
'ssl_verification': False,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
rack_ct = ContentType.objects.get_for_model(Rack)
|
||||
|
||||
webhooks = (
|
||||
Webhook(
|
||||
name='Webhook 1',
|
||||
type_create=True,
|
||||
payload_url='http://example.com/?1',
|
||||
),
|
||||
Webhook(
|
||||
name='Webhook 2',
|
||||
type_update=True,
|
||||
payload_url='http://example.com/?1',
|
||||
),
|
||||
Webhook(
|
||||
name='Webhook 3',
|
||||
type_delete=True,
|
||||
payload_url='http://example.com/?1',
|
||||
),
|
||||
)
|
||||
Webhook.objects.bulk_create(webhooks)
|
||||
for webhook in webhooks:
|
||||
webhook.content_types.add(site_ct, rack_ct)
|
||||
|
||||
|
||||
class CustomFieldTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CustomField
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'content_types': ['dcim.site'],
|
||||
@@ -77,9 +132,63 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
|
||||
cf.content_types.add(site_ct)
|
||||
|
||||
|
||||
class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CustomLink
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'content_type': 'dcim.site',
|
||||
'name': 'Custom Link 4',
|
||||
'link_text': 'Link 4',
|
||||
'link_url': 'http://example.com/?4',
|
||||
},
|
||||
{
|
||||
'content_type': 'dcim.site',
|
||||
'name': 'Custom Link 5',
|
||||
'link_text': 'Link 5',
|
||||
'link_url': 'http://example.com/?5',
|
||||
},
|
||||
{
|
||||
'content_type': 'dcim.site',
|
||||
'name': 'Custom Link 6',
|
||||
'link_text': 'Link 6',
|
||||
'link_url': 'http://example.com/?6',
|
||||
},
|
||||
]
|
||||
bulk_update_data = {
|
||||
'new_window': True,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
|
||||
custom_links = (
|
||||
CustomLink(
|
||||
content_type=site_ct,
|
||||
name='Custom Link 1',
|
||||
link_text='Link 1',
|
||||
link_url='http://example.com/?1',
|
||||
),
|
||||
CustomLink(
|
||||
content_type=site_ct,
|
||||
name='Custom Link 2',
|
||||
link_text='Link 2',
|
||||
link_url='http://example.com/?2',
|
||||
),
|
||||
CustomLink(
|
||||
content_type=site_ct,
|
||||
name='Custom Link 3',
|
||||
link_text='Link 3',
|
||||
link_url='http://example.com/?3',
|
||||
),
|
||||
)
|
||||
CustomLink.objects.bulk_create(custom_links)
|
||||
|
||||
|
||||
class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ExportTemplate
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'content_type': 'dcim.device',
|
||||
@@ -127,7 +236,7 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class TagTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Tag
|
||||
brief_fields = ['color', 'id', 'name', 'slug', 'url']
|
||||
brief_fields = ['color', 'display', 'id', 'name', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Tag 4',
|
||||
@@ -164,7 +273,7 @@ class ImageAttachmentTest(
|
||||
APIViewTestCases.DeleteObjectViewTestCase
|
||||
):
|
||||
model = ImageAttachment
|
||||
brief_fields = ['id', 'image', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'image', 'name', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -201,9 +310,59 @@ class ImageAttachmentTest(
|
||||
ImageAttachment.objects.bulk_create(image_attachments)
|
||||
|
||||
|
||||
class JournalEntryTest(APIViewTestCases.APIViewTestCase):
|
||||
model = JournalEntry
|
||||
brief_fields = ['created', 'display', 'id', 'url']
|
||||
bulk_update_data = {
|
||||
'comments': 'Overwritten',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
user = User.objects.first()
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
|
||||
journal_entries = (
|
||||
JournalEntry(
|
||||
created_by=user,
|
||||
assigned_object=site,
|
||||
comments='Fourth entry',
|
||||
),
|
||||
JournalEntry(
|
||||
created_by=user,
|
||||
assigned_object=site,
|
||||
comments='Fifth entry',
|
||||
),
|
||||
JournalEntry(
|
||||
created_by=user,
|
||||
assigned_object=site,
|
||||
comments='Sixth entry',
|
||||
),
|
||||
)
|
||||
JournalEntry.objects.bulk_create(journal_entries)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'assigned_object_type': 'dcim.site',
|
||||
'assigned_object_id': site.pk,
|
||||
'comments': 'First entry',
|
||||
},
|
||||
{
|
||||
'assigned_object_type': 'dcim.site',
|
||||
'assigned_object_id': site.pk,
|
||||
'comments': 'Second entry',
|
||||
},
|
||||
{
|
||||
'assigned_object_type': 'dcim.site',
|
||||
'assigned_object_id': site.pk,
|
||||
'comments': 'Third entry',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ConfigContext
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Config Context 4',
|
||||
@@ -382,13 +541,13 @@ class CreatedUpdatedFilterTest(APITestCase):
|
||||
super().setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.rackgroup1 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 1', slug='test-rack-group-1')
|
||||
self.location1 = Location.objects.create(site=self.site1, name='Test Location 1', slug='test-location-1')
|
||||
self.rackrole1 = RackRole.objects.create(name='Test Rack Role 1', slug='test-rack-role-1', color='ff0000')
|
||||
self.rack1 = Rack.objects.create(
|
||||
site=self.site1, group=self.rackgroup1, role=self.rackrole1, name='Test Rack 1', u_height=42,
|
||||
site=self.site1, location=self.location1, role=self.rackrole1, name='Test Rack 1', u_height=42,
|
||||
)
|
||||
self.rack2 = Rack.objects.create(
|
||||
site=self.site1, group=self.rackgroup1, role=self.rackrole1, name='Test Rack 2', u_height=42,
|
||||
site=self.site1, location=self.location1, role=self.rackrole1, name='Test Rack 2', u_height=42,
|
||||
)
|
||||
|
||||
# change the created and last_updated of one
|
||||
|
||||
@@ -40,8 +40,8 @@ class ChangeLogViewTest(ModelViewTestCase):
|
||||
def test_create_object(self):
|
||||
tags = self.create_tags('Tag 1', 'Tag 2')
|
||||
form_data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'name': 'Site 1',
|
||||
'slug': 'site-1',
|
||||
'status': SiteStatusChoices.STATUS_ACTIVE,
|
||||
'cf_my_field': 'ABC',
|
||||
'cf_my_field_select': 'Bar',
|
||||
@@ -56,7 +56,7 @@ class ChangeLogViewTest(ModelViewTestCase):
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 302)
|
||||
|
||||
site = Site.objects.get(name='Test Site 1')
|
||||
site = Site.objects.get(name='Site 1')
|
||||
# First OC is the creation; second is the tags update
|
||||
oc_list = ObjectChange.objects.filter(
|
||||
changed_object_type=ContentType.objects.get_for_model(Site),
|
||||
@@ -64,20 +64,21 @@ class ChangeLogViewTest(ModelViewTestCase):
|
||||
).order_by('pk')
|
||||
self.assertEqual(oc_list[0].changed_object, site)
|
||||
self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
|
||||
self.assertEqual(oc_list[0].object_data['custom_fields']['my_field'], form_data['cf_my_field'])
|
||||
self.assertEqual(oc_list[0].object_data['custom_fields']['my_field_select'], form_data['cf_my_field_select'])
|
||||
self.assertEqual(oc_list[0].prechange_data, None)
|
||||
self.assertEqual(oc_list[0].postchange_data['custom_fields']['my_field'], form_data['cf_my_field'])
|
||||
self.assertEqual(oc_list[0].postchange_data['custom_fields']['my_field_select'], form_data['cf_my_field_select'])
|
||||
self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(oc_list[1].object_data['tags'], ['Tag 1', 'Tag 2'])
|
||||
self.assertEqual(oc_list[1].postchange_data['tags'], ['Tag 1', 'Tag 2'])
|
||||
|
||||
def test_update_object(self):
|
||||
site = Site(name='Test Site 1', slug='test-site-1')
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
tags = self.create_tags('Tag 1', 'Tag 2', 'Tag 3')
|
||||
site.tags.set('Tag 1', 'Tag 2')
|
||||
|
||||
form_data = {
|
||||
'name': 'Test Site X',
|
||||
'slug': 'test-site-x',
|
||||
'name': 'Site X',
|
||||
'slug': 'site-x',
|
||||
'status': SiteStatusChoices.STATUS_PLANNED,
|
||||
'cf_my_field': 'DEF',
|
||||
'cf_my_field_select': 'Foo',
|
||||
@@ -100,14 +101,16 @@ class ChangeLogViewTest(ModelViewTestCase):
|
||||
).first()
|
||||
self.assertEqual(oc.changed_object, site)
|
||||
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(oc.object_data['custom_fields']['my_field'], form_data['cf_my_field'])
|
||||
self.assertEqual(oc.object_data['custom_fields']['my_field_select'], form_data['cf_my_field_select'])
|
||||
self.assertEqual(oc.object_data['tags'], ['Tag 3'])
|
||||
self.assertEqual(oc.prechange_data['name'], 'Site 1')
|
||||
self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
|
||||
self.assertEqual(oc.postchange_data['custom_fields']['my_field'], form_data['cf_my_field'])
|
||||
self.assertEqual(oc.postchange_data['custom_fields']['my_field_select'], form_data['cf_my_field_select'])
|
||||
self.assertEqual(oc.postchange_data['tags'], ['Tag 3'])
|
||||
|
||||
def test_delete_object(self):
|
||||
site = Site(
|
||||
name='Test Site 1',
|
||||
slug='test-site-1',
|
||||
name='Site 1',
|
||||
slug='site-1',
|
||||
custom_field_data={
|
||||
'my_field': 'ABC',
|
||||
'my_field_select': 'Bar'
|
||||
@@ -129,15 +132,83 @@ class ChangeLogViewTest(ModelViewTestCase):
|
||||
self.assertEqual(oc.changed_object, None)
|
||||
self.assertEqual(oc.object_repr, site.name)
|
||||
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_DELETE)
|
||||
self.assertEqual(oc.object_data['custom_fields']['my_field'], 'ABC')
|
||||
self.assertEqual(oc.object_data['custom_fields']['my_field_select'], 'Bar')
|
||||
self.assertEqual(oc.object_data['tags'], ['Tag 1', 'Tag 2'])
|
||||
self.assertEqual(oc.prechange_data['custom_fields']['my_field'], 'ABC')
|
||||
self.assertEqual(oc.prechange_data['custom_fields']['my_field_select'], 'Bar')
|
||||
self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
|
||||
self.assertEqual(oc.postchange_data, None)
|
||||
|
||||
def test_bulk_update_objects(self):
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE),
|
||||
Site(name='Site 2', slug='site-2', status=SiteStatusChoices.STATUS_ACTIVE),
|
||||
Site(name='Site 3', slug='site-3', status=SiteStatusChoices.STATUS_ACTIVE),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
form_data = {
|
||||
'pk': [site.pk for site in sites],
|
||||
'_apply': True,
|
||||
'status': SiteStatusChoices.STATUS_PLANNED,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
request = {
|
||||
'path': self._get_url('bulk_edit'),
|
||||
'data': post_data(form_data),
|
||||
}
|
||||
self.add_permissions('dcim.view_site', 'dcim.change_site')
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 302)
|
||||
|
||||
objectchange = ObjectChange.objects.get(
|
||||
changed_object_type=ContentType.objects.get_for_model(Site),
|
||||
changed_object_id=sites[0].pk
|
||||
)
|
||||
self.assertEqual(objectchange.changed_object, sites[0])
|
||||
self.assertEqual(objectchange.action, ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(objectchange.prechange_data['status'], SiteStatusChoices.STATUS_ACTIVE)
|
||||
self.assertEqual(objectchange.prechange_data['description'], '')
|
||||
self.assertEqual(objectchange.postchange_data['status'], form_data['status'])
|
||||
self.assertEqual(objectchange.postchange_data['description'], form_data['description'])
|
||||
|
||||
def test_bulk_delete_objects(self):
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE),
|
||||
Site(name='Site 2', slug='site-2', status=SiteStatusChoices.STATUS_ACTIVE),
|
||||
Site(name='Site 3', slug='site-3', status=SiteStatusChoices.STATUS_ACTIVE),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
form_data = {
|
||||
'pk': [site.pk for site in sites],
|
||||
'confirm': True,
|
||||
'_confirm': True,
|
||||
}
|
||||
|
||||
request = {
|
||||
'path': self._get_url('bulk_delete'),
|
||||
'data': post_data(form_data),
|
||||
}
|
||||
self.add_permissions('dcim.delete_site')
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 302)
|
||||
|
||||
objectchange = ObjectChange.objects.get(
|
||||
changed_object_type=ContentType.objects.get_for_model(Site),
|
||||
changed_object_id=sites[0].pk
|
||||
)
|
||||
self.assertEqual(objectchange.changed_object_type, ContentType.objects.get_for_model(Site))
|
||||
self.assertEqual(objectchange.changed_object_id, sites[0].pk)
|
||||
self.assertEqual(objectchange.action, ObjectChangeActionChoices.ACTION_DELETE)
|
||||
self.assertEqual(objectchange.prechange_data['name'], sites[0].name)
|
||||
self.assertEqual(objectchange.prechange_data['slug'], sites[0].slug)
|
||||
self.assertEqual(objectchange.postchange_data, None)
|
||||
|
||||
|
||||
class ChangeLogAPITest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
# Create a custom field on the Site model
|
||||
ct = ContentType.objects.get_for_model(Site)
|
||||
@@ -169,8 +240,8 @@ class ChangeLogAPITest(APITestCase):
|
||||
|
||||
def test_create_object(self):
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'name': 'Site 1',
|
||||
'slug': 'site-1',
|
||||
'custom_fields': {
|
||||
'my_field': 'ABC',
|
||||
'my_field_select': 'Bar',
|
||||
@@ -195,17 +266,18 @@ class ChangeLogAPITest(APITestCase):
|
||||
).order_by('pk')
|
||||
self.assertEqual(oc_list[0].changed_object, site)
|
||||
self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
|
||||
self.assertEqual(oc_list[0].object_data['custom_fields'], data['custom_fields'])
|
||||
self.assertEqual(oc_list[0].prechange_data, None)
|
||||
self.assertEqual(oc_list[0].postchange_data['custom_fields'], data['custom_fields'])
|
||||
self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(oc_list[1].object_data['tags'], ['Tag 1', 'Tag 2'])
|
||||
self.assertEqual(oc_list[1].postchange_data['tags'], ['Tag 1', 'Tag 2'])
|
||||
|
||||
def test_update_object(self):
|
||||
site = Site(name='Test Site 1', slug='test-site-1')
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
data = {
|
||||
'name': 'Test Site X',
|
||||
'slug': 'test-site-x',
|
||||
'name': 'Site X',
|
||||
'slug': 'site-x',
|
||||
'custom_fields': {
|
||||
'my_field': 'DEF',
|
||||
'my_field_select': 'Foo',
|
||||
@@ -229,13 +301,13 @@ class ChangeLogAPITest(APITestCase):
|
||||
).first()
|
||||
self.assertEqual(oc.changed_object, site)
|
||||
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(oc.object_data['custom_fields'], data['custom_fields'])
|
||||
self.assertEqual(oc.object_data['tags'], ['Tag 3'])
|
||||
self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
|
||||
self.assertEqual(oc.postchange_data['tags'], ['Tag 3'])
|
||||
|
||||
def test_delete_object(self):
|
||||
site = Site(
|
||||
name='Test Site 1',
|
||||
slug='test-site-1',
|
||||
name='Site 1',
|
||||
slug='site-1',
|
||||
custom_field_data={
|
||||
'my_field': 'ABC',
|
||||
'my_field_select': 'Bar'
|
||||
@@ -255,6 +327,123 @@ class ChangeLogAPITest(APITestCase):
|
||||
self.assertEqual(oc.changed_object, None)
|
||||
self.assertEqual(oc.object_repr, site.name)
|
||||
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_DELETE)
|
||||
self.assertEqual(oc.object_data['custom_fields']['my_field'], 'ABC')
|
||||
self.assertEqual(oc.object_data['custom_fields']['my_field_select'], 'Bar')
|
||||
self.assertEqual(oc.object_data['tags'], ['Tag 1', 'Tag 2'])
|
||||
self.assertEqual(oc.prechange_data['custom_fields']['my_field'], 'ABC')
|
||||
self.assertEqual(oc.prechange_data['custom_fields']['my_field_select'], 'Bar')
|
||||
self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
|
||||
self.assertEqual(oc.postchange_data, None)
|
||||
|
||||
def test_bulk_create_objects(self):
|
||||
data = (
|
||||
{
|
||||
'name': 'Site 1',
|
||||
'slug': 'site-1',
|
||||
},
|
||||
{
|
||||
'name': 'Site 2',
|
||||
'slug': 'site-2',
|
||||
},
|
||||
{
|
||||
'name': 'Site 3',
|
||||
'slug': 'site-3',
|
||||
},
|
||||
)
|
||||
self.assertEqual(ObjectChange.objects.count(), 0)
|
||||
url = reverse('dcim-api:site-list')
|
||||
self.add_permissions('dcim.add_site')
|
||||
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(ObjectChange.objects.count(), 3)
|
||||
|
||||
site1 = Site.objects.get(pk=response.data[0]['id'])
|
||||
objectchange = ObjectChange.objects.get(
|
||||
changed_object_type=ContentType.objects.get_for_model(Site),
|
||||
changed_object_id=site1.pk
|
||||
)
|
||||
self.assertEqual(objectchange.changed_object, site1)
|
||||
self.assertEqual(objectchange.action, ObjectChangeActionChoices.ACTION_CREATE)
|
||||
self.assertEqual(objectchange.prechange_data, None)
|
||||
self.assertEqual(objectchange.postchange_data['name'], data[0]['name'])
|
||||
self.assertEqual(objectchange.postchange_data['slug'], data[0]['slug'])
|
||||
|
||||
def test_bulk_edit_objects(self):
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
Site(name='Site 3', slug='site-3'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
data = (
|
||||
{
|
||||
'id': sites[0].pk,
|
||||
'name': 'Site A',
|
||||
'slug': 'site-A',
|
||||
},
|
||||
{
|
||||
'id': sites[1].pk,
|
||||
'name': 'Site B',
|
||||
'slug': 'site-b',
|
||||
},
|
||||
{
|
||||
'id': sites[2].pk,
|
||||
'name': 'Site C',
|
||||
'slug': 'site-c',
|
||||
},
|
||||
)
|
||||
self.assertEqual(ObjectChange.objects.count(), 0)
|
||||
url = reverse('dcim-api:site-list')
|
||||
self.add_permissions('dcim.change_site')
|
||||
|
||||
response = self.client.patch(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(ObjectChange.objects.count(), 3)
|
||||
|
||||
objectchange = ObjectChange.objects.get(
|
||||
changed_object_type=ContentType.objects.get_for_model(Site),
|
||||
changed_object_id=sites[0].pk
|
||||
)
|
||||
self.assertEqual(objectchange.changed_object, sites[0])
|
||||
self.assertEqual(objectchange.action, ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(objectchange.prechange_data['name'], 'Site 1')
|
||||
self.assertEqual(objectchange.prechange_data['slug'], 'site-1')
|
||||
self.assertEqual(objectchange.postchange_data['name'], data[0]['name'])
|
||||
self.assertEqual(objectchange.postchange_data['slug'], data[0]['slug'])
|
||||
|
||||
def test_bulk_delete_objects(self):
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
Site(name='Site 3', slug='site-3'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
data = (
|
||||
{
|
||||
'id': sites[0].pk,
|
||||
},
|
||||
{
|
||||
'id': sites[1].pk,
|
||||
},
|
||||
{
|
||||
'id': sites[2].pk,
|
||||
},
|
||||
)
|
||||
self.assertEqual(ObjectChange.objects.count(), 0)
|
||||
url = reverse('dcim-api:site-list')
|
||||
self.add_permissions('dcim.delete_site')
|
||||
|
||||
response = self.client.delete(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(ObjectChange.objects.count(), 3)
|
||||
|
||||
objectchange = ObjectChange.objects.get(
|
||||
changed_object_type=ContentType.objects.get_for_model(Site),
|
||||
changed_object_id=sites[0].pk
|
||||
)
|
||||
self.assertEqual(objectchange.changed_object_type, ContentType.objects.get_for_model(Site))
|
||||
self.assertEqual(objectchange.changed_object_id, sites[0].pk)
|
||||
self.assertEqual(objectchange.action, ObjectChangeActionChoices.ACTION_DELETE)
|
||||
self.assertEqual(objectchange.prechange_data['name'], 'Site 1')
|
||||
self.assertEqual(objectchange.prechange_data['slug'], 'site-1')
|
||||
self.assertEqual(objectchange.postchange_data, None)
|
||||
|
||||
@@ -4,15 +4,150 @@ from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Rack, Region, Site
|
||||
from extras.choices import ObjectChangeActionChoices
|
||||
from dcim.models import DeviceRole, Platform, Rack, Region, Site, SiteGroup
|
||||
from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
|
||||
from extras.filters import *
|
||||
from extras.models import ConfigContext, ExportTemplate, ImageAttachment, ObjectChange, Tag
|
||||
from extras.models import *
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
||||
|
||||
class WebhookTestCase(TestCase):
|
||||
queryset = Webhook.objects.all()
|
||||
filterset = WebhookFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
|
||||
|
||||
webhooks = (
|
||||
Webhook(
|
||||
name='Webhook 1',
|
||||
type_create=True,
|
||||
payload_url='http://example.com/?1',
|
||||
enabled=True,
|
||||
http_method='GET',
|
||||
ssl_verification=True,
|
||||
),
|
||||
Webhook(
|
||||
name='Webhook 2',
|
||||
type_update=True,
|
||||
payload_url='http://example.com/?2',
|
||||
enabled=True,
|
||||
http_method='POST',
|
||||
ssl_verification=True,
|
||||
),
|
||||
Webhook(
|
||||
name='Webhook 3',
|
||||
type_delete=True,
|
||||
payload_url='http://example.com/?3',
|
||||
enabled=False,
|
||||
http_method='PATCH',
|
||||
ssl_verification=False,
|
||||
),
|
||||
)
|
||||
Webhook.objects.bulk_create(webhooks)
|
||||
webhooks[0].content_types.add(content_types[0])
|
||||
webhooks[1].content_types.add(content_types[1])
|
||||
webhooks[2].content_types.add(content_types[2])
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Webhook 1', 'Webhook 2']}
|
||||
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)
|
||||
|
||||
def test_type_create(self):
|
||||
params = {'type_create': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_type_update(self):
|
||||
params = {'type_update': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_type_delete(self):
|
||||
params = {'type_delete': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_enabled(self):
|
||||
params = {'enabled': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_http_method(self):
|
||||
params = {'http_method': ['GET', 'POST']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_ssl_verification(self):
|
||||
params = {'ssl_verification': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class CustomLinkTestCase(TestCase):
|
||||
queryset = CustomLink.objects.all()
|
||||
filterset = CustomLinkFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
|
||||
|
||||
custom_links = (
|
||||
CustomLink(
|
||||
name='Custom Link 1',
|
||||
content_type=content_types[0],
|
||||
weight=100,
|
||||
new_window=False,
|
||||
link_text='Link 1',
|
||||
link_url='http://example.com/?1'
|
||||
),
|
||||
CustomLink(
|
||||
name='Custom Link 2',
|
||||
content_type=content_types[1],
|
||||
weight=200,
|
||||
new_window=False,
|
||||
link_text='Link 1',
|
||||
link_url='http://example.com/?2'
|
||||
),
|
||||
CustomLink(
|
||||
name='Custom Link 3',
|
||||
content_type=content_types[2],
|
||||
weight=300,
|
||||
new_window=True,
|
||||
link_text='Link 1',
|
||||
link_url='http://example.com/?3'
|
||||
),
|
||||
)
|
||||
CustomLink.objects.bulk_create(custom_links)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Custom Link 1', 'Custom Link 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_content_type(self):
|
||||
params = {'content_type': ContentType.objects.get(model='site').pk}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_weight(self):
|
||||
params = {'weight': [100, 200]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_new_window(self):
|
||||
params = {'new_window': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'new_window': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class ExportTemplateTestCase(TestCase):
|
||||
queryset = ExportTemplate.objects.all()
|
||||
filterset = ExportTemplateFilterSet
|
||||
@@ -120,6 +255,100 @@ class ImageAttachmentTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class JournalEntryTestCase(TestCase):
|
||||
queryset = JournalEntry.objects.all()
|
||||
filterset = JournalEntryFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
users = (
|
||||
User(username='Alice'),
|
||||
User(username='Bob'),
|
||||
User(username='Charlie'),
|
||||
)
|
||||
User.objects.bulk_create(users)
|
||||
|
||||
journal_entries = (
|
||||
JournalEntry(
|
||||
assigned_object=sites[0],
|
||||
created_by=users[0],
|
||||
kind=JournalEntryKindChoices.KIND_INFO,
|
||||
comments='New journal entry'
|
||||
),
|
||||
JournalEntry(
|
||||
assigned_object=sites[0],
|
||||
created_by=users[1],
|
||||
kind=JournalEntryKindChoices.KIND_SUCCESS,
|
||||
comments='New journal entry'
|
||||
),
|
||||
JournalEntry(
|
||||
assigned_object=sites[1],
|
||||
created_by=users[2],
|
||||
kind=JournalEntryKindChoices.KIND_WARNING,
|
||||
comments='New journal entry'
|
||||
),
|
||||
JournalEntry(
|
||||
assigned_object=racks[0],
|
||||
created_by=users[0],
|
||||
kind=JournalEntryKindChoices.KIND_INFO,
|
||||
comments='New journal entry'
|
||||
),
|
||||
JournalEntry(
|
||||
assigned_object=racks[0],
|
||||
created_by=users[1],
|
||||
kind=JournalEntryKindChoices.KIND_SUCCESS,
|
||||
comments='New journal entry'
|
||||
),
|
||||
JournalEntry(
|
||||
assigned_object=racks[1],
|
||||
created_by=users[2],
|
||||
kind=JournalEntryKindChoices.KIND_WARNING,
|
||||
comments='New journal entry'
|
||||
),
|
||||
)
|
||||
JournalEntry.objects.bulk_create(journal_entries)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_created_by(self):
|
||||
users = User.objects.filter(username__in=['Alice', 'Bob'])
|
||||
params = {'created_by': [users[0].username, users[1].username]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'created_by_id': [users[0].pk, users[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_assigned_object_type(self):
|
||||
params = {'assigned_object_type': 'dcim.site'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'assigned_object_type_id': ContentType.objects.get(app_label='dcim', model='site').pk}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_assigned_object(self):
|
||||
params = {
|
||||
'assigned_object_type': 'dcim.site',
|
||||
'assigned_object_id': [Site.objects.first().pk],
|
||||
}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_kind(self):
|
||||
params = {'kind': [JournalEntryKindChoices.KIND_INFO, JournalEntryKindChoices.KIND_SUCCESS]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class ConfigContextTestCase(TestCase):
|
||||
queryset = ConfigContext.objects.all()
|
||||
filterset = ConfigContextFilterSet
|
||||
@@ -132,10 +361,17 @@ class ConfigContextTestCase(TestCase):
|
||||
Region(name='Test Region 2', slug='test-region-2'),
|
||||
Region(name='Test Region 3', slug='test-region-3'),
|
||||
)
|
||||
# Can't use bulk_create for models with MPTT fields
|
||||
for r in regions:
|
||||
r.save()
|
||||
|
||||
site_groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for site_group in site_groups:
|
||||
site_group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Test Site 1', slug='test-site-1'),
|
||||
Site(name='Test Site 2', slug='test-site-2'),
|
||||
@@ -195,6 +431,7 @@ class ConfigContextTestCase(TestCase):
|
||||
data='{"foo": 123}'
|
||||
)
|
||||
c.regions.set([regions[i]])
|
||||
c.site_groups.set([site_groups[i]])
|
||||
c.sites.set([sites[i]])
|
||||
c.roles.set([device_roles[i]])
|
||||
c.platforms.set([platforms[i]])
|
||||
@@ -224,6 +461,13 @@ class ConfigContextTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -327,7 +571,7 @@ class ObjectChangeTestCase(TestCase):
|
||||
action=ObjectChangeActionChoices.ACTION_CREATE,
|
||||
changed_object=site,
|
||||
object_repr=str(site),
|
||||
object_data={'name': site.name, 'slug': site.slug}
|
||||
postchange_data={'name': site.name, 'slug': site.slug}
|
||||
),
|
||||
ObjectChange(
|
||||
user=users[0],
|
||||
@@ -336,7 +580,7 @@ class ObjectChangeTestCase(TestCase):
|
||||
action=ObjectChangeActionChoices.ACTION_UPDATE,
|
||||
changed_object=site,
|
||||
object_repr=str(site),
|
||||
object_data={'name': site.name, 'slug': site.slug}
|
||||
postchange_data={'name': site.name, 'slug': site.slug}
|
||||
),
|
||||
ObjectChange(
|
||||
user=users[1],
|
||||
@@ -345,7 +589,7 @@ class ObjectChangeTestCase(TestCase):
|
||||
action=ObjectChangeActionChoices.ACTION_DELETE,
|
||||
changed_object=site,
|
||||
object_repr=str(site),
|
||||
object_data={'name': site.name, 'slug': site.slug}
|
||||
postchange_data={'name': site.name, 'slug': site.slug}
|
||||
),
|
||||
ObjectChange(
|
||||
user=users[1],
|
||||
@@ -354,7 +598,7 @@ class ObjectChangeTestCase(TestCase):
|
||||
action=ObjectChangeActionChoices.ACTION_CREATE,
|
||||
changed_object=ipaddress,
|
||||
object_repr=str(ipaddress),
|
||||
object_data={'address': ipaddress.address, 'status': ipaddress.status}
|
||||
postchange_data={'address': ipaddress.address, 'status': ipaddress.status}
|
||||
),
|
||||
ObjectChange(
|
||||
user=users[2],
|
||||
@@ -363,7 +607,7 @@ class ObjectChangeTestCase(TestCase):
|
||||
action=ObjectChangeActionChoices.ACTION_UPDATE,
|
||||
changed_object=ipaddress,
|
||||
object_repr=str(ipaddress),
|
||||
object_data={'address': ipaddress.address, 'status': ipaddress.status}
|
||||
postchange_data={'address': ipaddress.address, 'status': ipaddress.status}
|
||||
),
|
||||
ObjectChange(
|
||||
user=users[2],
|
||||
@@ -372,7 +616,7 @@ class ObjectChangeTestCase(TestCase):
|
||||
action=ObjectChangeActionChoices.ACTION_DELETE,
|
||||
changed_object=ipaddress,
|
||||
object_repr=str(ipaddress),
|
||||
object_data={'address': ipaddress.address, 'status': ipaddress.status}
|
||||
postchange_data={'address': ipaddress.address, 'status': ipaddress.status}
|
||||
),
|
||||
)
|
||||
ObjectChange.objects.bulk_create(object_changes)
|
||||
|
||||
@@ -3,12 +3,11 @@ import uuid
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.models import Site
|
||||
from extras.choices import ObjectChangeActionChoices
|
||||
from extras.models import ConfigContext, CustomLink, ObjectChange, Tag
|
||||
from extras.models import ConfigContext, CustomLink, JournalEntry, ObjectChange, Tag
|
||||
from utilities.testing import ViewTestCases, TestCase
|
||||
|
||||
|
||||
@@ -128,6 +127,43 @@ class ObjectChangeTestCase(TestCase):
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
|
||||
class JournalEntryTestCase(
|
||||
# ViewTestCases.GetObjectViewTestCase,
|
||||
ViewTestCases.CreateObjectViewTestCase,
|
||||
ViewTestCases.EditObjectViewTestCase,
|
||||
ViewTestCases.DeleteObjectViewTestCase,
|
||||
ViewTestCases.ListObjectsViewTestCase,
|
||||
ViewTestCases.BulkEditObjectsViewTestCase,
|
||||
ViewTestCases.BulkDeleteObjectsViewTestCase
|
||||
):
|
||||
model = JournalEntry
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
user = User.objects.create(username='User 1')
|
||||
|
||||
JournalEntry.objects.bulk_create((
|
||||
JournalEntry(assigned_object=site, created_by=user, comments='First entry'),
|
||||
JournalEntry(assigned_object=site, created_by=user, comments='Second entry'),
|
||||
JournalEntry(assigned_object=site, created_by=user, comments='Third entry'),
|
||||
))
|
||||
|
||||
cls.form_data = {
|
||||
'assigned_object_type': site_ct.pk,
|
||||
'assigned_object_id': site.pk,
|
||||
'kind': 'info',
|
||||
'comments': 'A new entry',
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'kind': 'success',
|
||||
'comments': 'Overwritten',
|
||||
}
|
||||
|
||||
|
||||
class CustomLinkTest(TestCase):
|
||||
user_permissions = ['dcim.view_site']
|
||||
|
||||
@@ -135,8 +171,8 @@ class CustomLinkTest(TestCase):
|
||||
customlink = CustomLink(
|
||||
content_type=ContentType.objects.get_for_model(Site),
|
||||
name='Test',
|
||||
text='FOO {{ obj.name }} BAR',
|
||||
url='http://example.com/?site={{ obj.slug }}',
|
||||
link_text='FOO {{ obj.name }} BAR',
|
||||
link_url='http://example.com/?site={{ obj.slug }}',
|
||||
new_window=False
|
||||
)
|
||||
customlink.save()
|
||||
|
||||
@@ -56,10 +56,10 @@ class WebhookTest(APITestCase):
|
||||
# Verify that a job was queued for the object creation webhook
|
||||
self.assertEqual(self.queue.count, 1)
|
||||
job = self.queue.jobs[0]
|
||||
self.assertEqual(job.args[0], Webhook.objects.get(type_create=True))
|
||||
self.assertEqual(job.args[1]['id'], response.data['id'])
|
||||
self.assertEqual(job.args[2], 'site')
|
||||
self.assertEqual(job.args[3], ObjectChangeActionChoices.ACTION_CREATE)
|
||||
self.assertEqual(job.kwargs['webhook'], Webhook.objects.get(type_create=True))
|
||||
self.assertEqual(job.kwargs['data']['id'], response.data['id'])
|
||||
self.assertEqual(job.kwargs['model_name'], 'site')
|
||||
self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_CREATE)
|
||||
|
||||
def test_enqueue_webhook_update(self):
|
||||
# Update an object via the REST API
|
||||
@@ -75,10 +75,10 @@ class WebhookTest(APITestCase):
|
||||
# Verify that a job was queued for the object update webhook
|
||||
self.assertEqual(self.queue.count, 1)
|
||||
job = self.queue.jobs[0]
|
||||
self.assertEqual(job.args[0], Webhook.objects.get(type_update=True))
|
||||
self.assertEqual(job.args[1]['id'], site.pk)
|
||||
self.assertEqual(job.args[2], 'site')
|
||||
self.assertEqual(job.args[3], ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(job.kwargs['webhook'], Webhook.objects.get(type_update=True))
|
||||
self.assertEqual(job.kwargs['data']['id'], site.pk)
|
||||
self.assertEqual(job.kwargs['model_name'], 'site')
|
||||
self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
|
||||
def test_enqueue_webhook_delete(self):
|
||||
# Delete an object via the REST API
|
||||
@@ -91,10 +91,10 @@ class WebhookTest(APITestCase):
|
||||
# Verify that a job was queued for the object update webhook
|
||||
self.assertEqual(self.queue.count, 1)
|
||||
job = self.queue.jobs[0]
|
||||
self.assertEqual(job.args[0], Webhook.objects.get(type_delete=True))
|
||||
self.assertEqual(job.args[1]['id'], site.pk)
|
||||
self.assertEqual(job.args[2], 'site')
|
||||
self.assertEqual(job.args[3], ObjectChangeActionChoices.ACTION_DELETE)
|
||||
self.assertEqual(job.kwargs['webhook'], Webhook.objects.get(type_delete=True))
|
||||
self.assertEqual(job.kwargs['data']['id'], site.pk)
|
||||
self.assertEqual(job.kwargs['model_name'], 'site')
|
||||
self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_DELETE)
|
||||
|
||||
def test_webhooks_worker(self):
|
||||
|
||||
@@ -116,7 +116,7 @@ class WebhookTest(APITestCase):
|
||||
# Validate the outgoing request body
|
||||
body = json.loads(request.body)
|
||||
self.assertEqual(body['event'], 'created')
|
||||
self.assertEqual(body['timestamp'], job.args[4])
|
||||
self.assertEqual(body['timestamp'], job.kwargs['timestamp'])
|
||||
self.assertEqual(body['model'], 'site')
|
||||
self.assertEqual(body['username'], 'testuser')
|
||||
self.assertEqual(body['request_id'], str(request_id))
|
||||
@@ -138,4 +138,4 @@ class WebhookTest(APITestCase):
|
||||
|
||||
# Patch the Session object with our dummy_send() method, then process the webhook for sending
|
||||
with patch.object(Session, 'send', dummy_send) as mock_send:
|
||||
process_webhook(*job.args)
|
||||
process_webhook(**job.kwargs)
|
||||
|
||||
@@ -31,6 +31,14 @@ urlpatterns = [
|
||||
path('image-attachments/<int:pk>/edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
|
||||
path('image-attachments/<int:pk>/delete/', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
|
||||
|
||||
# Journal entries
|
||||
path('journal-entries/', views.JournalEntryListView.as_view(), name='journalentry_list'),
|
||||
path('journal-entries/add/', views.JournalEntryEditView.as_view(), name='journalentry_add'),
|
||||
path('journal-entries/edit/', views.JournalEntryBulkEditView.as_view(), name='journalentry_bulk_edit'),
|
||||
path('journal-entries/delete/', views.JournalEntryBulkDeleteView.as_view(), name='journalentry_bulk_delete'),
|
||||
path('journal-entries/<int:pk>/edit/', views.JournalEntryEditView.as_view(), name='journalentry_edit'),
|
||||
path('journal-entries/<int:pk>/delete/', views.JournalEntryDeleteView.as_view(), name='journalentry_delete'),
|
||||
|
||||
# Change logging
|
||||
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
||||
path('changelog/<int:pk>/', views.ObjectChangeView.as_view(), name='objectchange'),
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.http import Http404, HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.views.generic import View
|
||||
from django_rq.queues import get_connection
|
||||
from django_tables2 import RequestConfig
|
||||
@@ -16,7 +17,7 @@ from utilities.utils import copy_safe_request, count_related, shallow_compare_di
|
||||
from utilities.views import ContentTypePermissionRequiredMixin
|
||||
from . import filters, forms, tables
|
||||
from .choices import JobResultStatusChoices
|
||||
from .models import ConfigContext, ImageAttachment, ObjectChange, JobResult, Tag, TaggedItem
|
||||
from .models import ConfigContext, ImageAttachment, JournalEntry, ObjectChange, JobResult, Tag, TaggedItem
|
||||
from .reports import get_report, get_reports, run_report
|
||||
from .scripts import get_scripts, run_script
|
||||
|
||||
@@ -178,16 +179,18 @@ class ObjectChangeView(generic.ObjectView):
|
||||
next_change = objectchanges.filter(time__gt=instance.time).order_by('time').first()
|
||||
prev_change = objectchanges.filter(time__lt=instance.time).order_by('-time').first()
|
||||
|
||||
if prev_change:
|
||||
if instance.prechange_data and instance.postchange_data:
|
||||
diff_added = shallow_compare_dict(
|
||||
prev_change.object_data,
|
||||
instance.object_data,
|
||||
instance.prechange_data or dict(),
|
||||
instance.postchange_data or dict(),
|
||||
exclude=['last_updated'],
|
||||
)
|
||||
diff_removed = {x: prev_change.object_data.get(x) for x in diff_added}
|
||||
diff_removed = {
|
||||
x: instance.prechange_data.get(x) for x in diff_added
|
||||
} if instance.prechange_data else {}
|
||||
else:
|
||||
# No previous change; this is the initial change that added the object
|
||||
diff_added = diff_removed = instance.object_data
|
||||
diff_added = None
|
||||
diff_removed = None
|
||||
|
||||
return {
|
||||
'diff_added': diff_added,
|
||||
@@ -279,6 +282,120 @@ class ImageAttachmentDeleteView(generic.ObjectDeleteView):
|
||||
return imageattachment.parent.get_absolute_url()
|
||||
|
||||
|
||||
#
|
||||
# Journal entries
|
||||
#
|
||||
|
||||
class JournalEntryListView(generic.ObjectListView):
|
||||
queryset = JournalEntry.objects.all()
|
||||
filterset = filters.JournalEntryFilterSet
|
||||
filterset_form = forms.JournalEntryFilterForm
|
||||
table = tables.JournalEntryTable
|
||||
action_buttons = ('export',)
|
||||
|
||||
|
||||
class JournalEntryEditView(generic.ObjectEditView):
|
||||
queryset = JournalEntry.objects.all()
|
||||
model_form = forms.JournalEntryForm
|
||||
|
||||
def alter_obj(self, obj, request, args, kwargs):
|
||||
if not obj.pk:
|
||||
obj.created_by = request.user
|
||||
return obj
|
||||
|
||||
def get_return_url(self, request, instance):
|
||||
if not instance.assigned_object:
|
||||
return reverse('extras:journalentry_list')
|
||||
obj = instance.assigned_object
|
||||
viewname = f'{obj._meta.app_label}:{obj._meta.model_name}_journal'
|
||||
return reverse(viewname, kwargs={'pk': obj.pk})
|
||||
|
||||
|
||||
class JournalEntryDeleteView(generic.ObjectDeleteView):
|
||||
queryset = JournalEntry.objects.all()
|
||||
|
||||
def get_return_url(self, request, instance):
|
||||
obj = instance.assigned_object
|
||||
viewname = f'{obj._meta.app_label}:{obj._meta.model_name}_journal'
|
||||
return reverse(viewname, kwargs={'pk': obj.pk})
|
||||
|
||||
|
||||
class JournalEntryBulkEditView(generic.BulkEditView):
|
||||
queryset = JournalEntry.objects.prefetch_related('created_by')
|
||||
filterset = filters.JournalEntryFilterSet
|
||||
table = tables.JournalEntryTable
|
||||
form = forms.JournalEntryBulkEditForm
|
||||
|
||||
|
||||
class JournalEntryBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = JournalEntry.objects.prefetch_related('created_by')
|
||||
filterset = filters.JournalEntryFilterSet
|
||||
table = tables.JournalEntryTable
|
||||
|
||||
|
||||
class ObjectJournalView(View):
|
||||
"""
|
||||
Show all journal entries for an object.
|
||||
|
||||
base_template: The name of the template to extend. If not provided, "<app>/<model>.html" will be used.
|
||||
"""
|
||||
base_template = None
|
||||
|
||||
def get(self, request, model, **kwargs):
|
||||
|
||||
# Handle QuerySet restriction of parent object if needed
|
||||
if hasattr(model.objects, 'restrict'):
|
||||
obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs)
|
||||
else:
|
||||
obj = get_object_or_404(model, **kwargs)
|
||||
|
||||
# Gather all changes for this object (and its related objects)
|
||||
content_type = ContentType.objects.get_for_model(model)
|
||||
journalentries = JournalEntry.objects.restrict(request.user, 'view').prefetch_related('created_by').filter(
|
||||
assigned_object_type=content_type,
|
||||
assigned_object_id=obj.pk
|
||||
)
|
||||
journalentry_table = tables.ObjectJournalTable(
|
||||
data=journalentries,
|
||||
orderable=False
|
||||
)
|
||||
|
||||
# Apply the request context
|
||||
paginate = {
|
||||
'paginator_class': EnhancedPaginator,
|
||||
'per_page': get_paginate_count(request)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(journalentry_table)
|
||||
|
||||
if request.user.has_perm('extras.add_journalentry'):
|
||||
form = forms.JournalEntryForm(
|
||||
initial={
|
||||
'assigned_object_type': ContentType.objects.get_for_model(obj),
|
||||
'assigned_object_id': obj.pk
|
||||
}
|
||||
)
|
||||
else:
|
||||
form = None
|
||||
|
||||
# Default to using "<app>/<model>.html" as the template, if it exists. Otherwise,
|
||||
# fall back to using base.html.
|
||||
if self.base_template is None:
|
||||
self.base_template = f"{model._meta.app_label}/{model._meta.model_name}.html"
|
||||
# TODO: This can be removed once an object view has been established for every model.
|
||||
try:
|
||||
template.loader.get_template(self.base_template)
|
||||
except template.TemplateDoesNotExist:
|
||||
self.base_template = 'base.html'
|
||||
|
||||
return render(request, 'extras/object_journal.html', {
|
||||
'object': obj,
|
||||
'form': form,
|
||||
'table': journalentry_table,
|
||||
'base_template': self.base_template,
|
||||
'active_tab': 'journal',
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
# Reports
|
||||
#
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.utils import timezone
|
||||
from django_rq import get_queue
|
||||
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.utils import serialize_object
|
||||
from .choices import *
|
||||
from .models import Webhook
|
||||
from .registry import registry
|
||||
@@ -44,6 +45,7 @@ def enqueue_webhooks(instance, user, request_id, action):
|
||||
webhooks = Webhook.objects.filter(content_types=content_type, enabled=True, **{action_flag: True})
|
||||
|
||||
if webhooks.exists():
|
||||
|
||||
# Get the Model's API serializer class and serialize the object
|
||||
serializer_class = get_serializer_for_model(instance.__class__)
|
||||
serializer_context = {
|
||||
@@ -51,16 +53,23 @@ def enqueue_webhooks(instance, user, request_id, action):
|
||||
}
|
||||
serializer = serializer_class(instance, context=serializer_context)
|
||||
|
||||
# Gather pre- and post-change snapshots
|
||||
snapshots = {
|
||||
'prechange': getattr(instance, '_prechange_snapshot', None),
|
||||
'postchange': serialize_object(instance) if action != ObjectChangeActionChoices.ACTION_DELETE else None,
|
||||
}
|
||||
|
||||
# Enqueue the webhooks
|
||||
webhook_queue = get_queue('default')
|
||||
for webhook in webhooks:
|
||||
webhook_queue.enqueue(
|
||||
"extras.webhooks_worker.process_webhook",
|
||||
webhook,
|
||||
serializer.data,
|
||||
instance._meta.model_name,
|
||||
action,
|
||||
str(timezone.now()),
|
||||
user.username,
|
||||
request_id
|
||||
webhook=webhook,
|
||||
model_name=instance._meta.model_name,
|
||||
event=action,
|
||||
data=serializer.data,
|
||||
snapshots=snapshots,
|
||||
timestamp=str(timezone.now()),
|
||||
username=user.username,
|
||||
request_id=request_id
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ logger = logging.getLogger('netbox.webhooks_worker')
|
||||
|
||||
|
||||
@job('default')
|
||||
def process_webhook(webhook, data, model_name, event, timestamp, username, request_id):
|
||||
def process_webhook(webhook, model_name, event, data, snapshots, timestamp, username, request_id):
|
||||
"""
|
||||
Make a POST request to the defined Webhook
|
||||
"""
|
||||
@@ -22,7 +22,8 @@ def process_webhook(webhook, data, model_name, event, timestamp, username, reque
|
||||
'model': model_name,
|
||||
'username': username,
|
||||
'request_id': request_id,
|
||||
'data': data
|
||||
'data': data,
|
||||
'snapshots': snapshots,
|
||||
}
|
||||
|
||||
# Build the headers for the HTTP request
|
||||
|
||||
@@ -27,7 +27,7 @@ class NestedVRFSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.VRF
|
||||
fields = ['id', 'url', 'name', 'rd', 'display_name', 'prefix_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'rd', 'display_name', 'prefix_count']
|
||||
|
||||
|
||||
#
|
||||
@@ -39,7 +39,7 @@ class NestedRouteTargetSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.RouteTarget
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
#
|
||||
@@ -52,7 +52,7 @@ class NestedRIRSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.RIR
|
||||
fields = ['id', 'url', 'name', 'slug', 'aggregate_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'aggregate_count']
|
||||
|
||||
|
||||
class NestedAggregateSerializer(WritableNestedSerializer):
|
||||
@@ -61,7 +61,7 @@ class NestedAggregateSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Aggregate
|
||||
fields = ['id', 'url', 'family', 'prefix']
|
||||
fields = ['id', 'url', 'display', 'family', 'prefix']
|
||||
|
||||
|
||||
#
|
||||
@@ -75,7 +75,7 @@ class NestedRoleSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Role
|
||||
fields = ['id', 'url', 'name', 'slug', 'prefix_count', 'vlan_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'prefix_count', 'vlan_count']
|
||||
|
||||
|
||||
class NestedVLANGroupSerializer(WritableNestedSerializer):
|
||||
@@ -84,7 +84,7 @@ class NestedVLANGroupSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.VLANGroup
|
||||
fields = ['id', 'url', 'name', 'slug', 'vlan_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'vlan_count']
|
||||
|
||||
|
||||
class NestedVLANSerializer(WritableNestedSerializer):
|
||||
@@ -92,7 +92,7 @@ class NestedVLANSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.VLAN
|
||||
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
||||
fields = ['id', 'url', 'display', 'vid', 'name', 'display_name']
|
||||
|
||||
|
||||
#
|
||||
@@ -105,7 +105,7 @@ class NestedPrefixSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Prefix
|
||||
fields = ['id', 'url', 'family', 'prefix']
|
||||
fields = ['id', 'url', 'display', 'family', 'prefix']
|
||||
|
||||
|
||||
#
|
||||
@@ -118,7 +118,7 @@ class NestedIPAddressSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.IPAddress
|
||||
fields = ['id', 'url', 'family', 'address']
|
||||
fields = ['id', 'url', 'display', 'family', 'address']
|
||||
|
||||
|
||||
#
|
||||
@@ -130,4 +130,4 @@ class NestedServiceSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Service
|
||||
fields = ['id', 'url', 'name', 'protocol', 'ports']
|
||||
fields = ['id', 'url', 'display', 'name', 'protocol', 'ports']
|
||||
|
||||
@@ -6,13 +6,12 @@ from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
||||
from netbox.api.serializers import CustomFieldModelSerializer
|
||||
from extras.api.serializers import TaggedObjectSerializer
|
||||
from ipam.choices import *
|
||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import OrganizationalModelSerializer
|
||||
from netbox.api.serializers import PrimaryModelSerializer
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
||||
@@ -23,7 +22,7 @@ from .nested_serializers import *
|
||||
# VRFs
|
||||
#
|
||||
|
||||
class VRFSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class VRFSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
import_targets = SerializedPKRelatedField(
|
||||
@@ -44,8 +43,9 @@ class VRFSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = [
|
||||
'id', 'url', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets',
|
||||
'tags', 'display_name', 'custom_fields', 'created', 'last_updated', 'ipaddress_count', 'prefix_count',
|
||||
'id', 'url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets',
|
||||
'export_targets', 'tags', 'display_name', 'custom_fields', 'created', 'last_updated', 'ipaddress_count',
|
||||
'prefix_count',
|
||||
]
|
||||
|
||||
|
||||
@@ -53,14 +53,14 @@ class VRFSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
# Route targets
|
||||
#
|
||||
|
||||
class RouteTargetSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class RouteTargetSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = RouteTarget
|
||||
fields = [
|
||||
'id', 'url', 'name', 'tenant', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'tenant', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@@ -75,12 +75,12 @@ class RIRSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = RIR
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'is_private', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'aggregate_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'custom_fields', 'created',
|
||||
'last_updated', 'aggregate_count',
|
||||
]
|
||||
|
||||
|
||||
class AggregateSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class AggregateSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
rir = NestedRIRSerializer()
|
||||
@@ -89,8 +89,8 @@ class AggregateSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
fields = [
|
||||
'id', 'url', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
read_only_fields = ['family']
|
||||
|
||||
@@ -107,21 +107,28 @@ class RoleSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'weight', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'prefix_count', 'vlan_count',
|
||||
]
|
||||
|
||||
|
||||
class VLANGroupSerializer(OrganizationalModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
scope_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(
|
||||
app_label='dcim',
|
||||
model__in=['region', 'sitegroup', 'site', 'location', 'rack']
|
||||
),
|
||||
required=False
|
||||
)
|
||||
scope = serializers.SerializerMethodField(read_only=True)
|
||||
vlan_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = [
|
||||
'id', 'url', 'name', 'slug', 'site', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'vlan_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'description', 'custom_fields',
|
||||
'created', 'last_updated', 'vlan_count',
|
||||
]
|
||||
validators = []
|
||||
|
||||
@@ -138,8 +145,16 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
|
||||
|
||||
return data
|
||||
|
||||
def get_scope(self, obj):
|
||||
if obj.scope_id is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.scope, prefix='Nested')
|
||||
context = {'request': self.context['request']}
|
||||
|
||||
class VLANSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
return serializer(obj.scope, context=context).data
|
||||
|
||||
|
||||
class VLANSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
group = NestedVLANGroupSerializer(required=False, allow_null=True)
|
||||
@@ -151,7 +166,7 @@ class VLANSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = [
|
||||
'id', 'url', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'tags',
|
||||
'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'tags',
|
||||
'display_name', 'custom_fields', 'created', 'last_updated', 'prefix_count',
|
||||
]
|
||||
validators = []
|
||||
@@ -174,7 +189,7 @@ class VLANSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
# Prefixes
|
||||
#
|
||||
|
||||
class PrefixSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class PrefixSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
@@ -187,7 +202,7 @@ class PrefixSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = [
|
||||
'id', 'url', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool',
|
||||
'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool',
|
||||
'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
read_only_fields = ['family']
|
||||
@@ -244,7 +259,7 @@ class AvailablePrefixSerializer(serializers.Serializer):
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
class IPAddressSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class IPAddressSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||
@@ -263,7 +278,7 @@ class IPAddressSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'id', 'url', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type',
|
||||
'id', 'url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type',
|
||||
'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
@@ -302,7 +317,7 @@ class AvailableIPSerializer(serializers.Serializer):
|
||||
# Services
|
||||
#
|
||||
|
||||
class ServiceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class ServiceSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
|
||||
device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
|
||||
@@ -317,6 +332,6 @@ class ServiceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = Service
|
||||
fields = [
|
||||
'id', 'url', 'device', 'virtual_machine', 'name', 'ports', 'protocol', 'ipaddresses', 'description', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'device', 'virtual_machine', 'name', 'ports', 'protocol', 'ipaddresses',
|
||||
'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@@ -283,7 +283,7 @@ class IPAddressViewSet(CustomFieldModelViewSet):
|
||||
#
|
||||
|
||||
class VLANGroupViewSet(CustomFieldModelViewSet):
|
||||
queryset = VLANGroup.objects.prefetch_related('site').annotate(
|
||||
queryset = VLANGroup.objects.annotate(
|
||||
vlan_count=count_related(VLAN, 'group')
|
||||
)
|
||||
serializer_class = serializers.VLANGroupSerializer
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import django_filters
|
||||
import netaddr
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from dcim.models import Device, Interface, Region, Site
|
||||
from dcim.models import Device, Interface, Region, Site, SiteGroup
|
||||
from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from utilities.filters import (
|
||||
BaseFilterSet, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericArrayFilter, TagFilter,
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
BaseFilterSet, ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet,
|
||||
NumericArrayFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from .choices import *
|
||||
@@ -192,7 +193,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
|
||||
field_name='prefix',
|
||||
lookup_expr='family'
|
||||
)
|
||||
prefix = django_filters.CharFilter(
|
||||
prefix = MultiValueCharFilter(
|
||||
method='filter_prefix',
|
||||
label='Prefix',
|
||||
)
|
||||
@@ -254,6 +255,19 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@@ -304,13 +318,13 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def filter_prefix(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
try:
|
||||
query = str(netaddr.IPNetwork(value).cidr)
|
||||
return queryset.filter(prefix=query)
|
||||
except (AddrFormatError, ValueError):
|
||||
return queryset.none()
|
||||
query_values = []
|
||||
for v in value:
|
||||
try:
|
||||
query_values.append(netaddr.IPNetwork(v))
|
||||
except (AddrFormatError, ValueError):
|
||||
pass
|
||||
return queryset.filter(prefix__in=query_values)
|
||||
|
||||
def search_within(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
@@ -522,33 +536,38 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
|
||||
|
||||
|
||||
class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
scope_type = ContentTypeFilter()
|
||||
region = django_filters.NumberFilter(
|
||||
method='filter_scope'
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
sitegroup = django_filters.NumberFilter(
|
||||
method='filter_scope'
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
site = django_filters.NumberFilter(
|
||||
method='filter_scope'
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
location = django_filters.NumberFilter(
|
||||
method='filter_scope'
|
||||
)
|
||||
rack = django_filters.NumberFilter(
|
||||
method='filter_scope'
|
||||
)
|
||||
clustergroup = django_filters.NumberFilter(
|
||||
method='filter_scope'
|
||||
)
|
||||
cluster = django_filters.NumberFilter(
|
||||
method='filter_scope'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
fields = ['id', 'name', 'slug', 'description', 'scope_id']
|
||||
|
||||
def filter_scope(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
scope_type=ContentType.objects.get(model=name),
|
||||
scope_id=value
|
||||
)
|
||||
|
||||
|
||||
class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
@@ -569,6 +588,19 @@ class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
label='Site group (ID)',
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='site__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.models import Device, Interface, Rack, Region, Site
|
||||
from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
|
||||
from extras.forms import (
|
||||
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
|
||||
)
|
||||
@@ -13,7 +13,7 @@ from utilities.forms import (
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, NumericArrayField,
|
||||
ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from virtualization.models import Cluster, VirtualMachine, VMInterface
|
||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
||||
@@ -217,6 +217,24 @@ class RIRCSVForm(CustomFieldModelCSVForm):
|
||||
}
|
||||
|
||||
|
||||
class RIRBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RIR.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
is_private = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['is_private', 'description']
|
||||
|
||||
|
||||
class RIRFilterForm(BootstrapMixin, forms.Form):
|
||||
is_private = forms.NullBooleanField(
|
||||
required=False,
|
||||
@@ -351,6 +369,23 @@ class RoleCSVForm(CustomFieldModelCSVForm):
|
||||
fields = Role.csv_headers
|
||||
|
||||
|
||||
class RoleBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
weight = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
#
|
||||
# Prefixes
|
||||
#
|
||||
@@ -359,8 +394,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF',
|
||||
display_field='display_name'
|
||||
label='VRF'
|
||||
)
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -369,12 +403,20 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'region_id': '$region'
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
)
|
||||
vlan_group = DynamicModelChoiceField(
|
||||
@@ -393,7 +435,6 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False,
|
||||
label='VLAN',
|
||||
display_field='display_name',
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'group_id': '$vlan_group',
|
||||
@@ -416,7 +457,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
]
|
||||
fieldsets = (
|
||||
('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'description', 'tags')),
|
||||
('Site/VLAN Assignment', ('region', 'site', 'vlan_group', 'vlan')),
|
||||
('Site/VLAN Assignment', ('region', 'site_group', 'site', 'vlan_group', 'vlan')),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
)
|
||||
widgets = {
|
||||
@@ -497,18 +538,22 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
||||
queryset=Region.objects.all(),
|
||||
required=False
|
||||
)
|
||||
site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region'
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
)
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF',
|
||||
display_field='display_name'
|
||||
label='VRF'
|
||||
)
|
||||
prefix_length = forms.IntegerField(
|
||||
min_value=PREFIX_LENGTH_MIN,
|
||||
@@ -547,8 +592,8 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
||||
class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
model = Prefix
|
||||
field_order = [
|
||||
'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'region_id', 'site_id',
|
||||
'role_id', 'tenant_group_id', 'tenant_id', 'is_pool',
|
||||
'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'region_id',
|
||||
'site_group_id', 'site_id', 'role_id', 'tenant_group_id', 'tenant_id', 'is_pool',
|
||||
]
|
||||
mask_length__lte = forms.IntegerField(
|
||||
widget=forms.HiddenInput()
|
||||
@@ -599,6 +644,11 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
@@ -632,7 +682,6 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
display_field='display_name',
|
||||
initial_params={
|
||||
'interfaces': '$interface'
|
||||
}
|
||||
@@ -662,8 +711,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF',
|
||||
display_field='display_name'
|
||||
label='VRF'
|
||||
)
|
||||
nat_region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -673,19 +721,27 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
'sites': '$nat_site'
|
||||
}
|
||||
)
|
||||
nat_site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label='Site group',
|
||||
initial_params={
|
||||
'sites': '$nat_site'
|
||||
}
|
||||
)
|
||||
nat_site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
label='Site',
|
||||
query_params={
|
||||
'region_id': '$nat_region'
|
||||
'region_id': '$nat_region',
|
||||
'group_id': '$nat_site_group',
|
||||
}
|
||||
)
|
||||
nat_rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
label='Rack',
|
||||
display_field='display_name',
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
@@ -695,7 +751,6 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
label='Device',
|
||||
display_field='display_name',
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'rack_id': '$nat_rack',
|
||||
@@ -717,14 +772,12 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
nat_vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF',
|
||||
display_field='display_name'
|
||||
label='VRF'
|
||||
)
|
||||
nat_inside = DynamicModelChoiceField(
|
||||
queryset=IPAddress.objects.all(),
|
||||
required=False,
|
||||
label='IP Address',
|
||||
display_field='address',
|
||||
query_params={
|
||||
'device_id': '$nat_device',
|
||||
'virtual_machine_id': '$nat_virtual_machine',
|
||||
@@ -833,8 +886,7 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF',
|
||||
display_field='display_name'
|
||||
label='VRF'
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
@@ -965,8 +1017,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF',
|
||||
display_field='display_name'
|
||||
label='VRF'
|
||||
)
|
||||
mask_length = forms.IntegerField(
|
||||
min_value=IPADDRESS_MASK_LENGTH_MIN,
|
||||
@@ -1089,11 +1140,54 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'locations': '$location'
|
||||
},
|
||||
query_params={
|
||||
'region_id': '$region'
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
)
|
||||
location = DynamicModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'racks': '$rack'
|
||||
},
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
}
|
||||
)
|
||||
rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'location_id': '$location',
|
||||
}
|
||||
)
|
||||
cluster_group = DynamicModelChoiceField(
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'clusters': '$cluster'
|
||||
}
|
||||
)
|
||||
cluster = DynamicModelChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'group_id': '$cluster_group',
|
||||
}
|
||||
)
|
||||
slug = SlugField()
|
||||
@@ -1101,9 +1195,43 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = [
|
||||
'region', 'site', 'name', 'slug', 'description',
|
||||
'name', 'slug', 'description', 'region', 'site_group', 'site', 'location', 'rack', 'cluster_group',
|
||||
'cluster',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
if instance is not None and instance.scope:
|
||||
if type(instance.scope) is Rack:
|
||||
initial['rack'] = instance.scope
|
||||
elif type(instance.scope) is Location:
|
||||
initial['location'] = instance.scope
|
||||
elif type(instance.scope) is Site:
|
||||
initial['site'] = instance.scope
|
||||
elif type(instance.scope) is SiteGroup:
|
||||
initial['site_group'] = instance.scope
|
||||
elif type(instance.scope) is Region:
|
||||
initial['region'] = instance.scope
|
||||
elif type(instance.scope) is Cluster:
|
||||
initial['cluster'] = instance.scope
|
||||
elif type(instance.scope) is ClusterGroup:
|
||||
initial['cluster_group'] = instance.scope
|
||||
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Assign scope object
|
||||
self.instance.scope = self.cleaned_data['rack'] or self.cleaned_data['location'] or \
|
||||
self.cleaned_data['site'] or self.cleaned_data['site_group'] or \
|
||||
self.cleaned_data['region'] or self.cleaned_data['cluster'] or \
|
||||
self.cleaned_data['cluster_group'] or None
|
||||
|
||||
|
||||
class VLANGroupCSVForm(CustomFieldModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
@@ -1119,21 +1247,50 @@ class VLANGroupCSVForm(CustomFieldModelCSVForm):
|
||||
fields = VLANGroup.csv_headers
|
||||
|
||||
|
||||
class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['site', 'description']
|
||||
|
||||
|
||||
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
region = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
sitegroup = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group')
|
||||
)
|
||||
site = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
location = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
label=_('Location')
|
||||
)
|
||||
rack = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
label=_('Rack')
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -1141,28 +1298,59 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||
#
|
||||
|
||||
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
# VLANGroup assignment fields
|
||||
scope_type = forms.ChoiceField(
|
||||
choices=(
|
||||
('', ''),
|
||||
('dcim.region', 'Region'),
|
||||
('dcim.sitegroup', 'Site group'),
|
||||
('dcim.site', 'Site'),
|
||||
('dcim.location', 'Location'),
|
||||
('dcim.rack', 'Rack'),
|
||||
('virtualization.clustergroup', 'Cluster group'),
|
||||
('virtualization.cluster', 'Cluster'),
|
||||
),
|
||||
required=False,
|
||||
widget=StaticSelect2,
|
||||
label='Group scope'
|
||||
)
|
||||
group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'scope_type': '$scope_type',
|
||||
},
|
||||
label='VLAN Group'
|
||||
)
|
||||
|
||||
# Site assignment fields
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
},
|
||||
label='Region'
|
||||
)
|
||||
sitegroup = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
},
|
||||
label='Site group'
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'region_id': '$region'
|
||||
}
|
||||
)
|
||||
group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
'region_id': '$region',
|
||||
'group_id': '$sitegroup',
|
||||
}
|
||||
)
|
||||
|
||||
# Other fields
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
required=False
|
||||
@@ -1177,11 +1365,6 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
fields = [
|
||||
'site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('VLAN', ('vid', 'name', 'status', 'role', 'description', 'tags')),
|
||||
('Assignment', ('region', 'site', 'group')),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
)
|
||||
help_texts = {
|
||||
'site': "Leave blank if this VLAN spans multiple sites",
|
||||
'group': "VLAN group (optional)",
|
||||
@@ -1233,15 +1416,6 @@ class VLANCSVForm(CustomFieldModelCSVForm):
|
||||
'name': 'VLAN name',
|
||||
}
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit vlan queryset by assigned group
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['group'].queryset = self.fields['group'].queryset.filter(**params)
|
||||
|
||||
|
||||
class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
@@ -1252,11 +1426,16 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
||||
queryset=Region.objects.all(),
|
||||
required=False
|
||||
)
|
||||
site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region'
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
)
|
||||
group = DynamicModelChoiceField(
|
||||
@@ -1292,7 +1471,9 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
||||
|
||||
class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
model = VLAN
|
||||
field_order = ['q', 'region_id', 'site_id', 'group_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id']
|
||||
field_order = [
|
||||
'q', 'region_id', 'site_group_id', 'site_id', 'group_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
@@ -1302,6 +1483,11 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
|
||||
36
netbox/ipam/migrations/0045_vlangroup_scope.py
Normal file
36
netbox/ipam/migrations/0045_vlangroup_scope.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('ipam', '0044_standardize_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='vlangroup',
|
||||
old_name='site',
|
||||
new_name='scope_id',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vlangroup',
|
||||
name='scope_id',
|
||||
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='vlangroup',
|
||||
name='scope_type',
|
||||
field=models.ForeignKey(blank=True, limit_choices_to=models.Q(model__in=['region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster']), null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='vlangroup',
|
||||
options={'ordering': ('name', 'pk'), 'verbose_name': 'VLAN group', 'verbose_name_plural': 'VLAN groups'},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='vlangroup',
|
||||
unique_together={('scope_type', 'scope_id', 'name'), ('scope_type', 'scope_id', 'slug')},
|
||||
),
|
||||
]
|
||||
@@ -6,10 +6,8 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import F
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.models import Device
|
||||
from extras.models import ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
from ipam.choices import *
|
||||
@@ -19,7 +17,6 @@ from ipam.managers import IPAddressManager
|
||||
from ipam.querysets import PrefixQuerySet
|
||||
from ipam.validators import DNSValidator
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import serialize_object
|
||||
from virtualization.models import VirtualMachine
|
||||
|
||||
|
||||
@@ -108,7 +105,6 @@ class Aggregate(PrimaryModel):
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
@@ -293,7 +289,6 @@ class Prefix(PrimaryModel):
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = PrefixQuerySet.as_manager()
|
||||
|
||||
@@ -565,7 +560,6 @@ class IPAddress(PrimaryModel):
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = IPAddressManager()
|
||||
|
||||
@@ -649,13 +643,7 @@ class IPAddress(PrimaryModel):
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Annotate the assigned object, if any
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
related_object=self.assigned_object,
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
return super().to_objectchange(action, related_object=self.assigned_object)
|
||||
|
||||
def to_csv(self):
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from extras.models import TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from ipam.choices import *
|
||||
from ipam.constants import *
|
||||
@@ -66,7 +64,6 @@ class Service(PrimaryModel):
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.models import Interface
|
||||
from extras.models import TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from ipam.choices import *
|
||||
from ipam.constants import *
|
||||
@@ -31,13 +31,23 @@ class VLANGroup(OrganizationalModel):
|
||||
slug = models.SlugField(
|
||||
max_length=100
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='vlan_groups',
|
||||
scope_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
on_delete=models.CASCADE,
|
||||
limit_choices_to=Q(
|
||||
model__in=['region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster']
|
||||
),
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
scope_id = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
scope = GenericForeignKey(
|
||||
ct_field='scope_type',
|
||||
fk_field='scope_id'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
@@ -45,13 +55,13 @@ class VLANGroup(OrganizationalModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'site', 'description']
|
||||
csv_headers = ['name', 'slug', 'scope_type', 'scope_id', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ('site', 'name', 'pk') # (site, name) may be non-unique
|
||||
ordering = ('name', 'pk') # Name may be non-unique
|
||||
unique_together = [
|
||||
['site', 'name'],
|
||||
['site', 'slug'],
|
||||
['scope_type', 'scope_id', 'name'],
|
||||
['scope_type', 'scope_id', 'slug'],
|
||||
]
|
||||
verbose_name = 'VLAN group'
|
||||
verbose_name_plural = 'VLAN groups'
|
||||
@@ -62,11 +72,21 @@ class VLANGroup(OrganizationalModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:vlangroup_vlans', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate scope assignment
|
||||
if self.scope_type and not self.scope_id:
|
||||
raise ValidationError("Cannot set scope_type without scope_id.")
|
||||
if self.scope_id and not self.scope_type:
|
||||
raise ValidationError("Cannot set scope_id without scope_type.")
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.site.name if self.site else None,
|
||||
f'{self.scope_type.app_label}.{self.scope_type.model}',
|
||||
self.scope_id,
|
||||
self.description,
|
||||
)
|
||||
|
||||
@@ -135,7 +155,6 @@ class VLAN(PrimaryModel):
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
@@ -162,10 +181,11 @@ class VLAN(PrimaryModel):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate VLAN group
|
||||
if self.group and self.group.site != self.site:
|
||||
# Validate VLAN group (if assigned)
|
||||
if self.group and self.site and self.group.scope != self.site:
|
||||
raise ValidationError({
|
||||
'group': "VLAN group must belong to the assigned site ({}).".format(self.site)
|
||||
'group': f"VLAN is assigned to group {self.group} (scope: {self.group.scope}); cannot also assign to "
|
||||
f"site {self.site}."
|
||||
})
|
||||
|
||||
def to_csv(self):
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from extras.models import TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from ipam.constants import *
|
||||
from netbox.models import PrimaryModel
|
||||
@@ -59,7 +57,6 @@ class VRF(PrimaryModel):
|
||||
related_name='exporting_vrfs',
|
||||
blank=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
@@ -116,7 +113,6 @@ class RouteTarget(PrimaryModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
|
||||
@@ -3,20 +3,16 @@ from django.utils.safestring import mark_safe
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import Interface
|
||||
from tenancy.tables import COL_TENANT
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, TagColumn, ToggleColumn,
|
||||
UtilizationColumn,
|
||||
)
|
||||
from virtualization.models import VMInterface
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
AVAILABLE_LABEL = mark_safe('<span class="label label-success">Available</span>')
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}—{% endif %}
|
||||
"""
|
||||
|
||||
PREFIX_LINK = """
|
||||
{% load helpers %}
|
||||
{% for i in record.parents|as_range %}
|
||||
@@ -37,7 +33,7 @@ IPADDRESS_LINK = """
|
||||
{% if record.pk %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
|
||||
{% elif perms.ipam.add_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}{% if prefix.tenant %}&tenant={{ prefix.tenant.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
|
||||
{% else %}
|
||||
{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
|
||||
{% endif %}
|
||||
@@ -50,8 +46,8 @@ IPADDRESS_ASSIGN_LINK = """
|
||||
VRF_LINK = """
|
||||
{% if record.vrf %}
|
||||
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
|
||||
{% elif prefix.vrf %}
|
||||
{{ prefix.vrf }}
|
||||
{% elif object.vrf %}
|
||||
<a href="{{ object.vrf.get_absolute_url }}">{{ object.vrf }}</a>
|
||||
{% else %}
|
||||
Global
|
||||
{% endif %}
|
||||
@@ -94,7 +90,7 @@ VLAN_ROLE_LINK = """
|
||||
VLANGROUP_ADD_VLAN = """
|
||||
{% with next_vid=record.get_next_available_vid %}
|
||||
{% if next_vid and perms.ipam.add_vlan %}
|
||||
<a href="{% url 'ipam:vlan_add' %}?site={{ record.site_id }}&group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-xs btn-success">
|
||||
<a href="{% url 'ipam:vlan_add' %}?group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-xs btn-success">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -109,16 +105,6 @@ VLAN_MEMBER_TAGGED = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
TENANT_LINK = """
|
||||
{% if record.tenant %}
|
||||
<a href="{{ record.tenant.get_absolute_url }}" title="{{ record.tenant.description }}">{{ record.tenant }}</a>
|
||||
{% elif record.vrf.tenant %}
|
||||
<a href="{{ record.vrf.tenant.get_absolute_url }}" title="{{ record.vrf.tenant.description }}">{{ record.vrf.tenant }}</a>*
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
# VRFs
|
||||
@@ -130,9 +116,7 @@ class VRFTable(BaseTable):
|
||||
rd = tables.Column(
|
||||
verbose_name='RD'
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
enforce_unique = BooleanColumn(
|
||||
verbose_name='Unique'
|
||||
)
|
||||
@@ -163,9 +147,7 @@ class VRFTable(BaseTable):
|
||||
class RouteTargetTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
tags = TagColumn(
|
||||
url_name='ipam:vrf_list'
|
||||
)
|
||||
@@ -208,9 +190,7 @@ class AggregateTable(BaseTable):
|
||||
prefix = tables.LinkColumn(
|
||||
verbose_name='Aggregate'
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=TENANT_LINK
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
date_added = tables.DateColumn(
|
||||
format="Y-m-d",
|
||||
verbose_name='Added'
|
||||
@@ -225,8 +205,8 @@ class AggregateDetailTable(AggregateTable):
|
||||
child_count = tables.Column(
|
||||
verbose_name='Prefixes'
|
||||
)
|
||||
utilization = tables.TemplateColumn(
|
||||
template_code=UTILIZATION_GRAPH,
|
||||
utilization = UtilizationColumn(
|
||||
accessor='get_utilization',
|
||||
orderable=False
|
||||
)
|
||||
tags = TagColumn(
|
||||
@@ -279,9 +259,7 @@ class PrefixTable(BaseTable):
|
||||
template_code=VRF_LINK,
|
||||
verbose_name='VRF'
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=TENANT_LINK
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@@ -308,13 +286,11 @@ class PrefixTable(BaseTable):
|
||||
|
||||
|
||||
class PrefixDetailTable(PrefixTable):
|
||||
utilization = tables.TemplateColumn(
|
||||
template_code=UTILIZATION_GRAPH,
|
||||
utilization = UtilizationColumn(
|
||||
accessor='get_utilization',
|
||||
orderable=False
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
tags = TagColumn(
|
||||
url_name='ipam:prefix_list'
|
||||
)
|
||||
@@ -347,9 +323,7 @@ class IPAddressTable(BaseTable):
|
||||
default=AVAILABLE_LABEL
|
||||
)
|
||||
role = ChoiceFieldColumn()
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=TENANT_LINK
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
assigned_object = tables.Column(
|
||||
linkify=True,
|
||||
orderable=False,
|
||||
@@ -379,9 +353,7 @@ class IPAddressDetailTable(IPAddressTable):
|
||||
orderable=False,
|
||||
verbose_name='NAT (Inside)'
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
assigned = BooleanColumn(
|
||||
accessor='assigned_object_id',
|
||||
verbose_name='Assigned'
|
||||
@@ -428,8 +400,9 @@ class InterfaceIPAddressTable(BaseTable):
|
||||
verbose_name='VRF'
|
||||
)
|
||||
status = ChoiceFieldColumn()
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=TENANT_LINK
|
||||
tenant = TenantColumn()
|
||||
actions = ButtonsColumn(
|
||||
model=IPAddress
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@@ -444,7 +417,7 @@ class InterfaceIPAddressTable(BaseTable):
|
||||
class VLANGroupTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(linkify=True)
|
||||
site = tables.Column(
|
||||
scope = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
vlan_count = LinkedCountColumn(
|
||||
@@ -459,8 +432,8 @@ class VLANGroupTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VLANGroup
|
||||
fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'site', 'vlan_count', 'description', 'actions')
|
||||
fields = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions')
|
||||
|
||||
|
||||
#
|
||||
@@ -480,9 +453,7 @@ class VLANTable(BaseTable):
|
||||
viewname='ipam:vlangroup_vlans',
|
||||
args=[Accessor('group__pk')]
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
status = ChoiceFieldColumn(
|
||||
default=AVAILABLE_LABEL
|
||||
)
|
||||
@@ -504,9 +475,7 @@ class VLANDetailTable(VLANTable):
|
||||
orderable=False,
|
||||
verbose_name='Prefixes'
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
tags = TagColumn(
|
||||
url_name='ipam:vlan_list'
|
||||
)
|
||||
@@ -564,9 +533,7 @@ class InterfaceVLANTable(BaseTable):
|
||||
accessor=Accessor('group__name'),
|
||||
verbose_name='Group'
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
status = ChoiceFieldColumn()
|
||||
role = tables.TemplateColumn(
|
||||
template_code=VLAN_ROLE_LINK
|
||||
|
||||
@@ -22,7 +22,7 @@ class AppTest(APITestCase):
|
||||
|
||||
class VRFTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VRF
|
||||
brief_fields = ['display_name', 'id', 'name', 'prefix_count', 'rd', 'url']
|
||||
brief_fields = ['display', 'display_name', 'id', 'name', 'prefix_count', 'rd', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'VRF 4',
|
||||
@@ -54,7 +54,7 @@ class VRFTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class RouteTargetTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RouteTarget
|
||||
brief_fields = ['id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': '65000:1004',
|
||||
@@ -83,7 +83,7 @@ class RouteTargetTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class RIRTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RIR
|
||||
brief_fields = ['aggregate_count', 'id', 'name', 'slug', 'url']
|
||||
brief_fields = ['aggregate_count', 'display', 'id', 'name', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'RIR 4',
|
||||
@@ -115,7 +115,7 @@ class RIRTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class AggregateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Aggregate
|
||||
brief_fields = ['family', 'id', 'prefix', 'url']
|
||||
brief_fields = ['display', 'family', 'id', 'prefix', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -154,7 +154,7 @@ class AggregateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class RoleTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Role
|
||||
brief_fields = ['id', 'name', 'prefix_count', 'slug', 'url', 'vlan_count']
|
||||
brief_fields = ['display', 'id', 'name', 'prefix_count', 'slug', 'url', 'vlan_count']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Role 4',
|
||||
@@ -186,7 +186,7 @@ class RoleTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class PrefixTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Prefix
|
||||
brief_fields = ['family', 'id', 'prefix', 'url']
|
||||
brief_fields = ['display', 'family', 'id', 'prefix', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'prefix': '192.168.4.0/24',
|
||||
@@ -360,7 +360,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class IPAddressTest(APIViewTestCases.APIViewTestCase):
|
||||
model = IPAddress
|
||||
brief_fields = ['address', 'family', 'id', 'url']
|
||||
brief_fields = ['address', 'display', 'family', 'id', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'address': '192.168.0.4/24',
|
||||
@@ -389,7 +389,7 @@ class IPAddressTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class VLANGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VLANGroup
|
||||
brief_fields = ['id', 'name', 'slug', 'url', 'vlan_count']
|
||||
brief_fields = ['display', 'id', 'name', 'slug', 'url', 'vlan_count']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'VLAN Group 4',
|
||||
@@ -421,7 +421,7 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class VLANTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VLAN
|
||||
brief_fields = ['display_name', 'id', 'name', 'url', 'vid']
|
||||
brief_fields = ['display', 'display_name', 'id', 'name', 'url', 'vid']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@@ -481,7 +481,7 @@ class VLANTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class ServiceTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Service
|
||||
brief_fields = ['id', 'name', 'ports', 'protocol', 'url']
|
||||
brief_fields = ['display', 'id', 'name', 'ports', 'protocol', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Region, Site
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Rack, Region, Site, SiteGroup
|
||||
from ipam.choices import *
|
||||
from ipam.filters import *
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
||||
from virtualization.models import Cluster, ClusterType, VirtualMachine, VMInterface
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
|
||||
|
||||
@@ -343,14 +343,21 @@ class PrefixTestCase(TestCase):
|
||||
Region(name='Test Region 2', slug='test-region-2'),
|
||||
Region(name='Test Region 3', slug='test-region-3'),
|
||||
)
|
||||
# Can't use bulk_create for models with MPTT fields
|
||||
for r in regions:
|
||||
r.save()
|
||||
|
||||
site_groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for site_group in site_groups:
|
||||
site_group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0]),
|
||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1]),
|
||||
Site(name='Test Site 3', slug='test-site-3', region=regions[2]),
|
||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
|
||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
|
||||
Site(name='Test Site 3', slug='test-site-3', region=regions[2], group=site_groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@@ -422,6 +429,11 @@ class PrefixTestCase(TestCase):
|
||||
params = {'family': '6'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
|
||||
def test_prefix(self):
|
||||
prefixes = Prefix.objects.all()[:2]
|
||||
params = {'prefix': [prefixes[0].prefix, prefixes[1].prefix]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_is_pool(self):
|
||||
params = {'is_pool': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -468,6 +480,13 @@ class PrefixTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -696,27 +715,39 @@ class VLANGroupTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
regions = (
|
||||
Region(name='Test Region 1', slug='test-region-1'),
|
||||
Region(name='Test Region 2', slug='test-region-2'),
|
||||
Region(name='Test Region 3', slug='test-region-3'),
|
||||
)
|
||||
# Can't use bulk_create for models with MPTT fields
|
||||
for r in regions:
|
||||
r.save()
|
||||
region = Region(name='Region 1', slug='region-1')
|
||||
region.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0]),
|
||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1]),
|
||||
Site(name='Test Site 3', slug='test-site-3', region=regions[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
sitegroup = SiteGroup(name='Site Group 1', slug='site-group-1')
|
||||
sitegroup.save()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
location = Location(name='Location 1', slug='location-1', site=site)
|
||||
location.save()
|
||||
|
||||
rack = Rack(name='Rack 1', site=site)
|
||||
rack.save()
|
||||
|
||||
clustertype = ClusterType(name='Cluster Type 1', slug='cluster-type-1')
|
||||
clustertype.save()
|
||||
|
||||
clustergroup = ClusterGroup(name='Cluster Group 1', slug='cluster-group-1')
|
||||
clustergroup.save()
|
||||
|
||||
cluster = Cluster(name='Cluster 1', type=clustertype)
|
||||
cluster.save()
|
||||
|
||||
vlan_groups = (
|
||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=sites[0], description='A'),
|
||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=sites[1], description='B'),
|
||||
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=sites[2], description='C'),
|
||||
VLANGroup(name='VLAN Group 4', slug='vlan-group-4', site=None),
|
||||
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 8', slug='vlan-group-8'),
|
||||
)
|
||||
VLANGroup.objects.bulk_create(vlan_groups)
|
||||
|
||||
@@ -737,18 +768,32 @@ class VLANGroupTestCase(TestCase):
|
||||
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]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'region': Region.objects.first().pk}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_sitegroup(self):
|
||||
params = {'sitegroup': SiteGroup.objects.first().pk}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site': Site.objects.first().pk}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_location(self):
|
||||
params = {'location': Location.objects.first().pk}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_rack(self):
|
||||
params = {'rack': Rack.objects.first().pk}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_clustergroup(self):
|
||||
params = {'clustergroup': ClusterGroup.objects.first().pk}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_cluster(self):
|
||||
params = {'cluster': Cluster.objects.first().pk}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class VLANTestCase(TestCase):
|
||||
@@ -763,14 +808,21 @@ class VLANTestCase(TestCase):
|
||||
Region(name='Test Region 2', slug='test-region-2'),
|
||||
Region(name='Test Region 3', slug='test-region-3'),
|
||||
)
|
||||
# Can't use bulk_create for models with MPTT fields
|
||||
for r in regions:
|
||||
r.save()
|
||||
|
||||
site_groups = (
|
||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||
)
|
||||
for site_group in site_groups:
|
||||
site_group.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0]),
|
||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1]),
|
||||
Site(name='Test Site 3', slug='test-site-3', region=regions[2]),
|
||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
|
||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
|
||||
Site(name='Test Site 3', slug='test-site-3', region=regions[2], group=site_groups[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
@@ -782,9 +834,9 @@ class VLANTestCase(TestCase):
|
||||
Role.objects.bulk_create(roles)
|
||||
|
||||
groups = (
|
||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=sites[0]),
|
||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=sites[1]),
|
||||
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=None),
|
||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', scope=sites[0]),
|
||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', scope=sites[1]),
|
||||
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=None),
|
||||
)
|
||||
VLANGroup.objects.bulk_create(groups)
|
||||
|
||||
@@ -832,6 +884,13 @@ class VLANTestCase(TestCase):
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
|
||||
@@ -118,6 +118,10 @@ class RIRTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
"RIR 6,rir-6,Sixth RIR",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class AggregateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Aggregate
|
||||
@@ -187,6 +191,10 @@ class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
"Role 6,role-6,1000",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Prefix
|
||||
@@ -306,18 +314,22 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
VLANGroup.objects.bulk_create([
|
||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=site),
|
||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=site),
|
||||
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=site),
|
||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', scope=sites[0]),
|
||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', scope=sites[0]),
|
||||
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=sites[0]),
|
||||
])
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'VLAN Group X',
|
||||
'slug': 'vlan-group-x',
|
||||
'site': site.pk,
|
||||
'site': sites[1].pk,
|
||||
'description': 'A new VLAN group',
|
||||
}
|
||||
|
||||
@@ -328,6 +340,10 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
"VLAN Group 6,vlan-group-6,Sixth VLAN group",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VLAN
|
||||
@@ -342,8 +358,8 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
vlangroups = (
|
||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=sites[0]),
|
||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=sites[1]),
|
||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', scope=sites[0]),
|
||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', scope=sites[1]),
|
||||
)
|
||||
VLANGroup.objects.bulk_create(vlangroups)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.urls import path
|
||||
|
||||
from extras.views import ObjectChangeLogView
|
||||
from extras.views import ObjectChangeLogView, ObjectJournalView
|
||||
from . import views
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
@@ -17,6 +17,7 @@ urlpatterns = [
|
||||
path('vrfs/<int:pk>/edit/', views.VRFEditView.as_view(), name='vrf_edit'),
|
||||
path('vrfs/<int:pk>/delete/', views.VRFDeleteView.as_view(), name='vrf_delete'),
|
||||
path('vrfs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}),
|
||||
path('vrfs/<int:pk>/journal/', ObjectJournalView.as_view(), name='vrf_journal', kwargs={'model': VRF}),
|
||||
|
||||
# Route targets
|
||||
path('route-targets/', views.RouteTargetListView.as_view(), name='routetarget_list'),
|
||||
@@ -28,11 +29,13 @@ urlpatterns = [
|
||||
path('route-targets/<int:pk>/edit/', views.RouteTargetEditView.as_view(), name='routetarget_edit'),
|
||||
path('route-targets/<int:pk>/delete/', views.RouteTargetDeleteView.as_view(), name='routetarget_delete'),
|
||||
path('route-targets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='routetarget_changelog', kwargs={'model': RouteTarget}),
|
||||
path('route-targets/<int:pk>/journal/', ObjectJournalView.as_view(), name='routetarget_journal', kwargs={'model': RouteTarget}),
|
||||
|
||||
# RIRs
|
||||
path('rirs/', views.RIRListView.as_view(), name='rir_list'),
|
||||
path('rirs/add/', views.RIREditView.as_view(), name='rir_add'),
|
||||
path('rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'),
|
||||
path('rirs/edit/', views.RIRBulkEditView.as_view(), name='rir_bulk_edit'),
|
||||
path('rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
|
||||
path('rirs/<int:pk>/edit/', views.RIREditView.as_view(), name='rir_edit'),
|
||||
path('rirs/<int:pk>/delete/', views.RIRDeleteView.as_view(), name='rir_delete'),
|
||||
@@ -48,11 +51,13 @@ urlpatterns = [
|
||||
path('aggregates/<int:pk>/edit/', views.AggregateEditView.as_view(), name='aggregate_edit'),
|
||||
path('aggregates/<int:pk>/delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
|
||||
path('aggregates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}),
|
||||
path('aggregates/<int:pk>/journal/', ObjectJournalView.as_view(), name='aggregate_journal', kwargs={'model': Aggregate}),
|
||||
|
||||
# Roles
|
||||
path('roles/', views.RoleListView.as_view(), name='role_list'),
|
||||
path('roles/add/', views.RoleEditView.as_view(), name='role_add'),
|
||||
path('roles/import/', views.RoleBulkImportView.as_view(), name='role_import'),
|
||||
path('roles/edit/', views.RoleBulkEditView.as_view(), name='role_bulk_edit'),
|
||||
path('roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
|
||||
path('roles/<int:pk>/edit/', views.RoleEditView.as_view(), name='role_edit'),
|
||||
path('roles/<int:pk>/delete/', views.RoleDeleteView.as_view(), name='role_delete'),
|
||||
@@ -68,6 +73,7 @@ urlpatterns = [
|
||||
path('prefixes/<int:pk>/edit/', views.PrefixEditView.as_view(), name='prefix_edit'),
|
||||
path('prefixes/<int:pk>/delete/', views.PrefixDeleteView.as_view(), name='prefix_delete'),
|
||||
path('prefixes/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}),
|
||||
path('prefixes/<int:pk>/journal/', ObjectJournalView.as_view(), name='prefix_journal', kwargs={'model': Prefix}),
|
||||
path('prefixes/<int:pk>/prefixes/', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'),
|
||||
path('prefixes/<int:pk>/ip-addresses/', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
|
||||
|
||||
@@ -79,6 +85,7 @@ urlpatterns = [
|
||||
path('ip-addresses/edit/', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
|
||||
path('ip-addresses/delete/', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
|
||||
path('ip-addresses/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}),
|
||||
path('ip-addresses/<int:pk>/journal/', ObjectJournalView.as_view(), name='ipaddress_journal', kwargs={'model': IPAddress}),
|
||||
path('ip-addresses/assign/', views.IPAddressAssignView.as_view(), name='ipaddress_assign'),
|
||||
path('ip-addresses/<int:pk>/', views.IPAddressView.as_view(), name='ipaddress'),
|
||||
path('ip-addresses/<int:pk>/edit/', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
|
||||
@@ -88,6 +95,7 @@ urlpatterns = [
|
||||
path('vlan-groups/', views.VLANGroupListView.as_view(), name='vlangroup_list'),
|
||||
path('vlan-groups/add/', views.VLANGroupEditView.as_view(), name='vlangroup_add'),
|
||||
path('vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
|
||||
path('vlan-groups/edit/', views.VLANGroupBulkEditView.as_view(), name='vlangroup_bulk_edit'),
|
||||
path('vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
|
||||
path('vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
|
||||
path('vlan-groups/<int:pk>/delete/', views.VLANGroupDeleteView.as_view(), name='vlangroup_delete'),
|
||||
@@ -106,6 +114,7 @@ urlpatterns = [
|
||||
path('vlans/<int:pk>/edit/', views.VLANEditView.as_view(), name='vlan_edit'),
|
||||
path('vlans/<int:pk>/delete/', views.VLANDeleteView.as_view(), name='vlan_delete'),
|
||||
path('vlans/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),
|
||||
path('vlans/<int:pk>/journal/', ObjectJournalView.as_view(), name='vlan_journal', kwargs={'model': VLAN}),
|
||||
|
||||
# Services
|
||||
path('services/', views.ServiceListView.as_view(), name='service_list'),
|
||||
@@ -116,5 +125,6 @@ urlpatterns = [
|
||||
path('services/<int:pk>/edit/', views.ServiceEditView.as_view(), name='service_edit'),
|
||||
path('services/<int:pk>/delete/', views.ServiceDeleteView.as_view(), name='service_delete'),
|
||||
path('services/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}),
|
||||
path('services/<int:pk>/journal/', ObjectJournalView.as_view(), name='service_journal', kwargs={'model': Service}),
|
||||
|
||||
]
|
||||
|
||||
@@ -30,6 +30,7 @@ class VRFView(generic.ObjectView):
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
prefix_count = Prefix.objects.restrict(request.user, 'view').filter(vrf=instance).count()
|
||||
ipaddress_count = IPAddress.objects.restrict(request.user, 'view').filter(vrf=instance).count()
|
||||
|
||||
import_targets_table = tables.RouteTargetTable(
|
||||
instance.import_targets.prefetch_related('tenant'),
|
||||
@@ -42,6 +43,7 @@ class VRFView(generic.ObjectView):
|
||||
|
||||
return {
|
||||
'prefix_count': prefix_count,
|
||||
'ipaddress_count': ipaddress_count,
|
||||
'import_targets_table': import_targets_table,
|
||||
'export_targets_table': export_targets_table,
|
||||
}
|
||||
@@ -162,6 +164,15 @@ class RIRBulkImportView(generic.BulkImportView):
|
||||
table = tables.RIRTable
|
||||
|
||||
|
||||
class RIRBulkEditView(generic.BulkEditView):
|
||||
queryset = RIR.objects.annotate(
|
||||
aggregate_count=count_related(Aggregate, 'rir')
|
||||
)
|
||||
filterset = filters.RIRFilterSet
|
||||
table = tables.RIRTable
|
||||
form = forms.RIRBulkEditForm
|
||||
|
||||
|
||||
class RIRBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RIR.objects.annotate(
|
||||
aggregate_count=count_related(Aggregate, 'rir')
|
||||
@@ -296,6 +307,13 @@ class RoleBulkImportView(generic.BulkImportView):
|
||||
table = tables.RoleTable
|
||||
|
||||
|
||||
class RoleBulkEditView(generic.BulkEditView):
|
||||
queryset = Role.objects.all()
|
||||
filterset = filters.RoleFilterSet
|
||||
table = tables.RoleTable
|
||||
form = forms.RoleBulkEditForm
|
||||
|
||||
|
||||
class RoleBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Role.objects.all()
|
||||
table = tables.RoleTable
|
||||
@@ -629,7 +647,7 @@ class IPAddressBulkDeleteView(generic.BulkDeleteView):
|
||||
#
|
||||
|
||||
class VLANGroupListView(generic.ObjectListView):
|
||||
queryset = VLANGroup.objects.prefetch_related('site').annotate(
|
||||
queryset = VLANGroup.objects.annotate(
|
||||
vlan_count=count_related(VLAN, 'group')
|
||||
)
|
||||
filterset = filters.VLANGroupFilterSet
|
||||
@@ -640,6 +658,7 @@ class VLANGroupListView(generic.ObjectListView):
|
||||
class VLANGroupEditView(generic.ObjectEditView):
|
||||
queryset = VLANGroup.objects.all()
|
||||
model_form = forms.VLANGroupForm
|
||||
template_name = 'ipam/vlangroup_edit.html'
|
||||
|
||||
|
||||
class VLANGroupDeleteView(generic.ObjectDeleteView):
|
||||
@@ -652,8 +671,17 @@ class VLANGroupBulkImportView(generic.BulkImportView):
|
||||
table = tables.VLANGroupTable
|
||||
|
||||
|
||||
class VLANGroupBulkEditView(generic.BulkEditView):
|
||||
queryset = VLANGroup.objects.annotate(
|
||||
vlan_count=count_related(VLAN, 'group')
|
||||
)
|
||||
filterset = filters.VLANGroupFilterSet
|
||||
table = tables.VLANGroupTable
|
||||
form = forms.VLANGroupBulkEditForm
|
||||
|
||||
|
||||
class VLANGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VLANGroup.objects.prefetch_related('site').annotate(
|
||||
queryset = VLANGroup.objects.annotate(
|
||||
vlan_count=count_related(VLAN, 'group')
|
||||
)
|
||||
filterset = filters.VLANGroupFilterSet
|
||||
@@ -766,6 +794,7 @@ class VLANVMInterfacesView(generic.ObjectView):
|
||||
class VLANEditView(generic.ObjectEditView):
|
||||
queryset = VLAN.objects.all()
|
||||
model_form = forms.VLANForm
|
||||
template_name = 'ipam/vlan_edit.html'
|
||||
|
||||
|
||||
class VLANDeleteView(generic.ObjectDeleteView):
|
||||
|
||||
@@ -6,11 +6,18 @@ from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CreateOnlyDefault
|
||||
|
||||
from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues
|
||||
from extras.models import CustomField
|
||||
from extras.models import CustomField, Tag
|
||||
from utilities.utils import dict_to_filter_params
|
||||
|
||||
|
||||
class ValidatedModelSerializer(serializers.ModelSerializer):
|
||||
class BaseModelSerializer(serializers.ModelSerializer):
|
||||
display = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_display(self, obj):
|
||||
return str(obj)
|
||||
|
||||
|
||||
class ValidatedModelSerializer(BaseModelSerializer):
|
||||
"""
|
||||
Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during
|
||||
validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
|
||||
@@ -70,19 +77,14 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
||||
instance.custom_fields[field.name] = instance.cf.get(field.name)
|
||||
|
||||
|
||||
class OrganizationalModelSerializer(CustomFieldModelSerializer):
|
||||
pass
|
||||
#
|
||||
# Nested serializers
|
||||
#
|
||||
|
||||
|
||||
class NestedGroupModelSerializer(CustomFieldModelSerializer):
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
|
||||
class WritableNestedSerializer(serializers.ModelSerializer):
|
||||
class WritableNestedSerializer(BaseModelSerializer):
|
||||
"""
|
||||
Returns a nested representation of an object on read, but accepts only a primary key on write.
|
||||
"""
|
||||
|
||||
def to_internal_value(self, data):
|
||||
|
||||
if data is None:
|
||||
@@ -128,5 +130,71 @@ class WritableNestedSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Nested tags serialization
|
||||
#
|
||||
|
||||
# Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers
|
||||
class NestedTagSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'color']
|
||||
|
||||
|
||||
#
|
||||
# Base model serializers
|
||||
#
|
||||
|
||||
class OrganizationalModelSerializer(CustomFieldModelSerializer):
|
||||
"""
|
||||
Adds support for custom fields.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PrimaryModelSerializer(CustomFieldModelSerializer):
|
||||
"""
|
||||
Adds support for custom fields and tags.
|
||||
"""
|
||||
tags = NestedTagSerializer(many=True, required=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
tags = validated_data.pop('tags', None)
|
||||
instance = super().create(validated_data)
|
||||
|
||||
if tags is not None:
|
||||
return self._save_tags(instance, tags)
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
tags = validated_data.pop('tags', None)
|
||||
|
||||
# Cache tags on instance for change logging
|
||||
instance._tags = tags or []
|
||||
|
||||
instance = super().update(instance, validated_data)
|
||||
|
||||
if tags is not None:
|
||||
return self._save_tags(instance, tags)
|
||||
return instance
|
||||
|
||||
def _save_tags(self, instance, tags):
|
||||
if tags:
|
||||
instance.tags.set(*[t.name for t in tags])
|
||||
else:
|
||||
instance.tags.clear()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class NestedGroupModelSerializer(CustomFieldModelSerializer):
|
||||
"""
|
||||
Extends OrganizationalModelSerializer to include MPTT support.
|
||||
"""
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
|
||||
class BulkOperationSerializer(serializers.Serializer):
|
||||
id = serializers.IntegerField()
|
||||
|
||||
@@ -76,6 +76,8 @@ class BulkUpdateModelMixin:
|
||||
data_list = []
|
||||
for obj in objects:
|
||||
data = update_data.get(obj.id)
|
||||
if hasattr(obj, 'snapshot'):
|
||||
obj.snapshot()
|
||||
serializer = self.get_serializer(obj, data=data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
@@ -113,6 +115,8 @@ class BulkDestroyModelMixin:
|
||||
def perform_bulk_destroy(self, objects):
|
||||
with transaction.atomic():
|
||||
for obj in objects:
|
||||
if hasattr(obj, 'snapshot'):
|
||||
obj.snapshot()
|
||||
self.perform_destroy(obj)
|
||||
|
||||
|
||||
@@ -127,6 +131,16 @@ class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ModelViewSet_):
|
||||
brief = False
|
||||
brief_prefetch_fields = []
|
||||
|
||||
def get_object_with_snapshot(self):
|
||||
"""
|
||||
Save a pre-change snapshot of the object immediately after retrieving it. This snapshot will be used to
|
||||
record the "before" data in the changelog.
|
||||
"""
|
||||
obj = super().get_object()
|
||||
if hasattr(obj, 'snapshot'):
|
||||
obj.snapshot()
|
||||
return obj
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
# If a list of objects has been provided, initialize the serializer with many=True
|
||||
@@ -221,6 +235,11 @@ class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ModelViewSet_):
|
||||
except ObjectDoesNotExist:
|
||||
raise PermissionDenied()
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
# Hotwire get_object() to ensure we save a pre-change snapshot
|
||||
self.get_object = self.get_object_with_snapshot
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
model = self.queryset.model
|
||||
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
||||
@@ -234,6 +253,11 @@ class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ModelViewSet_):
|
||||
except ObjectDoesNotExist:
|
||||
raise PermissionDenied()
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
# Hotwire get_object() to ensure we save a pre-change snapshot
|
||||
self.get_object = self.get_object_with_snapshot
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
model = self.queryset.model
|
||||
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
||||
|
||||
@@ -6,12 +6,12 @@ from circuits.filters import CircuitFilterSet, ProviderFilterSet
|
||||
from circuits.models import Circuit, Provider
|
||||
from circuits.tables import CircuitTable, ProviderTable
|
||||
from dcim.filters import (
|
||||
CableFilterSet, DeviceFilterSet, DeviceTypeFilterSet, PowerFeedFilterSet, RackFilterSet, RackGroupFilterSet,
|
||||
CableFilterSet, DeviceFilterSet, DeviceTypeFilterSet, PowerFeedFilterSet, RackFilterSet, LocationFilterSet,
|
||||
SiteFilterSet, VirtualChassisFilterSet,
|
||||
)
|
||||
from dcim.models import Cable, Device, DeviceType, PowerFeed, Rack, RackGroup, Site, VirtualChassis
|
||||
from dcim.models import Cable, Device, DeviceType, PowerFeed, Rack, Location, Site, VirtualChassis
|
||||
from dcim.tables import (
|
||||
CableTable, DeviceTable, DeviceTypeTable, PowerFeedTable, RackTable, RackGroupTable, SiteTable,
|
||||
CableTable, DeviceTable, DeviceTypeTable, PowerFeedTable, RackTable, LocationTable, SiteTable,
|
||||
VirtualChassisTable,
|
||||
)
|
||||
from ipam.filters import AggregateFilterSet, IPAddressFilterSet, PrefixFilterSet, VLANFilterSet, VRFFilterSet
|
||||
@@ -55,22 +55,22 @@ SEARCH_TYPES = OrderedDict((
|
||||
'url': 'dcim:site_list',
|
||||
}),
|
||||
('rack', {
|
||||
'queryset': Rack.objects.prefetch_related('site', 'group', 'tenant', 'role'),
|
||||
'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'role'),
|
||||
'filterset': RackFilterSet,
|
||||
'table': RackTable,
|
||||
'url': 'dcim:rack_list',
|
||||
}),
|
||||
('rackgroup', {
|
||||
'queryset': RackGroup.objects.add_related_count(
|
||||
RackGroup.objects.all(),
|
||||
('location', {
|
||||
'queryset': Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Rack,
|
||||
'group',
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site'),
|
||||
'filterset': RackGroupFilterSet,
|
||||
'table': RackGroupTable,
|
||||
'url': 'dcim:rackgroup_list',
|
||||
'filterset': LocationFilterSet,
|
||||
'table': LocationTable,
|
||||
'url': 'dcim:location_list',
|
||||
}),
|
||||
('devicetype', {
|
||||
'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||
|
||||
@@ -11,7 +11,7 @@ OBJ_TYPE_CHOICES = (
|
||||
('DCIM', (
|
||||
('site', 'Sites'),
|
||||
('rack', 'Racks'),
|
||||
('rackgroup', 'Rack Groups'),
|
||||
('location', 'Locations'),
|
||||
('devicetype', 'Device Types'),
|
||||
('device', 'Devices'),
|
||||
('virtualchassis', 'Virtual Chassis'),
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from extras.choices import ObjectChangeActionChoices
|
||||
from utilities.mptt import TreeManager
|
||||
from utilities.utils import serialize_object
|
||||
|
||||
__all__ = (
|
||||
'BigIDModel',
|
||||
'ChangeLoggingMixin',
|
||||
'CustomFieldsMixin',
|
||||
'ChangeLoggedModel',
|
||||
'NestedGroupModel',
|
||||
'OrganizationalModel',
|
||||
'PrimaryModel',
|
||||
@@ -40,18 +43,32 @@ class ChangeLoggingMixin(models.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def to_objectchange(self, action):
|
||||
def snapshot(self):
|
||||
"""
|
||||
Save a snapshot of the object's current state in preparation for modification.
|
||||
"""
|
||||
logger = logging.getLogger('netbox')
|
||||
logger.debug(f"Taking a snapshot of {self}")
|
||||
self._prechange_snapshot = serialize_object(self)
|
||||
|
||||
def to_objectchange(self, action, related_object=None):
|
||||
"""
|
||||
Return a new ObjectChange representing a change made to this object. This will typically be called automatically
|
||||
by ChangeLoggingMiddleware.
|
||||
"""
|
||||
from extras.models import ObjectChange
|
||||
return ObjectChange(
|
||||
objectchange = ObjectChange(
|
||||
changed_object=self,
|
||||
related_object=related_object,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
object_data=serialize_object(self)
|
||||
action=action
|
||||
)
|
||||
if hasattr(self, '_prechange_snapshot'):
|
||||
objectchange.prechange_data = self._prechange_snapshot
|
||||
if action in (ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE):
|
||||
objectchange.postchange_data = serialize_object(self)
|
||||
|
||||
return objectchange
|
||||
|
||||
|
||||
class CustomFieldsMixin(models.Model):
|
||||
@@ -121,12 +138,26 @@ class BigIDModel(models.Model):
|
||||
abstract = True
|
||||
|
||||
|
||||
class ChangeLoggedModel(ChangeLoggingMixin, BigIDModel):
|
||||
"""
|
||||
Base model for all objects which support change logging.
|
||||
"""
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, BigIDModel):
|
||||
"""
|
||||
Primary models represent real objects within the infrastructure being modeled.
|
||||
"""
|
||||
# TODO
|
||||
# tags = TaggableManager(through=TaggedItem)
|
||||
journal_entries = GenericRelation(
|
||||
to='extras.JournalEntry',
|
||||
object_id_field='assigned_object_id',
|
||||
content_type_field='assigned_object_type'
|
||||
)
|
||||
tags = TaggableManager(
|
||||
through='extras.TaggedItem'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -164,16 +195,6 @@ class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, BigIDModel, MPTTMo
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Remove MPTT-internal fields
|
||||
from extras.models import ObjectChange
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
|
||||
)
|
||||
|
||||
|
||||
class OrganizationalModel(ChangeLoggingMixin, CustomFieldsMixin, BigIDModel):
|
||||
"""
|
||||
|
||||
@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '2.10.6-dev'
|
||||
VERSION = '2.11-beta1'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
@@ -29,6 +29,12 @@ if platform.python_version_tuple() < ('3', '6'):
|
||||
raise RuntimeError(
|
||||
"NetBox requires Python 3.6 or higher (current: Python {})".format(platform.python_version())
|
||||
)
|
||||
# TODO: Remove in NetBox v2.12
|
||||
if platform.python_version_tuple() < ('3', '7'):
|
||||
warnings.warn(
|
||||
"Support for Python 3.6 will be dropped in NetBox v2.12. Please upgrade to Python 3.7 or later at your "
|
||||
"earliest convenience."
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -429,7 +435,7 @@ CACHEOPS = {
|
||||
'circuits.*': {'ops': 'all'},
|
||||
'dcim.inventoryitem': None, # MPTT models are exempt due to raw SQL
|
||||
'dcim.region': None, # MPTT models are exempt due to raw SQL
|
||||
'dcim.rackgroup': None, # MPTT models are exempt due to raw SQL
|
||||
'dcim.location': None, # MPTT models are exempt due to raw SQL
|
||||
'dcim.*': {'ops': 'all'},
|
||||
'ipam.*': {'ops': 'all'},
|
||||
'extras.*': {'ops': 'all'},
|
||||
|
||||
@@ -15,6 +15,7 @@ from django.utils.http import is_safe_url
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
from django_tables2 import RequestConfig
|
||||
from django_tables2.export import TableExport
|
||||
|
||||
from extras.models import CustomField, ExportTemplate
|
||||
from utilities.error_handlers import handle_protectederror
|
||||
@@ -137,32 +138,35 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
||||
if self.filterset:
|
||||
self.queryset = self.filterset(request.GET, self.queryset).qs
|
||||
|
||||
# Check for export template rendering
|
||||
if request.GET.get('export'):
|
||||
et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET.get('export'))
|
||||
try:
|
||||
return et.render_to_response(self.queryset)
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
request,
|
||||
"There was an error rendering the selected export template ({}): {}".format(
|
||||
et.name, e
|
||||
# Check for export rendering (except for table-based)
|
||||
if 'export' in request.GET and request.GET['export'] != 'table':
|
||||
|
||||
# An export template has been specified
|
||||
if request.GET['export']:
|
||||
et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export'])
|
||||
try:
|
||||
return et.render_to_response(self.queryset)
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
request,
|
||||
"There was an error rendering the selected export template ({}): {}".format(
|
||||
et.name, e
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Check for YAML export support
|
||||
elif 'export' in request.GET and hasattr(model, 'to_yaml'):
|
||||
response = HttpResponse(self.queryset_to_yaml(), content_type='text/yaml')
|
||||
filename = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural)
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||
return response
|
||||
# Check for YAML export support
|
||||
elif hasattr(model, 'to_yaml'):
|
||||
response = HttpResponse(self.queryset_to_yaml(), content_type='text/yaml')
|
||||
filename = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural)
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||
return response
|
||||
|
||||
# Fall back to built-in CSV formatting if export requested but no template specified
|
||||
elif 'export' in request.GET and hasattr(model, 'to_csv'):
|
||||
response = HttpResponse(self.queryset_to_csv(), content_type='text/csv')
|
||||
filename = 'netbox_{}.csv'.format(self.queryset.model._meta.verbose_name_plural)
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||
return response
|
||||
# Fall back to built-in CSV formatting if export requested but no template specified
|
||||
elif 'export' in request.GET and hasattr(model, 'to_csv'):
|
||||
response = HttpResponse(self.queryset_to_csv(), content_type='text/csv')
|
||||
filename = 'netbox_{}.csv'.format(self.queryset.model._meta.verbose_name_plural)
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||
return response
|
||||
|
||||
# Compile a dictionary indicating which permissions are available to the current user for this model
|
||||
permissions = {}
|
||||
@@ -175,6 +179,22 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
||||
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
|
||||
table.columns.show('pk')
|
||||
|
||||
# Handle table-based export
|
||||
if request.GET.get('export') == 'table':
|
||||
exclude_columns = {'pk'}
|
||||
exclude_columns.update({
|
||||
col for col in table.base_columns if col not in table.visible_columns
|
||||
})
|
||||
exporter = TableExport(
|
||||
export_format=TableExport.CSV,
|
||||
table=table,
|
||||
exclude_columns=exclude_columns,
|
||||
dataset_kwargs={},
|
||||
)
|
||||
return exporter.response(
|
||||
filename=f'netbox_{self.queryset.model._meta.verbose_name_plural}.csv'
|
||||
)
|
||||
|
||||
# Apply the request context
|
||||
paginate = {
|
||||
'paginator_class': EnhancedPaginator,
|
||||
@@ -218,11 +238,18 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||
def get_object(self, kwargs):
|
||||
# Look up an existing object by slug or PK, if provided.
|
||||
if 'slug' in kwargs:
|
||||
return get_object_or_404(self.queryset, slug=kwargs['slug'])
|
||||
obj = get_object_or_404(self.queryset, slug=kwargs['slug'])
|
||||
elif 'pk' in kwargs:
|
||||
return get_object_or_404(self.queryset, pk=kwargs['pk'])
|
||||
obj = get_object_or_404(self.queryset, pk=kwargs['pk'])
|
||||
# Otherwise, return a new instance.
|
||||
return self.queryset.model()
|
||||
else:
|
||||
return self.queryset.model()
|
||||
|
||||
# Take a snapshot of change-logged models
|
||||
if hasattr(obj, 'snapshot'):
|
||||
obj.snapshot()
|
||||
|
||||
return obj
|
||||
|
||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||
# Allow views to add extra info to an object before it is processed. For example, a parent object can be defined
|
||||
@@ -328,9 +355,15 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||
def get_object(self, kwargs):
|
||||
# Look up object by slug if one has been provided. Otherwise, use PK.
|
||||
if 'slug' in kwargs:
|
||||
return get_object_or_404(self.queryset, slug=kwargs['slug'])
|
||||
obj = get_object_or_404(self.queryset, slug=kwargs['slug'])
|
||||
else:
|
||||
return get_object_or_404(self.queryset, pk=kwargs['pk'])
|
||||
obj = get_object_or_404(self.queryset, pk=kwargs['pk'])
|
||||
|
||||
# Take a snapshot of change-logged models
|
||||
if hasattr(obj, 'snapshot'):
|
||||
obj.snapshot()
|
||||
|
||||
return obj
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
obj = self.get_object(kwargs)
|
||||
@@ -771,6 +804,10 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||
updated_objects = []
|
||||
for obj in self.queryset.filter(pk__in=form.cleaned_data['pk']):
|
||||
|
||||
# Take a snapshot of change-logged models
|
||||
if hasattr(obj, 'snapshot'):
|
||||
obj.snapshot()
|
||||
|
||||
# Update standard fields. If a field is listed in _nullify, delete its value.
|
||||
for name in standard_fields:
|
||||
|
||||
@@ -898,6 +935,11 @@ class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||
with transaction.atomic():
|
||||
renamed_pks = []
|
||||
for obj in selected_objects:
|
||||
|
||||
# Take a snapshot of change-logged models
|
||||
if hasattr(obj, 'snapshot'):
|
||||
obj.snapshot()
|
||||
|
||||
find = form.cleaned_data['find']
|
||||
replace = form.cleaned_data['replace']
|
||||
if form.cleaned_data['use_regex']:
|
||||
@@ -986,14 +1028,19 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||
|
||||
# Delete objects
|
||||
queryset = self.queryset.filter(pk__in=pk_list)
|
||||
deleted_count = queryset.count()
|
||||
try:
|
||||
deleted_count = queryset.delete()[1][model._meta.label]
|
||||
for obj in queryset:
|
||||
# Take a snapshot of change-logged models
|
||||
if hasattr(obj, 'snapshot'):
|
||||
obj.snapshot()
|
||||
obj.delete()
|
||||
except ProtectedError as e:
|
||||
logger.info("Caught ProtectedError while attempting to delete objects")
|
||||
handle_protectederror(queryset, request, e)
|
||||
return redirect(self.get_return_url(request))
|
||||
|
||||
msg = 'Deleted {} {}'.format(deleted_count, model._meta.verbose_name_plural)
|
||||
msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}"
|
||||
logger.info(msg)
|
||||
messages.success(request, msg)
|
||||
return redirect(self.get_return_url(request))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user