Compare commits

...

22 Commits

Author SHA1 Message Date
Jeremy Stretch
1e04bd3c79 Release v2.1-beta 2017-07-10 13:15:52 -04:00
Jeremy Stretch
2d0638821d #1266: Exclude interfaces with existing connections or circuit terminations when creating a new connection 2017-07-10 12:44:16 -04:00
Jeremy Stretch
8a87d60f29 Closes #1269: Added circuit termination to interface serializer 2017-07-10 12:07:47 -04:00
Jeremy Stretch
530789b733 #1269: Reworked interface connection serialization 2017-07-10 11:52:36 -04:00
Jeremy Stretch
618d75566f Merge branch 'develop' into develop-2.1
Conflicts:
	netbox/netbox/settings.py
2017-07-10 09:48:20 -04:00
Jeremy Stretch
1f9806a480 Fixes #1285: Enforce model validation when creating/editing objects via the API 2017-07-06 17:37:24 -04:00
Jeremy Stretch
5b43a108bc Merge branch 'develop' into develop-2.1
Conflicts:
	netbox/ipam/models.py
	netbox/netbox/settings.py
	netbox/templates/dcim/inc/interface.html
2017-07-06 13:27:13 -04:00
Jeremy Stretch
30d1605007 Closes #1246: Added ability to auto-create the next available IP address within a prefix 2017-06-30 16:51:31 -04:00
Jeremy Stretch
a23da9f867 PEP8 fixes 2017-06-28 16:25:36 -04:00
Jeremy Stretch
d5bb37b552 #1246: Initial work on an API endpoint to retrieve available IPs for a prefix 2017-06-28 16:23:17 -04:00
Jeremy Stretch
5940feb64b Closes #1121: Added asset_tag and description fields to inventory items 2017-06-23 17:05:37 -04:00
Jeremy Stretch
229e6809d8 Closes #1041: Added enabled and MTU fields to the interface model 2017-06-23 14:04:15 -04:00
Jeremy Stretch
87e5687d03 Closes #1203: Implemented query filters for all models 2017-06-19 16:10:18 -04:00
Jeremy Stretch
68ebe85a98 Closes #1218: Added IEEE 802.11 wireless interface types 2017-06-16 17:52:09 -04:00
Jeremy Stretch
789ac5dfd4 Combined mgmt and non-mgmt interfaces into same list on device and device type views 2017-06-16 17:13:33 -04:00
Jeremy Stretch
ceb8fee0cc Moved constant definitions from models.py to constants.py 2017-06-16 16:01:44 -04:00
Jeremy Stretch
421270f4a6 Renamed IP address status 'virtual' to 'VIP' 2017-06-16 15:37:46 -04:00
Jeremy Stretch
ba8f48af65 Merge branch 'develop' into develop-2.1
Conflicts:
	netbox/netbox/settings.py
2017-06-15 14:26:02 -04:00
Jeremy Stretch
f427c00d94 Closes #819: Implemented IP address functional roles 2017-06-14 16:11:13 -04:00
Jeremy Stretch
8bcd8c404d Closes #1141: Include VRF name and RD in form selections 2017-06-14 15:00:27 -04:00
Jeremy Stretch
4d7f9c42c8 Version bump for v2.1 2017-06-14 14:55:59 -04:00
Jeremy Stretch
cd263484c3 Fixes #1079: Order interfaces naturally via API 2017-06-14 14:34:14 -04:00
48 changed files with 1164 additions and 620 deletions

View File

@@ -6,6 +6,7 @@ from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ModelValidationMixin
# #
@@ -44,7 +45,7 @@ class WritableProviderSerializer(CustomFieldModelSerializer):
# Circuit types # Circuit types
# #
class CircuitTypeSerializer(serializers.ModelSerializer): class CircuitTypeSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = CircuitType model = CircuitType
@@ -110,7 +111,7 @@ class CircuitTerminationSerializer(serializers.ModelSerializer):
] ]
class WritableCircuitTerminationSerializer(serializers.ModelSerializer): class WritableCircuitTerminationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination

View File

@@ -43,6 +43,7 @@ class ProviderViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
class CircuitTypeViewSet(ModelViewSet): class CircuitTypeViewSet(ModelViewSet):
queryset = CircuitType.objects.all() queryset = CircuitType.objects.all()
serializer_class = serializers.CircuitTypeSerializer serializer_class = serializers.CircuitTypeSerializer
filter_class = filters.CircuitTypeFilter
# #

View File

@@ -0,0 +1,10 @@
from __future__ import unicode_literals
# CircuitTermination sides
TERM_SIDE_A = 'A'
TERM_SIDE_Z = 'Z'
TERM_SIDE_CHOICES = (
(TERM_SIDE_A, 'A'),
(TERM_SIDE_Z, 'Z'),
)

View File

@@ -31,7 +31,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Provider model = Provider
fields = ['name', 'account', 'asn'] fields = ['name', 'slug', 'asn', 'account']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -39,10 +39,19 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.filter( return queryset.filter(
Q(name__icontains=value) | Q(name__icontains=value) |
Q(account__icontains=value) | Q(account__icontains=value) |
Q(noc_contact__icontains=value) |
Q(admin_contact__icontains=value) |
Q(comments__icontains=value) Q(comments__icontains=value)
) )
class CircuitTypeFilter(django_filters.FilterSet):
class Meta:
model = CircuitType
fields = ['name', 'slug']
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in') id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter( q = django_filters.CharFilter(
@@ -50,7 +59,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search', label='Search',
) )
provider_id = django_filters.ModelMultipleChoiceFilter( provider_id = django_filters.ModelMultipleChoiceFilter(
name='provider',
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
label='Provider (ID)', label='Provider (ID)',
) )
@@ -61,7 +69,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Provider (slug)', label='Provider (slug)',
) )
type_id = django_filters.ModelMultipleChoiceFilter( type_id = django_filters.ModelMultipleChoiceFilter(
name='type',
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
label='Circuit type (ID)', label='Circuit type (ID)',
) )
@@ -72,7 +79,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Circuit type (slug)', label='Circuit type (slug)',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
@@ -96,7 +102,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Circuit model = Circuit
fields = ['install_date'] fields = ['cid', 'install_date', 'commit_rate']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -111,12 +117,34 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
class CircuitTerminationFilter(django_filters.FilterSet): class CircuitTerminationFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
circuit_id = django_filters.ModelMultipleChoiceFilter( circuit_id = django_filters.ModelMultipleChoiceFilter(
name='circuit',
queryset=Circuit.objects.all(), queryset=Circuit.objects.all(),
label='Circuit', label='Circuit',
) )
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = ['term_side', 'site'] fields = ['term_side', 'port_speed', 'upstream_speed', 'xconnect_id']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(circuit__cid__icontains=value) |
Q(xconnect_id__icontains=value) |
Q(pp_info__icontains=value)
).distinct()

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
from django import forms from django import forms
from django.db.models import Count from django.db.models import Count
from dcim.models import Site, Device, Interface, Rack, VIRTUAL_IFACE_TYPES from dcim.models import Site, Device, Interface, Rack
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from tenancy.models import Tenant from tenancy.models import Tenant
@@ -210,7 +210,7 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
) )
) )
interface = ChainedModelChoiceField( interface = ChainedModelChoiceField(
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related( queryset=Interface.objects.connectable().select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b' 'circuit_termination', 'connected_as_a', 'connected_as_b'
), ),
chains=( chains=(
@@ -220,7 +220,7 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
label='Interface', label='Interface',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{device}}&type=physical', api_url='/api/dcim/interfaces/?device_id={{device}}&type=physical',
disabled_indicator='connection' disabled_indicator='is_connected'
) )
) )

View File

@@ -10,14 +10,7 @@ from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.utils import csv_format from utilities.utils import csv_format
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from .constants import *
TERM_SIDE_A = 'A'
TERM_SIDE_Z = 'Z'
TERM_SIDE_CHOICES = (
(TERM_SIDE_A, 'A'),
(TERM_SIDE_Z, 'Z'),
)
def humanize_speed(speed): def humanize_speed(speed):

View File

@@ -1,19 +1,21 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator
from ipam.models import IPAddress from ipam.models import IPAddress
from circuits.models import Circuit, CircuitTermination
from dcim.models import ( from dcim.models import (
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface, DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES, RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
) )
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
# #
@@ -36,7 +38,7 @@ class RegionSerializer(serializers.ModelSerializer):
fields = ['id', 'name', 'slug', 'parent'] fields = ['id', 'name', 'slug', 'parent']
class WritableRegionSerializer(serializers.ModelSerializer): class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Region model = Region
@@ -98,7 +100,7 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name', 'slug'] fields = ['id', 'url', 'name', 'slug']
class WritableRackGroupSerializer(serializers.ModelSerializer): class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = RackGroup model = RackGroup
@@ -109,7 +111,7 @@ class WritableRackGroupSerializer(serializers.ModelSerializer):
# Rack roles # Rack roles
# #
class RackRoleSerializer(serializers.ModelSerializer): class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = RackRole model = RackRole
@@ -174,6 +176,9 @@ class WritableRackSerializer(CustomFieldModelSerializer):
validator.set_context(self) validator.set_context(self)
validator(data) validator(data)
# Enforce model validation
super(WritableRackSerializer, self).validate(data)
return data return data
@@ -211,7 +216,7 @@ class RackReservationSerializer(serializers.ModelSerializer):
fields = ['id', 'rack', 'units', 'created', 'user', 'description'] fields = ['id', 'rack', 'units', 'created', 'user', 'description']
class WritableRackReservationSerializer(serializers.ModelSerializer): class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = RackReservation model = RackReservation
@@ -222,7 +227,7 @@ class WritableRackReservationSerializer(serializers.ModelSerializer):
# Manufacturers # Manufacturers
# #
class ManufacturerSerializer(serializers.ModelSerializer): class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
@@ -287,7 +292,7 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritableConsolePortTemplateSerializer(serializers.ModelSerializer): class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
@@ -306,7 +311,7 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritableConsoleServerPortTemplateSerializer(serializers.ModelSerializer): class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
@@ -325,7 +330,7 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritablePowerPortTemplateSerializer(serializers.ModelSerializer): class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
@@ -344,7 +349,7 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritablePowerOutletTemplateSerializer(serializers.ModelSerializer): class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
@@ -364,7 +369,7 @@ class InterfaceTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only'] fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
class WritableInterfaceTemplateSerializer(serializers.ModelSerializer): class WritableInterfaceTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
@@ -383,7 +388,7 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritableDeviceBayTemplateSerializer(serializers.ModelSerializer): class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = DeviceBayTemplate model = DeviceBayTemplate
@@ -394,7 +399,7 @@ class WritableDeviceBayTemplateSerializer(serializers.ModelSerializer):
# Device roles # Device roles
# #
class DeviceRoleSerializer(serializers.ModelSerializer): class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
@@ -413,7 +418,7 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
# Platforms # Platforms
# #
class PlatformSerializer(serializers.ModelSerializer): class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Platform model = Platform
@@ -496,6 +501,9 @@ class WritableDeviceSerializer(CustomFieldModelSerializer):
validator.set_context(self) validator.set_context(self)
validator(data) validator(data)
# Enforce model validation
super(WritableDeviceSerializer, self).validate(data)
return data return data
@@ -512,7 +520,7 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_console'] read_only_fields = ['connected_console']
class WritableConsoleServerPortSerializer(serializers.ModelSerializer): class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
@@ -532,7 +540,7 @@ class ConsolePortSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'cs_port', 'connection_status'] fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
class WritableConsolePortSerializer(serializers.ModelSerializer): class WritableConsolePortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = ConsolePort model = ConsolePort
@@ -552,7 +560,7 @@ class PowerOutletSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_port'] read_only_fields = ['connected_port']
class WritablePowerOutletSerializer(serializers.ModelSerializer): class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
@@ -572,7 +580,7 @@ class PowerPortSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status'] fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
class WritablePowerPortSerializer(serializers.ModelSerializer): class WritablePowerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = PowerPort model = PowerPort
@@ -591,28 +599,58 @@ class NestedInterfaceSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'name']
class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
class Meta:
model = Circuit
fields = ['id', 'url', 'cid']
class InterfaceCircuitTerminationSerializer(serializers.ModelSerializer):
circuit = InterfaceNestedCircuitSerializer()
class Meta:
model = CircuitTermination
fields = [
'id', 'circuit', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
]
class InterfaceSerializer(serializers.ModelSerializer): class InterfaceSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES) form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
lag = NestedInterfaceSerializer() lag = NestedInterfaceSerializer()
connection = serializers.SerializerMethodField(read_only=True) is_connected = serializers.SerializerMethodField(read_only=True)
connected_interface = serializers.SerializerMethodField(read_only=True) interface_connection = serializers.SerializerMethodField(read_only=True)
circuit_termination = InterfaceCircuitTerminationSerializer()
class Meta: class Meta:
model = Interface model = Interface
fields = [ fields = [
'id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description', 'connection', 'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
'connected_interface', 'is_connected', 'interface_connection', 'circuit_termination',
] ]
def get_connection(self, obj): def get_is_connected(self, obj):
"""
Return True if the interface has a connected interface or circuit termination.
"""
if obj.connection: if obj.connection:
return NestedInterfaceConnectionSerializer(obj.connection, context=self.context).data return True
return None try:
circuit_termination = obj.circuit_termination
return True
except CircuitTermination.DoesNotExist:
pass
return False
def get_connected_interface(self, obj): def get_interface_connection(self, obj):
if obj.connected_interface: if obj.connection:
return PeerInterfaceSerializer(obj.connected_interface, context=self.context).data return OrderedDict((
('interface', PeerInterfaceSerializer(obj.connected_interface, context=self.context).data),
('status', obj.connection.connection_status),
))
return None return None
@@ -624,14 +662,19 @@ class PeerInterfaceSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Interface model = Interface
fields = ['id', 'url', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description'] fields = [
'id', 'url', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
'description',
]
class WritableInterfaceSerializer(serializers.ModelSerializer): class WritableInterfaceSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Interface model = Interface
fields = ['id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description'] fields = [
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
]
# #
@@ -647,7 +690,7 @@ class DeviceBaySerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'installed_device'] fields = ['id', 'device', 'name', 'installed_device']
class WritableDeviceBaySerializer(serializers.ModelSerializer): class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = DeviceBay model = DeviceBay
@@ -664,14 +707,20 @@ class InventoryItemSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] fields = [
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description',
]
class WritableInventoryItemSerializer(serializers.ModelSerializer): class WritableInventoryItemSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] fields = [
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description',
]
# #
@@ -696,7 +745,7 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'connection_status'] fields = ['id', 'url', 'connection_status']
class WritableInterfaceConnectionSerializer(serializers.ModelSerializer): class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = InterfaceConnection model = InterfaceConnection

View File

@@ -32,6 +32,7 @@ class RegionViewSet(WritableSerializerMixin, ModelViewSet):
queryset = Region.objects.all() queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer serializer_class = serializers.RegionSerializer
write_serializer_class = serializers.WritableRegionSerializer write_serializer_class = serializers.WritableRegionSerializer
filter_class = filters.RegionFilter
# #
@@ -73,6 +74,7 @@ class RackGroupViewSet(WritableSerializerMixin, ModelViewSet):
class RackRoleViewSet(ModelViewSet): class RackRoleViewSet(ModelViewSet):
queryset = RackRole.objects.all() queryset = RackRole.objects.all()
serializer_class = serializers.RackRoleSerializer serializer_class = serializers.RackRoleSerializer
filter_class = filters.RackRoleFilter
# #
@@ -128,6 +130,7 @@ class RackReservationViewSet(WritableSerializerMixin, ModelViewSet):
class ManufacturerViewSet(ModelViewSet): class ManufacturerViewSet(ModelViewSet):
queryset = Manufacturer.objects.all() queryset = Manufacturer.objects.all()
serializer_class = serializers.ManufacturerSerializer serializer_class = serializers.ManufacturerSerializer
filter_class = filters.ManufacturerFilter
# #
@@ -194,6 +197,7 @@ class DeviceBayTemplateViewSet(WritableSerializerMixin, ModelViewSet):
class DeviceRoleViewSet(ModelViewSet): class DeviceRoleViewSet(ModelViewSet):
queryset = DeviceRole.objects.all() queryset = DeviceRole.objects.all()
serializer_class = serializers.DeviceRoleSerializer serializer_class = serializers.DeviceRoleSerializer
filter_class = filters.DeviceRoleFilter
# #
@@ -203,6 +207,7 @@ class DeviceRoleViewSet(ModelViewSet):
class PlatformViewSet(ModelViewSet): class PlatformViewSet(ModelViewSet):
queryset = Platform.objects.all() queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer serializer_class = serializers.PlatformSerializer
filter_class = filters.PlatformFilter
# #

230
netbox/dcim/constants.py Normal file
View File

@@ -0,0 +1,230 @@
from __future__ import unicode_literals
# Rack types
RACK_TYPE_2POST = 100
RACK_TYPE_4POST = 200
RACK_TYPE_CABINET = 300
RACK_TYPE_WALLFRAME = 1000
RACK_TYPE_WALLCABINET = 1100
RACK_TYPE_CHOICES = (
(RACK_TYPE_2POST, '2-post frame'),
(RACK_TYPE_4POST, '4-post frame'),
(RACK_TYPE_CABINET, '4-post cabinet'),
(RACK_TYPE_WALLFRAME, 'Wall-mounted frame'),
(RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'),
)
# Rack widths
RACK_WIDTH_19IN = 19
RACK_WIDTH_23IN = 23
RACK_WIDTH_CHOICES = (
(RACK_WIDTH_19IN, '19 inches'),
(RACK_WIDTH_23IN, '23 inches'),
)
# Rack faces
RACK_FACE_FRONT = 0
RACK_FACE_REAR = 1
RACK_FACE_CHOICES = [
[RACK_FACE_FRONT, 'Front'],
[RACK_FACE_REAR, 'Rear'],
]
# Parent/child device roles
SUBDEVICE_ROLE_PARENT = True
SUBDEVICE_ROLE_CHILD = False
SUBDEVICE_ROLE_CHOICES = (
(None, 'None'),
(SUBDEVICE_ROLE_PARENT, 'Parent'),
(SUBDEVICE_ROLE_CHILD, 'Child'),
)
# Interface ordering schemes (for device types)
IFACE_ORDERING_POSITION = 1
IFACE_ORDERING_NAME = 2
IFACE_ORDERING_CHOICES = [
[IFACE_ORDERING_POSITION, 'Slot/position'],
[IFACE_ORDERING_NAME, 'Name (alphabetically)']
]
# Interface form factors
# Virtual
IFACE_FF_VIRTUAL = 0
IFACE_FF_LAG = 200
# Ethernet
IFACE_FF_100ME_FIXED = 800
IFACE_FF_1GE_FIXED = 1000
IFACE_FF_1GE_GBIC = 1050
IFACE_FF_1GE_SFP = 1100
IFACE_FF_10GE_FIXED = 1150
IFACE_FF_10GE_SFP_PLUS = 1200
IFACE_FF_10GE_XFP = 1300
IFACE_FF_10GE_XENPAK = 1310
IFACE_FF_10GE_X2 = 1320
IFACE_FF_25GE_SFP28 = 1350
IFACE_FF_40GE_QSFP_PLUS = 1400
IFACE_FF_100GE_CFP = 1500
IFACE_FF_100GE_QSFP28 = 1600
# Wireless
IFACE_FF_80211A = 2600
IFACE_FF_80211G = 2610
IFACE_FF_80211N = 2620
IFACE_FF_80211AC = 2630
IFACE_FF_80211AD = 2640
# Fibrechannel
IFACE_FF_1GFC_SFP = 3010
IFACE_FF_2GFC_SFP = 3020
IFACE_FF_4GFC_SFP = 3040
IFACE_FF_8GFC_SFP_PLUS = 3080
IFACE_FF_16GFC_SFP_PLUS = 3160
# Serial
IFACE_FF_T1 = 4000
IFACE_FF_E1 = 4010
IFACE_FF_T3 = 4040
IFACE_FF_E3 = 4050
# Stacking
IFACE_FF_STACKWISE = 5000
IFACE_FF_STACKWISE_PLUS = 5050
IFACE_FF_FLEXSTACK = 5100
IFACE_FF_FLEXSTACK_PLUS = 5150
IFACE_FF_JUNIPER_VCP = 5200
# Other
IFACE_FF_OTHER = 32767
IFACE_FF_CHOICES = [
[
'Virtual interfaces',
[
[IFACE_FF_VIRTUAL, 'Virtual'],
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
]
],
[
'Ethernet (fixed)',
[
[IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
]
],
[
'Ethernet (modular)',
[
[IFACE_FF_1GE_GBIC, 'GBIC (1GE)'],
[IFACE_FF_1GE_SFP, 'SFP (1GE)'],
[IFACE_FF_10GE_SFP_PLUS, 'SFP+ (10GE)'],
[IFACE_FF_10GE_XFP, 'XFP (10GE)'],
[IFACE_FF_10GE_XENPAK, 'XENPAK (10GE)'],
[IFACE_FF_10GE_X2, 'X2 (10GE)'],
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
]
],
[
'Wireless',
[
[IFACE_FF_80211A, 'IEEE 802.11a'],
[IFACE_FF_80211G, 'IEEE 802.11b/g'],
[IFACE_FF_80211N, 'IEEE 802.11n'],
[IFACE_FF_80211AC, 'IEEE 802.11ac'],
[IFACE_FF_80211AD, 'IEEE 802.11ad'],
]
],
[
'FibreChannel',
[
[IFACE_FF_1GFC_SFP, 'SFP (1GFC)'],
[IFACE_FF_2GFC_SFP, 'SFP (2GFC)'],
[IFACE_FF_4GFC_SFP, 'SFP (4GFC)'],
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
]
],
[
'Serial',
[
[IFACE_FF_T1, 'T1 (1.544 Mbps)'],
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
[IFACE_FF_T3, 'T3 (45 Mbps)'],
[IFACE_FF_E3, 'E3 (34 Mbps)'],
]
],
[
'Stacking',
[
[IFACE_FF_STACKWISE, 'Cisco StackWise'],
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
]
],
[
'Other',
[
[IFACE_FF_OTHER, 'Other'],
]
],
]
VIRTUAL_IFACE_TYPES = [
IFACE_FF_VIRTUAL,
IFACE_FF_LAG,
]
WIRELESS_IFACE_TYPES = [
IFACE_FF_80211A,
IFACE_FF_80211G,
IFACE_FF_80211N,
IFACE_FF_80211AC,
IFACE_FF_80211AD,
]
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
# Device statuses
STATUS_OFFLINE = 0
STATUS_ACTIVE = 1
STATUS_PLANNED = 2
STATUS_STAGED = 3
STATUS_FAILED = 4
STATUS_INVENTORY = 5
STATUS_CHOICES = [
[STATUS_ACTIVE, 'Active'],
[STATUS_OFFLINE, 'Offline'],
[STATUS_PLANNED, 'Planned'],
[STATUS_STAGED, 'Staged'],
[STATUS_FAILED, 'Failed'],
[STATUS_INVENTORY, 'Inventory'],
]
# Bootstrap CSS classes for device stasuses
DEVICE_STATUS_CLASSES = {
0: 'warning',
1: 'success',
2: 'info',
3: 'primary',
4: 'danger',
5: 'default',
}
# Console/power/interface connection statuses
CONNECTION_STATUS_PLANNED = False
CONNECTION_STATUS_CONNECTED = True
CONNECTION_STATUS_CHOICES = [
[CONNECTION_STATUS_PLANNED, 'Planned'],
[CONNECTION_STATUS_CONNECTED, 'Connected'],
]
# Platform -> RPC client mappings
RPC_CLIENT_JUNIPER_JUNOS = 'juniper-junos'
RPC_CLIENT_CISCO_IOS = 'cisco-ios'
RPC_CLIENT_OPENGEAR = 'opengear'
RPC_CLIENT_CHOICES = [
[RPC_CLIENT_JUNIPER_JUNOS, 'Juniper Junos (NETCONF)'],
[RPC_CLIENT_CISCO_IOS, 'Cisco IOS (SSH)'],
[RPC_CLIENT_OPENGEAR, 'Opengear (SSH)'],
]

View File

@@ -3,6 +3,7 @@ from __future__ import unicode_literals
import django_filters import django_filters
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
@@ -11,11 +12,28 @@ from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection, DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, InterfaceTemplate, Manufacturer, InventoryItem, NONCONNECTABLE_IFACE_TYPES, Platform, PowerOutlet,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site, VIRTUAL_IFACE_TYPES, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site,
VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
) )
class RegionFilter(django_filters.FilterSet):
parent_id = NullableModelMultipleChoiceFilter(
queryset=Region.objects.all(),
label='Parent region (ID)',
)
parent = NullableModelMultipleChoiceFilter(
queryset=Region.objects.all(),
to_field_name='slug',
label='Parent region (slug)',
)
class Meta:
model = Region
fields = ['name', 'slug']
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in') id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter( q = django_filters.CharFilter(
@@ -23,23 +41,19 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search', label='Search',
) )
region_id = NullableModelMultipleChoiceFilter( region_id = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(), queryset=Region.objects.all(),
label='Region (ID)', label='Region (ID)',
) )
region = NullableModelMultipleChoiceFilter( region = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = NullableModelMultipleChoiceFilter( tenant = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
@@ -47,7 +61,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Site model = Site
fields = ['q', 'name', 'facility', 'asn'] fields = ['q', 'name', 'slug', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -57,6 +71,9 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
Q(facility__icontains=value) | Q(facility__icontains=value) |
Q(physical_address__icontains=value) | Q(physical_address__icontains=value) |
Q(shipping_address__icontains=value) | Q(shipping_address__icontains=value) |
Q(contact_name__icontains=value) |
Q(contact_phone__icontains=value) |
Q(contact_email__icontains=value) |
Q(comments__icontains=value) Q(comments__icontains=value)
) )
try: try:
@@ -68,7 +85,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
class RackGroupFilter(django_filters.FilterSet): class RackGroupFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
@@ -81,7 +97,14 @@ class RackGroupFilter(django_filters.FilterSet):
class Meta: class Meta:
model = RackGroup model = RackGroup
fields = ['name'] fields = ['site_id', 'name', 'slug']
class RackRoleFilter(django_filters.FilterSet):
class Meta:
model = RackRole
fields = ['name', 'slug', 'color']
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -91,7 +114,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search', label='Search',
) )
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
@@ -102,7 +124,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (slug)', label='Site (slug)',
) )
group_id = NullableModelMultipleChoiceFilter( group_id = NullableModelMultipleChoiceFilter(
name='group',
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
label='Group (ID)', label='Group (ID)',
) )
@@ -113,7 +134,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Group', label='Group',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
@@ -124,7 +144,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (slug)', label='Tenant (slug)',
) )
role_id = NullableModelMultipleChoiceFilter( role_id = NullableModelMultipleChoiceFilter(
name='role',
queryset=RackRole.objects.all(), queryset=RackRole.objects.all(),
label='Role (ID)', label='Role (ID)',
) )
@@ -137,7 +156,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Rack model = Rack
fields = ['u_height'] fields = ['facility_id', 'type', 'width', 'u_height', 'desc_units']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -155,6 +174,10 @@ class RackReservationFilter(django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
rack_id = django_filters.ModelMultipleChoiceFilter(
queryset=Rack.objects.all(),
label='Rack (ID)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
name='rack__site', name='rack__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
@@ -177,15 +200,20 @@ class RackReservationFilter(django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Group', label='Group',
) )
rack_id = django_filters.ModelMultipleChoiceFilter( user_id = django_filters.ModelMultipleChoiceFilter(
name='rack', queryset=User.objects.all(),
queryset=Rack.objects.all(), label='User (ID)',
label='Rack (ID)', )
user = django_filters.ModelMultipleChoiceFilter(
name='user',
queryset=User.objects.all(),
to_field_name='username',
label='User (name)',
) )
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = ['rack', 'user'] fields = ['created']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -198,6 +226,13 @@ class RackReservationFilter(django_filters.FilterSet):
) )
class ManufacturerFilter(django_filters.FilterSet):
class Meta:
model = Manufacturer
fields = ['name', 'slug']
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet): class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in') id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter( q = django_filters.CharFilter(
@@ -205,7 +240,6 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search', label='Search',
) )
manufacturer_id = django_filters.ModelMultipleChoiceFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(
name='manufacturer',
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)', label='Manufacturer (ID)',
) )
@@ -219,7 +253,8 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = DeviceType model = DeviceType
fields = [ fields = [
'model', 'part_number', 'u_height', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'is_network_device', 'subdevice_role',
] ]
def search(self, queryset, name, value): def search(self, queryset, name, value):
@@ -235,16 +270,9 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
class DeviceTypeComponentFilterSet(django_filters.FilterSet): class DeviceTypeComponentFilterSet(django_filters.FilterSet):
devicetype_id = django_filters.ModelMultipleChoiceFilter( devicetype_id = django_filters.ModelMultipleChoiceFilter(
name='device_type',
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
label='Device type (ID)', label='Device type (ID)',
) )
devicetype = django_filters.ModelMultipleChoiceFilter(
name='device_type',
queryset=DeviceType.objects.all(),
to_field_name='name',
label='Device type (name)',
)
class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet): class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet):
@@ -279,7 +307,7 @@ class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = ['name', 'form_factor'] fields = ['name', 'form_factor', 'mgmt_only']
class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet): class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
@@ -289,18 +317,73 @@ class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
fields = ['name'] fields = ['name']
class DeviceRoleFilter(django_filters.FilterSet):
class Meta:
model = DeviceRole
fields = ['name', 'slug', 'color']
class PlatformFilter(django_filters.FilterSet):
class Meta:
model = Platform
fields = ['name', 'slug']
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in') id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
) )
mac_address = django_filters.CharFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(
method='_mac_address', name='device_type__manufacturer',
label='MAC address', queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
name='device_type__manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
device_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
label='Device type (ID)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
name='device_role_id',
queryset=DeviceRole.objects.all(),
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
name='device_role__slug',
queryset=DeviceRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
tenant_id = NullableModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
platform_id = NullableModelMultipleChoiceFilter(
queryset=Platform.objects.all(),
label='Platform (ID)',
)
platform = NullableModelMultipleChoiceFilter(
name='platform',
queryset=Platform.objects.all(),
to_field_name='slug',
label='Platform (slug)',
) )
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
@@ -320,60 +403,18 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
label='Rack (ID)', label='Rack (ID)',
) )
role_id = django_filters.ModelMultipleChoiceFilter(
name='device_role',
queryset=DeviceRole.objects.all(),
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
name='device_role__slug',
queryset=DeviceRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
device_type_id = django_filters.ModelMultipleChoiceFilter(
name='device_type',
queryset=DeviceType.objects.all(),
label='Device type (ID)',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
name='device_type__manufacturer',
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
name='device_type__manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
model = django_filters.ModelMultipleChoiceFilter( model = django_filters.ModelMultipleChoiceFilter(
name='device_type__slug', name='device_type__slug',
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Device model (slug)', label='Device model (slug)',
) )
platform_id = NullableModelMultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
name='platform', choices=STATUS_CHOICES
queryset=Platform.objects.all(),
label='Platform (ID)',
) )
platform = NullableModelMultipleChoiceFilter( is_full_depth = django_filters.BooleanFilter(
name='platform', name='device_type__is_full_depth',
queryset=Platform.objects.all(), label='Is full depth',
to_field_name='slug',
label='Platform (slug)',
) )
is_console_server = django_filters.BooleanFilter( is_console_server = django_filters.BooleanFilter(
name='device_type__is_console_server', name='device_type__is_console_server',
@@ -387,13 +428,14 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
name='device_type__is_network_device', name='device_type__is_network_device',
label='Is a network device', label='Is a network device',
) )
mac_address = django_filters.CharFilter(
method='_mac_address',
label='MAC address',
)
has_primary_ip = django_filters.BooleanFilter( has_primary_ip = django_filters.BooleanFilter(
method='_has_primary_ip', method='_has_primary_ip',
label='Has a primary IP', label='Has a primary IP',
) )
status = django_filters.MultipleChoiceFilter(
choices=STATUS_CHOICES
)
class Meta: class Meta:
model = Device model = Device
@@ -433,13 +475,11 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
class DeviceComponentFilterSet(django_filters.FilterSet): class DeviceComponentFilterSet(django_filters.FilterSet):
device_id = django_filters.ModelMultipleChoiceFilter( device_id = django_filters.ModelChoiceFilter(
name='device',
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='Device (ID)', label='Device (ID)',
) )
device = django_filters.ModelMultipleChoiceFilter( device = django_filters.ModelChoiceFilter(
name='device__name',
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name', to_field_name='name',
label='Device (name)', label='Device (name)',
@@ -474,7 +514,21 @@ class PowerOutletFilter(DeviceComponentFilterSet):
fields = ['name'] fields = ['name']
class InterfaceFilter(DeviceComponentFilterSet): class InterfaceFilter(django_filters.FilterSet):
"""
Not using DeviceComponentFilterSet for Interfaces because we need to glean the ordering logic from the parent
Device's DeviceType.
"""
device = django_filters.CharFilter(
method='filter_device',
name='name',
label='Device',
)
device_id = django_filters.NumberFilter(
method='filter_device',
name='pk',
label='Device (ID)',
)
type = django_filters.CharFilter( type = django_filters.CharFilter(
method='filter_type', method='filter_type',
label='Interface type', label='Interface type',
@@ -491,17 +545,24 @@ class InterfaceFilter(DeviceComponentFilterSet):
class Meta: class Meta:
model = Interface model = Interface
fields = ['name', 'form_factor'] fields = ['name', 'form_factor', 'enabled', 'mtu', 'mgmt_only']
def filter_device(self, queryset, name, value):
try:
device = Device.objects.select_related('device_type').get(**{name: value})
ordering = device.device_type.interface_ordering
return queryset.filter(device=device).order_naturally(ordering)
except Device.DoesNotExist:
return queryset.none()
def filter_type(self, queryset, name, value): def filter_type(self, queryset, name, value):
value = value.strip().lower() value = value.strip().lower()
if value == 'physical': return {
return queryset.exclude(form_factor__in=VIRTUAL_IFACE_TYPES) 'physical': queryset.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES),
elif value == 'virtual': 'virtual': queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES),
return queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES) 'wireless': queryset.filter(form_factor__in=WIRELESS_IFACE_TYPES),
elif value == 'lag': 'lag': queryset.filter(form_factor=IFACE_FF_LAG),
return queryset.filter(form_factor=IFACE_FF_LAG) }.get(value, queryset.none())
return queryset
def _mac_address(self, queryset, name, value): def _mac_address(self, queryset, name, value):
value = value.strip() value = value.strip()
@@ -521,10 +582,24 @@ class DeviceBayFilter(DeviceComponentFilterSet):
class InventoryItemFilter(DeviceComponentFilterSet): class InventoryItemFilter(DeviceComponentFilterSet):
parent_id = NullableModelMultipleChoiceFilter(
queryset=InventoryItem.objects.all(),
label='Parent inventory item (ID)',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
name='manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ['name'] fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']
class ConsoleConnectionFilter(django_filters.FilterSet): class ConsoleConnectionFilter(django_filters.FilterSet):

View File

@@ -24,7 +24,7 @@ from .models import (
IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES,
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN,
Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
) )
@@ -1447,7 +1447,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = Interface model = Interface
fields = ['device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description'] fields = ['device', 'name', 'form_factor', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description']
widgets = { widgets = {
'device': forms.HiddenInput(), 'device': forms.HiddenInput(),
} }
@@ -1469,12 +1469,19 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
class InterfaceCreateForm(DeviceComponentForm): class InterfaceCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES) form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
enabled = forms.BooleanField(required=False)
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG') lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
mac_address = MACAddressFormField(required=False, label='MAC Address') mac_address = MACAddressFormField(required=False, label='MAC Address')
mgmt_only = forms.BooleanField(required=False, label='OOB Management') mgmt_only = forms.BooleanField(required=False, label='OOB Management')
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Set interfaces enabled by default
kwargs['initial'] = kwargs.get('initial', {})
kwargs['initial'].update({'enabled': True})
super(InterfaceCreateForm, self).__init__(*args, **kwargs) super(InterfaceCreateForm, self).__init__(*args, **kwargs)
# Limit LAG choices to interfaces belonging to this device # Limit LAG choices to interfaces belonging to this device
@@ -1489,13 +1496,15 @@ class InterfaceCreateForm(DeviceComponentForm):
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm): class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput) device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput)
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False) form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
enabled = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect)
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only') mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
class Meta: class Meta:
nullable_fields = ['lag', 'description'] nullable_fields = ['lag', 'mtu', 'description']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(InterfaceBulkEditForm, self).__init__(*args, **kwargs) super(InterfaceBulkEditForm, self).__init__(*args, **kwargs)
@@ -1574,7 +1583,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
) )
) )
interface_b = ChainedModelChoiceField( interface_b = ChainedModelChoiceField(
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related( queryset=Interface.objects.connectable().select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b' 'circuit_termination', 'connected_as_a', 'connected_as_b'
), ),
chains=( chains=(
@@ -1583,7 +1592,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
label='Interface', label='Interface',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical', api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
disabled_indicator='connection' disabled_indicator='is_connected'
) )
) )
@@ -1596,9 +1605,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
super(InterfaceConnectionForm, self).__init__(*args, **kwargs) super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
# Initialize interface A choices # Initialize interface A choices
device_a_interfaces = Interface.objects.order_naturally().filter(device=device_a).exclude( device_a_interfaces = Interface.objects.connectable().order_naturally().filter(device=device_a).select_related(
form_factor__in=VIRTUAL_IFACE_TYPES
).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b' 'circuit_termination', 'connected_as_a', 'connected_as_b'
) )
self.fields['interface_a'].choices = [ self.fields['interface_a'].choices = [
@@ -1758,4 +1765,4 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ['name', 'manufacturer', 'part_id', 'serial'] fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-06-16 21:38
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0037_unicode_literals'),
]
operations = [
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),
),
]

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-06-23 17:05
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0038_wireless_interfaces'),
]
operations = [
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'),
),
]

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-06-23 20:44
from __future__ import unicode_literals
from django.db import migrations, models
import utilities.fields
class Migration(migrations.Migration):
dependencies = [
('dcim', '0039_interface_add_enabled_mtu'),
]
operations = [
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),
),
]

View File

@@ -24,204 +24,10 @@ from utilities.fields import ColorField, NullableCharField
from utilities.managers import NaturalOrderByManager from utilities.managers import NaturalOrderByManager
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format from utilities.utils import csv_format
from .constants import *
from .fields import ASNField, MACAddressField from .fields import ASNField, MACAddressField
RACK_TYPE_2POST = 100
RACK_TYPE_4POST = 200
RACK_TYPE_CABINET = 300
RACK_TYPE_WALLFRAME = 1000
RACK_TYPE_WALLCABINET = 1100
RACK_TYPE_CHOICES = (
(RACK_TYPE_2POST, '2-post frame'),
(RACK_TYPE_4POST, '4-post frame'),
(RACK_TYPE_CABINET, '4-post cabinet'),
(RACK_TYPE_WALLFRAME, 'Wall-mounted frame'),
(RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'),
)
RACK_WIDTH_19IN = 19
RACK_WIDTH_23IN = 23
RACK_WIDTH_CHOICES = (
(RACK_WIDTH_19IN, '19 inches'),
(RACK_WIDTH_23IN, '23 inches'),
)
RACK_FACE_FRONT = 0
RACK_FACE_REAR = 1
RACK_FACE_CHOICES = [
[RACK_FACE_FRONT, 'Front'],
[RACK_FACE_REAR, 'Rear'],
]
SUBDEVICE_ROLE_PARENT = True
SUBDEVICE_ROLE_CHILD = False
SUBDEVICE_ROLE_CHOICES = (
(None, 'None'),
(SUBDEVICE_ROLE_PARENT, 'Parent'),
(SUBDEVICE_ROLE_CHILD, 'Child'),
)
IFACE_ORDERING_POSITION = 1
IFACE_ORDERING_NAME = 2
IFACE_ORDERING_CHOICES = [
[IFACE_ORDERING_POSITION, 'Slot/position'],
[IFACE_ORDERING_NAME, 'Name (alphabetically)']
]
# Virtual
IFACE_FF_VIRTUAL = 0
IFACE_FF_LAG = 200
# Ethernet
IFACE_FF_100ME_FIXED = 800
IFACE_FF_1GE_FIXED = 1000
IFACE_FF_1GE_GBIC = 1050
IFACE_FF_1GE_SFP = 1100
IFACE_FF_10GE_FIXED = 1150
IFACE_FF_10GE_SFP_PLUS = 1200
IFACE_FF_10GE_XFP = 1300
IFACE_FF_10GE_XENPAK = 1310
IFACE_FF_10GE_X2 = 1320
IFACE_FF_25GE_SFP28 = 1350
IFACE_FF_40GE_QSFP_PLUS = 1400
IFACE_FF_100GE_CFP = 1500
IFACE_FF_100GE_QSFP28 = 1600
# Fibrechannel
IFACE_FF_1GFC_SFP = 3010
IFACE_FF_2GFC_SFP = 3020
IFACE_FF_4GFC_SFP = 3040
IFACE_FF_8GFC_SFP_PLUS = 3080
IFACE_FF_16GFC_SFP_PLUS = 3160
# Serial
IFACE_FF_T1 = 4000
IFACE_FF_E1 = 4010
IFACE_FF_T3 = 4040
IFACE_FF_E3 = 4050
# Stacking
IFACE_FF_STACKWISE = 5000
IFACE_FF_STACKWISE_PLUS = 5050
IFACE_FF_FLEXSTACK = 5100
IFACE_FF_FLEXSTACK_PLUS = 5150
IFACE_FF_JUNIPER_VCP = 5200
# Other
IFACE_FF_OTHER = 32767
IFACE_FF_CHOICES = [
[
'Virtual interfaces',
[
[IFACE_FF_VIRTUAL, 'Virtual'],
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
]
],
[
'Ethernet (fixed)',
[
[IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
]
],
[
'Ethernet (modular)',
[
[IFACE_FF_1GE_GBIC, 'GBIC (1GE)'],
[IFACE_FF_1GE_SFP, 'SFP (1GE)'],
[IFACE_FF_10GE_SFP_PLUS, 'SFP+ (10GE)'],
[IFACE_FF_10GE_XFP, 'XFP (10GE)'],
[IFACE_FF_10GE_XENPAK, 'XENPAK (10GE)'],
[IFACE_FF_10GE_X2, 'X2 (10GE)'],
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
]
],
[
'FibreChannel',
[
[IFACE_FF_1GFC_SFP, 'SFP (1GFC)'],
[IFACE_FF_2GFC_SFP, 'SFP (2GFC)'],
[IFACE_FF_4GFC_SFP, 'SFP (4GFC)'],
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
]
],
[
'Serial',
[
[IFACE_FF_T1, 'T1 (1.544 Mbps)'],
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
[IFACE_FF_T3, 'T3 (45 Mbps)'],
[IFACE_FF_E3, 'E3 (34 Mbps)'],
[IFACE_FF_E3, 'E3 (34 Mbps)'],
]
],
[
'Stacking',
[
[IFACE_FF_STACKWISE, 'Cisco StackWise'],
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
]
],
[
'Other',
[
[IFACE_FF_OTHER, 'Other'],
]
],
]
VIRTUAL_IFACE_TYPES = [
IFACE_FF_VIRTUAL,
IFACE_FF_LAG,
]
STATUS_OFFLINE = 0
STATUS_ACTIVE = 1
STATUS_PLANNED = 2
STATUS_STAGED = 3
STATUS_FAILED = 4
STATUS_INVENTORY = 5
STATUS_CHOICES = [
[STATUS_ACTIVE, 'Active'],
[STATUS_OFFLINE, 'Offline'],
[STATUS_PLANNED, 'Planned'],
[STATUS_STAGED, 'Staged'],
[STATUS_FAILED, 'Failed'],
[STATUS_INVENTORY, 'Inventory'],
]
DEVICE_STATUS_CLASSES = {
0: 'warning',
1: 'success',
2: 'info',
3: 'primary',
4: 'danger',
5: 'default',
}
CONNECTION_STATUS_PLANNED = False
CONNECTION_STATUS_CONNECTED = True
CONNECTION_STATUS_CHOICES = [
[CONNECTION_STATUS_PLANNED, 'Planned'],
[CONNECTION_STATUS_CONNECTED, 'Connected'],
]
# For mapping platform -> NC client
RPC_CLIENT_JUNIPER_JUNOS = 'juniper-junos'
RPC_CLIENT_CISCO_IOS = 'cisco-ios'
RPC_CLIENT_OPENGEAR = 'opengear'
RPC_CLIENT_CHOICES = [
[RPC_CLIENT_JUNIPER_JUNOS, 'Juniper Junos (NETCONF)'],
[RPC_CLIENT_CISCO_IOS, 'Cisco IOS (SSH)'],
[RPC_CLIENT_OPENGEAR, 'Opengear (SSH)'],
]
# #
# Regions # Regions
# #
@@ -816,7 +622,7 @@ class PowerOutletTemplate(models.Model):
return self.name return self.name
class InterfaceManager(models.Manager): class InterfaceQuerySet(models.QuerySet):
def order_naturally(self, method=IFACE_ORDERING_POSITION): def order_naturally(self, method=IFACE_ORDERING_POSITION):
""" """
@@ -841,13 +647,12 @@ class InterfaceManager(models.Manager):
The original `name` field is taken as a whole to serve as a fallback in the event interfaces do not match any of The original `name` field is taken as a whole to serve as a fallback in the event interfaces do not match any of
the prescribed fields. the prescribed fields.
""" """
queryset = self.get_queryset() sql_col = '{}.name'.format(self.model._meta.db_table)
sql_col = '{}.name'.format(queryset.model._meta.db_table)
ordering = { ordering = {
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'), IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'),
IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'), IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'),
}[method] }[method]
return queryset.extra(select={ return self.extra(select={
'_type': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col), '_type': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col), '_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col), '_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
@@ -856,6 +661,13 @@ class InterfaceManager(models.Manager):
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col), '_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
}).order_by(*ordering) }).order_by(*ordering)
def connectable(self):
"""
Return only physical interfaces which are capable of being connected to other interfaces (i.e. not virtual or
wireless).
"""
return self.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES)
@python_2_unicode_compatible @python_2_unicode_compatible
class InterfaceTemplate(models.Model): class InterfaceTemplate(models.Model):
@@ -867,7 +679,7 @@ class InterfaceTemplate(models.Model):
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS) form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
mgmt_only = models.BooleanField(default=False, verbose_name='Management only') mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
objects = InterfaceManager() objects = InterfaceQuerySet.as_manager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
@@ -1308,16 +1120,27 @@ class Interface(models.Model):
of an InterfaceConnection. of an InterfaceConnection.
""" """
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE) device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
lag = models.ForeignKey('self', related_name='member_interfaces', null=True, blank=True, on_delete=models.SET_NULL, lag = models.ForeignKey(
verbose_name='Parent LAG') 'self',
models.SET_NULL,
related_name='member_interfaces',
null=True,
blank=True,
verbose_name='Parent LAG'
)
name = models.CharField(max_length=30) name = models.CharField(max_length=30)
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS) form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
enabled = models.BooleanField(default=True)
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address') mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
mgmt_only = models.BooleanField(default=False, verbose_name='OOB Management', mtu = models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU')
help_text="This interface is used only for out-of-band management") mgmt_only = models.BooleanField(
default=False,
verbose_name='OOB Management',
help_text="This interface is used only for out-of-band management"
)
description = models.CharField(max_length=100, blank=True) description = models.CharField(max_length=100, blank=True)
objects = InterfaceManager() objects = InterfaceQuerySet.as_manager()
class Meta: class Meta:
ordering = ['device', 'name'] ordering = ['device', 'name']
@@ -1329,10 +1152,10 @@ class Interface(models.Model):
def clean(self): def clean(self):
# Virtual interfaces cannot be connected # Virtual interfaces cannot be connected
if self.form_factor in VIRTUAL_IFACE_TYPES and self.is_connected: if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.is_connected:
raise ValidationError({ raise ValidationError({
'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the " 'form_factor': "Virtual and wireless interfaces cannot be connected to another interface or circuit. "
"interface or choose a physical form factor." "Disconnect the interface or choose a suitable form factor."
}) })
# An interface's LAG must belong to the same device # An interface's LAG must belong to the same device
@@ -1344,7 +1167,7 @@ class Interface(models.Model):
}) })
# A virtual interface cannot have a parent LAG # A virtual interface cannot have a parent LAG
if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None: if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.lag is not None:
raise ValidationError({ raise ValidationError({
'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display()) 'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
}) })
@@ -1361,6 +1184,10 @@ class Interface(models.Model):
def is_virtual(self): def is_virtual(self):
return self.form_factor in VIRTUAL_IFACE_TYPES return self.form_factor in VIRTUAL_IFACE_TYPES
@property
def is_wireless(self):
return self.form_factor in WIRELESS_IFACE_TYPES
@property @property
def is_lag(self): def is_lag(self):
return self.form_factor == IFACE_FF_LAG return self.form_factor == IFACE_FF_LAG
@@ -1479,11 +1306,17 @@ class InventoryItem(models.Model):
device = models.ForeignKey('Device', related_name='inventory_items', on_delete=models.CASCADE) device = models.ForeignKey('Device', related_name='inventory_items', on_delete=models.CASCADE)
parent = models.ForeignKey('self', related_name='child_items', blank=True, null=True, on_delete=models.CASCADE) parent = models.ForeignKey('self', related_name='child_items', blank=True, null=True, on_delete=models.CASCADE)
name = models.CharField(max_length=50, verbose_name='Name') name = models.CharField(max_length=50, verbose_name='Name')
manufacturer = models.ForeignKey('Manufacturer', related_name='inventory_items', blank=True, null=True, manufacturer = models.ForeignKey(
on_delete=models.PROTECT) 'Manufacturer', models.PROTECT, related_name='inventory_items', blank=True, null=True
)
part_id = models.CharField(max_length=50, verbose_name='Part ID', blank=True) part_id = models.CharField(max_length=50, verbose_name='Part ID', blank=True)
serial = models.CharField(max_length=50, verbose_name='Serial number', blank=True) serial = models.CharField(max_length=50, verbose_name='Serial number', blank=True)
asset_tag = NullableCharField(
max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
help_text='A unique tag used to identify this item'
)
discovered = models.BooleanField(default=False, verbose_name='Discovered') discovered = models.BooleanField(default=False, verbose_name='Discovered')
description = models.CharField(max_length=100, blank=True)
class Meta: class Meta:
ordering = ['device__id', 'parent__id', 'name'] ordering = ['device__id', 'parent__id', 'name']

View File

@@ -368,10 +368,11 @@ class PowerOutletTemplateTable(BaseTable):
class InterfaceTemplateTable(BaseTable): class InterfaceTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}")
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = InterfaceTemplate model = InterfaceTemplate
fields = ('pk', 'name', 'form_factor') fields = ('pk', 'name', 'mgmt_only', 'form_factor')
empty_text = "None" empty_text = "None"
show_header = False show_header = False

View File

@@ -573,15 +573,10 @@ class DeviceTypeView(View):
poweroutlet_table = tables.PowerOutletTemplateTable( poweroutlet_table = tables.PowerOutletTemplateTable(
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
) )
mgmt_interface_table = tables.InterfaceTemplateTable(
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(
device_type=devicetype, mgmt_only=True
))
)
interface_table = tables.InterfaceTemplateTable( interface_table = tables.InterfaceTemplateTable(
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter( list(InterfaceTemplate.objects.order_naturally(
device_type=devicetype, mgmt_only=False devicetype.interface_ordering
)) ).filter(device_type=devicetype))
) )
devicebay_table = tables.DeviceBayTemplateTable( devicebay_table = tables.DeviceBayTemplateTable(
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
@@ -591,7 +586,6 @@ class DeviceTypeView(View):
consoleserverport_table.base_columns['pk'].visible = True consoleserverport_table.base_columns['pk'].visible = True
powerport_table.base_columns['pk'].visible = True powerport_table.base_columns['pk'].visible = True
poweroutlet_table.base_columns['pk'].visible = True poweroutlet_table.base_columns['pk'].visible = True
mgmt_interface_table.base_columns['pk'].visible = True
interface_table.base_columns['pk'].visible = True interface_table.base_columns['pk'].visible = True
devicebay_table.base_columns['pk'].visible = True devicebay_table.base_columns['pk'].visible = True
@@ -601,7 +595,6 @@ class DeviceTypeView(View):
'consoleserverport_table': consoleserverport_table, 'consoleserverport_table': consoleserverport_table,
'powerport_table': powerport_table, 'powerport_table': powerport_table,
'poweroutlet_table': poweroutlet_table, 'poweroutlet_table': poweroutlet_table,
'mgmt_interface_table': mgmt_interface_table,
'interface_table': interface_table, 'interface_table': interface_table,
'devicebay_table': devicebay_table, 'devicebay_table': devicebay_table,
}) })
@@ -835,14 +828,10 @@ class DeviceView(View):
power_outlets = natsorted( power_outlets = natsorted(
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name') PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
) )
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter( interfaces = Interface.objects.order_naturally(
device=device, mgmt_only=False device.device_type.interface_ordering
).select_related( ).filter(
'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', device=device
'circuit_termination__circuit'
).prefetch_related('ip_addresses')
mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(
device=device, mgmt_only=True
).select_related( ).select_related(
'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', 'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
'circuit_termination__circuit' 'circuit_termination__circuit'
@@ -873,7 +862,6 @@ class DeviceView(View):
'power_ports': power_ports, 'power_ports': power_ports,
'power_outlets': power_outlets, 'power_outlets': power_outlets,
'interfaces': interfaces, 'interfaces': interfaces,
'mgmt_interfaces': mgmt_interfaces,
'device_bays': device_bays, 'device_bays': device_bays,
'services': services, 'services': services,
'secrets': secrets, 'secrets': secrets,

View File

@@ -111,6 +111,16 @@ class CustomFieldModelSerializer(serializers.ModelSerializer):
defaults={'serialized_value': custom_field.serialize_value(value)}, defaults={'serialized_value': custom_field.serialize_value(value)},
) )
def validate(self, data):
"""
Enforce model validation (see utilities.api.ModelValidationMixin)
"""
model_data = data.copy()
model_data.pop('custom_fields', None)
instance = self.Meta.model(**model_data)
instance.clean()
return data
def create(self, validated_data): def create(self, validated_data):
custom_fields = validated_data.pop('custom_fields', None) custom_fields = validated_data.pop('custom_fields', None)

View File

@@ -10,7 +10,7 @@ from extras.models import (
ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction, ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction,
) )
from users.api.serializers import NestedUserSerializer from users.api.serializers import NestedUserSerializer
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ModelValidationMixin
# #
@@ -104,7 +104,7 @@ class ImageAttachmentSerializer(serializers.ModelSerializer):
return serializer(obj.parent, context={'request': self.context['request']}).data return serializer(obj.parent, context={'request': self.context['request']}).data
class WritableImageAttachmentSerializer(serializers.ModelSerializer): class WritableImageAttachmentSerializer(ModelValidationMixin, serializers.ModelSerializer):
content_type = ContentTypeFieldSerializer() content_type = ContentTypeFieldSerializer()
class Meta: class Meta:
@@ -121,6 +121,9 @@ class WritableImageAttachmentSerializer(serializers.ModelSerializer):
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id']) "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
) )
# Enforce model validation
super(WritableImageAttachmentSerializer, self).validate(data)
return data return data

View File

@@ -0,0 +1,62 @@
from __future__ import unicode_literals
# Models which support custom fields
CUSTOMFIELD_MODELS = (
'site', 'rack', 'devicetype', 'device', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
)
# Custom field types
CF_TYPE_TEXT = 100
CF_TYPE_INTEGER = 200
CF_TYPE_BOOLEAN = 300
CF_TYPE_DATE = 400
CF_TYPE_URL = 500
CF_TYPE_SELECT = 600
CUSTOMFIELD_TYPE_CHOICES = (
(CF_TYPE_TEXT, 'Text'),
(CF_TYPE_INTEGER, 'Integer'),
(CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
(CF_TYPE_DATE, 'Date'),
(CF_TYPE_URL, 'URL'),
(CF_TYPE_SELECT, 'Selection'),
)
# Graph types
GRAPH_TYPE_INTERFACE = 100
GRAPH_TYPE_PROVIDER = 200
GRAPH_TYPE_SITE = 300
GRAPH_TYPE_CHOICES = (
(GRAPH_TYPE_INTERFACE, 'Interface'),
(GRAPH_TYPE_PROVIDER, 'Provider'),
(GRAPH_TYPE_SITE, 'Site'),
)
# Models which support export templates
EXPORTTEMPLATE_MODELS = [
'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
]
# User action types
ACTION_CREATE = 1
ACTION_IMPORT = 2
ACTION_EDIT = 3
ACTION_BULK_EDIT = 4
ACTION_DELETE = 5
ACTION_BULK_DELETE = 6
ACTION_BULK_CREATE = 7
ACTION_CHOICES = (
(ACTION_CREATE, 'created'),
(ACTION_BULK_CREATE, 'bulk created'),
(ACTION_IMPORT, 'imported'),
(ACTION_EDIT, 'modified'),
(ACTION_BULK_EDIT, 'bulk edited'),
(ACTION_DELETE, 'deleted'),
(ACTION_BULK_DELETE, 'bulk deleted'),
)

View File

@@ -15,62 +15,7 @@ from django.utils.encoding import python_2_unicode_compatible
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from utilities.utils import foreground_color from utilities.utils import foreground_color
from .constants import *
CUSTOMFIELD_MODELS = (
'site', 'rack', 'devicetype', 'device', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
)
CF_TYPE_TEXT = 100
CF_TYPE_INTEGER = 200
CF_TYPE_BOOLEAN = 300
CF_TYPE_DATE = 400
CF_TYPE_URL = 500
CF_TYPE_SELECT = 600
CUSTOMFIELD_TYPE_CHOICES = (
(CF_TYPE_TEXT, 'Text'),
(CF_TYPE_INTEGER, 'Integer'),
(CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
(CF_TYPE_DATE, 'Date'),
(CF_TYPE_URL, 'URL'),
(CF_TYPE_SELECT, 'Selection'),
)
GRAPH_TYPE_INTERFACE = 100
GRAPH_TYPE_PROVIDER = 200
GRAPH_TYPE_SITE = 300
GRAPH_TYPE_CHOICES = (
(GRAPH_TYPE_INTERFACE, 'Interface'),
(GRAPH_TYPE_PROVIDER, 'Provider'),
(GRAPH_TYPE_SITE, 'Site'),
)
EXPORTTEMPLATE_MODELS = [
'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
]
ACTION_CREATE = 1
ACTION_IMPORT = 2
ACTION_EDIT = 3
ACTION_BULK_EDIT = 4
ACTION_DELETE = 5
ACTION_BULK_DELETE = 6
ACTION_BULK_CREATE = 7
ACTION_CHOICES = (
(ACTION_CREATE, 'created'),
(ACTION_BULK_CREATE, 'bulk created'),
(ACTION_IMPORT, 'imported'),
(ACTION_EDIT, 'modified'),
(ACTION_BULK_EDIT, 'bulk edited'),
(ACTION_DELETE, 'deleted'),
(ACTION_BULK_DELETE, 'bulk deleted'),
)
# #

View File

@@ -1,4 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator
@@ -6,11 +7,11 @@ from rest_framework.validators import UniqueTogetherValidator
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from ipam.models import ( from ipam.models import (
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix,
Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
) )
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
# #
@@ -22,7 +23,7 @@ class VRFSerializer(CustomFieldModelSerializer):
class Meta: class Meta:
model = VRF model = VRF
fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_fields'] fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'display_name', 'custom_fields']
class NestedVRFSerializer(serializers.ModelSerializer): class NestedVRFSerializer(serializers.ModelSerializer):
@@ -44,7 +45,7 @@ class WritableVRFSerializer(CustomFieldModelSerializer):
# Roles # Roles
# #
class RoleSerializer(serializers.ModelSerializer): class RoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Role model = Role
@@ -63,7 +64,7 @@ class NestedRoleSerializer(serializers.ModelSerializer):
# RIRs # RIRs
# #
class RIRSerializer(serializers.ModelSerializer): class RIRSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = RIR model = RIR
@@ -141,6 +142,9 @@ class WritableVLANGroupSerializer(serializers.ModelSerializer):
validator.set_context(self) validator.set_context(self)
validator(data) validator(data)
# Enforce model validation
super(WritableVLANGroupSerializer, self).validate(data)
return data return data
@@ -187,6 +191,9 @@ class WritableVLANSerializer(CustomFieldModelSerializer):
validator.set_context(self) validator.set_context(self)
validator(data) validator(data)
# Enforce model validation
super(WritableVLANSerializer, self).validate(data)
return data return data
@@ -236,12 +243,13 @@ class IPAddressSerializer(CustomFieldModelSerializer):
vrf = NestedVRFSerializer() vrf = NestedVRFSerializer()
tenant = NestedTenantSerializer() tenant = NestedTenantSerializer()
status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES) status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES)
role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES)
interface = InterfaceSerializer() interface = InterfaceSerializer()
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = [ fields = [
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside', 'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
'nat_outside', 'custom_fields', 'nat_outside', 'custom_fields',
] ]
@@ -261,7 +269,24 @@ class WritableIPAddressSerializer(CustomFieldModelSerializer):
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = ['id', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside', 'custom_fields'] fields = [
'id', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
'custom_fields',
]
class AvailableIPSerializer(serializers.Serializer):
def to_representation(self, instance):
if self.context.get('vrf'):
vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
else:
vrf = None
return OrderedDict([
('family', self.context['prefix'].version),
('address', '{}/{}'.format(instance, self.context['prefix'].prefixlen)),
('vrf', vrf),
])
# #
@@ -278,6 +303,7 @@ class ServiceSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description'] fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
# TODO: Figure out how to use ModelValidationMixin with ManyToManyFields. Calling clean() yields a ValueError.
class WritableServiceSerializer(serializers.ModelSerializer): class WritableServiceSerializer(serializers.ModelSerializer):
class Meta: class Meta:

View File

@@ -1,7 +1,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework import status
from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from django.conf import settings
from django.shortcuts import get_object_or_404
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from ipam import filters from ipam import filters
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
@@ -20,15 +27,6 @@ class VRFViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
filter_class = filters.VRFFilter filter_class = filters.VRFFilter
#
# Roles
#
class RoleViewSet(ModelViewSet):
queryset = Role.objects.all()
serializer_class = serializers.RoleSerializer
# #
# RIRs # RIRs
# #
@@ -50,6 +48,16 @@ class AggregateViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
filter_class = filters.AggregateFilter filter_class = filters.AggregateFilter
#
# Roles
#
class RoleViewSet(ModelViewSet):
queryset = Role.objects.all()
serializer_class = serializers.RoleSerializer
filter_class = filters.RoleFilter
# #
# Prefixes # Prefixes
# #
@@ -60,6 +68,62 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
write_serializer_class = serializers.WritablePrefixSerializer write_serializer_class = serializers.WritablePrefixSerializer
filter_class = filters.PrefixFilter filter_class = filters.PrefixFilter
@detail_route(url_path='available-ips', methods=['get', 'post'])
def available_ips(self, request, pk=None):
"""
A convenience method for returning available IP addresses within a prefix. By default, the number of IPs
returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed,
however results will not be paginated.
"""
prefix = get_object_or_404(Prefix, pk=pk)
# Create the next available IP within the prefix
if request.method == 'POST':
# Permissions check
if not request.user.has_perm('ipam.add_ipaddress'):
raise PermissionDenied()
# Find the first available IP address in the prefix
try:
ipaddress = list(prefix.get_available_ips())[0]
except IndexError:
return Response(
{
"detail": "There are no available IPs within this prefix ({})".format(prefix)
},
status=status.HTTP_400_BAD_REQUEST
)
# Create the new IP address
data = request.data.copy()
data['address'] = '{}/{}'.format(ipaddress, prefix.prefix.prefixlen)
data['vrf'] = prefix.vrf
serializer = serializers.WritableIPAddressSerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# Determine the maximum amount of IPs to return
else:
try:
limit = int(request.query_params.get('limit', settings.PAGINATE_COUNT))
except ValueError:
limit = settings.PAGINATE_COUNT
if settings.MAX_PAGE_SIZE:
limit = min(limit, settings.MAX_PAGE_SIZE)
# Calculate available IPs within the prefix
ip_list = list(prefix.get_available_ips())[:limit]
serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={
'request': request,
'prefix': prefix.prefix,
'vrf': prefix.vrf,
})
return Response(serializer.data)
# #
# IP addresses # IP addresses

78
netbox/ipam/constants.py Normal file
View File

@@ -0,0 +1,78 @@
from __future__ import unicode_literals
# IP address families
AF_CHOICES = (
(4, 'IPv4'),
(6, 'IPv6'),
)
# Prefix statuses
PREFIX_STATUS_CONTAINER = 0
PREFIX_STATUS_ACTIVE = 1
PREFIX_STATUS_RESERVED = 2
PREFIX_STATUS_DEPRECATED = 3
PREFIX_STATUS_CHOICES = (
(PREFIX_STATUS_CONTAINER, 'Container'),
(PREFIX_STATUS_ACTIVE, 'Active'),
(PREFIX_STATUS_RESERVED, 'Reserved'),
(PREFIX_STATUS_DEPRECATED, 'Deprecated')
)
# IP address statuses
IPADDRESS_STATUS_ACTIVE = 1
IPADDRESS_STATUS_RESERVED = 2
IPADDRESS_STATUS_DEPRECATED = 3
IPADDRESS_STATUS_DHCP = 5
IPADDRESS_STATUS_CHOICES = (
(IPADDRESS_STATUS_ACTIVE, 'Active'),
(IPADDRESS_STATUS_RESERVED, 'Reserved'),
(IPADDRESS_STATUS_DEPRECATED, 'Deprecated'),
(IPADDRESS_STATUS_DHCP, 'DHCP')
)
# IP address roles
IPADDRESS_ROLE_LOOPBACK = 10
IPADDRESS_ROLE_SECONDARY = 20
IPADDRESS_ROLE_ANYCAST = 30
IPADDRESS_ROLE_VIP = 40
IPADDRESS_ROLE_VRRP = 41
IPADDRESS_ROLE_HSRP = 42
IPADDRESS_ROLE_GLBP = 43
IPADDRESS_ROLE_CHOICES = (
(IPADDRESS_ROLE_LOOPBACK, 'Loopback'),
(IPADDRESS_ROLE_SECONDARY, 'Secondary'),
(IPADDRESS_ROLE_ANYCAST, 'Anycast'),
(IPADDRESS_ROLE_VIP, 'VIP'),
(IPADDRESS_ROLE_VRRP, 'VRRP'),
(IPADDRESS_ROLE_HSRP, 'HSRP'),
(IPADDRESS_ROLE_GLBP, 'GLBP'),
)
# VLAN statuses
VLAN_STATUS_ACTIVE = 1
VLAN_STATUS_RESERVED = 2
VLAN_STATUS_DEPRECATED = 3
VLAN_STATUS_CHOICES = (
(VLAN_STATUS_ACTIVE, 'Active'),
(VLAN_STATUS_RESERVED, 'Reserved'),
(VLAN_STATUS_DEPRECATED, 'Deprecated')
)
# Bootstrap CSS classes for various statuses
STATUS_CHOICE_CLASSES = {
0: 'default',
1: 'primary',
2: 'info',
3: 'danger',
4: 'warning',
5: 'success',
}
# IP protocols (for services)
IP_PROTOCOL_TCP = 6
IP_PROTOCOL_UDP = 17
IP_PROTOCOL_CHOICES = (
(IP_PROTOCOL_TCP, 'TCP'),
(IP_PROTOCOL_UDP, 'UDP'),
)

View File

@@ -11,8 +11,8 @@ from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import ( from .models import (
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
VLAN_STATUS_CHOICES, VLANGroup, VRF, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
) )
@@ -23,7 +23,6 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search', label='Search',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
@@ -45,7 +44,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = VRF model = VRF
fields = ['name', 'rd'] fields = ['name', 'rd', 'enforce_unique']
class RIRFilter(django_filters.FilterSet): class RIRFilter(django_filters.FilterSet):
@@ -53,7 +52,7 @@ class RIRFilter(django_filters.FilterSet):
class Meta: class Meta:
model = RIR model = RIR
fields = ['is_private'] fields = ['name', 'slug', 'is_private']
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet): class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -63,7 +62,6 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search', label='Search',
) )
rir_id = django_filters.ModelMultipleChoiceFilter( rir_id = django_filters.ModelMultipleChoiceFilter(
name='rir',
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
label='RIR (ID)', label='RIR (ID)',
) )
@@ -90,6 +88,13 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class RoleFilter(django_filters.FilterSet):
class Meta:
model = Role
fields = ['name', 'slug']
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in') id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter( q = django_filters.CharFilter(
@@ -105,7 +110,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Mask length', label='Mask length',
) )
vrf_id = NullableModelMultipleChoiceFilter( vrf_id = NullableModelMultipleChoiceFilter(
name='vrf_id',
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
label='VRF', label='VRF',
) )
@@ -116,7 +120,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='VRF (RD)', label='VRF (RD)',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
@@ -127,7 +130,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (slug)', label='Tenant (slug)',
) )
site_id = NullableModelMultipleChoiceFilter( site_id = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
@@ -138,7 +140,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (slug)', label='Site (slug)',
) )
vlan_id = NullableModelMultipleChoiceFilter( vlan_id = NullableModelMultipleChoiceFilter(
name='vlan',
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
label='VLAN (ID)', label='VLAN (ID)',
) )
@@ -147,7 +148,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='VLAN number (1-4095)', label='VLAN number (1-4095)',
) )
role_id = NullableModelMultipleChoiceFilter( role_id = NullableModelMultipleChoiceFilter(
name='role',
queryset=Role.objects.all(), queryset=Role.objects.all(),
label='Role (ID)', label='Role (ID)',
) )
@@ -163,7 +163,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Prefix model = Prefix
fields = ['family'] fields = ['family', 'is_pool']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -207,7 +207,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Mask length', label='Mask length',
) )
vrf_id = NullableModelMultipleChoiceFilter( vrf_id = NullableModelMultipleChoiceFilter(
name='vrf_id',
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
label='VRF', label='VRF',
) )
@@ -218,7 +217,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='VRF (RD)', label='VRF (RD)',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
@@ -240,13 +238,15 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Device (name)', label='Device (name)',
) )
interface_id = django_filters.ModelMultipleChoiceFilter( interface_id = django_filters.ModelMultipleChoiceFilter(
name='interface',
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
label='Interface (ID)', label='Interface (ID)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_STATUS_CHOICES choices=IPADDRESS_STATUS_CHOICES
) )
role = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_ROLE_CHOICES
)
class Meta: class Meta:
model = IPAddress model = IPAddress
@@ -281,7 +281,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
class VLANGroupFilter(django_filters.FilterSet): class VLANGroupFilter(django_filters.FilterSet):
site_id = NullableModelMultipleChoiceFilter( site_id = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
@@ -294,7 +293,7 @@ class VLANGroupFilter(django_filters.FilterSet):
class Meta: class Meta:
model = VLANGroup model = VLANGroup
fields = ['name'] fields = ['name', 'slug']
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -304,7 +303,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search', label='Search',
) )
site_id = NullableModelMultipleChoiceFilter( site_id = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
@@ -315,7 +313,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (slug)', label='Site (slug)',
) )
group_id = NullableModelMultipleChoiceFilter( group_id = NullableModelMultipleChoiceFilter(
name='group',
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
label='Group (ID)', label='Group (ID)',
) )
@@ -326,7 +323,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Group', label='Group',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
@@ -337,7 +333,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (slug)', label='Tenant (slug)',
) )
role_id = NullableModelMultipleChoiceFilter( role_id = NullableModelMultipleChoiceFilter(
name='role',
queryset=Role.objects.all(), queryset=Role.objects.all(),
label='Role (ID)', label='Role (ID)',
) )
@@ -353,7 +348,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = VLAN model = VLAN
fields = ['name', 'vid'] fields = ['vid', 'name']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -368,7 +363,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
class ServiceFilter(django_filters.FilterSet): class ServiceFilter(django_filters.FilterSet):
device_id = django_filters.ModelMultipleChoiceFilter( device_id = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='Device (ID)', label='Device (ID)',
) )

View File

@@ -14,8 +14,8 @@ from utilities.forms import (
add_blank_choice, add_blank_choice,
) )
from .models import ( from .models import (
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
VLANGroup, VLAN_STATUS_CHOICES, VRF, Service, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
) )
@@ -484,7 +484,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = [ fields = [
'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack', 'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack',
'nat_inside', 'tenant_group', 'tenant', 'nat_inside', 'tenant_group', 'tenant',
] ]
@@ -562,7 +562,7 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = ['address', 'status', 'vrf', 'description', 'tenant_group', 'tenant'] fields = ['address', 'vrf', 'status', 'role', 'description', 'tenant_group', 'tenant']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(IPAddressBulkAddForm, self).__init__(*args, **kwargs) super(IPAddressBulkAddForm, self).__init__(*args, **kwargs)
@@ -592,6 +592,11 @@ class IPAddressCSVForm(forms.ModelForm):
choices=IPADDRESS_STATUS_CHOICES, choices=IPADDRESS_STATUS_CHOICES,
help_text='Operational status' help_text='Operational status'
) )
role = CSVChoiceField(
choices=IPADDRESS_ROLE_CHOICES,
required=False,
help_text='Functional role'
)
device = FlexibleModelChoiceField( device = FlexibleModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
@@ -612,7 +617,7 @@ class IPAddressCSVForm(forms.ModelForm):
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description'] fields = ['address', 'vrf', 'tenant', 'status', 'role', 'device', 'interface_name', 'is_primary', 'description']
def clean(self): def clean(self):
@@ -665,10 +670,11 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF') vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
status = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_STATUS_CHOICES), required=False) status = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_STATUS_CHOICES), required=False)
role = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_ROLE_CHOICES), required=False)
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
class Meta: class Meta:
nullable_fields = ['vrf', 'tenant', 'description'] nullable_fields = ['vrf', 'role', 'tenant', 'description']
def ipaddress_status_choices(): def ipaddress_status_choices():
@@ -678,6 +684,13 @@ def ipaddress_status_choices():
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES] return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES]
def ipaddress_role_choices():
role_counts = {}
for role in IPAddress.objects.values('role').annotate(count=Count('role')).order_by('role'):
role_counts[role['role']] = role['count']
return [(r[0], '{} ({})'.format(r[1], role_counts.get(r[0], 0))) for r in IPADDRESS_ROLE_CHOICES]
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = IPAddress model = IPAddress
q = forms.CharField(required=False, label='Search') q = forms.CharField(required=False, label='Search')
@@ -698,6 +711,7 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
null_option=(0, 'None') null_option=(0, 'None')
) )
status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False) status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
role = forms.MultipleChoiceField(choices=ipaddress_role_choices, required=False)
# #

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-06-16 19:37
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0016_unicode_literals'),
]
operations = [
migrations.AddField(
model_name='ipaddress',
name='role',
field=models.PositiveSmallIntegerField(blank=True, choices=[(10, 'Loopback'), (20, 'Secondary'), (30, 'Anycast'), (40, 'VIP'), (41, 'VRRP'), (42, 'HSRP'), (43, 'GLBP')], help_text='The functional role of this IP', null=True, verbose_name='Role'),
),
migrations.AlterField(
model_name='ipaddress',
name='status',
field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated'), (5, 'DHCP')], default=1, help_text='The operational status of this IP', verbose_name='Status'),
),
]

View File

@@ -1,6 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import netaddr
from netaddr import IPNetwork, IPSet
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
@@ -17,63 +16,10 @@ from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from utilities.sql import NullsFirstQuerySet from utilities.sql import NullsFirstQuerySet
from utilities.utils import csv_format from utilities.utils import csv_format
from .constants import *
from .fields import IPNetworkField, IPAddressField from .fields import IPNetworkField, IPAddressField
AF_CHOICES = (
(4, 'IPv4'),
(6, 'IPv6'),
)
PREFIX_STATUS_CONTAINER = 0
PREFIX_STATUS_ACTIVE = 1
PREFIX_STATUS_RESERVED = 2
PREFIX_STATUS_DEPRECATED = 3
PREFIX_STATUS_CHOICES = (
(PREFIX_STATUS_CONTAINER, 'Container'),
(PREFIX_STATUS_ACTIVE, 'Active'),
(PREFIX_STATUS_RESERVED, 'Reserved'),
(PREFIX_STATUS_DEPRECATED, 'Deprecated')
)
IPADDRESS_STATUS_ACTIVE = 1
IPADDRESS_STATUS_RESERVED = 2
IPADDRESS_STATUS_DEPRECATED = 3
IPADDRESS_STATUS_DHCP = 5
IPADDRESS_STATUS_CHOICES = (
(IPADDRESS_STATUS_ACTIVE, 'Active'),
(IPADDRESS_STATUS_RESERVED, 'Reserved'),
(IPADDRESS_STATUS_DEPRECATED, 'Deprecated'),
(IPADDRESS_STATUS_DHCP, 'DHCP')
)
VLAN_STATUS_ACTIVE = 1
VLAN_STATUS_RESERVED = 2
VLAN_STATUS_DEPRECATED = 3
VLAN_STATUS_CHOICES = (
(VLAN_STATUS_ACTIVE, 'Active'),
(VLAN_STATUS_RESERVED, 'Reserved'),
(VLAN_STATUS_DEPRECATED, 'Deprecated')
)
STATUS_CHOICE_CLASSES = {
0: 'default',
1: 'primary',
2: 'info',
3: 'danger',
4: 'warning',
5: 'success',
}
IP_PROTOCOL_TCP = 6
IP_PROTOCOL_UDP = 17
IP_PROTOCOL_CHOICES = (
(IP_PROTOCOL_TCP, 'TCP'),
(IP_PROTOCOL_UDP, 'UDP'),
)
@python_2_unicode_compatible @python_2_unicode_compatible
class VRF(CreatedUpdatedModel, CustomFieldModel): class VRF(CreatedUpdatedModel, CustomFieldModel):
""" """
@@ -97,7 +43,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
verbose_name_plural = 'VRFs' verbose_name_plural = 'VRFs'
def __str__(self): def __str__(self):
return self.name return self.display_name or super(VRF, self).__str__()
def get_absolute_url(self): def get_absolute_url(self):
return reverse('ipam:vrf', args=[self.pk]) return reverse('ipam:vrf', args=[self.pk])
@@ -111,6 +57,12 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
self.description, self.description,
]) ])
@property
def display_name(self):
if self.name and self.rd:
return "{} ({})".format(self.name, self.rd)
return None
@python_2_unicode_compatible @python_2_unicode_compatible
class RIR(models.Model): class RIR(models.Model):
@@ -207,7 +159,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
Determine the prefix utilization of the aggregate and return it as a percentage. Determine the prefix utilization of the aggregate and return it as a percentage.
""" """
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix)) queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
child_prefixes = IPSet([p.prefix for p in queryset]) child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
return int(float(child_prefixes.size) / self.prefix.size * 100) return int(float(child_prefixes.size) / self.prefix.size * 100)
@@ -364,6 +316,29 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
def get_duplicates(self): def get_duplicates(self):
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk) return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
def get_child_ips(self):
"""
Return all IPAddresses within this Prefix.
"""
return IPAddress.objects.filter(address__net_contained_or_equal=str(self.prefix), vrf=self.vrf)
def get_available_ips(self):
"""
Return all available IPs within this prefix as an IPSet.
"""
prefix = netaddr.IPSet(self.prefix)
child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()])
available_ips = prefix - child_ips
# Remove unusable IPs from non-pool prefixes
if not self.is_pool:
available_ips -= netaddr.IPSet([
netaddr.IPAddress(self.prefix.first),
netaddr.IPAddress(self.prefix.last),
])
return available_ips
def get_utilization(self): def get_utilization(self):
""" """
Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of
@@ -371,7 +346,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
""" """
if self.status == PREFIX_STATUS_CONTAINER: if self.status == PREFIX_STATUS_CONTAINER:
queryset = Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf) queryset = Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
child_prefixes = IPSet([p.prefix for p in queryset]) child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
return int(float(child_prefixes.size) / self.prefix.size * 100) return int(float(child_prefixes.size) / self.prefix.size * 100)
else: else:
child_count = IPAddress.objects.filter( child_count = IPAddress.objects.filter(
@@ -386,11 +361,11 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
def new_subnet(self): def new_subnet(self):
if self.family == 4: if self.family == 4:
if self.prefix.prefixlen <= 30: if self.prefix.prefixlen <= 30:
return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1)) return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
return None return None
if self.family == 6: if self.family == 6:
if self.prefix.prefixlen <= 126: if self.prefix.prefixlen <= 126:
return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1)) return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
return None return None
@@ -425,7 +400,13 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True, vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
verbose_name='VRF') verbose_name='VRF')
tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT) tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
status = models.PositiveSmallIntegerField('Status', choices=IPADDRESS_STATUS_CHOICES, default=1) status = models.PositiveSmallIntegerField(
'Status', choices=IPADDRESS_STATUS_CHOICES, default=IPADDRESS_STATUS_ACTIVE,
help_text='The operational status of this IP'
)
role = models.PositiveSmallIntegerField(
'Role', choices=IPADDRESS_ROLE_CHOICES, blank=True, null=True, help_text='The functional role of this IP'
)
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True, interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
null=True) null=True)
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True, nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
@@ -436,7 +417,9 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
objects = IPAddressManager() objects = IPAddressManager()
csv_headers = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description'] csv_headers = [
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'interface_name', 'is_primary', 'description',
]
class Meta: class Meta:
ordering = ['family', 'address'] ordering = ['family', 'address']
@@ -488,6 +471,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
self.vrf.rd if self.vrf else None, self.vrf.rd if self.vrf else None,
self.tenant.name if self.tenant else None, self.tenant.name if self.tenant else None,
self.get_status_display(), self.get_status_display(),
self.get_role_display(),
self.device.identifier if self.device else None, self.device.identifier if self.device else None,
self.interface.name if self.interface else None, self.interface.name if self.interface else None,
is_primary, is_primary,

View File

@@ -299,7 +299,7 @@ class IPAddressTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = IPAddress model = IPAddress
fields = ('pk', 'address', 'status', 'vrf', 'tenant', 'nat_inside', 'device', 'description') fields = ('pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'device', 'description')
row_attrs = { row_attrs = {
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '', 'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
} }
@@ -328,7 +328,7 @@ class IPAddressSearchTable(SearchTable):
class Meta(SearchTable.Meta): class Meta(SearchTable.Meta):
model = IPAddress model = IPAddress
fields = ('address', 'status', 'vrf', 'tenant', 'device', 'interface', 'description') fields = ('address', 'vrf', 'status', 'role', 'tenant', 'device', 'interface', 'description')
# #

View File

@@ -367,6 +367,35 @@ class PrefixTest(HttpStatusMixin, APITestCase):
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(Prefix.objects.count(), 2) self.assertEqual(Prefix.objects.count(), 2)
def test_available_ips(self):
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/29'), is_pool=True)
url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
# Retrieve all available IPs
response = self.client.get(url, **self.header)
self.assertEqual(len(response.data), 8) # 8 because prefix.is_pool = True
# Change the prefix to not be a pool and try again
prefix.is_pool = False
prefix.save()
response = self.client.get(url, **self.header)
self.assertEqual(len(response.data), 6) # 8 - 2 because prefix.is_pool = False
# Create all six available IPs
for i in range(6):
data = {
'description': 'Test IP {}'.format(i)
}
response = self.client.post(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(response.data['description'], data['description'])
# Try to create one more IP
response = self.client.post(url, {}, **self.header)
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
self.assertIn('detail', response.data)
class IPAddressTest(HttpStatusMixin, APITestCase): class IPAddressTest(HttpStatusMixin, APITestCase):

View File

@@ -13,7 +13,7 @@ except ImportError:
) )
VERSION = '2.0.10-dev' VERSION = '2.1-beta'
# Import required configuration parameters # Import required configuration parameters
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None ALLOWED_HOSTS = DATABASE = SECRET_KEY = None

View File

@@ -5,13 +5,14 @@ from rest_framework.validators import UniqueTogetherValidator
from dcim.api.serializers import NestedDeviceSerializer from dcim.api.serializers import NestedDeviceSerializer
from secrets.models import Secret, SecretRole from secrets.models import Secret, SecretRole
from utilities.api import ModelValidationMixin
# #
# SecretRoles # SecretRoles
# #
class SecretRoleSerializer(serializers.ModelSerializer): class SecretRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = SecretRole model = SecretRole
@@ -55,4 +56,7 @@ class WritableSecretSerializer(serializers.ModelSerializer):
validator.set_context(self) validator.set_context(self)
validator(data) validator(data)
# Enforce model validation
super(WritableSecretSerializer, self).validate(data)
return data return data

View File

@@ -30,6 +30,7 @@ class SecretRoleViewSet(ModelViewSet):
queryset = SecretRole.objects.all() queryset = SecretRole.objects.all()
serializer_class = serializers.SecretRoleSerializer serializer_class = serializers.SecretRoleSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
filter_class = filters.SecretRoleFilter
# #

View File

@@ -9,6 +9,13 @@ from dcim.models import Device
from utilities.filters import NumericInFilter from utilities.filters import NumericInFilter
class SecretRoleFilter(django_filters.FilterSet):
class Meta:
model = SecretRole
fields = ['name', 'slug']
class SecretFilter(django_filters.FilterSet): class SecretFilter(django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in') id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter( q = django_filters.CharFilter(
@@ -16,7 +23,6 @@ class SecretFilter(django_filters.FilterSet):
label='Search', label='Search',
) )
role_id = django_filters.ModelMultipleChoiceFilter( role_id = django_filters.ModelMultipleChoiceFilter(
name='role',
queryset=SecretRole.objects.all(), queryset=SecretRole.objects.all(),
label='Role (ID)', label='Role (ID)',
) )
@@ -27,7 +33,6 @@ class SecretFilter(django_filters.FilterSet):
label='Role (slug)', label='Role (slug)',
) )
device_id = django_filters.ModelMultipleChoiceFilter( device_id = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='Device (ID)', label='Device (ID)',
) )

View File

@@ -7,7 +7,7 @@
{% block content %} {% block content %}
{% include 'dcim/inc/device_header.html' with active_tab='info' %} {% include 'dcim/inc/device_header.html' with active_tab='info' %}
<div class="row"> <div class="row">
<div class="col-md-5 col-lg-6"> <div class="col-md-5">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Device</strong> <strong>Device</strong>
@@ -214,23 +214,9 @@
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Critical Connections</strong> <strong>Console / Power</strong>
</div> </div>
<table class="table table-hover panel-body component-list"> <table class="table table-hover panel-body component-list">
{% for iface in mgmt_interfaces %}
{% include 'dcim/inc/interface.html' with icon='wrench' %}
{% empty %}
{% if device.device_type.interface_templates.exists %}
<tr>
<td colspan="6" class="alert-warning">
<i class="fa fa-fw fa-warning"></i> No management interfaces defined
{% if perms.dcim.add_interface %}
<a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
{% for cp in console_ports %} {% for cp in console_ports %}
{% include 'dcim/inc/consoleport.html' %} {% include 'dcim/inc/consoleport.html' %}
{% empty %} {% empty %}
@@ -262,11 +248,6 @@
</table> </table>
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %} {% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
<div class="panel-footer text-right"> <div class="panel-footer text-right">
{% if perms.dcim.add_interface %}
<a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interface
</a>
{% endif %}
{% if perms.dcim.add_consoleport %} {% if perms.dcim.add_consoleport %}
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary"> <a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
@@ -333,7 +314,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="col-md-7 col-lg-6"> <div class="col-md-7">
{% if device_bays or device.device_type.is_parent_device %} {% if device_bays or device.device_type.is_parent_device %}
{% if perms.dcim.delete_devicebay %} {% if perms.dcim.delete_devicebay %}
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}"> <form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">

View File

@@ -51,6 +51,8 @@
<th>Manufacturer</th> <th>Manufacturer</th>
<th>Part Number</th> <th>Part Number</th>
<th>Serial Number</th> <th>Serial Number</th>
<th>Asset Tag</th>
<th>Description</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>

View File

@@ -33,7 +33,7 @@
<h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1> <h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-5">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Chassis</strong> <strong>Chassis</strong>
@@ -163,21 +163,20 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
<div class="col-md-7">
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %} {% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %}
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %} {% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
{% include 'dcim/inc/devicetype_component_table.html' with table=mgmt_interface_table title='Management Interfaces' add_url='dcim:devicetype_add_interface' add_url_extra='?mgmt_only=1' edit_url='dcim:devicetype_bulkedit_interface' delete_url='dcim:devicetype_delete_interface' %} {% if devicetype.is_parent_device or devicebay_table.rows %}
</div>
<div class="col-md-6">
{% if devicetype.is_parent_device %}
{% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicetype_add_devicebay' delete_url='dcim:devicetype_delete_devicebay' %} {% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicetype_add_devicebay' delete_url='dcim:devicetype_delete_devicebay' %}
{% endif %} {% endif %}
{% if devicetype.is_network_device %} {% if devicetype.is_network_device or interface_table.rows %}
{% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' edit_url='dcim:devicetype_bulkedit_interface' delete_url='dcim:devicetype_delete_interface' %} {% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' edit_url='dcim:devicetype_bulkedit_interface' delete_url='dcim:devicetype_delete_interface' %}
{% endif %} {% endif %}
{% if devicetype.is_console_server %} {% if devicetype.is_console_server or consoleserverport_table.rows %}
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:devicetype_add_consoleserverport' delete_url='dcim:devicetype_delete_consoleserverport' %} {% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:devicetype_add_consoleserverport' delete_url='dcim:devicetype_delete_consoleserverport' %}
{% endif %} {% endif %}
{% if devicetype.is_pdu %} {% if devicetype.is_pdu or poweroutlet_table.rows %}
{% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' add_url='dcim:devicetype_add_poweroutlet' delete_url='dcim:devicetype_delete_poweroutlet' %} {% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' add_url='dcim:devicetype_add_poweroutlet' delete_url='dcim:devicetype_delete_poweroutlet' %}
{% endif %} {% endif %}
</div> </div>

View File

@@ -1,13 +1,7 @@
<tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}"> <tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_consoleport or perms.dcim.delete_consoleport %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ cp.pk }}" />
</td>
{% endif %}
<td> <td>
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }} <i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
</td> </td>
<td></td>
{% if cp.cs_port %} {% if cp.cs_port %}
<td> <td>
<a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a> <a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
@@ -20,7 +14,7 @@
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td colspan="2" class="text-right"> <td class="text-right">
{% if perms.dcim.change_consoleport %} {% if perms.dcim.change_consoleport %}
{% if cp.cs_port %} {% if cp.cs_port %}
{% if cp.connection_status %} {% if cp.connection_status %}

View File

@@ -1,11 +1,12 @@
<tr class="interface{% if iface.connection and iface.connection.connection_status %} success{% elif iface.connection and not iface.connection.connection_status %} info{% endif %}"> <tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status %} success{% elif iface.connection and not iface.connection.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %} {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ iface.pk }}" /> <input name="pk" type="checkbox" value="{{ iface.pk }}" />
</td> </td>
{% endif %} {% endif %}
<td> <td>
<i class="fa fa-fw fa-{{ icon|default:"exchange" }}"></i> <span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span> <i class="fa fa-fw fa-{% if iface.mgmt_only %}wrench{% elif iface.is_virtual %}square{% elif iface.is_wireless %}wifi{% else %}exchange{% endif %}"></i>
<span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
{% if iface.lag %} {% if iface.lag %}
<span class="label label-primary">{{ iface.lag.name }}</span> <span class="label label-primary">{{ iface.lag.name }}</span>
{% endif %} {% endif %}
@@ -13,6 +14,7 @@
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i> <i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
{% endif %} {% endif %}
</td> </td>
<td>{{ iface.mtu|default:"" }}</td>
<td>{{ iface.mac_address|default:"" }}</td> <td>{{ iface.mac_address|default:"" }}</td>
{% if iface.is_lag %} {% if iface.is_lag %}
<td colspan="2" class="text-muted"> <td colspan="2" class="text-muted">
@@ -21,6 +23,8 @@
</td> </td>
{% elif iface.is_virtual %} {% elif iface.is_virtual %}
<td colspan="2" class="text-muted">Virtual interface</td> <td colspan="2" class="text-muted">Virtual interface</td>
{% elif iface.is_wireless %}
<td colspan="2" class="text-muted">Wireless interface</td>
{% elif iface.connection %} {% elif iface.connection %}
{% with iface.connected_interface as connected_iface %} {% with iface.connected_interface as connected_iface %}
<td> <td>

View File

@@ -1,9 +1,11 @@
<tr> <tr>
<td style="padding-left: {{ indent|add:5 }}px">{{ item.name }}</td> <td style="padding-left: {{ indent|add:5 }}px">{{ item.name }}</td>
<td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td> <td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
<td>{{ item.manufacturer|default:'' }}</td> <td>{{ item.manufacturer|default:"" }}</td>
<td>{{ item.part_id }}</td> <td>{{ item.part_id }}</td>
<td>{{ item.serial }}</td> <td>{{ item.serial }}</td>
<td>{{ item.asset_tag|default:"" }}</td>
<td>{{ item.description }}</td>
<td class="text-right"> <td class="text-right">
{% if perms.dcim.change_inventoryitem %} {% if perms.dcim.change_inventoryitem %}
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a> <a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>

View File

@@ -1,13 +1,7 @@
<tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}"> <tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_powerport or perms.dcim.delete_powerport %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ pp.pk }}" />
</td>
{% endif %}
<td> <td>
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }} <i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
</td> </td>
<td></td>
{% if pp.power_outlet %} {% if pp.power_outlet %}
<td> <td>
<a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a> <a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
@@ -20,7 +14,7 @@
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td colspan="2" class="text-right"> <td class="text-right">
{% if perms.dcim.change_powerport %} {% if perms.dcim.change_powerport %}
{% if pp.power_outlet %} {% if pp.power_outlet %}
{% if pp.connection_status %} {% if pp.connection_status %}

View File

@@ -82,6 +82,12 @@
<span class="label label-{{ ipaddress.get_status_class }}">{{ ipaddress.get_status_display }}</span> <span class="label label-{{ ipaddress.get_status_class }}">{{ ipaddress.get_status_display }}</span>
</td> </td>
</tr> </tr>
<tr>
<td>Role</td>
<td>
<a href="{% url 'ipam:ipaddress_list' %}?role={{ ipaddress.role }}">{{ ipaddress.get_role_display }}</a>
</td>
</tr>
<tr> <tr>
<td>Description</td> <td>Description</td>
<td> <td>

View File

@@ -14,6 +14,7 @@
<div class="panel-body"> <div class="panel-body">
{% render_field pattern_form.pattern %} {% render_field pattern_form.pattern %}
{% render_field model_form.status %} {% render_field model_form.status %}
{% render_field model_form.role %}
{% render_field model_form.vrf %} {% render_field model_form.vrf %}
{% render_field model_form.description %} {% render_field model_form.description %}
</div> </div>

View File

@@ -14,6 +14,7 @@
<div class="panel-body"> <div class="panel-body">
{% render_field form.address %} {% render_field form.address %}
{% render_field form.status %} {% render_field form.status %}
{% render_field form.role %}
{% render_field form.vrf %} {% render_field form.vrf %}
{% render_field form.description %} {% render_field form.description %}
</div> </div>

View File

@@ -4,13 +4,14 @@ from rest_framework import serializers
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from utilities.api import ModelValidationMixin
# #
# Tenant groups # Tenant groups
# #
class TenantGroupSerializer(serializers.ModelSerializer): class TenantGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = TenantGroup model = TenantGroup

View File

@@ -3,8 +3,8 @@ from __future__ import unicode_literals
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from tenancy import filters
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from tenancy.filters import TenantFilter
from utilities.api import WritableSerializerMixin from utilities.api import WritableSerializerMixin
from . import serializers from . import serializers
@@ -16,6 +16,7 @@ from . import serializers
class TenantGroupViewSet(ModelViewSet): class TenantGroupViewSet(ModelViewSet):
queryset = TenantGroup.objects.all() queryset = TenantGroup.objects.all()
serializer_class = serializers.TenantGroupSerializer serializer_class = serializers.TenantGroupSerializer
filter_class = filters.TenantGroupFilter
# #
@@ -26,4 +27,4 @@ class TenantViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Tenant.objects.select_related('group') queryset = Tenant.objects.select_related('group')
serializer_class = serializers.TenantSerializer serializer_class = serializers.TenantSerializer
write_serializer_class = serializers.WritableTenantSerializer write_serializer_class = serializers.WritableTenantSerializer
filter_class = TenantFilter filter_class = filters.TenantFilter

View File

@@ -9,6 +9,13 @@ from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import Tenant, TenantGroup from .models import Tenant, TenantGroup
class TenantGroupFilter(django_filters.FilterSet):
class Meta:
model = TenantGroup
fields = ['name', 'slug']
class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet): class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in') id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter( q = django_filters.CharFilter(
@@ -16,7 +23,6 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search', label='Search',
) )
group_id = NullableModelMultipleChoiceFilter( group_id = NullableModelMultipleChoiceFilter(
name='group',
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
label='Group (ID)', label='Group (ID)',
) )

View File

@@ -98,6 +98,17 @@ class ContentTypeFieldSerializer(Field):
raise ValidationError("Invalid content type") raise ValidationError("Invalid content type")
class ModelValidationMixin(object):
"""
Enforce a model's validation through clean() when validating serializer data. This is necessary to ensure we're
employing the same validation logic via both forms and the API.
"""
def validate(self, attrs):
instance = self.Meta.model(**attrs)
instance.clean()
return attrs
class WritableSerializerMixin(object): class WritableSerializerMixin(object):
""" """
Allow for the use of an alternate, writable serializer class for write operations (e.g. POST, PUT). Allow for the use of an alternate, writable serializer class for write operations (e.g. POST, PUT).