mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-08 16:48:16 -06:00
Merge remote-tracking branch 'upstream/develop-2.4' into feature/webhooks-backend
This commit is contained in:
commit
201b27e52c
@ -1,13 +1,14 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from circuits.constants import CIRCUIT_STATUS_CHOICES
|
from circuits.constants import CIRCUIT_STATUS_CHOICES
|
||||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||||
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
|
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
|
||||||
from extras.api.customfields import CustomFieldModelSerializer
|
from extras.api.customfields import CustomFieldModelSerializer
|
||||||
from tenancy.api.serializers import NestedTenantSerializer
|
from tenancy.api.serializers import NestedTenantSerializer
|
||||||
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
from utilities.api import ChoiceFieldSerializer, TagField, ValidatedModelSerializer, WritableNestedSerializer
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -15,16 +16,17 @@ from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ProviderSerializer(CustomFieldModelSerializer):
|
class ProviderSerializer(CustomFieldModelSerializer):
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
|
||||||
'custom_fields', 'created', 'last_updated',
|
'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class NestedProviderSerializer(serializers.ModelSerializer):
|
class NestedProviderSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -32,16 +34,6 @@ class NestedProviderSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'name', 'slug']
|
fields = ['id', 'url', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class WritableProviderSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Provider
|
|
||||||
fields = [
|
|
||||||
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
|
||||||
'custom_fields', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Circuit types
|
# Circuit types
|
||||||
#
|
#
|
||||||
@ -53,7 +45,7 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class NestedCircuitTypeSerializer(serializers.ModelSerializer):
|
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -67,19 +59,20 @@ class NestedCircuitTypeSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class CircuitSerializer(CustomFieldModelSerializer):
|
class CircuitSerializer(CustomFieldModelSerializer):
|
||||||
provider = NestedProviderSerializer()
|
provider = NestedProviderSerializer()
|
||||||
status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES)
|
status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES, required=False)
|
||||||
type = NestedCircuitTypeSerializer()
|
type = NestedCircuitTypeSerializer()
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
|
'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
|
||||||
'comments', 'custom_fields', 'created', 'last_updated',
|
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class NestedCircuitSerializer(serializers.ModelSerializer):
|
class NestedCircuitSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -87,33 +80,14 @@ class NestedCircuitSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'cid']
|
fields = ['id', 'url', 'cid']
|
||||||
|
|
||||||
|
|
||||||
class WritableCircuitSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Circuit
|
|
||||||
fields = [
|
|
||||||
'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
|
|
||||||
'comments', 'custom_fields', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Circuit Terminations
|
# Circuit Terminations
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTerminationSerializer(serializers.ModelSerializer):
|
class CircuitTerminationSerializer(ValidatedModelSerializer):
|
||||||
circuit = NestedCircuitSerializer()
|
circuit = NestedCircuitSerializer()
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
interface = InterfaceSerializer()
|
interface = InterfaceSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CircuitTermination
|
|
||||||
fields = [
|
|
||||||
'id', 'circuit', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class WritableCircuitTerminationSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
|
@ -30,7 +30,6 @@ class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
|
|||||||
class ProviderViewSet(CustomFieldModelViewSet):
|
class ProviderViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Provider.objects.all()
|
queryset = Provider.objects.all()
|
||||||
serializer_class = serializers.ProviderSerializer
|
serializer_class = serializers.ProviderSerializer
|
||||||
write_serializer_class = serializers.WritableProviderSerializer
|
|
||||||
filter_class = filters.ProviderFilter
|
filter_class = filters.ProviderFilter
|
||||||
|
|
||||||
@detail_route()
|
@detail_route()
|
||||||
@ -61,7 +60,6 @@ class CircuitTypeViewSet(ModelViewSet):
|
|||||||
class CircuitViewSet(CustomFieldModelViewSet):
|
class CircuitViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')
|
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')
|
||||||
serializer_class = serializers.CircuitSerializer
|
serializer_class = serializers.CircuitSerializer
|
||||||
write_serializer_class = serializers.WritableCircuitSerializer
|
|
||||||
filter_class = filters.CircuitFilter
|
filter_class = filters.CircuitFilter
|
||||||
|
|
||||||
|
|
||||||
@ -72,5 +70,4 @@ class CircuitViewSet(CustomFieldModelViewSet):
|
|||||||
class CircuitTerminationViewSet(ModelViewSet):
|
class CircuitTerminationViewSet(ModelViewSet):
|
||||||
queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device')
|
queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device')
|
||||||
serializer_class = serializers.CircuitTerminationSerializer
|
serializer_class = serializers.CircuitTerminationSerializer
|
||||||
write_serializer_class = serializers.WritableCircuitTerminationSerializer
|
|
||||||
filter_class = filters.CircuitTerminationFilter
|
filter_class = filters.CircuitTerminationFilter
|
||||||
|
@ -28,6 +28,9 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Provider
|
model = Provider
|
||||||
@ -103,6 +106,9 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
|
@ -2,6 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
from taggit.forms import TagField
|
||||||
|
|
||||||
from dcim.models import Site, Device, Interface, Rack
|
from dcim.models import Site, Device, Interface, Rack
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
@ -22,10 +23,11 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
|
|||||||
class ProviderForm(BootstrapMixin, CustomFieldForm):
|
class ProviderForm(BootstrapMixin, CustomFieldForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags']
|
||||||
widgets = {
|
widgets = {
|
||||||
'noc_contact': SmallTextarea(attrs={'rows': 5}),
|
'noc_contact': SmallTextarea(attrs={'rows': 5}),
|
||||||
'admin_contact': SmallTextarea(attrs={'rows': 5}),
|
'admin_contact': SmallTextarea(attrs={'rows': 5}),
|
||||||
@ -102,12 +104,13 @@ class CircuitTypeCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = [
|
fields = [
|
||||||
'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
|
'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
|
||||||
'comments',
|
'comments', 'tags',
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'cid': "Unique circuit ID",
|
'cid': "Unique circuit ID",
|
||||||
|
27
netbox/circuits/migrations/0011_tags.py
Normal file
27
netbox/circuits/migrations/0011_tags.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-05-22 19:04
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('taggit', '0002_auto_20150616_2121'),
|
||||||
|
('circuits', '0010_circuit_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='provider',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
]
|
@ -4,11 +4,11 @@ from django.contrib.contenttypes.fields import GenericRelation
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from dcim.constants import STATUS_CLASSES
|
from dcim.constants import STATUS_CLASSES
|
||||||
from dcim.fields import ASNField
|
from dcim.fields import ASNField
|
||||||
from extras.models import CustomFieldModel, CustomFieldValue
|
from extras.models import CustomFieldModel
|
||||||
from tenancy.models import Tenant
|
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
|
from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
|
||||||
|
|
||||||
@ -19,15 +19,45 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
||||||
stores information pertinent to the user's relationship with the Provider.
|
stores information pertinent to the user's relationship with the Provider.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=50,
|
||||||
asn = ASNField(blank=True, null=True, verbose_name='ASN')
|
unique=True
|
||||||
account = models.CharField(max_length=30, blank=True, verbose_name='Account number')
|
)
|
||||||
portal_url = models.URLField(blank=True, verbose_name='Portal')
|
slug = models.SlugField(
|
||||||
noc_contact = models.TextField(blank=True, verbose_name='NOC contact')
|
unique=True
|
||||||
admin_contact = models.TextField(blank=True, verbose_name='Admin contact')
|
)
|
||||||
comments = models.TextField(blank=True)
|
asn = ASNField(
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='ASN'
|
||||||
|
)
|
||||||
|
account = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Account number'
|
||||||
|
)
|
||||||
|
portal_url = models.URLField(
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Portal'
|
||||||
|
)
|
||||||
|
noc_contact = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
verbose_name='NOC contact'
|
||||||
|
)
|
||||||
|
admin_contact = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Admin contact'
|
||||||
|
)
|
||||||
|
comments = models.TextField(
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
||||||
|
|
||||||
@ -63,8 +93,13 @@ class CircuitType(models.Model):
|
|||||||
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
||||||
"Long Haul," "Metro," or "Out-of-Band".
|
"Long Haul," "Metro," or "Out-of-Band".
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=50,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
slug = models.SlugField(
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
|
||||||
csv_headers = ['name', 'slug']
|
csv_headers = ['name', 'slug']
|
||||||
|
|
||||||
@ -91,16 +126,54 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device
|
circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device
|
||||||
interface, but this is not required. Circuit port speed and commit rate are measured in Kbps.
|
interface, but this is not required. Circuit port speed and commit rate are measured in Kbps.
|
||||||
"""
|
"""
|
||||||
cid = models.CharField(max_length=50, verbose_name='Circuit ID')
|
cid = models.CharField(
|
||||||
provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
|
max_length=50,
|
||||||
type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT)
|
verbose_name='Circuit ID'
|
||||||
status = models.PositiveSmallIntegerField(choices=CIRCUIT_STATUS_CHOICES, default=CIRCUIT_STATUS_ACTIVE)
|
)
|
||||||
tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT)
|
provider = models.ForeignKey(
|
||||||
install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
|
to='circuits.Provider',
|
||||||
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')
|
on_delete=models.PROTECT,
|
||||||
description = models.CharField(max_length=100, blank=True)
|
related_name='circuits'
|
||||||
comments = models.TextField(blank=True)
|
)
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
type = models.ForeignKey(
|
||||||
|
to='CircuitType',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='circuits'
|
||||||
|
)
|
||||||
|
status = models.PositiveSmallIntegerField(
|
||||||
|
choices=CIRCUIT_STATUS_CHOICES,
|
||||||
|
default=CIRCUIT_STATUS_ACTIVE
|
||||||
|
)
|
||||||
|
tenant = models.ForeignKey(
|
||||||
|
to='tenancy.Tenant',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='circuits',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
install_date = models.DateField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='Date installed'
|
||||||
|
)
|
||||||
|
commit_rate = models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='Commit rate (Kbps)')
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
comments = models.TextField(
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||||
@ -153,19 +226,47 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class CircuitTermination(models.Model):
|
class CircuitTermination(models.Model):
|
||||||
circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE)
|
circuit = models.ForeignKey(
|
||||||
term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination')
|
to='circuits.Circuit',
|
||||||
site = models.ForeignKey('dcim.Site', related_name='circuit_terminations', on_delete=models.PROTECT)
|
on_delete=models.CASCADE,
|
||||||
interface = models.OneToOneField(
|
related_name='terminations'
|
||||||
'dcim.Interface', related_name='circuit_termination', blank=True, null=True, on_delete=models.PROTECT
|
)
|
||||||
|
term_side = models.CharField(
|
||||||
|
max_length=1,
|
||||||
|
choices=TERM_SIDE_CHOICES,
|
||||||
|
verbose_name='Termination'
|
||||||
|
)
|
||||||
|
site = models.ForeignKey(
|
||||||
|
to='dcim.Site',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='circuit_terminations'
|
||||||
|
)
|
||||||
|
interface = models.OneToOneField(
|
||||||
|
to='dcim.Interface',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='circuit_termination',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
port_speed = models.PositiveIntegerField(
|
||||||
|
verbose_name='Port speed (Kbps)'
|
||||||
)
|
)
|
||||||
port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)')
|
|
||||||
upstream_speed = models.PositiveIntegerField(
|
upstream_speed = models.PositiveIntegerField(
|
||||||
blank=True, null=True, verbose_name='Upstream speed (Kbps)',
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='Upstream speed (Kbps)',
|
||||||
help_text='Upstream speed, if different from port speed'
|
help_text='Upstream speed, if different from port speed'
|
||||||
)
|
)
|
||||||
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
|
xconnect_id = models.CharField(
|
||||||
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
|
max_length=50,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Cross-connect ID'
|
||||||
|
)
|
||||||
|
pp_info = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Patch panel/port(s)'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['circuit', 'term_side']
|
ordering = ['circuit', 'term_side']
|
||||||
|
@ -5,7 +5,7 @@ from django.urls import reverse
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from circuits.constants import TERM_SIDE_A, TERM_SIDE_Z
|
from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z
|
||||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.constants import GRAPH_TYPE_PROVIDER
|
from extras.constants import GRAPH_TYPE_PROVIDER
|
||||||
@ -231,6 +231,7 @@ class CircuitTest(HttpStatusMixin, APITestCase):
|
|||||||
'cid': 'TEST0004',
|
'cid': 'TEST0004',
|
||||||
'provider': self.provider1.pk,
|
'provider': self.provider1.pk,
|
||||||
'type': self.circuittype1.pk,
|
'type': self.circuittype1.pk,
|
||||||
|
'status': CIRCUIT_STATUS_ACTIVE,
|
||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('circuits-api:circuit-list')
|
url = reverse('circuits-api:circuit-list')
|
||||||
@ -250,16 +251,19 @@ class CircuitTest(HttpStatusMixin, APITestCase):
|
|||||||
'cid': 'TEST0004',
|
'cid': 'TEST0004',
|
||||||
'provider': self.provider1.pk,
|
'provider': self.provider1.pk,
|
||||||
'type': self.circuittype1.pk,
|
'type': self.circuittype1.pk,
|
||||||
|
'status': CIRCUIT_STATUS_ACTIVE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'cid': 'TEST0005',
|
'cid': 'TEST0005',
|
||||||
'provider': self.provider1.pk,
|
'provider': self.provider1.pk,
|
||||||
'type': self.circuittype1.pk,
|
'type': self.circuittype1.pk,
|
||||||
|
'status': CIRCUIT_STATUS_ACTIVE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'cid': 'TEST0006',
|
'cid': 'TEST0006',
|
||||||
'provider': self.provider1.pk,
|
'provider': self.provider1.pk,
|
||||||
'type': self.circuittype1.pk,
|
'type': self.circuittype1.pk,
|
||||||
|
'status': CIRCUIT_STATUS_ACTIVE,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from circuits.models import Circuit, CircuitTermination
|
from circuits.models import Circuit, CircuitTermination
|
||||||
from dcim.constants import (
|
from dcim.constants import (
|
||||||
@ -20,7 +21,10 @@ from extras.api.customfields import CustomFieldModelSerializer
|
|||||||
from ipam.models import IPAddress, VLAN
|
from ipam.models import IPAddress, VLAN
|
||||||
from tenancy.api.serializers import NestedTenantSerializer
|
from tenancy.api.serializers import NestedTenantSerializer
|
||||||
from users.api.serializers import NestedUserSerializer
|
from users.api.serializers import NestedUserSerializer
|
||||||
from utilities.api import ChoiceFieldSerializer, TimeZoneField, ValidatedModelSerializer
|
from utilities.api import (
|
||||||
|
ChoiceFieldSerializer, SerializedPKRelatedField, TagField, TimeZoneField, ValidatedModelSerializer,
|
||||||
|
WritableNestedSerializer,
|
||||||
|
)
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster
|
||||||
|
|
||||||
|
|
||||||
@ -28,7 +32,7 @@ from virtualization.models import Cluster
|
|||||||
# Regions
|
# Regions
|
||||||
#
|
#
|
||||||
|
|
||||||
class NestedRegionSerializer(serializers.ModelSerializer):
|
class NestedRegionSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -37,14 +41,7 @@ class NestedRegionSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class RegionSerializer(serializers.ModelSerializer):
|
class RegionSerializer(serializers.ModelSerializer):
|
||||||
parent = NestedRegionSerializer()
|
parent = NestedRegionSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Region
|
|
||||||
fields = ['id', 'name', 'slug', 'parent']
|
|
||||||
|
|
||||||
|
|
||||||
class WritableRegionSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Region
|
model = Region
|
||||||
@ -56,22 +53,23 @@ class WritableRegionSerializer(ValidatedModelSerializer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class SiteSerializer(CustomFieldModelSerializer):
|
class SiteSerializer(CustomFieldModelSerializer):
|
||||||
status = ChoiceFieldSerializer(choices=SITE_STATUS_CHOICES)
|
status = ChoiceFieldSerializer(choices=SITE_STATUS_CHOICES, required=False)
|
||||||
region = NestedRegionSerializer()
|
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
time_zone = TimeZoneField(required=False)
|
time_zone = TimeZoneField(required=False)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||||
'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||||
'custom_fields', 'created', 'last_updated', 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices',
|
'tags', 'custom_fields', 'created', 'last_updated', 'count_prefixes', 'count_vlans', 'count_racks',
|
||||||
'count_circuits',
|
'count_devices', 'count_circuits',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class NestedSiteSerializer(serializers.ModelSerializer):
|
class NestedSiteSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -79,23 +77,11 @@ class NestedSiteSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'name', 'slug']
|
fields = ['id', 'url', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class WritableSiteSerializer(CustomFieldModelSerializer):
|
|
||||||
time_zone = TimeZoneField(required=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Site
|
|
||||||
fields = [
|
|
||||||
'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
|
||||||
'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
|
||||||
'custom_fields', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rack groups
|
# Rack groups
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackGroupSerializer(serializers.ModelSerializer):
|
class RackGroupSerializer(ValidatedModelSerializer):
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -103,7 +89,7 @@ class RackGroupSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug', 'site']
|
fields = ['id', 'name', 'slug', 'site']
|
||||||
|
|
||||||
|
|
||||||
class NestedRackGroupSerializer(serializers.ModelSerializer):
|
class NestedRackGroupSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -111,13 +97,6 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'name', 'slug']
|
fields = ['id', 'url', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class WritableRackGroupSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = RackGroup
|
|
||||||
fields = ['id', 'name', 'slug', 'site']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rack roles
|
# Rack roles
|
||||||
#
|
#
|
||||||
@ -129,7 +108,7 @@ class RackRoleSerializer(ValidatedModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug', 'color']
|
fields = ['id', 'name', 'slug', 'color']
|
||||||
|
|
||||||
|
|
||||||
class NestedRackRoleSerializer(serializers.ModelSerializer):
|
class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -143,21 +122,40 @@ class NestedRackRoleSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class RackSerializer(CustomFieldModelSerializer):
|
class RackSerializer(CustomFieldModelSerializer):
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
group = NestedRackGroupSerializer()
|
group = NestedRackGroupSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
role = NestedRackRoleSerializer()
|
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||||
type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES)
|
type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES, required=False)
|
||||||
width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES)
|
width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES, required=False)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width',
|
'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width',
|
||||||
'u_height', 'desc_units', 'comments', 'custom_fields', 'created', 'last_updated',
|
'u_height', 'desc_units', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
# Omit the UniqueTogetherValidator that would be automatically added to validate (group, facility_id). This
|
||||||
|
# prevents facility_id from being interpreted as a required field.
|
||||||
|
validators = [
|
||||||
|
UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'name'))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
|
||||||
class NestedRackSerializer(serializers.ModelSerializer):
|
# Validate uniqueness of (group, 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.set_context(self)
|
||||||
|
validator(data)
|
||||||
|
|
||||||
|
# Enforce model validation
|
||||||
|
super(RackSerializer, self).validate(data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class NestedRackSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -165,39 +163,11 @@ class NestedRackSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'name', 'display_name']
|
fields = ['id', 'url', 'name', 'display_name']
|
||||||
|
|
||||||
|
|
||||||
class WritableRackSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Rack
|
|
||||||
fields = [
|
|
||||||
'id', 'name', 'facility_id', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width', 'u_height',
|
|
||||||
'desc_units', 'comments', 'custom_fields', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
# Omit the UniqueTogetherValidator that would be automatically added to validate (site, facility_id). This
|
|
||||||
# prevents facility_id from being interpreted as a required field.
|
|
||||||
validators = [
|
|
||||||
UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('site', 'name'))
|
|
||||||
]
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
|
|
||||||
# Validate uniqueness of (site, facility_id) since we omitted the automatically-created validator from Meta.
|
|
||||||
if data.get('facility_id', None):
|
|
||||||
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('site', 'facility_id'))
|
|
||||||
validator.set_context(self)
|
|
||||||
validator(data)
|
|
||||||
|
|
||||||
# Enforce model validation
|
|
||||||
super(WritableRackSerializer, self).validate(data)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rack units
|
# Rack units
|
||||||
#
|
#
|
||||||
|
|
||||||
class NestedDeviceSerializer(serializers.ModelSerializer):
|
class NestedDeviceSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -219,23 +189,16 @@ class RackUnitSerializer(serializers.Serializer):
|
|||||||
# Rack reservations
|
# Rack reservations
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackReservationSerializer(serializers.ModelSerializer):
|
class RackReservationSerializer(ValidatedModelSerializer):
|
||||||
rack = NestedRackSerializer()
|
rack = NestedRackSerializer()
|
||||||
user = NestedUserSerializer()
|
user = NestedUserSerializer()
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
|
fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
|
||||||
|
|
||||||
|
|
||||||
class WritableRackReservationSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = RackReservation
|
|
||||||
fields = ['id', 'rack', 'units', 'user', 'description']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
@ -247,7 +210,7 @@ class ManufacturerSerializer(ValidatedModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class NestedManufacturerSerializer(serializers.ModelSerializer):
|
class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -261,43 +224,34 @@ class NestedManufacturerSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class DeviceTypeSerializer(CustomFieldModelSerializer):
|
class DeviceTypeSerializer(CustomFieldModelSerializer):
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer()
|
||||||
interface_ordering = ChoiceFieldSerializer(choices=IFACE_ORDERING_CHOICES)
|
interface_ordering = ChoiceFieldSerializer(choices=IFACE_ORDERING_CHOICES, required=False)
|
||||||
subdevice_role = ChoiceFieldSerializer(choices=SUBDEVICE_ROLE_CHOICES)
|
subdevice_role = ChoiceFieldSerializer(choices=SUBDEVICE_ROLE_CHOICES, required=False)
|
||||||
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
|
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
|
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
|
||||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields',
|
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'tags', 'custom_fields',
|
||||||
'instance_count',
|
'instance_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class NestedDeviceTypeSerializer(serializers.ModelSerializer):
|
class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['id', 'url', 'manufacturer', 'model', 'slug']
|
fields = ['id', 'url', 'manufacturer', 'model', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class WritableDeviceTypeSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = DeviceType
|
|
||||||
fields = [
|
|
||||||
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
|
|
||||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Console port templates
|
# Console port templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortTemplateSerializer(serializers.ModelSerializer):
|
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -305,18 +259,11 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name']
|
fields = ['id', 'device_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritableConsolePortTemplateSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ConsolePortTemplate
|
|
||||||
fields = ['id', 'device_type', 'name']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Console server port templates
|
# Console server port templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
|
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -324,18 +271,11 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name']
|
fields = ['id', 'device_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritableConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ConsoleServerPortTemplate
|
|
||||||
fields = ['id', 'device_type', 'name']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Power port templates
|
# Power port templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerPortTemplateSerializer(serializers.ModelSerializer):
|
class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -343,18 +283,11 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name']
|
fields = ['id', 'device_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritablePowerPortTemplateSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PowerPortTemplate
|
|
||||||
fields = ['id', 'device_type', 'name']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Power outlet templates
|
# Power outlet templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerOutletTemplateSerializer(serializers.ModelSerializer):
|
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -362,27 +295,13 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name']
|
fields = ['id', 'device_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritablePowerOutletTemplateSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PowerOutletTemplate
|
|
||||||
fields = ['id', 'device_type', 'name']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interface templates
|
# Interface templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceTemplateSerializer(serializers.ModelSerializer):
|
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
|
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES, required=False)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = InterfaceTemplate
|
|
||||||
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
|
|
||||||
|
|
||||||
|
|
||||||
class WritableInterfaceTemplateSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceTemplate
|
model = InterfaceTemplate
|
||||||
@ -393,7 +312,7 @@ class WritableInterfaceTemplateSerializer(ValidatedModelSerializer):
|
|||||||
# Device bay templates
|
# Device bay templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceBayTemplateSerializer(serializers.ModelSerializer):
|
class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -401,13 +320,6 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name']
|
fields = ['id', 'device_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritableDeviceBayTemplateSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = DeviceBayTemplate
|
|
||||||
fields = ['id', 'device_type', 'name']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device roles
|
# Device roles
|
||||||
#
|
#
|
||||||
@ -419,7 +331,7 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug', 'color', 'vm_role']
|
fields = ['id', 'name', 'slug', 'color', 'vm_role']
|
||||||
|
|
||||||
|
|
||||||
class NestedDeviceRoleSerializer(serializers.ModelSerializer):
|
class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -431,15 +343,15 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
|
|||||||
# Platforms
|
# Platforms
|
||||||
#
|
#
|
||||||
|
|
||||||
class PlatformSerializer(serializers.ModelSerializer):
|
class PlatformSerializer(ValidatedModelSerializer):
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'rpc_client']
|
fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'rpc_client']
|
||||||
|
|
||||||
|
|
||||||
class NestedPlatformSerializer(serializers.ModelSerializer):
|
class NestedPlatformSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -447,13 +359,6 @@ class NestedPlatformSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'name', 'slug']
|
fields = ['id', 'url', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class WritablePlatformSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Platform
|
|
||||||
fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'rpc_client']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
@ -489,48 +394,28 @@ class DeviceVirtualChassisSerializer(serializers.ModelSerializer):
|
|||||||
class DeviceSerializer(CustomFieldModelSerializer):
|
class DeviceSerializer(CustomFieldModelSerializer):
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
device_role = NestedDeviceRoleSerializer()
|
device_role = NestedDeviceRoleSerializer()
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
platform = NestedPlatformSerializer()
|
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
rack = NestedRackSerializer()
|
rack = NestedRackSerializer(required=False, allow_null=True)
|
||||||
face = ChoiceFieldSerializer(choices=RACK_FACE_CHOICES)
|
face = ChoiceFieldSerializer(choices=RACK_FACE_CHOICES, required=False)
|
||||||
status = ChoiceFieldSerializer(choices=DEVICE_STATUS_CHOICES)
|
status = ChoiceFieldSerializer(choices=DEVICE_STATUS_CHOICES, required=False)
|
||||||
primary_ip = DeviceIPAddressSerializer()
|
primary_ip = DeviceIPAddressSerializer(read_only=True)
|
||||||
primary_ip4 = DeviceIPAddressSerializer()
|
primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True)
|
||||||
primary_ip6 = DeviceIPAddressSerializer()
|
primary_ip6 = DeviceIPAddressSerializer(required=False, allow_null=True)
|
||||||
parent_device = serializers.SerializerMethodField()
|
parent_device = serializers.SerializerMethodField()
|
||||||
cluster = NestedClusterSerializer()
|
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
||||||
virtual_chassis = DeviceVirtualChassisSerializer()
|
virtual_chassis = DeviceVirtualChassisSerializer(required=False, allow_null=True)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
'id', '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',
|
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
||||||
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'custom_fields', 'created',
|
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields', 'created',
|
||||||
'last_updated',
|
'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_parent_device(self, obj):
|
|
||||||
try:
|
|
||||||
device_bay = obj.parent_bay
|
|
||||||
except DeviceBay.DoesNotExist:
|
|
||||||
return None
|
|
||||||
context = {'request': self.context['request']}
|
|
||||||
data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
|
|
||||||
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class WritableDeviceSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Device
|
|
||||||
fields = [
|
|
||||||
'id', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'rack',
|
|
||||||
'position', 'face', 'status', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position',
|
|
||||||
'vc_priority', 'comments', 'custom_fields', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
validators = []
|
validators = []
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
@ -542,16 +427,26 @@ class WritableDeviceSerializer(CustomFieldModelSerializer):
|
|||||||
validator(data)
|
validator(data)
|
||||||
|
|
||||||
# Enforce model validation
|
# Enforce model validation
|
||||||
super(WritableDeviceSerializer, self).validate(data)
|
super(DeviceSerializer, self).validate(data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def get_parent_device(self, obj):
|
||||||
|
try:
|
||||||
|
device_bay = obj.parent_bay
|
||||||
|
except DeviceBay.DoesNotExist:
|
||||||
|
return None
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
|
||||||
|
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Console server ports
|
# Console server ports
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsoleServerPortSerializer(serializers.ModelSerializer):
|
class ConsoleServerPortSerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -560,27 +455,22 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
|
|||||||
read_only_fields = ['connected_console']
|
read_only_fields = ['connected_console']
|
||||||
|
|
||||||
|
|
||||||
class WritableConsoleServerPortSerializer(ValidatedModelSerializer):
|
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||||
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = ['id', 'device', 'name']
|
fields = ['id', 'url', 'device', 'name']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Console ports
|
# Console ports
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortSerializer(serializers.ModelSerializer):
|
class ConsolePortSerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
cs_port = ConsoleServerPortSerializer()
|
cs_port = NestedConsoleServerPortSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ConsolePort
|
|
||||||
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
|
|
||||||
|
|
||||||
|
|
||||||
class WritableConsolePortSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
@ -591,7 +481,7 @@ class WritableConsolePortSerializer(ValidatedModelSerializer):
|
|||||||
# Power outlets
|
# Power outlets
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerOutletSerializer(serializers.ModelSerializer):
|
class PowerOutletSerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -600,27 +490,22 @@ class PowerOutletSerializer(serializers.ModelSerializer):
|
|||||||
read_only_fields = ['connected_port']
|
read_only_fields = ['connected_port']
|
||||||
|
|
||||||
|
|
||||||
class WritablePowerOutletSerializer(ValidatedModelSerializer):
|
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||||
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = ['id', 'device', 'name']
|
fields = ['id', 'url', 'device', 'name']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Power ports
|
# Power ports
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerPortSerializer(serializers.ModelSerializer):
|
class PowerPortSerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
power_outlet = PowerOutletSerializer()
|
power_outlet = NestedPowerOutletSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PowerPort
|
|
||||||
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
|
|
||||||
|
|
||||||
|
|
||||||
class WritablePowerPortSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
@ -631,12 +516,13 @@ class WritablePowerPortSerializer(ValidatedModelSerializer):
|
|||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
class NestedInterfaceSerializer(serializers.ModelSerializer):
|
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||||
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['id', 'url', 'name']
|
fields = ['id', 'url', 'device', 'name']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
|
class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
|
||||||
@ -647,8 +533,8 @@ class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'cid']
|
fields = ['id', 'url', 'cid']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceCircuitTerminationSerializer(serializers.ModelSerializer):
|
class InterfaceCircuitTerminationSerializer(WritableNestedSerializer):
|
||||||
circuit = InterfaceNestedCircuitSerializer()
|
circuit = InterfaceNestedCircuitSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
@ -658,7 +544,7 @@ class InterfaceCircuitTerminationSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
# Cannot import ipam.api.NestedVLANSerializer due to circular dependency
|
# Cannot import ipam.api.NestedVLANSerializer due to circular dependency
|
||||||
class InterfaceVLANSerializer(serializers.ModelSerializer):
|
class InterfaceVLANSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -666,16 +552,21 @@ class InterfaceVLANSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceSerializer(serializers.ModelSerializer):
|
class InterfaceSerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
|
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES, required=False)
|
||||||
lag = NestedInterfaceSerializer()
|
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
is_connected = serializers.SerializerMethodField(read_only=True)
|
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||||
interface_connection = serializers.SerializerMethodField(read_only=True)
|
interface_connection = serializers.SerializerMethodField(read_only=True)
|
||||||
circuit_termination = InterfaceCircuitTerminationSerializer()
|
circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True)
|
||||||
untagged_vlan = InterfaceVLANSerializer()
|
mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES, required=False)
|
||||||
mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES)
|
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
||||||
tagged_vlans = InterfaceVLANSerializer(many=True)
|
tagged_vlans = SerializedPKRelatedField(
|
||||||
|
queryset=VLAN.objects.all(),
|
||||||
|
serializer=InterfaceVLANSerializer,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
@ -684,51 +575,6 @@ class InterfaceSerializer(serializers.ModelSerializer):
|
|||||||
'is_connected', 'interface_connection', 'circuit_termination', 'mode', 'untagged_vlan', 'tagged_vlans',
|
'is_connected', 'interface_connection', 'circuit_termination', 'mode', 'untagged_vlan', 'tagged_vlans',
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_is_connected(self, obj):
|
|
||||||
"""
|
|
||||||
Return True if the interface has a connected interface or circuit termination.
|
|
||||||
"""
|
|
||||||
if obj.connection:
|
|
||||||
return True
|
|
||||||
try:
|
|
||||||
circuit_termination = obj.circuit_termination
|
|
||||||
return True
|
|
||||||
except CircuitTermination.DoesNotExist:
|
|
||||||
pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_interface_connection(self, obj):
|
|
||||||
if obj.connection:
|
|
||||||
return OrderedDict((
|
|
||||||
('interface', PeerInterfaceSerializer(obj.connected_interface, context=self.context).data),
|
|
||||||
('status', obj.connection.connection_status),
|
|
||||||
))
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class PeerInterfaceSerializer(serializers.ModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
|
||||||
device = NestedDeviceSerializer()
|
|
||||||
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
|
|
||||||
lag = NestedInterfaceSerializer()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Interface
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
|
|
||||||
'description',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class WritableInterfaceSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Interface
|
|
||||||
fields = [
|
|
||||||
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
|
|
||||||
'mode', 'untagged_vlan', 'tagged_vlans',
|
|
||||||
]
|
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
|
||||||
# All associated VLANs be global or assigned to the parent device's site.
|
# All associated VLANs be global or assigned to the parent device's site.
|
||||||
@ -746,23 +592,45 @@ class WritableInterfaceSerializer(ValidatedModelSerializer):
|
|||||||
"be global.".format(vlan)
|
"be global.".format(vlan)
|
||||||
})
|
})
|
||||||
|
|
||||||
return super(WritableInterfaceSerializer, self).validate(data)
|
return super(InterfaceSerializer, self).validate(data)
|
||||||
|
|
||||||
|
def get_is_connected(self, obj):
|
||||||
|
"""
|
||||||
|
Return True if the interface has a connected interface or circuit termination.
|
||||||
|
"""
|
||||||
|
if obj.connection:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
circuit_termination = obj.circuit_termination
|
||||||
|
return True
|
||||||
|
except CircuitTermination.DoesNotExist:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_interface_connection(self, obj):
|
||||||
|
if obj.connection:
|
||||||
|
context = {
|
||||||
|
'request': self.context['request'],
|
||||||
|
'interface': obj.connected_interface,
|
||||||
|
}
|
||||||
|
return ContextualInterfaceConnectionSerializer(obj.connection, context=context).data
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device bays
|
# Device bays
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceBaySerializer(serializers.ModelSerializer):
|
class DeviceBaySerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
installed_device = NestedDeviceSerializer()
|
installed_device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
fields = ['id', 'device', 'name', 'installed_device']
|
fields = ['id', 'device', 'name', 'installed_device']
|
||||||
|
|
||||||
|
|
||||||
class NestedDeviceBaySerializer(serializers.ModelSerializer):
|
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -770,32 +638,15 @@ class NestedDeviceBaySerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'name']
|
fields = ['id', 'url', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritableDeviceBaySerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = DeviceBay
|
|
||||||
fields = ['id', 'device', 'name', 'installed_device']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Inventory items
|
# Inventory items
|
||||||
#
|
#
|
||||||
|
|
||||||
class InventoryItemSerializer(serializers.ModelSerializer):
|
class InventoryItemSerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
manufacturer = NestedManufacturerSerializer()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = InventoryItem
|
|
||||||
fields = [
|
|
||||||
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
|
||||||
'description',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class WritableInventoryItemSerializer(ValidatedModelSerializer):
|
|
||||||
# Provide a default value to satisfy UniqueTogetherValidator
|
# Provide a default value to satisfy UniqueTogetherValidator
|
||||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||||
|
manufacturer = NestedManufacturerSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
@ -809,17 +660,17 @@ class WritableInventoryItemSerializer(ValidatedModelSerializer):
|
|||||||
# Interface connections
|
# Interface connections
|
||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceConnectionSerializer(serializers.ModelSerializer):
|
class InterfaceConnectionSerializer(ValidatedModelSerializer):
|
||||||
interface_a = PeerInterfaceSerializer()
|
interface_a = NestedInterfaceSerializer()
|
||||||
interface_b = PeerInterfaceSerializer()
|
interface_b = NestedInterfaceSerializer()
|
||||||
connection_status = ChoiceFieldSerializer(choices=CONNECTION_STATUS_CHOICES)
|
connection_status = ChoiceFieldSerializer(choices=CONNECTION_STATUS_CHOICES, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceConnection
|
model = InterfaceConnection
|
||||||
fields = ['id', 'interface_a', 'interface_b', 'connection_status']
|
fields = ['id', 'interface_a', 'interface_b', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
|
class NestedInterfaceConnectionSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -827,18 +678,26 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'connection_status']
|
fields = ['id', 'url', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):
|
class ContextualInterfaceConnectionSerializer(serializers.ModelSerializer):
|
||||||
|
"""
|
||||||
|
A read-only representation of an InterfaceConnection from the perspective of either of its two connected Interfaces.
|
||||||
|
"""
|
||||||
|
interface = serializers.SerializerMethodField(read_only=True)
|
||||||
|
connection_status = ChoiceFieldSerializer(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceConnection
|
model = InterfaceConnection
|
||||||
fields = ['id', 'interface_a', 'interface_b', 'connection_status']
|
fields = ['id', 'interface', 'connection_status']
|
||||||
|
|
||||||
|
def get_interface(self, obj):
|
||||||
|
return NestedInterfaceSerializer(self.context['interface'], context=self.context).data
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
#
|
#
|
||||||
|
|
||||||
class VirtualChassisSerializer(serializers.ModelSerializer):
|
class VirtualChassisSerializer(ValidatedModelSerializer):
|
||||||
master = NestedDeviceSerializer()
|
master = NestedDeviceSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -846,16 +705,9 @@ class VirtualChassisSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'master', 'domain']
|
fields = ['id', 'master', 'domain']
|
||||||
|
|
||||||
|
|
||||||
class NestedVirtualChassisSerializer(serializers.ModelSerializer):
|
class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fields = ['id', 'url']
|
fields = ['id', 'url']
|
||||||
|
|
||||||
|
|
||||||
class WritableVirtualChassisSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = VirtualChassis
|
|
||||||
fields = ['id', 'master', 'domain']
|
|
||||||
|
@ -52,7 +52,6 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
|
|||||||
class RegionViewSet(ModelViewSet):
|
class RegionViewSet(ModelViewSet):
|
||||||
queryset = Region.objects.all()
|
queryset = Region.objects.all()
|
||||||
serializer_class = serializers.RegionSerializer
|
serializer_class = serializers.RegionSerializer
|
||||||
write_serializer_class = serializers.WritableRegionSerializer
|
|
||||||
filter_class = filters.RegionFilter
|
filter_class = filters.RegionFilter
|
||||||
|
|
||||||
|
|
||||||
@ -63,7 +62,6 @@ class RegionViewSet(ModelViewSet):
|
|||||||
class SiteViewSet(CustomFieldModelViewSet):
|
class SiteViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Site.objects.select_related('region', 'tenant')
|
queryset = Site.objects.select_related('region', 'tenant')
|
||||||
serializer_class = serializers.SiteSerializer
|
serializer_class = serializers.SiteSerializer
|
||||||
write_serializer_class = serializers.WritableSiteSerializer
|
|
||||||
filter_class = filters.SiteFilter
|
filter_class = filters.SiteFilter
|
||||||
|
|
||||||
@detail_route()
|
@detail_route()
|
||||||
@ -84,7 +82,6 @@ class SiteViewSet(CustomFieldModelViewSet):
|
|||||||
class RackGroupViewSet(ModelViewSet):
|
class RackGroupViewSet(ModelViewSet):
|
||||||
queryset = RackGroup.objects.select_related('site')
|
queryset = RackGroup.objects.select_related('site')
|
||||||
serializer_class = serializers.RackGroupSerializer
|
serializer_class = serializers.RackGroupSerializer
|
||||||
write_serializer_class = serializers.WritableRackGroupSerializer
|
|
||||||
filter_class = filters.RackGroupFilter
|
filter_class = filters.RackGroupFilter
|
||||||
|
|
||||||
|
|
||||||
@ -105,7 +102,6 @@ class RackRoleViewSet(ModelViewSet):
|
|||||||
class RackViewSet(CustomFieldModelViewSet):
|
class RackViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Rack.objects.select_related('site', 'group__site', 'tenant')
|
queryset = Rack.objects.select_related('site', 'group__site', 'tenant')
|
||||||
serializer_class = serializers.RackSerializer
|
serializer_class = serializers.RackSerializer
|
||||||
write_serializer_class = serializers.WritableRackSerializer
|
|
||||||
filter_class = filters.RackFilter
|
filter_class = filters.RackFilter
|
||||||
|
|
||||||
@detail_route()
|
@detail_route()
|
||||||
@ -136,7 +132,6 @@ class RackViewSet(CustomFieldModelViewSet):
|
|||||||
class RackReservationViewSet(ModelViewSet):
|
class RackReservationViewSet(ModelViewSet):
|
||||||
queryset = RackReservation.objects.select_related('rack', 'user', 'tenant')
|
queryset = RackReservation.objects.select_related('rack', 'user', 'tenant')
|
||||||
serializer_class = serializers.RackReservationSerializer
|
serializer_class = serializers.RackReservationSerializer
|
||||||
write_serializer_class = serializers.WritableRackReservationSerializer
|
|
||||||
filter_class = filters.RackReservationFilter
|
filter_class = filters.RackReservationFilter
|
||||||
|
|
||||||
# Assign user from request
|
# Assign user from request
|
||||||
@ -161,7 +156,6 @@ class ManufacturerViewSet(ModelViewSet):
|
|||||||
class DeviceTypeViewSet(CustomFieldModelViewSet):
|
class DeviceTypeViewSet(CustomFieldModelViewSet):
|
||||||
queryset = DeviceType.objects.select_related('manufacturer')
|
queryset = DeviceType.objects.select_related('manufacturer')
|
||||||
serializer_class = serializers.DeviceTypeSerializer
|
serializer_class = serializers.DeviceTypeSerializer
|
||||||
write_serializer_class = serializers.WritableDeviceTypeSerializer
|
|
||||||
filter_class = filters.DeviceTypeFilter
|
filter_class = filters.DeviceTypeFilter
|
||||||
|
|
||||||
|
|
||||||
@ -172,42 +166,36 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
|
|||||||
class ConsolePortTemplateViewSet(ModelViewSet):
|
class ConsolePortTemplateViewSet(ModelViewSet):
|
||||||
queryset = ConsolePortTemplate.objects.select_related('device_type__manufacturer')
|
queryset = ConsolePortTemplate.objects.select_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.ConsolePortTemplateSerializer
|
serializer_class = serializers.ConsolePortTemplateSerializer
|
||||||
write_serializer_class = serializers.WritableConsolePortTemplateSerializer
|
|
||||||
filter_class = filters.ConsolePortTemplateFilter
|
filter_class = filters.ConsolePortTemplateFilter
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateViewSet(ModelViewSet):
|
class ConsoleServerPortTemplateViewSet(ModelViewSet):
|
||||||
queryset = ConsoleServerPortTemplate.objects.select_related('device_type__manufacturer')
|
queryset = ConsoleServerPortTemplate.objects.select_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.ConsoleServerPortTemplateSerializer
|
serializer_class = serializers.ConsoleServerPortTemplateSerializer
|
||||||
write_serializer_class = serializers.WritableConsoleServerPortTemplateSerializer
|
|
||||||
filter_class = filters.ConsoleServerPortTemplateFilter
|
filter_class = filters.ConsoleServerPortTemplateFilter
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateViewSet(ModelViewSet):
|
class PowerPortTemplateViewSet(ModelViewSet):
|
||||||
queryset = PowerPortTemplate.objects.select_related('device_type__manufacturer')
|
queryset = PowerPortTemplate.objects.select_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.PowerPortTemplateSerializer
|
serializer_class = serializers.PowerPortTemplateSerializer
|
||||||
write_serializer_class = serializers.WritablePowerPortTemplateSerializer
|
|
||||||
filter_class = filters.PowerPortTemplateFilter
|
filter_class = filters.PowerPortTemplateFilter
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateViewSet(ModelViewSet):
|
class PowerOutletTemplateViewSet(ModelViewSet):
|
||||||
queryset = PowerOutletTemplate.objects.select_related('device_type__manufacturer')
|
queryset = PowerOutletTemplate.objects.select_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.PowerOutletTemplateSerializer
|
serializer_class = serializers.PowerOutletTemplateSerializer
|
||||||
write_serializer_class = serializers.WritablePowerOutletTemplateSerializer
|
|
||||||
filter_class = filters.PowerOutletTemplateFilter
|
filter_class = filters.PowerOutletTemplateFilter
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateViewSet(ModelViewSet):
|
class InterfaceTemplateViewSet(ModelViewSet):
|
||||||
queryset = InterfaceTemplate.objects.select_related('device_type__manufacturer')
|
queryset = InterfaceTemplate.objects.select_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.InterfaceTemplateSerializer
|
serializer_class = serializers.InterfaceTemplateSerializer
|
||||||
write_serializer_class = serializers.WritableInterfaceTemplateSerializer
|
|
||||||
filter_class = filters.InterfaceTemplateFilter
|
filter_class = filters.InterfaceTemplateFilter
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateViewSet(ModelViewSet):
|
class DeviceBayTemplateViewSet(ModelViewSet):
|
||||||
queryset = DeviceBayTemplate.objects.select_related('device_type__manufacturer')
|
queryset = DeviceBayTemplate.objects.select_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.DeviceBayTemplateSerializer
|
serializer_class = serializers.DeviceBayTemplateSerializer
|
||||||
write_serializer_class = serializers.WritableDeviceBayTemplateSerializer
|
|
||||||
filter_class = filters.DeviceBayTemplateFilter
|
filter_class = filters.DeviceBayTemplateFilter
|
||||||
|
|
||||||
|
|
||||||
@ -228,7 +216,6 @@ class DeviceRoleViewSet(ModelViewSet):
|
|||||||
class PlatformViewSet(ModelViewSet):
|
class PlatformViewSet(ModelViewSet):
|
||||||
queryset = Platform.objects.all()
|
queryset = Platform.objects.all()
|
||||||
serializer_class = serializers.PlatformSerializer
|
serializer_class = serializers.PlatformSerializer
|
||||||
write_serializer_class = serializers.WritablePlatformSerializer
|
|
||||||
filter_class = filters.PlatformFilter
|
filter_class = filters.PlatformFilter
|
||||||
|
|
||||||
|
|
||||||
@ -244,7 +231,6 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
|||||||
'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
|
'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
|
||||||
)
|
)
|
||||||
serializer_class = serializers.DeviceSerializer
|
serializer_class = serializers.DeviceSerializer
|
||||||
write_serializer_class = serializers.WritableDeviceSerializer
|
|
||||||
filter_class = filters.DeviceFilter
|
filter_class = filters.DeviceFilter
|
||||||
|
|
||||||
@detail_route(url_path='napalm')
|
@detail_route(url_path='napalm')
|
||||||
@ -318,35 +304,30 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
|||||||
class ConsolePortViewSet(ModelViewSet):
|
class ConsolePortViewSet(ModelViewSet):
|
||||||
queryset = ConsolePort.objects.select_related('device', 'cs_port__device')
|
queryset = ConsolePort.objects.select_related('device', 'cs_port__device')
|
||||||
serializer_class = serializers.ConsolePortSerializer
|
serializer_class = serializers.ConsolePortSerializer
|
||||||
write_serializer_class = serializers.WritableConsolePortSerializer
|
|
||||||
filter_class = filters.ConsolePortFilter
|
filter_class = filters.ConsolePortFilter
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortViewSet(ModelViewSet):
|
class ConsoleServerPortViewSet(ModelViewSet):
|
||||||
queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device')
|
queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device')
|
||||||
serializer_class = serializers.ConsoleServerPortSerializer
|
serializer_class = serializers.ConsoleServerPortSerializer
|
||||||
write_serializer_class = serializers.WritableConsoleServerPortSerializer
|
|
||||||
filter_class = filters.ConsoleServerPortFilter
|
filter_class = filters.ConsoleServerPortFilter
|
||||||
|
|
||||||
|
|
||||||
class PowerPortViewSet(ModelViewSet):
|
class PowerPortViewSet(ModelViewSet):
|
||||||
queryset = PowerPort.objects.select_related('device', 'power_outlet__device')
|
queryset = PowerPort.objects.select_related('device', 'power_outlet__device')
|
||||||
serializer_class = serializers.PowerPortSerializer
|
serializer_class = serializers.PowerPortSerializer
|
||||||
write_serializer_class = serializers.WritablePowerPortSerializer
|
|
||||||
filter_class = filters.PowerPortFilter
|
filter_class = filters.PowerPortFilter
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletViewSet(ModelViewSet):
|
class PowerOutletViewSet(ModelViewSet):
|
||||||
queryset = PowerOutlet.objects.select_related('device', 'connected_port__device')
|
queryset = PowerOutlet.objects.select_related('device', 'connected_port__device')
|
||||||
serializer_class = serializers.PowerOutletSerializer
|
serializer_class = serializers.PowerOutletSerializer
|
||||||
write_serializer_class = serializers.WritablePowerOutletSerializer
|
|
||||||
filter_class = filters.PowerOutletFilter
|
filter_class = filters.PowerOutletFilter
|
||||||
|
|
||||||
|
|
||||||
class InterfaceViewSet(ModelViewSet):
|
class InterfaceViewSet(ModelViewSet):
|
||||||
queryset = Interface.objects.select_related('device')
|
queryset = Interface.objects.select_related('device')
|
||||||
serializer_class = serializers.InterfaceSerializer
|
serializer_class = serializers.InterfaceSerializer
|
||||||
write_serializer_class = serializers.WritableInterfaceSerializer
|
|
||||||
filter_class = filters.InterfaceFilter
|
filter_class = filters.InterfaceFilter
|
||||||
|
|
||||||
@detail_route()
|
@detail_route()
|
||||||
@ -363,14 +344,12 @@ class InterfaceViewSet(ModelViewSet):
|
|||||||
class DeviceBayViewSet(ModelViewSet):
|
class DeviceBayViewSet(ModelViewSet):
|
||||||
queryset = DeviceBay.objects.select_related('installed_device')
|
queryset = DeviceBay.objects.select_related('installed_device')
|
||||||
serializer_class = serializers.DeviceBaySerializer
|
serializer_class = serializers.DeviceBaySerializer
|
||||||
write_serializer_class = serializers.WritableDeviceBaySerializer
|
|
||||||
filter_class = filters.DeviceBayFilter
|
filter_class = filters.DeviceBayFilter
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemViewSet(ModelViewSet):
|
class InventoryItemViewSet(ModelViewSet):
|
||||||
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
|
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
|
||||||
serializer_class = serializers.InventoryItemSerializer
|
serializer_class = serializers.InventoryItemSerializer
|
||||||
write_serializer_class = serializers.WritableInventoryItemSerializer
|
|
||||||
filter_class = filters.InventoryItemFilter
|
filter_class = filters.InventoryItemFilter
|
||||||
|
|
||||||
|
|
||||||
@ -393,7 +372,6 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
|
|||||||
class InterfaceConnectionViewSet(ModelViewSet):
|
class InterfaceConnectionViewSet(ModelViewSet):
|
||||||
queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')
|
queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')
|
||||||
serializer_class = serializers.InterfaceConnectionSerializer
|
serializer_class = serializers.InterfaceConnectionSerializer
|
||||||
write_serializer_class = serializers.WritableInterfaceConnectionSerializer
|
|
||||||
filter_class = filters.InterfaceConnectionFilter
|
filter_class = filters.InterfaceConnectionFilter
|
||||||
|
|
||||||
|
|
||||||
@ -404,7 +382,6 @@ class InterfaceConnectionViewSet(ModelViewSet):
|
|||||||
class VirtualChassisViewSet(ModelViewSet):
|
class VirtualChassisViewSet(ModelViewSet):
|
||||||
queryset = VirtualChassis.objects.all()
|
queryset = VirtualChassis.objects.all()
|
||||||
serializer_class = serializers.VirtualChassisSerializer
|
serializer_class = serializers.VirtualChassisSerializer
|
||||||
write_serializer_class = serializers.WritableVirtualChassisSerializer
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -82,6 +82,9 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
@ -179,6 +182,9 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
@ -286,6 +292,9 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Manufacturer (slug)',
|
label='Manufacturer (slug)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
@ -497,6 +506,9 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
queryset=VirtualChassis.objects.all(),
|
queryset=VirtualChassis.objects.all(),
|
||||||
label='Virtual chassis (ID)',
|
label='Virtual chassis (ID)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
|
@ -7,6 +7,7 @@ from django.contrib.auth.models import User
|
|||||||
from django.contrib.postgres.forms.array import SimpleArrayField
|
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
from mptt.forms import TreeNodeChoiceField
|
from mptt.forms import TreeNodeChoiceField
|
||||||
|
from taggit.forms import TagField
|
||||||
from timezone_field import TimeZoneFormField
|
from timezone_field import TimeZoneFormField
|
||||||
|
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
@ -108,12 +109,14 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False)
|
region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||||
'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||||
|
'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
||||||
@ -274,12 +277,13 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = [
|
fields = [
|
||||||
'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'role', 'serial', 'type', 'width',
|
'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'role', 'serial', 'type', 'width',
|
||||||
'u_height', 'desc_units', 'comments',
|
'u_height', 'desc_units', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'site': "The site at which the rack exists",
|
'site': "The site at which the rack exists",
|
||||||
@ -350,6 +354,8 @@ class RackCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
site = self.cleaned_data.get('site')
|
site = self.cleaned_data.get('site')
|
||||||
group_name = self.cleaned_data.get('group_name')
|
group_name = self.cleaned_data.get('group_name')
|
||||||
|
name = self.cleaned_data.get('name')
|
||||||
|
facility_id = self.cleaned_data.get('facility_id')
|
||||||
|
|
||||||
# Validate rack group
|
# Validate rack group
|
||||||
if group_name:
|
if group_name:
|
||||||
@ -358,6 +364,18 @@ class RackCSVForm(forms.ModelForm):
|
|||||||
except RackGroup.DoesNotExist:
|
except RackGroup.DoesNotExist:
|
||||||
raise forms.ValidationError("Rack group {} not found for site {}".format(group_name, site))
|
raise forms.ValidationError("Rack group {} not found for site {}".format(group_name, site))
|
||||||
|
|
||||||
|
# Validate uniqueness of rack name within group
|
||||||
|
if Rack.objects.filter(group=self.instance.group, name=name).exists():
|
||||||
|
raise forms.ValidationError(
|
||||||
|
"A rack named {} already exists within group {}".format(name, group_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate uniqueness of facility ID within group
|
||||||
|
if facility_id and Rack.objects.filter(group=self.instance.group, facility_id=facility_id).exists():
|
||||||
|
raise forms.ValidationError(
|
||||||
|
"A rack with the facility ID {} already exists within group {}".format(facility_id, group_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
@ -485,11 +503,14 @@ class ManufacturerCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
|
class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
|
||||||
slug = SlugField(slug_source='model')
|
slug = SlugField(slug_source='model')
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
fields = [
|
||||||
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments']
|
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||||
|
'is_network_device', 'subdevice_role', 'interface_ordering', 'comments', 'tags',
|
||||||
|
]
|
||||||
labels = {
|
labels = {
|
||||||
'interface_ordering': 'Order interfaces by',
|
'interface_ordering': 'Order interfaces by',
|
||||||
}
|
}
|
||||||
@ -706,7 +727,7 @@ class PlatformCSVForm(forms.ModelForm):
|
|||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
manufacturer = forms.ModelChoiceField(
|
manufacturer = forms.ModelChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
required=True,
|
required=False,
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
help_text='Manufacturer name',
|
help_text='Manufacturer name',
|
||||||
error_messages={
|
error_messages={
|
||||||
@ -772,12 +793,13 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face', 'status',
|
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
|
||||||
'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments',
|
'status', 'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'device_role': "The function this device serves",
|
'device_role': "The function this device serves",
|
||||||
|
24
netbox/dcim/migrations/0056_django2.py
Normal file
24
netbox/dcim/migrations/0056_django2.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 2.0.3 on 2018-03-30 14:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0055_virtualchassis_ordering'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='untagged_vlan',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interfaces_as_untagged', to='ipam.VLAN', verbose_name='Untagged VLAN'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='platform',
|
||||||
|
name='manufacturer',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Optionally limit this platform to devices of a certain manufacturer', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='platforms', to='dcim.Manufacturer'),
|
||||||
|
),
|
||||||
|
]
|
37
netbox/dcim/migrations/0057_tags.py
Normal file
37
netbox/dcim/migrations/0057_tags.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-05-22 19:04
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('taggit', '0002_auto_20150616_2121'),
|
||||||
|
('dcim', '0056_django2'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='device',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rack',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='site',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
]
|
23
netbox/dcim/migrations/0058_relax_rack_naming_constraints.py
Normal file
23
netbox/dcim/migrations/0058_relax_rack_naming_constraints.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-05-22 19:27
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0057_tags'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='rack',
|
||||||
|
options={'ordering': ['site', 'group', 'name']},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='rack',
|
||||||
|
unique_together=set([('group', 'name'), ('group', 'facility_id')]),
|
||||||
|
),
|
||||||
|
]
|
@ -14,12 +14,12 @@ from django.db.models import Count, Q, ObjectDoesNotExist
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
from taggit.managers import TaggableManager
|
||||||
from timezone_field import TimeZoneField
|
from timezone_field import TimeZoneField
|
||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from extras.models import CustomFieldModel, CustomFieldValue, ImageAttachment
|
from extras.models import CustomFieldModel
|
||||||
from extras.rpc import RPC_CLIENTS
|
from extras.rpc import RPC_CLIENTS
|
||||||
from tenancy.models import Tenant
|
|
||||||
from utilities.fields import ColorField, NullableCharField
|
from utilities.fields import ColorField, NullableCharField
|
||||||
from utilities.managers import NaturalOrderByManager
|
from utilities.managers import NaturalOrderByManager
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
@ -38,10 +38,20 @@ class Region(MPTTModel):
|
|||||||
Sites can be grouped within geographic Regions.
|
Sites can be grouped within geographic Regions.
|
||||||
"""
|
"""
|
||||||
parent = TreeForeignKey(
|
parent = TreeForeignKey(
|
||||||
'self', null=True, blank=True, related_name='children', db_index=True, on_delete=models.CASCADE
|
to='self',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='children',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
db_index=True
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
slug = models.SlugField(
|
||||||
|
unique=True
|
||||||
)
|
)
|
||||||
name = models.CharField(max_length=50, unique=True)
|
|
||||||
slug = models.SlugField(unique=True)
|
|
||||||
|
|
||||||
csv_headers = ['name', 'slug', 'parent']
|
csv_headers = ['name', 'slug', 'parent']
|
||||||
|
|
||||||
@ -78,25 +88,81 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
||||||
field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
|
field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=50,
|
||||||
status = models.PositiveSmallIntegerField(choices=SITE_STATUS_CHOICES, default=SITE_STATUS_ACTIVE)
|
unique=True
|
||||||
region = models.ForeignKey('Region', related_name='sites', blank=True, null=True, on_delete=models.SET_NULL)
|
)
|
||||||
tenant = models.ForeignKey(Tenant, related_name='sites', blank=True, null=True, on_delete=models.PROTECT)
|
slug = models.SlugField(
|
||||||
facility = models.CharField(max_length=50, blank=True)
|
unique=True
|
||||||
asn = ASNField(blank=True, null=True, verbose_name='ASN')
|
)
|
||||||
time_zone = TimeZoneField(blank=True)
|
status = models.PositiveSmallIntegerField(
|
||||||
description = models.CharField(max_length=100, blank=True)
|
choices=SITE_STATUS_CHOICES,
|
||||||
physical_address = models.CharField(max_length=200, blank=True)
|
default=SITE_STATUS_ACTIVE
|
||||||
shipping_address = models.CharField(max_length=200, blank=True)
|
)
|
||||||
contact_name = models.CharField(max_length=50, blank=True)
|
region = models.ForeignKey(
|
||||||
contact_phone = models.CharField(max_length=20, blank=True)
|
to='dcim.Region',
|
||||||
contact_email = models.EmailField(blank=True, verbose_name="Contact E-mail")
|
on_delete=models.SET_NULL,
|
||||||
comments = models.TextField(blank=True)
|
related_name='sites',
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
blank=True,
|
||||||
images = GenericRelation(ImageAttachment)
|
null=True
|
||||||
|
)
|
||||||
|
tenant = models.ForeignKey(
|
||||||
|
to='tenancy.Tenant',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='sites',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
facility = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
asn = ASNField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='ASN'
|
||||||
|
)
|
||||||
|
time_zone = TimeZoneField(
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
physical_address = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
shipping_address = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
contact_name = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
contact_phone = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
contact_email = models.EmailField(
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Contact E-mail'
|
||||||
|
)
|
||||||
|
comments = models.TextField(
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
images = GenericRelation(
|
||||||
|
to='extras.ImageAttachment'
|
||||||
|
)
|
||||||
|
|
||||||
objects = SiteManager()
|
objects = SiteManager()
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
||||||
@ -175,9 +241,15 @@ class RackGroup(models.Model):
|
|||||||
example, if a Site spans a corporate campus, a RackGroup might be defined to represent each building within that
|
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.
|
campus. If a Site instead represents a single building, a RackGroup might represent a single room or floor.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
slug = models.SlugField()
|
slug = models.SlugField()
|
||||||
site = models.ForeignKey('Site', related_name='rack_groups', on_delete=models.CASCADE)
|
site = models.ForeignKey(
|
||||||
|
to='dcim.Site',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='rack_groups'
|
||||||
|
)
|
||||||
|
|
||||||
csv_headers = ['site', 'name', 'slug']
|
csv_headers = ['site', 'name', 'slug']
|
||||||
|
|
||||||
@ -211,8 +283,13 @@ class RackRole(models.Model):
|
|||||||
"""
|
"""
|
||||||
Racks can be organized by functional role, similar to Devices.
|
Racks can be organized by functional role, similar to Devices.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=50,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
slug = models.SlugField(
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
color = ColorField()
|
color = ColorField()
|
||||||
|
|
||||||
csv_headers = ['name', 'slug', 'color']
|
csv_headers = ['name', 'slug', 'color']
|
||||||
@ -246,25 +323,82 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
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 RackGroup.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(
|
||||||
facility_id = NullableCharField(max_length=50, blank=True, null=True, verbose_name='Facility ID')
|
max_length=50
|
||||||
site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT)
|
)
|
||||||
group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL)
|
facility_id = NullableCharField(
|
||||||
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT)
|
max_length=50,
|
||||||
role = models.ForeignKey('RackRole', related_name='racks', blank=True, null=True, on_delete=models.PROTECT)
|
blank=True,
|
||||||
serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
|
null=True,
|
||||||
type = models.PositiveSmallIntegerField(choices=RACK_TYPE_CHOICES, blank=True, null=True, verbose_name='Type')
|
verbose_name='Facility ID'
|
||||||
width = models.PositiveSmallIntegerField(choices=RACK_WIDTH_CHOICES, default=RACK_WIDTH_19IN, verbose_name='Width',
|
)
|
||||||
help_text='Rail-to-rail width')
|
site = models.ForeignKey(
|
||||||
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)',
|
to='dcim.Site',
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(100)])
|
on_delete=models.PROTECT,
|
||||||
desc_units = models.BooleanField(default=False, verbose_name='Descending units',
|
related_name='racks'
|
||||||
help_text='Units are numbered top-to-bottom')
|
)
|
||||||
comments = models.TextField(blank=True)
|
group = models.ForeignKey(
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
to='dcim.RackGroup',
|
||||||
images = GenericRelation(ImageAttachment)
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='racks',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
tenant = models.ForeignKey(
|
||||||
|
to='tenancy.Tenant',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='racks',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
role = models.ForeignKey(
|
||||||
|
to='dcim.RackRole',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='racks',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
serial = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Serial number'
|
||||||
|
)
|
||||||
|
type = models.PositiveSmallIntegerField(
|
||||||
|
choices=RACK_TYPE_CHOICES,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='Type'
|
||||||
|
)
|
||||||
|
width = models.PositiveSmallIntegerField(
|
||||||
|
choices=RACK_WIDTH_CHOICES,
|
||||||
|
default=RACK_WIDTH_19IN,
|
||||||
|
verbose_name='Width',
|
||||||
|
help_text='Rail-to-rail width'
|
||||||
|
)
|
||||||
|
u_height = models.PositiveSmallIntegerField(
|
||||||
|
default=42,
|
||||||
|
verbose_name='Height (U)',
|
||||||
|
validators=[MinValueValidator(1), MaxValueValidator(100)]
|
||||||
|
)
|
||||||
|
desc_units = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name='Descending units',
|
||||||
|
help_text='Units are numbered top-to-bottom'
|
||||||
|
)
|
||||||
|
comments = models.TextField(
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
images = GenericRelation(
|
||||||
|
to='extras.ImageAttachment'
|
||||||
|
)
|
||||||
|
|
||||||
objects = RackManager()
|
objects = RackManager()
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'serial', 'width', 'u_height',
|
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'serial', 'width', 'u_height',
|
||||||
@ -272,10 +406,10 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
]
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['site', 'name']
|
ordering = ['site', 'group', 'name']
|
||||||
unique_together = [
|
unique_together = [
|
||||||
['site', 'name'],
|
['group', 'name'],
|
||||||
['site', 'facility_id'],
|
['group', 'facility_id'],
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -450,12 +584,31 @@ class RackReservation(models.Model):
|
|||||||
"""
|
"""
|
||||||
One or more reserved units within a Rack.
|
One or more reserved units within a Rack.
|
||||||
"""
|
"""
|
||||||
rack = models.ForeignKey('Rack', related_name='reservations', on_delete=models.CASCADE)
|
rack = models.ForeignKey(
|
||||||
units = ArrayField(models.PositiveSmallIntegerField())
|
to='dcim.Rack',
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
on_delete=models.CASCADE,
|
||||||
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='rackreservations', on_delete=models.PROTECT)
|
related_name='reservations'
|
||||||
user = models.ForeignKey(User, on_delete=models.PROTECT)
|
)
|
||||||
description = models.CharField(max_length=100)
|
units = ArrayField(
|
||||||
|
base_field=models.PositiveSmallIntegerField()
|
||||||
|
)
|
||||||
|
created = models.DateTimeField(
|
||||||
|
auto_now_add=True
|
||||||
|
)
|
||||||
|
tenant = models.ForeignKey(
|
||||||
|
to='tenancy.Tenant',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='rackreservations',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
to=User,
|
||||||
|
on_delete=models.PROTECT
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['created']
|
ordering = ['created']
|
||||||
@ -508,8 +661,13 @@ class Manufacturer(models.Model):
|
|||||||
"""
|
"""
|
||||||
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
|
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=50,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
slug = models.SlugField(
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
|
||||||
csv_headers = ['name', 'slug']
|
csv_headers = ['name', 'slug']
|
||||||
|
|
||||||
@ -545,27 +703,65 @@ class DeviceType(models.Model, CustomFieldModel):
|
|||||||
When a new Device of this type is created, the appropriate console, power, and interface objects (as defined by the
|
When a new Device of this type is created, the appropriate console, power, and interface objects (as defined by the
|
||||||
DeviceType) are automatically created as well.
|
DeviceType) are automatically created as well.
|
||||||
"""
|
"""
|
||||||
manufacturer = models.ForeignKey('Manufacturer', related_name='device_types', on_delete=models.PROTECT)
|
manufacturer = models.ForeignKey(
|
||||||
model = models.CharField(max_length=50)
|
to='dcim.Manufacturer',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='device_types'
|
||||||
|
)
|
||||||
|
model = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
slug = models.SlugField()
|
slug = models.SlugField()
|
||||||
part_number = models.CharField(max_length=50, blank=True, help_text="Discrete part number (optional)")
|
part_number = models.CharField(
|
||||||
u_height = models.PositiveSmallIntegerField(verbose_name='Height (U)', default=1)
|
max_length=50,
|
||||||
is_full_depth = models.BooleanField(default=True, verbose_name="Is full depth",
|
blank=True,
|
||||||
help_text="Device consumes both front and rear rack faces")
|
help_text='Discrete part number (optional)'
|
||||||
interface_ordering = models.PositiveSmallIntegerField(choices=IFACE_ORDERING_CHOICES,
|
)
|
||||||
default=IFACE_ORDERING_POSITION)
|
u_height = models.PositiveSmallIntegerField(
|
||||||
is_console_server = models.BooleanField(default=False, verbose_name='Is a console server',
|
default=1,
|
||||||
help_text="This type of device has console server ports")
|
verbose_name='Height (U)'
|
||||||
is_pdu = models.BooleanField(default=False, verbose_name='Is a PDU',
|
)
|
||||||
help_text="This type of device has power outlets")
|
is_full_depth = models.BooleanField(
|
||||||
is_network_device = models.BooleanField(default=True, verbose_name='Is a network device',
|
default=True,
|
||||||
help_text="This type of device has network interfaces")
|
verbose_name='Is full depth',
|
||||||
subdevice_role = models.NullBooleanField(default=None, verbose_name='Parent/child status',
|
help_text='Device consumes both front and rear rack faces'
|
||||||
|
)
|
||||||
|
interface_ordering = models.PositiveSmallIntegerField(
|
||||||
|
choices=IFACE_ORDERING_CHOICES,
|
||||||
|
default=IFACE_ORDERING_POSITION
|
||||||
|
)
|
||||||
|
is_console_server = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name='Is a console server',
|
||||||
|
help_text='This type of device has console server ports'
|
||||||
|
)
|
||||||
|
is_pdu = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name='Is a PDU',
|
||||||
|
help_text='This type of device has power outlets'
|
||||||
|
)
|
||||||
|
is_network_device = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
verbose_name='Is a network device',
|
||||||
|
help_text='This type of device has network interfaces'
|
||||||
|
)
|
||||||
|
subdevice_role = models.NullBooleanField(
|
||||||
|
default=None,
|
||||||
|
verbose_name='Parent/child status',
|
||||||
choices=SUBDEVICE_ROLE_CHOICES,
|
choices=SUBDEVICE_ROLE_CHOICES,
|
||||||
help_text="Parent devices house child devices in device bays. Select "
|
help_text='Parent devices house child devices in device bays. Select '
|
||||||
"\"None\" if this device type is neither a parent nor a child.")
|
'"None" if this device type is neither a parent nor a child.'
|
||||||
comments = models.TextField(blank=True)
|
)
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
comments = models.TextField(
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
||||||
@ -670,8 +866,14 @@ class ConsolePortTemplate(models.Model):
|
|||||||
"""
|
"""
|
||||||
A template for a ConsolePort to be created for a new Device.
|
A template for a ConsolePort to be created for a new Device.
|
||||||
"""
|
"""
|
||||||
device_type = models.ForeignKey('DeviceType', related_name='console_port_templates', on_delete=models.CASCADE)
|
device_type = models.ForeignKey(
|
||||||
name = models.CharField(max_length=50)
|
to='dcim.DeviceType',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='console_port_templates'
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ['device_type', 'name']
|
||||||
@ -686,8 +888,14 @@ class ConsoleServerPortTemplate(models.Model):
|
|||||||
"""
|
"""
|
||||||
A template for a ConsoleServerPort to be created for a new Device.
|
A template for a ConsoleServerPort to be created for a new Device.
|
||||||
"""
|
"""
|
||||||
device_type = models.ForeignKey('DeviceType', related_name='cs_port_templates', on_delete=models.CASCADE)
|
device_type = models.ForeignKey(
|
||||||
name = models.CharField(max_length=50)
|
to='dcim.DeviceType',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='cs_port_templates'
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ['device_type', 'name']
|
||||||
@ -702,8 +910,14 @@ class PowerPortTemplate(models.Model):
|
|||||||
"""
|
"""
|
||||||
A template for a PowerPort to be created for a new Device.
|
A template for a PowerPort to be created for a new Device.
|
||||||
"""
|
"""
|
||||||
device_type = models.ForeignKey('DeviceType', related_name='power_port_templates', on_delete=models.CASCADE)
|
device_type = models.ForeignKey(
|
||||||
name = models.CharField(max_length=50)
|
to='dcim.DeviceType',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='power_port_templates'
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ['device_type', 'name']
|
||||||
@ -718,8 +932,14 @@ class PowerOutletTemplate(models.Model):
|
|||||||
"""
|
"""
|
||||||
A template for a PowerOutlet to be created for a new Device.
|
A template for a PowerOutlet to be created for a new Device.
|
||||||
"""
|
"""
|
||||||
device_type = models.ForeignKey('DeviceType', related_name='power_outlet_templates', on_delete=models.CASCADE)
|
device_type = models.ForeignKey(
|
||||||
name = models.CharField(max_length=50)
|
to='dcim.DeviceType',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='power_outlet_templates'
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ['device_type', 'name']
|
||||||
@ -734,10 +954,22 @@ class InterfaceTemplate(models.Model):
|
|||||||
"""
|
"""
|
||||||
A template for a physical data interface on a new Device.
|
A template for a physical data interface on a new Device.
|
||||||
"""
|
"""
|
||||||
device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE)
|
device_type = models.ForeignKey(
|
||||||
name = models.CharField(max_length=64)
|
to='dcim.DeviceType',
|
||||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
on_delete=models.CASCADE,
|
||||||
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
|
related_name='interface_templates'
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=64
|
||||||
|
)
|
||||||
|
form_factor = models.PositiveSmallIntegerField(
|
||||||
|
choices=IFACE_FF_CHOICES,
|
||||||
|
default=IFACE_FF_10GE_SFP_PLUS
|
||||||
|
)
|
||||||
|
mgmt_only = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name='Management only'
|
||||||
|
)
|
||||||
|
|
||||||
objects = InterfaceQuerySet.as_manager()
|
objects = InterfaceQuerySet.as_manager()
|
||||||
|
|
||||||
@ -754,8 +986,14 @@ class DeviceBayTemplate(models.Model):
|
|||||||
"""
|
"""
|
||||||
A template for a DeviceBay to be created for a new parent Device.
|
A template for a DeviceBay to be created for a new parent Device.
|
||||||
"""
|
"""
|
||||||
device_type = models.ForeignKey('DeviceType', related_name='device_bay_templates', on_delete=models.CASCADE)
|
device_type = models.ForeignKey(
|
||||||
name = models.CharField(max_length=50)
|
to='dcim.DeviceType',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='device_bay_templates'
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ['device_type', 'name']
|
||||||
@ -776,13 +1014,18 @@ class DeviceRole(models.Model):
|
|||||||
color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
|
color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
|
||||||
virtual machines as well.
|
virtual machines as well.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=50,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
slug = models.SlugField(
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
color = ColorField()
|
color = ColorField()
|
||||||
vm_role = models.BooleanField(
|
vm_role = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
verbose_name="VM Role",
|
verbose_name='VM Role',
|
||||||
help_text="Virtual machines may be assigned to this role"
|
help_text='Virtual machines may be assigned to this role'
|
||||||
)
|
)
|
||||||
|
|
||||||
csv_headers = ['name', 'slug', 'color', 'vm_role']
|
csv_headers = ['name', 'slug', 'color', 'vm_role']
|
||||||
@ -812,26 +1055,32 @@ class Platform(models.Model):
|
|||||||
NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
|
NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
|
||||||
specifying a NAPALM driver.
|
specifying a NAPALM driver.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=50,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
slug = models.SlugField(
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
manufacturer = models.ForeignKey(
|
manufacturer = models.ForeignKey(
|
||||||
to='Manufacturer',
|
to='dcim.Manufacturer',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
related_name='platforms',
|
related_name='platforms',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
help_text="Optionally limit this platform to devices of a certain manufacturer"
|
help_text='Optionally limit this platform to devices of a certain manufacturer'
|
||||||
)
|
)
|
||||||
napalm_driver = models.CharField(
|
napalm_driver = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name='NAPALM driver',
|
verbose_name='NAPALM driver',
|
||||||
help_text="The name of the NAPALM driver to use when interacting with devices"
|
help_text='The name of the NAPALM driver to use when interacting with devices'
|
||||||
)
|
)
|
||||||
rpc_client = models.CharField(
|
rpc_client = models.CharField(
|
||||||
max_length=30,
|
max_length=30,
|
||||||
choices=RPC_CLIENT_CHOICES,
|
choices=RPC_CLIENT_CHOICES,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="Legacy RPC client"
|
verbose_name='Legacy RPC client'
|
||||||
)
|
)
|
||||||
|
|
||||||
csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver']
|
csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver']
|
||||||
@ -873,30 +1122,93 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
by the component templates assigned to its DeviceType. Components can also be added, modified, or deleted after the
|
by the component templates assigned to its DeviceType. Components can also be added, modified, or deleted after the
|
||||||
creation of a Device.
|
creation of a Device.
|
||||||
"""
|
"""
|
||||||
device_type = models.ForeignKey('DeviceType', related_name='instances', on_delete=models.PROTECT)
|
device_type = models.ForeignKey(
|
||||||
device_role = models.ForeignKey('DeviceRole', related_name='devices', on_delete=models.PROTECT)
|
to='dcim.DeviceType',
|
||||||
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='devices', on_delete=models.PROTECT)
|
on_delete=models.PROTECT,
|
||||||
platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
|
related_name='instances'
|
||||||
name = NullableCharField(max_length=64, blank=True, null=True, unique=True)
|
)
|
||||||
serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
|
device_role = models.ForeignKey(
|
||||||
|
to='dcim.DeviceRole',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='devices'
|
||||||
|
)
|
||||||
|
tenant = models.ForeignKey(
|
||||||
|
to='tenancy.Tenant',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='devices',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
platform = models.ForeignKey(
|
||||||
|
to='dcim.Platform',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='devices',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
name = NullableCharField(
|
||||||
|
max_length=64,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
serial = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Serial number'
|
||||||
|
)
|
||||||
asset_tag = NullableCharField(
|
asset_tag = NullableCharField(
|
||||||
max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
|
max_length=50,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
unique=True,
|
||||||
|
verbose_name='Asset tag',
|
||||||
help_text='A unique tag used to identify this device'
|
help_text='A unique tag used to identify this device'
|
||||||
)
|
)
|
||||||
site = models.ForeignKey('Site', related_name='devices', on_delete=models.PROTECT)
|
site = models.ForeignKey(
|
||||||
rack = models.ForeignKey('Rack', related_name='devices', blank=True, null=True, on_delete=models.PROTECT)
|
to='dcim.Site',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='devices'
|
||||||
|
)
|
||||||
|
rack = models.ForeignKey(
|
||||||
|
to='dcim.Rack',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='devices',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
position = models.PositiveSmallIntegerField(
|
position = models.PositiveSmallIntegerField(
|
||||||
blank=True, null=True, validators=[MinValueValidator(1)], verbose_name='Position (U)',
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
validators=[MinValueValidator(1)],
|
||||||
|
verbose_name='Position (U)',
|
||||||
help_text='The lowest-numbered unit occupied by the device'
|
help_text='The lowest-numbered unit occupied by the device'
|
||||||
)
|
)
|
||||||
face = models.PositiveSmallIntegerField(blank=True, null=True, choices=RACK_FACE_CHOICES, verbose_name='Rack face')
|
face = models.PositiveSmallIntegerField(
|
||||||
status = models.PositiveSmallIntegerField(choices=DEVICE_STATUS_CHOICES, default=DEVICE_STATUS_ACTIVE, verbose_name='Status')
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
choices=RACK_FACE_CHOICES,
|
||||||
|
verbose_name='Rack face'
|
||||||
|
)
|
||||||
|
status = models.PositiveSmallIntegerField(
|
||||||
|
choices=DEVICE_STATUS_CHOICES,
|
||||||
|
default=DEVICE_STATUS_ACTIVE,
|
||||||
|
verbose_name='Status'
|
||||||
|
)
|
||||||
primary_ip4 = models.OneToOneField(
|
primary_ip4 = models.OneToOneField(
|
||||||
'ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL, blank=True, null=True,
|
to='ipam.IPAddress',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='primary_ip4_for',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
verbose_name='Primary IPv4'
|
verbose_name='Primary IPv4'
|
||||||
)
|
)
|
||||||
primary_ip6 = models.OneToOneField(
|
primary_ip6 = models.OneToOneField(
|
||||||
'ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL, blank=True, null=True,
|
to='ipam.IPAddress',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='primary_ip6_for',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
verbose_name='Primary IPv6'
|
verbose_name='Primary IPv6'
|
||||||
)
|
)
|
||||||
cluster = models.ForeignKey(
|
cluster = models.ForeignKey(
|
||||||
@ -923,11 +1235,20 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
null=True,
|
null=True,
|
||||||
validators=[MaxValueValidator(255)]
|
validators=[MaxValueValidator(255)]
|
||||||
)
|
)
|
||||||
comments = models.TextField(blank=True)
|
comments = models.TextField(
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
blank=True
|
||||||
images = GenericRelation(ImageAttachment)
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
images = GenericRelation(
|
||||||
|
to='extras.ImageAttachment'
|
||||||
|
)
|
||||||
|
|
||||||
objects = DeviceManager()
|
objects = DeviceManager()
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||||
@ -1184,11 +1505,26 @@ class ConsolePort(models.Model):
|
|||||||
"""
|
"""
|
||||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||||
"""
|
"""
|
||||||
device = models.ForeignKey('Device', related_name='console_ports', on_delete=models.CASCADE)
|
device = models.ForeignKey(
|
||||||
name = models.CharField(max_length=50)
|
to='dcim.Device',
|
||||||
cs_port = models.OneToOneField('ConsoleServerPort', related_name='connected_console', on_delete=models.SET_NULL,
|
on_delete=models.CASCADE,
|
||||||
verbose_name='Console server port', blank=True, null=True)
|
related_name='console_ports'
|
||||||
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
|
cs_port = models.OneToOneField(
|
||||||
|
to='dcim.ConsoleServerPort',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='connected_console',
|
||||||
|
verbose_name='Console server port',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
connection_status = models.NullBooleanField(
|
||||||
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
|
default=CONNECTION_STATUS_CONNECTED
|
||||||
|
)
|
||||||
|
|
||||||
csv_headers = ['console_server', 'cs_port', 'device', 'console_port', 'connection_status']
|
csv_headers = ['console_server', 'cs_port', 'device', 'console_port', 'connection_status']
|
||||||
|
|
||||||
@ -1231,8 +1567,14 @@ class ConsoleServerPort(models.Model):
|
|||||||
"""
|
"""
|
||||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||||
"""
|
"""
|
||||||
device = models.ForeignKey('Device', related_name='cs_ports', on_delete=models.CASCADE)
|
device = models.ForeignKey(
|
||||||
name = models.CharField(max_length=50)
|
to='dcim.Device',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='cs_ports'
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
|
|
||||||
objects = ConsoleServerPortManager()
|
objects = ConsoleServerPortManager()
|
||||||
|
|
||||||
@ -1266,11 +1608,25 @@ class PowerPort(models.Model):
|
|||||||
"""
|
"""
|
||||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||||
"""
|
"""
|
||||||
device = models.ForeignKey('Device', related_name='power_ports', on_delete=models.CASCADE)
|
device = models.ForeignKey(
|
||||||
name = models.CharField(max_length=50)
|
to='dcim.Device',
|
||||||
power_outlet = models.OneToOneField('PowerOutlet', related_name='connected_port', on_delete=models.SET_NULL,
|
on_delete=models.CASCADE,
|
||||||
blank=True, null=True)
|
related_name='power_ports'
|
||||||
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
|
power_outlet = models.OneToOneField(
|
||||||
|
to='dcim.PowerOutlet',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='connected_port',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
connection_status = models.NullBooleanField(
|
||||||
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
|
default=CONNECTION_STATUS_CONNECTED
|
||||||
|
)
|
||||||
|
|
||||||
csv_headers = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status']
|
csv_headers = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status']
|
||||||
|
|
||||||
@ -1313,8 +1669,14 @@ class PowerOutlet(models.Model):
|
|||||||
"""
|
"""
|
||||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||||
"""
|
"""
|
||||||
device = models.ForeignKey('Device', related_name='power_outlets', on_delete=models.CASCADE)
|
device = models.ForeignKey(
|
||||||
name = models.CharField(max_length=50)
|
to='dcim.Device',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='power_outlets'
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
|
|
||||||
objects = PowerOutletManager()
|
objects = PowerOutletManager()
|
||||||
|
|
||||||
@ -1371,17 +1733,35 @@ class Interface(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name='Parent LAG'
|
verbose_name='Parent LAG'
|
||||||
)
|
)
|
||||||
name = models.CharField(max_length=64)
|
name = models.CharField(
|
||||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
max_length=64
|
||||||
enabled = models.BooleanField(default=True)
|
)
|
||||||
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
|
form_factor = models.PositiveSmallIntegerField(
|
||||||
mtu = models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU')
|
choices=IFACE_FF_CHOICES,
|
||||||
|
default=IFACE_FF_10GE_SFP_PLUS
|
||||||
|
)
|
||||||
|
enabled = models.BooleanField(
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
mac_address = MACAddressField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='MAC Address'
|
||||||
|
)
|
||||||
|
mtu = models.PositiveSmallIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='MTU'
|
||||||
|
)
|
||||||
mgmt_only = models.BooleanField(
|
mgmt_only = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name='OOB Management',
|
verbose_name='OOB Management',
|
||||||
help_text="This interface is used only for out-of-band management"
|
help_text='This interface is used only for out-of-band management'
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
)
|
)
|
||||||
description = models.CharField(max_length=100, blank=True)
|
|
||||||
mode = models.PositiveSmallIntegerField(
|
mode = models.PositiveSmallIntegerField(
|
||||||
choices=IFACE_MODE_CHOICES,
|
choices=IFACE_MODE_CHOICES,
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -1389,16 +1769,17 @@ class Interface(models.Model):
|
|||||||
)
|
)
|
||||||
untagged_vlan = models.ForeignKey(
|
untagged_vlan = models.ForeignKey(
|
||||||
to='ipam.VLAN',
|
to='ipam.VLAN',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='interfaces_as_untagged',
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name='Untagged VLAN',
|
verbose_name='Untagged VLAN'
|
||||||
related_name='interfaces_as_untagged'
|
|
||||||
)
|
)
|
||||||
tagged_vlans = models.ManyToManyField(
|
tagged_vlans = models.ManyToManyField(
|
||||||
to='ipam.VLAN',
|
to='ipam.VLAN',
|
||||||
|
related_name='interfaces_as_tagged',
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name='Tagged VLANs',
|
verbose_name='Tagged VLANs'
|
||||||
related_name='interfaces_as_tagged'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = InterfaceQuerySet.as_manager()
|
objects = InterfaceQuerySet.as_manager()
|
||||||
@ -1543,10 +1924,21 @@ class InterfaceConnection(models.Model):
|
|||||||
An InterfaceConnection represents a symmetrical, one-to-one connection between two Interfaces. There is no
|
An InterfaceConnection represents a symmetrical, one-to-one connection between two Interfaces. There is no
|
||||||
significant difference between the interface_a and interface_b fields.
|
significant difference between the interface_a and interface_b fields.
|
||||||
"""
|
"""
|
||||||
interface_a = models.OneToOneField('Interface', related_name='connected_as_a', on_delete=models.CASCADE)
|
interface_a = models.OneToOneField(
|
||||||
interface_b = models.OneToOneField('Interface', related_name='connected_as_b', on_delete=models.CASCADE)
|
to='dcim.Interface',
|
||||||
connection_status = models.BooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED,
|
on_delete=models.CASCADE,
|
||||||
verbose_name='Status')
|
related_name='connected_as_a'
|
||||||
|
)
|
||||||
|
interface_b = models.OneToOneField(
|
||||||
|
to='dcim.Interface',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='connected_as_b'
|
||||||
|
)
|
||||||
|
connection_status = models.BooleanField(
|
||||||
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
|
default=CONNECTION_STATUS_CONNECTED,
|
||||||
|
verbose_name='Status'
|
||||||
|
)
|
||||||
|
|
||||||
csv_headers = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
|
csv_headers = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
|
||||||
|
|
||||||
@ -1578,10 +1970,22 @@ class DeviceBay(models.Model):
|
|||||||
"""
|
"""
|
||||||
An empty space within a Device which can house a child device
|
An empty space within a Device which can house a child device
|
||||||
"""
|
"""
|
||||||
device = models.ForeignKey('Device', related_name='device_bays', on_delete=models.CASCADE)
|
device = models.ForeignKey(
|
||||||
name = models.CharField(max_length=50, verbose_name='Name')
|
to='dcim.Device',
|
||||||
installed_device = models.OneToOneField('Device', related_name='parent_bay', on_delete=models.SET_NULL, blank=True,
|
on_delete=models.CASCADE,
|
||||||
null=True)
|
related_name='device_bays'
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
verbose_name='Name'
|
||||||
|
)
|
||||||
|
installed_device = models.OneToOneField(
|
||||||
|
to='dcim.Device',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='parent_bay',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'name']
|
ordering = ['device', 'name']
|
||||||
@ -1616,20 +2020,55 @@ class InventoryItem(models.Model):
|
|||||||
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
||||||
InventoryItems are used only for inventory purposes.
|
InventoryItems are used only for inventory purposes.
|
||||||
"""
|
"""
|
||||||
device = models.ForeignKey('Device', related_name='inventory_items', on_delete=models.CASCADE)
|
device = models.ForeignKey(
|
||||||
parent = models.ForeignKey('self', related_name='child_items', blank=True, null=True, on_delete=models.CASCADE)
|
to='dcim.Device',
|
||||||
name = models.CharField(max_length=50, verbose_name='Name')
|
on_delete=models.CASCADE,
|
||||||
manufacturer = models.ForeignKey(
|
related_name='inventory_items'
|
||||||
'Manufacturer', models.PROTECT, related_name='inventory_items', blank=True, null=True
|
)
|
||||||
|
parent = models.ForeignKey(
|
||||||
|
to='self',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='child_items',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
verbose_name='Name'
|
||||||
|
)
|
||||||
|
manufacturer = models.ForeignKey(
|
||||||
|
to='dcim.Manufacturer',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='inventory_items',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
part_id = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
verbose_name='Part ID',
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
serial = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
verbose_name='Serial number',
|
||||||
|
blank=True
|
||||||
)
|
)
|
||||||
part_id = models.CharField(max_length=50, verbose_name='Part ID', blank=True)
|
|
||||||
serial = models.CharField(max_length=50, verbose_name='Serial number', blank=True)
|
|
||||||
asset_tag = NullableCharField(
|
asset_tag = NullableCharField(
|
||||||
max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
|
max_length=50,
|
||||||
|
unique=True,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='Asset tag',
|
||||||
help_text='A unique tag used to identify this item'
|
help_text='A unique tag used to identify this item'
|
||||||
)
|
)
|
||||||
discovered = models.BooleanField(default=False, verbose_name='Discovered')
|
discovered = models.BooleanField(
|
||||||
description = models.CharField(max_length=100, blank=True)
|
default=False,
|
||||||
|
verbose_name='Discovered'
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
|
'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
|
||||||
|
@ -43,13 +43,13 @@ class InterfaceQuerySet(QuerySet):
|
|||||||
}[method]
|
}[method]
|
||||||
|
|
||||||
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
|
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
|
||||||
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
|
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(\d{{1,9}})$') AS integer)"
|
||||||
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?([0-9]+)\/') AS integer)"
|
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(\d{{1,9}})\/') AS integer)"
|
||||||
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/)([0-9]+)') AS integer), 0)"
|
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/)(\d{{1,9}})') AS integer), 0)"
|
||||||
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)"
|
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{2}}(\d{{1,9}})') AS integer), 0)"
|
||||||
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)"
|
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{3}}(\d{{1,9}})') AS integer), 0)"
|
||||||
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
|
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':(\d{{1,9}})(\.\d{{1,9}})?$') AS integer), 0)"
|
||||||
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"
|
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.(\d{{1,9}})$') AS integer), 0)"
|
||||||
|
|
||||||
fields = {
|
fields = {
|
||||||
'_type': RawSQL(TYPE_RE.format(sql_col), []),
|
'_type': RawSQL(TYPE_RE.format(sql_col), []),
|
||||||
|
@ -6,7 +6,8 @@ from rest_framework import status
|
|||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from dcim.constants import (
|
from dcim.constants import (
|
||||||
IFACE_FF_1GE_FIXED, IFACE_FF_LAG, IFACE_MODE_TAGGED, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
|
IFACE_FF_1GE_FIXED, IFACE_FF_LAG, IFACE_MODE_TAGGED, SITE_STATUS_ACTIVE, SUBDEVICE_ROLE_CHILD,
|
||||||
|
SUBDEVICE_ROLE_PARENT,
|
||||||
)
|
)
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
@ -168,6 +169,7 @@ class SiteTest(HttpStatusMixin, APITestCase):
|
|||||||
'name': 'Test Site 4',
|
'name': 'Test Site 4',
|
||||||
'slug': 'test-site-4',
|
'slug': 'test-site-4',
|
||||||
'region': self.region1.pk,
|
'region': self.region1.pk,
|
||||||
|
'status': SITE_STATUS_ACTIVE,
|
||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('dcim-api:site-list')
|
url = reverse('dcim-api:site-list')
|
||||||
@ -187,16 +189,19 @@ class SiteTest(HttpStatusMixin, APITestCase):
|
|||||||
'name': 'Test Site 4',
|
'name': 'Test Site 4',
|
||||||
'slug': 'test-site-4',
|
'slug': 'test-site-4',
|
||||||
'region': self.region1.pk,
|
'region': self.region1.pk,
|
||||||
|
'status': SITE_STATUS_ACTIVE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Test Site 5',
|
'name': 'Test Site 5',
|
||||||
'slug': 'test-site-5',
|
'slug': 'test-site-5',
|
||||||
'region': self.region1.pk,
|
'region': self.region1.pk,
|
||||||
|
'status': SITE_STATUS_ACTIVE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Test Site 6',
|
'name': 'Test Site 6',
|
||||||
'slug': 'test-site-6',
|
'slug': 'test-site-6',
|
||||||
'region': self.region1.pk,
|
'region': self.region1.pk,
|
||||||
|
'status': SITE_STATUS_ACTIVE,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2322,8 +2327,8 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
|
|||||||
'device': self.device.pk,
|
'device': self.device.pk,
|
||||||
'name': 'Test Interface 4',
|
'name': 'Test Interface 4',
|
||||||
'mode': IFACE_MODE_TAGGED,
|
'mode': IFACE_MODE_TAGGED,
|
||||||
|
'untagged_vlan': self.vlan3.id,
|
||||||
'tagged_vlans': [self.vlan1.id, self.vlan2.id],
|
'tagged_vlans': [self.vlan1.id, self.vlan2.id],
|
||||||
'untagged_vlan': self.vlan3.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('dcim-api:interface-list')
|
url = reverse('dcim-api:interface-list')
|
||||||
@ -2331,11 +2336,10 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
|
|||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(Interface.objects.count(), 4)
|
self.assertEqual(Interface.objects.count(), 4)
|
||||||
interface5 = Interface.objects.get(pk=response.data['id'])
|
self.assertEqual(response.data['device']['id'], data['device'])
|
||||||
self.assertEqual(interface5.device_id, data['device'])
|
self.assertEqual(response.data['name'], data['name'])
|
||||||
self.assertEqual(interface5.name, data['name'])
|
self.assertEqual(response.data['untagged_vlan']['id'], data['untagged_vlan'])
|
||||||
self.assertEqual(interface5.tagged_vlans.count(), 2)
|
self.assertEqual([v['id'] for v in response.data['tagged_vlans']], data['tagged_vlans'])
|
||||||
self.assertEqual(interface5.untagged_vlan.id, data['untagged_vlan'])
|
|
||||||
|
|
||||||
def test_create_interface_bulk(self):
|
def test_create_interface_bulk(self):
|
||||||
|
|
||||||
@ -2370,22 +2374,22 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
|
|||||||
'device': self.device.pk,
|
'device': self.device.pk,
|
||||||
'name': 'Test Interface 4',
|
'name': 'Test Interface 4',
|
||||||
'mode': IFACE_MODE_TAGGED,
|
'mode': IFACE_MODE_TAGGED,
|
||||||
'tagged_vlans': [self.vlan1.id],
|
|
||||||
'untagged_vlan': self.vlan2.id,
|
'untagged_vlan': self.vlan2.id,
|
||||||
|
'tagged_vlans': [self.vlan1.id],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device': self.device.pk,
|
'device': self.device.pk,
|
||||||
'name': 'Test Interface 5',
|
'name': 'Test Interface 5',
|
||||||
'mode': IFACE_MODE_TAGGED,
|
'mode': IFACE_MODE_TAGGED,
|
||||||
'tagged_vlans': [self.vlan1.id],
|
|
||||||
'untagged_vlan': self.vlan2.id,
|
'untagged_vlan': self.vlan2.id,
|
||||||
|
'tagged_vlans': [self.vlan1.id],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device': self.device.pk,
|
'device': self.device.pk,
|
||||||
'name': 'Test Interface 6',
|
'name': 'Test Interface 6',
|
||||||
'mode': IFACE_MODE_TAGGED,
|
'mode': IFACE_MODE_TAGGED,
|
||||||
'tagged_vlans': [self.vlan1.id],
|
|
||||||
'untagged_vlan': self.vlan2.id,
|
'untagged_vlan': self.vlan2.id,
|
||||||
|
'tagged_vlans': [self.vlan1.id],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2394,15 +2398,10 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
|
|||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(Interface.objects.count(), 6)
|
self.assertEqual(Interface.objects.count(), 6)
|
||||||
self.assertEqual(response.data[0]['name'], data[0]['name'])
|
for i in range(0, 3):
|
||||||
self.assertEqual(response.data[1]['name'], data[1]['name'])
|
self.assertEqual(response.data[i]['name'], data[i]['name'])
|
||||||
self.assertEqual(response.data[2]['name'], data[2]['name'])
|
self.assertEqual([v['id'] for v in response.data[i]['tagged_vlans']], data[i]['tagged_vlans'])
|
||||||
self.assertEqual(len(response.data[0]['tagged_vlans']), 1)
|
self.assertEqual(response.data[i]['untagged_vlan']['id'], data[i]['untagged_vlan'])
|
||||||
self.assertEqual(len(response.data[1]['tagged_vlans']), 1)
|
|
||||||
self.assertEqual(len(response.data[2]['tagged_vlans']), 1)
|
|
||||||
self.assertEqual(response.data[0]['untagged_vlan'], self.vlan2.id)
|
|
||||||
self.assertEqual(response.data[1]['untagged_vlan'], self.vlan2.id)
|
|
||||||
self.assertEqual(response.data[2]['untagged_vlan'], self.vlan2.id)
|
|
||||||
|
|
||||||
def test_update_interface(self):
|
def test_update_interface(self):
|
||||||
|
|
||||||
@ -2847,9 +2846,9 @@ class InterfaceConnectionTest(HttpStatusMixin, APITestCase):
|
|||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(InterfaceConnection.objects.count(), 6)
|
self.assertEqual(InterfaceConnection.objects.count(), 6)
|
||||||
self.assertEqual(response.data[0]['interface_a'], data[0]['interface_a'])
|
for i in range(0, 3):
|
||||||
self.assertEqual(response.data[1]['interface_a'], data[1]['interface_a'])
|
self.assertEqual(response.data[i]['interface_a']['id'], data[i]['interface_a'])
|
||||||
self.assertEqual(response.data[2]['interface_a'], data[2]['interface_a'])
|
self.assertEqual(response.data[i]['interface_b']['id'], data[i]['interface_b'])
|
||||||
|
|
||||||
def test_update_interfaceconnection(self):
|
def test_update_interfaceconnection(self):
|
||||||
|
|
||||||
@ -3047,12 +3046,9 @@ class VirtualChassisTest(HttpStatusMixin, APITestCase):
|
|||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(VirtualChassis.objects.count(), 5)
|
self.assertEqual(VirtualChassis.objects.count(), 5)
|
||||||
self.assertEqual(response.data[0]['master'], data[0]['master'])
|
for i in range(0, 3):
|
||||||
self.assertEqual(response.data[0]['domain'], data[0]['domain'])
|
self.assertEqual(response.data[i]['master']['id'], data[i]['master'])
|
||||||
self.assertEqual(response.data[1]['master'], data[1]['master'])
|
self.assertEqual(response.data[i]['domain'], data[i]['domain'])
|
||||||
self.assertEqual(response.data[1]['domain'], data[1]['domain'])
|
|
||||||
self.assertEqual(response.data[2]['master'], data[2]['master'])
|
|
||||||
self.assertEqual(response.data[2]['domain'], data[2]['domain'])
|
|
||||||
|
|
||||||
def test_update_virtualchassis(self):
|
def test_update_virtualchassis(self):
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
|
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
|
||||||
from dcim.models import Device, Rack, Site
|
from dcim.models import Device, Rack, Site
|
||||||
@ -16,7 +17,7 @@ from extras.constants import *
|
|||||||
# Graphs
|
# Graphs
|
||||||
#
|
#
|
||||||
|
|
||||||
class GraphSerializer(serializers.ModelSerializer):
|
class GraphSerializer(ValidatedModelSerializer):
|
||||||
type = ChoiceFieldSerializer(choices=GRAPH_TYPE_CHOICES)
|
type = ChoiceFieldSerializer(choices=GRAPH_TYPE_CHOICES)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -24,13 +25,6 @@ class GraphSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'type', 'weight', 'name', 'source', 'link']
|
fields = ['id', 'type', 'weight', 'name', 'source', 'link']
|
||||||
|
|
||||||
|
|
||||||
class WritableGraphSerializer(serializers.ModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Graph
|
|
||||||
fields = ['id', 'type', 'weight', 'name', 'source', 'link']
|
|
||||||
|
|
||||||
|
|
||||||
class RenderedGraphSerializer(serializers.ModelSerializer):
|
class RenderedGraphSerializer(serializers.ModelSerializer):
|
||||||
embed_url = serializers.SerializerMethodField()
|
embed_url = serializers.SerializerMethodField()
|
||||||
embed_link = serializers.SerializerMethodField()
|
embed_link = serializers.SerializerMethodField()
|
||||||
@ -51,7 +45,7 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
|
|||||||
# Export templates
|
# Export templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class ExportTemplateSerializer(serializers.ModelSerializer):
|
class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
@ -62,7 +56,7 @@ class ExportTemplateSerializer(serializers.ModelSerializer):
|
|||||||
# Topology maps
|
# Topology maps
|
||||||
#
|
#
|
||||||
|
|
||||||
class TopologyMapSerializer(serializers.ModelSerializer):
|
class TopologyMapSerializer(ValidatedModelSerializer):
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -70,23 +64,46 @@ class TopologyMapSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
|
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
|
||||||
|
|
||||||
|
|
||||||
class WritableTopologyMapSerializer(serializers.ModelSerializer):
|
#
|
||||||
|
# Tags
|
||||||
|
#
|
||||||
|
|
||||||
|
class TagSerializer(ValidatedModelSerializer):
|
||||||
|
tagged_items = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TopologyMap
|
model = Tag
|
||||||
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
|
fields = ['id', 'name', 'slug', 'tagged_items']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Image attachments
|
# Image attachments
|
||||||
#
|
#
|
||||||
|
|
||||||
class ImageAttachmentSerializer(serializers.ModelSerializer):
|
class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||||
parent = serializers.SerializerMethodField()
|
content_type = ContentTypeFieldSerializer()
|
||||||
|
parent = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ImageAttachment
|
model = ImageAttachment
|
||||||
fields = ['id', 'parent', 'name', 'image', 'image_height', 'image_width', 'created']
|
fields = [
|
||||||
|
'id', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height', 'image_width', 'created',
|
||||||
|
]
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
|
||||||
|
# Validate that the parent object exists
|
||||||
|
try:
|
||||||
|
data['content_type'].get_object_for_this_type(id=data['object_id'])
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enforce model validation
|
||||||
|
super(ImageAttachmentSerializer, self).validate(data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
def get_parent(self, obj):
|
def get_parent(self, obj):
|
||||||
|
|
||||||
@ -103,29 +120,6 @@ class ImageAttachmentSerializer(serializers.ModelSerializer):
|
|||||||
return serializer(obj.parent, context={'request': self.context['request']}).data
|
return serializer(obj.parent, context={'request': self.context['request']}).data
|
||||||
|
|
||||||
|
|
||||||
class WritableImageAttachmentSerializer(ValidatedModelSerializer):
|
|
||||||
content_type = ContentTypeFieldSerializer()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ImageAttachment
|
|
||||||
fields = ['id', 'content_type', 'object_id', 'name', 'image']
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
|
|
||||||
# Validate that the parent object exists
|
|
||||||
try:
|
|
||||||
data['content_type'].get_object_for_this_type(id=data['object_id'])
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
|
|
||||||
)
|
|
||||||
|
|
||||||
# Enforce model validation
|
|
||||||
super(WritableImageAttachmentSerializer, self).validate(data)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Reports
|
# Reports
|
||||||
#
|
#
|
||||||
|
@ -28,6 +28,9 @@ router.register(r'export-templates', views.ExportTemplateViewSet)
|
|||||||
# Topology maps
|
# Topology maps
|
||||||
router.register(r'topology-maps', views.TopologyMapViewSet)
|
router.register(r'topology-maps', views.TopologyMapViewSet)
|
||||||
|
|
||||||
|
# Tags
|
||||||
|
router.register(r'tags', views.TagViewSet)
|
||||||
|
|
||||||
# Image attachments
|
# Image attachments
|
||||||
router.register(r'image-attachments', views.ImageAttachmentViewSet)
|
router.register(r'image-attachments', views.ImageAttachmentViewSet)
|
||||||
|
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models import Count
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import detail_route
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from extras import filters
|
from extras import filters
|
||||||
from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
|
from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
|
||||||
@ -67,7 +69,6 @@ class CustomFieldModelViewSet(ModelViewSet):
|
|||||||
class GraphViewSet(ModelViewSet):
|
class GraphViewSet(ModelViewSet):
|
||||||
queryset = Graph.objects.all()
|
queryset = Graph.objects.all()
|
||||||
serializer_class = serializers.GraphSerializer
|
serializer_class = serializers.GraphSerializer
|
||||||
write_serializer_class = serializers.WritableGraphSerializer
|
|
||||||
filter_class = filters.GraphFilter
|
filter_class = filters.GraphFilter
|
||||||
|
|
||||||
|
|
||||||
@ -88,7 +89,6 @@ class ExportTemplateViewSet(ModelViewSet):
|
|||||||
class TopologyMapViewSet(ModelViewSet):
|
class TopologyMapViewSet(ModelViewSet):
|
||||||
queryset = TopologyMap.objects.select_related('site')
|
queryset = TopologyMap.objects.select_related('site')
|
||||||
serializer_class = serializers.TopologyMapSerializer
|
serializer_class = serializers.TopologyMapSerializer
|
||||||
write_serializer_class = serializers.WritableTopologyMapSerializer
|
|
||||||
filter_class = filters.TopologyMapFilter
|
filter_class = filters.TopologyMapFilter
|
||||||
|
|
||||||
@detail_route()
|
@detail_route()
|
||||||
@ -111,6 +111,16 @@ class TopologyMapViewSet(ModelViewSet):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Tags
|
||||||
|
#
|
||||||
|
|
||||||
|
class TagViewSet(ModelViewSet):
|
||||||
|
queryset = Tag.objects.annotate(tagged_items=Count('taggit_taggeditem_items'))
|
||||||
|
serializer_class = serializers.TagSerializer
|
||||||
|
filter_class = filters.TagFilter
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Image attachments
|
# Image attachments
|
||||||
#
|
#
|
||||||
@ -118,7 +128,6 @@ class TopologyMapViewSet(ModelViewSet):
|
|||||||
class ImageAttachmentViewSet(ModelViewSet):
|
class ImageAttachmentViewSet(ModelViewSet):
|
||||||
queryset = ImageAttachment.objects.all()
|
queryset = ImageAttachment.objects.all()
|
||||||
serializer_class = serializers.ImageAttachmentSerializer
|
serializer_class = serializers.ImageAttachmentSerializer
|
||||||
write_serializer_class = serializers.WritableImageAttachmentSerializer
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -3,6 +3,8 @@ from __future__ import unicode_literals
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models import Q
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
|
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
|
||||||
@ -85,6 +87,25 @@ class ExportTemplateFilter(django_filters.FilterSet):
|
|||||||
fields = ['content_type', 'name']
|
fields = ['content_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
class TagFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Tag
|
||||||
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(slug__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TopologyMapFilter(django_filters.FilterSet):
|
class TopologyMapFilter(django_filters.FilterSet):
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site',
|
||||||
|
@ -4,12 +4,17 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
|
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField, SlugField
|
||||||
from .constants import CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL
|
from .constants import CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL
|
||||||
from .models import CustomField, CustomFieldValue, ImageAttachment
|
from .models import CustomField, CustomFieldValue, ImageAttachment
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Custom fields
|
||||||
|
#
|
||||||
|
|
||||||
def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):
|
def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):
|
||||||
"""
|
"""
|
||||||
Retrieve all CustomFields applicable to the given ContentType
|
Retrieve all CustomFields applicable to the given ContentType
|
||||||
@ -162,6 +167,23 @@ class CustomFieldFilterForm(forms.Form):
|
|||||||
self.fields[name] = field
|
self.fields[name] = field
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Tags
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
class TagForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
slug = SlugField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Tag
|
||||||
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Image attachments
|
||||||
|
#
|
||||||
|
|
||||||
class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
29
netbox/extras/migrations/0011_django2.py
Normal file
29
netbox/extras/migrations/0011_django2.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 2.0.3 on 2018-03-30 14:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0010_customfield_filter_logic'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='obj_type',
|
||||||
|
field=models.ManyToManyField(help_text='The object(s) to which this field applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'tenant', 'cluster', 'virtualmachine')}, related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfieldchoice',
|
||||||
|
name='field',
|
||||||
|
field=models.ForeignKey(limit_choices_to={'type': 600}, on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='exporttemplate',
|
||||||
|
name='content_type',
|
||||||
|
field=models.ForeignKey(limit_choices_to={'model__in': ['provider', 'circuit', 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', 'consoleport', 'powerport', 'interfaceconnection', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'tenant', 'cluster', 'virtualmachine']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
|
||||||
|
),
|
||||||
|
]
|
@ -122,7 +122,8 @@ class CustomField(models.Model):
|
|||||||
label = models.CharField(
|
label = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text='Name of the field as displayed to users (if not provided, the field\'s name will be used)'
|
help_text='Name of the field as displayed to users (if not provided, '
|
||||||
|
'the field\'s name will be used)'
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
@ -130,17 +131,20 @@ class CustomField(models.Model):
|
|||||||
)
|
)
|
||||||
required = models.BooleanField(
|
required = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text='If true, this field is required when creating new objects or editing an existing object.'
|
help_text='If true, this field is required when creating new objects '
|
||||||
|
'or editing an existing object.'
|
||||||
)
|
)
|
||||||
filter_logic = models.PositiveSmallIntegerField(
|
filter_logic = models.PositiveSmallIntegerField(
|
||||||
choices=CF_FILTER_CHOICES,
|
choices=CF_FILTER_CHOICES,
|
||||||
default=CF_FILTER_LOOSE,
|
default=CF_FILTER_LOOSE,
|
||||||
help_text="Loose matches any instance of a given string; exact matches the entire field."
|
help_text='Loose matches any instance of a given string; exact '
|
||||||
|
'matches the entire field.'
|
||||||
)
|
)
|
||||||
default = models.CharField(
|
default = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text='Default value for the field. Use "true" or "false" for booleans. N/A for selection fields.'
|
help_text='Default value for the field. Use "true" or "false" for '
|
||||||
|
'booleans. N/A for selection fields.'
|
||||||
)
|
)
|
||||||
weight = models.PositiveSmallIntegerField(
|
weight = models.PositiveSmallIntegerField(
|
||||||
default=100,
|
default=100,
|
||||||
@ -192,11 +196,24 @@ class CustomField(models.Model):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class CustomFieldValue(models.Model):
|
class CustomFieldValue(models.Model):
|
||||||
field = models.ForeignKey('CustomField', related_name='values', on_delete=models.CASCADE)
|
field = models.ForeignKey(
|
||||||
obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT)
|
to='extras.CustomField',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='values'
|
||||||
|
)
|
||||||
|
obj_type = models.ForeignKey(
|
||||||
|
to=ContentType,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='+'
|
||||||
|
)
|
||||||
obj_id = models.PositiveIntegerField()
|
obj_id = models.PositiveIntegerField()
|
||||||
obj = GenericForeignKey('obj_type', 'obj_id')
|
obj = GenericForeignKey(
|
||||||
serialized_value = models.CharField(max_length=255)
|
ct_field='obj_type',
|
||||||
|
fk_field='obj_id'
|
||||||
|
)
|
||||||
|
serialized_value = models.CharField(
|
||||||
|
max_length=255
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['obj_type', 'obj_id']
|
ordering = ['obj_type', 'obj_id']
|
||||||
@ -223,10 +240,19 @@ class CustomFieldValue(models.Model):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class CustomFieldChoice(models.Model):
|
class CustomFieldChoice(models.Model):
|
||||||
field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT},
|
field = models.ForeignKey(
|
||||||
on_delete=models.CASCADE)
|
to='extras.CustomField',
|
||||||
value = models.CharField(max_length=100)
|
on_delete=models.CASCADE,
|
||||||
weight = models.PositiveSmallIntegerField(default=100, help_text="Higher weights appear lower in the list")
|
related_name='choices',
|
||||||
|
limit_choices_to={'type': CF_TYPE_SELECT}
|
||||||
|
)
|
||||||
|
value = models.CharField(
|
||||||
|
max_length=100
|
||||||
|
)
|
||||||
|
weight = models.PositiveSmallIntegerField(
|
||||||
|
default=100,
|
||||||
|
help_text='Higher weights appear lower in the list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['field', 'weight', 'value']
|
ordering = ['field', 'weight', 'value']
|
||||||
@ -252,11 +278,24 @@ class CustomFieldChoice(models.Model):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Graph(models.Model):
|
class Graph(models.Model):
|
||||||
type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
|
type = models.PositiveSmallIntegerField(
|
||||||
weight = models.PositiveSmallIntegerField(default=1000)
|
choices=GRAPH_TYPE_CHOICES
|
||||||
name = models.CharField(max_length=100, verbose_name='Name')
|
)
|
||||||
source = models.CharField(max_length=500, verbose_name='Source URL')
|
weight = models.PositiveSmallIntegerField(
|
||||||
link = models.URLField(verbose_name='Link URL', blank=True)
|
default=1000
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
verbose_name='Name'
|
||||||
|
)
|
||||||
|
source = models.CharField(
|
||||||
|
max_length=500,
|
||||||
|
verbose_name='Source URL'
|
||||||
|
)
|
||||||
|
link = models.URLField(
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Link URL'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['type', 'weight', 'name']
|
ordering = ['type', 'weight', 'name']
|
||||||
@ -282,13 +321,26 @@ class Graph(models.Model):
|
|||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ExportTemplate(models.Model):
|
class ExportTemplate(models.Model):
|
||||||
content_type = models.ForeignKey(
|
content_type = models.ForeignKey(
|
||||||
ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}, on_delete=models.CASCADE
|
to=ContentType,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
)
|
)
|
||||||
name = models.CharField(max_length=100)
|
|
||||||
description = models.CharField(max_length=200, blank=True)
|
|
||||||
template_code = models.TextField()
|
template_code = models.TextField()
|
||||||
mime_type = models.CharField(max_length=15, blank=True)
|
mime_type = models.CharField(
|
||||||
file_extension = models.CharField(max_length=15, blank=True)
|
max_length=15,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
file_extension = models.CharField(
|
||||||
|
max_length=15,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['content_type', 'name']
|
ordering = ['content_type', 'name']
|
||||||
@ -327,25 +379,35 @@ class ExportTemplate(models.Model):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class TopologyMap(models.Model):
|
class TopologyMap(models.Model):
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=50,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
slug = models.SlugField(
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
type = models.PositiveSmallIntegerField(
|
type = models.PositiveSmallIntegerField(
|
||||||
choices=TOPOLOGYMAP_TYPE_CHOICES,
|
choices=TOPOLOGYMAP_TYPE_CHOICES,
|
||||||
default=TOPOLOGYMAP_TYPE_NETWORK
|
default=TOPOLOGYMAP_TYPE_NETWORK
|
||||||
)
|
)
|
||||||
site = models.ForeignKey(
|
site = models.ForeignKey(
|
||||||
to='dcim.Site',
|
to='dcim.Site',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
related_name='topology_maps',
|
related_name='topology_maps',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True
|
||||||
on_delete=models.CASCADE
|
|
||||||
)
|
)
|
||||||
device_patterns = models.TextField(
|
device_patterns = models.TextField(
|
||||||
help_text="Identify devices to include in the diagram using regular expressions, one per line. Each line will "
|
help_text='Identify devices to include in the diagram using regular '
|
||||||
"result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. "
|
'expressions, one per line. Each line will result in a new '
|
||||||
"Devices will be rendered in the order they are defined."
|
'tier of the drawing. Separate multiple regexes within a '
|
||||||
|
'line using semicolons. Devices will be rendered in the '
|
||||||
|
'order they are defined.'
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
)
|
)
|
||||||
description = models.CharField(max_length=100, blank=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
@ -481,14 +543,29 @@ class ImageAttachment(models.Model):
|
|||||||
"""
|
"""
|
||||||
An uploaded image which is associated with an object.
|
An uploaded image which is associated with an object.
|
||||||
"""
|
"""
|
||||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
content_type = models.ForeignKey(
|
||||||
|
to=ContentType,
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
object_id = models.PositiveIntegerField()
|
object_id = models.PositiveIntegerField()
|
||||||
parent = GenericForeignKey('content_type', 'object_id')
|
parent = GenericForeignKey(
|
||||||
image = models.ImageField(upload_to=image_upload, height_field='image_height', width_field='image_width')
|
ct_field='content_type',
|
||||||
|
fk_field='object_id'
|
||||||
|
)
|
||||||
|
image = models.ImageField(
|
||||||
|
upload_to=image_upload,
|
||||||
|
height_field='image_height',
|
||||||
|
width_field='image_width'
|
||||||
|
)
|
||||||
image_height = models.PositiveSmallIntegerField()
|
image_height = models.PositiveSmallIntegerField()
|
||||||
image_width = models.PositiveSmallIntegerField()
|
image_width = models.PositiveSmallIntegerField()
|
||||||
name = models.CharField(max_length=50, blank=True)
|
name = models.CharField(
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
max_length=50,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
created = models.DateTimeField(
|
||||||
|
auto_now_add=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
@ -531,9 +608,20 @@ class ReportResult(models.Model):
|
|||||||
"""
|
"""
|
||||||
This model stores the results from running a user-defined report.
|
This model stores the results from running a user-defined report.
|
||||||
"""
|
"""
|
||||||
report = models.CharField(max_length=255, unique=True)
|
report = models.CharField(
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
max_length=255,
|
||||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='+', blank=True, null=True)
|
unique=True
|
||||||
|
)
|
||||||
|
created = models.DateTimeField(
|
||||||
|
auto_now_add=True
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
to=User,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='+',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
failed = models.BooleanField()
|
failed = models.BooleanField()
|
||||||
data = JSONField()
|
data = JSONField()
|
||||||
|
|
||||||
@ -593,12 +681,29 @@ class UserAction(models.Model):
|
|||||||
"""
|
"""
|
||||||
A record of an action (add, edit, or delete) performed on an object by a User.
|
A record of an action (add, edit, or delete) performed on an object by a User.
|
||||||
"""
|
"""
|
||||||
time = models.DateTimeField(auto_now_add=True, editable=False)
|
time = models.DateTimeField(
|
||||||
user = models.ForeignKey(User, related_name='actions', on_delete=models.CASCADE)
|
auto_now_add=True,
|
||||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
editable=False
|
||||||
object_id = models.PositiveIntegerField(blank=True, null=True)
|
)
|
||||||
action = models.PositiveSmallIntegerField(choices=ACTION_CHOICES)
|
user = models.ForeignKey(
|
||||||
message = models.TextField(blank=True)
|
to=User,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='actions'
|
||||||
|
)
|
||||||
|
content_type = models.ForeignKey(
|
||||||
|
to=ContentType,
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
object_id = models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
action = models.PositiveSmallIntegerField(
|
||||||
|
choices=ACTION_CHOICES
|
||||||
|
)
|
||||||
|
message = models.TextField(
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
objects = UserActionManager()
|
objects = UserActionManager()
|
||||||
|
|
||||||
|
28
netbox/extras/tables.py
Normal file
28
netbox/extras/tables.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django_tables2 as tables
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
|
from utilities.tables import BaseTable, ToggleColumn
|
||||||
|
|
||||||
|
TAG_ACTIONS = """
|
||||||
|
{% if perms.taggit.change_tag %}
|
||||||
|
<a href="{% url 'extras:tag_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.taggit.delete_tag %}
|
||||||
|
<a href="{% url 'extras:tag_delete' slug=record.slug %}" class="btn btn-xs btn-danger"><i class="glyphicon glyphicon-trash" aria-hidden="true"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TagTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
actions = tables.TemplateColumn(
|
||||||
|
template_code=TAG_ACTIONS,
|
||||||
|
attrs={'td': {'class': 'text-right'}},
|
||||||
|
verbose_name=''
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = Tag
|
||||||
|
fields = ('pk', 'name', 'items')
|
@ -5,6 +5,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from extras.constants import GRAPH_TYPE_SITE
|
from extras.constants import GRAPH_TYPE_SITE
|
||||||
@ -226,3 +227,96 @@ class ExportTemplateTest(HttpStatusMixin, APITestCase):
|
|||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||||
self.assertEqual(ExportTemplate.objects.count(), 2)
|
self.assertEqual(ExportTemplate.objects.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class TagTest(HttpStatusMixin, APITestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
|
||||||
|
user = User.objects.create(username='testuser', is_superuser=True)
|
||||||
|
token = Token.objects.create(user=user)
|
||||||
|
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||||
|
|
||||||
|
self.tag1 = Tag.objects.create(name='Test Tag 1', slug='test-tag-1')
|
||||||
|
self.tag2 = Tag.objects.create(name='Test Tag 2', slug='test-tag-2')
|
||||||
|
self.tag3 = Tag.objects.create(name='Test Tag 3', slug='test-tag-3')
|
||||||
|
|
||||||
|
def test_get_tag(self):
|
||||||
|
|
||||||
|
url = reverse('extras-api:tag-detail', kwargs={'pk': self.tag1.pk})
|
||||||
|
response = self.client.get(url, **self.header)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['name'], self.tag1.name)
|
||||||
|
|
||||||
|
def test_list_tags(self):
|
||||||
|
|
||||||
|
url = reverse('extras-api:tag-list')
|
||||||
|
response = self.client.get(url, **self.header)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['count'], 3)
|
||||||
|
|
||||||
|
def test_create_tag(self):
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'name': 'Test Tag 4',
|
||||||
|
'slug': 'test-tag-4',
|
||||||
|
}
|
||||||
|
|
||||||
|
url = reverse('extras-api:tag-list')
|
||||||
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
|
||||||
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(Tag.objects.count(), 4)
|
||||||
|
tag4 = Tag.objects.get(pk=response.data['id'])
|
||||||
|
self.assertEqual(tag4.name, data['name'])
|
||||||
|
self.assertEqual(tag4.slug, data['slug'])
|
||||||
|
|
||||||
|
def test_create_tag_bulk(self):
|
||||||
|
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
'name': 'Test Tag 4',
|
||||||
|
'slug': 'test-tag-4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Test Tag 5',
|
||||||
|
'slug': 'test-tag-5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Test Tag 6',
|
||||||
|
'slug': 'test-tag-6',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
url = reverse('extras-api:tag-list')
|
||||||
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
|
||||||
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(Tag.objects.count(), 6)
|
||||||
|
self.assertEqual(response.data[0]['name'], data[0]['name'])
|
||||||
|
self.assertEqual(response.data[1]['name'], data[1]['name'])
|
||||||
|
self.assertEqual(response.data[2]['name'], data[2]['name'])
|
||||||
|
|
||||||
|
def test_update_tag(self):
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'name': 'Test Tag X',
|
||||||
|
'slug': 'test-tag-x',
|
||||||
|
}
|
||||||
|
|
||||||
|
url = reverse('extras-api:tag-detail', kwargs={'pk': self.tag1.pk})
|
||||||
|
response = self.client.put(url, data, format='json', **self.header)
|
||||||
|
|
||||||
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(Tag.objects.count(), 3)
|
||||||
|
tag1 = Tag.objects.get(pk=response.data['id'])
|
||||||
|
self.assertEqual(tag1.name, data['name'])
|
||||||
|
self.assertEqual(tag1.slug, data['slug'])
|
||||||
|
|
||||||
|
def test_delete_tag(self):
|
||||||
|
|
||||||
|
url = reverse('extras-api:tag-detail', kwargs={'pk': self.tag1.pk})
|
||||||
|
response = self.client.delete(url, **self.header)
|
||||||
|
|
||||||
|
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||||
|
self.assertEqual(Tag.objects.count(), 2)
|
||||||
|
@ -45,7 +45,7 @@ class CustomFieldTest(TestCase):
|
|||||||
# Create a custom field
|
# Create a custom field
|
||||||
cf = CustomField(type=data['field_type'], name='my_field', required=False)
|
cf = CustomField(type=data['field_type'], name='my_field', required=False)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.obj_type = [obj_type]
|
cf.obj_type.set([obj_type])
|
||||||
cf.save()
|
cf.save()
|
||||||
|
|
||||||
# Assign a value to the first Site
|
# Assign a value to the first Site
|
||||||
@ -73,7 +73,7 @@ class CustomFieldTest(TestCase):
|
|||||||
# Create a custom field
|
# Create a custom field
|
||||||
cf = CustomField(type=CF_TYPE_SELECT, name='my_field', required=False)
|
cf = CustomField(type=CF_TYPE_SELECT, name='my_field', required=False)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.obj_type = [obj_type]
|
cf.obj_type.set([obj_type])
|
||||||
cf.save()
|
cf.save()
|
||||||
|
|
||||||
# Create some choices for the field
|
# Create some choices for the field
|
||||||
@ -115,37 +115,37 @@ class CustomFieldAPITest(HttpStatusMixin, APITestCase):
|
|||||||
# Text custom field
|
# Text custom field
|
||||||
self.cf_text = CustomField(type=CF_TYPE_TEXT, name='magic_word')
|
self.cf_text = CustomField(type=CF_TYPE_TEXT, name='magic_word')
|
||||||
self.cf_text.save()
|
self.cf_text.save()
|
||||||
self.cf_text.obj_type = [content_type]
|
self.cf_text.obj_type.set([content_type])
|
||||||
self.cf_text.save()
|
self.cf_text.save()
|
||||||
|
|
||||||
# Integer custom field
|
# Integer custom field
|
||||||
self.cf_integer = CustomField(type=CF_TYPE_INTEGER, name='magic_number')
|
self.cf_integer = CustomField(type=CF_TYPE_INTEGER, name='magic_number')
|
||||||
self.cf_integer.save()
|
self.cf_integer.save()
|
||||||
self.cf_integer.obj_type = [content_type]
|
self.cf_integer.obj_type.set([content_type])
|
||||||
self.cf_integer.save()
|
self.cf_integer.save()
|
||||||
|
|
||||||
# Boolean custom field
|
# Boolean custom field
|
||||||
self.cf_boolean = CustomField(type=CF_TYPE_BOOLEAN, name='is_magic')
|
self.cf_boolean = CustomField(type=CF_TYPE_BOOLEAN, name='is_magic')
|
||||||
self.cf_boolean.save()
|
self.cf_boolean.save()
|
||||||
self.cf_boolean.obj_type = [content_type]
|
self.cf_boolean.obj_type.set([content_type])
|
||||||
self.cf_boolean.save()
|
self.cf_boolean.save()
|
||||||
|
|
||||||
# Date custom field
|
# Date custom field
|
||||||
self.cf_date = CustomField(type=CF_TYPE_DATE, name='magic_date')
|
self.cf_date = CustomField(type=CF_TYPE_DATE, name='magic_date')
|
||||||
self.cf_date.save()
|
self.cf_date.save()
|
||||||
self.cf_date.obj_type = [content_type]
|
self.cf_date.obj_type.set([content_type])
|
||||||
self.cf_date.save()
|
self.cf_date.save()
|
||||||
|
|
||||||
# URL custom field
|
# URL custom field
|
||||||
self.cf_url = CustomField(type=CF_TYPE_URL, name='magic_url')
|
self.cf_url = CustomField(type=CF_TYPE_URL, name='magic_url')
|
||||||
self.cf_url.save()
|
self.cf_url.save()
|
||||||
self.cf_url.obj_type = [content_type]
|
self.cf_url.obj_type.set([content_type])
|
||||||
self.cf_url.save()
|
self.cf_url.save()
|
||||||
|
|
||||||
# Select custom field
|
# Select custom field
|
||||||
self.cf_select = CustomField(type=CF_TYPE_SELECT, name='magic_choice')
|
self.cf_select = CustomField(type=CF_TYPE_SELECT, name='magic_choice')
|
||||||
self.cf_select.save()
|
self.cf_select.save()
|
||||||
self.cf_select.obj_type = [content_type]
|
self.cf_select.obj_type.set([content_type])
|
||||||
self.cf_select.save()
|
self.cf_select.save()
|
||||||
self.cf_select_choice1 = CustomFieldChoice(field=self.cf_select, value='Foo')
|
self.cf_select_choice1 = CustomFieldChoice(field=self.cf_select, value='Foo')
|
||||||
self.cf_select_choice1.save()
|
self.cf_select_choice1.save()
|
||||||
|
@ -7,6 +7,12 @@ from extras import views
|
|||||||
app_name = 'extras'
|
app_name = 'extras'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
||||||
|
# Tags
|
||||||
|
url(r'^tags/$', views.TagListView.as_view(), name='tag_list'),
|
||||||
|
url(r'^tags/(?P<slug>[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'),
|
||||||
|
url(r'^tags/(?P<slug>[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||||
|
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||||
|
|
||||||
# Image attachments
|
# Image attachments
|
||||||
url(r'^image-attachments/(?P<pk>\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
|
url(r'^image-attachments/(?P<pk>\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
|
||||||
url(r'^image-attachments/(?P<pk>\d+)/delete/$', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
|
url(r'^image-attachments/(?P<pk>\d+)/delete/$', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
|
||||||
|
@ -2,16 +2,52 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
from django.db.models import Count
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render, reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.views import ObjectDeleteView, ObjectEditView
|
from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView
|
||||||
from .forms import ImageAttachmentForm
|
from .forms import ImageAttachmentForm, TagForm
|
||||||
from .models import ImageAttachment, ReportResult, UserAction
|
from .models import ImageAttachment, ReportResult, UserAction
|
||||||
from .reports import get_report, get_reports
|
from .reports import get_report, get_reports
|
||||||
|
from .tables import TagTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Tags
|
||||||
|
#
|
||||||
|
|
||||||
|
class TagListView(ObjectListView):
|
||||||
|
queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name')
|
||||||
|
table = TagTable
|
||||||
|
template_name = 'extras/tag_list.html'
|
||||||
|
|
||||||
|
|
||||||
|
class TagEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
|
permission_required = 'taggit.change_tag'
|
||||||
|
model = Tag
|
||||||
|
model_form = TagForm
|
||||||
|
|
||||||
|
def get_return_url(self, request, obj):
|
||||||
|
return reverse('extras:tag', kwargs={'slug': obj.slug})
|
||||||
|
|
||||||
|
|
||||||
|
class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
|
permission_required = 'taggit.delete_tag'
|
||||||
|
model = Tag
|
||||||
|
default_return_url = 'extras:tag_list'
|
||||||
|
|
||||||
|
|
||||||
|
class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'circuits.delete_circuittype'
|
||||||
|
cls = Tag
|
||||||
|
queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name')
|
||||||
|
table = TagTable
|
||||||
|
default_return_url = 'extras:tag_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -5,6 +5,7 @@ from collections import OrderedDict
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
|
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
|
||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
@ -14,7 +15,9 @@ from ipam.constants import (
|
|||||||
)
|
)
|
||||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
from tenancy.api.serializers import NestedTenantSerializer
|
from tenancy.api.serializers import NestedTenantSerializer
|
||||||
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
from utilities.api import (
|
||||||
|
ChoiceFieldSerializer, SerializedPKRelatedField, TagField, ValidatedModelSerializer, WritableNestedSerializer,
|
||||||
|
)
|
||||||
from virtualization.api.serializers import NestedVirtualMachineSerializer
|
from virtualization.api.serializers import NestedVirtualMachineSerializer
|
||||||
|
|
||||||
|
|
||||||
@ -23,17 +26,18 @@ from virtualization.api.serializers import NestedVirtualMachineSerializer
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VRFSerializer(CustomFieldModelSerializer):
|
class VRFSerializer(CustomFieldModelSerializer):
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VRF
|
model = VRF
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'display_name', 'custom_fields', 'created',
|
'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'tags', 'display_name', 'custom_fields',
|
||||||
'last_updated',
|
'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class NestedVRFSerializer(serializers.ModelSerializer):
|
class NestedVRFSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -41,15 +45,6 @@ class NestedVRFSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'name', 'rd']
|
fields = ['id', 'url', 'name', 'rd']
|
||||||
|
|
||||||
|
|
||||||
class WritableVRFSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = VRF
|
|
||||||
fields = [
|
|
||||||
'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_fields', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Roles
|
# Roles
|
||||||
#
|
#
|
||||||
@ -61,7 +56,7 @@ class RoleSerializer(ValidatedModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug', 'weight']
|
fields = ['id', 'name', 'slug', 'weight']
|
||||||
|
|
||||||
|
|
||||||
class NestedRoleSerializer(serializers.ModelSerializer):
|
class NestedRoleSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -80,7 +75,7 @@ class RIRSerializer(ValidatedModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug', 'is_private']
|
fields = ['id', 'name', 'slug', 'is_private']
|
||||||
|
|
||||||
|
|
||||||
class NestedRIRSerializer(serializers.ModelSerializer):
|
class NestedRIRSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -94,15 +89,18 @@ class NestedRIRSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class AggregateSerializer(CustomFieldModelSerializer):
|
class AggregateSerializer(CustomFieldModelSerializer):
|
||||||
rir = NestedRIRSerializer()
|
rir = NestedRIRSerializer()
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'family', 'prefix', 'rir', 'date_added', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'family', 'prefix', 'rir', 'date_added', 'description', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated',
|
||||||
]
|
]
|
||||||
|
read_only_fields = ['family']
|
||||||
|
|
||||||
|
|
||||||
class NestedAggregateSerializer(serializers.ModelSerializer):
|
class NestedAggregateSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
|
||||||
|
|
||||||
class Meta(AggregateSerializer.Meta):
|
class Meta(AggregateSerializer.Meta):
|
||||||
@ -110,34 +108,12 @@ class NestedAggregateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'family', 'prefix']
|
fields = ['id', 'url', 'family', 'prefix']
|
||||||
|
|
||||||
|
|
||||||
class WritableAggregateSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Aggregate
|
|
||||||
fields = ['id', 'prefix', 'rir', 'date_added', 'description', 'custom_fields', 'created', 'last_updated']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VLAN groups
|
# VLAN groups
|
||||||
#
|
#
|
||||||
|
|
||||||
class VLANGroupSerializer(serializers.ModelSerializer):
|
class VLANGroupSerializer(ValidatedModelSerializer):
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = VLANGroup
|
|
||||||
fields = ['id', 'name', 'slug', 'site']
|
|
||||||
|
|
||||||
|
|
||||||
class NestedVLANGroupSerializer(serializers.ModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = VLANGroup
|
|
||||||
fields = ['id', 'url', 'name', 'slug']
|
|
||||||
|
|
||||||
|
|
||||||
class WritableVLANGroupSerializer(serializers.ModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
@ -154,46 +130,37 @@ class WritableVLANGroupSerializer(serializers.ModelSerializer):
|
|||||||
validator(data)
|
validator(data)
|
||||||
|
|
||||||
# Enforce model validation
|
# Enforce model validation
|
||||||
super(WritableVLANGroupSerializer, self).validate(data)
|
super(VLANGroupSerializer, self).validate(data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class NestedVLANGroupSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VLANGroup
|
||||||
|
fields = ['id', 'url', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VLANs
|
# VLANs
|
||||||
#
|
#
|
||||||
|
|
||||||
class VLANSerializer(CustomFieldModelSerializer):
|
class VLANSerializer(CustomFieldModelSerializer):
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||||
group = NestedVLANGroupSerializer()
|
group = NestedVLANGroupSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
status = ChoiceFieldSerializer(choices=VLAN_STATUS_CHOICES)
|
status = ChoiceFieldSerializer(choices=VLAN_STATUS_CHOICES, required=False)
|
||||||
role = NestedRoleSerializer()
|
role = NestedRoleSerializer(required=False, allow_null=True)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name',
|
'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'tags', 'display_name',
|
||||||
'custom_fields', 'created', 'last_updated',
|
'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class NestedVLANSerializer(serializers.ModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = VLAN
|
|
||||||
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
|
||||||
|
|
||||||
|
|
||||||
class WritableVLANSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = VLAN
|
|
||||||
fields = [
|
|
||||||
'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'custom_fields', 'created',
|
|
||||||
'last_updated',
|
|
||||||
]
|
|
||||||
validators = []
|
validators = []
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
@ -206,32 +173,42 @@ class WritableVLANSerializer(CustomFieldModelSerializer):
|
|||||||
validator(data)
|
validator(data)
|
||||||
|
|
||||||
# Enforce model validation
|
# Enforce model validation
|
||||||
super(WritableVLANSerializer, self).validate(data)
|
super(VLANSerializer, self).validate(data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class NestedVLANSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VLAN
|
||||||
|
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Prefixes
|
# Prefixes
|
||||||
#
|
#
|
||||||
|
|
||||||
class PrefixSerializer(CustomFieldModelSerializer):
|
class PrefixSerializer(CustomFieldModelSerializer):
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||||
vrf = NestedVRFSerializer()
|
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
vlan = NestedVLANSerializer()
|
vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||||
status = ChoiceFieldSerializer(choices=PREFIX_STATUS_CHOICES)
|
status = ChoiceFieldSerializer(choices=PREFIX_STATUS_CHOICES, required=False)
|
||||||
role = NestedRoleSerializer()
|
role = NestedRoleSerializer(required=False, allow_null=True)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
|
'id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
|
||||||
'custom_fields', 'created', 'last_updated',
|
'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
read_only_fields = ['family']
|
||||||
|
|
||||||
|
|
||||||
class NestedPrefixSerializer(serializers.ModelSerializer):
|
class NestedPrefixSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -239,16 +216,6 @@ class NestedPrefixSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'family', 'prefix']
|
fields = ['id', 'url', 'family', 'prefix']
|
||||||
|
|
||||||
|
|
||||||
class WritablePrefixSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Prefix
|
|
||||||
fields = [
|
|
||||||
'id', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
|
|
||||||
'custom_fields', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class AvailablePrefixSerializer(serializers.Serializer):
|
class AvailablePrefixSerializer(serializers.Serializer):
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
@ -288,21 +255,23 @@ class IPAddressInterfaceSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class IPAddressSerializer(CustomFieldModelSerializer):
|
class IPAddressSerializer(CustomFieldModelSerializer):
|
||||||
vrf = NestedVRFSerializer()
|
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES)
|
status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES, required=False)
|
||||||
role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES)
|
role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES, required=False)
|
||||||
interface = IPAddressInterfaceSerializer()
|
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
|
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
|
||||||
'nat_outside', 'custom_fields', 'created', 'last_updated',
|
'nat_outside', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
read_only_fields = ['family']
|
||||||
|
|
||||||
|
|
||||||
class NestedIPAddressSerializer(serializers.ModelSerializer):
|
class NestedIPAddressSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -310,18 +279,8 @@ class NestedIPAddressSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'family', 'address']
|
fields = ['id', 'url', 'family', 'address']
|
||||||
|
|
||||||
|
|
||||||
IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer()
|
IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||||
IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer()
|
IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class WritableIPAddressSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = IPAddress
|
|
||||||
fields = [
|
|
||||||
'id', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
|
|
||||||
'custom_fields', 'created', 'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class AvailableIPSerializer(serializers.Serializer):
|
class AvailableIPSerializer(serializers.Serializer):
|
||||||
@ -342,22 +301,16 @@ class AvailableIPSerializer(serializers.Serializer):
|
|||||||
# Services
|
# Services
|
||||||
#
|
#
|
||||||
|
|
||||||
class ServiceSerializer(serializers.ModelSerializer):
|
class ServiceSerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||||
virtual_machine = NestedVirtualMachineSerializer()
|
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
|
||||||
protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES)
|
protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES)
|
||||||
ipaddresses = NestedIPAddressSerializer(many=True)
|
ipaddresses = SerializedPKRelatedField(
|
||||||
|
queryset=IPAddress.objects.all(),
|
||||||
class Meta:
|
serializer=NestedIPAddressSerializer,
|
||||||
model = Service
|
required=False,
|
||||||
fields = [
|
many=True
|
||||||
'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description', 'created',
|
)
|
||||||
'last_updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Figure out how to use model validation with ManyToManyFields. Calling clean() yields a ValueError.
|
|
||||||
class WritableServiceSerializer(serializers.ModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Service
|
model = Service
|
||||||
|
@ -35,7 +35,6 @@ class IPAMFieldChoicesViewSet(FieldChoicesViewSet):
|
|||||||
class VRFViewSet(CustomFieldModelViewSet):
|
class VRFViewSet(CustomFieldModelViewSet):
|
||||||
queryset = VRF.objects.select_related('tenant')
|
queryset = VRF.objects.select_related('tenant')
|
||||||
serializer_class = serializers.VRFSerializer
|
serializer_class = serializers.VRFSerializer
|
||||||
write_serializer_class = serializers.WritableVRFSerializer
|
|
||||||
filter_class = filters.VRFFilter
|
filter_class = filters.VRFFilter
|
||||||
|
|
||||||
|
|
||||||
@ -56,7 +55,6 @@ class RIRViewSet(ModelViewSet):
|
|||||||
class AggregateViewSet(CustomFieldModelViewSet):
|
class AggregateViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Aggregate.objects.select_related('rir')
|
queryset = Aggregate.objects.select_related('rir')
|
||||||
serializer_class = serializers.AggregateSerializer
|
serializer_class = serializers.AggregateSerializer
|
||||||
write_serializer_class = serializers.WritableAggregateSerializer
|
|
||||||
filter_class = filters.AggregateFilter
|
filter_class = filters.AggregateFilter
|
||||||
|
|
||||||
|
|
||||||
@ -77,7 +75,6 @@ class RoleViewSet(ModelViewSet):
|
|||||||
class PrefixViewSet(CustomFieldModelViewSet):
|
class PrefixViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||||
serializer_class = serializers.PrefixSerializer
|
serializer_class = serializers.PrefixSerializer
|
||||||
write_serializer_class = serializers.WritablePrefixSerializer
|
|
||||||
filter_class = filters.PrefixFilter
|
filter_class = filters.PrefixFilter
|
||||||
|
|
||||||
@detail_route(url_path='available-prefixes', methods=['get', 'post'])
|
@detail_route(url_path='available-prefixes', methods=['get', 'post'])
|
||||||
@ -120,9 +117,9 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
# Initialize the serializer with a list or a single object depending on what was requested
|
# Initialize the serializer with a list or a single object depending on what was requested
|
||||||
if isinstance(request.data, list):
|
if isinstance(request.data, list):
|
||||||
serializer = serializers.WritablePrefixSerializer(data=requested_prefixes, many=True)
|
serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True)
|
||||||
else:
|
else:
|
||||||
serializer = serializers.WritablePrefixSerializer(data=requested_prefixes[0])
|
serializer = serializers.PrefixSerializer(data=requested_prefixes[0])
|
||||||
|
|
||||||
# Create the new Prefix(es)
|
# Create the new Prefix(es)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
@ -177,9 +174,9 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
# Initialize the serializer with a list or a single object depending on what was requested
|
# Initialize the serializer with a list or a single object depending on what was requested
|
||||||
if isinstance(request.data, list):
|
if isinstance(request.data, list):
|
||||||
serializer = serializers.WritableIPAddressSerializer(data=requested_ips, many=True)
|
serializer = serializers.IPAddressSerializer(data=requested_ips, many=True)
|
||||||
else:
|
else:
|
||||||
serializer = serializers.WritableIPAddressSerializer(data=requested_ips[0])
|
serializer = serializers.IPAddressSerializer(data=requested_ips[0])
|
||||||
|
|
||||||
# Create the new IP address(es)
|
# Create the new IP address(es)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
@ -223,7 +220,6 @@ class IPAddressViewSet(CustomFieldModelViewSet):
|
|||||||
'nat_outside'
|
'nat_outside'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.IPAddressSerializer
|
serializer_class = serializers.IPAddressSerializer
|
||||||
write_serializer_class = serializers.WritableIPAddressSerializer
|
|
||||||
filter_class = filters.IPAddressFilter
|
filter_class = filters.IPAddressFilter
|
||||||
|
|
||||||
|
|
||||||
@ -234,7 +230,6 @@ class IPAddressViewSet(CustomFieldModelViewSet):
|
|||||||
class VLANGroupViewSet(ModelViewSet):
|
class VLANGroupViewSet(ModelViewSet):
|
||||||
queryset = VLANGroup.objects.select_related('site')
|
queryset = VLANGroup.objects.select_related('site')
|
||||||
serializer_class = serializers.VLANGroupSerializer
|
serializer_class = serializers.VLANGroupSerializer
|
||||||
write_serializer_class = serializers.WritableVLANGroupSerializer
|
|
||||||
filter_class = filters.VLANGroupFilter
|
filter_class = filters.VLANGroupFilter
|
||||||
|
|
||||||
|
|
||||||
@ -245,7 +240,6 @@ class VLANGroupViewSet(ModelViewSet):
|
|||||||
class VLANViewSet(CustomFieldModelViewSet):
|
class VLANViewSet(CustomFieldModelViewSet):
|
||||||
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
|
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
|
||||||
serializer_class = serializers.VLANSerializer
|
serializer_class = serializers.VLANSerializer
|
||||||
write_serializer_class = serializers.WritableVLANSerializer
|
|
||||||
filter_class = filters.VLANFilter
|
filter_class = filters.VLANFilter
|
||||||
|
|
||||||
|
|
||||||
@ -256,5 +250,4 @@ class VLANViewSet(CustomFieldModelViewSet):
|
|||||||
class ServiceViewSet(ModelViewSet):
|
class ServiceViewSet(ModelViewSet):
|
||||||
queryset = Service.objects.select_related('device')
|
queryset = Service.objects.select_related('device')
|
||||||
serializer_class = serializers.ServiceSerializer
|
serializer_class = serializers.ServiceSerializer
|
||||||
write_serializer_class = serializers.WritableServiceSerializer
|
|
||||||
filter_class = filters.ServiceFilter
|
filter_class = filters.ServiceFilter
|
||||||
|
@ -30,6 +30,9 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -69,6 +72,9 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='RIR (slug)',
|
label='RIR (slug)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
@ -167,6 +173,9 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
choices=PREFIX_STATUS_CHOICES,
|
choices=PREFIX_STATUS_CHOICES,
|
||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
@ -289,6 +298,9 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
role = django_filters.MultipleChoiceFilter(
|
role = django_filters.MultipleChoiceFilter(
|
||||||
choices=IPADDRESS_ROLE_CHOICES
|
choices=IPADDRESS_ROLE_CHOICES
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
@ -394,6 +406,9 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
choices=VLAN_STATUS_CHOICES,
|
choices=VLAN_STATUS_CHOICES,
|
||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import MultipleObjectsReturned
|
from django.core.exceptions import MultipleObjectsReturned
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
from taggit.forms import TagField
|
||||||
|
|
||||||
from dcim.models import Site, Rack, Device, Interface
|
from dcim.models import Site, Rack, Device, Interface
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
@ -32,10 +33,11 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 129)]
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VRF
|
model = VRF
|
||||||
fields = ['name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant']
|
fields = ['name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant', 'tags']
|
||||||
labels = {
|
labels = {
|
||||||
'rd': "RD",
|
'rd': "RD",
|
||||||
}
|
}
|
||||||
@ -121,10 +123,11 @@ class RIRFilterForm(BootstrapMixin, forms.Form):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class AggregateForm(BootstrapMixin, CustomFieldForm):
|
class AggregateForm(BootstrapMixin, CustomFieldForm):
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
fields = ['prefix', 'rir', 'date_added', 'description']
|
fields = ['prefix', 'rir', 'date_added', 'description', 'tags']
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'prefix': "IPv4 or IPv6 network",
|
'prefix': "IPv4 or IPv6 network",
|
||||||
'rir': "Regional Internet Registry responsible for this prefix",
|
'rir': "Regional Internet Registry responsible for this prefix",
|
||||||
@ -228,10 +231,14 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}', display_field='display_name'
|
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}', display_field='display_name'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant']
|
fields = [
|
||||||
|
'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant',
|
||||||
|
'tags',
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
@ -455,12 +462,13 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
primary_for_parent = forms.BooleanField(required=False, label='Make this the primary IP for the device/VM')
|
primary_for_parent = forms.BooleanField(required=False, label='Make this the primary IP for the device/VM')
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = [
|
fields = [
|
||||||
'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_parent', 'nat_site',
|
'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_parent', 'nat_site',
|
||||||
'nat_rack', 'nat_inside', 'tenant_group', 'tenant',
|
'nat_rack', 'nat_inside', 'tenant_group', 'tenant', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -508,7 +516,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
|
|||||||
|
|
||||||
ipaddress = super(IPAddressForm, self).save(*args, **kwargs)
|
ipaddress = super(IPAddressForm, self).save(*args, **kwargs)
|
||||||
|
|
||||||
# Assign this IPAddress as the primary for the associated Device.
|
# Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
|
||||||
if self.cleaned_data['primary_for_parent']:
|
if self.cleaned_data['primary_for_parent']:
|
||||||
parent = self.cleaned_data['interface'].parent
|
parent = self.cleaned_data['interface'].parent
|
||||||
if ipaddress.address.version == 4:
|
if ipaddress.address.version == 4:
|
||||||
@ -516,14 +524,12 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
|
|||||||
else:
|
else:
|
||||||
parent.primary_ip6 = ipaddress
|
parent.primary_ip6 = ipaddress
|
||||||
parent.save()
|
parent.save()
|
||||||
|
|
||||||
# Clear assignment as primary for device if set.
|
|
||||||
elif self.cleaned_data['interface']:
|
elif self.cleaned_data['interface']:
|
||||||
parent = self.cleaned_data['interface'].parent
|
parent = self.cleaned_data['interface'].parent
|
||||||
if ipaddress.address.version == 4 and parent.primary_ip4 == self:
|
if ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress:
|
||||||
parent.primary_ip4 = None
|
parent.primary_ip4 = None
|
||||||
parent.save()
|
parent.save()
|
||||||
elif ipaddress.address.version == 6 and parent.primary_ip6 == self:
|
elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress:
|
||||||
parent.primary_ip6 = None
|
parent.primary_ip6 = None
|
||||||
parent.save()
|
parent.save()
|
||||||
|
|
||||||
@ -782,10 +788,11 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
|
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ['site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant']
|
fields = ['site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags']
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'site': "Leave blank if this VLAN spans multiple sites",
|
'site': "Leave blank if this VLAN spans multiple sites",
|
||||||
'group': "VLAN group (optional)",
|
'group': "VLAN group (optional)",
|
||||||
|
42
netbox/ipam/migrations/0022_tags.py
Normal file
42
netbox/ipam/migrations/0022_tags.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-05-22 19:04
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('taggit', '0002_auto_20150616_2121'),
|
||||||
|
('ipam', '0021_vrf_ordering'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='aggregate',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ipaddress',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vlan',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vrf',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
]
|
@ -10,10 +10,10 @@ from django.db.models import Q
|
|||||||
from django.db.models.expressions import RawSQL
|
from django.db.models.expressions import RawSQL
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
from extras.models import CustomFieldModel, CustomFieldValue
|
from extras.models import CustomFieldModel
|
||||||
from tenancy.models import Tenant
|
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .fields import IPNetworkField, IPAddressField
|
from .fields import IPNetworkField, IPAddressField
|
||||||
@ -27,13 +27,37 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
table). Prefixes and IPAddresses can optionally be assigned to VRFs. (Prefixes and IPAddresses not assigned to a VRF
|
table). Prefixes and IPAddresses can optionally be assigned to VRFs. (Prefixes and IPAddresses not assigned to a VRF
|
||||||
are said to exist in the "global" table.)
|
are said to exist in the "global" table.)
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(
|
||||||
rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher')
|
max_length=50
|
||||||
tenant = models.ForeignKey(Tenant, related_name='vrfs', blank=True, null=True, on_delete=models.PROTECT)
|
)
|
||||||
enforce_unique = models.BooleanField(default=True, verbose_name='Enforce unique space',
|
rd = models.CharField(
|
||||||
help_text="Prevent duplicate prefixes/IP addresses within this VRF")
|
max_length=21,
|
||||||
description = models.CharField(max_length=100, blank=True)
|
unique=True,
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
verbose_name='Route distinguisher'
|
||||||
|
)
|
||||||
|
tenant = models.ForeignKey(
|
||||||
|
to='tenancy.Tenant',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='vrfs',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
enforce_unique = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
verbose_name='Enforce unique space',
|
||||||
|
help_text='Prevent duplicate prefixes/IP addresses within this VRF'
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||||
|
|
||||||
@ -74,10 +98,18 @@ class RIR(models.Model):
|
|||||||
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
|
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
|
||||||
space. This can be an organization like ARIN or RIPE, or a governing standard such as RFC 1918.
|
space. This can be an organization like ARIN or RIPE, or a governing standard such as RFC 1918.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=50,
|
||||||
is_private = models.BooleanField(default=False, verbose_name='Private',
|
unique=True
|
||||||
help_text='IP space managed by this RIR is considered private')
|
)
|
||||||
|
slug = models.SlugField(
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
is_private = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name='Private',
|
||||||
|
help_text='IP space managed by this RIR is considered private'
|
||||||
|
)
|
||||||
|
|
||||||
csv_headers = ['name', 'slug', 'is_private']
|
csv_headers = ['name', 'slug', 'is_private']
|
||||||
|
|
||||||
@ -106,12 +138,31 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
|
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
|
||||||
the hierarchy and track the overall utilization of available address space. Each Aggregate is assigned to a RIR.
|
the hierarchy and track the overall utilization of available address space. Each Aggregate is assigned to a RIR.
|
||||||
"""
|
"""
|
||||||
family = models.PositiveSmallIntegerField(choices=AF_CHOICES)
|
family = models.PositiveSmallIntegerField(
|
||||||
|
choices=AF_CHOICES
|
||||||
|
)
|
||||||
prefix = IPNetworkField()
|
prefix = IPNetworkField()
|
||||||
rir = models.ForeignKey('RIR', related_name='aggregates', on_delete=models.PROTECT, verbose_name='RIR')
|
rir = models.ForeignKey(
|
||||||
date_added = models.DateField(blank=True, null=True)
|
to='ipam.RIR',
|
||||||
description = models.CharField(max_length=100, blank=True)
|
on_delete=models.PROTECT,
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
related_name='aggregates',
|
||||||
|
verbose_name='RIR'
|
||||||
|
)
|
||||||
|
date_added = models.DateField(
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = ['prefix', 'rir', 'date_added', 'description']
|
csv_headers = ['prefix', 'rir', 'date_added', 'description']
|
||||||
|
|
||||||
@ -186,9 +237,16 @@ class Role(models.Model):
|
|||||||
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
|
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
|
||||||
"Management."
|
"Management."
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=50,
|
||||||
weight = models.PositiveSmallIntegerField(default=1000)
|
unique=True
|
||||||
|
)
|
||||||
|
slug = models.SlugField(
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
weight = models.PositiveSmallIntegerField(
|
||||||
|
default=1000
|
||||||
|
)
|
||||||
|
|
||||||
csv_headers = ['name', 'slug', 'weight']
|
csv_headers = ['name', 'slug', 'weight']
|
||||||
|
|
||||||
@ -213,24 +271,74 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role. A Prefix can also be
|
VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role. A Prefix can also be
|
||||||
assigned to a VLAN where appropriate.
|
assigned to a VLAN where appropriate.
|
||||||
"""
|
"""
|
||||||
family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False)
|
family = models.PositiveSmallIntegerField(
|
||||||
prefix = IPNetworkField(help_text="IPv4 or IPv6 network with mask")
|
choices=AF_CHOICES,
|
||||||
site = models.ForeignKey('dcim.Site', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True)
|
editable=False
|
||||||
vrf = models.ForeignKey('VRF', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
|
)
|
||||||
verbose_name='VRF')
|
prefix = IPNetworkField(
|
||||||
tenant = models.ForeignKey(Tenant, related_name='prefixes', blank=True, null=True, on_delete=models.PROTECT)
|
help_text='IPv4 or IPv6 network with mask'
|
||||||
vlan = models.ForeignKey('VLAN', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
|
)
|
||||||
verbose_name='VLAN')
|
site = models.ForeignKey(
|
||||||
status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=PREFIX_STATUS_ACTIVE,
|
to='dcim.Site',
|
||||||
help_text="Operational status of this prefix")
|
on_delete=models.PROTECT,
|
||||||
role = models.ForeignKey('Role', related_name='prefixes', on_delete=models.SET_NULL, blank=True, null=True,
|
related_name='prefixes',
|
||||||
help_text="The primary function of this prefix")
|
blank=True,
|
||||||
is_pool = models.BooleanField(verbose_name='Is a pool', default=False,
|
null=True
|
||||||
help_text="All IP addresses within this prefix are considered usable")
|
)
|
||||||
description = models.CharField(max_length=100, blank=True)
|
vrf = models.ForeignKey(
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
to='ipam.VRF',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='prefixes',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='VRF'
|
||||||
|
)
|
||||||
|
tenant = models.ForeignKey(
|
||||||
|
to='tenancy.Tenant',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='prefixes',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
vlan = models.ForeignKey(
|
||||||
|
to='ipam.VLAN',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='prefixes',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='VLAN'
|
||||||
|
)
|
||||||
|
status = models.PositiveSmallIntegerField(
|
||||||
|
choices=PREFIX_STATUS_CHOICES,
|
||||||
|
default=PREFIX_STATUS_ACTIVE,
|
||||||
|
verbose_name='Status',
|
||||||
|
help_text='Operational status of this prefix'
|
||||||
|
)
|
||||||
|
role = models.ForeignKey(
|
||||||
|
to='ipam.Role',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='prefixes',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
help_text='The primary function of this prefix'
|
||||||
|
)
|
||||||
|
is_pool = models.BooleanField(
|
||||||
|
verbose_name='Is a pool',
|
||||||
|
default=False,
|
||||||
|
help_text='All IP addresses within this prefix are considered usable'
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
|
||||||
objects = PrefixQuerySet.as_manager()
|
objects = PrefixQuerySet.as_manager()
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
|
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
|
||||||
@ -422,27 +530,69 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
for example, when mapping public addresses to private addresses. When an Interface has been assigned an IPAddress
|
for example, when mapping public addresses to private addresses. When an Interface has been assigned an IPAddress
|
||||||
which has a NAT outside IP, that Interface's Device can use either the inside or outside IP as its primary IP.
|
which has a NAT outside IP, that Interface's Device can use either the inside or outside IP as its primary IP.
|
||||||
"""
|
"""
|
||||||
family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False)
|
family = models.PositiveSmallIntegerField(
|
||||||
address = IPAddressField(help_text="IPv4 or IPv6 address (with mask)")
|
choices=AF_CHOICES,
|
||||||
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
|
editable=False
|
||||||
verbose_name='VRF')
|
)
|
||||||
tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
|
address = IPAddressField(
|
||||||
|
help_text='IPv4 or IPv6 address (with mask)'
|
||||||
|
)
|
||||||
|
vrf = models.ForeignKey(
|
||||||
|
to='ipam.VRF',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='ip_addresses',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='VRF'
|
||||||
|
)
|
||||||
|
tenant = models.ForeignKey(
|
||||||
|
to='tenancy.Tenant',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='ip_addresses',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
status = models.PositiveSmallIntegerField(
|
status = models.PositiveSmallIntegerField(
|
||||||
'Status', choices=IPADDRESS_STATUS_CHOICES, default=IPADDRESS_STATUS_ACTIVE,
|
choices=IPADDRESS_STATUS_CHOICES,
|
||||||
|
default=IPADDRESS_STATUS_ACTIVE,
|
||||||
|
verbose_name='Status',
|
||||||
help_text='The operational status of this IP'
|
help_text='The operational status of this IP'
|
||||||
)
|
)
|
||||||
role = models.PositiveSmallIntegerField(
|
role = models.PositiveSmallIntegerField(
|
||||||
'Role', choices=IPADDRESS_ROLE_CHOICES, blank=True, null=True, help_text='The functional role of this IP'
|
verbose_name='Role',
|
||||||
|
choices=IPADDRESS_ROLE_CHOICES,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
help_text='The functional role of this IP'
|
||||||
|
)
|
||||||
|
interface = models.ForeignKey(
|
||||||
|
to='dcim.Interface',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='ip_addresses',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
nat_inside = models.OneToOneField(
|
||||||
|
to='self',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='nat_outside',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='NAT (Inside)',
|
||||||
|
help_text='The IP for which this address is the "outside" IP'
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
)
|
)
|
||||||
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
|
|
||||||
null=True)
|
|
||||||
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
|
|
||||||
null=True, verbose_name='NAT (Inside)',
|
|
||||||
help_text="The IP for which this address is the \"outside\" IP")
|
|
||||||
description = models.CharField(max_length=100, blank=True)
|
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
|
||||||
|
|
||||||
objects = IPAddressManager()
|
objects = IPAddressManager()
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary',
|
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary',
|
||||||
@ -535,9 +685,17 @@ class VLANGroup(models.Model):
|
|||||||
"""
|
"""
|
||||||
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
|
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
slug = models.SlugField()
|
slug = models.SlugField()
|
||||||
site = models.ForeignKey('dcim.Site', related_name='vlan_groups', on_delete=models.PROTECT, blank=True, null=True)
|
site = models.ForeignKey(
|
||||||
|
to='dcim.Site',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='vlan_groups',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
csv_headers = ['name', 'slug', 'site']
|
csv_headers = ['name', 'slug', 'site']
|
||||||
|
|
||||||
@ -588,18 +746,57 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
|
Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
|
||||||
or more Prefixes assigned to it.
|
or more Prefixes assigned to it.
|
||||||
"""
|
"""
|
||||||
site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT, blank=True, null=True)
|
site = models.ForeignKey(
|
||||||
group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
|
to='dcim.Site',
|
||||||
vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
|
on_delete=models.PROTECT,
|
||||||
MinValueValidator(1),
|
related_name='vlans',
|
||||||
MaxValueValidator(4094)
|
blank=True,
|
||||||
])
|
null=True
|
||||||
name = models.CharField(max_length=64)
|
)
|
||||||
tenant = models.ForeignKey(Tenant, related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
|
group = models.ForeignKey(
|
||||||
status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1)
|
to='ipam.VLANGroup',
|
||||||
role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True)
|
on_delete=models.PROTECT,
|
||||||
description = models.CharField(max_length=100, blank=True)
|
related_name='vlans',
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
vid = models.PositiveSmallIntegerField(
|
||||||
|
verbose_name='ID',
|
||||||
|
validators=[MinValueValidator(1), MaxValueValidator(4094)]
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=64
|
||||||
|
)
|
||||||
|
tenant = models.ForeignKey(
|
||||||
|
to='tenancy.Tenant',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='vlans',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
status = models.PositiveSmallIntegerField(
|
||||||
|
choices=VLAN_STATUS_CHOICES,
|
||||||
|
default=1,
|
||||||
|
verbose_name='Status'
|
||||||
|
)
|
||||||
|
role = models.ForeignKey(
|
||||||
|
to='ipam.Role',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='vlans',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
|
|||||||
DeprecationWarning
|
DeprecationWarning
|
||||||
)
|
)
|
||||||
|
|
||||||
VERSION = '2.3.3-dev'
|
VERSION = '2.4-dev'
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
@ -139,6 +139,7 @@ INSTALLED_APPS = [
|
|||||||
'django_tables2',
|
'django_tables2',
|
||||||
'mptt',
|
'mptt',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
|
'taggit',
|
||||||
'timezone_field',
|
'timezone_field',
|
||||||
'circuits',
|
'circuits',
|
||||||
'dcim',
|
'dcim',
|
||||||
@ -164,7 +165,6 @@ MIDDLEWARE = (
|
|||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
@ -2,10 +2,11 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from dcim.api.serializers import NestedDeviceSerializer
|
from dcim.api.serializers import NestedDeviceSerializer
|
||||||
from secrets.models import Secret, SecretRole
|
from secrets.models import Secret, SecretRole
|
||||||
from utilities.api import ValidatedModelSerializer
|
from utilities.api import TagField, ValidatedModelSerializer, WritableNestedSerializer
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -19,7 +20,7 @@ class SecretRoleSerializer(ValidatedModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class NestedSecretRoleSerializer(serializers.ModelSerializer):
|
class NestedSecretRoleSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -31,21 +32,15 @@ class NestedSecretRoleSerializer(serializers.ModelSerializer):
|
|||||||
# Secrets
|
# Secrets
|
||||||
#
|
#
|
||||||
|
|
||||||
class SecretSerializer(serializers.ModelSerializer):
|
class SecretSerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
role = NestedSecretRoleSerializer()
|
role = NestedSecretRoleSerializer()
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Secret
|
|
||||||
fields = ['id', 'device', 'role', 'name', 'plaintext', 'hash', 'created', 'last_updated']
|
|
||||||
|
|
||||||
|
|
||||||
class WritableSecretSerializer(serializers.ModelSerializer):
|
|
||||||
plaintext = serializers.CharField()
|
plaintext = serializers.CharField()
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Secret
|
model = Secret
|
||||||
fields = ['id', 'device', 'role', 'name', 'plaintext', 'hash', 'created', 'last_updated']
|
fields = ['id', 'device', 'role', 'name', 'plaintext', 'hash', 'tags', 'created', 'last_updated']
|
||||||
validators = []
|
validators = []
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
@ -64,6 +59,6 @@ class WritableSecretSerializer(serializers.ModelSerializer):
|
|||||||
validator(data)
|
validator(data)
|
||||||
|
|
||||||
# Enforce model validation
|
# Enforce model validation
|
||||||
super(WritableSecretSerializer, self).validate(data)
|
super(SecretSerializer, self).validate(data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
@ -51,7 +51,6 @@ class SecretViewSet(ModelViewSet):
|
|||||||
'role__users', 'role__groups',
|
'role__users', 'role__groups',
|
||||||
)
|
)
|
||||||
serializer_class = serializers.SecretSerializer
|
serializer_class = serializers.SecretSerializer
|
||||||
write_serializer_class = serializers.WritableSecretSerializer
|
|
||||||
filter_class = filters.SecretFilter
|
filter_class = filters.SecretFilter
|
||||||
|
|
||||||
master_key = None
|
master_key = None
|
||||||
@ -68,7 +67,7 @@ class SecretViewSet(ModelViewSet):
|
|||||||
|
|
||||||
super(SecretViewSet, self).initial(request, *args, **kwargs)
|
super(SecretViewSet, self).initial(request, *args, **kwargs)
|
||||||
|
|
||||||
if request.user.is_authenticated():
|
if request.user.is_authenticated:
|
||||||
|
|
||||||
# Read session key from HTTP cookie or header if it has been provided. The session key must be provided in
|
# Read session key from HTTP cookie or header if it has been provided. The session key must be provided in
|
||||||
# order to encrypt/decrypt secrets.
|
# order to encrypt/decrypt secrets.
|
||||||
|
@ -41,6 +41,9 @@ class SecretFilter(django_filters.FilterSet):
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label='Device (name)',
|
label='Device (name)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Secret
|
model = Secret
|
||||||
|
@ -4,6 +4,7 @@ from Crypto.Cipher import PKCS1_OAEP
|
|||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
from taggit.forms import TagField
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from utilities.forms import BootstrapMixin, BulkEditForm, FilterChoiceField, FlexibleModelChoiceField, SlugField
|
from utilities.forms import BootstrapMixin, BulkEditForm, FilterChoiceField, FlexibleModelChoiceField, SlugField
|
||||||
@ -70,10 +71,11 @@ class SecretForm(BootstrapMixin, forms.ModelForm):
|
|||||||
label='Plaintext (verify)',
|
label='Plaintext (verify)',
|
||||||
widget=forms.PasswordInput()
|
widget=forms.PasswordInput()
|
||||||
)
|
)
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Secret
|
model = Secret
|
||||||
fields = ['role', 'name', 'plaintext', 'plaintext2']
|
fields = ['role', 'name', 'plaintext', 'plaintext2', 'tags']
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
22
netbox/secrets/migrations/0004_tags.py
Normal file
22
netbox/secrets/migrations/0004_tags.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-05-22 19:04
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('taggit', '0002_auto_20150616_2121'),
|
||||||
|
('secrets', '0003_unicode_literals'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='secret',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
]
|
@ -12,8 +12,8 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import force_bytes, python_2_unicode_compatible
|
from django.utils.encoding import force_bytes, python_2_unicode_compatible
|
||||||
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from dcim.models import Device
|
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
from .exceptions import InvalidKey
|
from .exceptions import InvalidKey
|
||||||
from .hashers import SecretValidationHasher
|
from .hashers import SecretValidationHasher
|
||||||
@ -54,9 +54,21 @@ class UserKey(CreatedUpdatedModel):
|
|||||||
copy of the master encryption key. The encrypted instance of the master key can be decrypted only with the user's
|
copy of the master encryption key. The encrypted instance of the master key can be decrypted only with the user's
|
||||||
matching (private) decryption key.
|
matching (private) decryption key.
|
||||||
"""
|
"""
|
||||||
user = models.OneToOneField(User, related_name='user_key', editable=False, on_delete=models.CASCADE)
|
user = models.OneToOneField(
|
||||||
public_key = models.TextField(verbose_name='RSA public key')
|
to=User,
|
||||||
master_key_cipher = models.BinaryField(max_length=512, blank=True, null=True, editable=False)
|
on_delete=models.CASCADE,
|
||||||
|
related_name='user_key',
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
|
public_key = models.TextField(
|
||||||
|
verbose_name='RSA public key'
|
||||||
|
)
|
||||||
|
master_key_cipher = models.BinaryField(
|
||||||
|
max_length=512,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
|
|
||||||
objects = UserKeyQuerySet.as_manager()
|
objects = UserKeyQuerySet.as_manager()
|
||||||
|
|
||||||
@ -172,10 +184,23 @@ class SessionKey(models.Model):
|
|||||||
"""
|
"""
|
||||||
A SessionKey stores a User's temporary key to be used for the encryption and decryption of secrets.
|
A SessionKey stores a User's temporary key to be used for the encryption and decryption of secrets.
|
||||||
"""
|
"""
|
||||||
userkey = models.OneToOneField(UserKey, related_name='session_key', on_delete=models.CASCADE, editable=False)
|
userkey = models.OneToOneField(
|
||||||
cipher = models.BinaryField(max_length=512, editable=False)
|
to='secrets.UserKey',
|
||||||
hash = models.CharField(max_length=128, editable=False)
|
on_delete=models.CASCADE,
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
related_name='session_key',
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
|
cipher = models.BinaryField(
|
||||||
|
max_length=512,
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
|
hash = models.CharField(
|
||||||
|
max_length=128,
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
|
created = models.DateTimeField(
|
||||||
|
auto_now_add=True
|
||||||
|
)
|
||||||
|
|
||||||
key = None
|
key = None
|
||||||
|
|
||||||
@ -234,10 +259,23 @@ class SecretRole(models.Model):
|
|||||||
By default, only superusers will have access to decrypt Secrets. To allow other users to decrypt Secrets, grant them
|
By default, only superusers will have access to decrypt Secrets. To allow other users to decrypt Secrets, grant them
|
||||||
access to the appropriate SecretRoles either individually or by group.
|
access to the appropriate SecretRoles either individually or by group.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=50,
|
||||||
users = models.ManyToManyField(User, related_name='secretroles', blank=True)
|
unique=True
|
||||||
groups = models.ManyToManyField(Group, related_name='secretroles', blank=True)
|
)
|
||||||
|
slug = models.SlugField(
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
users = models.ManyToManyField(
|
||||||
|
to=User,
|
||||||
|
related_name='secretroles',
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
groups = models.ManyToManyField(
|
||||||
|
to=Group,
|
||||||
|
related_name='secretroles',
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
csv_headers = ['name', 'slug']
|
csv_headers = ['name', 'slug']
|
||||||
|
|
||||||
@ -276,11 +314,30 @@ class Secret(CreatedUpdatedModel):
|
|||||||
A Secret can be up to 65,536 bytes (64KB) in length. Each secret string will be padded with random data to a minimum
|
A Secret can be up to 65,536 bytes (64KB) in length. Each secret string will be padded with random data to a minimum
|
||||||
of 64 bytes during encryption in order to protect short strings from ciphertext analysis.
|
of 64 bytes during encryption in order to protect short strings from ciphertext analysis.
|
||||||
"""
|
"""
|
||||||
device = models.ForeignKey(Device, related_name='secrets', on_delete=models.CASCADE)
|
device = models.ForeignKey(
|
||||||
role = models.ForeignKey('SecretRole', related_name='secrets', on_delete=models.PROTECT)
|
to='dcim.Device',
|
||||||
name = models.CharField(max_length=100, blank=True)
|
on_delete=models.CASCADE,
|
||||||
ciphertext = models.BinaryField(editable=False, max_length=65568) # 16B IV + 2B pad length + {62-65550}B padded
|
related_name='secrets'
|
||||||
hash = models.CharField(max_length=128, editable=False)
|
)
|
||||||
|
role = models.ForeignKey(
|
||||||
|
to='secrets.SecretRole',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='secrets'
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
ciphertext = models.BinaryField(
|
||||||
|
max_length=65568, # 16B IV + 2B pad length + {62-65550}B padded
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
|
hash = models.CharField(
|
||||||
|
max_length=128,
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
plaintext = None
|
plaintext = None
|
||||||
csv_headers = ['device', 'role', 'name', 'plaintext']
|
csv_headers = ['device', 'role', 'name', 'plaintext']
|
||||||
|
@ -110,6 +110,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in circuit.tags.all %}
|
||||||
|
{% tag 'circuits:circuit_list' tag %}
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% with circuit.get_custom_fields as custom_fields %}
|
{% with circuit.get_custom_fields as custom_fields %}
|
||||||
|
@ -44,6 +44,12 @@
|
|||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -102,6 +102,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in provider.tags.all %}
|
||||||
|
{% tag 'circuits:provider_list' tag %}
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Circuits</td>
|
<td>Circuits</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -33,4 +33,10 @@
|
|||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -96,6 +96,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in device.tags.all %}
|
||||||
|
{% tag 'dcim:device_list' tag %}
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if vc_members %}
|
{% if vc_members %}
|
||||||
|
@ -83,4 +83,10 @@
|
|||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -73,6 +73,16 @@
|
|||||||
<td>Interface Ordering</td>
|
<td>Interface Ordering</td>
|
||||||
<td>{{ devicetype.get_interface_ordering_display }}</td>
|
<td>{{ devicetype.get_interface_ordering_display }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in devicetype.tags.all %}
|
||||||
|
{% tag 'dcim:devicetype_list' tag %}
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Instances</td>
|
<td>Instances</td>
|
||||||
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></td>
|
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></td>
|
||||||
|
@ -37,4 +37,10 @@
|
|||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -114,6 +114,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in rack.tags.all %}
|
||||||
|
{% tag 'dcim:rack_list' tag %}
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Devices</td>
|
<td>Devices</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -43,4 +43,10 @@
|
|||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -133,6 +133,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in site.tags.all %}
|
||||||
|
{% tag 'dcim:site_list' tag %}
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -46,4 +46,10 @@
|
|||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
11
netbox/templates/extras/tag_list.html
Normal file
11
netbox/templates/extras/tag_list.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% block title %}Tags{% endblock %}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
{% include 'utilities/obj_table.html' with bulk_delete_url='extras:tag_bulk_delete' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -16,7 +16,7 @@
|
|||||||
<div id="navbar" class="navbar-collapse collapse">
|
<div id="navbar" class="navbar-collapse collapse">
|
||||||
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
|
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
<li class="dropdown{% if request.path|contains:'/dcim/sites/,/dcim/regions/,/tenancy/,/extras/reports/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|contains:'/dcim/sites/,/dcim/regions/,/tenancy/,/extras/tags/,/extras/reports/' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li class="dropdown-header">Sites</li>
|
<li class="dropdown-header">Sites</li>
|
||||||
@ -60,6 +60,9 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li class="dropdown-header">Miscellaneous</li>
|
<li class="dropdown-header">Miscellaneous</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'extras:tag_list' %}">Tags</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'extras:report_list' %}">Reports</a>
|
<a href="{% url 'extras:report_list' %}">Reports</a>
|
||||||
</li>
|
</li>
|
||||||
|
13
netbox/templates/inc/tags_panel.html
Normal file
13
netbox/templates/inc/tags_panel.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<span class="fa fa-tags" aria-hidden="true"></span>
|
||||||
|
<strong>Tags</strong>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body text-center">
|
||||||
|
{% for tag in tags %}
|
||||||
|
<a href="{% querystring request tag=tag.slug %}" class="btn btn-sm {% if tag.slug in request.GET.tag %}btn-primary{% else %}btn-link{% endif %}">{{ tag }} <span class="badge">{{ tag.count }}</span></a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,4 +1,5 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -81,6 +82,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in aggregate.tags.all %}
|
||||||
|
{% tag 'ipam:aggregate_list' tag %}
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,4 +19,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong><i class="fa fa-bar-chart"></i> Statistics</strong>
|
<strong><i class="fa fa-bar-chart"></i> Statistics</strong>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -133,6 +134,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in ipaddress.tags.all %}
|
||||||
|
{% tag 'ipam:ipaddress_list' tag %}
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% with ipaddress.get_custom_fields as custom_fields %}
|
{% with ipaddress.get_custom_fields as custom_fields %}
|
||||||
|
@ -66,6 +66,12 @@
|
|||||||
{% render_field form.nat_inside %}
|
{% render_field form.nat_inside %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% if form.custom_fields %}
|
{% if form.custom_fields %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -121,6 +121,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in prefix.tags.all %}
|
||||||
|
{% tag 'ipam:prefix_list' tag %}
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Utilization</td>
|
<td>Utilization</td>
|
||||||
<td>{% utilization_graph prefix.get_utilization %}</td>
|
<td>{% utilization_graph prefix.get_utilization %}</td>
|
||||||
|
@ -28,6 +28,12 @@
|
|||||||
{% render_field form.tenant %}
|
{% render_field form.tenant %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% if form.custom_fields %}
|
{% if form.custom_fields %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'ipam/inc/vlan_header.html' with active_tab='vlan' %}
|
{% include 'ipam/inc/vlan_header.html' with active_tab='vlan' %}
|
||||||
@ -80,6 +81,16 @@
|
|||||||
<span class="text-muted">N/A</span>
|
<span class="text-muted">N/A</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in vlan.tags.all %}
|
||||||
|
{% tag 'ipam:vlan_list' tag %}
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,6 +21,12 @@
|
|||||||
{% render_field form.tenant %}
|
{% render_field form.tenant %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% if form.custom_fields %}
|
{% if form.custom_fields %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -77,6 +78,16 @@
|
|||||||
<span class="text-muted">N/A</span>
|
<span class="text-muted">N/A</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in vrf.tags.all %}
|
||||||
|
{% tag 'ipam:vrf_list' tag %}
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,6 +18,12 @@
|
|||||||
{% render_field form.tenant %}
|
{% render_field form.tenant %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% if form.custom_fields %}
|
{% if form.custom_fields %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
{% load static from staticfiles %}
|
{% load static from staticfiles %}
|
||||||
|
{% load helpers %}
|
||||||
{% load secret_helpers %}
|
{% load secret_helpers %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -55,6 +56,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in secret.tags.all %}
|
||||||
|
{% tag 'secrets:secret_list' tag %}
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,6 +54,12 @@
|
|||||||
{% render_field form.plaintext2 %}
|
{% render_field form.plaintext2 %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -68,6 +68,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in tenant.tags.all %}
|
||||||
|
{% tag 'tenancy:tenant_list' tag %}
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% with tenant.get_custom_fields as custom_fields %}
|
{% with tenant.get_custom_fields as custom_fields %}
|
||||||
|
@ -26,4 +26,10 @@
|
|||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
1
netbox/templates/utilities/templatetags/tag.html
Normal file
1
netbox/templates/utilities/templatetags/tag.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<a href="{% url url_name %}?tag={{ tag.slug }}"><span class="label label-default">{{ tag }}</span></a>
|
@ -76,6 +76,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in cluster.tags.all %}
|
||||||
|
{% tag 'virtualization:cluster_list' tag %}
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Virtual Machines</td>
|
<td>Virtual Machines</td>
|
||||||
<td><a href="{% url 'virtualization:virtualmachine_list' %}?cluster_id={{ cluster.pk }}">{{ cluster.virtual_machines.count }}</a></td>
|
<td><a href="{% url 'virtualization:virtualmachine_list' %}?cluster_id={{ cluster.pk }}">{{ cluster.virtual_machines.count }}</a></td>
|
||||||
|
34
netbox/templates/virtualization/cluster_edit.html
Normal file
34
netbox/templates/virtualization/cluster_edit.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{% extends 'utilities/obj_edit.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Cluster</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.name %}
|
||||||
|
{% render_field form.type %}
|
||||||
|
{% render_field form.group %}
|
||||||
|
{% render_field form.site %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if form.custom_fields %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_custom_fields form %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Comments</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.comments %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -16,6 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -121,6 +121,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in vm.tags.all %}
|
||||||
|
{% tag 'virtualization:virtualmachine_list' tag %}
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/custom_fields_panel.html' with custom_fields=vm.get_custom_fields %}
|
{% include 'inc/custom_fields_panel.html' with custom_fields=vm.get_custom_fields %}
|
||||||
|
@ -54,4 +54,10 @@
|
|||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from extras.api.customfields import CustomFieldModelSerializer
|
from extras.api.customfields import CustomFieldModelSerializer
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.api import ValidatedModelSerializer
|
from utilities.api import TagField, ValidatedModelSerializer, WritableNestedSerializer
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -18,7 +19,7 @@ class TenantGroupSerializer(ValidatedModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class NestedTenantGroupSerializer(serializers.ModelSerializer):
|
class NestedTenantGroupSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -31,23 +32,20 @@ class NestedTenantGroupSerializer(serializers.ModelSerializer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class TenantSerializer(CustomFieldModelSerializer):
|
class TenantSerializer(CustomFieldModelSerializer):
|
||||||
group = NestedTenantGroupSerializer()
|
group = NestedTenantGroupSerializer(required=False)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tenant
|
model = Tenant
|
||||||
fields = ['id', 'name', 'slug', 'group', 'description', 'comments', 'custom_fields', 'created', 'last_updated']
|
fields = [
|
||||||
|
'id', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class NestedTenantSerializer(serializers.ModelSerializer):
|
class NestedTenantSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tenant
|
model = Tenant
|
||||||
fields = ['id', 'url', 'name', 'slug']
|
fields = ['id', 'url', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class WritableTenantSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Tenant
|
|
||||||
fields = ['id', 'name', 'slug', 'group', 'description', 'comments', 'custom_fields', 'created', 'last_updated']
|
|
||||||
|
@ -32,5 +32,4 @@ class TenantGroupViewSet(ModelViewSet):
|
|||||||
class TenantViewSet(CustomFieldModelViewSet):
|
class TenantViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Tenant.objects.select_related('group')
|
queryset = Tenant.objects.select_related('group')
|
||||||
serializer_class = serializers.TenantSerializer
|
serializer_class = serializers.TenantSerializer
|
||||||
write_serializer_class = serializers.WritableTenantSerializer
|
|
||||||
filter_class = filters.TenantFilter
|
filter_class = filters.TenantFilter
|
||||||
|
@ -31,6 +31,9 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Group (slug)',
|
label='Group (slug)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tenant
|
model = Tenant
|
||||||
|
@ -2,6 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
from taggit.forms import TagField
|
||||||
|
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
@ -40,10 +41,11 @@ class TenantGroupCSVForm(forms.ModelForm):
|
|||||||
class TenantForm(BootstrapMixin, CustomFieldForm):
|
class TenantForm(BootstrapMixin, CustomFieldForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tenant
|
model = Tenant
|
||||||
fields = ['name', 'slug', 'group', 'description', 'comments']
|
fields = ['name', 'slug', 'group', 'description', 'comments', 'tags']
|
||||||
|
|
||||||
|
|
||||||
class TenantCSVForm(forms.ModelForm):
|
class TenantCSVForm(forms.ModelForm):
|
||||||
|
22
netbox/tenancy/migrations/0004_tags.py
Normal file
22
netbox/tenancy/migrations/0004_tags.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-05-22 19:04
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('taggit', '0002_auto_20150616_2121'),
|
||||||
|
('tenancy', '0003_unicode_literals'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tenant',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
]
|
@ -4,8 +4,9 @@ from django.contrib.contenttypes.fields import GenericRelation
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from extras.models import CustomFieldModel, CustomFieldValue
|
from extras.models import CustomFieldModel
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
|
|
||||||
|
|
||||||
@ -14,8 +15,13 @@ class TenantGroup(models.Model):
|
|||||||
"""
|
"""
|
||||||
An arbitrary collection of Tenants.
|
An arbitrary collection of Tenants.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=50,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
slug = models.SlugField(
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
|
||||||
csv_headers = ['name', 'slug']
|
csv_headers = ['name', 'slug']
|
||||||
|
|
||||||
@ -45,12 +51,35 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
|
A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
|
||||||
department.
|
department.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=30, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=30,
|
||||||
group = models.ForeignKey('TenantGroup', related_name='tenants', blank=True, null=True, on_delete=models.SET_NULL)
|
unique=True
|
||||||
description = models.CharField(max_length=100, blank=True, help_text="Long-form name (optional)")
|
)
|
||||||
comments = models.TextField(blank=True)
|
slug = models.SlugField(
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
unique=True
|
||||||
|
)
|
||||||
|
group = models.ForeignKey(
|
||||||
|
to='tenancy.TenantGroup',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='tenants',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True,
|
||||||
|
help_text='Long-form name (optional)'
|
||||||
|
)
|
||||||
|
comments = models.TextField(
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
csv_headers = ['name', 'slug', 'group', 'description', 'comments']
|
csv_headers = ['name', 'slug', 'group', 'description', 'comments']
|
||||||
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from rest_framework import serializers
|
|
||||||
|
from utilities.api import WritableNestedSerializer
|
||||||
|
|
||||||
|
|
||||||
class NestedUserSerializer(serializers.ModelSerializer):
|
class NestedUserSerializer(WritableNestedSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
@ -16,12 +16,31 @@ class Token(models.Model):
|
|||||||
An API token used for user authentication. This extends the stock model to allow each user to have multiple tokens.
|
An API token used for user authentication. This extends the stock model to allow each user to have multiple tokens.
|
||||||
It also supports setting an expiration time and toggling write ability.
|
It also supports setting an expiration time and toggling write ability.
|
||||||
"""
|
"""
|
||||||
user = models.ForeignKey(User, related_name='tokens', on_delete=models.CASCADE)
|
user = models.ForeignKey(
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
to=User,
|
||||||
expires = models.DateTimeField(blank=True, null=True)
|
on_delete=models.CASCADE,
|
||||||
key = models.CharField(max_length=40, unique=True, validators=[MinLengthValidator(40)])
|
related_name='tokens'
|
||||||
write_enabled = models.BooleanField(default=True, help_text="Permit create/update/delete operations using this key")
|
)
|
||||||
description = models.CharField(max_length=100, blank=True)
|
created = models.DateTimeField(
|
||||||
|
auto_now_add=True
|
||||||
|
)
|
||||||
|
expires = models.DateTimeField(
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
key = models.CharField(
|
||||||
|
max_length=40,
|
||||||
|
unique=True,
|
||||||
|
validators=[MinLengthValidator(40)]
|
||||||
|
)
|
||||||
|
write_enabled = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text='Permit create/update/delete operations using this key'
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = []
|
default_permissions = []
|
||||||
|
@ -5,13 +5,15 @@ import pytz
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db.models import ManyToManyField
|
from django.db.models import ManyToManyField
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
from rest_framework.permissions import BasePermission
|
from rest_framework.permissions import BasePermission
|
||||||
|
from rest_framework.relations import PrimaryKeyRelatedField
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import Field, ModelSerializer, ValidationError
|
from rest_framework.serializers import Field, ModelSerializer, RelatedField, ValidationError
|
||||||
from rest_framework.viewsets import GenericViewSet, ViewSet
|
from rest_framework.viewsets import GenericViewSet, ViewSet
|
||||||
|
|
||||||
WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete']
|
WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete']
|
||||||
@ -33,7 +35,93 @@ class IsAuthenticatedOrLoginNotRequired(BasePermission):
|
|||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
if not settings.LOGIN_REQUIRED:
|
if not settings.LOGIN_REQUIRED:
|
||||||
return True
|
return True
|
||||||
return request.user.is_authenticated()
|
return request.user.is_authenticated
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Fields
|
||||||
|
#
|
||||||
|
|
||||||
|
class TagField(RelatedField):
|
||||||
|
"""
|
||||||
|
Represent a writable list of Tags associated with an object (use with many=True).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
obj = self.parent.parent.instance
|
||||||
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
|
tag, _ = Tag.objects.get_or_create(content_type=content_type, object_id=obj.pk, name=data)
|
||||||
|
return tag
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
return value.name
|
||||||
|
|
||||||
|
|
||||||
|
class ChoiceFieldSerializer(Field):
|
||||||
|
"""
|
||||||
|
Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
|
||||||
|
"""
|
||||||
|
def __init__(self, choices, **kwargs):
|
||||||
|
self._choices = dict()
|
||||||
|
for k, v in choices:
|
||||||
|
# Unpack grouped choices
|
||||||
|
if type(v) in [list, tuple]:
|
||||||
|
for k2, v2 in v:
|
||||||
|
self._choices[k2] = v2
|
||||||
|
else:
|
||||||
|
self._choices[k] = v
|
||||||
|
super(ChoiceFieldSerializer, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def to_representation(self, obj):
|
||||||
|
return {'value': obj, 'label': self._choices[obj]}
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class ContentTypeFieldSerializer(Field):
|
||||||
|
"""
|
||||||
|
Represent a ContentType as '<app_label>.<model>'
|
||||||
|
"""
|
||||||
|
def to_representation(self, obj):
|
||||||
|
return "{}.{}".format(obj.app_label, obj.model)
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
app_label, model = data.split('.')
|
||||||
|
try:
|
||||||
|
return ContentType.objects.get_by_natural_key(app_label=app_label, model=model)
|
||||||
|
except ContentType.DoesNotExist:
|
||||||
|
raise ValidationError("Invalid content type")
|
||||||
|
|
||||||
|
|
||||||
|
class TimeZoneField(Field):
|
||||||
|
"""
|
||||||
|
Represent a pytz time zone.
|
||||||
|
"""
|
||||||
|
def to_representation(self, obj):
|
||||||
|
return obj.zone if obj else None
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return pytz.timezone(str(data))
|
||||||
|
except pytz.exceptions.UnknownTimeZoneError:
|
||||||
|
raise ValidationError('Invalid time zone "{}"'.format(data))
|
||||||
|
|
||||||
|
|
||||||
|
class SerializedPKRelatedField(PrimaryKeyRelatedField):
|
||||||
|
"""
|
||||||
|
Extends PrimaryKeyRelatedField to return a serialized object on read. This is useful for representing related
|
||||||
|
objects in a ManyToManyField while still allowing a set of primary keys to be written.
|
||||||
|
"""
|
||||||
|
def __init__(self, serializer, **kwargs):
|
||||||
|
self.serializer = serializer
|
||||||
|
self.pk_field = kwargs.pop('pk_field', None)
|
||||||
|
super(SerializedPKRelatedField, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
return self.serializer(value, context={'request': self.context['request']}).data
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -67,58 +155,17 @@ class ValidatedModelSerializer(ModelSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class ChoiceFieldSerializer(Field):
|
class WritableNestedSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
|
Returns a nested representation of an object on read, but accepts only a primary key on write.
|
||||||
"""
|
"""
|
||||||
def __init__(self, choices, **kwargs):
|
|
||||||
self._choices = dict()
|
|
||||||
for k, v in choices:
|
|
||||||
# Unpack grouped choices
|
|
||||||
if type(v) in [list, tuple]:
|
|
||||||
for k2, v2 in v:
|
|
||||||
self._choices[k2] = v2
|
|
||||||
else:
|
|
||||||
self._choices[k] = v
|
|
||||||
super(ChoiceFieldSerializer, self).__init__(**kwargs)
|
|
||||||
|
|
||||||
def to_representation(self, obj):
|
|
||||||
return {'value': obj, 'label': self._choices[obj]}
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
return self._choices.get(data)
|
if data is None:
|
||||||
|
return None
|
||||||
|
|
||||||
class ContentTypeFieldSerializer(Field):
|
|
||||||
"""
|
|
||||||
Represent a ContentType as '<app_label>.<model>'
|
|
||||||
"""
|
|
||||||
def to_representation(self, obj):
|
|
||||||
return "{}.{}".format(obj.app_label, obj.model)
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
app_label, model = data.split('.')
|
|
||||||
try:
|
try:
|
||||||
return ContentType.objects.get_by_natural_key(app_label=app_label, model=model)
|
return self.Meta.model.objects.get(pk=data)
|
||||||
except ContentType.DoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise ValidationError("Invalid content type")
|
raise ValidationError("Invalid ID")
|
||||||
|
|
||||||
|
|
||||||
class TimeZoneField(Field):
|
|
||||||
"""
|
|
||||||
Represent a pytz time zone.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def to_representation(self, obj):
|
|
||||||
return obj.zone if obj else None
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
if not data:
|
|
||||||
return ""
|
|
||||||
try:
|
|
||||||
return pytz.timezone(str(data))
|
|
||||||
except pytz.exceptions.UnknownTimeZoneError:
|
|
||||||
raise ValidationError('Invalid time zone "{}"'.format(data))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -132,16 +179,8 @@ class ModelViewSet(mixins.CreateModelMixin,
|
|||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
GenericViewSet):
|
GenericViewSet):
|
||||||
"""
|
"""
|
||||||
Substitute DRF's built-in ModelViewSet for our own, which introduces a bit of additional functionality:
|
Accept either a single object or a list of objects to create.
|
||||||
1. Use an alternate serializer (if provided) for write operations
|
|
||||||
2. Accept either a single object or a list of objects to create
|
|
||||||
"""
|
"""
|
||||||
def get_serializer_class(self):
|
|
||||||
# Check for a different serializer to use for write operations
|
|
||||||
if self.action in WRITE_OPERATIONS and hasattr(self, 'write_serializer_class'):
|
|
||||||
return self.write_serializer_class
|
|
||||||
return self.serializer_class
|
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
# If a list of objects has been provided, initialize the serializer with many=True
|
# If a list of objects has been provided, initialize the serializer with many=True
|
||||||
if isinstance(kwargs.get('data', {}), list):
|
if isinstance(kwargs.get('data', {}), list):
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user