mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-25 22:57:45 -06:00
Merge upstream v2.8.5 into develop
# Conflicts: # netbox/dcim/api/serializers.py # netbox/dcim/models/__init__.py # netbox/extras/constants.py # netbox/netbox/admin.py # netbox/templates/users/base.html # requirements.txt
This commit is contained in:
@@ -14,9 +14,6 @@ class CircuitsRootView(routers.APIRootView):
|
||||
router = routers.DefaultRouter()
|
||||
router.APIRootView = CircuitsRootView
|
||||
|
||||
# Field choices
|
||||
router.register('_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice')
|
||||
|
||||
# Providers
|
||||
router.register('providers', views.ProviderViewSet)
|
||||
|
||||
|
||||
@@ -8,21 +8,10 @@ from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
|
||||
from extras.api.serializers import RenderedGraphSerializer
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from extras.models import Graph
|
||||
from utilities.api import FieldChoicesViewSet, ModelViewSet
|
||||
from utilities.api import ModelViewSet
|
||||
from . import serializers
|
||||
|
||||
|
||||
#
|
||||
# Field choices
|
||||
#
|
||||
|
||||
class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(serializers.CircuitSerializer, ['status']),
|
||||
(serializers.CircuitTerminationSerializer, ['term_side']),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
@@ -5,7 +5,7 @@ from dcim.models import Region, Site
|
||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from utilities.filters import (
|
||||
BaseFilterSet, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
|
||||
BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter
|
||||
)
|
||||
from .choices import *
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
@@ -19,10 +19,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ProviderFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -55,7 +51,7 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilte
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['name', 'slug', 'asn', 'account']
|
||||
fields = ['id', 'name', 'slug', 'asn', 'account']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -77,10 +73,6 @@ class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
||||
|
||||
class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -137,7 +129,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, Cr
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['cid', 'install_date', 'commit_rate']
|
||||
fields = ['id', 'cid', 'install_date', 'commit_rate']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
from django import forms
|
||||
from taggit.forms import TagField
|
||||
|
||||
from dcim.models import Region, Site
|
||||
from extras.forms import (
|
||||
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
|
||||
TagField,
|
||||
)
|
||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker,
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2,
|
||||
StaticSelect2Multiple, TagFilterField,
|
||||
APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField,
|
||||
CSVModelForm, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField,
|
||||
StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
||||
)
|
||||
from .choices import CircuitStatusChoices
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
@@ -55,12 +55,6 @@ class ProviderCSVForm(CustomFieldModelCSVForm):
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = Provider.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Provider name',
|
||||
'asn': '32-bit autonomous system number',
|
||||
'portal_url': 'Portal URL',
|
||||
'comments': 'Free-form comments',
|
||||
}
|
||||
|
||||
|
||||
class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
@@ -113,7 +107,6 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/regions/",
|
||||
value_field="slug",
|
||||
filter_for={
|
||||
'site': 'region'
|
||||
@@ -125,7 +118,6 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
@@ -150,7 +142,7 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
|
||||
]
|
||||
|
||||
|
||||
class CircuitTypeCSVForm(forms.ModelForm):
|
||||
class CircuitTypeCSVForm(CSVModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@@ -167,16 +159,10 @@ class CircuitTypeCSVForm(forms.ModelForm):
|
||||
|
||||
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
widget=APISelect(
|
||||
api_url="/api/circuits/providers/"
|
||||
)
|
||||
queryset=Provider.objects.all()
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
widget=APISelect(
|
||||
api_url="/api/circuits/circuit-types/"
|
||||
)
|
||||
queryset=CircuitType.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = TagField(
|
||||
@@ -200,35 +186,26 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
|
||||
|
||||
class CircuitCSVForm(CustomFieldModelCSVForm):
|
||||
provider = forms.ModelChoiceField(
|
||||
provider = CSVModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of parent provider',
|
||||
error_messages={
|
||||
'invalid_choice': 'Provider not found.'
|
||||
}
|
||||
help_text='Assigned provider'
|
||||
)
|
||||
type = forms.ModelChoiceField(
|
||||
type = CSVModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Type of circuit',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid circuit type.'
|
||||
}
|
||||
help_text='Type of circuit'
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=CircuitStatusChoices,
|
||||
required=False,
|
||||
help_text='Operational status'
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.'
|
||||
}
|
||||
help_text='Assigned tenant'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -245,17 +222,11 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/circuits/circuit-types/"
|
||||
)
|
||||
required=False
|
||||
)
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/circuits/providers/"
|
||||
)
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(CircuitStatusChoices),
|
||||
@@ -265,10 +236,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/tenancy/tenants/"
|
||||
)
|
||||
required=False
|
||||
)
|
||||
commit_rate = forms.IntegerField(
|
||||
required=False,
|
||||
@@ -303,7 +271,6 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/circuits/circuit-types/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
@@ -312,7 +279,6 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/circuits/providers/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
@@ -326,7 +292,6 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/regions/",
|
||||
value_field="slug",
|
||||
filter_for={
|
||||
'site': 'region'
|
||||
@@ -338,7 +303,6 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
@@ -355,6 +319,9 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
||||
#
|
||||
|
||||
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
@@ -368,7 +335,4 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
widgets = {
|
||||
'term_side': forms.HiddenInput(),
|
||||
'site': APISelect(
|
||||
api_url="/api/dcim/sites/"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import dcim.fields
|
||||
|
||||
|
||||
def circuits_to_terms(apps, schema_editor):
|
||||
Circuit = apps.get_model('circuits', 'Circuit')
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
for c in Circuit.objects.all():
|
||||
CircuitTermination(
|
||||
circuit=c,
|
||||
term_side=b'A',
|
||||
site=c.site,
|
||||
interface=c.interface,
|
||||
port_speed=c.port_speed,
|
||||
upstream_speed=c.upstream_speed,
|
||||
xconnect_id=c.xconnect_id,
|
||||
pp_info=c.pp_info,
|
||||
).save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('circuits', '0001_initial'), ('circuits', '0002_auto_20160622_1821'), ('circuits', '0003_provider_32bit_asn_support'), ('circuits', '0004_circuit_add_tenant'), ('circuits', '0005_circuit_add_upstream_speed'), ('circuits', '0006_terminations')]
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0001_initial'),
|
||||
('dcim', '0001_initial'),
|
||||
('dcim', '0022_color_names_to_rgb'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CircuitType',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Provider',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateField(auto_now_add=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('asn', dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN')),
|
||||
('account', models.CharField(blank=True, max_length=30, verbose_name=b'Account number')),
|
||||
('portal_url', models.URLField(blank=True, verbose_name=b'Portal')),
|
||||
('noc_contact', models.TextField(blank=True, verbose_name=b'NOC contact')),
|
||||
('admin_contact', models.TextField(blank=True, verbose_name=b'Admin contact')),
|
||||
('comments', models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Circuit',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateField(auto_now_add=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('cid', models.CharField(max_length=50, verbose_name=b'Circuit ID')),
|
||||
('install_date', models.DateField(blank=True, null=True, verbose_name=b'Date installed')),
|
||||
('port_speed', models.PositiveIntegerField(verbose_name=b'Port speed (Kbps)')),
|
||||
('commit_rate', models.PositiveIntegerField(blank=True, null=True, verbose_name=b'Commit rate (Kbps)')),
|
||||
('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name=b'Cross-connect ID')),
|
||||
('pp_info', models.CharField(blank=True, max_length=100, verbose_name=b'Patch panel/port(s)')),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='circuit', to='dcim.Interface')),
|
||||
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.Provider')),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='dcim.Site')),
|
||||
('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.CircuitType')),
|
||||
('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.Tenant')),
|
||||
('upstream_speed', models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed', null=True, verbose_name=b'Upstream speed (Kbps)')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['provider', 'cid'],
|
||||
'unique_together': {('provider', 'cid')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CircuitTermination',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('term_side', models.CharField(choices=[(b'A', b'A'), (b'Z', b'Z')], max_length=1, verbose_name='Termination')),
|
||||
('port_speed', models.PositiveIntegerField(verbose_name=b'Port speed (Kbps)')),
|
||||
('upstream_speed', models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed', null=True, verbose_name=b'Upstream speed (Kbps)')),
|
||||
('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name=b'Cross-connect ID')),
|
||||
('pp_info', models.CharField(blank=True, max_length=100, verbose_name=b'Patch panel/port(s)')),
|
||||
('circuit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.Circuit')),
|
||||
('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='circuit_termination', to='dcim.Interface')),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='dcim.Site')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['circuit', 'term_side'],
|
||||
'unique_together': {('circuit', 'term_side')},
|
||||
},
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=circuits_to_terms,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='circuit',
|
||||
name='interface',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='circuit',
|
||||
name='port_speed',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='circuit',
|
||||
name='pp_info',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='circuit',
|
||||
name='site',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='circuit',
|
||||
name='upstream_speed',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='circuit',
|
||||
name='xconnect_id',
|
||||
),
|
||||
]
|
||||
@@ -1,254 +0,0 @@
|
||||
import sys
|
||||
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
import dcim.fields
|
||||
|
||||
CONNECTION_STATUS_CONNECTED = True
|
||||
|
||||
CIRCUIT_STATUS_CHOICES = (
|
||||
(0, 'deprovisioning'),
|
||||
(1, 'active'),
|
||||
(2, 'planned'),
|
||||
(3, 'provisioning'),
|
||||
(4, 'offline'),
|
||||
(5, 'decommissioned')
|
||||
)
|
||||
|
||||
|
||||
def circuit_terminations_to_cables(apps, schema_editor):
|
||||
"""
|
||||
Copy all existing CircuitTermination Interface associations as Cables
|
||||
"""
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
Interface = apps.get_model('dcim', 'Interface')
|
||||
Cable = apps.get_model('dcim', 'Cable')
|
||||
|
||||
# Load content types
|
||||
circuittermination_type = ContentType.objects.get_for_model(CircuitTermination)
|
||||
interface_type = ContentType.objects.get_for_model(Interface)
|
||||
|
||||
# Create a new Cable instance from each console connection
|
||||
if 'test' not in sys.argv:
|
||||
print("\n Adding circuit terminations... ", end='', flush=True)
|
||||
for circuittermination in CircuitTermination.objects.filter(interface__isnull=False):
|
||||
|
||||
# Create the new Cable
|
||||
cable = Cable.objects.create(
|
||||
termination_a_type=circuittermination_type,
|
||||
termination_a_id=circuittermination.id,
|
||||
termination_b_type=interface_type,
|
||||
termination_b_id=circuittermination.interface_id,
|
||||
status=CONNECTION_STATUS_CONNECTED
|
||||
)
|
||||
|
||||
# Cache the Cable on its two termination points
|
||||
CircuitTermination.objects.filter(pk=circuittermination.pk).update(
|
||||
cable=cable,
|
||||
connected_endpoint=circuittermination.interface,
|
||||
connection_status=CONNECTION_STATUS_CONNECTED
|
||||
)
|
||||
# Cache the connected Cable on the Interface
|
||||
Interface.objects.filter(pk=circuittermination.interface_id).update(
|
||||
cable=cable,
|
||||
_connected_circuittermination=circuittermination,
|
||||
connection_status=CONNECTION_STATUS_CONNECTED
|
||||
)
|
||||
|
||||
cable_count = Cable.objects.filter(termination_a_type=circuittermination_type).count()
|
||||
if 'test' not in sys.argv:
|
||||
print("{} cables created".format(cable_count))
|
||||
|
||||
|
||||
def circuit_status_to_slug(apps, schema_editor):
|
||||
Circuit = apps.get_model('circuits', 'Circuit')
|
||||
for id, slug in CIRCUIT_STATUS_CHOICES:
|
||||
Circuit.objects.filter(status=str(id)).update(status=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('circuits', '0007_circuit_add_description'), ('circuits', '0008_circuittermination_interface_protect_on_delete'), ('circuits', '0009_unicode_literals'), ('circuits', '0010_circuit_status'), ('circuits', '0011_tags'), ('circuits', '0012_change_logging'), ('circuits', '0013_cables'), ('circuits', '0014_circuittermination_description'), ('circuits', '0015_custom_tag_models'), ('circuits', '0016_3569_circuit_fields'), ('circuits', '0017_circuittype_description')]
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0006_terminations'),
|
||||
('extras', '0019_tag_taggeditem'),
|
||||
('taggit', '0002_auto_20150616_2121'),
|
||||
('dcim', '0066_cables'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='interface',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_termination', to='dcim.Interface'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='cid',
|
||||
field=models.CharField(max_length=50, verbose_name='Circuit ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='commit_rate',
|
||||
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='install_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Date installed'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='port_speed',
|
||||
field=models.PositiveIntegerField(verbose_name='Port speed (Kbps)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='pp_info',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='Patch panel/port(s)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='term_side',
|
||||
field=models.CharField(choices=[('A', 'A'), ('Z', 'Z')], max_length=1, verbose_name='Termination'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='upstream_speed',
|
||||
field=models.PositiveIntegerField(blank=True, help_text='Upstream speed, if different from port speed', null=True, verbose_name='Upstream speed (Kbps)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='xconnect_id',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Cross-connect ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='account',
|
||||
field=models.CharField(blank=True, max_length=30, verbose_name='Account number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='admin_contact',
|
||||
field=models.TextField(blank=True, verbose_name='Admin contact'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='asn',
|
||||
field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='noc_contact',
|
||||
field=models.TextField(blank=True, verbose_name='NOC contact'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='portal_url',
|
||||
field=models.URLField(blank=True, verbose_name='Portal'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[[2, 'Planned'], [3, 'Provisioning'], [1, 'Active'], [4, 'Offline'], [0, 'Deprovisioning'], [5, 'Decommissioned']], default=1),
|
||||
),
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittype',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittype',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='connected_endpoint',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Interface'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='cable',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Cable'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=circuit_terminations_to_cables,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='circuittermination',
|
||||
name='interface',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='status',
|
||||
field=models.CharField(default='active', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=circuit_status_to_slug,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittype',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
]
|
||||
28
netbox/circuits/migrations/0018_standardize_description.py
Normal file
28
netbox/circuits/migrations/0018_standardize_description.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.0.3 on 2020-03-13 20:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0017_circuittype_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittype',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
]
|
||||
@@ -7,6 +7,7 @@ from dcim.constants import CONNECTION_STATUS_CHOICES
|
||||
from dcim.fields import ASNField
|
||||
from dcim.models import CableTermination
|
||||
from extras.models import CustomFieldModel, ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.utils import serialize_object
|
||||
from .choices import *
|
||||
@@ -21,6 +22,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
|
||||
class Provider(ChangeLoggedModel, CustomFieldModel):
|
||||
"""
|
||||
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
||||
@@ -36,7 +38,8 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
|
||||
asn = ASNField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='ASN'
|
||||
verbose_name='ASN',
|
||||
help_text='32-bit autonomous system number'
|
||||
)
|
||||
account = models.CharField(
|
||||
max_length=30,
|
||||
@@ -45,7 +48,7 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
|
||||
)
|
||||
portal_url = models.URLField(
|
||||
blank=True,
|
||||
verbose_name='Portal'
|
||||
verbose_name='Portal URL'
|
||||
)
|
||||
noc_contact = models.TextField(
|
||||
blank=True,
|
||||
@@ -108,7 +111,7 @@ class CircuitType(ChangeLoggedModel):
|
||||
unique=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
max_length=200,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
@@ -131,6 +134,7 @@ class CircuitType(ChangeLoggedModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||
"""
|
||||
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
||||
@@ -173,7 +177,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||
null=True,
|
||||
verbose_name='Commit rate (Kbps)')
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
comments = models.TextField(
|
||||
@@ -292,7 +296,7 @@ class CircuitTermination(CableTermination):
|
||||
verbose_name='Patch panel/port(s)'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
from utilities.tables import BaseTable, TagColumn, ToggleColumn
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
|
||||
CIRCUITTYPE_ACTIONS = """
|
||||
@@ -27,18 +27,20 @@ STATUS_LABEL = """
|
||||
class ProviderTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
circuit_count = tables.Column(
|
||||
accessor=Accessor('count_circuits'),
|
||||
verbose_name='Circuits'
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='circuits:provider_list'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Provider
|
||||
fields = ('pk', 'name', 'asn', 'account',)
|
||||
|
||||
|
||||
class ProviderDetailTable(ProviderTable):
|
||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
|
||||
|
||||
class Meta(ProviderTable.Meta):
|
||||
model = Provider
|
||||
fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
||||
fields = (
|
||||
'pk', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
||||
|
||||
|
||||
#
|
||||
@@ -48,7 +50,9 @@ class ProviderDetailTable(ProviderTable):
|
||||
class CircuitTypeTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
circuit_count = tables.Column(verbose_name='Circuits')
|
||||
circuit_count = tables.Column(
|
||||
verbose_name='Circuits'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=CIRCUITTYPE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -58,6 +62,7 @@ class CircuitTypeTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = CircuitType
|
||||
fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
||||
|
||||
|
||||
#
|
||||
@@ -66,17 +71,33 @@ class CircuitTypeTable(BaseTable):
|
||||
|
||||
class CircuitTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
cid = tables.LinkColumn(verbose_name='ID')
|
||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
|
||||
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
cid = tables.LinkColumn(
|
||||
verbose_name='ID'
|
||||
)
|
||||
provider = tables.LinkColumn(
|
||||
viewname='circuits:provider',
|
||||
args=[Accessor('provider.slug')]
|
||||
)
|
||||
status = tables.TemplateColumn(
|
||||
template_code=STATUS_LABEL
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
a_side = tables.Column(
|
||||
verbose_name='A Side'
|
||||
)
|
||||
z_side = tables.Column(
|
||||
verbose_name='Z Side'
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='circuits:circuit_list'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Circuit
|
||||
fields = ('pk', 'cid', 'status', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')
|
||||
fields = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'install_date', 'commit_rate',
|
||||
'description', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'description')
|
||||
|
||||
@@ -6,7 +6,7 @@ from circuits.choices import *
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from dcim.models import Site
|
||||
from extras.models import Graph
|
||||
from utilities.testing import APITestCase, choices_to_dict
|
||||
from utilities.testing import APITestCase
|
||||
|
||||
|
||||
class AppTest(APITestCase):
|
||||
@@ -18,19 +18,6 @@ class AppTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_choices(self):
|
||||
|
||||
url = reverse('circuits-api:field-choice-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Circuit
|
||||
self.assertEqual(choices_to_dict(response.data.get('circuit:status')), CircuitStatusChoices.as_dict())
|
||||
|
||||
# CircuitTermination
|
||||
self.assertEqual(choices_to_dict(response.data.get('circuit-termination:term_side')), CircuitTerminationSideChoices.as_dict())
|
||||
|
||||
|
||||
class ProviderTest(APITestCase):
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@ class ProviderTestCase(TestCase):
|
||||
CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A', port_speed=1000),
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Provider 1', 'Provider 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -70,11 +74,6 @@ class ProviderTestCase(TestCase):
|
||||
params = {'account': ['1234', '2345']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:3]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -144,7 +143,8 @@ class CircuitTestCase(TestCase):
|
||||
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
||||
TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
|
||||
)
|
||||
TenantGroup.objects.bulk_create(tenant_groups)
|
||||
for tenantgroup in tenant_groups:
|
||||
tenantgroup.save()
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
|
||||
@@ -182,6 +182,10 @@ class CircuitTestCase(TestCase):
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_cid(self):
|
||||
params = {'cid': ['Test Circuit 1', 'Test Circuit 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -194,11 +198,6 @@ class CircuitTestCase(TestCase):
|
||||
params = {'commit_rate': ['1000', '2000']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:3]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_provider(self):
|
||||
provider = Provider.objects.first()
|
||||
params = {'provider_id': [provider.pk]}
|
||||
|
||||
@@ -28,7 +28,7 @@ class ProviderListView(PermissionRequiredMixin, ObjectListView):
|
||||
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
|
||||
filterset = filters.ProviderFilterSet
|
||||
filterset_form = forms.ProviderFilterForm
|
||||
table = tables.ProviderDetailTable
|
||||
table = tables.ProviderTable
|
||||
|
||||
|
||||
class ProviderView(PermissionRequiredMixin, View):
|
||||
@@ -87,7 +87,7 @@ class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'circuits.change_provider'
|
||||
queryset = Provider.objects.all()
|
||||
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
|
||||
filterset = filters.ProviderFilterSet
|
||||
table = tables.ProviderTable
|
||||
form = forms.ProviderBulkEditForm
|
||||
@@ -96,7 +96,7 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_provider'
|
||||
queryset = Provider.objects.all()
|
||||
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
|
||||
filterset = filters.ProviderFilterSet
|
||||
table = tables.ProviderTable
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
@@ -64,7 +64,7 @@ class RegionSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = ['id', 'name', 'slug', 'parent', 'site_count', 'custom_fields']
|
||||
fields = ['id', 'name', 'slug', 'parent', 'description', 'site_count', 'custom_fields']
|
||||
|
||||
|
||||
class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
@@ -96,11 +96,12 @@ class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
|
||||
class RackGroupSerializer(ValidatedModelSerializer):
|
||||
site = NestedSiteSerializer()
|
||||
parent = NestedRackGroupSerializer(required=False, allow_null=True)
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = ['id', 'name', 'slug', 'site', 'rack_count']
|
||||
fields = ['id', 'name', 'slug', 'site', 'parent', 'description', 'rack_count']
|
||||
|
||||
|
||||
class RackRoleSerializer(ValidatedModelSerializer):
|
||||
@@ -142,8 +143,7 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
# 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)
|
||||
validator(data, self)
|
||||
|
||||
# Enforce model validation
|
||||
super().validate(data)
|
||||
@@ -218,7 +218,9 @@ class ManufacturerSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = ['id', 'name', 'slug', 'devicetype_count', 'inventoryitem_count', 'platform_count']
|
||||
fields = [
|
||||
'id', 'name', 'slug', 'description', 'devicetype_count', 'inventoryitem_count', 'platform_count',
|
||||
]
|
||||
|
||||
|
||||
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
@@ -355,7 +357,7 @@ class PlatformSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = [
|
||||
'id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'device_count',
|
||||
'id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'device_count',
|
||||
'virtualmachine_count',
|
||||
]
|
||||
|
||||
@@ -392,8 +394,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
# Validate uniqueness of (rack, position, face) since we omitted the automatically-created validator from Meta.
|
||||
if data.get('rack') and data.get('position') and data.get('face'):
|
||||
validator = UniqueTogetherValidator(queryset=Device.objects.all(), fields=('rack', 'position', 'face'))
|
||||
validator.set_context(self)
|
||||
validator(data)
|
||||
validator(data, self)
|
||||
|
||||
# Enforce model validation
|
||||
super().validate(data)
|
||||
@@ -530,6 +531,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
count_ipaddresses = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
|
||||
@@ -14,9 +14,6 @@ class DCIMRootView(routers.APIRootView):
|
||||
router = routers.DefaultRouter()
|
||||
router.APIRootView = DCIMRootView
|
||||
|
||||
# Field choices
|
||||
router.register('_choices', views.DCIMFieldChoicesViewSet, basename='field-choice')
|
||||
|
||||
# Sites
|
||||
router.register('regions', views.RegionViewSet)
|
||||
router.register('sites', views.SiteViewSet)
|
||||
|
||||
@@ -26,7 +26,7 @@ from extras.api.views import CustomFieldModelViewSet
|
||||
from extras.models import Graph
|
||||
from ipam.models import Prefix, VLAN
|
||||
from utilities.api import (
|
||||
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable,
|
||||
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable,
|
||||
)
|
||||
from utilities.utils import get_subquery
|
||||
from virtualization.models import VirtualMachine
|
||||
@@ -34,35 +34,6 @@ from . import serializers
|
||||
from .exceptions import MissingFilterException
|
||||
|
||||
|
||||
#
|
||||
# Field choices
|
||||
#
|
||||
|
||||
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(serializers.CableSerializer, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']),
|
||||
(serializers.ConsolePortSerializer, ['type', 'connection_status']),
|
||||
(serializers.ConsolePortTemplateSerializer, ['type']),
|
||||
(serializers.ConsoleServerPortSerializer, ['type']),
|
||||
(serializers.ConsoleServerPortTemplateSerializer, ['type']),
|
||||
(serializers.DeviceSerializer, ['face', 'status']),
|
||||
(serializers.DeviceTypeSerializer, ['subdevice_role']),
|
||||
(serializers.FrontPortSerializer, ['type']),
|
||||
(serializers.FrontPortTemplateSerializer, ['type']),
|
||||
(serializers.InterfaceSerializer, ['type', 'mode']),
|
||||
(serializers.InterfaceTemplateSerializer, ['type']),
|
||||
(serializers.PowerFeedSerializer, ['phase', 'status', 'supply', 'type']),
|
||||
(serializers.PowerOutletSerializer, ['type', 'feed_leg']),
|
||||
(serializers.PowerOutletTemplateSerializer, ['type', 'feed_leg']),
|
||||
(serializers.PowerPortSerializer, ['type', 'connection_status']),
|
||||
(serializers.PowerPortTemplateSerializer, ['type']),
|
||||
(serializers.RackSerializer, ['outer_unit', 'status', 'type', 'width']),
|
||||
(serializers.RearPortSerializer, ['type']),
|
||||
(serializers.RearPortTemplateSerializer, ['type']),
|
||||
(serializers.SiteSerializer, ['status']),
|
||||
)
|
||||
|
||||
|
||||
# Mixins
|
||||
|
||||
class CableTraceMixin(object):
|
||||
@@ -77,7 +48,7 @@ class CableTraceMixin(object):
|
||||
# Initialize the path array
|
||||
path = []
|
||||
|
||||
for near_end, cable, far_end in obj.trace(follow_circuits=True):
|
||||
for near_end, cable, far_end in obj.trace()[0]:
|
||||
|
||||
# Serialize each object
|
||||
serializer_a = get_serializer_for_model(near_end, prefix='Nested')
|
||||
@@ -176,33 +147,6 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
serializer_class = serializers.RackSerializer
|
||||
filterset_class = filters.RackFilterSet
|
||||
|
||||
@swagger_auto_schema(deprecated=True)
|
||||
@action(detail=True)
|
||||
def units(self, request, pk=None):
|
||||
"""
|
||||
List rack units (by rack)
|
||||
"""
|
||||
# TODO: Remove this action detail route in v2.8
|
||||
rack = get_object_or_404(Rack, pk=pk)
|
||||
face = request.GET.get('face', 'front')
|
||||
exclude_pk = request.GET.get('exclude', None)
|
||||
if exclude_pk is not None:
|
||||
try:
|
||||
exclude_pk = int(exclude_pk)
|
||||
except ValueError:
|
||||
exclude_pk = None
|
||||
elevation = rack.get_rack_units(face, exclude_pk)
|
||||
|
||||
# Enable filtering rack units by ID
|
||||
q = request.GET.get('q', None)
|
||||
if q:
|
||||
elevation = [u for u in elevation if q in str(u['id'])]
|
||||
|
||||
page = self.paginate_queryset(elevation)
|
||||
if page is not None:
|
||||
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
|
||||
return self.get_paginated_response(rack_units.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
responses={200: serializers.RackUnitSerializer(many=True)},
|
||||
query_serializer=serializers.RackElevationDetailFilterSerializer
|
||||
@@ -225,7 +169,8 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
unit_width=data['unit_width'],
|
||||
unit_height=data['unit_height'],
|
||||
legend_width=data['legend_width'],
|
||||
include_images=data['include_images']
|
||||
include_images=data['include_images'],
|
||||
base_url=request.build_absolute_uri('/')
|
||||
)
|
||||
return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
|
||||
|
||||
|
||||
@@ -63,11 +63,13 @@ class RackWidthChoices(ChoiceSet):
|
||||
|
||||
WIDTH_10IN = 10
|
||||
WIDTH_19IN = 19
|
||||
WIDTH_21IN = 21
|
||||
WIDTH_23IN = 23
|
||||
|
||||
CHOICES = (
|
||||
(WIDTH_10IN, '10 inches'),
|
||||
(WIDTH_19IN, '19 inches'),
|
||||
(WIDTH_21IN, '21 inches'),
|
||||
(WIDTH_23IN, '23 inches'),
|
||||
)
|
||||
|
||||
@@ -280,6 +282,10 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
TYPE_NEMA_L620P = 'nema-l6-20p'
|
||||
TYPE_NEMA_L630P = 'nema-l6-30p'
|
||||
TYPE_NEMA_L650P = 'nema-l6-50p'
|
||||
TYPE_NEMA_L1420P = 'nema-l14-20p'
|
||||
TYPE_NEMA_L1430P = 'nema-l14-30p'
|
||||
TYPE_NEMA_L2120P = 'nema-l21-20p'
|
||||
TYPE_NEMA_L2130P = 'nema-l21-30p'
|
||||
# California style
|
||||
TYPE_CS6361C = 'cs6361c'
|
||||
TYPE_CS6365C = 'cs6365c'
|
||||
@@ -341,6 +347,10 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
(TYPE_NEMA_L620P, 'NEMA L6-20P'),
|
||||
(TYPE_NEMA_L630P, 'NEMA L6-30P'),
|
||||
(TYPE_NEMA_L650P, 'NEMA L6-50P'),
|
||||
(TYPE_NEMA_L1420P, 'NEMA L14-20P'),
|
||||
(TYPE_NEMA_L1430P, 'NEMA L14-30P'),
|
||||
(TYPE_NEMA_L2120P, 'NEMA L21-20P'),
|
||||
(TYPE_NEMA_L2130P, 'NEMA L21-30P'),
|
||||
)),
|
||||
('California Style', (
|
||||
(TYPE_CS6361C, 'CS6361C'),
|
||||
@@ -409,6 +419,10 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
TYPE_NEMA_L620R = 'nema-l6-20r'
|
||||
TYPE_NEMA_L630R = 'nema-l6-30r'
|
||||
TYPE_NEMA_L650R = 'nema-l6-50r'
|
||||
TYPE_NEMA_L1420R = 'nema-l14-20r'
|
||||
TYPE_NEMA_L1430R = 'nema-l14-30r'
|
||||
TYPE_NEMA_L2120R = 'nema-l21-20r'
|
||||
TYPE_NEMA_L2130R = 'nema-l21-30r'
|
||||
# California style
|
||||
TYPE_CS6360C = 'CS6360C'
|
||||
TYPE_CS6364C = 'CS6364C'
|
||||
@@ -428,6 +442,8 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
TYPE_ITA_M = 'ita-m'
|
||||
TYPE_ITA_N = 'ita-n'
|
||||
TYPE_ITA_O = 'ita-o'
|
||||
# Proprietary
|
||||
TYPE_HDOT_CX = 'hdot-cx'
|
||||
|
||||
CHOICES = (
|
||||
('IEC 60320', (
|
||||
@@ -469,6 +485,10 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_NEMA_L620R, 'NEMA L6-20R'),
|
||||
(TYPE_NEMA_L630R, 'NEMA L6-30R'),
|
||||
(TYPE_NEMA_L650R, 'NEMA L6-50R'),
|
||||
(TYPE_NEMA_L1420R, 'NEMA L14-20R'),
|
||||
(TYPE_NEMA_L1430R, 'NEMA L14-30R'),
|
||||
(TYPE_NEMA_L2120R, 'NEMA L21-20R'),
|
||||
(TYPE_NEMA_L2130R, 'NEMA L21-30R'),
|
||||
)),
|
||||
('California Style', (
|
||||
(TYPE_CS6360C, 'CS6360C'),
|
||||
@@ -491,6 +511,9 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_ITA_N, 'ITA Type N'),
|
||||
(TYPE_ITA_O, 'ITA Type O'),
|
||||
)),
|
||||
('Proprietary', (
|
||||
(TYPE_HDOT_CX, 'HDOT Cx'),
|
||||
)),
|
||||
)
|
||||
|
||||
|
||||
@@ -581,15 +604,15 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_128GFC_QSFP28 = '128gfc-sfp28'
|
||||
|
||||
# InfiniBand
|
||||
TYPE_INFINIBAND_SDR = 'inifiband-sdr'
|
||||
TYPE_INFINIBAND_DDR = 'inifiband-ddr'
|
||||
TYPE_INFINIBAND_QDR = 'inifiband-qdr'
|
||||
TYPE_INFINIBAND_FDR10 = 'inifiband-fdr10'
|
||||
TYPE_INFINIBAND_FDR = 'inifiband-fdr'
|
||||
TYPE_INFINIBAND_EDR = 'inifiband-edr'
|
||||
TYPE_INFINIBAND_HDR = 'inifiband-hdr'
|
||||
TYPE_INFINIBAND_NDR = 'inifiband-ndr'
|
||||
TYPE_INFINIBAND_XDR = 'inifiband-xdr'
|
||||
TYPE_INFINIBAND_SDR = 'infiniband-sdr'
|
||||
TYPE_INFINIBAND_DDR = 'infiniband-ddr'
|
||||
TYPE_INFINIBAND_QDR = 'infiniband-qdr'
|
||||
TYPE_INFINIBAND_FDR10 = 'infiniband-fdr10'
|
||||
TYPE_INFINIBAND_FDR = 'infiniband-fdr'
|
||||
TYPE_INFINIBAND_EDR = 'infiniband-edr'
|
||||
TYPE_INFINIBAND_HDR = 'infiniband-hdr'
|
||||
TYPE_INFINIBAND_NDR = 'infiniband-ndr'
|
||||
TYPE_INFINIBAND_XDR = 'infiniband-xdr'
|
||||
|
||||
# Serial
|
||||
TYPE_T1 = 't1'
|
||||
|
||||
@@ -92,5 +92,5 @@ COMPATIBLE_TERMINATION_TYPES = {
|
||||
'interface': ['interface', 'circuittermination', 'frontport', 'rearport'],
|
||||
'frontport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
|
||||
'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
|
||||
'circuittermination': ['interface', 'frontport', 'rearport'],
|
||||
'circuittermination': ['interface', 'frontport', 'rearport', 'circuittermination'],
|
||||
}
|
||||
|
||||
@@ -15,10 +15,15 @@ class RackElevationSVG:
|
||||
|
||||
:param rack: A NetBox Rack instance
|
||||
:param include_images: If true, the SVG document will embed front/rear device face images, where available
|
||||
:param base_url: Base URL for links within the SVG document. If none, links will be relative.
|
||||
"""
|
||||
def __init__(self, rack, include_images=True):
|
||||
def __init__(self, rack, include_images=True, base_url=None):
|
||||
self.rack = rack
|
||||
self.include_images = include_images
|
||||
if base_url is not None:
|
||||
self.base_url = base_url.rstrip('/')
|
||||
else:
|
||||
self.base_url = ''
|
||||
|
||||
def _get_device_description(self, device):
|
||||
return '{} ({}) — {} ({}U) {} {}'.format(
|
||||
@@ -69,7 +74,7 @@ class RackElevationSVG:
|
||||
color = device.device_role.color
|
||||
link = drawing.add(
|
||||
drawing.a(
|
||||
href=reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
href='{}{}'.format(self.base_url, reverse('dcim:device', kwargs={'pk': device.pk})),
|
||||
target='_top',
|
||||
fill='black'
|
||||
)
|
||||
@@ -81,7 +86,7 @@ class RackElevationSVG:
|
||||
|
||||
# Embed front device type image if one exists
|
||||
if self.include_images and device.device_type.front_image:
|
||||
url = device.device_type.front_image.url
|
||||
url = '{}{}'.format(self.base_url, device.device_type.front_image.url)
|
||||
image = drawing.image(href=url, insert=start, size=end, class_='device-image')
|
||||
image.fit(scale='slice')
|
||||
link.add(image)
|
||||
|
||||
@@ -3,3 +3,12 @@ class LoopDetected(Exception):
|
||||
A loop has been detected while tracing a cable path.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class CableTraceSplit(Exception):
|
||||
"""
|
||||
A cable trace cannot be completed because a RearPort maps to multiple FrontPorts and
|
||||
we don't know which one to follow.
|
||||
"""
|
||||
def __init__(self, termination, *args, **kwargs):
|
||||
self.termination = termination
|
||||
|
||||
@@ -4,10 +4,10 @@ from django.contrib.auth.models import User
|
||||
from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.constants import COLOR_CHOICES
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.filters import (
|
||||
BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
|
||||
NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
from .choices import *
|
||||
@@ -74,14 +74,10 @@ class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CustomFieldFilterS
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = ['id', 'name', 'slug']
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -157,10 +153,20 @@ class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Rack group (ID)',
|
||||
)
|
||||
parent = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent__slug',
|
||||
queryset=RackGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Rack group (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = ['id', 'name', 'slug']
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
@@ -171,10 +177,6 @@ class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
||||
|
||||
class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -202,15 +204,18 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Creat
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
field_name='group',
|
||||
lookup_expr='in',
|
||||
label='Rack group (ID)',
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='group__slug',
|
||||
group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
label='Rack group (slug)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=RackStatusChoices,
|
||||
@@ -251,10 +256,6 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Creat
|
||||
|
||||
|
||||
class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -274,16 +275,18 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='rack__group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
lookup_expr='in',
|
||||
label='Rack group (ID)',
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='rack__group__slug',
|
||||
group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='rack__group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
label='Rack group (slug)',
|
||||
)
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
@@ -298,7 +301,7 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ['created']
|
||||
fields = ['id', 'created']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -315,14 +318,10 @@ class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = ['id', 'name', 'slug']
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -370,7 +369,7 @@ class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFil
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -494,7 +493,7 @@ class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['id', 'name', 'slug', 'napalm_driver']
|
||||
fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
|
||||
|
||||
|
||||
class DeviceFilterSet(
|
||||
@@ -504,10 +503,6 @@ class DeviceFilterSet(
|
||||
CustomFieldFilterSet,
|
||||
CreatedUpdatedFilterSet
|
||||
):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -571,9 +566,10 @@ class DeviceFilterSet(
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
rack_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='rack__group',
|
||||
rack_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='rack__group',
|
||||
lookup_expr='in',
|
||||
label='Rack group (ID)',
|
||||
)
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@@ -1088,7 +1084,7 @@ class CableFilterSet(BaseFilterSet):
|
||||
choices=CableStatusChoices
|
||||
)
|
||||
color = django_filters.MultipleChoiceFilter(
|
||||
choices=COLOR_CHOICES
|
||||
choices=ColorChoices
|
||||
)
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device'
|
||||
@@ -1236,10 +1232,6 @@ class InterfaceConnectionFilterSet(BaseFilterSet):
|
||||
|
||||
|
||||
class PowerPanelFilterSet(BaseFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -1267,15 +1259,16 @@ class PowerPanelFilterSet(BaseFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
rack_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='rack_group',
|
||||
rack_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
field_name='rack_group',
|
||||
lookup_expr='in',
|
||||
label='Rack group (ID)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = ['name']
|
||||
fields = ['id', 'name']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -1287,10 +1280,6 @@ class PowerPanelFilterSet(BaseFilterSet):
|
||||
|
||||
|
||||
class PowerFeedFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -1332,7 +1321,7 @@ class PowerFeedFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = ['name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization']
|
||||
fields = ['id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
||||
1743
netbox/dcim/forms.py
1743
netbox/dcim/forms.py
File diff suppressed because it is too large
Load Diff
@@ -1,101 +0,0 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import dcim.fields
|
||||
|
||||
|
||||
def copy_primary_ip(apps, schema_editor):
|
||||
Device = apps.get_model('dcim', 'Device')
|
||||
for d in Device.objects.select_related('primary_ip'):
|
||||
if not d.primary_ip:
|
||||
continue
|
||||
if d.primary_ip.family == 4:
|
||||
d.primary_ip4 = d.primary_ip
|
||||
elif d.primary_ip.family == 6:
|
||||
d.primary_ip6 = d.primary_ip
|
||||
d.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('dcim', '0003_auto_20160628_1721'), ('dcim', '0004_auto_20160701_2049'), ('dcim', '0005_auto_20160706_1722'), ('dcim', '0006_add_device_primary_ip4_ip6'), ('dcim', '0007_device_copy_primary_ip'), ('dcim', '0008_device_remove_primary_ip'), ('dcim', '0009_site_32bit_asn_support'), ('dcim', '0010_devicebay_installed_device_set_null')]
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0001_initial'),
|
||||
('dcim', '0002_auto_20160622_1821'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (100BASE-TX)'], [1000, b'1GE (1000BASE-T)'], [1100, b'1GE (SFP)'], [1150, b'10GE (10GBASE-T)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (100BASE-TX)'], [1000, b'1GE (1000BASE-T)'], [1100, b'1GE (SFP)'], [1150, b'10GE (10GBASE-T)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='subdevice_role',
|
||||
field=models.NullBooleanField(choices=[(None, b'None'), (True, b'Parent'), (False, b'Child')], default=None, help_text=b'Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name=b'Parent/child status'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DeviceBayTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bay_templates', to='dcim.DeviceType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
'unique_together': {('device_type', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DeviceBay',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, verbose_name=b'Name')),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bays', to='dcim.Device')),
|
||||
('installed_device', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_bay', to='dcim.Device')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'name'],
|
||||
'unique_together': {('device', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='mac_address',
|
||||
field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name=b'MAC Address'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='primary_ip4',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name=b'Primary IPv4'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='primary_ip6',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name=b'Primary IPv6'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=copy_primary_ip,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='device',
|
||||
name='primary_ip',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='asn',
|
||||
field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicebay',
|
||||
name='installed_device',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent_bay', to='dcim.Device'),
|
||||
),
|
||||
]
|
||||
@@ -1,154 +0,0 @@
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import utilities.fields
|
||||
|
||||
COLOR_CONVERSION = {
|
||||
'teal': '009688',
|
||||
'green': '4caf50',
|
||||
'blue': '2196f3',
|
||||
'purple': '9c27b0',
|
||||
'yellow': 'ffeb3b',
|
||||
'orange': 'ff9800',
|
||||
'red': 'f44336',
|
||||
'light_gray': 'c0c0c0',
|
||||
'medium_gray': '9e9e9e',
|
||||
'dark_gray': '607d8b',
|
||||
}
|
||||
|
||||
|
||||
def color_names_to_rgb(apps, schema_editor):
|
||||
RackRole = apps.get_model('dcim', 'RackRole')
|
||||
DeviceRole = apps.get_model('dcim', 'DeviceRole')
|
||||
for color_name, color_rgb in COLOR_CONVERSION.items():
|
||||
RackRole.objects.filter(color=color_name).update(color=color_rgb)
|
||||
DeviceRole.objects.filter(color=color_name).update(color=color_rgb)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('dcim', '0011_devicetype_part_number'), ('dcim', '0012_site_rack_device_add_tenant'), ('dcim', '0013_add_interface_form_factors'), ('dcim', '0014_rack_add_type_width'), ('dcim', '0015_rack_add_u_height_validator'), ('dcim', '0016_module_add_manufacturer'), ('dcim', '0017_rack_add_role'), ('dcim', '0018_device_add_asset_tag'), ('dcim', '0019_new_iface_form_factors'), ('dcim', '0020_rack_desc_units'), ('dcim', '0021_add_ff_flexstack'), ('dcim', '0022_color_names_to_rgb')]
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0010_devicebay_installed_device_set_null'),
|
||||
('tenancy', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='part_number',
|
||||
field=models.CharField(blank=True, help_text=b'Discrete part number (optional)', max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sites', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='type',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[(100, b'2-post frame'), (200, b'4-post frame'), (300, b'4-post cabinet'), (1000, b'Wall-mounted frame'), (1100, b'Wall-mounted cabinet')], null=True, verbose_name=b'Type'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='width',
|
||||
field=models.PositiveSmallIntegerField(choices=[(19, b'19 inches'), (23, b'23 inches')], default=19, help_text=b'Rail-to-rail width', verbose_name=b'Width'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='u_height',
|
||||
field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name=b'Height (U)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='module',
|
||||
name='manufacturer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='modules', to='dcim.Manufacturer'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RackRole',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('color', models.CharField(choices=[[b'teal', b'Teal'], [b'green', b'Green'], [b'blue', b'Blue'], [b'purple', b'Purple'], [b'yellow', b'Yellow'], [b'orange', b'Orange'], [b'red', b'Red'], [b'light_gray', b'Light Gray'], [b'medium_gray', b'Medium Gray'], [b'dark_gray', b'Dark Gray']], max_length=30)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='role',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='dcim.RackRole'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='asset_tag',
|
||||
field=utilities.fields.NullableCharField(blank=True, help_text=b'A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name=b'Asset tag'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='desc_units',
|
||||
field=models.BooleanField(default=False, help_text=b'Units are numbered top-to-bottom', verbose_name=b'Descending units'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='position',
|
||||
field=models.PositiveSmallIntegerField(blank=True, help_text=b'The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Position (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=color_names_to_rgb,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicerole',
|
||||
name='color',
|
||||
field=utilities.fields.ColorField(max_length=6),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rackrole',
|
||||
name='color',
|
||||
field=utilities.fields.ColorField(max_length=6),
|
||||
),
|
||||
]
|
||||
@@ -1,478 +0,0 @@
|
||||
import django.contrib.postgres.fields
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import mptt.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import dcim.fields
|
||||
import utilities.fields
|
||||
|
||||
|
||||
def copy_site_from_rack(apps, schema_editor):
|
||||
Device = apps.get_model('dcim', 'Device')
|
||||
for device in Device.objects.all():
|
||||
device.site = device.rack.site
|
||||
device.save()
|
||||
|
||||
|
||||
def rpc_client_to_napalm_driver(apps, schema_editor):
|
||||
"""
|
||||
Migrate legacy RPC clients to their respective NAPALM drivers
|
||||
"""
|
||||
Platform = apps.get_model('dcim', 'Platform')
|
||||
|
||||
Platform.objects.filter(rpc_client='juniper-junos').update(napalm_driver='junos')
|
||||
Platform.objects.filter(rpc_client='cisco-ios').update(napalm_driver='ios')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('dcim', '0023_devicetype_comments'), ('dcim', '0024_site_add_contact_fields'), ('dcim', '0025_devicetype_add_interface_ordering'), ('dcim', '0026_add_rack_reservations'), ('dcim', '0027_device_add_site'), ('dcim', '0028_device_copy_rack_to_site'), ('dcim', '0029_allow_rackless_devices'), ('dcim', '0030_interface_add_lag'), ('dcim', '0031_regions'), ('dcim', '0032_device_increase_name_length'), ('dcim', '0033_rackreservation_rack_editable'), ('dcim', '0034_rename_module_to_inventoryitem'), ('dcim', '0035_device_expand_status_choices'), ('dcim', '0036_add_ff_juniper_vcp'), ('dcim', '0037_unicode_literals'), ('dcim', '0038_wireless_interfaces'), ('dcim', '0039_interface_add_enabled_mtu'), ('dcim', '0040_inventoryitem_add_asset_tag_description'), ('dcim', '0041_napalm_integration'), ('dcim', '0042_interface_ff_10ge_cx4'), ('dcim', '0043_device_component_name_lengths')]
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0022_color_names_to_rgb'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='contact_email',
|
||||
field=models.EmailField(blank=True, max_length=254, verbose_name=b'Contact E-mail'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='contact_name',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='contact_phone',
|
||||
field=models.CharField(blank=True, max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='interface_ordering',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, b'Slot/position'], [2, b'Name (alphabetically)']], default=1),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RackReservation',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('units', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(), size=None)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('description', models.CharField(max_length=100)),
|
||||
('rack', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='dcim.Rack')),
|
||||
('user', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['created'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='site',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=copy_site_from_rack,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='rack',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Rack'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='site',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Region',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('lft', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('rght', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('level', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.Region')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='region',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='dcim.Region'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='name',
|
||||
field=utilities.fields.NullableCharField(blank=True, max_length=64, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rackreservation',
|
||||
name='rack',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='dcim.Rack'),
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='Module',
|
||||
new_name='InventoryItem',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='device',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventory_items', to='dcim.Device'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child_items', to='dcim.InventoryItem'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='manufacturer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_items', to='dcim.Manufacturer'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='status',
|
||||
field=models.PositiveIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus'], [5200, b'Juniper VCP']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus'], [5200, b'Juniper VCP']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='cs_port',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_console', to='dcim.ConsoleServerPort', verbose_name='Console server port'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='asset_tag',
|
||||
field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name='Asset tag'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='face',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[[0, 'Front'], [1, 'Rear']], null=True, verbose_name='Rack face'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='position',
|
||||
field=models.PositiveSmallIntegerField(blank=True, help_text='The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Position (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='primary_ip4',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name='Primary IPv4'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='primary_ip6',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name='Primary IPv6'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='serial',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [2, 'Planned'], [3, 'Staged'], [4, 'Failed'], [5, 'Inventory']], default=1, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicebay',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='interface_ordering',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, 'Slot/position'], [2, 'Name (alphabetically)']], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_console_server',
|
||||
field=models.BooleanField(default=False, help_text='This type of device has console server ports', verbose_name='Is a console server'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_full_depth',
|
||||
field=models.BooleanField(default=True, help_text='Device consumes both front and rear rack faces', verbose_name='Is full depth'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_network_device',
|
||||
field=models.BooleanField(default=True, help_text='This type of device has network interfaces', verbose_name='Is a network device'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_pdu',
|
||||
field=models.BooleanField(default=False, help_text='This type of device has power outlets', verbose_name='Is a PDU'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='part_number',
|
||||
field=models.CharField(blank=True, help_text='Discrete part number (optional)', max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='subdevice_role',
|
||||
field=models.NullBooleanField(choices=[(None, 'None'), (True, 'Parent'), (False, 'Child')], default=None, help_text='Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name='Parent/child status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='u_height',
|
||||
field=models.PositiveSmallIntegerField(default=1, verbose_name='Height (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='lag',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name='Parent LAG'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='mac_address',
|
||||
field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name='MAC Address'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='mgmt_only',
|
||||
field=models.BooleanField(default=False, help_text='This interface is used only for out-of-band management', verbose_name='OOB Management'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfaceconnection',
|
||||
name='connection_status',
|
||||
field=models.BooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='mgmt_only',
|
||||
field=models.BooleanField(default=False, verbose_name='Management only'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='discovered',
|
||||
field=models.BooleanField(default=False, verbose_name='Discovered'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='part_id',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Part ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='serial',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='rpc_client',
|
||||
field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='RPC client'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerport',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='desc_units',
|
||||
field=models.BooleanField(default=False, help_text='Units are numbered top-to-bottom', verbose_name='Descending units'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='facility_id',
|
||||
field=utilities.fields.NullableCharField(blank=True, max_length=30, null=True, verbose_name='Facility ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='type',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[(100, '2-post frame'), (200, '4-post frame'), (300, '4-post cabinet'), (1000, 'Wall-mounted frame'), (1100, 'Wall-mounted cabinet')], null=True, verbose_name='Type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='u_height',
|
||||
field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Height (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='width',
|
||||
field=models.PositiveSmallIntegerField(choices=[(19, '19 inches'), (23, '23 inches')], default=19, help_text='Rail-to-rail width', verbose_name='Width'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='asn',
|
||||
field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='contact_email',
|
||||
field=models.EmailField(blank=True, max_length=254, verbose_name='Contact E-mail'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='enabled',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='mtu',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='asset_tag',
|
||||
field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this item', max_length=50, null=True, unique=True, verbose_name='Asset tag'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='device',
|
||||
options={'ordering': ['name'], 'permissions': (('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'))},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='napalm_driver',
|
||||
field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices.', max_length=50, verbose_name='NAPALM driver'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='rpc_client',
|
||||
field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='Legacy RPC client'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=rpc_client_to_napalm_driver,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleporttemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleserverport',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicebaytemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlet',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerport',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerporttemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
]
|
||||
@@ -1,354 +0,0 @@
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import timezone_field.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import utilities.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('dcim', '0044_virtualization'), ('dcim', '0045_devicerole_vm_role'), ('dcim', '0046_rack_lengthen_facility_id'), ('dcim', '0047_more_100ge_form_factors'), ('dcim', '0048_rack_serial'), ('dcim', '0049_rackreservation_change_user'), ('dcim', '0050_interface_vlan_tagging'), ('dcim', '0051_rackreservation_tenant'), ('dcim', '0052_virtual_chassis'), ('dcim', '0053_platform_manufacturer'), ('dcim', '0054_site_status_timezone_description'), ('dcim', '0055_virtualchassis_ordering'), ('dcim', '0056_django2'), ('dcim', '0057_tags'), ('dcim', '0058_relax_rack_naming_constraints'), ('dcim', '0059_site_latitude_longitude'), ('dcim', '0060_change_logging'), ('dcim', '0061_platform_napalm_args')]
|
||||
|
||||
dependencies = [
|
||||
('virtualization', '0001_virtualization'),
|
||||
('tenancy', '0003_unicode_literals'),
|
||||
('ipam', '0020_ipaddress_add_role_carp'),
|
||||
('dcim', '0043_device_component_name_lengths'),
|
||||
('taggit', '0002_auto_20150616_2121'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='cluster',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='virtualization.Cluster'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='virtual_machine',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='virtualization.VirtualMachine'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='device',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicerole',
|
||||
name='vm_role',
|
||||
field=models.BooleanField(default=True, help_text='Virtual machines may be assigned to this role', verbose_name='VM Role'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='facility_id',
|
||||
field=utilities.fields.NullableCharField(blank=True, max_length=50, null=True, verbose_name='Facility ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='serial',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rackreservation',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='mode',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[[100, 'Access'], [200, 'Tagged'], [300, 'Tagged All']], null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='tagged_vlans',
|
||||
field=models.ManyToManyField(blank=True, related_name='interfaces_as_tagged', to='ipam.VLAN', verbose_name='Tagged VLANs'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackreservation',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rackreservations', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VirtualChassis',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('domain', models.CharField(blank=True, max_length=30)),
|
||||
('master', models.OneToOneField(default=1, on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['master'],
|
||||
'verbose_name_plural': 'virtual chassis',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='virtual_chassis',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='dcim.VirtualChassis'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='vc_position',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='vc_priority',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='device',
|
||||
unique_together={('rack', 'position', 'face'), ('virtual_chassis', 'vc_position')},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='napalm_driver',
|
||||
field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices', max_length=50, verbose_name='NAPALM driver'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [2, 'Planned'], [4, 'Retired']], default=1),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='time_zone',
|
||||
field=timezone_field.fields.TimeZoneField(blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='virtualchassis',
|
||||
name='master',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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.AddField(
|
||||
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'),
|
||||
),
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
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='consoleserverport',
|
||||
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='devicebay',
|
||||
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='interface',
|
||||
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='inventoryitem',
|
||||
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='poweroutlet',
|
||||
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='powerport',
|
||||
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='virtualchassis',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='rack',
|
||||
options={'ordering': ['site', 'group', 'name']},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rack',
|
||||
unique_together={('group', 'name'), ('group', 'facility_id')},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='latitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=8, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='longitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicerole',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicerole',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='manufacturer',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='manufacturer',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackgroup',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackgroup',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackreservation',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackrole',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackrole',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='region',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='region',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='virtualchassis',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='virtualchassis',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rackreservation',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='napalm_args',
|
||||
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)', null=True, verbose_name='NAPALM arguments'),
|
||||
),
|
||||
]
|
||||
@@ -1,124 +0,0 @@
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('dcim', '0062_interface_mtu'), ('dcim', '0063_device_local_context_data'), ('dcim', '0064_remove_platform_rpc_client'), ('dcim', '0065_front_rear_ports')]
|
||||
|
||||
dependencies = [
|
||||
('taggit', '0002_auto_20150616_2121'),
|
||||
('dcim', '0061_platform_napalm_args'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='mtu',
|
||||
field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65536)], verbose_name='MTU'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['SONET', [[6100, 'OC-3/STM-1'], [6200, 'OC-12/STM-4'], [6300, 'OC-48/STM-16'], [6400, 'OC-192/STM-64'], [6500, 'OC-768/STM-256'], [6600, 'OC-1920/STM-640'], [6700, 'OC-3840/STM-1234']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)'], [3320, 'SFP28 (32GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['SONET', [[6100, 'OC-3/STM-1'], [6200, 'OC-12/STM-4'], [6300, 'OC-48/STM-16'], [6400, 'OC-192/STM-64'], [6500, 'OC-768/STM-256'], [6600, 'OC-1920/STM-640'], [6700, 'OC-3840/STM-1234']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)'], [3320, 'SFP28 (32GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='local_context_data',
|
||||
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='platform',
|
||||
name='rpc_client',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RearPort',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('type', models.PositiveSmallIntegerField()),
|
||||
('positions', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rearports', to='dcim.Device')),
|
||||
('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'name'],
|
||||
'unique_together': {('device', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RearPortTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('type', models.PositiveSmallIntegerField()),
|
||||
('positions', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])),
|
||||
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rearport_templates', to='dcim.DeviceType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
'unique_together': {('device_type', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FrontPortTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('type', models.PositiveSmallIntegerField()),
|
||||
('rear_port_position', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])),
|
||||
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontport_templates', to='dcim.DeviceType')),
|
||||
('rear_port', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontport_templates', to='dcim.RearPortTemplate')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
'unique_together': {('rear_port', 'rear_port_position'), ('device_type', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FrontPort',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('type', models.PositiveSmallIntegerField()),
|
||||
('rear_port_position', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontports', to='dcim.Device')),
|
||||
('rear_port', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontports', to='dcim.RearPort')),
|
||||
('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'name'],
|
||||
'unique_together': {('device', 'name'), ('rear_port', 'rear_port_position')},
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleporttemplate',
|
||||
name='device_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='consoleport_templates', to='dcim.DeviceType'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='device_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='consoleserverport_templates', to='dcim.DeviceType'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='device_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='poweroutlet_templates', to='dcim.DeviceType'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerporttemplate',
|
||||
name='device_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='powerport_templates', to='dcim.DeviceType'),
|
||||
),
|
||||
]
|
||||
@@ -1,146 +0,0 @@
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('dcim', '0067_device_type_remove_qualifiers'), ('dcim', '0068_rack_new_fields'), ('dcim', '0069_deprecate_nullablecharfield'), ('dcim', '0070_custom_tag_models')]
|
||||
|
||||
dependencies = [
|
||||
('extras', '0019_tag_taggeditem'),
|
||||
('dcim', '0066_cables'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='devicetype',
|
||||
name='is_console_server',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='devicetype',
|
||||
name='is_network_device',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='devicetype',
|
||||
name='is_pdu',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='devicetype',
|
||||
name='interface_ordering',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(default=3),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='outer_depth',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='outer_unit',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='outer_width',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='asset_tag',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=64, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='asset_tag',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='asset_tag',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='facility_id',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleserverport',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicebay',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='frontport',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlet',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerport',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rearport',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='virtualchassis',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
|
||||
),
|
||||
]
|
||||
@@ -19,8 +19,7 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Fixes a missed field migration from #3569; see bug #4056. The original migration has also been fixed,
|
||||
# so this can be omitted when squashing in the future.
|
||||
# Fixes a missed field migration from #3569; see bug #4056. The original migration has also been fixed.
|
||||
migrations.RunPython(
|
||||
code=rack_outer_unit_to_slug
|
||||
),
|
||||
|
||||
28
netbox/dcim/migrations/0100_mptt_remove_indexes.py
Normal file
28
netbox/dcim/migrations/0100_mptt_remove_indexes.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.0.3 on 2020-02-18 21:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0099_powerfeed_negative_voltage'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='region',
|
||||
name='level',
|
||||
field=models.PositiveIntegerField(editable=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='region',
|
||||
name='lft',
|
||||
field=models.PositiveIntegerField(editable=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='region',
|
||||
name='rght',
|
||||
field=models.PositiveIntegerField(editable=False),
|
||||
),
|
||||
]
|
||||
43
netbox/dcim/migrations/0101_nested_rackgroups.py
Normal file
43
netbox/dcim/migrations/0101_nested_rackgroups.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import mptt.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0100_mptt_remove_indexes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rackgroup',
|
||||
name='parent',
|
||||
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.RackGroup'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackgroup',
|
||||
name='level',
|
||||
field=models.PositiveIntegerField(default=0, editable=False),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackgroup',
|
||||
name='lft',
|
||||
field=models.PositiveIntegerField(default=1, editable=False),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackgroup',
|
||||
name='rght',
|
||||
field=models.PositiveIntegerField(default=2, editable=False),
|
||||
preserve_default=False,
|
||||
),
|
||||
# tree_id will be set to a valid value during the following migration (which needs to be a separate migration)
|
||||
migrations.AddField(
|
||||
model_name='rackgroup',
|
||||
name='tree_id',
|
||||
field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
21
netbox/dcim/migrations/0102_nested_rackgroups_rebuild.py
Normal file
21
netbox/dcim/migrations/0102_nested_rackgroups_rebuild.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def rebuild_mptt(apps, schema_editor):
|
||||
RackGroup = apps.get_model('dcim', 'RackGroup')
|
||||
for i, rackgroup in enumerate(RackGroup.objects.all(), start=1):
|
||||
RackGroup.objects.filter(pk=rackgroup.pk).update(tree_id=i)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0101_nested_rackgroups'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=rebuild_mptt,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
98
netbox/dcim/migrations/0103_standardize_description.py
Normal file
98
netbox/dcim/migrations/0103_standardize_description.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# Generated by Django 3.0.3 on 2020-03-13 20:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0102_nested_rackgroups_rebuild'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='manufacturer',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackgroup',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='region',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleserverport',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicebay',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicerole',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='frontport',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlet',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerport',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rackreservation',
|
||||
name='description',
|
||||
field=models.CharField(max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rackrole',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rearport',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
]
|
||||
34
netbox/dcim/migrations/0104_correct_infiniband_types.py
Normal file
34
netbox/dcim/migrations/0104_correct_infiniband_types.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
INFINIBAND_SLUGS = (
|
||||
('inifiband-sdr', 'infiniband-sdr'),
|
||||
('inifiband-ddr', 'infiniband-ddr'),
|
||||
('inifiband-qdr', 'infiniband-qdr'),
|
||||
('inifiband-fdr10', 'infiniband-fdr10'),
|
||||
('inifiband-fdr', 'infiniband-fdr'),
|
||||
('inifiband-edr', 'infiniband-edr'),
|
||||
('inifiband-hdr', 'infiniband-hdr'),
|
||||
('inifiband-ndr', 'infiniband-ndr'),
|
||||
('inifiband-xdr', 'infiniband-xdr'),
|
||||
)
|
||||
|
||||
|
||||
def correct_infiniband_types(apps, schema_editor):
|
||||
Interface = apps.get_model('dcim', 'Interface')
|
||||
for old, new in INFINIBAND_SLUGS:
|
||||
Interface.objects.filter(type=old).update(type=new)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0103_standardize_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=correct_infiniband_types,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
18
netbox/dcim/migrations/0105_interface_name_collation.py
Normal file
18
netbox/dcim/migrations/0105_interface_name_collation.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.5 on 2020-04-21 20:13
|
||||
|
||||
from django.db import migrations
|
||||
import utilities.query_functions
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0104_correct_infiniband_types'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='interface',
|
||||
options={'ordering': ('device', utilities.query_functions.CollateAsChar('_name'))},
|
||||
),
|
||||
]
|
||||
24
netbox/dcim/migrations/0106_role_default_color.py
Normal file
24
netbox/dcim/migrations/0106_role_default_color.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.0.6 on 2020-05-26 13:33
|
||||
|
||||
from django.db import migrations
|
||||
import utilities.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0105_interface_name_collation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='devicerole',
|
||||
name='color',
|
||||
field=utilities.fields.ColorField(default='9e9e9e', max_length=6),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rackrole',
|
||||
name='color',
|
||||
field=utilities.fields.ColorField(default='9e9e9e', max_length=6),
|
||||
),
|
||||
]
|
||||
@@ -12,6 +12,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Count, F, ProtectedError, Sum
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from taggit.managers import TaggableManager
|
||||
from timezone_field import TimeZoneField
|
||||
@@ -21,6 +22,8 @@ from dcim.constants import *
|
||||
from dcim.fields import ASNField
|
||||
from dcim.elevations import RackElevationSVG
|
||||
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.utils import serialize_object, to_meters
|
||||
@@ -75,6 +78,7 @@ __all__ = (
|
||||
# Regions
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
class Region(MPTTModel, ChangeLoggedModel, CustomFieldModel):
|
||||
"""
|
||||
Sites can be grouped within geographic Regions.
|
||||
@@ -94,13 +98,19 @@ class Region(MPTTModel, ChangeLoggedModel, CustomFieldModel):
|
||||
slug = models.SlugField(
|
||||
unique=True
|
||||
)
|
||||
|
||||
custom_field_values = GenericRelation(
|
||||
to='extras.CustomFieldValue',
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
csv_headers = ['name', 'slug', 'parent']
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
csv_headers = ['name', 'slug', 'parent', 'description']
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ['name']
|
||||
@@ -116,6 +126,7 @@ class Region(MPTTModel, ChangeLoggedModel, CustomFieldModel):
|
||||
self.name,
|
||||
self.slug,
|
||||
self.parent.name if self.parent else None,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def get_site_count(self):
|
||||
@@ -138,6 +149,7 @@ class Region(MPTTModel, ChangeLoggedModel, CustomFieldModel):
|
||||
# Sites
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
|
||||
class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
"""
|
||||
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
||||
@@ -176,18 +188,20 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
)
|
||||
facility = models.CharField(
|
||||
max_length=50,
|
||||
blank=True
|
||||
blank=True,
|
||||
help_text='Local facility ID or description'
|
||||
)
|
||||
asn = ASNField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='ASN'
|
||||
verbose_name='ASN',
|
||||
help_text='32-bit autonomous system number'
|
||||
)
|
||||
time_zone = TimeZoneField(
|
||||
blank=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
physical_address = models.CharField(
|
||||
@@ -202,13 +216,15 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
max_digits=8,
|
||||
decimal_places=6,
|
||||
blank=True,
|
||||
null=True
|
||||
null=True,
|
||||
help_text='GPS coordinate (latitude)'
|
||||
)
|
||||
longitude = models.DecimalField(
|
||||
max_digits=9,
|
||||
decimal_places=6,
|
||||
blank=True,
|
||||
null=True
|
||||
null=True,
|
||||
help_text='GPS coordinate (longitude)'
|
||||
)
|
||||
contact_name = models.CharField(
|
||||
max_length=50,
|
||||
@@ -288,7 +304,8 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackGroup(ChangeLoggedModel):
|
||||
@extras_features('export_templates')
|
||||
class RackGroup(MPTTModel, ChangeLoggedModel):
|
||||
"""
|
||||
Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
|
||||
example, if a Site spans a corporate campus, a RackGroup might be defined to represent each building within that
|
||||
@@ -303,8 +320,20 @@ class RackGroup(ChangeLoggedModel):
|
||||
on_delete=models.CASCADE,
|
||||
related_name='rack_groups'
|
||||
)
|
||||
parent = TreeForeignKey(
|
||||
to='self',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='children',
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
csv_headers = ['site', 'name', 'slug']
|
||||
csv_headers = ['site', 'parent', 'name', 'slug', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
@@ -313,6 +342,9 @@ class RackGroup(ChangeLoggedModel):
|
||||
['site', 'slug'],
|
||||
]
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -322,10 +354,27 @@ class RackGroup(ChangeLoggedModel):
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.site,
|
||||
self.parent.name if self.parent else '',
|
||||
self.name,
|
||||
self.slug,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Remove MPTT-internal fields
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Parent RackGroup (if any) must belong to the same Site
|
||||
if self.parent and self.parent.site != self.site:
|
||||
raise ValidationError(f"Parent rack group ({self.parent}) must belong to the same site ({self.site})")
|
||||
|
||||
|
||||
class RackRole(ChangeLoggedModel):
|
||||
"""
|
||||
@@ -338,9 +387,11 @@ class RackRole(ChangeLoggedModel):
|
||||
slug = models.SlugField(
|
||||
unique=True
|
||||
)
|
||||
color = ColorField()
|
||||
color = ColorField(
|
||||
default=ColorChoices.COLOR_GREY
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
max_length=200,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
@@ -364,6 +415,7 @@ class RackRole(ChangeLoggedModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
"""
|
||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||
@@ -381,7 +433,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
max_length=50,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Facility ID'
|
||||
verbose_name='Facility ID',
|
||||
help_text='Locally-assigned identifier'
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
@@ -393,7 +446,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='racks',
|
||||
blank=True,
|
||||
null=True
|
||||
null=True,
|
||||
help_text='Assigned group'
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
@@ -412,7 +466,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
on_delete=models.PROTECT,
|
||||
related_name='racks',
|
||||
blank=True,
|
||||
null=True
|
||||
null=True,
|
||||
help_text='Functional role'
|
||||
)
|
||||
serial = models.CharField(
|
||||
max_length=50,
|
||||
@@ -442,7 +497,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
u_height = models.PositiveSmallIntegerField(
|
||||
default=RACK_U_HEIGHT_DEFAULT,
|
||||
verbose_name='Height (U)',
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)]
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)],
|
||||
help_text='Height in rack units'
|
||||
)
|
||||
desc_units = models.BooleanField(
|
||||
default=False,
|
||||
@@ -451,11 +507,13 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
)
|
||||
outer_width = models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
null=True,
|
||||
help_text='Outer dimension of rack (width)'
|
||||
)
|
||||
outer_depth = models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
null=True,
|
||||
help_text='Outer dimension of rack (depth)'
|
||||
)
|
||||
outer_unit = models.CharField(
|
||||
max_length=50,
|
||||
@@ -476,7 +534,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
'site', 'group_name', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
|
||||
'site', 'group', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
|
||||
'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
@@ -615,7 +673,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
pk=exclude
|
||||
).filter(
|
||||
rack=self,
|
||||
position__gt=0
|
||||
position__gt=0,
|
||||
device_type__u_height__gt=0
|
||||
).filter(
|
||||
Q(face=face) | Q(device_type__is_full_depth=True)
|
||||
)
|
||||
@@ -682,7 +741,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT,
|
||||
unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT,
|
||||
legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT,
|
||||
include_images=True
|
||||
include_images=True,
|
||||
base_url=None
|
||||
):
|
||||
"""
|
||||
Return an SVG of the rack elevation
|
||||
@@ -693,8 +753,9 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
height of the elevation
|
||||
:param legend_width: Width of the unit legend, in pixels
|
||||
:param include_images: Embed front/rear device images where available
|
||||
:param base_url: Base URL for links and images. If none, URLs will be relative.
|
||||
"""
|
||||
elevation = RackElevationSVG(self, include_images=include_images)
|
||||
elevation = RackElevationSVG(self, include_images=include_images, base_url=base_url)
|
||||
|
||||
return elevation.render(face, unit_width, unit_height, legend_width)
|
||||
|
||||
@@ -739,6 +800,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
return 0
|
||||
|
||||
|
||||
@extras_features('custom_links', 'export_templates', 'webhooks')
|
||||
class RackReservation(ChangeLoggedModel):
|
||||
"""
|
||||
One or more reserved units within a Rack.
|
||||
@@ -763,7 +825,7 @@ class RackReservation(ChangeLoggedModel):
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100
|
||||
max_length=200
|
||||
)
|
||||
|
||||
csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description']
|
||||
@@ -774,9 +836,12 @@ class RackReservation(ChangeLoggedModel):
|
||||
def __str__(self):
|
||||
return "Reservation for rack {}".format(self.rack)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rackreservation', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
|
||||
if self.units:
|
||||
if hasattr(self, 'rack') and self.units:
|
||||
|
||||
# Validate that all specified units exist in the Rack.
|
||||
invalid_units = [u for u in self.units if u not in self.rack.units]
|
||||
@@ -825,6 +890,7 @@ class RackReservation(ChangeLoggedModel):
|
||||
# Device Types
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
class Manufacturer(ChangeLoggedModel):
|
||||
"""
|
||||
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
|
||||
@@ -836,8 +902,12 @@ class Manufacturer(ChangeLoggedModel):
|
||||
slug = models.SlugField(
|
||||
unique=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
csv_headers = ['name', 'slug']
|
||||
csv_headers = ['name', 'slug', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@@ -852,9 +922,11 @@ class Manufacturer(ChangeLoggedModel):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.description
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
"""
|
||||
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
||||
@@ -1039,17 +1111,32 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
# If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have
|
||||
# room to expand within their racks. This validation will impose a very high performance penalty when there are
|
||||
# many instances to check, but increasing the u_height of a DeviceType should be a very rare occurrence.
|
||||
if self.pk is not None and self.u_height > self._original_u_height:
|
||||
if self.pk and self.u_height > self._original_u_height:
|
||||
for d in Device.objects.filter(device_type=self, position__isnull=False):
|
||||
face_required = None if self.is_full_depth else d.face
|
||||
u_available = d.rack.get_available_units(u_height=self.u_height, rack_face=face_required,
|
||||
exclude=[d.pk])
|
||||
u_available = d.rack.get_available_units(
|
||||
u_height=self.u_height,
|
||||
rack_face=face_required,
|
||||
exclude=[d.pk]
|
||||
)
|
||||
if d.position not in u_available:
|
||||
raise ValidationError({
|
||||
'u_height': "Device {} in rack {} does not have sufficient space to accommodate a height of "
|
||||
"{}U".format(d, d.rack, self.u_height)
|
||||
})
|
||||
|
||||
# If modifying the height of an existing DeviceType to 0U, check for any instances assigned to a rack position.
|
||||
elif self.pk and self._original_u_height > 0 and self.u_height == 0:
|
||||
racked_instance_count = Device.objects.filter(device_type=self, position__isnull=False).count()
|
||||
if racked_instance_count:
|
||||
url = f"{reverse('dcim:device_list')}?manufactuer_id={self.manufacturer_id}&device_type_id={self.pk}"
|
||||
raise ValidationError({
|
||||
'u_height': mark_safe(
|
||||
f'Unable to set 0U height: Found <a href="{url}">{racked_instance_count} instances</a> already '
|
||||
f'mounted within racks.'
|
||||
)
|
||||
})
|
||||
|
||||
if (
|
||||
self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT
|
||||
) and self.device_bay_templates.count():
|
||||
@@ -1113,14 +1200,16 @@ class DeviceRole(ChangeLoggedModel):
|
||||
slug = models.SlugField(
|
||||
unique=True
|
||||
)
|
||||
color = ColorField()
|
||||
color = ColorField(
|
||||
default=ColorChoices.COLOR_GREY
|
||||
)
|
||||
vm_role = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name='VM Role',
|
||||
help_text='Virtual machines may be assigned to this role'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
max_length=200,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
@@ -1176,8 +1265,12 @@ class Platform(ChangeLoggedModel):
|
||||
verbose_name='NAPALM arguments',
|
||||
help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args']
|
||||
csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@@ -1195,9 +1288,11 @@ class Platform(ChangeLoggedModel):
|
||||
self.manufacturer.name if self.manufacturer else None,
|
||||
self.napalm_driver,
|
||||
self.napalm_args,
|
||||
self.description,
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
|
||||
class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
"""
|
||||
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
||||
@@ -1342,7 +1437,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
@@ -1383,7 +1478,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
# because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
|
||||
# of the uniqueness constraint without manual intervention.
|
||||
if self.name and self.tenant is None:
|
||||
if Device.objects.exclude(pk=self.pk).filter(name=self.name, tenant__isnull=True):
|
||||
if Device.objects.exclude(pk=self.pk).filter(name=self.name, site=self.site, tenant__isnull=True):
|
||||
raise ValidationError({
|
||||
'name': 'A device with this name already exists.'
|
||||
})
|
||||
@@ -1458,24 +1553,30 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
# Validate primary IP addresses
|
||||
vc_interfaces = self.vc_interfaces.all()
|
||||
if self.primary_ip4:
|
||||
if self.primary_ip4.family != 4:
|
||||
raise ValidationError({
|
||||
'primary_ip4': f"{self.primary_ip4} is not an IPv4 address."
|
||||
})
|
||||
if self.primary_ip4.interface in vc_interfaces:
|
||||
pass
|
||||
elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.interface in vc_interfaces:
|
||||
pass
|
||||
else:
|
||||
raise ValidationError({
|
||||
'primary_ip4': "The specified IP address ({}) is not assigned to this device.".format(
|
||||
self.primary_ip4),
|
||||
'primary_ip4': f"The specified IP address ({self.primary_ip4}) is not assigned to this device."
|
||||
})
|
||||
if self.primary_ip6:
|
||||
if self.primary_ip6.family != 6:
|
||||
raise ValidationError({
|
||||
'primary_ip6': f"{self.primary_ip6} is not an IPv6 address."
|
||||
})
|
||||
if self.primary_ip6.interface in vc_interfaces:
|
||||
pass
|
||||
elif self.primary_ip6.nat_inside is not None and self.primary_ip6.nat_inside.interface in vc_interfaces:
|
||||
pass
|
||||
else:
|
||||
raise ValidationError({
|
||||
'primary_ip6': "The specified IP address ({}) is not assigned to this device.".format(
|
||||
self.primary_ip6),
|
||||
'primary_ip6': f"The specified IP address ({self.primary_ip6}) is not assigned to this device."
|
||||
})
|
||||
|
||||
# Validate manufacturer/platform
|
||||
@@ -1633,6 +1734,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
@extras_features('custom_links', 'export_templates', 'webhooks')
|
||||
class VirtualChassis(ChangeLoggedModel):
|
||||
"""
|
||||
A collection of Devices which operate with a shared control plane (e.g. a switch stack).
|
||||
@@ -1659,7 +1761,7 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
return str(self.master) if hasattr(self, 'master') else 'New Virtual Chassis'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.master.get_absolute_url()
|
||||
return reverse('dcim:virtualchassis', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -1699,6 +1801,7 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
# Power
|
||||
#
|
||||
|
||||
@extras_features('custom_links', 'export_templates', 'webhooks')
|
||||
class PowerPanel(ChangeLoggedModel):
|
||||
"""
|
||||
A distribution point for electrical power; e.g. a data center RPP.
|
||||
@@ -1717,7 +1820,7 @@ class PowerPanel(ChangeLoggedModel):
|
||||
max_length=50
|
||||
)
|
||||
|
||||
csv_headers = ['site', 'rack_group_name', 'name']
|
||||
csv_headers = ['site', 'rack_group', 'name']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
@@ -1745,6 +1848,7 @@ class PowerPanel(ChangeLoggedModel):
|
||||
))
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
|
||||
"""
|
||||
An electrical circuit delivered from a PowerPanel.
|
||||
@@ -1823,7 +1927,7 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
'site', 'panel_name', 'rack_group', 'rack_name', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
||||
'site', 'power_panel', 'rack_group', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
||||
'amperage', 'max_utilization', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
@@ -1895,6 +1999,10 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return self.power_panel
|
||||
|
||||
def get_type_class(self):
|
||||
return self.TYPE_CLASS_MAP.get(self.type)
|
||||
|
||||
@@ -1906,6 +2014,7 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
|
||||
# Cables
|
||||
#
|
||||
|
||||
@extras_features('custom_links', 'export_templates', 'webhooks')
|
||||
class Cable(ChangeLoggedModel):
|
||||
"""
|
||||
A physical connection between two endpoints.
|
||||
@@ -2006,6 +2115,20 @@ class Cable(ChangeLoggedModel):
|
||||
# A copy of the PK to be used by __str__ in case the object is deleted
|
||||
self._pk = self.pk
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, db, field_names, values):
|
||||
"""
|
||||
Cache the original A and B terminations of existing Cable instances for later reference inside clean().
|
||||
"""
|
||||
instance = super().from_db(db, field_names, values)
|
||||
|
||||
instance._orig_termination_a_type = instance.termination_a_type
|
||||
instance._orig_termination_a_id = instance.termination_a_id
|
||||
instance._orig_termination_b_type = instance.termination_b_type
|
||||
instance._orig_termination_b_id = instance.termination_b_id
|
||||
|
||||
return instance
|
||||
|
||||
def __str__(self):
|
||||
return self.label or '#{}'.format(self._pk)
|
||||
|
||||
@@ -2034,6 +2157,24 @@ class Cable(ChangeLoggedModel):
|
||||
'termination_b': 'Invalid ID for type {}'.format(self.termination_b_type)
|
||||
})
|
||||
|
||||
# If editing an existing Cable instance, check that neither termination has been modified.
|
||||
if self.pk:
|
||||
err_msg = 'Cable termination points may not be modified. Delete and recreate the cable instead.'
|
||||
if (
|
||||
self.termination_a_type != self._orig_termination_a_type or
|
||||
self.termination_a_id != self._orig_termination_a_id
|
||||
):
|
||||
raise ValidationError({
|
||||
'termination_a': err_msg
|
||||
})
|
||||
if (
|
||||
self.termination_b_type != self._orig_termination_b_type or
|
||||
self.termination_b_id != self._orig_termination_b_id
|
||||
):
|
||||
raise ValidationError({
|
||||
'termination_b': err_msg
|
||||
})
|
||||
|
||||
type_a = self.termination_a_type.model
|
||||
type_b = self.termination_b_type.model
|
||||
|
||||
@@ -2053,23 +2194,29 @@ class Cable(ChangeLoggedModel):
|
||||
|
||||
# Check that termination types are compatible
|
||||
if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
|
||||
raise ValidationError("Incompatible termination types: {} and {}".format(
|
||||
self.termination_a_type, self.termination_b_type
|
||||
))
|
||||
|
||||
# A component with multiple positions must be connected to a component with an equal number of positions
|
||||
term_a_positions = getattr(self.termination_a, 'positions', 1)
|
||||
term_b_positions = getattr(self.termination_b, 'positions', 1)
|
||||
if term_a_positions != term_b_positions:
|
||||
raise ValidationError(
|
||||
"{} has {} positions and {} has {}. Both terminations must have the same number of positions.".format(
|
||||
self.termination_a, term_a_positions, self.termination_b, term_b_positions
|
||||
)
|
||||
f"Incompatible termination types: {self.termination_a_type} and {self.termination_b_type}"
|
||||
)
|
||||
|
||||
# A RearPort with multiple positions must be connected to a RearPort with an equal number of positions
|
||||
for term_a, term_b in [
|
||||
(self.termination_a, self.termination_b),
|
||||
(self.termination_b, self.termination_a)
|
||||
]:
|
||||
if isinstance(term_a, RearPort) and term_a.positions > 1:
|
||||
if not isinstance(term_b, RearPort):
|
||||
raise ValidationError(
|
||||
"Rear ports with multiple positions may only be connected to other rear ports"
|
||||
)
|
||||
elif term_a.positions != term_b.positions:
|
||||
raise ValidationError(
|
||||
f"{term_a} has {term_a.positions} position(s) but {term_b} has {term_b.positions}. "
|
||||
f"Both terminations must have the same number of positions."
|
||||
)
|
||||
|
||||
# A termination point cannot be connected to itself
|
||||
if self.termination_a == self.termination_b:
|
||||
raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type))
|
||||
raise ValidationError(f"Cannot connect {self.termination_a_type} to itself")
|
||||
|
||||
# A front port cannot be connected to its corresponding rear port
|
||||
if (
|
||||
@@ -2141,26 +2288,3 @@ class Cable(ChangeLoggedModel):
|
||||
if self.termination_a is None:
|
||||
return
|
||||
return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
|
||||
|
||||
def get_path_endpoints(self):
|
||||
"""
|
||||
Traverse both ends of a cable path and return its connected endpoints. Note that one or both endpoints may be
|
||||
None.
|
||||
"""
|
||||
a_path = self.termination_b.trace()
|
||||
b_path = self.termination_a.trace()
|
||||
|
||||
# Determine overall path status (connected or planned)
|
||||
if self.status == CableStatusChoices.STATUS_CONNECTED:
|
||||
path_status = True
|
||||
for segment in a_path[1:] + b_path[1:]:
|
||||
if segment[1] is None or segment[1].status != CableStatusChoices.STATUS_CONNECTED:
|
||||
path_status = False
|
||||
break
|
||||
else:
|
||||
path_status = False
|
||||
|
||||
a_endpoint = a_path[-1][2]
|
||||
b_endpoint = b_path[-1][2]
|
||||
|
||||
return a_endpoint, b_endpoint, path_status
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
@@ -8,11 +10,13 @@ from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.exceptions import LoopDetected
|
||||
from dcim.exceptions import CableTraceSplit
|
||||
from dcim.fields import MACAddressField
|
||||
from extras.models import ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from utilities.fields import NaturalOrderingField
|
||||
from utilities.ordering import naturalize_interface
|
||||
from utilities.query_functions import CollateAsChar
|
||||
from utilities.utils import serialize_object
|
||||
from virtualization.choices import VMInterfaceTypeChoices
|
||||
|
||||
@@ -33,7 +37,7 @@ __all__ = (
|
||||
|
||||
class ComponentModel(models.Model):
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
@@ -87,74 +91,102 @@ class CableTermination(models.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def trace(self, position=1, follow_circuits=False, cable_history=None):
|
||||
def trace(self):
|
||||
"""
|
||||
Return a list representing a complete cable path, with each individual segment represented as a three-tuple:
|
||||
Return two items: the traceable portion of a cable path, and the termination points where it splits (if any).
|
||||
This occurs when the trace is initiated from a midpoint along a path which traverses a RearPort. In cases where
|
||||
the originating endpoint is unknown, it is not possible to know which corresponding FrontPort to follow.
|
||||
|
||||
The path is a list representing a complete cable path, with each individual segment represented as a
|
||||
three-tuple:
|
||||
|
||||
[
|
||||
(termination A, cable, termination B),
|
||||
(termination C, cable, termination D),
|
||||
(termination E, cable, termination F)
|
||||
]
|
||||
"""
|
||||
def get_peer_port(termination, position=1, follow_circuits=False):
|
||||
endpoint = self
|
||||
path = []
|
||||
position_stack = []
|
||||
|
||||
def get_peer_port(termination):
|
||||
from circuits.models import CircuitTermination
|
||||
|
||||
# Map a front port to its corresponding rear port
|
||||
if isinstance(termination, FrontPort):
|
||||
return termination.rear_port, termination.rear_port_position
|
||||
position_stack.append(termination.rear_port_position)
|
||||
# Retrieve the corresponding RearPort from database to ensure we have an up-to-date instance
|
||||
peer_port = RearPort.objects.get(pk=termination.rear_port.pk)
|
||||
return peer_port
|
||||
|
||||
# Map a rear port/position to its corresponding front port
|
||||
elif isinstance(termination, RearPort):
|
||||
|
||||
# Can't map to a FrontPort without a position if there are multiple options
|
||||
if termination.positions > 1 and not position_stack:
|
||||
raise CableTraceSplit(termination)
|
||||
|
||||
# We can assume position 1 if the RearPort has only one position
|
||||
position = position_stack.pop() if position_stack else 1
|
||||
|
||||
# Validate the position
|
||||
if position not in range(1, termination.positions + 1):
|
||||
raise Exception("Invalid position for {} ({} positions): {})".format(
|
||||
termination, termination.positions, position
|
||||
))
|
||||
|
||||
try:
|
||||
peer_port = FrontPort.objects.get(
|
||||
rear_port=termination,
|
||||
rear_port_position=position,
|
||||
)
|
||||
return peer_port, 1
|
||||
return peer_port
|
||||
except ObjectDoesNotExist:
|
||||
return None, None
|
||||
return None
|
||||
|
||||
# Follow a circuit to its other termination
|
||||
elif isinstance(termination, CircuitTermination) and follow_circuits:
|
||||
elif isinstance(termination, CircuitTermination):
|
||||
peer_termination = termination.get_peer_termination()
|
||||
if peer_termination is None:
|
||||
return None, None
|
||||
return peer_termination, position
|
||||
return None
|
||||
return peer_termination
|
||||
|
||||
# Termination is not a pass-through port
|
||||
else:
|
||||
return None, None
|
||||
return None
|
||||
|
||||
if not self.cable:
|
||||
return [(self, None, None)]
|
||||
logger = logging.getLogger('netbox.dcim.cable.trace')
|
||||
logger.debug("Tracing cable from {} {}".format(self.parent, self))
|
||||
|
||||
# Record cable history to detect loops
|
||||
if cable_history is None:
|
||||
cable_history = []
|
||||
elif self.cable in cable_history:
|
||||
raise LoopDetected()
|
||||
cable_history.append(self.cable)
|
||||
while endpoint is not None:
|
||||
|
||||
far_end = self.cable.termination_b if self.cable.termination_a == self else self.cable.termination_a
|
||||
path = [(self, self.cable, far_end)]
|
||||
# No cable connected; nothing to trace
|
||||
if not endpoint.cable:
|
||||
path.append((endpoint, None, None))
|
||||
logger.debug("No cable connected")
|
||||
return path, None
|
||||
|
||||
peer_port, position = get_peer_port(far_end, position, follow_circuits)
|
||||
if peer_port is None:
|
||||
return path
|
||||
# Check for loops
|
||||
if endpoint.cable in [segment[1] for segment in path]:
|
||||
logger.debug("Loop detected!")
|
||||
return path, None
|
||||
|
||||
try:
|
||||
next_segment = peer_port.trace(position, follow_circuits, cable_history)
|
||||
except LoopDetected:
|
||||
return path
|
||||
# Record the current segment in the path
|
||||
far_end = endpoint.get_cable_peer()
|
||||
path.append((endpoint, endpoint.cable, far_end))
|
||||
logger.debug("{}[{}] --- Cable {} ---> {}[{}]".format(
|
||||
endpoint.parent, endpoint, endpoint.cable.pk, far_end.parent, far_end
|
||||
))
|
||||
|
||||
if next_segment is None:
|
||||
return path + [(peer_port, None, None)]
|
||||
# Get the peer port of the far end termination
|
||||
try:
|
||||
endpoint = get_peer_port(far_end)
|
||||
except CableTraceSplit as e:
|
||||
return path, e.termination.frontports.all()
|
||||
|
||||
return path + next_segment
|
||||
if endpoint is None:
|
||||
return path, None
|
||||
|
||||
def get_cable_peer(self):
|
||||
if self.cable is None:
|
||||
@@ -164,11 +196,29 @@ class CableTermination(models.Model):
|
||||
if self._cabled_as_b.exists():
|
||||
return self.cable.termination_a
|
||||
|
||||
def get_path_endpoints(self):
|
||||
"""
|
||||
Return all endpoints of paths which traverse this object.
|
||||
"""
|
||||
endpoints = []
|
||||
|
||||
# Get the far end of the last path segment
|
||||
path, split_ends = self.trace()
|
||||
endpoint = path[-1][2]
|
||||
if split_ends is not None:
|
||||
for termination in split_ends:
|
||||
endpoints.extend(termination.get_path_endpoints())
|
||||
elif endpoint is not None:
|
||||
endpoints.append(endpoint)
|
||||
|
||||
return endpoints
|
||||
|
||||
|
||||
#
|
||||
# Console ports
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
class ConsolePort(CableTermination, ComponentModel):
|
||||
"""
|
||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||
@@ -189,7 +239,8 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=ConsolePortTypeChoices,
|
||||
blank=True
|
||||
blank=True,
|
||||
help_text='Physical port type'
|
||||
)
|
||||
connected_endpoint = models.OneToOneField(
|
||||
to='dcim.ConsoleServerPort',
|
||||
@@ -229,6 +280,7 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
"""
|
||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||
@@ -249,7 +301,8 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=ConsolePortTypeChoices,
|
||||
blank=True
|
||||
blank=True,
|
||||
help_text='Physical port type'
|
||||
)
|
||||
connection_status = models.NullBooleanField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
@@ -282,6 +335,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
# Power ports
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
class PowerPort(CableTermination, ComponentModel):
|
||||
"""
|
||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||
@@ -302,7 +356,8 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PowerPortTypeChoices,
|
||||
blank=True
|
||||
blank=True,
|
||||
help_text='Physical port type'
|
||||
)
|
||||
maximum_draw = models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
@@ -443,6 +498,7 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
class PowerOutlet(CableTermination, ComponentModel):
|
||||
"""
|
||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||
@@ -463,7 +519,8 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PowerOutletTypeChoices,
|
||||
blank=True
|
||||
blank=True,
|
||||
help_text='Physical port type'
|
||||
)
|
||||
power_port = models.ForeignKey(
|
||||
to='dcim.PowerPort',
|
||||
@@ -519,6 +576,7 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
@extras_features('graphs', 'export_templates', 'webhooks')
|
||||
class Interface(CableTermination, ComponentModel):
|
||||
"""
|
||||
A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
|
||||
@@ -599,7 +657,7 @@ class Interface(CableTermination, ComponentModel):
|
||||
mode = models.CharField(
|
||||
max_length=50,
|
||||
choices=InterfaceModeChoices,
|
||||
blank=True,
|
||||
blank=True
|
||||
)
|
||||
untagged_vlan = models.ForeignKey(
|
||||
to='ipam.VLAN',
|
||||
@@ -624,7 +682,7 @@ class Interface(CableTermination, ComponentModel):
|
||||
|
||||
class Meta:
|
||||
# TODO: ordering and unique_together should include virtual_machine
|
||||
ordering = ('device', '_name')
|
||||
ordering = ('device', CollateAsChar('_name'))
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def __str__(self):
|
||||
@@ -792,6 +850,7 @@ class Interface(CableTermination, ComponentModel):
|
||||
# Pass-through ports
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
class FrontPort(CableTermination, ComponentModel):
|
||||
"""
|
||||
A pass-through port on the front of a Device.
|
||||
@@ -864,6 +923,7 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
class RearPort(CableTermination, ComponentModel):
|
||||
"""
|
||||
A pass-through port on the rear of a Device.
|
||||
@@ -915,6 +975,7 @@ class RearPort(CableTermination, ComponentModel):
|
||||
# Device bays
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
class DeviceBay(ComponentModel):
|
||||
"""
|
||||
An empty space within a Device which can house a child device
|
||||
@@ -989,6 +1050,7 @@ class DeviceBay(ComponentModel):
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
class InventoryItem(ComponentModel):
|
||||
"""
|
||||
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
||||
@@ -1025,7 +1087,8 @@ class InventoryItem(ComponentModel):
|
||||
part_id = models.CharField(
|
||||
max_length=50,
|
||||
verbose_name='Part ID',
|
||||
blank=True
|
||||
blank=True,
|
||||
help_text='Manufacturer-assigned part identifier'
|
||||
)
|
||||
serial = models.CharField(
|
||||
max_length=50,
|
||||
@@ -1042,7 +1105,7 @@ class InventoryItem(ComponentModel):
|
||||
)
|
||||
discovered = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name='Discovered'
|
||||
help_text='This item was automatically discovered'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import logging
|
||||
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .choices import CableStatusChoices
|
||||
from .models import Cable, Device, VirtualChassis
|
||||
|
||||
|
||||
@@ -34,24 +37,40 @@ def update_connected_endpoints(instance, **kwargs):
|
||||
"""
|
||||
When a Cable is saved, check for and update its two connected endpoints
|
||||
"""
|
||||
logger = logging.getLogger('netbox.dcim.cable')
|
||||
|
||||
# Cache the Cable on its two termination points
|
||||
if instance.termination_a.cable != instance:
|
||||
logger.debug("Updating termination A for cable {}".format(instance))
|
||||
instance.termination_a.cable = instance
|
||||
instance.termination_a.save()
|
||||
if instance.termination_b.cable != instance:
|
||||
logger.debug("Updating termination B for cable {}".format(instance))
|
||||
instance.termination_b.cable = instance
|
||||
instance.termination_b.save()
|
||||
|
||||
# Check if this Cable has formed a complete path. If so, update both endpoints.
|
||||
endpoint_a, endpoint_b, path_status = instance.get_path_endpoints()
|
||||
if getattr(endpoint_a, 'is_path_endpoint', False) and getattr(endpoint_b, 'is_path_endpoint', False):
|
||||
endpoint_a.connected_endpoint = endpoint_b
|
||||
endpoint_a.connection_status = path_status
|
||||
endpoint_a.save()
|
||||
endpoint_b.connected_endpoint = endpoint_a
|
||||
endpoint_b.connection_status = path_status
|
||||
endpoint_b.save()
|
||||
# Update any endpoints for this Cable.
|
||||
endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints()
|
||||
for endpoint in endpoints:
|
||||
path, split_ends = endpoint.trace()
|
||||
# Determine overall path status (connected or planned)
|
||||
path_status = True
|
||||
for segment in path:
|
||||
if segment[1] is None or segment[1].status != CableStatusChoices.STATUS_CONNECTED:
|
||||
path_status = False
|
||||
break
|
||||
|
||||
endpoint_a = path[0][0]
|
||||
endpoint_b = path[-1][2]
|
||||
|
||||
if getattr(endpoint_a, 'is_path_endpoint', False) and getattr(endpoint_b, 'is_path_endpoint', False):
|
||||
logger.debug("Updating path endpoints: {} <---> {}".format(endpoint_a, endpoint_b))
|
||||
endpoint_a.connected_endpoint = endpoint_b
|
||||
endpoint_a.connection_status = path_status
|
||||
endpoint_a.save()
|
||||
endpoint_b.connected_endpoint = endpoint_a
|
||||
endpoint_b.connection_status = path_status
|
||||
endpoint_b.save()
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Cable)
|
||||
@@ -59,21 +78,24 @@ def nullify_connected_endpoints(instance, **kwargs):
|
||||
"""
|
||||
When a Cable is deleted, check for and update its two connected endpoints
|
||||
"""
|
||||
endpoint_a, endpoint_b, _ = instance.get_path_endpoints()
|
||||
logger = logging.getLogger('netbox.dcim.cable')
|
||||
|
||||
endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints()
|
||||
|
||||
# Disassociate the Cable from its termination points
|
||||
if instance.termination_a is not None:
|
||||
logger.debug("Nullifying termination A for cable {}".format(instance))
|
||||
instance.termination_a.cable = None
|
||||
instance.termination_a.save()
|
||||
if instance.termination_b is not None:
|
||||
logger.debug("Nullifying termination B for cable {}".format(instance))
|
||||
instance.termination_b.cable = None
|
||||
instance.termination_b.save()
|
||||
|
||||
# If this Cable was part of a complete path, tear it down
|
||||
if hasattr(endpoint_a, 'connected_endpoint') and hasattr(endpoint_b, 'connected_endpoint'):
|
||||
endpoint_a.connected_endpoint = None
|
||||
endpoint_a.connection_status = None
|
||||
endpoint_a.save()
|
||||
endpoint_b.connected_endpoint = None
|
||||
endpoint_b.connection_status = None
|
||||
endpoint_b.save()
|
||||
# If this Cable was part of any complete end-to-end paths, tear them down.
|
||||
for endpoint in endpoints:
|
||||
logger.debug(f"Removing path information for {endpoint}")
|
||||
if hasattr(endpoint, 'connected_endpoint'):
|
||||
endpoint.connected_endpoint = None
|
||||
endpoint.connection_status = None
|
||||
endpoint.save()
|
||||
|
||||
@@ -2,7 +2,7 @@ import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
|
||||
from utilities.tables import BaseTable, BooleanColumn, ColorColumn, TagColumn, ToggleColumn
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
@@ -11,13 +11,13 @@ from .models import (
|
||||
VirtualChassis,
|
||||
)
|
||||
|
||||
REGION_LINK = """
|
||||
MPTT_LINK = """
|
||||
{% if record.get_children %}
|
||||
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>
|
||||
{% else %}
|
||||
<span style="padding-left: {{ record.get_ancestors|length }}9px">
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:site_list' %}?region={{ record.slug }}">{{ record.name }}</a>
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.name }}</a>
|
||||
</span>
|
||||
"""
|
||||
|
||||
@@ -165,15 +165,6 @@ UTILIZATION_GRAPH = """
|
||||
{% utilization_graph value %}
|
||||
"""
|
||||
|
||||
VIRTUALCHASSIS_ACTIONS = """
|
||||
<a href="{% url 'dcim:virtualchassis_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_virtualchassis %}
|
||||
<a href="{% url 'dcim:virtualchassis_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
CABLE_TERMINATION_PARENT = """
|
||||
{% if value.device %}
|
||||
<a href="{{ value.device.get_absolute_url }}">{{ value.device }}</a>
|
||||
@@ -214,9 +205,13 @@ def get_component_template_actions(model_name):
|
||||
|
||||
class RegionTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.TemplateColumn(template_code=REGION_LINK, orderable=False)
|
||||
site_count = tables.Column(verbose_name='Sites')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
name = tables.TemplateColumn(
|
||||
template_code=MPTT_LINK,
|
||||
orderable=False
|
||||
)
|
||||
site_count = tables.Column(
|
||||
verbose_name='Sites'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=REGION_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -225,7 +220,8 @@ class RegionTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Region
|
||||
fields = ('pk', 'name', 'site_count', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
||||
|
||||
|
||||
#
|
||||
@@ -234,14 +230,30 @@ class RegionTable(BaseTable):
|
||||
|
||||
class SiteTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(order_by=('_name',))
|
||||
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
name = tables.LinkColumn(
|
||||
order_by=('_name',)
|
||||
)
|
||||
status = tables.TemplateColumn(
|
||||
template_code=STATUS_LABEL
|
||||
)
|
||||
region = tables.TemplateColumn(
|
||||
template_code=SITE_REGION_LINK
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='dcim:site_list'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Site
|
||||
fields = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description')
|
||||
fields = (
|
||||
'pk', 'name', 'slug', 'status', 'facility', 'region', 'tenant', 'asn', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description')
|
||||
|
||||
|
||||
#
|
||||
@@ -250,7 +262,10 @@ class SiteTable(BaseTable):
|
||||
|
||||
class RackGroupTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
name = tables.TemplateColumn(
|
||||
template_code=MPTT_LINK,
|
||||
orderable=False
|
||||
)
|
||||
site = tables.LinkColumn(
|
||||
viewname='dcim:site',
|
||||
args=[Accessor('site.slug')],
|
||||
@@ -259,7 +274,6 @@ class RackGroupTable(BaseTable):
|
||||
rack_count = tables.Column(
|
||||
verbose_name='Racks'
|
||||
)
|
||||
slug = tables.Column()
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKGROUP_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -268,7 +282,8 @@ class RackGroupTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackGroup
|
||||
fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'site', 'rack_count', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'site', 'rack_count', 'description', 'actions')
|
||||
|
||||
|
||||
#
|
||||
@@ -288,6 +303,7 @@ class RackRoleTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackRole
|
||||
fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions')
|
||||
|
||||
|
||||
#
|
||||
@@ -296,17 +312,34 @@ class RackRoleTable(BaseTable):
|
||||
|
||||
class RackTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(order_by=('_name',))
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
role = tables.TemplateColumn(RACK_ROLE)
|
||||
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
||||
name = tables.LinkColumn(
|
||||
order_by=('_name',)
|
||||
)
|
||||
site = tables.LinkColumn(
|
||||
viewname='dcim:site',
|
||||
args=[Accessor('site.slug')]
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
status = tables.TemplateColumn(
|
||||
template_code=STATUS_LABEL
|
||||
)
|
||||
role = tables.TemplateColumn(
|
||||
template_code=RACK_ROLE
|
||||
)
|
||||
u_height = tables.TemplateColumn(
|
||||
template_code="{{ record.u_height }}U",
|
||||
verbose_name='Height'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Rack
|
||||
fields = ('pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height')
|
||||
fields = (
|
||||
'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
|
||||
'width', 'u_height',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height')
|
||||
|
||||
|
||||
class RackDetailTable(RackTable):
|
||||
@@ -324,9 +357,16 @@ class RackDetailTable(RackTable):
|
||||
orderable=False,
|
||||
verbose_name='Power'
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='dcim:rack_list'
|
||||
)
|
||||
|
||||
class Meta(RackTable.Meta):
|
||||
fields = (
|
||||
'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
|
||||
'width', 'u_height', 'device_count', 'get_utilization', 'get_power_utilization', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
|
||||
'get_utilization', 'get_power_utilization',
|
||||
)
|
||||
@@ -338,21 +378,41 @@ class RackDetailTable(RackTable):
|
||||
|
||||
class RackReservationTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
reservation = tables.LinkColumn(
|
||||
viewname='dcim:rackreservation',
|
||||
args=[Accessor('pk')],
|
||||
accessor='pk'
|
||||
)
|
||||
site = tables.LinkColumn(
|
||||
viewname='dcim:site',
|
||||
accessor=Accessor('rack.site'),
|
||||
args=[Accessor('rack.site.slug')],
|
||||
)
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||
unit_list = tables.Column(orderable=False, verbose_name='Units')
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
rack = tables.LinkColumn(
|
||||
viewname='dcim:rack',
|
||||
args=[Accessor('rack.pk')]
|
||||
)
|
||||
unit_list = tables.Column(
|
||||
orderable=False,
|
||||
verbose_name='Units'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
|
||||
template_code=RACKRESERVATION_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackReservation
|
||||
fields = ('pk', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions')
|
||||
fields = (
|
||||
'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description', 'actions',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -380,7 +440,9 @@ class ManufacturerTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Manufacturer
|
||||
fields = ('pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'slug', 'actions')
|
||||
fields = (
|
||||
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -394,17 +456,25 @@ class DeviceTypeTable(BaseTable):
|
||||
args=[Accessor('pk')],
|
||||
verbose_name='Device Type'
|
||||
)
|
||||
is_full_depth = BooleanColumn(verbose_name='Full Depth')
|
||||
is_full_depth = BooleanColumn(
|
||||
verbose_name='Full Depth'
|
||||
)
|
||||
instance_count = tables.TemplateColumn(
|
||||
template_code=DEVICETYPE_INSTANCES_TEMPLATE,
|
||||
verbose_name='Instances'
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='dcim:devicetype_list'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceType
|
||||
fields = (
|
||||
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
'instance_count',
|
||||
'pk', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
'instance_count', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
|
||||
)
|
||||
|
||||
|
||||
@@ -414,7 +484,9 @@ class DeviceTypeTable(BaseTable):
|
||||
|
||||
class ConsolePortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
name = tables.Column(
|
||||
order_by=('_name',)
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('consoleporttemplate'),
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -428,7 +500,10 @@ class ConsolePortTemplateTable(BaseTable):
|
||||
|
||||
|
||||
class ConsolePortImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
@@ -438,7 +513,9 @@ class ConsolePortImportTable(BaseTable):
|
||||
|
||||
class ConsoleServerPortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
name = tables.Column(
|
||||
order_by=('_name',)
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('consoleserverporttemplate'),
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -452,7 +529,10 @@ class ConsoleServerPortTemplateTable(BaseTable):
|
||||
|
||||
|
||||
class ConsoleServerPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
@@ -462,7 +542,9 @@ class ConsoleServerPortImportTable(BaseTable):
|
||||
|
||||
class PowerPortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
name = tables.Column(
|
||||
order_by=('_name',)
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('powerporttemplate'),
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -476,7 +558,10 @@ class PowerPortTemplateTable(BaseTable):
|
||||
|
||||
|
||||
class PowerPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
@@ -486,7 +571,9 @@ class PowerPortImportTable(BaseTable):
|
||||
|
||||
class PowerOutletTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
name = tables.Column(
|
||||
order_by=('_name',)
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('poweroutlettemplate'),
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -500,7 +587,10 @@ class PowerOutletTemplateTable(BaseTable):
|
||||
|
||||
|
||||
class PowerOutletImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerOutlet
|
||||
@@ -510,7 +600,9 @@ class PowerOutletImportTable(BaseTable):
|
||||
|
||||
class InterfaceTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}")
|
||||
mgmt_only = tables.TemplateColumn(
|
||||
template_code="{% if value %}OOB Management{% endif %}"
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('interfacetemplate'),
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -524,18 +616,30 @@ class InterfaceTemplateTable(BaseTable):
|
||||
|
||||
|
||||
class InterfaceImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
virtual_machine = tables.LinkColumn('virtualization:virtualmachine', args=[Accessor('virtual_machine.pk')], verbose_name='Virtual Machine')
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
virtual_machine = tables.LinkColumn(
|
||||
viewname='virtualization:virtualmachine',
|
||||
args=[Accessor('virtual_machine.pk')],
|
||||
verbose_name='Virtual Machine'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'mode')
|
||||
fields = (
|
||||
'device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu',
|
||||
'mgmt_only', 'mode',
|
||||
)
|
||||
empty_text = False
|
||||
|
||||
|
||||
class FrontPortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
name = tables.Column(
|
||||
order_by=('_name',)
|
||||
)
|
||||
rear_port_position = tables.Column(
|
||||
verbose_name='Position'
|
||||
)
|
||||
@@ -552,7 +656,10 @@ class FrontPortTemplateTable(BaseTable):
|
||||
|
||||
|
||||
class FrontPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = FrontPort
|
||||
@@ -562,7 +669,9 @@ class FrontPortImportTable(BaseTable):
|
||||
|
||||
class RearPortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
name = tables.Column(
|
||||
order_by=('_name',)
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('rearporttemplate'),
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -576,7 +685,10 @@ class RearPortTemplateTable(BaseTable):
|
||||
|
||||
|
||||
class RearPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RearPort
|
||||
@@ -586,7 +698,9 @@ class RearPortImportTable(BaseTable):
|
||||
|
||||
class DeviceBayTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
name = tables.Column(
|
||||
order_by=('_name',)
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('devicebaytemplate'),
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -617,8 +731,10 @@ class DeviceRoleTable(BaseTable):
|
||||
orderable=False,
|
||||
verbose_name='VMs'
|
||||
)
|
||||
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Label')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
color = tables.TemplateColumn(
|
||||
template_code=COLOR_LABEL,
|
||||
verbose_name='Label'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=DEVICEROLE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -628,6 +744,7 @@ class DeviceRoleTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceRole
|
||||
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions')
|
||||
|
||||
|
||||
#
|
||||
@@ -656,7 +773,13 @@ class PlatformTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Platform
|
||||
fields = ('pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'actions')
|
||||
fields = (
|
||||
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
|
||||
'description', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -669,40 +792,99 @@ class DeviceTable(BaseTable):
|
||||
order_by=('_name',),
|
||||
template_code=DEVICE_LINK
|
||||
)
|
||||
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
||||
status = tables.TemplateColumn(
|
||||
template_code=STATUS_LABEL
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
site = tables.LinkColumn(
|
||||
viewname='dcim:site',
|
||||
args=[Accessor('site.slug')]
|
||||
)
|
||||
rack = tables.LinkColumn(
|
||||
viewname='dcim:rack',
|
||||
args=[Accessor('rack.pk')]
|
||||
)
|
||||
device_role = tables.TemplateColumn(
|
||||
template_code=DEVICE_ROLE,
|
||||
verbose_name='Role'
|
||||
)
|
||||
device_type = tables.LinkColumn(
|
||||
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
||||
viewname='dcim:devicetype',
|
||||
args=[Accessor('device_type.pk')],
|
||||
verbose_name='Type',
|
||||
text=lambda record: record.device_type.display_name
|
||||
)
|
||||
primary_ip = tables.TemplateColumn(
|
||||
template_code=DEVICE_PRIMARY_IP,
|
||||
orderable=False,
|
||||
verbose_name='IP Address'
|
||||
)
|
||||
primary_ip4 = tables.LinkColumn(
|
||||
viewname='ipam:ipaddress',
|
||||
args=[Accessor('primary_ip4.pk')],
|
||||
verbose_name='IPv4 Address'
|
||||
)
|
||||
primary_ip6 = tables.LinkColumn(
|
||||
viewname='ipam:ipaddress',
|
||||
args=[Accessor('primary_ip6.pk')],
|
||||
verbose_name='IPv6 Address'
|
||||
)
|
||||
cluster = tables.LinkColumn(
|
||||
viewname='virtualization:cluster',
|
||||
args=[Accessor('cluster.pk')]
|
||||
)
|
||||
virtual_chassis = tables.LinkColumn(
|
||||
viewname='dcim:virtualchassis',
|
||||
args=[Accessor('virtual_chassis.pk')]
|
||||
)
|
||||
vc_position = tables.Column(
|
||||
verbose_name='VC Position'
|
||||
)
|
||||
vc_priority = tables.Column(
|
||||
verbose_name='VC Priority'
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='dcim:device_list'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type')
|
||||
|
||||
|
||||
class DeviceDetailTable(DeviceTable):
|
||||
primary_ip = tables.TemplateColumn(
|
||||
orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP
|
||||
)
|
||||
|
||||
class Meta(DeviceTable.Meta):
|
||||
model = Device
|
||||
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
|
||||
fields = (
|
||||
'pk', 'name', 'status', 'tenant', 'device_role', 'device_type', 'platform', 'serial', 'asset_tag', 'site',
|
||||
'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis',
|
||||
'vc_position', 'vc_priority', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip',
|
||||
)
|
||||
|
||||
|
||||
class DeviceImportTable(BaseTable):
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
||||
position = tables.Column(verbose_name='Position')
|
||||
device_role = tables.Column(verbose_name='Role')
|
||||
device_type = tables.Column(verbose_name='Type')
|
||||
name = tables.TemplateColumn(
|
||||
template_code=DEVICE_LINK
|
||||
)
|
||||
status = tables.TemplateColumn(
|
||||
template_code=STATUS_LABEL
|
||||
)
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
site = tables.LinkColumn(
|
||||
viewname='dcim:site',
|
||||
args=[Accessor('site.slug')]
|
||||
)
|
||||
rack = tables.LinkColumn(
|
||||
viewname='dcim:rack',
|
||||
args=[Accessor('rack.pk')]
|
||||
)
|
||||
device_role = tables.Column(
|
||||
verbose_name='Role'
|
||||
)
|
||||
device_type = tables.Column(
|
||||
verbose_name='Type'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
@@ -840,7 +1022,7 @@ class DeviceBayTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceBay
|
||||
fields = ('name',)
|
||||
fields = ('name', 'description')
|
||||
|
||||
|
||||
class DeviceBayDetailTable(DeviceComponentDetailTable):
|
||||
@@ -848,8 +1030,8 @@ class DeviceBayDetailTable(DeviceComponentDetailTable):
|
||||
installed_device = tables.LinkColumn()
|
||||
|
||||
class Meta(DeviceBayTable.Meta):
|
||||
fields = ('pk', 'name', 'device', 'installed_device')
|
||||
sequence = ('pk', 'name', 'device', 'installed_device')
|
||||
fields = ('pk', 'name', 'device', 'installed_device', 'description')
|
||||
sequence = ('pk', 'name', 'device', 'installed_device', 'description')
|
||||
exclude = ('cable',)
|
||||
|
||||
|
||||
@@ -878,23 +1060,23 @@ class CableTable(BaseTable):
|
||||
template_code=CABLE_TERMINATION_PARENT,
|
||||
accessor=Accessor('termination_a'),
|
||||
orderable=False,
|
||||
verbose_name='Termination A'
|
||||
verbose_name='Side A'
|
||||
)
|
||||
termination_a = tables.LinkColumn(
|
||||
accessor=Accessor('termination_a'),
|
||||
orderable=False,
|
||||
verbose_name=''
|
||||
verbose_name='Termination A'
|
||||
)
|
||||
termination_b_parent = tables.TemplateColumn(
|
||||
template_code=CABLE_TERMINATION_PARENT,
|
||||
accessor=Accessor('termination_b'),
|
||||
orderable=False,
|
||||
verbose_name='Termination B'
|
||||
verbose_name='Side B'
|
||||
)
|
||||
termination_b = tables.LinkColumn(
|
||||
accessor=Accessor('termination_b'),
|
||||
orderable=False,
|
||||
verbose_name=''
|
||||
verbose_name='Termination B'
|
||||
)
|
||||
status = tables.TemplateColumn(
|
||||
template_code=STATUS_LABEL
|
||||
@@ -911,6 +1093,10 @@ class CableTable(BaseTable):
|
||||
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
||||
'status', 'type', 'color', 'length',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
||||
'status', 'type',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -978,10 +1164,6 @@ class InterfaceConnectionTable(BaseTable):
|
||||
args=[Accessor('pk')],
|
||||
verbose_name='Interface A'
|
||||
)
|
||||
description_a = tables.Column(
|
||||
accessor=Accessor('description'),
|
||||
verbose_name='Description'
|
||||
)
|
||||
device_b = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
accessor=Accessor('_connected_interface.device'),
|
||||
@@ -994,15 +1176,11 @@ class InterfaceConnectionTable(BaseTable):
|
||||
args=[Accessor('_connected_interface.pk')],
|
||||
verbose_name='Interface B'
|
||||
)
|
||||
description_b = tables.Column(
|
||||
accessor=Accessor('_connected_interface.description'),
|
||||
verbose_name='Description'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = (
|
||||
'device_a', 'interface_a', 'description_a', 'device_b', 'interface_b', 'description_b', 'connection_status',
|
||||
'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status',
|
||||
)
|
||||
|
||||
|
||||
@@ -1012,12 +1190,21 @@ class InterfaceConnectionTable(BaseTable):
|
||||
|
||||
class InventoryItemTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
device = tables.LinkColumn('dcim:device_inventory', args=[Accessor('device.pk')])
|
||||
manufacturer = tables.Column(accessor=Accessor('manufacturer.name'), verbose_name='Manufacturer')
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device_inventory',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
manufacturer = tables.Column(
|
||||
accessor=Accessor('manufacturer')
|
||||
)
|
||||
discovered = BooleanColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InventoryItem
|
||||
fields = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description')
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered'
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||
|
||||
|
||||
#
|
||||
@@ -1026,17 +1213,21 @@ class InventoryItemTable(BaseTable):
|
||||
|
||||
class VirtualChassisTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
master = tables.LinkColumn()
|
||||
member_count = tables.Column(verbose_name='Members')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=VIRTUALCHASSIS_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
name = tables.Column(
|
||||
accessor=Accessor('master__name'),
|
||||
linkify=True
|
||||
)
|
||||
member_count = tables.Column(
|
||||
verbose_name='Members'
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='dcim:virtualchassis_list'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VirtualChassis
|
||||
fields = ('pk', 'master', 'domain', 'member_count', 'actions')
|
||||
fields = ('pk', 'name', 'domain', 'member_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'domain', 'member_count')
|
||||
|
||||
|
||||
#
|
||||
@@ -1058,6 +1249,7 @@ class PowerPanelTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPanel
|
||||
fields = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count')
|
||||
default_columns = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count')
|
||||
|
||||
|
||||
#
|
||||
@@ -1081,7 +1273,22 @@ class PowerFeedTable(BaseTable):
|
||||
type = tables.TemplateColumn(
|
||||
template_code=TYPE_LABEL
|
||||
)
|
||||
max_utilization = tables.TemplateColumn(
|
||||
template_code="{{ value }}%"
|
||||
)
|
||||
available_power = tables.Column(
|
||||
verbose_name='Available power (VA)'
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='dcim:powerfeed_list'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerFeed
|
||||
fields = ('pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase')
|
||||
fields = (
|
||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
||||
'max_utilization', 'available_power', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ from netaddr import IPNetwork
|
||||
from rest_framework import status
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from dcim.api import serializers
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
@@ -15,7 +14,7 @@ from dcim.models import (
|
||||
)
|
||||
from ipam.models import IPAddress, VLAN
|
||||
from extras.models import Graph
|
||||
from utilities.testing import APITestCase, choices_to_dict
|
||||
from utilities.testing import APITestCase
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
|
||||
|
||||
@@ -28,79 +27,6 @@ class AppTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_choices(self):
|
||||
|
||||
url = reverse('dcim-api:field-choice-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Cable
|
||||
self.assertEqual(choices_to_dict(response.data.get('cable:length_unit')), CableLengthUnitChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('cable:status')), CableStatusChoices.as_dict())
|
||||
content_types = ContentType.objects.filter(CABLE_TERMINATION_MODELS)
|
||||
cable_termination_choices = {
|
||||
"{}.{}".format(ct.app_label, ct.model): ct.name for ct in content_types
|
||||
}
|
||||
self.assertEqual(choices_to_dict(response.data.get('cable:termination_a_type')), cable_termination_choices)
|
||||
self.assertEqual(choices_to_dict(response.data.get('cable:termination_b_type')), cable_termination_choices)
|
||||
self.assertEqual(choices_to_dict(response.data.get('cable:type')), CableTypeChoices.as_dict())
|
||||
|
||||
# Console ports
|
||||
self.assertEqual(choices_to_dict(response.data.get('console-port:type')), ConsolePortTypeChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('console-port:connection_status')), dict(CONNECTION_STATUS_CHOICES))
|
||||
self.assertEqual(choices_to_dict(response.data.get('console-port-template:type')), ConsolePortTypeChoices.as_dict())
|
||||
|
||||
# Console server ports
|
||||
self.assertEqual(choices_to_dict(response.data.get('console-server-port:type')), ConsolePortTypeChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('console-server-port-template:type')), ConsolePortTypeChoices.as_dict())
|
||||
|
||||
# Device
|
||||
self.assertEqual(choices_to_dict(response.data.get('device:face')), DeviceFaceChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('device:status')), DeviceStatusChoices.as_dict())
|
||||
|
||||
# Device type
|
||||
self.assertEqual(choices_to_dict(response.data.get('device-type:subdevice_role')), SubdeviceRoleChoices.as_dict())
|
||||
|
||||
# Front ports
|
||||
self.assertEqual(choices_to_dict(response.data.get('front-port:type')), PortTypeChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('front-port-template:type')), PortTypeChoices.as_dict())
|
||||
|
||||
# Interfaces
|
||||
self.assertEqual(choices_to_dict(response.data.get('interface:type')), InterfaceTypeChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('interface:mode')), InterfaceModeChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('interface-template:type')), InterfaceTypeChoices.as_dict())
|
||||
|
||||
# Power feed
|
||||
self.assertEqual(choices_to_dict(response.data.get('power-feed:phase')), PowerFeedPhaseChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('power-feed:status')), PowerFeedStatusChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('power-feed:supply')), PowerFeedSupplyChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('power-feed:type')), PowerFeedTypeChoices.as_dict())
|
||||
|
||||
# Power outlets
|
||||
self.assertEqual(choices_to_dict(response.data.get('power-outlet:type')), PowerOutletTypeChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('power-outlet:feed_leg')), PowerOutletFeedLegChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('power-outlet-template:type')), PowerOutletTypeChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('power-outlet-template:feed_leg')), PowerOutletFeedLegChoices.as_dict())
|
||||
|
||||
# Power ports
|
||||
self.assertEqual(choices_to_dict(response.data.get('power-port:type')), PowerPortTypeChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('power-port:connection_status')), dict(CONNECTION_STATUS_CHOICES))
|
||||
self.assertEqual(choices_to_dict(response.data.get('power-port-template:type')), PowerPortTypeChoices.as_dict())
|
||||
|
||||
# Rack
|
||||
self.assertEqual(choices_to_dict(response.data.get('rack:type')), RackTypeChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('rack:width')), RackWidthChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('rack:status')), RackStatusChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('rack:outer_unit')), RackDimensionUnitChoices.as_dict())
|
||||
|
||||
# Rear ports
|
||||
self.assertEqual(choices_to_dict(response.data.get('rear-port:type')), PortTypeChoices.as_dict())
|
||||
self.assertEqual(choices_to_dict(response.data.get('rear-port-template:type')), PortTypeChoices.as_dict())
|
||||
|
||||
# Site
|
||||
self.assertEqual(choices_to_dict(response.data.get('site:status')), SiteStatusChoices.as_dict())
|
||||
|
||||
|
||||
class RegionTest(APITestCase):
|
||||
|
||||
@@ -350,9 +276,11 @@ class RackGroupTest(APITestCase):
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
||||
self.rackgroup1 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 1', slug='test-rack-group-1')
|
||||
self.rackgroup2 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 2', slug='test-rack-group-2')
|
||||
self.rackgroup3 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 3', slug='test-rack-group-3')
|
||||
self.parent_rackgroup1 = RackGroup.objects.create(site=self.site1, name='Parent Rack Group 1', slug='parent-rack-group-1')
|
||||
self.parent_rackgroup2 = RackGroup.objects.create(site=self.site2, name='Parent Rack Group 2', slug='parent-rack-group-2')
|
||||
self.rackgroup1 = RackGroup.objects.create(site=self.site1, name='Rack Group 1', slug='rack-group-1', parent=self.parent_rackgroup1)
|
||||
self.rackgroup2 = RackGroup.objects.create(site=self.site1, name='Rack Group 2', slug='rack-group-2', parent=self.parent_rackgroup1)
|
||||
self.rackgroup3 = RackGroup.objects.create(site=self.site1, name='Rack Group 3', slug='rack-group-3', parent=self.parent_rackgroup1)
|
||||
|
||||
def test_get_rackgroup(self):
|
||||
|
||||
@@ -366,7 +294,7 @@ class RackGroupTest(APITestCase):
|
||||
url = reverse('dcim-api:rackgroup-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
self.assertEqual(response.data['count'], 5)
|
||||
|
||||
def test_list_rackgroups_brief(self):
|
||||
|
||||
@@ -381,20 +309,22 @@ class RackGroupTest(APITestCase):
|
||||
def test_create_rackgroup(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Rack Group 4',
|
||||
'slug': 'test-rack-group-4',
|
||||
'name': 'Rack Group 4',
|
||||
'slug': 'rack-group-4',
|
||||
'site': self.site1.pk,
|
||||
'parent': self.parent_rackgroup1.pk,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:rackgroup-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(RackGroup.objects.count(), 4)
|
||||
self.assertEqual(RackGroup.objects.count(), 6)
|
||||
rackgroup4 = RackGroup.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(rackgroup4.name, data['name'])
|
||||
self.assertEqual(rackgroup4.slug, data['slug'])
|
||||
self.assertEqual(rackgroup4.site_id, data['site'])
|
||||
self.assertEqual(rackgroup4.parent_id, data['parent'])
|
||||
|
||||
def test_create_rackgroup_bulk(self):
|
||||
|
||||
@@ -403,16 +333,19 @@ class RackGroupTest(APITestCase):
|
||||
'name': 'Test Rack Group 4',
|
||||
'slug': 'test-rack-group-4',
|
||||
'site': self.site1.pk,
|
||||
'parent': self.parent_rackgroup1.pk,
|
||||
},
|
||||
{
|
||||
'name': 'Test Rack Group 5',
|
||||
'slug': 'test-rack-group-5',
|
||||
'site': self.site1.pk,
|
||||
'parent': self.parent_rackgroup1.pk,
|
||||
},
|
||||
{
|
||||
'name': 'Test Rack Group 6',
|
||||
'slug': 'test-rack-group-6',
|
||||
'site': self.site1.pk,
|
||||
'parent': self.parent_rackgroup1.pk,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -420,7 +353,7 @@ class RackGroupTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(RackGroup.objects.count(), 6)
|
||||
self.assertEqual(RackGroup.objects.count(), 8)
|
||||
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'])
|
||||
@@ -431,17 +364,19 @@ class RackGroupTest(APITestCase):
|
||||
'name': 'Test Rack Group X',
|
||||
'slug': 'test-rack-group-x',
|
||||
'site': self.site2.pk,
|
||||
'parent': self.parent_rackgroup2.pk,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:rackgroup-detail', kwargs={'pk': self.rackgroup1.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(RackGroup.objects.count(), 3)
|
||||
self.assertEqual(RackGroup.objects.count(), 5)
|
||||
rackgroup1 = RackGroup.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(rackgroup1.name, data['name'])
|
||||
self.assertEqual(rackgroup1.slug, data['slug'])
|
||||
self.assertEqual(rackgroup1.site_id, data['site'])
|
||||
self.assertEqual(rackgroup1.parent_id, data['parent'])
|
||||
|
||||
def test_delete_rackgroup(self):
|
||||
|
||||
@@ -449,7 +384,7 @@ class RackGroupTest(APITestCase):
|
||||
response = self.client.delete(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(RackGroup.objects.count(), 2)
|
||||
self.assertEqual(RackGroup.objects.count(), 4)
|
||||
|
||||
|
||||
class RackRoleTest(APITestCase):
|
||||
@@ -589,13 +524,6 @@ class RackTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['name'], self.rack1.name)
|
||||
|
||||
def test_get_rack_units(self):
|
||||
|
||||
url = reverse('dcim-api:rack-units', kwargs={'pk': self.rack1.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 42)
|
||||
|
||||
def test_get_elevation_rack_units(self):
|
||||
|
||||
url = '{}?q=3'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk}))
|
||||
@@ -654,6 +582,7 @@ class RackTest(APITestCase):
|
||||
|
||||
data = {
|
||||
'name': 'Test Rack 4',
|
||||
'facility_id': '1234',
|
||||
'site': self.site1.pk,
|
||||
'group': self.rackgroup1.pk,
|
||||
'role': self.rackrole1.pk,
|
||||
@@ -1887,6 +1816,7 @@ class DeviceTest(APITestCase):
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
||||
self.rack1 = Rack.objects.create(name='Test Rack 1', site=self.site1, u_height=48)
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.devicetype1 = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
|
||||
@@ -1992,6 +1922,9 @@ class DeviceTest(APITestCase):
|
||||
'device_role': self.devicerole1.pk,
|
||||
'name': 'Test Device 4',
|
||||
'site': self.site1.pk,
|
||||
'rack': self.rack1.pk,
|
||||
'face': DeviceFaceChoices.FACE_FRONT,
|
||||
'position': 1,
|
||||
'cluster': self.cluster1.pk,
|
||||
}
|
||||
|
||||
@@ -2089,6 +2022,20 @@ class DeviceTest(APITestCase):
|
||||
|
||||
self.assertFalse('config_context' in response.data['results'][0])
|
||||
|
||||
def test_unique_name_per_site_constraint(self):
|
||||
|
||||
data = {
|
||||
'device_type': self.devicetype1.pk,
|
||||
'device_role': self.devicerole1.pk,
|
||||
'name': 'Test Device 1',
|
||||
'site': self.site1.pk,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:device-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class ConsolePortTest(APITestCase):
|
||||
|
||||
|
||||
@@ -17,14 +17,15 @@ from virtualization.models import Cluster, ClusterType
|
||||
|
||||
class RegionTestCase(TestCase):
|
||||
queryset = Region.objects.all()
|
||||
filterset = RegionFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
regions = (
|
||||
Region(name='Region 1', slug='region-1'),
|
||||
Region(name='Region 2', slug='region-2'),
|
||||
Region(name='Region 3', slug='region-3'),
|
||||
Region(name='Region 1', slug='region-1', description='A'),
|
||||
Region(name='Region 2', slug='region-2', description='B'),
|
||||
Region(name='Region 3', slug='region-3', description='C'),
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
@@ -41,24 +42,27 @@ class RegionTestCase(TestCase):
|
||||
region.save()
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 2)
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Region 1', 'Region 2']}
|
||||
self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['region-1', 'region-2']}
|
||||
self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_parent(self):
|
||||
parent_regions = Region.objects.filter(parent__isnull=True)[:2]
|
||||
params = {'parent_id': [parent_regions[0].pk, parent_regions[1].pk]}
|
||||
self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'parent': [parent_regions[0].slug, parent_regions[1].slug]}
|
||||
self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class SiteTestCase(TestCase):
|
||||
@@ -81,7 +85,8 @@ class SiteTestCase(TestCase):
|
||||
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
||||
TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
|
||||
)
|
||||
TenantGroup.objects.bulk_create(tenant_groups)
|
||||
for tenantgroup in tenant_groups:
|
||||
tenantgroup.save()
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
|
||||
@@ -98,8 +103,7 @@ class SiteTestCase(TestCase):
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -138,11 +142,6 @@ class SiteTestCase(TestCase):
|
||||
params = {'contact_email': ['contact1@example.com', 'contact2@example.com']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [SiteStatusChoices.STATUS_ACTIVE, SiteStatusChoices.STATUS_PLANNED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -191,16 +190,24 @@ class RackGroupTestCase(TestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
|
||||
parent_rack_groups = (
|
||||
RackGroup(name='Parent Rack Group 1', slug='parent-rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Parent Rack Group 2', slug='parent-rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Parent Rack Group 3', slug='parent-rack-group-3', site=sites[2]),
|
||||
)
|
||||
RackGroup.objects.bulk_create(rack_groups)
|
||||
for rackgroup in parent_rack_groups:
|
||||
rackgroup.save()
|
||||
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0], parent=parent_rack_groups[0], description='A'),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1], parent=parent_rack_groups[1], description='B'),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2], parent=parent_rack_groups[2], description='C'),
|
||||
)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -211,18 +218,29 @@ class RackGroupTestCase(TestCase):
|
||||
params = {'slug': ['rack-group-1', 'rack-group-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_parent(self):
|
||||
parent_groups = RackGroup.objects.filter(name__startswith='Parent')[:2]
|
||||
params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
@@ -241,8 +259,7 @@ class RackRoleTestCase(TestCase):
|
||||
RackRole.objects.bulk_create(rack_roles)
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -285,7 +302,8 @@ class RackTestCase(TestCase):
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
|
||||
)
|
||||
RackGroup.objects.bulk_create(rack_groups)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
|
||||
rack_roles = (
|
||||
RackRole(name='Rack Role 1', slug='rack-role-1'),
|
||||
@@ -299,7 +317,8 @@ class RackTestCase(TestCase):
|
||||
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
||||
TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
|
||||
)
|
||||
TenantGroup.objects.bulk_create(tenant_groups)
|
||||
for tenantgroup in tenant_groups:
|
||||
tenantgroup.save()
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
|
||||
@@ -316,8 +335,7 @@ class RackTestCase(TestCase):
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -365,11 +383,6 @@ class RackTestCase(TestCase):
|
||||
params = {'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
@@ -442,7 +455,8 @@ class RackReservationTestCase(TestCase):
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
|
||||
)
|
||||
RackGroup.objects.bulk_create(rack_groups)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0], group=rack_groups[0]),
|
||||
@@ -463,7 +477,8 @@ class RackReservationTestCase(TestCase):
|
||||
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
||||
TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
|
||||
)
|
||||
TenantGroup.objects.bulk_create(tenant_groups)
|
||||
for tenantgroup in tenant_groups:
|
||||
tenantgroup.save()
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
|
||||
@@ -479,9 +494,8 @@ class RackReservationTestCase(TestCase):
|
||||
)
|
||||
RackReservation.objects.bulk_create(reservations)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
@@ -529,15 +543,14 @@ class ManufacturerTestCase(TestCase):
|
||||
def setUpTestData(cls):
|
||||
|
||||
manufacturers = (
|
||||
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
|
||||
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
|
||||
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
|
||||
Manufacturer(name='Manufacturer 1', slug='manufacturer-1', description='A'),
|
||||
Manufacturer(name='Manufacturer 2', slug='manufacturer-2', description='B'),
|
||||
Manufacturer(name='Manufacturer 3', slug='manufacturer-3', description='C'),
|
||||
)
|
||||
Manufacturer.objects.bulk_create(manufacturers)
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -548,6 +561,10 @@ class ManufacturerTestCase(TestCase):
|
||||
params = {'slug': ['manufacturer-1', 'manufacturer-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class DeviceTypeTestCase(TestCase):
|
||||
queryset = DeviceType.objects.all()
|
||||
@@ -605,6 +622,10 @@ class DeviceTypeTestCase(TestCase):
|
||||
DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2'),
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_model(self):
|
||||
params = {'model': ['Model 1', 'Model 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -631,11 +652,6 @@ class DeviceTypeTestCase(TestCase):
|
||||
params = {'subdevice_role': SubdeviceRoleChoices.ROLE_PARENT}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_manufacturer(self):
|
||||
manufacturers = Manufacturer.objects.all()[:2]
|
||||
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
|
||||
@@ -709,8 +725,7 @@ class ConsolePortTemplateTestCase(TestCase):
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -746,8 +761,7 @@ class ConsoleServerPortTemplateTestCase(TestCase):
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -783,8 +797,7 @@ class PowerPortTemplateTestCase(TestCase):
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -828,8 +841,7 @@ class PowerOutletTemplateTestCase(TestCase):
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -870,8 +882,7 @@ class InterfaceTemplateTestCase(TestCase):
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -925,8 +936,7 @@ class FrontPortTemplateTestCase(TestCase):
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -967,8 +977,7 @@ class RearPortTemplateTestCase(TestCase):
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -1013,8 +1022,7 @@ class DeviceBayTemplateTestCase(TestCase):
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -1042,8 +1050,7 @@ class DeviceRoleTestCase(TestCase):
|
||||
DeviceRole.objects.bulk_create(device_roles)
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -1080,15 +1087,14 @@ class PlatformTestCase(TestCase):
|
||||
Manufacturer.objects.bulk_create(manufacturers)
|
||||
|
||||
platforms = (
|
||||
Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], napalm_driver='driver-1'),
|
||||
Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], napalm_driver='driver-2'),
|
||||
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], napalm_driver='driver-3'),
|
||||
Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], napalm_driver='driver-1', description='A'),
|
||||
Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], napalm_driver='driver-2', description='B'),
|
||||
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], napalm_driver='driver-3', description='C'),
|
||||
)
|
||||
Platform.objects.bulk_create(platforms)
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -1099,6 +1105,10 @@ class PlatformTestCase(TestCase):
|
||||
params = {'slug': ['platform-1', 'platform-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_napalm_driver(self):
|
||||
params = {'napalm_driver': ['driver-1', 'driver-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1166,7 +1176,8 @@ class DeviceTestCase(TestCase):
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
|
||||
)
|
||||
RackGroup.objects.bulk_create(rack_groups)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0], group=rack_groups[0]),
|
||||
@@ -1188,7 +1199,8 @@ class DeviceTestCase(TestCase):
|
||||
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
||||
TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
|
||||
)
|
||||
TenantGroup.objects.bulk_create(tenant_groups)
|
||||
for tenantgroup in tenant_groups:
|
||||
tenantgroup.save()
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
|
||||
@@ -1242,8 +1254,8 @@ class DeviceTestCase(TestCase):
|
||||
|
||||
# Assign primary IPs for filtering
|
||||
ipaddresses = (
|
||||
IPAddress(family=4, address='192.0.2.1/24', interface=interfaces[0]),
|
||||
IPAddress(family=4, address='192.0.2.2/24', interface=interfaces[1]),
|
||||
IPAddress(address='192.0.2.1/24', interface=interfaces[0]),
|
||||
IPAddress(address='192.0.2.2/24', interface=interfaces[1]),
|
||||
)
|
||||
IPAddress.objects.bulk_create(ipaddresses)
|
||||
Device.objects.filter(pk=devices[0].pk).update(primary_ip4=ipaddresses[0])
|
||||
@@ -1255,8 +1267,7 @@ class DeviceTestCase(TestCase):
|
||||
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2)
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -1283,11 +1294,6 @@ class DeviceTestCase(TestCase):
|
||||
params = {'vc_priority': [1, 2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_manufacturer(self):
|
||||
manufacturers = Manufacturer.objects.all()[:2]
|
||||
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
|
||||
@@ -1497,8 +1503,7 @@ class ConsolePortTestCase(TestCase):
|
||||
# Third port is not connected
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -1593,8 +1598,7 @@ class ConsoleServerPortTestCase(TestCase):
|
||||
# Third port is not connected
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -1689,8 +1693,7 @@ class PowerPortTestCase(TestCase):
|
||||
# Third port is not connected
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -1793,8 +1796,7 @@ class PowerOutletTestCase(TestCase):
|
||||
# Third port is not connected
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -1891,9 +1893,8 @@ class InterfaceTestCase(TestCase):
|
||||
# Third pair is not connected
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:3]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Interface 1', 'Interface 2']}
|
||||
@@ -2028,8 +2029,7 @@ class FrontPortTestCase(TestCase):
|
||||
# Third port is not connected
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -2121,8 +2121,7 @@ class RearPortTestCase(TestCase):
|
||||
# Third port is not connected
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -2209,8 +2208,7 @@ class DeviceBayTestCase(TestCase):
|
||||
DeviceBay.objects.bulk_create(device_bays)
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -2297,8 +2295,7 @@ class InventoryItemTestCase(TestCase):
|
||||
InventoryItem.objects.bulk_create(child_inventory_items)
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
@@ -2409,8 +2406,7 @@ class VirtualChassisTestCase(TestCase):
|
||||
Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=virtual_chassis[2])
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_domain(self):
|
||||
@@ -2498,8 +2494,7 @@ class CableTestCase(TestCase):
|
||||
Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
||||
|
||||
def test_id(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id': [str(id) for id in id_list]}
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_label(self):
|
||||
@@ -2584,7 +2579,8 @@ class PowerPanelTestCase(TestCase):
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
|
||||
)
|
||||
RackGroup.objects.bulk_create(rack_groups)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
|
||||
power_panels = (
|
||||
PowerPanel(name='Power Panel 1', site=sites[0], rack_group=rack_groups[0]),
|
||||
@@ -2593,6 +2589,10 @@ class PowerPanelTestCase(TestCase):
|
||||
)
|
||||
PowerPanel.objects.bulk_create(power_panels)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Power Panel 1', 'Power Panel 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -2660,6 +2660,10 @@ class PowerFeedTestCase(TestCase):
|
||||
)
|
||||
PowerFeed.objects.bulk_create(power_feeds)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Power Feed 1', 'Power Feed 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
|
||||
from circuits.models import *
|
||||
from dcim.choices import *
|
||||
from dcim.models import *
|
||||
from tenancy.models import Tenant
|
||||
@@ -459,95 +460,494 @@ class CableTestCase(TestCase):
|
||||
|
||||
class CablePathTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetype = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
|
||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||
)
|
||||
devicerole = DeviceRole.objects.create(
|
||||
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
||||
)
|
||||
self.device1 = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site
|
||||
)
|
||||
self.device2 = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='Test Device 2', site=site
|
||||
)
|
||||
self.interface1 = Interface.objects.create(device=self.device1, name='eth0')
|
||||
self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
|
||||
self.panel1 = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='Test Panel 1', site=site
|
||||
)
|
||||
self.panel2 = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=site
|
||||
)
|
||||
self.rear_port1 = RearPort.objects.create(
|
||||
device=self.panel1, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C
|
||||
)
|
||||
self.front_port1 = FrontPort.objects.create(
|
||||
device=self.panel1, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port1
|
||||
)
|
||||
self.rear_port2 = RearPort.objects.create(
|
||||
device=self.panel2, name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C
|
||||
)
|
||||
self.front_port2 = FrontPort.objects.create(
|
||||
device=self.panel2, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port2
|
||||
name='Device Role 1', slug='device-role-1', color='ff0000'
|
||||
)
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
circuit = Circuit.objects.create(provider=provider, type=circuittype, cid='1')
|
||||
CircuitTermination.objects.bulk_create((
|
||||
CircuitTermination(circuit=circuit, site=site, term_side='A', port_speed=1000),
|
||||
CircuitTermination(circuit=circuit, site=site, term_side='Z', port_speed=1000),
|
||||
))
|
||||
|
||||
def test_path_completion(self):
|
||||
# Create four network devices with four interfaces each
|
||||
devices = (
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Device 1', site=site),
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Device 2', site=site),
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Device 3', site=site),
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Device 4', site=site),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
for device in devices:
|
||||
Interface.objects.bulk_create((
|
||||
Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=device, name='Interface 4', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
))
|
||||
|
||||
# First segment
|
||||
cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1)
|
||||
# Create four patch panels, each with one rear port and four front ports
|
||||
patch_panels = (
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Panel 1', site=site),
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Panel 2', site=site),
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Panel 3', site=site),
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Panel 4', site=site),
|
||||
)
|
||||
Device.objects.bulk_create(patch_panels)
|
||||
for patch_panel in patch_panels:
|
||||
rearport = RearPort.objects.create(device=patch_panel, name='Rear Port 1', positions=4, type=PortTypeChoices.TYPE_8P8C)
|
||||
FrontPort.objects.bulk_create((
|
||||
FrontPort(device=patch_panel, name='Front Port 1', rear_port=rearport, rear_port_position=1, type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPort(device=patch_panel, name='Front Port 2', rear_port=rearport, rear_port_position=2, type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPort(device=patch_panel, name='Front Port 3', rear_port=rearport, rear_port_position=3, type=PortTypeChoices.TYPE_8P8C),
|
||||
FrontPort(device=patch_panel, name='Front Port 4', rear_port=rearport, rear_port_position=4, type=PortTypeChoices.TYPE_8P8C),
|
||||
))
|
||||
|
||||
def test_direct_connection(self):
|
||||
"""
|
||||
Test a direct connection between two interfaces.
|
||||
|
||||
[Device 1] ----- [Device 2]
|
||||
Iface1 Iface1
|
||||
"""
|
||||
# Create cable
|
||||
cable = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
|
||||
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
|
||||
# Validate connections
|
||||
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
|
||||
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
|
||||
self.assertTrue(endpoint_a.connection_status)
|
||||
self.assertTrue(endpoint_b.connection_status)
|
||||
|
||||
# Delete cable
|
||||
cable.delete()
|
||||
|
||||
# Refresh endpoints
|
||||
endpoint_a.refresh_from_db()
|
||||
endpoint_b.refresh_from_db()
|
||||
|
||||
# Check that connections have been nullified
|
||||
self.assertIsNone(endpoint_a.connected_endpoint)
|
||||
self.assertIsNone(endpoint_b.connected_endpoint)
|
||||
self.assertIsNone(endpoint_a.connection_status)
|
||||
self.assertIsNone(endpoint_b.connection_status)
|
||||
|
||||
def test_connection_via_single_rear_port(self):
|
||||
"""
|
||||
Test a connection which passes through a single front/rear port pair.
|
||||
|
||||
1 2
|
||||
[Device 1] ----- [Panel 1] ----- [Device 2]
|
||||
Iface1 FP1 RP1 Iface1
|
||||
"""
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
)
|
||||
cable1.save()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertIsNone(interface1.connected_endpoint)
|
||||
self.assertIsNone(interface1.connection_status)
|
||||
|
||||
# Second segment
|
||||
cable2 = Cable(termination_a=self.rear_port1, termination_b=self.rear_port2)
|
||||
cable2 = Cable(
|
||||
termination_b=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable2.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
|
||||
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
|
||||
# Validate connections
|
||||
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
|
||||
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
|
||||
self.assertTrue(endpoint_a.connection_status)
|
||||
self.assertTrue(endpoint_b.connection_status)
|
||||
|
||||
# Delete cable 1
|
||||
cable1.delete()
|
||||
|
||||
# Refresh endpoints
|
||||
endpoint_a.refresh_from_db()
|
||||
endpoint_b.refresh_from_db()
|
||||
|
||||
# Check that connections have been nullified
|
||||
self.assertIsNone(endpoint_a.connected_endpoint)
|
||||
self.assertIsNone(endpoint_b.connected_endpoint)
|
||||
self.assertIsNone(endpoint_a.connection_status)
|
||||
self.assertIsNone(endpoint_b.connection_status)
|
||||
|
||||
def test_connections_via_patch(self):
|
||||
"""
|
||||
Test two connections via patched rear ports:
|
||||
Device 1 <---> Device 2
|
||||
Device 3 <---> Device 4
|
||||
|
||||
1 2
|
||||
[Device 1] -----------+ +----------- [Device 2]
|
||||
Iface1 | | Iface1
|
||||
FP1 | 3 | FP1
|
||||
[Panel 1] ----- [Panel 2]
|
||||
FP2 | RP1 RP1 | FP2
|
||||
Iface1 | | Iface1
|
||||
[Device 3] -----------+ +----------- [Device 4]
|
||||
4 5
|
||||
"""
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
)
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1')
|
||||
)
|
||||
cable2.save()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertIsNone(interface1.connected_endpoint)
|
||||
self.assertIsNone(interface1.connection_status)
|
||||
|
||||
# Third segment
|
||||
cable3 = Cable(
|
||||
termination_a=self.front_port2,
|
||||
termination_b=self.interface2,
|
||||
status=CableStatusChoices.STATUS_PLANNED
|
||||
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
|
||||
)
|
||||
cable3.save()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertEqual(interface1.connected_endpoint, self.interface2)
|
||||
self.assertFalse(interface1.connection_status)
|
||||
|
||||
# Switch third segment from planned to connected
|
||||
cable3.status = CableStatusChoices.STATUS_CONNECTED
|
||||
cable3.save()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertEqual(interface1.connected_endpoint, self.interface2)
|
||||
self.assertTrue(interface1.connection_status)
|
||||
cable4 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
|
||||
)
|
||||
cable4.save()
|
||||
cable5 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 4', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2')
|
||||
)
|
||||
cable5.save()
|
||||
|
||||
def test_path_teardown(self):
|
||||
# Retrieve endpoints
|
||||
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
|
||||
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1')
|
||||
endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1')
|
||||
|
||||
# Build the path
|
||||
cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1)
|
||||
# Validate connections
|
||||
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
|
||||
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
|
||||
self.assertEqual(endpoint_c.connected_endpoint, endpoint_d)
|
||||
self.assertEqual(endpoint_d.connected_endpoint, endpoint_c)
|
||||
self.assertTrue(endpoint_a.connection_status)
|
||||
self.assertTrue(endpoint_b.connection_status)
|
||||
self.assertTrue(endpoint_c.connection_status)
|
||||
self.assertTrue(endpoint_d.connection_status)
|
||||
|
||||
# Delete cable 3
|
||||
cable3.delete()
|
||||
|
||||
# Refresh endpoints
|
||||
endpoint_a.refresh_from_db()
|
||||
endpoint_b.refresh_from_db()
|
||||
endpoint_c.refresh_from_db()
|
||||
endpoint_d.refresh_from_db()
|
||||
|
||||
# Check that connections have been nullified
|
||||
self.assertIsNone(endpoint_a.connected_endpoint)
|
||||
self.assertIsNone(endpoint_b.connected_endpoint)
|
||||
self.assertIsNone(endpoint_c.connected_endpoint)
|
||||
self.assertIsNone(endpoint_d.connected_endpoint)
|
||||
self.assertIsNone(endpoint_a.connection_status)
|
||||
self.assertIsNone(endpoint_b.connection_status)
|
||||
self.assertIsNone(endpoint_c.connection_status)
|
||||
self.assertIsNone(endpoint_d.connection_status)
|
||||
|
||||
def test_connections_via_multiple_patches(self):
|
||||
"""
|
||||
Test two connections via patched rear ports:
|
||||
Device 1 <---> Device 2
|
||||
Device 3 <---> Device 4
|
||||
|
||||
1 2 3
|
||||
[Device 1] -----------+ +---------------+ +----------- [Device 2]
|
||||
Iface1 | | | | Iface1
|
||||
FP1 | 4 | FP1 FP1 | 5 | FP1
|
||||
[Panel 1] ----- [Panel 2] [Panel 3] ----- [Panel 4]
|
||||
FP2 | RP1 RP1 | FP2 FP2 | RP1 RP1 | FP2
|
||||
Iface1 | | | | Iface1
|
||||
[Device 3] -----------+ +---------------+ +----------- [Device 4]
|
||||
6 7 8
|
||||
"""
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
)
|
||||
cable1.save()
|
||||
cable2 = Cable(termination_a=self.rear_port1, termination_b=self.rear_port2)
|
||||
cable2 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1')
|
||||
)
|
||||
cable2.save()
|
||||
cable3 = Cable(termination_a=self.front_port2, termination_b=self.interface2)
|
||||
cable3 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable3.save()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertEqual(interface1.connected_endpoint, self.interface2)
|
||||
self.assertTrue(interface1.connection_status)
|
||||
|
||||
# Remove a cable
|
||||
cable2.delete()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertIsNone(interface1.connected_endpoint)
|
||||
self.assertIsNone(interface1.connection_status)
|
||||
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
||||
self.assertIsNone(interface2.connected_endpoint)
|
||||
self.assertIsNone(interface2.connection_status)
|
||||
cable4 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
|
||||
)
|
||||
cable4.save()
|
||||
cable5 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1')
|
||||
)
|
||||
cable5.save()
|
||||
|
||||
cable6 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
|
||||
)
|
||||
cable6.save()
|
||||
cable7 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 2')
|
||||
)
|
||||
cable7.save()
|
||||
cable8 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'),
|
||||
termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1')
|
||||
)
|
||||
cable8.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
|
||||
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1')
|
||||
endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1')
|
||||
|
||||
# Validate connections
|
||||
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
|
||||
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
|
||||
self.assertEqual(endpoint_c.connected_endpoint, endpoint_d)
|
||||
self.assertEqual(endpoint_d.connected_endpoint, endpoint_c)
|
||||
self.assertTrue(endpoint_a.connection_status)
|
||||
self.assertTrue(endpoint_b.connection_status)
|
||||
self.assertTrue(endpoint_c.connection_status)
|
||||
self.assertTrue(endpoint_d.connection_status)
|
||||
|
||||
# Delete cables 4 and 5
|
||||
cable4.delete()
|
||||
cable5.delete()
|
||||
|
||||
# Refresh endpoints
|
||||
endpoint_a.refresh_from_db()
|
||||
endpoint_b.refresh_from_db()
|
||||
endpoint_c.refresh_from_db()
|
||||
endpoint_d.refresh_from_db()
|
||||
|
||||
# Check that connections have been nullified
|
||||
self.assertIsNone(endpoint_a.connected_endpoint)
|
||||
self.assertIsNone(endpoint_b.connected_endpoint)
|
||||
self.assertIsNone(endpoint_c.connected_endpoint)
|
||||
self.assertIsNone(endpoint_d.connected_endpoint)
|
||||
self.assertIsNone(endpoint_a.connection_status)
|
||||
self.assertIsNone(endpoint_b.connection_status)
|
||||
self.assertIsNone(endpoint_c.connection_status)
|
||||
self.assertIsNone(endpoint_d.connection_status)
|
||||
|
||||
def test_connections_via_nested_rear_ports(self):
|
||||
"""
|
||||
Test two connections via nested rear ports:
|
||||
Device 1 <---> Device 2
|
||||
Device 3 <---> Device 4
|
||||
|
||||
1 2
|
||||
[Device 1] -----------+ +----------- [Device 2]
|
||||
Iface1 | | Iface1
|
||||
FP1 | 3 4 5 | FP1
|
||||
[Panel 1] ----- [Panel 2] ----- [Panel 3] ----- [Panel 4]
|
||||
FP2 | RP1 FP1 RP1 RP1 FP1 RP1 | FP2
|
||||
Iface1 | | Iface1
|
||||
[Device 3] -----------+ +----------- [Device 4]
|
||||
6 7
|
||||
"""
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
)
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable2.save()
|
||||
|
||||
cable3 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1')
|
||||
)
|
||||
cable3.save()
|
||||
cable4 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1')
|
||||
)
|
||||
cable4.save()
|
||||
cable5 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1')
|
||||
)
|
||||
cable5.save()
|
||||
|
||||
cable6 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
|
||||
)
|
||||
cable6.save()
|
||||
cable7 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'),
|
||||
termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1')
|
||||
)
|
||||
cable7.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
|
||||
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1')
|
||||
endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1')
|
||||
|
||||
# Validate connections
|
||||
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
|
||||
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
|
||||
self.assertEqual(endpoint_c.connected_endpoint, endpoint_d)
|
||||
self.assertEqual(endpoint_d.connected_endpoint, endpoint_c)
|
||||
self.assertTrue(endpoint_a.connection_status)
|
||||
self.assertTrue(endpoint_b.connection_status)
|
||||
self.assertTrue(endpoint_c.connection_status)
|
||||
self.assertTrue(endpoint_d.connection_status)
|
||||
|
||||
# Delete cable 4
|
||||
cable4.delete()
|
||||
|
||||
# Refresh endpoints
|
||||
endpoint_a.refresh_from_db()
|
||||
endpoint_b.refresh_from_db()
|
||||
endpoint_c.refresh_from_db()
|
||||
endpoint_d.refresh_from_db()
|
||||
|
||||
# Check that connections have been nullified
|
||||
self.assertIsNone(endpoint_a.connected_endpoint)
|
||||
self.assertIsNone(endpoint_b.connected_endpoint)
|
||||
self.assertIsNone(endpoint_c.connected_endpoint)
|
||||
self.assertIsNone(endpoint_d.connected_endpoint)
|
||||
self.assertIsNone(endpoint_a.connection_status)
|
||||
self.assertIsNone(endpoint_b.connection_status)
|
||||
self.assertIsNone(endpoint_c.connection_status)
|
||||
self.assertIsNone(endpoint_d.connection_status)
|
||||
|
||||
def test_connection_via_circuit(self):
|
||||
"""
|
||||
1 2
|
||||
[Device 1] ----- [Circuit] ----- [Device 2]
|
||||
Iface1 A Z Iface1
|
||||
|
||||
"""
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=CircuitTermination.objects.get(term_side='A')
|
||||
)
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_a=CircuitTermination.objects.get(term_side='Z'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable2.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
|
||||
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
|
||||
# Validate connections
|
||||
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
|
||||
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
|
||||
self.assertTrue(endpoint_a.connection_status)
|
||||
self.assertTrue(endpoint_b.connection_status)
|
||||
|
||||
# Delete circuit
|
||||
circuit = Circuit.objects.first().delete()
|
||||
|
||||
# Refresh endpoints
|
||||
endpoint_a.refresh_from_db()
|
||||
endpoint_b.refresh_from_db()
|
||||
|
||||
# Check that connections have been nullified
|
||||
self.assertIsNone(endpoint_a.connected_endpoint)
|
||||
self.assertIsNone(endpoint_b.connected_endpoint)
|
||||
self.assertIsNone(endpoint_a.connection_status)
|
||||
self.assertIsNone(endpoint_b.connection_status)
|
||||
|
||||
def test_connection_via_patched_circuit(self):
|
||||
"""
|
||||
1 2 3 4
|
||||
[Device 1] ----- [Panel 1] ----- [Circuit] ----- [Panel 2] ----- [Device 2]
|
||||
Iface1 FP1 RP1 A Z RP1 FP1 Iface1
|
||||
|
||||
"""
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
)
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_b=CircuitTermination.objects.get(term_side='A')
|
||||
)
|
||||
cable2.save()
|
||||
cable3 = Cable(
|
||||
termination_a=CircuitTermination.objects.get(term_side='Z'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
|
||||
)
|
||||
cable3.save()
|
||||
cable4 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable4.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
|
||||
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
|
||||
# Validate connections
|
||||
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
|
||||
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
|
||||
self.assertTrue(endpoint_a.connection_status)
|
||||
self.assertTrue(endpoint_b.connection_status)
|
||||
|
||||
# Delete circuit
|
||||
circuit = Circuit.objects.first().delete()
|
||||
|
||||
# Refresh endpoints
|
||||
endpoint_a.refresh_from_db()
|
||||
endpoint_b.refresh_from_db()
|
||||
|
||||
# Check that connections have been nullified
|
||||
self.assertIsNone(endpoint_a.connected_endpoint)
|
||||
self.assertIsNone(endpoint_b.connected_endpoint)
|
||||
self.assertIsNone(endpoint_a.connection_status)
|
||||
self.assertIsNone(endpoint_b.connection_status)
|
||||
|
||||
@@ -23,28 +23,34 @@ class NaturalOrderingTestCase(TestCase):
|
||||
|
||||
INTERFACES = [
|
||||
'0',
|
||||
'0.0',
|
||||
'0.1',
|
||||
'0.2',
|
||||
'0.10',
|
||||
'0.100',
|
||||
'0:1',
|
||||
'0:1.0',
|
||||
'0:1.1',
|
||||
'0:1.2',
|
||||
'0:1.10',
|
||||
'0:2',
|
||||
'0:2.0',
|
||||
'0:2.1',
|
||||
'0:2.2',
|
||||
'0:2.10',
|
||||
'1',
|
||||
'1.0',
|
||||
'1.1',
|
||||
'1.2',
|
||||
'1.10',
|
||||
'1.100',
|
||||
'1:1',
|
||||
'1:1.0',
|
||||
'1:1.1',
|
||||
'1:1.2',
|
||||
'1:1.10',
|
||||
'1:2',
|
||||
'1:2.0',
|
||||
'1:2.1',
|
||||
'1:2.2',
|
||||
'1:2.10',
|
||||
|
||||
@@ -46,13 +46,14 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
'name': 'Region X',
|
||||
'slug': 'region-x',
|
||||
'parent': regions[2].pk,
|
||||
'description': 'A new region',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug",
|
||||
"Region 4,region-4",
|
||||
"Region 5,region-5",
|
||||
"Region 6,region-6",
|
||||
"name,slug,description",
|
||||
"Region 4,region-4,Fourth region",
|
||||
"Region 5,region-5,Fifth region",
|
||||
"Region 6,region-6,Sixth region",
|
||||
)
|
||||
|
||||
|
||||
@@ -122,23 +123,26 @@ class RackGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
RackGroup.objects.bulk_create([
|
||||
rack_groups = (
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=site),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=site),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=site),
|
||||
])
|
||||
)
|
||||
for rackgroup in rack_groups:
|
||||
rackgroup.save()
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Rack Group X',
|
||||
'slug': 'rack-group-x',
|
||||
'site': site.pk,
|
||||
'description': 'A new rack group',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"site,name,slug",
|
||||
"Site 1,Rack Group 4,rack-group-4",
|
||||
"Site 1,Rack Group 5,rack-group-5",
|
||||
"Site 1,Rack Group 6,rack-group-6",
|
||||
"site,name,slug,description",
|
||||
"Site 1,Rack Group 4,rack-group-4,Fourth rack group",
|
||||
"Site 1,Rack Group 5,rack-group-5,Fifth rack group",
|
||||
"Site 1,Rack Group 6,rack-group-6,Sixth rack group",
|
||||
)
|
||||
|
||||
|
||||
@@ -172,10 +176,6 @@ class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = RackReservation
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_get_object = None
|
||||
test_create_object = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -184,7 +184,10 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
|
||||
rack = Rack(name='Rack 1', site=site)
|
||||
rack_group = RackGroup(name='Rack Group 1', slug='rack-group-1', site=site)
|
||||
rack_group.save()
|
||||
|
||||
rack = Rack(name='Rack 1', site=site, group=rack_group)
|
||||
rack.save()
|
||||
|
||||
RackReservation.objects.bulk_create([
|
||||
@@ -202,10 +205,10 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
'site,rack_name,units,description',
|
||||
'Site 1,Rack 1,"10,11,12",Reservation 1',
|
||||
'Site 1,Rack 1,"13,14,15",Reservation 2',
|
||||
'Site 1,Rack 1,"16,17,18",Reservation 3',
|
||||
'site,rack_group,rack,units,description',
|
||||
'Site 1,Rack Group 1,Rack 1,"10,11,12",Reservation 1',
|
||||
'Site 1,Rack Group 1,Rack 1,"13,14,15",Reservation 2',
|
||||
'Site 1,Rack Group 1,Rack 1,"16,17,18",Reservation 3',
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@@ -231,7 +234,8 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1])
|
||||
)
|
||||
RackGroup.objects.bulk_create(rackgroups)
|
||||
for rackgroup in rackgroups:
|
||||
rackgroup.save()
|
||||
|
||||
rackroles = (
|
||||
RackRole(name='Rack Role 1', slug='rack-role-1'),
|
||||
@@ -267,10 +271,10 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"site,name,width,u_height",
|
||||
"Site 1,Rack 4,19,42",
|
||||
"Site 1,Rack 5,19,42",
|
||||
"Site 1,Rack 6,19,42",
|
||||
"site,group,name,width,u_height",
|
||||
"Site 1,,Rack 4,19,42",
|
||||
"Site 1,Rack Group 1,Rack 5,19,42",
|
||||
"Site 2,Rack Group 2,Rack 6,19,42",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@@ -306,13 +310,14 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
cls.form_data = {
|
||||
'name': 'Manufacturer X',
|
||||
'slug': 'manufacturer-x',
|
||||
'description': 'A new manufacturer',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug",
|
||||
"Manufacturer 4,manufacturer-4",
|
||||
"Manufacturer 5,manufacturer-5",
|
||||
"Manufacturer 6,manufacturer-6",
|
||||
"name,slug,description",
|
||||
"Manufacturer 4,manufacturer-4,Fourth manufacturer",
|
||||
"Manufacturer 5,manufacturer-5,Fifth manufacturer",
|
||||
"Manufacturer 6,manufacturer-6,Sixth manufacturer",
|
||||
)
|
||||
|
||||
|
||||
@@ -361,6 +366,7 @@ manufacturer: Generic
|
||||
model: TEST-1000
|
||||
slug: test-1000
|
||||
u_height: 2
|
||||
comments: test comment
|
||||
console-ports:
|
||||
- name: Console Port 1
|
||||
type: de-9
|
||||
@@ -451,6 +457,7 @@ device-bays:
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
dt = DeviceType.objects.get(model='TEST-1000')
|
||||
self.assertEqual(dt.comments, 'test comment')
|
||||
|
||||
# Verify all of the components were created
|
||||
self.assertEqual(dt.consoleport_templates.count(), 3)
|
||||
@@ -865,13 +872,14 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
'manufacturer': manufacturer.pk,
|
||||
'napalm_driver': 'junos',
|
||||
'napalm_args': None,
|
||||
'description': 'A new platform',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug",
|
||||
"Platform 4,platform-4",
|
||||
"Platform 5,platform-5",
|
||||
"Platform 6,platform-6",
|
||||
"name,slug,description",
|
||||
"Platform 4,platform-4,Fourth platform",
|
||||
"Platform 5,platform-5,Fifth platform",
|
||||
"Platform 6,platform-6,Sixth platform",
|
||||
)
|
||||
|
||||
|
||||
@@ -887,8 +895,11 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
rack_group = RackGroup(site=sites[0], name='Rack Group 1', slug='rack-group-1')
|
||||
rack_group.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
Rack(name='Rack 1', site=sites[0], group=rack_group),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
@@ -944,10 +955,10 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"device_role,manufacturer,model_name,status,site,name",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 4",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 5",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 6",
|
||||
"device_role,manufacturer,device_type,status,name,site,rack_group,rack,position,face",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,Active,Device 4,Site 1,Rack Group 1,Rack 1,10,Front",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,Active,Device 5,Site 1,Rack Group 1,Rack 1,20,Front",
|
||||
"Device Role 1,Manufacturer 1,Device Type 1,Active,Device 6,Site 1,Rack Group 1,Rack 1,30,Front",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@@ -1333,37 +1344,37 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = DeviceBay
|
||||
|
||||
# Disable inapplicable views
|
||||
test_bulk_edit_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
device1 = create_test_device('Device 1')
|
||||
device2 = create_test_device('Device 2')
|
||||
device = create_test_device('Device 1')
|
||||
|
||||
# Update the DeviceType subdevice role to allow adding DeviceBays
|
||||
DeviceType.objects.update(subdevice_role=SubdeviceRoleChoices.ROLE_PARENT)
|
||||
|
||||
DeviceBay.objects.bulk_create([
|
||||
DeviceBay(device=device1, name='Device Bay 1'),
|
||||
DeviceBay(device=device1, name='Device Bay 2'),
|
||||
DeviceBay(device=device1, name='Device Bay 3'),
|
||||
DeviceBay(device=device, name='Device Bay 1'),
|
||||
DeviceBay(device=device, name='Device Bay 2'),
|
||||
DeviceBay(device=device, name='Device Bay 3'),
|
||||
])
|
||||
|
||||
cls.form_data = {
|
||||
'device': device2.pk,
|
||||
'device': device.pk,
|
||||
'name': 'Device Bay X',
|
||||
'description': 'A device bay',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device': device2.pk,
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Device Bay [4-6]',
|
||||
'description': 'A device bay',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"device,name",
|
||||
"Device 1,Device Bay 4",
|
||||
@@ -1504,10 +1515,7 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VirtualChassis
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_get_object = None
|
||||
test_import_objects = None
|
||||
test_bulk_edit_objects = None
|
||||
test_bulk_delete_objects = None
|
||||
|
||||
# TODO: Requires special form handling
|
||||
test_create_object = None
|
||||
@@ -1570,7 +1578,8 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
|
||||
)
|
||||
RackGroup.objects.bulk_create(rackgroups)
|
||||
for rackgroup in rackgroups:
|
||||
rackgroup.save()
|
||||
|
||||
PowerPanel.objects.bulk_create((
|
||||
PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 1'),
|
||||
@@ -1585,7 +1594,7 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"site,rack_group_name,name",
|
||||
"site,rack_group,name",
|
||||
"Site 1,Rack Group 1,Power Panel 4",
|
||||
"Site 1,Rack Group 1,Power Panel 5",
|
||||
"Site 1,Rack Group 1,Power Panel 6",
|
||||
@@ -1644,7 +1653,7 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"site,panel_name,name,voltage,amperage,max_utilization",
|
||||
"site,power_panel,name,voltage,amperage,max_utilization",
|
||||
"Site 1,Power Panel 1,Power Feed 4,120,20,80",
|
||||
"Site 1,Power Panel 1,Power Feed 5,120,20,80",
|
||||
"Site 1,Power Panel 1,Power Feed 6,120,20,80",
|
||||
|
||||
@@ -2,7 +2,6 @@ from django.urls import path
|
||||
|
||||
from extras.views import ObjectChangeLogView, ImageAttachmentEditView
|
||||
from ipam.views import ServiceCreateView
|
||||
from secrets.views import secret_add
|
||||
from . import views
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, FrontPort, Interface, Manufacturer, Platform,
|
||||
@@ -51,9 +50,11 @@ urlpatterns = [
|
||||
|
||||
# Rack reservations
|
||||
path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'),
|
||||
path('rack-reservations/add/', views.RackReservationCreateView.as_view(), name='rackreservation_add'),
|
||||
path('rack-reservations/import/', views.RackReservationImportView.as_view(), name='rackreservation_import'),
|
||||
path('rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
|
||||
path('rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
|
||||
path('rack-reservations/<int:pk>/', views.RackReservationView.as_view(), name='rackreservation'),
|
||||
path('rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||
path('rack-reservations/<int:pk>/delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||
path('rack-reservations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
|
||||
@@ -69,7 +70,6 @@ urlpatterns = [
|
||||
path('racks/<int:pk>/edit/', views.RackEditView.as_view(), name='rack_edit'),
|
||||
path('racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||
path('racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
|
||||
path('racks/<int:rack>/reservations/add/', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
|
||||
path('racks/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
|
||||
|
||||
# Manufacturers
|
||||
@@ -179,7 +179,6 @@ urlpatterns = [
|
||||
path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
|
||||
path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||
path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
|
||||
path('devices/<int:pk>/add-secret/', secret_add, name='device_addsecret'),
|
||||
path('devices/<int:device>/services/assign/', ServiceCreateView.as_view(), name='device_service_assign'),
|
||||
path('devices/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
||||
|
||||
@@ -279,13 +278,13 @@ urlpatterns = [
|
||||
path('rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
|
||||
path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
|
||||
path('rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
|
||||
# path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
|
||||
path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
|
||||
|
||||
# Device bays
|
||||
path('device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'),
|
||||
path('device-bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
|
||||
path('device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'),
|
||||
# TODO: Bulk edit view for DeviceBays
|
||||
path('device-bays/edit/', views.DeviceBayBulkEditView.as_view(), name='devicebay_bulk_edit'),
|
||||
path('device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
|
||||
path('device-bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
||||
path('device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
||||
@@ -322,6 +321,9 @@ urlpatterns = [
|
||||
# Virtual chassis
|
||||
path('virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
|
||||
path('virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
|
||||
path('virtual-chassis/edit/', views.VirtualChassisBulkEditView.as_view(), name='virtualchassis_bulk_edit'),
|
||||
path('virtual-chassis/delete/', views.VirtualChassisBulkDeleteView.as_view(), name='virtualchassis_bulk_delete'),
|
||||
path('virtual-chassis/<int:pk>/', views.VirtualChassisView.as_view(), name='virtualchassis'),
|
||||
path('virtual-chassis/<int:pk>/edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
|
||||
path('virtual-chassis/<int:pk>/delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
|
||||
path('virtual-chassis/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
|
||||
|
||||
@@ -266,7 +266,13 @@ class SiteBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
|
||||
class RackGroupListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'dcim.view_rackgroup'
|
||||
queryset = RackGroup.objects.prefetch_related('site').annotate(rack_count=Count('racks'))
|
||||
queryset = RackGroup.objects.add_related_count(
|
||||
RackGroup.objects.all(),
|
||||
Rack,
|
||||
'group',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site')
|
||||
filterset = filters.RackGroupFilterSet
|
||||
filterset_form = forms.RackGroupFilterForm
|
||||
table = tables.RackGroupTable
|
||||
@@ -473,20 +479,32 @@ class RackReservationListView(PermissionRequiredMixin, ObjectListView):
|
||||
action_buttons = ('export',)
|
||||
|
||||
|
||||
class RackReservationView(PermissionRequiredMixin, View):
|
||||
permission_required = 'dcim.view_rackreservation'
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
rackreservation = get_object_or_404(RackReservation.objects.prefetch_related('rack'), pk=pk)
|
||||
|
||||
return render(request, 'dcim/rackreservation.html', {
|
||||
'rackreservation': rackreservation,
|
||||
})
|
||||
|
||||
|
||||
class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.add_rackreservation'
|
||||
model = RackReservation
|
||||
model_form = forms.RackReservationForm
|
||||
template_name = 'dcim/rackreservation_edit.html'
|
||||
default_return_url = 'dcim:rackreservation_list'
|
||||
|
||||
def alter_obj(self, obj, request, args, kwargs):
|
||||
if not obj.pk:
|
||||
obj.rack = get_object_or_404(Rack, pk=kwargs['rack'])
|
||||
if 'rack' in request.GET:
|
||||
obj.rack = get_object_or_404(Rack, pk=request.GET.get('rack'))
|
||||
obj.user = request.user
|
||||
return obj
|
||||
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.rack.get_absolute_url()
|
||||
|
||||
|
||||
class RackReservationEditView(RackReservationCreateView):
|
||||
permission_required = 'dcim.change_rackreservation'
|
||||
@@ -495,9 +513,7 @@ class RackReservationEditView(RackReservationCreateView):
|
||||
class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_rackreservation'
|
||||
model = RackReservation
|
||||
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.rack.get_absolute_url()
|
||||
default_return_url = 'dcim:rackreservation_list'
|
||||
|
||||
|
||||
class RackReservationImportView(PermissionRequiredMixin, BulkImportView):
|
||||
@@ -1079,7 +1095,7 @@ class DeviceListView(PermissionRequiredMixin, ObjectListView):
|
||||
)
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset_form = forms.DeviceFilterForm
|
||||
table = tables.DeviceDetailTable
|
||||
table = tables.DeviceTable
|
||||
template_name = 'dcim/device_list.html'
|
||||
|
||||
|
||||
@@ -1089,7 +1105,7 @@ class DeviceView(PermissionRequiredMixin, View):
|
||||
def get(self, request, pk):
|
||||
|
||||
device = get_object_or_404(Device.objects.prefetch_related(
|
||||
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
|
||||
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform', 'primary_ip4', 'primary_ip6'
|
||||
), pk=pk)
|
||||
|
||||
# VirtualChassis members
|
||||
@@ -1883,6 +1899,14 @@ class DeviceBayBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
default_return_url = 'dcim:devicebay_list'
|
||||
|
||||
|
||||
class DeviceBayBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_devicebay'
|
||||
queryset = DeviceBay.objects.all()
|
||||
filterset = filters.DeviceBayFilterSet
|
||||
table = tables.DeviceBayTable
|
||||
form = forms.DeviceBayBulkEditForm
|
||||
|
||||
|
||||
class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
permission_required = 'dcim.change_devicebay'
|
||||
queryset = DeviceBay.objects.all()
|
||||
@@ -1905,7 +1929,7 @@ class DeviceBulkAddConsolePortView(PermissionRequiredMixin, BulkComponentCreateV
|
||||
permission_required = 'dcim.add_consoleport'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
form = forms.ConsolePortBulkCreateForm
|
||||
model = ConsolePort
|
||||
model_form = forms.ConsolePortForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
@@ -1917,7 +1941,7 @@ class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, BulkComponentC
|
||||
permission_required = 'dcim.add_consoleserverport'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
form = forms.ConsoleServerPortBulkCreateForm
|
||||
model = ConsoleServerPort
|
||||
model_form = forms.ConsoleServerPortForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
@@ -1929,7 +1953,7 @@ class DeviceBulkAddPowerPortView(PermissionRequiredMixin, BulkComponentCreateVie
|
||||
permission_required = 'dcim.add_powerport'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
form = forms.PowerPortBulkCreateForm
|
||||
model = PowerPort
|
||||
model_form = forms.PowerPortForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
@@ -1941,7 +1965,7 @@ class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, BulkComponentCreateV
|
||||
permission_required = 'dcim.add_poweroutlet'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
form = forms.PowerOutletBulkCreateForm
|
||||
model = PowerOutlet
|
||||
model_form = forms.PowerOutletForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
@@ -1953,7 +1977,7 @@ class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateVie
|
||||
permission_required = 'dcim.add_interface'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
form = forms.DeviceBulkAddInterfaceForm
|
||||
form = forms.InterfaceBulkCreateForm
|
||||
model = Interface
|
||||
model_form = forms.InterfaceForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
@@ -1961,11 +1985,35 @@ class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateVie
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
|
||||
# class DeviceBulkAddFrontPortView(PermissionRequiredMixin, BulkComponentCreateView):
|
||||
# permission_required = 'dcim.add_frontport'
|
||||
# parent_model = Device
|
||||
# parent_field = 'device'
|
||||
# form = forms.FrontPortBulkCreateForm
|
||||
# model = FrontPort
|
||||
# model_form = forms.FrontPortForm
|
||||
# filterset = filters.DeviceFilterSet
|
||||
# table = tables.DeviceTable
|
||||
# default_return_url = 'dcim:device_list'
|
||||
|
||||
|
||||
class DeviceBulkAddRearPortView(PermissionRequiredMixin, BulkComponentCreateView):
|
||||
permission_required = 'dcim.add_rearport'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
form = forms.RearPortBulkCreateForm
|
||||
model = RearPort
|
||||
model_form = forms.RearPortForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
|
||||
class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateView):
|
||||
permission_required = 'dcim.add_devicebay'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
form = forms.DeviceBayBulkCreateForm
|
||||
model = DeviceBay
|
||||
model_form = forms.DeviceBayForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
@@ -2009,12 +2057,15 @@ class CableTraceView(PermissionRequiredMixin, View):
|
||||
def get(self, request, model, pk):
|
||||
|
||||
obj = get_object_or_404(model, pk=pk)
|
||||
trace = obj.trace(follow_circuits=True)
|
||||
total_length = sum([entry[1]._abs_length for entry in trace if entry[1] and entry[1]._abs_length])
|
||||
path, split_ends = obj.trace()
|
||||
total_length = sum(
|
||||
[entry[1]._abs_length for entry in path if entry[1] and entry[1]._abs_length]
|
||||
)
|
||||
|
||||
return render(request, 'dcim/cable_trace.html', {
|
||||
'obj': obj,
|
||||
'trace': trace,
|
||||
'trace': path,
|
||||
'split_ends': split_ends,
|
||||
'total_length': total_length,
|
||||
})
|
||||
|
||||
@@ -2227,19 +2278,15 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
|
||||
csv_data = [
|
||||
# Headers
|
||||
','.join([
|
||||
'device_a', 'interface_a', 'interface_a_description',
|
||||
'device_b', 'interface_b', 'interface_b_description',
|
||||
'connection_status'
|
||||
'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status'
|
||||
])
|
||||
]
|
||||
for obj in self.queryset:
|
||||
csv = csv_format([
|
||||
obj.connected_endpoint.device.identifier if obj.connected_endpoint else None,
|
||||
obj.connected_endpoint.name if obj.connected_endpoint else None,
|
||||
obj.connected_endpoint.description if obj.connected_endpoint else None,
|
||||
obj.device.identifier,
|
||||
obj.name,
|
||||
obj.description,
|
||||
obj.get_connection_status_display(),
|
||||
])
|
||||
csv_data.append(csv)
|
||||
@@ -2316,6 +2363,17 @@ class VirtualChassisListView(PermissionRequiredMixin, ObjectListView):
|
||||
action_buttons = ('export',)
|
||||
|
||||
|
||||
class VirtualChassisView(PermissionRequiredMixin, View):
|
||||
permission_required = 'dcim.view_virtualchassis'
|
||||
|
||||
def get(self, request, pk):
|
||||
virtualchassis = get_object_or_404(VirtualChassis.objects.prefetch_related('members'), pk=pk)
|
||||
|
||||
return render(request, 'dcim/virtualchassis.html', {
|
||||
'virtualchassis': virtualchassis,
|
||||
})
|
||||
|
||||
|
||||
class VirtualChassisCreateView(PermissionRequiredMixin, View):
|
||||
permission_required = 'dcim.add_virtualchassis'
|
||||
|
||||
@@ -2543,6 +2601,23 @@ class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin,
|
||||
})
|
||||
|
||||
|
||||
class VirtualChassisBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_virtualchassis'
|
||||
queryset = VirtualChassis.objects.all()
|
||||
filterset = filters.VirtualChassisFilterSet
|
||||
table = tables.VirtualChassisTable
|
||||
form = forms.VirtualChassisBulkEditForm
|
||||
default_return_url = 'dcim:virtualchassis_list'
|
||||
|
||||
|
||||
class VirtualChassisBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_virtualchassis'
|
||||
queryset = VirtualChassis.objects.all()
|
||||
filterset = filters.VirtualChassisFilterSet
|
||||
table = tables.VirtualChassisTable
|
||||
default_return_url = 'dcim:virtualchassis_list'
|
||||
|
||||
|
||||
#
|
||||
# Power panels
|
||||
#
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
|
||||
from netbox.admin import admin_site
|
||||
from utilities.forms import LaxURLField
|
||||
from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, Webhook
|
||||
from .reports import get_report
|
||||
@@ -35,7 +34,7 @@ class WebhookForm(forms.ModelForm):
|
||||
order_content_types(self.fields['obj_type'])
|
||||
|
||||
|
||||
@admin.register(Webhook, site=admin_site)
|
||||
@admin.register(Webhook)
|
||||
class WebhookAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update', 'type_delete',
|
||||
@@ -93,7 +92,7 @@ class CustomFieldChoiceAdmin(admin.TabularInline):
|
||||
extra = 5
|
||||
|
||||
|
||||
@admin.register(CustomField, site=admin_site)
|
||||
@admin.register(CustomField)
|
||||
class CustomFieldAdmin(admin.ModelAdmin):
|
||||
inlines = [CustomFieldChoiceAdmin]
|
||||
list_display = [
|
||||
@@ -135,7 +134,7 @@ class CustomLinkForm(forms.ModelForm):
|
||||
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
||||
|
||||
|
||||
@admin.register(CustomLink, site=admin_site)
|
||||
@admin.register(CustomLink)
|
||||
class CustomLinkAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'name', 'content_type', 'group_name', 'weight',
|
||||
@@ -150,7 +149,7 @@ class CustomLinkAdmin(admin.ModelAdmin):
|
||||
# Graphs
|
||||
#
|
||||
|
||||
@admin.register(Graph, site=admin_site)
|
||||
@admin.register(Graph)
|
||||
class GraphAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'name', 'type', 'weight', 'template_language', 'source',
|
||||
@@ -178,7 +177,7 @@ class ExportTemplateForm(forms.ModelForm):
|
||||
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
||||
|
||||
|
||||
@admin.register(ExportTemplate, site=admin_site)
|
||||
@admin.register(ExportTemplate)
|
||||
class ExportTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'name', 'content_type', 'description', 'mime_type', 'file_extension',
|
||||
@@ -193,7 +192,7 @@ class ExportTemplateAdmin(admin.ModelAdmin):
|
||||
# Reports
|
||||
#
|
||||
|
||||
@admin.register(ReportResult, site=admin_site)
|
||||
@admin.register(ReportResult)
|
||||
class ReportResultAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'report', 'active', 'created', 'user', 'passing',
|
||||
|
||||
@@ -20,7 +20,10 @@ class CustomFieldDefaultValues:
|
||||
"""
|
||||
Return a dictionary of all CustomFields assigned to the parent model and their default values.
|
||||
"""
|
||||
def __call__(self):
|
||||
requires_context = True
|
||||
|
||||
def __call__(self, serializer_field):
|
||||
self.model = serializer_field.parent.Meta.model
|
||||
|
||||
# Retrieve the CustomFields for the parent model
|
||||
content_type = ContentType.objects.get_for_model(self.model)
|
||||
@@ -49,9 +52,6 @@ class CustomFieldDefaultValues:
|
||||
|
||||
return value
|
||||
|
||||
def set_context(self, serializer_field):
|
||||
self.model = serializer_field.parent.Meta.model
|
||||
|
||||
|
||||
class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from extras.constants import *
|
||||
from extras.models import (
|
||||
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
|
||||
)
|
||||
from extras.utils import FeatureQuery
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
@@ -31,7 +32,7 @@ from .nested_serializers import *
|
||||
|
||||
class GraphSerializer(ValidatedModelSerializer):
|
||||
type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(GRAPH_MODELS),
|
||||
queryset=ContentType.objects.filter(FeatureQuery('graphs').get_query()),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -67,7 +68,7 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
|
||||
|
||||
class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||
content_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(EXPORTTEMPLATE_MODELS),
|
||||
queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
|
||||
)
|
||||
template_language = ChoiceField(
|
||||
choices=TemplateLanguageChoices,
|
||||
@@ -91,7 +92,7 @@ class TagSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ['id', 'name', 'slug', 'color', 'comments', 'tagged_items']
|
||||
fields = ['id', 'name', 'slug', 'color', 'description', 'tagged_items']
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -14,9 +14,6 @@ class ExtrasRootView(routers.APIRootView):
|
||||
router = routers.DefaultRouter()
|
||||
router.APIRootView = ExtrasRootView
|
||||
|
||||
# Field choices
|
||||
router.register('_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
|
||||
|
||||
# Custom field choices
|
||||
router.register('_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice')
|
||||
|
||||
|
||||
@@ -15,22 +15,10 @@ from extras.models import (
|
||||
)
|
||||
from extras.reports import get_report, get_reports
|
||||
from extras.scripts import get_script, get_scripts, run_script
|
||||
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
|
||||
from utilities.api import IsAuthenticatedOrLoginNotRequired, ModelViewSet
|
||||
from . import serializers
|
||||
|
||||
|
||||
#
|
||||
# Field choices
|
||||
#
|
||||
|
||||
class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(serializers.ExportTemplateSerializer, ['template_language']),
|
||||
(serializers.GraphSerializer, ['type', 'template_language']),
|
||||
(serializers.ObjectChangeSerializer, ['action']),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Custom field choices
|
||||
#
|
||||
|
||||
@@ -1,131 +1,3 @@
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
# Models which support custom fields
|
||||
CUSTOMFIELD_MODELS = Q(
|
||||
Q(app_label='circuits', model__in=[
|
||||
'circuit',
|
||||
'provider',
|
||||
]) |
|
||||
Q(app_label='dcim', model__in=[
|
||||
'device',
|
||||
'devicetype',
|
||||
'powerfeed',
|
||||
'rack',
|
||||
'region',
|
||||
'site',
|
||||
]) |
|
||||
Q(app_label='ipam', model__in=[
|
||||
'aggregate',
|
||||
'ipaddress',
|
||||
'prefix',
|
||||
'service',
|
||||
'vlan',
|
||||
'vrf',
|
||||
]) |
|
||||
Q(app_label='secrets', model__in=[
|
||||
'secret',
|
||||
]) |
|
||||
Q(app_label='tenancy', model__in=[
|
||||
'tenant',
|
||||
]) |
|
||||
Q(app_label='virtualization', model__in=[
|
||||
'cluster',
|
||||
'virtualmachine',
|
||||
])
|
||||
)
|
||||
|
||||
# Custom links
|
||||
CUSTOMLINK_MODELS = Q(
|
||||
Q(app_label='circuits', model__in=[
|
||||
'circuit',
|
||||
'provider',
|
||||
]) |
|
||||
Q(app_label='dcim', model__in=[
|
||||
'cable',
|
||||
'device',
|
||||
'devicetype',
|
||||
'powerpanel',
|
||||
'powerfeed',
|
||||
'rack',
|
||||
'region',
|
||||
'site',
|
||||
]) |
|
||||
Q(app_label='ipam', model__in=[
|
||||
'aggregate',
|
||||
'ipaddress',
|
||||
'prefix',
|
||||
'service',
|
||||
'vlan',
|
||||
'vrf',
|
||||
]) |
|
||||
Q(app_label='secrets', model__in=[
|
||||
'secret',
|
||||
]) |
|
||||
Q(app_label='tenancy', model__in=[
|
||||
'tenant',
|
||||
]) |
|
||||
Q(app_label='virtualization', model__in=[
|
||||
'cluster',
|
||||
'virtualmachine',
|
||||
])
|
||||
)
|
||||
|
||||
# Models which can have Graphs associated with them
|
||||
GRAPH_MODELS = Q(
|
||||
Q(app_label='circuits', model__in=[
|
||||
'provider',
|
||||
]) |
|
||||
Q(app_label='dcim', model__in=[
|
||||
'device',
|
||||
'interface',
|
||||
'site',
|
||||
])
|
||||
)
|
||||
|
||||
# Models which support export templates
|
||||
EXPORTTEMPLATE_MODELS = Q(
|
||||
Q(app_label='circuits', model__in=[
|
||||
'circuit',
|
||||
'provider',
|
||||
]) |
|
||||
Q(app_label='dcim', model__in=[
|
||||
'cable',
|
||||
'consoleport',
|
||||
'device',
|
||||
'devicetype',
|
||||
'interface',
|
||||
'inventoryitem',
|
||||
'manufacturer',
|
||||
'powerpanel',
|
||||
'powerport',
|
||||
'powerfeed',
|
||||
'rack',
|
||||
'rackgroup',
|
||||
'region',
|
||||
'site',
|
||||
'virtualchassis',
|
||||
]) |
|
||||
Q(app_label='ipam', model__in=[
|
||||
'aggregate',
|
||||
'ipaddress',
|
||||
'prefix',
|
||||
'service',
|
||||
'vlan',
|
||||
'vrf',
|
||||
]) |
|
||||
Q(app_label='secrets', model__in=[
|
||||
'secret',
|
||||
]) |
|
||||
Q(app_label='tenancy', model__in=[
|
||||
'tenant',
|
||||
]) |
|
||||
Q(app_label='virtualization', model__in=[
|
||||
'cluster',
|
||||
'virtualmachine',
|
||||
])
|
||||
)
|
||||
|
||||
# Report logging levels
|
||||
LOG_DEFAULT = 0
|
||||
LOG_SUCCESS = 10
|
||||
@@ -140,51 +12,14 @@ LOG_LEVEL_CODES = {
|
||||
LOG_FAILURE: 'failure',
|
||||
}
|
||||
|
||||
# Webhook content types
|
||||
HTTP_CONTENT_TYPE_JSON = 'application/json'
|
||||
|
||||
# Models which support registered webhooks
|
||||
WEBHOOK_MODELS = Q(
|
||||
Q(app_label='circuits', model__in=[
|
||||
'circuit',
|
||||
'provider',
|
||||
]) |
|
||||
Q(app_label='dcim', model__in=[
|
||||
'cable',
|
||||
'consoleport',
|
||||
'consoleserverport',
|
||||
'device',
|
||||
'devicebay',
|
||||
'devicetype',
|
||||
'frontport',
|
||||
'interface',
|
||||
'inventoryitem',
|
||||
'manufacturer',
|
||||
'poweroutlet',
|
||||
'powerpanel',
|
||||
'powerport',
|
||||
'powerfeed',
|
||||
'rack',
|
||||
'rearport',
|
||||
'region',
|
||||
'site',
|
||||
'virtualchassis',
|
||||
]) |
|
||||
Q(app_label='ipam', model__in=[
|
||||
'aggregate',
|
||||
'ipaddress',
|
||||
'prefix',
|
||||
'service',
|
||||
'vlan',
|
||||
'vrf',
|
||||
]) |
|
||||
Q(app_label='secrets', model__in=[
|
||||
'secret',
|
||||
]) |
|
||||
Q(app_label='tenancy', model__in=[
|
||||
'tenant',
|
||||
]) |
|
||||
Q(app_label='virtualization', model__in=[
|
||||
'cluster',
|
||||
'virtualmachine',
|
||||
])
|
||||
)
|
||||
# Registerable extras features
|
||||
EXTRAS_FEATURES = [
|
||||
'custom_fields',
|
||||
'custom_links',
|
||||
'graphs',
|
||||
'export_templates',
|
||||
'webhooks'
|
||||
]
|
||||
|
||||
@@ -94,14 +94,14 @@ class GraphFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Graph
|
||||
fields = ['type', 'name', 'template_language']
|
||||
fields = ['id', 'type', 'name', 'template_language']
|
||||
|
||||
|
||||
class ExportTemplateFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
fields = ['content_type', 'name', 'template_language']
|
||||
fields = ['id', 'content_type', 'name', 'template_language']
|
||||
|
||||
|
||||
class TagFilterSet(BaseFilterSet):
|
||||
@@ -112,7 +112,7 @@ class TagFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ['name', 'slug']
|
||||
fields = ['id', 'name', 'slug', 'color']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -219,7 +219,7 @@ class ConfigContextFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = ['name', 'is_active']
|
||||
fields = ['id', 'name', 'is_active']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -255,7 +255,8 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
fields = [
|
||||
'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id', 'object_repr',
|
||||
'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id',
|
||||
'object_repr',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
|
||||
@@ -2,13 +2,13 @@ from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from mptt.forms import TreeNodeMultipleChoiceField
|
||||
from taggit.forms import TagField
|
||||
from taggit.forms import TagField as TagField_
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
|
||||
CommentField, ContentTypeSelect, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField,
|
||||
ContentTypeSelect, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField,
|
||||
StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
@@ -89,7 +89,7 @@ class CustomFieldModelForm(forms.ModelForm):
|
||||
return obj
|
||||
|
||||
|
||||
class CustomFieldModelCSVForm(CustomFieldModelForm):
|
||||
class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm):
|
||||
|
||||
def _append_customfield_fields(self):
|
||||
|
||||
@@ -142,14 +142,22 @@ class CustomFieldFilterForm(forms.Form):
|
||||
# Tags
|
||||
#
|
||||
|
||||
class TagField(TagField_):
|
||||
|
||||
def widget_attrs(self, widget):
|
||||
# Apply the "tagfield" CSS class to trigger the special API-based selection widget for tags
|
||||
return {
|
||||
'class': 'tagfield'
|
||||
}
|
||||
|
||||
|
||||
class TagForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'comments'
|
||||
'name', 'slug', 'color', 'description'
|
||||
]
|
||||
|
||||
|
||||
@@ -181,9 +189,13 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
required=False,
|
||||
widget=ColorSelect()
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
#
|
||||
@@ -198,60 +210,35 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
)
|
||||
sites = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/"
|
||||
)
|
||||
required=False
|
||||
)
|
||||
roles = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/device-roles/"
|
||||
)
|
||||
required=False
|
||||
)
|
||||
platforms = DynamicModelMultipleChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/platforms/"
|
||||
)
|
||||
required=False
|
||||
)
|
||||
cluster_groups = DynamicModelMultipleChoiceField(
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/virtualization/cluster-groups/"
|
||||
)
|
||||
required=False
|
||||
)
|
||||
clusters = DynamicModelMultipleChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/virtualization/clusters/"
|
||||
)
|
||||
required=False
|
||||
)
|
||||
tenant_groups = DynamicModelMultipleChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenant-groups/"
|
||||
)
|
||||
required=False
|
||||
)
|
||||
tenants = DynamicModelMultipleChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/"
|
||||
)
|
||||
required=False
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/extras/tags/"
|
||||
)
|
||||
required=False
|
||||
)
|
||||
data = JSONField(
|
||||
label=''
|
||||
@@ -299,7 +286,6 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/regions/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
@@ -308,7 +294,6 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
@@ -317,7 +302,6 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/device-roles/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
@@ -326,7 +310,6 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/platforms/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
@@ -335,24 +318,19 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/virtualization/cluster-groups/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
cluster_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
required=False,
|
||||
label='Cluster',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/virtualization/clusters/",
|
||||
)
|
||||
label='Cluster'
|
||||
)
|
||||
tenant_group = DynamicModelMultipleChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenant-groups/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
@@ -361,7 +339,6 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
@@ -370,7 +347,6 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/extras/tags/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
@@ -456,18 +432,19 @@ class ScriptForm(BootstrapMixin, forms.Form):
|
||||
|
||||
def __init__(self, vars, *args, commit_default=True, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Dynamically populate fields for variables
|
||||
for name, var in vars.items():
|
||||
self.fields[name] = var.as_field()
|
||||
self.base_fields[name] = var.as_field()
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Toggle default commit behavior based on Meta option
|
||||
if not commit_default:
|
||||
self.fields['_commit'].initial = False
|
||||
|
||||
# Move _commit to the end of the form
|
||||
self.fields.move_to_end('_commit', True)
|
||||
commit = self.fields.pop('_commit')
|
||||
self.fields['_commit'] = commit
|
||||
|
||||
@property
|
||||
def requires_input(self):
|
||||
|
||||
16
netbox/extras/management/commands/rqworker.py
Normal file
16
netbox/extras/management/commands/rqworker.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.conf import settings
|
||||
from django_rq.management.commands.rqworker import Command as _Command
|
||||
|
||||
|
||||
class Command(_Command):
|
||||
"""
|
||||
Subclass django_rq's built-in rqworker to listen on all configured queues if none are specified (instead
|
||||
of only the 'default' queue).
|
||||
"""
|
||||
def handle(self, *args, **options):
|
||||
|
||||
# If no queues have been specified on the command line, listen on all configured queues.
|
||||
if len(args) < 1:
|
||||
args = settings.RQ_QUEUES
|
||||
|
||||
super().handle(*args, **options)
|
||||
85
netbox/extras/management/commands/webhook_receiver.py
Normal file
85
netbox/extras/management/commands/webhook_receiver.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import sys
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
request_counter = 1
|
||||
|
||||
|
||||
class WebhookHandler(BaseHTTPRequestHandler):
|
||||
show_headers = True
|
||||
|
||||
def __getattr__(self, item):
|
||||
|
||||
# Return the same method for any type of HTTP request (GET, POST, etc.)
|
||||
if item.startswith('do_'):
|
||||
return self.do_ANY
|
||||
|
||||
raise AttributeError
|
||||
|
||||
def log_message(self, format_str, *args):
|
||||
global request_counter
|
||||
|
||||
print("[{}] {} {} {}".format(
|
||||
request_counter,
|
||||
self.date_time_string(),
|
||||
self.address_string(),
|
||||
format_str % args
|
||||
))
|
||||
|
||||
def do_ANY(self):
|
||||
global request_counter
|
||||
|
||||
# Send a 200 response regardless of the request content
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b'Webhook received!\n')
|
||||
|
||||
request_counter += 1
|
||||
|
||||
# Print the request headers to stdout
|
||||
if self.show_headers:
|
||||
for k, v in self.headers.items():
|
||||
print('{}: {}'.format(k, v))
|
||||
print()
|
||||
|
||||
# Print the request body (if any)
|
||||
content_length = self.headers.get('Content-Length')
|
||||
if content_length is not None:
|
||||
body = self.rfile.read(int(content_length))
|
||||
print(body.decode('utf-8'))
|
||||
else:
|
||||
print('(No body)')
|
||||
|
||||
print('------------')
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Start a simple listener to display received HTTP requests"
|
||||
|
||||
default_port = 9000
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--port', type=int, default=self.default_port,
|
||||
help="Optional port number (default: {})".format(self.default_port)
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-headers", action='store_true', dest='no_headers',
|
||||
help="Hide HTTP request headers"
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
port = options['port']
|
||||
quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C'
|
||||
|
||||
WebhookHandler.show_headers = not options['no_headers']
|
||||
|
||||
self.stdout.write('Listening on port http://localhost:{}. Stop with {}.'.format(port, quit_command))
|
||||
httpd = HTTPServer(('localhost', port), WebhookHandler)
|
||||
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
self.stdout.write("\nExiting...")
|
||||
@@ -1,265 +0,0 @@
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import connection, migrations, models
|
||||
from django.db.utils import OperationalError
|
||||
|
||||
import extras.models
|
||||
|
||||
|
||||
def verify_postgresql_version(apps, schema_editor):
|
||||
"""
|
||||
Verify that PostgreSQL is version 9.4 or higher.
|
||||
"""
|
||||
# https://www.postgresql.org/docs/current/libpq-status.html#LIBPQ-PQSERVERVERSION
|
||||
DB_MINIMUM_VERSION = 90400 # 9.4.0
|
||||
|
||||
try:
|
||||
pg_version = connection.pg_version
|
||||
|
||||
if pg_version < DB_MINIMUM_VERSION:
|
||||
raise Exception("PostgreSQL 9.4.0 ({}) or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(DB_MINIMUM_VERSION, pg_version))
|
||||
|
||||
# Skip if the database is missing (e.g. for CI testing) or misconfigured.
|
||||
except OperationalError:
|
||||
pass
|
||||
|
||||
|
||||
def is_filterable_to_filter_logic(apps, schema_editor):
|
||||
CustomField = apps.get_model('extras', 'CustomField')
|
||||
CustomField.objects.filter(is_filterable=False).update(filter_logic=0)
|
||||
CustomField.objects.filter(is_filterable=True).update(filter_logic=1)
|
||||
# Select fields match on primary key only
|
||||
CustomField.objects.filter(is_filterable=True, type=600).update(filter_logic=2)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('extras', '0001_initial'), ('extras', '0002_custom_fields'), ('extras', '0003_exporttemplate_add_description'), ('extras', '0004_topologymap_change_comma_to_semicolon'), ('extras', '0005_useraction_add_bulk_create'), ('extras', '0006_add_imageattachments'), ('extras', '0007_unicode_literals'), ('extras', '0008_reports'), ('extras', '0009_topologymap_type'), ('extras', '0010_customfield_filter_logic'), ('extras', '0011_django2'), ('extras', '0012_webhooks'), ('extras', '0013_objectchange')]
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0002_auto_20160622_1821'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CustomField',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('type', models.PositiveSmallIntegerField(choices=[(100, 'Text'), (200, 'Integer'), (300, 'Boolean (true/false)'), (400, 'Date'), (500, 'URL'), (600, 'Selection')], default=100)),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('label', models.CharField(blank=True, help_text="Name of the field as displayed to users (if not provided, the field's name will be used)", max_length=50)),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('required', models.BooleanField(default=False, help_text='Determines whether this field is required when creating new objects or editing an existing object.')),
|
||||
('is_filterable', models.BooleanField(default=True, help_text='This field can be used to filter objects.')),
|
||||
('default', models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans.', max_length=100)),
|
||||
('weight', models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form')),
|
||||
('obj_type', models.ManyToManyField(help_text='The object(s) to which this field applies.', related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['weight', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomFieldValue',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('obj_id', models.PositiveIntegerField()),
|
||||
('serialized_value', models.CharField(max_length=255)),
|
||||
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='values', to='extras.CustomField')),
|
||||
('obj_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['obj_type', 'obj_id'],
|
||||
'unique_together': {('field', 'obj_type', 'obj_id')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ExportTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('template_code', models.TextField()),
|
||||
('mime_type', models.CharField(blank=True, max_length=15)),
|
||||
('file_extension', models.CharField(blank=True, max_length=15)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['content_type', 'name'],
|
||||
'unique_together': {('content_type', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomFieldChoice',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.CharField(max_length=100)),
|
||||
('weight', models.PositiveSmallIntegerField(default=100, help_text='Higher weights appear lower in the list')),
|
||||
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['field', 'weight', 'value'],
|
||||
'unique_together': {('field', 'value')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Graph',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('type', models.PositiveSmallIntegerField(choices=[(100, 'Interface'), (200, 'Provider'), (300, 'Site')])),
|
||||
('weight', models.PositiveSmallIntegerField(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')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['type', 'weight', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ImageAttachment',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('image', models.ImageField(height_field='image_height', upload_to=extras.models.image_upload, width_field='image_width')),
|
||||
('image_height', models.PositiveSmallIntegerField()),
|
||||
('image_width', models.PositiveSmallIntegerField()),
|
||||
('name', models.CharField(blank=True, max_length=50)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TopologyMap',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('device_patterns', models.TextField(help_text='Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new 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(blank=True, max_length=100)),
|
||||
('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='topology_maps', to='dcim.Site')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserAction',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('time', models.DateTimeField(auto_now_add=True)),
|
||||
('object_id', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('action', models.PositiveSmallIntegerField(choices=[(1, 'created'), (7, 'bulk created'), (2, 'imported'), (3, 'modified'), (4, 'bulk edited'), (5, 'deleted'), (6, 'bulk deleted')])),
|
||||
('message', models.TextField(blank=True)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='actions', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-time'],
|
||||
},
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=verify_postgresql_version,
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReportResult',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('report', models.CharField(max_length=255, unique=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('failed', models.BooleanField()),
|
||||
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['report'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='topologymap',
|
||||
name='type',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, 'Network'), (2, 'Console'), (3, 'Power')], default=1),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customfield',
|
||||
name='filter_logic',
|
||||
field=models.PositiveSmallIntegerField(choices=[(0, 'Disabled'), (1, 'Loose'), (2, 'Exact')], default=1, help_text='Loose matches any instance of a given string; exact matches the entire field.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='required',
|
||||
field=models.BooleanField(default=False, help_text='If true, this field is required when creating new objects or editing an existing object.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='weight',
|
||||
field=models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form.'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=is_filterable_to_filter_logic,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='customfield',
|
||||
name='is_filterable',
|
||||
),
|
||||
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'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Webhook',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=150, unique=True)),
|
||||
('type_create', models.BooleanField(default=False, help_text='Call this webhook when a matching object is created.')),
|
||||
('type_update', models.BooleanField(default=False, help_text='Call this webhook when a matching object is updated.')),
|
||||
('type_delete', models.BooleanField(default=False, help_text='Call this webhook when a matching object is deleted.')),
|
||||
('payload_url', models.CharField(help_text='A POST will be sent to this URL when the webhook is called.', max_length=500, verbose_name='URL')),
|
||||
('http_content_type', models.PositiveSmallIntegerField(choices=[(1, 'application/json'), (2, 'application/x-www-form-urlencoded')], default=1, verbose_name='HTTP content type')),
|
||||
('secret', models.CharField(blank=True, help_text="When provided, the request will include a 'X-Hook-Signature' header containing a HMAC hex digest of the payload body using the secret as the key. The secret is not transmitted in the request.", max_length=255)),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
('ssl_verification', models.BooleanField(default=True, help_text='Enable SSL certificate verification. Disable with caution!', verbose_name='SSL verification')),
|
||||
('obj_type', models.ManyToManyField(help_text='The object(s) to which this Webhook applies.', related_name='webhooks', to='contenttypes.ContentType', verbose_name='Object types')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('payload_url', 'type_create', 'type_update', 'type_delete')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ObjectChange',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('time', models.DateTimeField(auto_now_add=True)),
|
||||
('user_name', models.CharField(editable=False, max_length=150)),
|
||||
('request_id', models.UUIDField(editable=False)),
|
||||
('action', models.PositiveSmallIntegerField(choices=[(1, 'Created'), (2, 'Updated'), (3, 'Deleted')])),
|
||||
('changed_object_id', models.PositiveIntegerField()),
|
||||
('related_object_id', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('object_repr', models.CharField(editable=False, max_length=200)),
|
||||
('object_data', django.contrib.postgres.fields.jsonb.JSONField(editable=False)),
|
||||
('changed_object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
|
||||
('related_object_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='changes', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-time'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -2,7 +2,7 @@
|
||||
# Generated by Django 1.11 on 2017-04-04 19:58
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import extras.models
|
||||
import extras.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('image', models.ImageField(height_field=b'image_height', upload_to=extras.models.image_upload, width_field=b'image_width')),
|
||||
('image', models.ImageField(height_field=b'image_height', upload_to=extras.utils.image_upload, width_field=b'image_width')),
|
||||
('image_height', models.PositiveSmallIntegerField()),
|
||||
('image_width', models.PositiveSmallIntegerField()),
|
||||
('name', models.CharField(blank=True, max_length=50)),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-05-24 15:34
|
||||
from django.db import migrations, models
|
||||
import extras.models
|
||||
import extras.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -74,7 +74,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='imageattachment',
|
||||
name='image',
|
||||
field=models.ImageField(height_field='image_height', upload_to=extras.models.image_upload, width_field='image_width'),
|
||||
field=models.ImageField(height_field='image_height', upload_to=extras.utils.image_upload, width_field='image_width'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='topologymap',
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_template_language(apps, schema_editor):
|
||||
"""
|
||||
Set the language for all existing ExportTemplates to Django (Jinja2 is the default for new ExportTemplates).
|
||||
"""
|
||||
ExportTemplate = apps.get_model('extras', 'ExportTemplate')
|
||||
ExportTemplate.objects.update(template_language=10)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('extras', '0014_configcontexts'), ('extras', '0015_remove_useraction'), ('extras', '0016_exporttemplate_add_cable'), ('extras', '0017_exporttemplate_mime_type_length'), ('extras', '0018_exporttemplate_add_jinja2'), ('extras', '0019_tag_taggeditem')]
|
||||
|
||||
dependencies = [
|
||||
('extras', '0013_objectchange'),
|
||||
('tenancy', '0005_change_logging'),
|
||||
('dcim', '0061_platform_napalm_args'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ConfigContext',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('weight', models.PositiveSmallIntegerField(default=1000)),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||
('platforms', models.ManyToManyField(blank=True, related_name='_configcontext_platforms_+', to='dcim.Platform')),
|
||||
('regions', models.ManyToManyField(blank=True, related_name='_configcontext_regions_+', to='dcim.Region')),
|
||||
('roles', models.ManyToManyField(blank=True, related_name='_configcontext_roles_+', to='dcim.DeviceRole')),
|
||||
('sites', models.ManyToManyField(blank=True, related_name='_configcontext_sites_+', to='dcim.Site')),
|
||||
('tenant_groups', models.ManyToManyField(blank=True, related_name='_configcontext_tenant_groups_+', to='tenancy.TenantGroup')),
|
||||
('tenants', models.ManyToManyField(blank=True, related_name='_configcontext_tenants_+', to='tenancy.Tenant')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['weight', 'name'],
|
||||
},
|
||||
),
|
||||
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', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine')}, related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'),
|
||||
),
|
||||
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', 'virtualchassis', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='webhook',
|
||||
name='obj_type',
|
||||
field=models.ManyToManyField(help_text='The object(s) to which this Webhook applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'virtualchassis', 'consoleport', 'consoleserverport', 'powerport', 'poweroutlet', 'interface', 'devicebay', 'inventoryitem', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine')}, related_name='webhooks', to='contenttypes.ContentType', verbose_name='Object types'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='UserAction',
|
||||
),
|
||||
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', 'interface', 'cable', 'virtualchassis', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='exporttemplate',
|
||||
name='mime_type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='exporttemplate',
|
||||
name='template_language',
|
||||
field=models.PositiveSmallIntegerField(default=20),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=set_template_language,
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tag',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TaggedItem',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('object_id', models.IntegerField(db_index=True)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extras_taggeditem_tagged_items', to='contenttypes.ContentType')),
|
||||
('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extras_taggeditem_items', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
'index_together': {('content_type', 'object_id')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,93 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
import utilities.fields
|
||||
|
||||
|
||||
def copy_tags(apps, schema_editor):
|
||||
"""
|
||||
Copy data from taggit_tag to extras_tag
|
||||
"""
|
||||
TaggitTag = apps.get_model('taggit', 'Tag')
|
||||
ExtrasTag = apps.get_model('extras', 'Tag')
|
||||
|
||||
tags_values = TaggitTag.objects.all().values('id', 'name', 'slug')
|
||||
tags = [ExtrasTag(**tag) for tag in tags_values]
|
||||
ExtrasTag.objects.bulk_create(tags)
|
||||
|
||||
|
||||
def copy_taggeditems(apps, schema_editor):
|
||||
"""
|
||||
Copy data from taggit_taggeditem to extras_taggeditem
|
||||
"""
|
||||
TaggitTaggedItem = apps.get_model('taggit', 'TaggedItem')
|
||||
ExtrasTaggedItem = apps.get_model('extras', 'TaggedItem')
|
||||
|
||||
tagged_items_values = TaggitTaggedItem.objects.all().values('id', 'object_id', 'content_type_id', 'tag_id')
|
||||
tagged_items = [ExtrasTaggedItem(**tagged_item) for tagged_item in tagged_items_values]
|
||||
ExtrasTaggedItem.objects.bulk_create(tagged_items)
|
||||
|
||||
|
||||
def delete_taggit_taggeditems(apps, schema_editor):
|
||||
"""
|
||||
Delete all TaggedItem instances from taggit_taggeditem
|
||||
"""
|
||||
TaggitTaggedItem = apps.get_model('taggit', 'TaggedItem')
|
||||
TaggitTaggedItem.objects.all().delete()
|
||||
|
||||
|
||||
def delete_taggit_tags(apps, schema_editor):
|
||||
"""
|
||||
Delete all Tag instances from taggit_tag
|
||||
"""
|
||||
TaggitTag = apps.get_model('taggit', 'Tag')
|
||||
TaggitTag.objects.all().delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('extras', '0020_tag_data'), ('extras', '0021_add_color_comments_changelog_to_tag')]
|
||||
|
||||
dependencies = [
|
||||
('extras', '0019_tag_taggeditem'),
|
||||
('virtualization', '0009_custom_tag_models'),
|
||||
('tenancy', '0006_custom_tag_models'),
|
||||
('secrets', '0006_custom_tag_models'),
|
||||
('dcim', '0070_custom_tag_models'),
|
||||
('ipam', '0025_custom_tag_models'),
|
||||
('circuits', '0015_custom_tag_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=copy_tags,
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=copy_taggeditems,
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=delete_taggit_taggeditems,
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=delete_taggit_tags,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='color',
|
||||
field=utilities.fields.ColorField(default='9e9e9e', max_length=6),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='created',
|
||||
field=models.DateField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,227 +0,0 @@
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import extras.models
|
||||
|
||||
CUSTOMFIELD_TYPE_CHOICES = (
|
||||
(100, 'text'),
|
||||
(200, 'integer'),
|
||||
(300, 'boolean'),
|
||||
(400, 'date'),
|
||||
(500, 'url'),
|
||||
(600, 'select')
|
||||
)
|
||||
|
||||
CUSTOMFIELD_FILTER_LOGIC_CHOICES = (
|
||||
(0, 'disabled'),
|
||||
(1, 'integer'),
|
||||
(2, 'exact'),
|
||||
)
|
||||
|
||||
OBJECTCHANGE_ACTION_CHOICES = (
|
||||
(1, 'create'),
|
||||
(2, 'update'),
|
||||
(3, 'delete'),
|
||||
)
|
||||
|
||||
EXPORTTEMPLATE_LANGUAGE_CHOICES = (
|
||||
(10, 'django'),
|
||||
(20, 'jinja2'),
|
||||
)
|
||||
|
||||
WEBHOOK_CONTENTTYPE_CHOICES = (
|
||||
(1, 'application/json'),
|
||||
(2, 'application/x-www-form-urlencoded'),
|
||||
)
|
||||
|
||||
GRAPH_TYPE_CHOICES = (
|
||||
(100, 'dcim', 'interface'),
|
||||
(150, 'dcim', 'device'),
|
||||
(200, 'circuits', 'provider'),
|
||||
(300, 'dcim', 'site'),
|
||||
)
|
||||
|
||||
|
||||
def customfield_type_to_slug(apps, schema_editor):
|
||||
CustomField = apps.get_model('extras', 'CustomField')
|
||||
for id, slug in CUSTOMFIELD_TYPE_CHOICES:
|
||||
CustomField.objects.filter(type=str(id)).update(type=slug)
|
||||
|
||||
|
||||
def customfield_filter_logic_to_slug(apps, schema_editor):
|
||||
CustomField = apps.get_model('extras', 'CustomField')
|
||||
for id, slug in CUSTOMFIELD_FILTER_LOGIC_CHOICES:
|
||||
CustomField.objects.filter(filter_logic=str(id)).update(filter_logic=slug)
|
||||
|
||||
|
||||
def objectchange_action_to_slug(apps, schema_editor):
|
||||
ObjectChange = apps.get_model('extras', 'ObjectChange')
|
||||
for id, slug in OBJECTCHANGE_ACTION_CHOICES:
|
||||
ObjectChange.objects.filter(action=str(id)).update(action=slug)
|
||||
|
||||
|
||||
def exporttemplate_language_to_slug(apps, schema_editor):
|
||||
ExportTemplate = apps.get_model('extras', 'ExportTemplate')
|
||||
for id, slug in EXPORTTEMPLATE_LANGUAGE_CHOICES:
|
||||
ExportTemplate.objects.filter(template_language=str(id)).update(template_language=slug)
|
||||
|
||||
|
||||
def webhook_contenttype_to_slug(apps, schema_editor):
|
||||
Webhook = apps.get_model('extras', 'Webhook')
|
||||
for id, slug in WEBHOOK_CONTENTTYPE_CHOICES:
|
||||
Webhook.objects.filter(http_content_type=str(id)).update(http_content_type=slug)
|
||||
|
||||
|
||||
def graph_type_to_fk(apps, schema_editor):
|
||||
Graph = apps.get_model('extras', 'Graph')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
|
||||
# On a new installation (and during tests) content types might not yet exist. So, we only perform the bulk
|
||||
# updates if a Graph has been created, which implies that we're working with a populated database.
|
||||
if Graph.objects.exists():
|
||||
for id, app_label, model in GRAPH_TYPE_CHOICES:
|
||||
content_type = ContentType.objects.get(app_label=app_label, model=model)
|
||||
Graph.objects.filter(type=id).update(type=content_type.pk)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('extras', '0022_custom_links'), ('extras', '0023_fix_tag_sequences'), ('extras', '0024_scripts'), ('extras', '0025_objectchange_time_index'), ('extras', '0026_webhook_ca_file_path'), ('extras', '0027_webhook_additional_headers'), ('extras', '0028_remove_topology_maps'), ('extras', '0029_3569_customfield_fields'), ('extras', '0030_3569_objectchange_fields'), ('extras', '0031_3569_exporttemplate_fields'), ('extras', '0032_3569_webhook_fields'), ('extras', '0033_graph_type_template_language'), ('extras', '0034_configcontext_tags')]
|
||||
|
||||
dependencies = [
|
||||
('extras', '0021_add_color_comments_changelog_to_tag'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CustomLink',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('text', models.CharField(max_length=500)),
|
||||
('url', models.CharField(max_length=500)),
|
||||
('weight', models.PositiveSmallIntegerField(default=100)),
|
||||
('group_name', models.CharField(blank=True, max_length=50)),
|
||||
('button_class', models.CharField(default='default', max_length=30)),
|
||||
('new_window', models.BooleanField()),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['group_name', 'weight', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='obj_type',
|
||||
field=models.ManyToManyField(related_name='custom_fields', to='contenttypes.ContentType'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='exporttemplate',
|
||||
name='content_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='webhook',
|
||||
name='obj_type',
|
||||
field=models.ManyToManyField(related_name='webhooks', to='contenttypes.ContentType'),
|
||||
),
|
||||
migrations.RunSQL(
|
||||
sql="SELECT setval('extras_tag_id_seq', (SELECT id FROM extras_tag ORDER BY id DESC LIMIT 1) + 1)",
|
||||
),
|
||||
migrations.RunSQL(
|
||||
sql="SELECT setval('extras_taggeditem_id_seq', (SELECT id FROM extras_taggeditem ORDER BY id DESC LIMIT 1) + 1)",
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Script',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
],
|
||||
options={
|
||||
'permissions': (('run_script', 'Can run script'),),
|
||||
'managed': False,
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objectchange',
|
||||
name='time',
|
||||
field=models.DateTimeField(auto_now_add=True, db_index=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='webhook',
|
||||
name='ca_file_path',
|
||||
field=models.CharField(blank=True, max_length=4096, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='webhook',
|
||||
name='additional_headers',
|
||||
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='TopologyMap',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='type',
|
||||
field=models.CharField(default='text', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=customfield_type_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfieldchoice',
|
||||
name='field',
|
||||
field=models.ForeignKey(limit_choices_to={'type': 'select'}, on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='filter_logic',
|
||||
field=models.CharField(default='loose', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=customfield_filter_logic_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objectchange',
|
||||
name='action',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=objectchange_action_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='exporttemplate',
|
||||
name='template_language',
|
||||
field=models.CharField(default='jinja2', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=exporttemplate_language_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='webhook',
|
||||
name='http_content_type',
|
||||
field=models.CharField(default='application/json', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=webhook_contenttype_to_slug,
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=graph_type_to_fk,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='graph',
|
||||
name='type',
|
||||
field=models.ForeignKey(limit_choices_to={'model__in': ['provider', 'device', 'interface', 'site']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='graph',
|
||||
name='template_language',
|
||||
field=models.CharField(default='jinja2', max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='configcontext',
|
||||
name='tags',
|
||||
field=models.ManyToManyField(blank=True, related_name='_configcontext_tags_+', to='extras.Tag'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,40 @@
|
||||
# Generated by Django 2.2.11 on 2020-03-14 06:50
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import extras.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0038_webhook_template_support'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='obj_type',
|
||||
field=models.ManyToManyField(limit_choices_to=extras.utils.FeatureQuery('custom_fields'), related_name='custom_fields', to='contenttypes.ContentType'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customlink',
|
||||
name='content_type',
|
||||
field=models.ForeignKey(limit_choices_to=extras.utils.FeatureQuery('custom_links'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='exporttemplate',
|
||||
name='content_type',
|
||||
field=models.ForeignKey(limit_choices_to=extras.utils.FeatureQuery('export_templates'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='graph',
|
||||
name='type',
|
||||
field=models.ForeignKey(limit_choices_to=extras.utils.FeatureQuery('graphs'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='webhook',
|
||||
name='obj_type',
|
||||
field=models.ManyToManyField(limit_choices_to=extras.utils.FeatureQuery('webhooks'), related_name='webhooks', to='contenttypes.ContentType'),
|
||||
),
|
||||
]
|
||||
23
netbox/extras/migrations/0040_standardize_description.py
Normal file
23
netbox/extras/migrations/0040_standardize_description.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.0.3 on 2020-03-13 20:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0039_update_features_content_types'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='configcontext',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
]
|
||||
23
netbox/extras/migrations/0041_tag_description.py
Normal file
23
netbox/extras/migrations/0041_tag_description.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.0.3 on 2020-03-13 20:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0040_standardize_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='comments',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='tag',
|
||||
old_name='comments',
|
||||
new_name='description',
|
||||
),
|
||||
]
|
||||
20
netbox/extras/migrations/0042_customfield_manager.py
Normal file
20
netbox/extras/migrations/0042_customfield_manager.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.0.5 on 2020-05-07 21:06
|
||||
|
||||
from django.db import migrations
|
||||
import extras.models.customfields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0041_tag_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelManagers(
|
||||
name='customfield',
|
||||
managers=[
|
||||
('objects', extras.models.customfields.CustomFieldManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
25
netbox/extras/models/__init__.py
Normal file
25
netbox/extras/models/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from .customfields import CustomField, CustomFieldChoice, CustomFieldModel, CustomFieldValue
|
||||
from .models import (
|
||||
ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult,
|
||||
Script, Webhook,
|
||||
)
|
||||
from .tags import Tag, TaggedItem
|
||||
|
||||
__all__ = (
|
||||
'ConfigContext',
|
||||
'ConfigContextModel',
|
||||
'CustomField',
|
||||
'CustomFieldChoice',
|
||||
'CustomFieldModel',
|
||||
'CustomFieldValue',
|
||||
'CustomLink',
|
||||
'ExportTemplate',
|
||||
'Graph',
|
||||
'ImageAttachment',
|
||||
'ObjectChange',
|
||||
'ReportResult',
|
||||
'Script',
|
||||
'Tag',
|
||||
'TaggedItem',
|
||||
'Webhook',
|
||||
)
|
||||
308
netbox/extras/models/customfields.py
Normal file
308
netbox/extras/models/customfields.py
Normal file
@@ -0,0 +1,308 @@
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from datetime import date
|
||||
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
|
||||
from utilities.forms import CSVChoiceField, DatePicker, LaxURLField, StaticSelect2, add_blank_choice
|
||||
from extras.choices import *
|
||||
from extras.utils import FeatureQuery
|
||||
|
||||
|
||||
#
|
||||
# Custom fields
|
||||
#
|
||||
|
||||
class CustomFieldModel(models.Model):
|
||||
_cf = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def cache_custom_fields(self):
|
||||
"""
|
||||
Cache all custom field values for this instance
|
||||
"""
|
||||
self._cf = {
|
||||
field.name: value for field, value in self.get_custom_fields().items()
|
||||
}
|
||||
|
||||
@property
|
||||
def cf(self):
|
||||
"""
|
||||
Name-based CustomFieldValue accessor for use in templates
|
||||
"""
|
||||
if self._cf is None:
|
||||
self.cache_custom_fields()
|
||||
return self._cf
|
||||
|
||||
def get_custom_fields(self):
|
||||
"""
|
||||
Return a dictionary of custom fields for a single object in the form {<field>: value}.
|
||||
"""
|
||||
fields = CustomField.objects.get_for_model(self)
|
||||
|
||||
# If the object exists, populate its custom fields with values
|
||||
if hasattr(self, 'pk'):
|
||||
values = self.custom_field_values.all()
|
||||
values_dict = {cfv.field_id: cfv.value for cfv in values}
|
||||
return OrderedDict([(field, values_dict.get(field.pk)) for field in fields])
|
||||
else:
|
||||
return OrderedDict([(field, None) for field in fields])
|
||||
|
||||
|
||||
class CustomFieldManager(models.Manager):
|
||||
use_in_migrations = True
|
||||
|
||||
def get_for_model(self, model):
|
||||
"""
|
||||
Return all CustomFields assigned to the given model.
|
||||
"""
|
||||
content_type = ContentType.objects.get_for_model(model._meta.concrete_model)
|
||||
return self.get_queryset().filter(obj_type=content_type)
|
||||
|
||||
|
||||
class CustomField(models.Model):
|
||||
obj_type = models.ManyToManyField(
|
||||
to=ContentType,
|
||||
related_name='custom_fields',
|
||||
verbose_name='Object(s)',
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
help_text='The object(s) to which this field applies.'
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=CustomFieldTypeChoices,
|
||||
default=CustomFieldTypeChoices.TYPE_TEXT
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=50,
|
||||
unique=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
help_text='Name of the field as displayed to users (if not provided, '
|
||||
'the field\'s name will be used)'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
required = models.BooleanField(
|
||||
default=False,
|
||||
help_text='If true, this field is required when creating new objects '
|
||||
'or editing an existing object.'
|
||||
)
|
||||
filter_logic = models.CharField(
|
||||
max_length=50,
|
||||
choices=CustomFieldFilterLogicChoices,
|
||||
default=CustomFieldFilterLogicChoices.FILTER_LOOSE,
|
||||
help_text='Loose matches any instance of a given string; exact '
|
||||
'matches the entire field.'
|
||||
)
|
||||
default = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text='Default value for the field. Use "true" or "false" for booleans.'
|
||||
)
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
default=100,
|
||||
help_text='Fields with higher weights appear lower in a form.'
|
||||
)
|
||||
|
||||
objects = CustomFieldManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['weight', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return self.label or self.name.replace('_', ' ').capitalize()
|
||||
|
||||
def serialize_value(self, value):
|
||||
"""
|
||||
Serialize the given value to a string suitable for storage as a CustomFieldValue
|
||||
"""
|
||||
if value is None:
|
||||
return ''
|
||||
if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
return str(int(bool(value)))
|
||||
if self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||
# Could be date/datetime object or string
|
||||
try:
|
||||
return value.strftime('%Y-%m-%d')
|
||||
except AttributeError:
|
||||
return value
|
||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
# Could be ModelChoiceField or TypedChoiceField
|
||||
return str(value.id) if hasattr(value, 'id') else str(value)
|
||||
return value
|
||||
|
||||
def deserialize_value(self, serialized_value):
|
||||
"""
|
||||
Convert a string into the object it represents depending on the type of field
|
||||
"""
|
||||
if serialized_value == '':
|
||||
return None
|
||||
if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
return int(serialized_value)
|
||||
if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
return bool(int(serialized_value))
|
||||
if self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||
# Read date as YYYY-MM-DD
|
||||
return date(*[int(n) for n in serialized_value.split('-')])
|
||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
return self.choices.get(pk=int(serialized_value))
|
||||
return serialized_value
|
||||
|
||||
def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False):
|
||||
"""
|
||||
Return a form field suitable for setting a CustomField's value for an object.
|
||||
|
||||
set_initial: Set initial date for the field. This should be False when generating a field for bulk editing.
|
||||
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
|
||||
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
|
||||
"""
|
||||
initial = self.default if set_initial else None
|
||||
required = self.required if enforce_required else False
|
||||
|
||||
# Integer
|
||||
if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
field = forms.IntegerField(required=required, initial=initial)
|
||||
|
||||
# Boolean
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
choices = (
|
||||
(None, '---------'),
|
||||
(1, 'True'),
|
||||
(0, 'False'),
|
||||
)
|
||||
if initial is not None and initial.lower() in ['true', 'yes', '1']:
|
||||
initial = 1
|
||||
elif initial is not None and initial.lower() in ['false', 'no', '0']:
|
||||
initial = 0
|
||||
else:
|
||||
initial = None
|
||||
field = forms.NullBooleanField(
|
||||
required=required, initial=initial, widget=StaticSelect2(choices=choices)
|
||||
)
|
||||
|
||||
# Date
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||
field = forms.DateField(required=required, initial=initial, widget=DatePicker())
|
||||
|
||||
# Select
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
choices = [(cfc.pk, cfc.value) for cfc in self.choices.all()]
|
||||
|
||||
if not required:
|
||||
choices = add_blank_choice(choices)
|
||||
|
||||
# Set the initial value to the PK of the default choice, if any
|
||||
if set_initial:
|
||||
default_choice = self.choices.filter(value=self.default).first()
|
||||
if default_choice:
|
||||
initial = default_choice.pk
|
||||
|
||||
field_class = CSVChoiceField if for_csv_import else forms.ChoiceField
|
||||
field = field_class(
|
||||
choices=choices, required=required, initial=initial, widget=StaticSelect2()
|
||||
)
|
||||
|
||||
# URL
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_URL:
|
||||
field = LaxURLField(required=required, initial=initial)
|
||||
|
||||
# Text
|
||||
else:
|
||||
field = forms.CharField(max_length=255, required=required, initial=initial)
|
||||
|
||||
field.model = self
|
||||
field.label = self.label if self.label else self.name.replace('_', ' ').capitalize()
|
||||
if self.description:
|
||||
field.help_text = self.description
|
||||
|
||||
return field
|
||||
|
||||
|
||||
class CustomFieldValue(models.Model):
|
||||
field = models.ForeignKey(
|
||||
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 = GenericForeignKey(
|
||||
ct_field='obj_type',
|
||||
fk_field='obj_id'
|
||||
)
|
||||
serialized_value = models.CharField(
|
||||
max_length=255
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('obj_type', 'obj_id', 'pk') # (obj_type, obj_id) may be non-unique
|
||||
unique_together = ('field', 'obj_type', 'obj_id')
|
||||
|
||||
def __str__(self):
|
||||
return '{} {}'.format(self.obj, self.field)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.field.deserialize_value(self.serialized_value)
|
||||
|
||||
@value.setter
|
||||
def value(self, value):
|
||||
self.serialized_value = self.field.serialize_value(value)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Delete this object if it no longer has a value to store
|
||||
if self.pk and self.value is None:
|
||||
self.delete()
|
||||
else:
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class CustomFieldChoice(models.Model):
|
||||
field = models.ForeignKey(
|
||||
to='extras.CustomField',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='choices',
|
||||
limit_choices_to={'type': CustomFieldTypeChoices.TYPE_SELECT}
|
||||
)
|
||||
value = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
default=100,
|
||||
help_text='Higher weights appear lower in the list'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['field', 'weight', 'value']
|
||||
unique_together = ['field', 'value']
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
def clean(self):
|
||||
if self.field.type != CustomFieldTypeChoices.TYPE_SELECT:
|
||||
raise ValidationError("Custom field choices can only be assigned to selection fields.")
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
# When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
|
||||
pk = self.pk
|
||||
super().delete(using, keep_parents)
|
||||
CustomFieldValue.objects.filter(
|
||||
field__type=CustomFieldTypeChoices.TYPE_SELECT,
|
||||
serialized_value=str(pk)
|
||||
).delete()
|
||||
@@ -1,8 +1,6 @@
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from datetime import date
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -12,36 +10,13 @@ from django.db import models
|
||||
from django.http import HttpResponse
|
||||
from django.template import Template, Context
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
from taggit.models import TagBase, GenericTaggedItemBase
|
||||
|
||||
from utilities.fields import ColorField
|
||||
from utilities.forms import CSVChoiceField, DatePicker, LaxURLField, StaticSelect2, add_blank_choice
|
||||
from utilities.utils import deepmerge, render_jinja2
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .querysets import ConfigContextQuerySet
|
||||
|
||||
|
||||
__all__ = (
|
||||
'ConfigContext',
|
||||
'ConfigContextModel',
|
||||
'CustomField',
|
||||
'CustomFieldChoice',
|
||||
'CustomFieldModel',
|
||||
'CustomFieldValue',
|
||||
'CustomLink',
|
||||
'ExportTemplate',
|
||||
'Graph',
|
||||
'ImageAttachment',
|
||||
'ObjectChange',
|
||||
'ReportResult',
|
||||
'Script',
|
||||
'Tag',
|
||||
'TaggedItem',
|
||||
'Webhook',
|
||||
)
|
||||
from extras.choices import *
|
||||
from extras.constants import *
|
||||
from extras.querysets import ConfigContextQuerySet
|
||||
from extras.utils import FeatureQuery, image_upload
|
||||
|
||||
|
||||
#
|
||||
@@ -58,7 +33,7 @@ class Webhook(models.Model):
|
||||
to=ContentType,
|
||||
related_name='webhooks',
|
||||
verbose_name='Object types',
|
||||
limit_choices_to=WEBHOOK_MODELS,
|
||||
limit_choices_to=FeatureQuery('webhooks'),
|
||||
help_text="The object(s) to which this Webhook applies."
|
||||
)
|
||||
name = models.CharField(
|
||||
@@ -173,291 +148,6 @@ class Webhook(models.Model):
|
||||
return json.dumps(context, cls=JSONEncoder)
|
||||
|
||||
|
||||
#
|
||||
# Custom fields
|
||||
#
|
||||
|
||||
class CustomFieldModel(models.Model):
|
||||
_cf = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def cache_custom_fields(self):
|
||||
"""
|
||||
Cache all custom field values for this instance
|
||||
"""
|
||||
self._cf = {
|
||||
field.name: value for field, value in self.get_custom_fields().items()
|
||||
}
|
||||
|
||||
@property
|
||||
def cf(self):
|
||||
"""
|
||||
Name-based CustomFieldValue accessor for use in templates
|
||||
"""
|
||||
if self._cf is None:
|
||||
self.cache_custom_fields()
|
||||
return self._cf
|
||||
|
||||
def get_custom_fields(self):
|
||||
"""
|
||||
Return a dictionary of custom fields for a single object in the form {<field>: value}.
|
||||
"""
|
||||
|
||||
# Find all custom fields applicable to this type of object
|
||||
content_type = ContentType.objects.get_for_model(self)
|
||||
fields = CustomField.objects.filter(obj_type=content_type)
|
||||
|
||||
# If the object exists, populate its custom fields with values
|
||||
if hasattr(self, 'pk'):
|
||||
values = self.custom_field_values.all()
|
||||
values_dict = {cfv.field_id: cfv.value for cfv in values}
|
||||
return OrderedDict([(field, values_dict.get(field.pk)) for field in fields])
|
||||
else:
|
||||
return OrderedDict([(field, None) for field in fields])
|
||||
|
||||
|
||||
class CustomField(models.Model):
|
||||
obj_type = models.ManyToManyField(
|
||||
to=ContentType,
|
||||
related_name='custom_fields',
|
||||
verbose_name='Object(s)',
|
||||
limit_choices_to=CUSTOMFIELD_MODELS,
|
||||
help_text='The object(s) to which this field applies.'
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=CustomFieldTypeChoices,
|
||||
default=CustomFieldTypeChoices.TYPE_TEXT
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=50,
|
||||
unique=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
help_text='Name of the field as displayed to users (if not provided, '
|
||||
'the field\'s name will be used)'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
required = models.BooleanField(
|
||||
default=False,
|
||||
help_text='If true, this field is required when creating new objects '
|
||||
'or editing an existing object.'
|
||||
)
|
||||
filter_logic = models.CharField(
|
||||
max_length=50,
|
||||
choices=CustomFieldFilterLogicChoices,
|
||||
default=CustomFieldFilterLogicChoices.FILTER_LOOSE,
|
||||
help_text='Loose matches any instance of a given string; exact '
|
||||
'matches the entire field.'
|
||||
)
|
||||
default = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text='Default value for the field. Use "true" or "false" for booleans.'
|
||||
)
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
default=100,
|
||||
help_text='Fields with higher weights appear lower in a form.'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['weight', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return self.label or self.name.replace('_', ' ').capitalize()
|
||||
|
||||
def serialize_value(self, value):
|
||||
"""
|
||||
Serialize the given value to a string suitable for storage as a CustomFieldValue
|
||||
"""
|
||||
if value is None:
|
||||
return ''
|
||||
if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
return str(int(bool(value)))
|
||||
if self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||
# Could be date/datetime object or string
|
||||
try:
|
||||
return value.strftime('%Y-%m-%d')
|
||||
except AttributeError:
|
||||
return value
|
||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
# Could be ModelChoiceField or TypedChoiceField
|
||||
return str(value.id) if hasattr(value, 'id') else str(value)
|
||||
return value
|
||||
|
||||
def deserialize_value(self, serialized_value):
|
||||
"""
|
||||
Convert a string into the object it represents depending on the type of field
|
||||
"""
|
||||
if serialized_value == '':
|
||||
return None
|
||||
if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
return int(serialized_value)
|
||||
if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
return bool(int(serialized_value))
|
||||
if self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||
# Read date as YYYY-MM-DD
|
||||
return date(*[int(n) for n in serialized_value.split('-')])
|
||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
return self.choices.get(pk=int(serialized_value))
|
||||
return serialized_value
|
||||
|
||||
def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False):
|
||||
"""
|
||||
Return a form field suitable for setting a CustomField's value for an object.
|
||||
|
||||
set_initial: Set initial date for the field. This should be False when generating a field for bulk editing.
|
||||
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
|
||||
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
|
||||
"""
|
||||
initial = self.default if set_initial else None
|
||||
required = self.required if enforce_required else False
|
||||
|
||||
# Integer
|
||||
if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
field = forms.IntegerField(required=required, initial=initial)
|
||||
|
||||
# Boolean
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
choices = (
|
||||
(None, '---------'),
|
||||
(1, 'True'),
|
||||
(0, 'False'),
|
||||
)
|
||||
if initial is not None and initial.lower() in ['true', 'yes', '1']:
|
||||
initial = 1
|
||||
elif initial is not None and initial.lower() in ['false', 'no', '0']:
|
||||
initial = 0
|
||||
else:
|
||||
initial = None
|
||||
field = forms.NullBooleanField(
|
||||
required=required, initial=initial, widget=StaticSelect2(choices=choices)
|
||||
)
|
||||
|
||||
# Date
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||
field = forms.DateField(required=required, initial=initial, widget=DatePicker())
|
||||
|
||||
# Select
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
choices = [(cfc.pk, cfc.value) for cfc in self.choices.all()]
|
||||
|
||||
if not required:
|
||||
choices = add_blank_choice(choices)
|
||||
|
||||
# Set the initial value to the PK of the default choice, if any
|
||||
if set_initial:
|
||||
default_choice = self.choices.filter(value=self.default).first()
|
||||
if default_choice:
|
||||
initial = default_choice.pk
|
||||
|
||||
field_class = CSVChoiceField if for_csv_import else forms.ChoiceField
|
||||
field = field_class(
|
||||
choices=choices, required=required, initial=initial, widget=StaticSelect2()
|
||||
)
|
||||
|
||||
# URL
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_URL:
|
||||
field = LaxURLField(required=required, initial=initial)
|
||||
|
||||
# Text
|
||||
else:
|
||||
field = forms.CharField(max_length=255, required=required, initial=initial)
|
||||
|
||||
field.model = self
|
||||
field.label = self.label if self.label else self.name.replace('_', ' ').capitalize()
|
||||
if self.description:
|
||||
field.help_text = self.description
|
||||
|
||||
return field
|
||||
|
||||
|
||||
class CustomFieldValue(models.Model):
|
||||
field = models.ForeignKey(
|
||||
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 = GenericForeignKey(
|
||||
ct_field='obj_type',
|
||||
fk_field='obj_id'
|
||||
)
|
||||
serialized_value = models.CharField(
|
||||
max_length=255
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('obj_type', 'obj_id', 'pk') # (obj_type, obj_id) may be non-unique
|
||||
unique_together = ('field', 'obj_type', 'obj_id')
|
||||
|
||||
def __str__(self):
|
||||
return '{} {}'.format(self.obj, self.field)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.field.deserialize_value(self.serialized_value)
|
||||
|
||||
@value.setter
|
||||
def value(self, value):
|
||||
self.serialized_value = self.field.serialize_value(value)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Delete this object if it no longer has a value to store
|
||||
if self.pk and self.value is None:
|
||||
self.delete()
|
||||
else:
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class CustomFieldChoice(models.Model):
|
||||
field = models.ForeignKey(
|
||||
to='extras.CustomField',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='choices',
|
||||
limit_choices_to={'type': CustomFieldTypeChoices.TYPE_SELECT}
|
||||
)
|
||||
value = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
default=100,
|
||||
help_text='Higher weights appear lower in the list'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['field', 'weight', 'value']
|
||||
unique_together = ['field', 'value']
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
def clean(self):
|
||||
if self.field.type != CustomFieldTypeChoices.TYPE_SELECT:
|
||||
raise ValidationError("Custom field choices can only be assigned to selection fields.")
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
# When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
|
||||
pk = self.pk
|
||||
super().delete(using, keep_parents)
|
||||
CustomFieldValue.objects.filter(
|
||||
field__type=CustomFieldTypeChoices.TYPE_SELECT,
|
||||
serialized_value=str(pk)
|
||||
).delete()
|
||||
|
||||
|
||||
#
|
||||
# Custom links
|
||||
#
|
||||
@@ -470,7 +160,7 @@ class CustomLink(models.Model):
|
||||
content_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
on_delete=models.CASCADE,
|
||||
limit_choices_to=CUSTOMLINK_MODELS
|
||||
limit_choices_to=FeatureQuery('custom_links')
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
@@ -518,7 +208,7 @@ class Graph(models.Model):
|
||||
type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
on_delete=models.CASCADE,
|
||||
limit_choices_to=GRAPH_MODELS
|
||||
limit_choices_to=FeatureQuery('graphs')
|
||||
)
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
default=1000
|
||||
@@ -550,7 +240,6 @@ class Graph(models.Model):
|
||||
def embed_url(self, obj):
|
||||
context = {'obj': obj}
|
||||
|
||||
# TODO: Remove in v2.8
|
||||
if self.template_language == TemplateLanguageChoices.LANGUAGE_DJANGO:
|
||||
template = Template(self.source)
|
||||
return template.render(Context(context))
|
||||
@@ -564,7 +253,6 @@ class Graph(models.Model):
|
||||
|
||||
context = {'obj': obj}
|
||||
|
||||
# TODO: Remove in v2.8
|
||||
if self.template_language == TemplateLanguageChoices.LANGUAGE_DJANGO:
|
||||
template = Template(self.link)
|
||||
return template.render(Context(context))
|
||||
@@ -581,7 +269,7 @@ class ExportTemplate(models.Model):
|
||||
content_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
on_delete=models.CASCADE,
|
||||
limit_choices_to=EXPORTTEMPLATE_MODELS
|
||||
limit_choices_to=FeatureQuery('export_templates')
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
@@ -664,20 +352,6 @@ class ExportTemplate(models.Model):
|
||||
# Image attachments
|
||||
#
|
||||
|
||||
def image_upload(instance, filename):
|
||||
|
||||
path = 'image-attachments/'
|
||||
|
||||
# Rename the file to the provided name, if any. Attempt to preserve the file extension.
|
||||
extension = filename.rsplit('.')[-1].lower()
|
||||
if instance.name and extension in ['bmp', 'gif', 'jpeg', 'jpg', 'png']:
|
||||
filename = '.'.join([instance.name, extension])
|
||||
elif instance.name:
|
||||
filename = instance.name
|
||||
|
||||
return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
|
||||
|
||||
|
||||
class ImageAttachment(models.Model):
|
||||
"""
|
||||
An uploaded image which is associated with an object.
|
||||
@@ -766,7 +440,7 @@ class ConfigContext(models.Model):
|
||||
default=1000
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
@@ -1039,44 +713,3 @@ class ObjectChange(models.Model):
|
||||
self.object_repr,
|
||||
self.object_data,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Tags
|
||||
#
|
||||
|
||||
# TODO: figure out a way around this circular import for ObjectChange
|
||||
from utilities.models import ChangeLoggedModel # noqa: E402
|
||||
|
||||
|
||||
class Tag(TagBase, ChangeLoggedModel):
|
||||
color = ColorField(
|
||||
default='9e9e9e'
|
||||
)
|
||||
comments = models.TextField(
|
||||
blank=True,
|
||||
default=''
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:tag', args=[self.slug])
|
||||
|
||||
def slugify(self, tag, i=None):
|
||||
# Allow Unicode in Tag slugs (avoids empty slugs for Tags with all-Unicode names)
|
||||
slug = slugify(tag, allow_unicode=True)
|
||||
if i is not None:
|
||||
slug += "_%d" % i
|
||||
return slug
|
||||
|
||||
|
||||
class TaggedItem(GenericTaggedItemBase):
|
||||
tag = models.ForeignKey(
|
||||
to=Tag,
|
||||
related_name="%(app_label)s_%(class)s_items",
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
class Meta:
|
||||
index_together = (
|
||||
("content_type", "object_id")
|
||||
)
|
||||
45
netbox/extras/models/tags.py
Normal file
45
netbox/extras/models/tags.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
from taggit.models import TagBase, GenericTaggedItemBase
|
||||
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField
|
||||
from utilities.models import ChangeLoggedModel
|
||||
|
||||
|
||||
#
|
||||
# Tags
|
||||
#
|
||||
|
||||
class Tag(TagBase, ChangeLoggedModel):
|
||||
color = ColorField(
|
||||
default=ColorChoices.COLOR_GREY
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:tag', args=[self.slug])
|
||||
|
||||
def slugify(self, tag, i=None):
|
||||
# Allow Unicode in Tag slugs (avoids empty slugs for Tags with all-Unicode names)
|
||||
slug = slugify(tag, allow_unicode=True)
|
||||
if i is not None:
|
||||
slug += "_%d" % i
|
||||
return slug
|
||||
|
||||
|
||||
class TaggedItem(GenericTaggedItemBase):
|
||||
tag = models.ForeignKey(
|
||||
to=Tag,
|
||||
related_name="%(app_label)s_%(class)s_items",
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
class Meta:
|
||||
index_together = (
|
||||
("content_type", "object_id")
|
||||
)
|
||||
250
netbox/extras/plugins/__init__.py
Normal file
250
netbox/extras/plugins/__init__.py
Normal file
@@ -0,0 +1,250 @@
|
||||
import collections
|
||||
import inspect
|
||||
from packaging import version
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.template.loader import get_template
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from extras.registry import registry
|
||||
from utilities.choices import ButtonColorChoices
|
||||
|
||||
|
||||
# Initialize plugin registry stores
|
||||
registry['plugin_template_extensions'] = collections.defaultdict(list)
|
||||
registry['plugin_menu_items'] = {}
|
||||
|
||||
|
||||
#
|
||||
# Plugin AppConfig class
|
||||
#
|
||||
|
||||
class PluginConfig(AppConfig):
|
||||
"""
|
||||
Subclass of Django's built-in AppConfig class, to be used for NetBox plugins.
|
||||
"""
|
||||
# Plugin metadata
|
||||
author = ''
|
||||
author_email = ''
|
||||
description = ''
|
||||
version = ''
|
||||
|
||||
# Root URL path under /plugins. If not set, the plugin's label will be used.
|
||||
base_url = None
|
||||
|
||||
# Minimum/maximum compatible versions of NetBox
|
||||
min_version = None
|
||||
max_version = None
|
||||
|
||||
# Default configuration parameters
|
||||
default_settings = {}
|
||||
|
||||
# Mandatory configuration parameters
|
||||
required_settings = []
|
||||
|
||||
# Middleware classes provided by the plugin
|
||||
middleware = []
|
||||
|
||||
# Cacheops configuration. Cache all operations by default.
|
||||
caching_config = {
|
||||
'*': {'ops': 'all'},
|
||||
}
|
||||
|
||||
# Default integration paths. Plugin authors can override these to customize the paths to
|
||||
# integrated components.
|
||||
template_extensions = 'template_content.template_extensions'
|
||||
menu_items = 'navigation.menu_items'
|
||||
|
||||
def ready(self):
|
||||
|
||||
# Register template content
|
||||
try:
|
||||
template_extensions = import_string(f"{self.__module__}.{self.template_extensions}")
|
||||
register_template_extensions(template_extensions)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Register navigation menu items (if defined)
|
||||
try:
|
||||
menu_items = import_string(f"{self.__module__}.{self.menu_items}")
|
||||
register_menu_items(self.verbose_name, menu_items)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def validate(cls, user_config):
|
||||
|
||||
# Enforce version constraints
|
||||
current_version = version.parse(settings.VERSION)
|
||||
if cls.min_version is not None:
|
||||
min_version = version.parse(cls.min_version)
|
||||
if current_version < min_version:
|
||||
raise ImproperlyConfigured(
|
||||
f"Plugin {cls.__module__} requires NetBox minimum version {cls.min_version}."
|
||||
)
|
||||
if cls.max_version is not None:
|
||||
max_version = version.parse(cls.max_version)
|
||||
if current_version > max_version:
|
||||
raise ImproperlyConfigured(
|
||||
f"Plugin {cls.__module__} requires NetBox maximum version {cls.max_version}."
|
||||
)
|
||||
|
||||
# Verify required configuration settings
|
||||
for setting in cls.required_settings:
|
||||
if setting not in user_config:
|
||||
raise ImproperlyConfigured(
|
||||
f"Plugin {cls.__module__} requires '{setting}' to be present in the PLUGINS_CONFIG section of "
|
||||
f"configuration.py."
|
||||
)
|
||||
|
||||
# Apply default configuration values
|
||||
for setting, value in cls.default_settings.items():
|
||||
if setting not in user_config:
|
||||
user_config[setting] = value
|
||||
|
||||
|
||||
#
|
||||
# Template content injection
|
||||
#
|
||||
|
||||
class PluginTemplateExtension:
|
||||
"""
|
||||
This class is used to register plugin content to be injected into core NetBox templates. It contains methods
|
||||
that are overridden by plugin authors to return template content.
|
||||
|
||||
The `model` attribute on the class defines the which model detail page this class renders content for. It
|
||||
should be set as a string in the form '<app_label>.<model_name>'. render() provides the following context data:
|
||||
|
||||
* object - The object being viewed
|
||||
* request - The current request
|
||||
* settings - Global NetBox settings
|
||||
* config - Plugin-specific configuration parameters
|
||||
"""
|
||||
model = None
|
||||
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
|
||||
def render(self, template_name, extra_context=None):
|
||||
"""
|
||||
Convenience method for rendering the specified Django template using the default context data. An additional
|
||||
context dictionary may be passed as `extra_context`.
|
||||
"""
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
elif not isinstance(extra_context, dict):
|
||||
raise TypeError("extra_context must be a dictionary")
|
||||
|
||||
return get_template(template_name).render({**self.context, **extra_context})
|
||||
|
||||
def left_page(self):
|
||||
"""
|
||||
Content that will be rendered on the left of the detail page view. Content should be returned as an
|
||||
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def right_page(self):
|
||||
"""
|
||||
Content that will be rendered on the right of the detail page view. Content should be returned as an
|
||||
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def full_width_page(self):
|
||||
"""
|
||||
Content that will be rendered within the full width of the detail page view. Content should be returned as an
|
||||
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def buttons(self):
|
||||
"""
|
||||
Buttons that will be rendered and added to the existing list of buttons on the detail page view. Content
|
||||
should be returned as an HTML string. Note that content does not need to be marked as safe because this is
|
||||
automatically handled.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def register_template_extensions(class_list):
|
||||
"""
|
||||
Register a list of PluginTemplateExtension classes
|
||||
"""
|
||||
# Validation
|
||||
for template_extension in class_list:
|
||||
if not inspect.isclass(template_extension):
|
||||
raise TypeError(f"PluginTemplateExtension class {template_extension} was passes as an instance!")
|
||||
if not issubclass(template_extension, PluginTemplateExtension):
|
||||
raise TypeError(f"{template_extension} is not a subclass of extras.plugins.PluginTemplateExtension!")
|
||||
if template_extension.model is None:
|
||||
raise TypeError(f"PluginTemplateExtension class {template_extension} does not define a valid model!")
|
||||
|
||||
registry['plugin_template_extensions'][template_extension.model].append(template_extension)
|
||||
|
||||
|
||||
#
|
||||
# Navigation menu links
|
||||
#
|
||||
|
||||
class PluginMenuItem:
|
||||
"""
|
||||
This class represents a navigation menu item. This constitutes primary link and its text, but also allows for
|
||||
specifying additional link buttons that appear to the right of the item in the van menu.
|
||||
|
||||
Links are specified as Django reverse URL strings.
|
||||
Buttons are each specified as a list of PluginMenuButton instances.
|
||||
"""
|
||||
permissions = []
|
||||
buttons = []
|
||||
|
||||
def __init__(self, link, link_text, permissions=None, buttons=None):
|
||||
self.link = link
|
||||
self.link_text = link_text
|
||||
if permissions is not None:
|
||||
if type(permissions) not in (list, tuple):
|
||||
raise TypeError("Permissions must be passed as a tuple or list.")
|
||||
self.permissions = permissions
|
||||
if buttons is not None:
|
||||
if type(buttons) not in (list, tuple):
|
||||
raise TypeError("Buttons must be passed as a tuple or list.")
|
||||
self.buttons = buttons
|
||||
|
||||
|
||||
class PluginMenuButton:
|
||||
"""
|
||||
This class represents a button within a PluginMenuItem. Note that button colors should come from
|
||||
ButtonColorChoices.
|
||||
"""
|
||||
color = ButtonColorChoices.DEFAULT
|
||||
permissions = []
|
||||
|
||||
def __init__(self, link, title, icon_class, color=None, permissions=None):
|
||||
self.link = link
|
||||
self.title = title
|
||||
self.icon_class = icon_class
|
||||
if permissions is not None:
|
||||
if type(permissions) not in (list, tuple):
|
||||
raise TypeError("Permissions must be passed as a tuple or list.")
|
||||
self.permissions = permissions
|
||||
if color is not None:
|
||||
if color not in ButtonColorChoices.values():
|
||||
raise ValueError("Button color must be a choice within ButtonColorChoices.")
|
||||
self.color = color
|
||||
|
||||
|
||||
def register_menu_items(section_name, class_list):
|
||||
"""
|
||||
Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name)
|
||||
"""
|
||||
# Validation
|
||||
for menu_link in class_list:
|
||||
if not isinstance(menu_link, PluginMenuItem):
|
||||
raise TypeError(f"{menu_link} must be an instance of extras.plugins.PluginMenuItem")
|
||||
for button in menu_link.buttons:
|
||||
if not isinstance(button, PluginMenuButton):
|
||||
raise TypeError(f"{button} must be an instance of extras.plugins.PluginMenuButton")
|
||||
|
||||
registry['plugin_menu_items'][section_name] = class_list
|
||||
42
netbox/extras/plugins/urls.py
Normal file
42
netbox/extras/plugins/urls.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.urls import path
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from . import views
|
||||
|
||||
# Initialize URL base, API, and admin URL patterns for plugins
|
||||
plugin_patterns = []
|
||||
plugin_api_patterns = [
|
||||
path('', views.PluginsAPIRootView.as_view(), name='api-root'),
|
||||
path('installed-plugins/', views.InstalledPluginsAPIView.as_view(), name='plugins-list')
|
||||
]
|
||||
plugin_admin_patterns = [
|
||||
path('installed-plugins/', staff_member_required(views.InstalledPluginsAdminView.as_view()), name='plugins_list')
|
||||
]
|
||||
|
||||
# Register base/API URL patterns for each plugin
|
||||
for plugin_path in settings.PLUGINS:
|
||||
plugin_name = plugin_path.split('.')[-1]
|
||||
app = apps.get_app_config(plugin_name)
|
||||
base_url = getattr(app, 'base_url') or app.label
|
||||
|
||||
# Check if the plugin specifies any base URLs
|
||||
try:
|
||||
urlpatterns = import_string(f"{plugin_path}.urls.urlpatterns")
|
||||
plugin_patterns.append(
|
||||
path(f"{base_url}/", include((urlpatterns, app.label)))
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Check if the plugin specifies any API URLs
|
||||
try:
|
||||
urlpatterns = import_string(f"{plugin_path}.api.urls.urlpatterns")
|
||||
plugin_api_patterns.append(
|
||||
path(f"{base_url}/", include((urlpatterns, f"{app.label}-api")))
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
93
netbox/extras/plugins/views.py
Normal file
93
netbox/extras/plugins/views.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.shortcuts import render
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
from django.utils.module_loading import import_string
|
||||
from django.views.generic import View
|
||||
from rest_framework import permissions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
||||
class InstalledPluginsAdminView(View):
|
||||
"""
|
||||
Admin view for listing all installed plugins
|
||||
"""
|
||||
def get(self, request):
|
||||
plugins = [apps.get_app_config(plugin) for plugin in settings.PLUGINS]
|
||||
return render(request, 'extras/admin/plugins_list.html', {
|
||||
'plugins': plugins,
|
||||
})
|
||||
|
||||
|
||||
class InstalledPluginsAPIView(APIView):
|
||||
"""
|
||||
API view for listing all installed plugins
|
||||
"""
|
||||
permission_classes = [permissions.IsAdminUser]
|
||||
_ignore_model_permissions = True
|
||||
exclude_from_schema = True
|
||||
swagger_schema = None
|
||||
|
||||
def get_view_name(self):
|
||||
return "Installed Plugins"
|
||||
|
||||
@staticmethod
|
||||
def _get_plugin_data(plugin_app_config):
|
||||
return {
|
||||
'name': plugin_app_config.verbose_name,
|
||||
'package': plugin_app_config.name,
|
||||
'author': plugin_app_config.author,
|
||||
'author_email': plugin_app_config.author_email,
|
||||
'description': plugin_app_config.description,
|
||||
'verison': plugin_app_config.version
|
||||
}
|
||||
|
||||
def get(self, request, format=None):
|
||||
return Response([self._get_plugin_data(apps.get_app_config(plugin)) for plugin in settings.PLUGINS])
|
||||
|
||||
|
||||
class PluginsAPIRootView(APIView):
|
||||
_ignore_model_permissions = True
|
||||
exclude_from_schema = True
|
||||
swagger_schema = None
|
||||
|
||||
def get_view_name(self):
|
||||
return "Plugins"
|
||||
|
||||
@staticmethod
|
||||
def _get_plugin_entry(plugin, app_config, request, format):
|
||||
try:
|
||||
api_app_name = import_string(f"{plugin}.api.urls.app_name")
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
# Plugin does not expose an API
|
||||
return None
|
||||
|
||||
try:
|
||||
entry = (getattr(app_config, 'base_url', app_config.label), reverse(
|
||||
f"plugins-api:{api_app_name}:api-root",
|
||||
request=request,
|
||||
format=format
|
||||
))
|
||||
except NoReverseMatch:
|
||||
# The plugin does not include an api-root
|
||||
entry = None
|
||||
|
||||
return entry
|
||||
|
||||
def get(self, request, format=None):
|
||||
|
||||
entries = []
|
||||
for plugin in settings.PLUGINS:
|
||||
app_config = apps.get_app_config(plugin)
|
||||
entry = self._get_plugin_entry(plugin, app_config, request, format)
|
||||
if entry is not None:
|
||||
entries.append(entry)
|
||||
|
||||
return Response(OrderedDict((
|
||||
('installed-plugins', reverse('plugins-api:plugins-list', request=request, format=format)),
|
||||
*entries
|
||||
)))
|
||||
21
netbox/extras/registry.py
Normal file
21
netbox/extras/registry.py
Normal file
@@ -0,0 +1,21 @@
|
||||
class Registry(dict):
|
||||
"""
|
||||
Central registry for registration of functionality. Once a store (key) is defined, it cannot be overwritten or
|
||||
deleted (although its value may be manipulated).
|
||||
"""
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return super().__getitem__(key)
|
||||
except KeyError:
|
||||
raise KeyError("Invalid store: {}".format(key))
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key in self:
|
||||
raise KeyError("Store already set: {}".format(key))
|
||||
super().__setitem__(key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise TypeError("Cannot delete stores from registry")
|
||||
|
||||
|
||||
registry = Registry()
|
||||
@@ -1,5 +1,6 @@
|
||||
import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import pkgutil
|
||||
from collections import OrderedDict
|
||||
|
||||
@@ -91,6 +92,8 @@ class Report(object):
|
||||
self.active_test = None
|
||||
self.failed = False
|
||||
|
||||
self.logger = logging.getLogger(f"netbox.reports.{self.full_name}")
|
||||
|
||||
# Compile test methods and initialize results skeleton
|
||||
test_methods = []
|
||||
for method in dir(self):
|
||||
@@ -117,7 +120,7 @@ class Report(object):
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
return '.'.join([self.module, self.name])
|
||||
return '.'.join([self.__module__, self.__class__.__name__])
|
||||
|
||||
def _log(self, obj, message, level=LOG_DEFAULT):
|
||||
"""
|
||||
@@ -138,6 +141,7 @@ class Report(object):
|
||||
Log a message which is not associated with a particular object.
|
||||
"""
|
||||
self._log(None, message, level=LOG_DEFAULT)
|
||||
self.logger.info(message)
|
||||
|
||||
def log_success(self, obj, message=None):
|
||||
"""
|
||||
@@ -146,6 +150,7 @@ class Report(object):
|
||||
if message:
|
||||
self._log(obj, message, level=LOG_SUCCESS)
|
||||
self._results[self.active_test]['success'] += 1
|
||||
self.logger.info(f"Success | {obj}: {message}")
|
||||
|
||||
def log_info(self, obj, message):
|
||||
"""
|
||||
@@ -153,6 +158,7 @@ class Report(object):
|
||||
"""
|
||||
self._log(obj, message, level=LOG_INFO)
|
||||
self._results[self.active_test]['info'] += 1
|
||||
self.logger.info(f"Info | {obj}: {message}")
|
||||
|
||||
def log_warning(self, obj, message):
|
||||
"""
|
||||
@@ -160,6 +166,7 @@ class Report(object):
|
||||
"""
|
||||
self._log(obj, message, level=LOG_WARNING)
|
||||
self._results[self.active_test]['warning'] += 1
|
||||
self.logger.info(f"Warning | {obj}: {message}")
|
||||
|
||||
def log_failure(self, obj, message):
|
||||
"""
|
||||
@@ -167,12 +174,15 @@ class Report(object):
|
||||
"""
|
||||
self._log(obj, message, level=LOG_FAILURE)
|
||||
self._results[self.active_test]['failure'] += 1
|
||||
self.logger.info(f"Failure | {obj}: {message}")
|
||||
self.failed = True
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Run the report and return its results. Each test method will be executed in order.
|
||||
"""
|
||||
self.logger.info(f"Running report")
|
||||
|
||||
for method_name in self.test_methods:
|
||||
self.active_test = method_name
|
||||
test_method = getattr(self, method_name)
|
||||
@@ -184,6 +194,11 @@ class Report(object):
|
||||
result.save()
|
||||
self.result = result
|
||||
|
||||
if self.failed:
|
||||
self.logger.warning("Report failed")
|
||||
else:
|
||||
self.logger.info("Report completed successfully")
|
||||
|
||||
# Perform any post-run tasks
|
||||
self.post_run()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import time
|
||||
@@ -18,6 +19,7 @@ from ipam.formfields import IPAddressFormField, IPNetworkFormField
|
||||
from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
|
||||
from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_SUCCESS, LOG_WARNING
|
||||
from utilities.exceptions import AbortTransaction
|
||||
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from .forms import ScriptForm
|
||||
from .signals import purge_changelog
|
||||
|
||||
@@ -167,7 +169,7 @@ class ObjectVar(ScriptVariable):
|
||||
"""
|
||||
NetBox object representation. The provided QuerySet will determine the choices available.
|
||||
"""
|
||||
form_field = forms.ModelChoiceField
|
||||
form_field = DynamicModelChoiceField
|
||||
|
||||
def __init__(self, queryset, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -184,7 +186,7 @@ class MultiObjectVar(ScriptVariable):
|
||||
"""
|
||||
Like ObjectVar, but can represent one or more objects.
|
||||
"""
|
||||
form_field = forms.ModelMultipleChoiceField
|
||||
form_field = DynamicModelMultipleChoiceField
|
||||
|
||||
def __init__(self, queryset, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -254,6 +256,7 @@ class BaseScript:
|
||||
def __init__(self):
|
||||
|
||||
# Initiate the log
|
||||
self.logger = logging.getLogger(f"netbox.scripts.{self.module()}.{self.__class__.__name__}")
|
||||
self.log = []
|
||||
|
||||
# Declare the placeholder for the current request
|
||||
@@ -301,18 +304,23 @@ class BaseScript:
|
||||
# Logging
|
||||
|
||||
def log_debug(self, message):
|
||||
self.logger.log(logging.DEBUG, message)
|
||||
self.log.append((LOG_DEFAULT, message))
|
||||
|
||||
def log_success(self, message):
|
||||
self.logger.log(logging.INFO, message) # No syslog equivalent for SUCCESS
|
||||
self.log.append((LOG_SUCCESS, message))
|
||||
|
||||
def log_info(self, message):
|
||||
self.logger.log(logging.INFO, message)
|
||||
self.log.append((LOG_INFO, message))
|
||||
|
||||
def log_warning(self, message):
|
||||
self.logger.log(logging.WARNING, message)
|
||||
self.log.append((LOG_WARNING, message))
|
||||
|
||||
def log_failure(self, message):
|
||||
self.logger.log(logging.ERROR, message)
|
||||
self.log.append((LOG_FAILURE, message))
|
||||
|
||||
# Convenience functions
|
||||
@@ -375,6 +383,10 @@ def run_script(script, data, request, commit=True):
|
||||
start_time = None
|
||||
end_time = None
|
||||
|
||||
script_name = script.__class__.__name__
|
||||
logger = logging.getLogger(f"netbox.scripts.{script.module()}.{script_name}")
|
||||
logger.info(f"Running script (commit={commit})")
|
||||
|
||||
# Add files to form data
|
||||
files = request.FILES
|
||||
for field_name, fileobj in files.items():
|
||||
@@ -404,6 +416,7 @@ def run_script(script, data, request, commit=True):
|
||||
script.log_failure(
|
||||
"An exception occurred: `{}: {}`\n```\n{}\n```".format(type(e).__name__, e, stacktrace)
|
||||
)
|
||||
logger.error(f"Exception raised during script execution: {e}")
|
||||
commit = False
|
||||
finally:
|
||||
if not commit:
|
||||
@@ -416,6 +429,7 @@ def run_script(script, data, request, commit=True):
|
||||
# Calculate execution time
|
||||
if end_time is not None:
|
||||
execution_time = end_time - start_time
|
||||
logger.info(f"Script completed in {execution_time:.4f} seconds")
|
||||
else:
|
||||
execution_time = None
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class TagTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Tag
|
||||
fields = ('pk', 'name', 'items', 'slug', 'color', 'actions')
|
||||
fields = ('pk', 'name', 'items', 'slug', 'color', 'description', 'actions')
|
||||
|
||||
|
||||
class TaggedItemTable(BaseTable):
|
||||
@@ -104,7 +104,11 @@ class ConfigContextTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConfigContext
|
||||
fields = ('pk', 'name', 'weight', 'is_active', 'description')
|
||||
fields = (
|
||||
'pk', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles', 'platforms',
|
||||
'cluster_groups', 'clusters', 'tenant_groups', 'tenants',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'weight', 'is_active', 'description')
|
||||
|
||||
|
||||
class ObjectChangeTable(BaseTable):
|
||||
|
||||
75
netbox/extras/templatetags/plugins.py
Normal file
75
netbox/extras/templatetags/plugins.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from django import template as template_
|
||||
from django.conf import settings
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from extras.plugins import PluginTemplateExtension
|
||||
from extras.registry import registry
|
||||
|
||||
register = template_.Library()
|
||||
|
||||
|
||||
def _get_registered_content(obj, method, template_context):
|
||||
"""
|
||||
Given an object and a PluginTemplateExtension method name and the template context, return all the
|
||||
registered content for the object's model.
|
||||
"""
|
||||
html = ''
|
||||
context = {
|
||||
'object': obj,
|
||||
'request': template_context['request'],
|
||||
'settings': template_context['settings'],
|
||||
'csrf_token': template_context['csrf_token'],
|
||||
'perms': template_context['perms'],
|
||||
}
|
||||
|
||||
model_name = obj._meta.label_lower
|
||||
template_extensions = registry['plugin_template_extensions'].get(model_name, [])
|
||||
for template_extension in template_extensions:
|
||||
|
||||
# If the class has not overridden the specified method, we can skip it (because we know it
|
||||
# will raise NotImplementedError).
|
||||
if getattr(template_extension, method) == getattr(PluginTemplateExtension, method):
|
||||
continue
|
||||
|
||||
# Update context with plugin-specific configuration parameters
|
||||
plugin_name = template_extension.__module__.split('.')[0]
|
||||
context['config'] = settings.PLUGINS_CONFIG.get(plugin_name, {})
|
||||
|
||||
# Call the method to render content
|
||||
instance = template_extension(context)
|
||||
content = getattr(instance, method)()
|
||||
html += content
|
||||
|
||||
return mark_safe(html)
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def plugin_buttons(context, obj):
|
||||
"""
|
||||
Render all buttons registered by plugins
|
||||
"""
|
||||
return _get_registered_content(obj, 'buttons', context)
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def plugin_left_page(context, obj):
|
||||
"""
|
||||
Render all left page content registered by plugins
|
||||
"""
|
||||
return _get_registered_content(obj, 'left_page', context)
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def plugin_right_page(context, obj):
|
||||
"""
|
||||
Render all right page content registered by plugins
|
||||
"""
|
||||
return _get_registered_content(obj, 'right_page', context)
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def plugin_full_width_page(context, obj):
|
||||
"""
|
||||
Render all full width page content registered by plugins
|
||||
"""
|
||||
return _get_registered_content(obj, 'full_width_page', context)
|
||||
17
netbox/extras/tests/dummy_plugin/__init__.py
Normal file
17
netbox/extras/tests/dummy_plugin/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from extras.plugins import PluginConfig
|
||||
|
||||
|
||||
class DummyPluginConfig(PluginConfig):
|
||||
name = 'extras.tests.dummy_plugin'
|
||||
verbose_name = 'Dummy plugin'
|
||||
version = '0.0'
|
||||
description = 'For testing purposes only'
|
||||
base_url = 'dummy-plugin'
|
||||
min_version = '1.0'
|
||||
max_version = '9.0'
|
||||
middleware = [
|
||||
'extras.tests.dummy_plugin.middleware.DummyMiddleware'
|
||||
]
|
||||
|
||||
|
||||
config = DummyPluginConfig
|
||||
9
netbox/extras/tests/dummy_plugin/admin.py
Normal file
9
netbox/extras/tests/dummy_plugin/admin.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from netbox.admin import admin_site
|
||||
from .models import DummyModel
|
||||
|
||||
|
||||
@admin.register(DummyModel, site=admin_site)
|
||||
class DummyModelAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'number')
|
||||
9
netbox/extras/tests/dummy_plugin/api/serializers.py
Normal file
9
netbox/extras/tests/dummy_plugin/api/serializers.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from extras.tests.dummy_plugin.models import DummyModel
|
||||
|
||||
|
||||
class DummySerializer(ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DummyModel
|
||||
fields = ('id', 'name', 'number')
|
||||
6
netbox/extras/tests/dummy_plugin/api/urls.py
Normal file
6
netbox/extras/tests/dummy_plugin/api/urls.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from rest_framework import routers
|
||||
from .views import DummyViewSet
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register('dummy-models', DummyViewSet)
|
||||
urlpatterns = router.urls
|
||||
8
netbox/extras/tests/dummy_plugin/api/views.py
Normal file
8
netbox/extras/tests/dummy_plugin/api/views.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from extras.tests.dummy_plugin.models import DummyModel
|
||||
from .serializers import DummySerializer
|
||||
|
||||
|
||||
class DummyViewSet(ModelViewSet):
|
||||
queryset = DummyModel.objects.all()
|
||||
serializer_class = DummySerializer
|
||||
7
netbox/extras/tests/dummy_plugin/middleware.py
Normal file
7
netbox/extras/tests/dummy_plugin/middleware.py
Normal file
@@ -0,0 +1,7 @@
|
||||
class DummyMiddleware:
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
return self.get_response(request)
|
||||
23
netbox/extras/tests/dummy_plugin/migrations/0001_initial.py
Normal file
23
netbox/extras/tests/dummy_plugin/migrations/0001_initial.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DummyModel',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=20)),
|
||||
('number', models.IntegerField(default=100)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
]
|
||||
13
netbox/extras/tests/dummy_plugin/models.py
Normal file
13
netbox/extras/tests/dummy_plugin/models.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class DummyModel(models.Model):
|
||||
name = models.CharField(
|
||||
max_length=20
|
||||
)
|
||||
number = models.IntegerField(
|
||||
default=100
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
25
netbox/extras/tests/dummy_plugin/navigation.py
Normal file
25
netbox/extras/tests/dummy_plugin/navigation.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from extras.plugins import PluginMenuButton, PluginMenuItem
|
||||
|
||||
|
||||
menu_items = (
|
||||
PluginMenuItem(
|
||||
link='plugins:dummy_plugin:dummy_models',
|
||||
link_text='Item 1',
|
||||
buttons=(
|
||||
PluginMenuButton(
|
||||
link='admin:dummy_plugin_dummymodel_add',
|
||||
title='Add a new dummy model',
|
||||
icon_class='fa fa-plus',
|
||||
),
|
||||
PluginMenuButton(
|
||||
link='admin:dummy_plugin_dummymodel_add',
|
||||
title='Add a new dummy model',
|
||||
icon_class='fa fa-plus',
|
||||
),
|
||||
)
|
||||
),
|
||||
PluginMenuItem(
|
||||
link='plugins:dummy_plugin:dummy_models',
|
||||
link_text='Item 2',
|
||||
),
|
||||
)
|
||||
20
netbox/extras/tests/dummy_plugin/template_content.py
Normal file
20
netbox/extras/tests/dummy_plugin/template_content.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from extras.plugins import PluginTemplateExtension
|
||||
|
||||
|
||||
class SiteContent(PluginTemplateExtension):
|
||||
model = 'dcim.site'
|
||||
|
||||
def left_page(self):
|
||||
return "SITE CONTENT - LEFT PAGE"
|
||||
|
||||
def right_page(self):
|
||||
return "SITE CONTENT - RIGHT PAGE"
|
||||
|
||||
def full_width_page(self):
|
||||
return "SITE CONTENT - FULL WIDTH PAGE"
|
||||
|
||||
def full_buttons(self):
|
||||
return "SITE CONTENT - BUTTONS"
|
||||
|
||||
|
||||
template_extensions = [SiteContent]
|
||||
8
netbox/extras/tests/dummy_plugin/urls.py
Normal file
8
netbox/extras/tests/dummy_plugin/urls.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = (
|
||||
path('models/', views.DummyModelsView.as_view(), name='dummy_models'),
|
||||
)
|
||||
11
netbox/extras/tests/dummy_plugin/views.py
Normal file
11
netbox/extras/tests/dummy_plugin/views.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.http import HttpResponse
|
||||
from django.views.generic import View
|
||||
|
||||
from .models import DummyModel
|
||||
|
||||
|
||||
class DummyModelsView(View):
|
||||
|
||||
def get(self, request):
|
||||
instance_count = DummyModel.objects.count()
|
||||
return HttpResponse(f"Instances: {instance_count}")
|
||||
@@ -7,12 +7,11 @@ from rest_framework import status
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Rack, RackGroup, RackRole, Region, Site
|
||||
from extras.api.views import ScriptViewSet
|
||||
from extras.choices import *
|
||||
from extras.constants import GRAPH_MODELS
|
||||
from extras.models import ConfigContext, Graph, ExportTemplate, Tag
|
||||
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
|
||||
from extras.utils import FeatureQuery
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.testing import APITestCase, choices_to_dict
|
||||
from utilities.testing import APITestCase
|
||||
|
||||
|
||||
class AppTest(APITestCase):
|
||||
@@ -24,27 +23,6 @@ class AppTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_choices(self):
|
||||
|
||||
url = reverse('extras-api:field-choice-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# ExportTemplate
|
||||
self.assertEqual(choices_to_dict(response.data.get('export-template:template_language')), TemplateLanguageChoices.as_dict())
|
||||
|
||||
# Graph
|
||||
content_types = ContentType.objects.filter(GRAPH_MODELS)
|
||||
graph_type_choices = {
|
||||
"{}.{}".format(ct.app_label, ct.model): ct.name for ct in content_types
|
||||
}
|
||||
self.assertEqual(choices_to_dict(response.data.get('graph:type')), graph_type_choices)
|
||||
self.assertEqual(choices_to_dict(response.data.get('graph:template_language')), TemplateLanguageChoices.as_dict())
|
||||
|
||||
# ObjectChange
|
||||
self.assertEqual(choices_to_dict(response.data.get('object-change:action')), ObjectChangeActionChoices.as_dict())
|
||||
|
||||
|
||||
class GraphTest(APITestCase):
|
||||
|
||||
@@ -402,8 +380,10 @@ class ConfigContextTest(APITestCase):
|
||||
role2 = DeviceRole.objects.create(name='Test Role 2', slug='test-role-2')
|
||||
platform1 = Platform.objects.create(name='Test Platform 1', slug='test-platform-1')
|
||||
platform2 = Platform.objects.create(name='Test Platform 2', slug='test-platform-2')
|
||||
tenantgroup1 = TenantGroup.objects.create(name='Test Tenant Group 1', slug='test-tenant-group-1')
|
||||
tenantgroup2 = TenantGroup.objects.create(name='Test Tenant Group 2', slug='test-tenant-group-2')
|
||||
tenantgroup1 = TenantGroup(name='Test Tenant Group 1', slug='test-tenant-group-1')
|
||||
tenantgroup1.save()
|
||||
tenantgroup2 = TenantGroup(name='Test Tenant Group 2', slug='test-tenant-group-2')
|
||||
tenantgroup2.save()
|
||||
tenant1 = Tenant.objects.create(name='Test Tenant 1', slug='test-tenant-1')
|
||||
tenant2 = Tenant.objects.create(name='Test Tenant 2', slug='test-tenant-2')
|
||||
tag1 = Tag.objects.create(name='Test Tag 1', slug='test-tag-1')
|
||||
|
||||
@@ -99,6 +99,19 @@ class CustomFieldTest(TestCase):
|
||||
cf.delete()
|
||||
|
||||
|
||||
class CustomFieldManagerTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
content_type = ContentType.objects.get_for_model(Site)
|
||||
custom_field = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo')
|
||||
custom_field.save()
|
||||
custom_field.obj_type.set([content_type])
|
||||
|
||||
def test_get_for_model(self):
|
||||
self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1)
|
||||
self.assertEqual(CustomField.objects.get_for_model(VirtualMachine).count(), 0)
|
||||
|
||||
|
||||
class CustomFieldAPITest(APITestCase):
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -3,9 +3,9 @@ from django.test import TestCase
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from extras.choices import *
|
||||
from extras.constants import GRAPH_MODELS
|
||||
from extras.filters import *
|
||||
from extras.models import ConfigContext, ExportTemplate, Graph
|
||||
from extras.utils import FeatureQuery
|
||||
from extras.models import ConfigContext, ExportTemplate, Graph, Tag
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
||||
@@ -18,7 +18,7 @@ class GraphTestCase(TestCase):
|
||||
def setUpTestData(cls):
|
||||
|
||||
# Get the first three available types
|
||||
content_types = ContentType.objects.filter(GRAPH_MODELS)[:3]
|
||||
content_types = ContentType.objects.filter(FeatureQuery('graphs').get_query())[:3]
|
||||
|
||||
graphs = (
|
||||
Graph(name='Graph 1', type=content_types[0], template_language=TemplateLanguageChoices.LANGUAGE_DJANGO, source='http://example.com/1'),
|
||||
@@ -27,16 +27,19 @@ class GraphTestCase(TestCase):
|
||||
)
|
||||
Graph.objects.bulk_create(graphs)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Graph 1', 'Graph 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_type(self):
|
||||
content_type = ContentType.objects.filter(GRAPH_MODELS).first()
|
||||
content_type = ContentType.objects.filter(FeatureQuery('graphs').get_query()).first()
|
||||
params = {'type': content_type.pk}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
# TODO: Remove in v2.8
|
||||
def test_template_language(self):
|
||||
params = {'template_language': TemplateLanguageChoices.LANGUAGE_JINJA2}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -58,6 +61,10 @@ class ExportTemplateTestCase(TestCase):
|
||||
)
|
||||
ExportTemplate.objects.bulk_create(export_templates)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Export Template 1', 'Export Template 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -128,7 +135,8 @@ class ConfigContextTestCase(TestCase):
|
||||
TenantGroup(name='Tenant Group 2', slug='tenant-group-2'),
|
||||
TenantGroup(name='Tenant Group 3', slug='tenant-group-3'),
|
||||
)
|
||||
TenantGroup.objects.bulk_create(tenant_groups)
|
||||
for tenantgroup in tenant_groups:
|
||||
tenantgroup.save()
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||
@@ -153,6 +161,10 @@ class ConfigContextTestCase(TestCase):
|
||||
c.tenant_groups.set([tenant_groups[i]])
|
||||
c.tenants.set([tenants[i]])
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Config Context 1', 'Config Context 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -218,4 +230,35 @@ class ConfigContextTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class TagTestCase(TestCase):
|
||||
queryset = Tag.objects.all()
|
||||
filterset = TagFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
tags = (
|
||||
Tag(name='Tag 1', slug='tag-1', color='ff0000'),
|
||||
Tag(name='Tag 2', slug='tag-2', color='00ff00'),
|
||||
Tag(name='Tag 3', slug='tag-3', color='0000ff'),
|
||||
)
|
||||
Tag.objects.bulk_create(tags)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Tag 1', 'Tag 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['tag-1', 'tag-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_color(self):
|
||||
params = {'color': ['ff0000', '00ff00']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
# TODO: ObjectChangeFilter test
|
||||
|
||||
136
netbox/extras/tests/test_plugins.py
Normal file
136
netbox/extras/tests/test_plugins.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from unittest import skipIf
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import Client, TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from extras.registry import registry
|
||||
from extras.tests.dummy_plugin import config as dummy_config
|
||||
|
||||
|
||||
@skipIf('extras.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS")
|
||||
class PluginTest(TestCase):
|
||||
|
||||
def test_config(self):
|
||||
|
||||
self.assertIn('extras.tests.dummy_plugin.DummyPluginConfig', settings.INSTALLED_APPS)
|
||||
|
||||
def test_models(self):
|
||||
from extras.tests.dummy_plugin.models import DummyModel
|
||||
|
||||
# Test saving an instance
|
||||
instance = DummyModel(name='Instance 1', number=100)
|
||||
instance.save()
|
||||
self.assertIsNotNone(instance.pk)
|
||||
|
||||
# Test deleting an instance
|
||||
instance.delete()
|
||||
self.assertIsNone(instance.pk)
|
||||
|
||||
def test_admin(self):
|
||||
|
||||
# Test admin view URL resolution
|
||||
url = reverse('admin:dummy_plugin_dummymodel_add')
|
||||
self.assertEqual(url, '/admin/dummy_plugin/dummymodel/add/')
|
||||
|
||||
def test_views(self):
|
||||
|
||||
# Test URL resolution
|
||||
url = reverse('plugins:dummy_plugin:dummy_models')
|
||||
self.assertEqual(url, '/plugins/dummy-plugin/models/')
|
||||
|
||||
# Test GET request
|
||||
client = Client()
|
||||
response = client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_api_views(self):
|
||||
|
||||
# Test URL resolution
|
||||
url = reverse('plugins-api:dummy_plugin-api:dummymodel-list')
|
||||
self.assertEqual(url, '/api/plugins/dummy-plugin/dummy-models/')
|
||||
|
||||
# Test GET request
|
||||
client = Client()
|
||||
response = client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_menu_items(self):
|
||||
"""
|
||||
Check that plugin MenuItems and MenuButtons are registered.
|
||||
"""
|
||||
self.assertIn('Dummy plugin', registry['plugin_menu_items'])
|
||||
menu_items = registry['plugin_menu_items']['Dummy plugin']
|
||||
self.assertEqual(len(menu_items), 2)
|
||||
self.assertEqual(len(menu_items[0].buttons), 2)
|
||||
|
||||
def test_template_extensions(self):
|
||||
"""
|
||||
Check that plugin TemplateExtensions are registered.
|
||||
"""
|
||||
from extras.tests.dummy_plugin.template_content import SiteContent
|
||||
|
||||
self.assertIn(SiteContent, registry['plugin_template_extensions']['dcim.site'])
|
||||
|
||||
def test_middleware(self):
|
||||
"""
|
||||
Check that plugin middleware is registered.
|
||||
"""
|
||||
self.assertIn('extras.tests.dummy_plugin.middleware.DummyMiddleware', settings.MIDDLEWARE)
|
||||
|
||||
def test_caching_config(self):
|
||||
"""
|
||||
Check that plugin caching configuration is registered.
|
||||
"""
|
||||
self.assertIn('extras.tests.dummy_plugin.*', settings.CACHEOPS)
|
||||
|
||||
@override_settings(VERSION='0.9')
|
||||
def test_min_version(self):
|
||||
"""
|
||||
Check enforcement of minimum NetBox version.
|
||||
"""
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
dummy_config.validate({})
|
||||
|
||||
@override_settings(VERSION='10.0')
|
||||
def test_max_version(self):
|
||||
"""
|
||||
Check enforcement of maximum NetBox version.
|
||||
"""
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
dummy_config.validate({})
|
||||
|
||||
def test_required_settings(self):
|
||||
"""
|
||||
Validate enforcement of required settings.
|
||||
"""
|
||||
class DummyConfigWithRequiredSettings(dummy_config):
|
||||
required_settings = ['foo']
|
||||
|
||||
# Validation should pass when all required settings are present
|
||||
DummyConfigWithRequiredSettings.validate({'foo': True})
|
||||
|
||||
# Validation should fail when a required setting is missing
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
DummyConfigWithRequiredSettings.validate({})
|
||||
|
||||
def test_default_settings(self):
|
||||
"""
|
||||
Validate population of default config settings.
|
||||
"""
|
||||
class DummyConfigWithDefaultSettings(dummy_config):
|
||||
default_settings = {
|
||||
'bar': 123,
|
||||
}
|
||||
|
||||
# Populate the default value if setting has not been specified
|
||||
user_config = {}
|
||||
DummyConfigWithDefaultSettings.validate(user_config)
|
||||
self.assertEqual(user_config['bar'], 123)
|
||||
|
||||
# Don't overwrite specified values
|
||||
user_config = {'bar': 456}
|
||||
DummyConfigWithDefaultSettings.validate(user_config)
|
||||
self.assertEqual(user_config['bar'], 456)
|
||||
33
netbox/extras/tests/test_registry.py
Normal file
33
netbox/extras/tests/test_registry.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from extras.registry import Registry
|
||||
|
||||
|
||||
class RegistryTest(TestCase):
|
||||
|
||||
def test_add_store(self):
|
||||
reg = Registry()
|
||||
reg['foo'] = 123
|
||||
|
||||
self.assertEqual(reg['foo'], 123)
|
||||
|
||||
def test_manipulate_store(self):
|
||||
reg = Registry()
|
||||
reg['foo'] = [1, 2]
|
||||
reg['foo'].append(3)
|
||||
|
||||
self.assertListEqual(reg['foo'], [1, 2, 3])
|
||||
|
||||
def test_overwrite_store(self):
|
||||
reg = Registry()
|
||||
reg['foo'] = 123
|
||||
|
||||
with self.assertRaises(KeyError):
|
||||
reg['foo'] = 456
|
||||
|
||||
def test_delete_store(self):
|
||||
reg = Registry()
|
||||
reg['foo'] = 123
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
del(reg['foo'])
|
||||
@@ -102,7 +102,7 @@ class WebhookTest(APITestCase):
|
||||
|
||||
request_id = uuid.uuid4()
|
||||
|
||||
def dummy_send(_, request):
|
||||
def dummy_send(_, request, **kwargs):
|
||||
"""
|
||||
A dummy implementation of Session.send() to be used for testing.
|
||||
Always returns a 200 HTTP response.
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import collections
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from taggit.managers import _TaggableManager
|
||||
from utilities.querysets import DummyQuerySet
|
||||
|
||||
from extras.constants import EXTRAS_FEATURES
|
||||
from extras.registry import registry
|
||||
|
||||
|
||||
def is_taggable(obj):
|
||||
"""
|
||||
@@ -13,3 +20,62 @@ def is_taggable(obj):
|
||||
if isinstance(obj.tags, DummyQuerySet):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def image_upload(instance, filename):
|
||||
"""
|
||||
Return a path for uploading image attchments.
|
||||
"""
|
||||
path = 'image-attachments/'
|
||||
|
||||
# Rename the file to the provided name, if any. Attempt to preserve the file extension.
|
||||
extension = filename.rsplit('.')[-1].lower()
|
||||
if instance.name and extension in ['bmp', 'gif', 'jpeg', 'jpg', 'png']:
|
||||
filename = '.'.join([instance.name, extension])
|
||||
elif instance.name:
|
||||
filename = instance.name
|
||||
|
||||
return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
|
||||
|
||||
|
||||
@deconstructible
|
||||
class FeatureQuery:
|
||||
"""
|
||||
Helper class that delays evaluation of the registry contents for the functionality store
|
||||
until it has been populated.
|
||||
"""
|
||||
def __init__(self, feature):
|
||||
self.feature = feature
|
||||
|
||||
def __call__(self):
|
||||
return self.get_query()
|
||||
|
||||
def get_query(self):
|
||||
"""
|
||||
Given an extras feature, return a Q object for content type lookup
|
||||
"""
|
||||
query = Q()
|
||||
for app_label, models in registry['model_features'][self.feature].items():
|
||||
query |= Q(app_label=app_label, model__in=models)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def extras_features(*features):
|
||||
"""
|
||||
Decorator used to register extras provided features to a model
|
||||
"""
|
||||
def wrapper(model_class):
|
||||
# Initialize the model_features store if not already defined
|
||||
if 'model_features' not in registry:
|
||||
registry['model_features'] = {
|
||||
f: collections.defaultdict(list) for f in EXTRAS_FEATURES
|
||||
}
|
||||
for feature in features:
|
||||
if feature in EXTRAS_FEATURES:
|
||||
app_label, model_name = model_class._meta.label_lower.split('.')
|
||||
registry['model_features'][feature][app_label].append(model_name)
|
||||
else:
|
||||
raise ValueError('{} is not a valid extras feature!'.format(feature))
|
||||
return model_class
|
||||
return wrapper
|
||||
|
||||
@@ -119,11 +119,21 @@ class ConfigContextView(PermissionRequiredMixin, View):
|
||||
permission_required = 'extras.view_configcontext'
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
configcontext = get_object_or_404(ConfigContext, pk=pk)
|
||||
|
||||
# Determine user's preferred output format
|
||||
if request.GET.get('format') in ['json', 'yaml']:
|
||||
format = request.GET.get('format')
|
||||
if request.user.is_authenticated:
|
||||
request.user.config.set('extras.configcontext.format', format, commit=True)
|
||||
elif request.user.is_authenticated:
|
||||
format = request.user.config.get('extras.configcontext.format', 'json')
|
||||
else:
|
||||
format = 'json'
|
||||
|
||||
return render(request, 'extras/configcontext.html', {
|
||||
'configcontext': configcontext,
|
||||
'format': format,
|
||||
})
|
||||
|
||||
|
||||
@@ -171,11 +181,22 @@ class ObjectConfigContextView(View):
|
||||
source_contexts = ConfigContext.objects.get_for_object(obj)
|
||||
model_name = self.object_class._meta.model_name
|
||||
|
||||
# Determine user's preferred output format
|
||||
if request.GET.get('format') in ['json', 'yaml']:
|
||||
format = request.GET.get('format')
|
||||
if request.user.is_authenticated:
|
||||
request.user.config.set('extras.configcontext.format', format, commit=True)
|
||||
elif request.user.is_authenticated:
|
||||
format = request.user.config.get('extras.configcontext.format', 'json')
|
||||
else:
|
||||
format = 'json'
|
||||
|
||||
return render(request, 'extras/object_configcontext.html', {
|
||||
model_name: obj,
|
||||
'obj': obj,
|
||||
'rendered_context': obj.get_config_context(),
|
||||
'source_contexts': source_contexts,
|
||||
'format': format,
|
||||
'base_template': self.base_template,
|
||||
'active_tab': 'config-context',
|
||||
})
|
||||
@@ -274,7 +295,7 @@ class ObjectChangeLogView(View):
|
||||
template.loader.get_template(base_template)
|
||||
object_var = model._meta.model_name
|
||||
except template.TemplateDoesNotExist:
|
||||
base_template = '_base.html'
|
||||
base_template = 'base.html'
|
||||
object_var = 'obj'
|
||||
|
||||
return render(request, 'extras/object_changelog.html', {
|
||||
|
||||
@@ -3,11 +3,12 @@ import hmac
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils import timezone
|
||||
from django_rq import get_queue
|
||||
|
||||
from extras.models import Webhook
|
||||
from utilities.api import get_serializer_for_model
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .utils import FeatureQuery
|
||||
|
||||
|
||||
def generate_signature(request_body, secret):
|
||||
@@ -16,7 +17,7 @@ def generate_signature(request_body, secret):
|
||||
"""
|
||||
hmac_prep = hmac.new(
|
||||
key=secret.encode('utf8'),
|
||||
msg=request_body.encode('utf8'),
|
||||
msg=request_body,
|
||||
digestmod=hashlib.sha512
|
||||
)
|
||||
return hmac_prep.hexdigest()
|
||||
@@ -29,7 +30,7 @@ def enqueue_webhooks(instance, user, request_id, action):
|
||||
"""
|
||||
obj_type = ContentType.objects.get_for_model(instance.__class__)
|
||||
|
||||
webhook_models = ContentType.objects.filter(WEBHOOK_MODELS)
|
||||
webhook_models = ContentType.objects.filter(FeatureQuery('webhooks').get_query())
|
||||
if obj_type not in webhook_models:
|
||||
return
|
||||
|
||||
@@ -49,12 +50,8 @@ def enqueue_webhooks(instance, user, request_id, action):
|
||||
}
|
||||
serializer = serializer_class(instance, context=serializer_context)
|
||||
|
||||
# We must only import django_rq if the Webhooks feature is enabled.
|
||||
# Only if we have gotten to ths point, is the feature enabled
|
||||
from django_rq import get_queue
|
||||
# Enqueue the webhooks
|
||||
webhook_queue = get_queue('default')
|
||||
|
||||
# enqueue the webhooks:
|
||||
for webhook in webhooks:
|
||||
webhook_queue.enqueue(
|
||||
"extras.webhooks_worker.process_webhook",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user