Просмотр исходного кода

Merge pull request #4035 from netbox-community/develop

Release v2.7.3
Jeremy Stretch 6 лет назад
Родитель
Сommit
3143f75a38
61 измененных файлов с 1176 добавлено и 6363 удалено
  1. 13 2
      docs/additional-features/custom-scripts.md
  2. 29 0
      docs/release-notes/version-2.7.md
  3. 15 3
      netbox/circuits/api/serializers.py
  4. 3 1
      netbox/circuits/api/views.py
  5. 2 1
      netbox/circuits/forms.py
  6. 26 2
      netbox/dcim/constants.py
  7. 0 5732
      netbox/dcim/fixtures/dcim.json
  8. 50 17
      netbox/dcim/forms.py
  9. 25 14
      netbox/dcim/models/__init__.py
  10. 397 0
      netbox/dcim/tests/test_api.py
  11. 80 42
      netbox/dcim/tests/test_forms.py
  12. 22 1
      netbox/dcim/tests/test_models.py
  13. 11 5
      netbox/dcim/views.py
  14. 0 35
      netbox/extras/fixtures/extras.json
  15. 32 11
      netbox/extras/scripts.py
  16. 55 1
      netbox/extras/tests/test_scripts.py
  17. 58 4
      netbox/extras/tests/test_webhooks.py
  18. 4 4
      netbox/extras/urls.py
  19. 16 1
      netbox/extras/webhooks.py
  20. 4 10
      netbox/extras/webhooks_worker.py
  21. 43 1
      netbox/ipam/constants.py
  22. 4 9
      netbox/ipam/fields.py
  23. 0 329
      netbox/ipam/fixtures/ipam.json
  24. 33 2
      netbox/ipam/formfields.py
  25. 28 16
      netbox/ipam/forms.py
  26. 1 1
      netbox/ipam/migrations/0029_3569_ipaddress_fields.py
  27. 21 0
      netbox/ipam/migrations/0034_fix_ipaddress_status_dhcp.py
  28. 3 3
      netbox/ipam/models.py
  29. 23 1
      netbox/ipam/validators.py
  30. 7 9
      netbox/ipam/views.py
  31. 2 1
      netbox/netbox/settings.py
  32. 0 0
      netbox/netbox/tests/__init__.py
  33. 13 0
      netbox/netbox/tests/test_api.py
  34. 24 0
      netbox/netbox/tests/test_views.py
  35. 9 7
      netbox/netbox/urls.py
  36. 7 4
      netbox/project-static/js/forms.js
  37. 9 4
      netbox/project-static/js/interface_toggles.js
  38. 5 0
      netbox/secrets/constants.py
  39. 3 2
      netbox/secrets/forms.py
  40. 0 1
      netbox/secrets/tests/test_form.py
  41. 2 19
      netbox/templates/dcim/cable_connect.html
  42. 1 19
      netbox/templates/dcim/cable_edit.html
  43. 1 1
      netbox/templates/dcim/cable_trace.html
  44. 19 0
      netbox/templates/dcim/inc/cable_form.html
  45. 1 1
      netbox/templates/dcim/inc/cable_toggle_buttons.html
  46. 1 1
      netbox/templates/dcim/inc/consoleport.html
  47. 1 1
      netbox/templates/dcim/inc/consoleserverport.html
  48. 1 1
      netbox/templates/dcim/inc/frontport.html
  49. 1 1
      netbox/templates/dcim/inc/interface.html
  50. 1 1
      netbox/templates/dcim/inc/poweroutlet.html
  51. 1 1
      netbox/templates/dcim/inc/powerport.html
  52. 1 1
      netbox/templates/dcim/inc/rearport.html
  53. 3 7
      netbox/templates/dcim/rack_elevation_list.html
  54. 1 1
      netbox/templates/ipam/ipaddress_edit.html
  55. 0 1
      netbox/templates/virtualization/cluster_edit.html
  56. 0 1
      netbox/utilities/api.py
  57. 1 1
      netbox/utilities/choices.py
  58. 1 4
      netbox/utilities/middleware.py
  59. 50 0
      netbox/utilities/tests/test_choices.py
  60. 1 17
      netbox/utilities/validators.py
  61. 11 8
      netbox/virtualization/forms.py

+ 13 - 2
docs/additional-features/custom-scripts.md

@@ -124,7 +124,7 @@ Arbitrary text of any length. Renders as multi-line text input field.
 
 
 Stored a numeric integer. Options include:
 Stored a numeric integer. Options include:
 
 
-* `min_value:` - Minimum value
+* `min_value` - Minimum value
 * `max_value` - Maximum value
 * `max_value` - Maximum value
 
 
 ### BooleanVar
 ### BooleanVar
@@ -158,9 +158,20 @@ A NetBox object. The list of available objects is defined by the queryset parame
 
 
 An uploaded file. Note that uploaded files are present in memory only for the duration of the script's execution: They will not be save for future use.
 An uploaded file. Note that uploaded files are present in memory only for the duration of the script's execution: They will not be save for future use.
 
 
+### IPAddressVar
+
+An IPv4 or IPv6 address, without a mask. Returns a `netaddr.IPAddress` object.
+
+### IPAddressWithMaskVar
+
+An IPv4 or IPv6 address with a mask. Returns a `netaddr.IPNetwork` object which includes the mask.
+
 ### IPNetworkVar
 ### IPNetworkVar
 
 
-An IPv4 or IPv6 network with a mask.
+An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two attributes are available to validate the provided mask:
+
+* `min_prefix_length` - Minimum length of the mask (default: none)
+* `max_prefix_length` - Maximum length of the mask (default: none)
 
 
 ### Default Options
 ### Default Options
 
 

+ 29 - 0
docs/release-notes/version-2.7.md

@@ -1,3 +1,32 @@
+# v2.7.3 (2020-01-28)
+
+## Enhancements
+
+* [#3310](https://github.com/netbox-community/netbox/issues/3310) - Pre-select site/rack for B side when creating a new cable
+* [#3338](https://github.com/netbox-community/netbox/issues/3338) - Include circuit terminations in API representation of circuits
+* [#3509](https://github.com/netbox-community/netbox/issues/3509) - Add IP address variables for custom scripts
+* [#3978](https://github.com/netbox-community/netbox/issues/3978) - Add VRF filtering to search NAT IP
+* [#4005](https://github.com/netbox-community/netbox/issues/4005) - Include timezone context in webhook timestamps
+
+## Bug Fixes
+
+* [#3950](https://github.com/netbox-community/netbox/issues/3950) - Automatically select parent manufacturer when specifying initial device type during device creation
+* [#3982](https://github.com/netbox-community/netbox/issues/3982) - Restore tooltip for reservations on rack elevations
+* [#3983](https://github.com/netbox-community/netbox/issues/3983) - Permit the creation of multiple unnamed devices
+* [#3989](https://github.com/netbox-community/netbox/issues/3989) - Correct HTTP content type assignment for webhooks
+* [#3999](https://github.com/netbox-community/netbox/issues/3999) - Do not filter child results by null if non-required parent fields are blank
+* [#4008](https://github.com/netbox-community/netbox/issues/4008) - Toggle rack elevation face using front/rear strings
+* [#4017](https://github.com/netbox-community/netbox/issues/4017) - Remove redundant tenant field from cluster form
+* [#4019](https://github.com/netbox-community/netbox/issues/4019) - Restore border around background devices in rack elevations
+* [#4022](https://github.com/netbox-community/netbox/issues/4022) - Fix display of assigned IPs when filtering device interfaces
+* [#4025](https://github.com/netbox-community/netbox/issues/4025) - Correct display of cable status (various places)
+* [#4027](https://github.com/netbox-community/netbox/issues/4027) - Repair schema migration for #3569 to convert IP addresses with DHCP status
+* [#4028](https://github.com/netbox-community/netbox/issues/4028) - Correct URL patterns to match Unicode characters in tag slugs
+* [#4030](https://github.com/netbox-community/netbox/issues/4030) - Fix exception when setting interfaces to tagged mode in bulk
+* [#4033](https://github.com/netbox-community/netbox/issues/4033) - Restore missing comments field label of various bulk edit forms
+
+---
+
 # v2.7.2 (2020-01-21)
 # v2.7.2 (2020-01-21)
 
 
 ## Enhancements
 ## Enhancements

+ 15 - 3
netbox/circuits/api/serializers.py

@@ -3,11 +3,11 @@ from taggit_serializer.serializers import TaggitSerializer, TagListSerializerFie
 
 
 from circuits.choices import CircuitStatusChoices
 from circuits.choices import CircuitStatusChoices
 from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
 from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
-from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
+from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer
 from dcim.api.serializers import ConnectedEndpointSerializer
 from dcim.api.serializers import ConnectedEndpointSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
-from utilities.api import ChoiceField, ValidatedModelSerializer
+from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
 from .nested_serializers import *
 from .nested_serializers import *
 
 
 
 
@@ -39,18 +39,30 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug', 'description', 'circuit_count']
         fields = ['id', 'name', 'slug', 'description', 'circuit_count']
 
 
 
 
+class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
+    site = NestedSiteSerializer()
+    connected_endpoint = NestedInterfaceSerializer()
+
+    class Meta:
+        model = CircuitTermination
+        fields = ['id', 'url', 'site', 'connected_endpoint', 'port_speed', 'upstream_speed', 'xconnect_id']
+
+
 class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
 class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
     provider = NestedProviderSerializer()
     provider = NestedProviderSerializer()
     status = ChoiceField(choices=CircuitStatusChoices, required=False)
     status = ChoiceField(choices=CircuitStatusChoices, required=False)
     type = NestedCircuitTypeSerializer()
     type = NestedCircuitTypeSerializer()
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
+    termination_a = CircuitCircuitTerminationSerializer(read_only=True)
+    termination_z = CircuitCircuitTerminationSerializer(read_only=True)
     tags = TagListSerializerField(required=False)
     tags = TagListSerializerField(required=False)
 
 
     class Meta:
     class Meta:
         model = Circuit
         model = Circuit
         fields = [
         fields = [
             'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
             'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
-            'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+            'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
         ]
         ]
 
 
 
 

+ 3 - 1
netbox/circuits/api/views.py

@@ -62,7 +62,9 @@ class CircuitTypeViewSet(ModelViewSet):
 #
 #
 
 
 class CircuitViewSet(CustomFieldModelViewSet):
 class CircuitViewSet(CustomFieldModelViewSet):
-    queryset = Circuit.objects.prefetch_related('type', 'tenant', 'provider').prefetch_related('tags')
+    queryset = Circuit.objects.prefetch_related(
+        'type', 'tenant', 'provider', 'terminations__site', 'terminations__connected_endpoint__device'
+    ).prefetch_related('tags')
     serializer_class = serializers.CircuitSerializer
     serializer_class = serializers.CircuitSerializer
     filterset_class = filters.CircuitFilterSet
     filterset_class = filters.CircuitFilterSet
 
 

+ 2 - 1
netbox/circuits/forms.py

@@ -89,7 +89,8 @@ class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdi
         label='Admin contact'
         label='Admin contact'
     )
     )
     comments = CommentField(
     comments = CommentField(
-        widget=SmallTextarea()
+        widget=SmallTextarea,
+        label='Comments'
     )
     )
 
 
     class Meta:
     class Meta:

+ 26 - 2
netbox/dcim/constants.py

@@ -4,17 +4,30 @@ from .choices import InterfaceTypeChoices
 
 
 
 
 #
 #
-# Rack elevation rendering
+# Racks
 #
 #
 
 
+RACK_U_HEIGHT_DEFAULT = 42
+
 RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 230
 RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 230
 RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 20
 RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 20
 
 
 
 
 #
 #
-# Interface type groups
+# RearPorts
 #
 #
 
 
+REARPORT_POSITIONS_MIN = 1
+REARPORT_POSITIONS_MAX = 64
+
+
+#
+# Interfaces
+#
+
+INTERFACE_MTU_MIN = 1
+INTERFACE_MTU_MAX = 32767  # Max value of a signed 16-bit integer
+
 VIRTUAL_IFACE_TYPES = [
 VIRTUAL_IFACE_TYPES = [
     InterfaceTypeChoices.TYPE_VIRTUAL,
     InterfaceTypeChoices.TYPE_VIRTUAL,
     InterfaceTypeChoices.TYPE_LAG,
     InterfaceTypeChoices.TYPE_LAG,
@@ -31,6 +44,17 @@ WIRELESS_IFACE_TYPES = [
 NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
 NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
 
 
 
 
+#
+# PowerFeeds
+#
+
+POWERFEED_VOLTAGE_DEFAULT = 120
+
+POWERFEED_AMPERAGE_DEFAULT = 20
+
+POWERFEED_MAX_UTILIZATION_DEFAULT = 80  # Percentage
+
+
 #
 #
 # Cabling and connections
 # Cabling and connections
 #
 #

+ 0 - 5732
netbox/dcim/fixtures/dcim.json

@@ -1,5732 +0,0 @@
-[
-{
-    "model": "dcim.site",
-    "pk": 1,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "name": "TEST1",
-        "slug": "test1",
-        "facility": "Test Facility",
-        "asn": 65535,
-        "physical_address": "555 Test Ave.\r\nTest, NY 55555",
-        "shipping_address": "",
-        "comments": ""
-    }
-},
-{
-    "model": "dcim.rack",
-    "pk": 1,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "name": "A1R1",
-        "facility_id": "T23A01",
-        "site": 1,
-        "group": null,
-        "u_height": 42,
-        "comments": ""
-    }
-},
-{
-    "model": "dcim.rack",
-    "pk": 2,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "name": "A1R2",
-        "facility_id": "T24A01",
-        "site": 1,
-        "group": null,
-        "u_height": 42,
-        "comments": ""
-    }
-},
-{
-    "model": "dcim.manufacturer",
-    "pk": 1,
-    "fields": {
-        "name": "Juniper",
-        "slug": "juniper"
-    }
-},
-{
-    "model": "dcim.manufacturer",
-    "pk": 2,
-    "fields": {
-        "name": "Opengear",
-        "slug": "opengear"
-    }
-},
-{
-    "model": "dcim.manufacturer",
-    "pk": 3,
-    "fields": {
-        "name": "ServerTech",
-        "slug": "servertech"
-    }
-},
-{
-    "model": "dcim.devicetype",
-    "pk": 1,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "manufacturer": 1,
-        "model": "MX960",
-        "slug": "mx960",
-        "u_height": 16,
-        "is_full_depth": true
-    }
-},
-{
-    "model": "dcim.devicetype",
-    "pk": 2,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "manufacturer": 1,
-        "model": "EX9214",
-        "slug": "ex9214",
-        "u_height": 16,
-        "is_full_depth": true
-    }
-},
-{
-    "model": "dcim.devicetype",
-    "pk": 3,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "manufacturer": 1,
-        "model": "QFX5100-24Q",
-        "slug": "qfx5100-24q",
-        "u_height": 1,
-        "is_full_depth": true
-    }
-},
-{
-    "model": "dcim.devicetype",
-    "pk": 4,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "manufacturer": 1,
-        "model": "QFX5100-48S",
-        "slug": "qfx5100-48s",
-        "u_height": 1,
-        "is_full_depth": true
-    }
-},
-{
-    "model": "dcim.devicetype",
-    "pk": 5,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "manufacturer": 2,
-        "model": "CM4148",
-        "slug": "cm4148",
-        "u_height": 1,
-        "is_full_depth": true
-    }
-},
-{
-    "model": "dcim.devicetype",
-    "pk": 6,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "manufacturer": 3,
-        "model": "CWG-24VYM415C9",
-        "slug": "cwg-24vym415c9",
-        "u_height": 0,
-        "is_full_depth": false
-    }
-},
-{
-    "model": "dcim.consoleporttemplate",
-    "pk": 1,
-    "fields": {
-        "device_type": 1,
-        "name": "Console (RE0)"
-    }
-},
-{
-    "model": "dcim.consoleporttemplate",
-    "pk": 2,
-    "fields": {
-        "device_type": 1,
-        "name": "Console (RE1)"
-    }
-},
-{
-    "model": "dcim.consoleporttemplate",
-    "pk": 3,
-    "fields": {
-        "device_type": 2,
-        "name": "Console (RE0)"
-    }
-},
-{
-    "model": "dcim.consoleporttemplate",
-    "pk": 4,
-    "fields": {
-        "device_type": 2,
-        "name": "Console (RE1)"
-    }
-},
-{
-    "model": "dcim.consoleporttemplate",
-    "pk": 5,
-    "fields": {
-        "device_type": 3,
-        "name": "Console"
-    }
-},
-{
-    "model": "dcim.consoleporttemplate",
-    "pk": 6,
-    "fields": {
-        "device_type": 5,
-        "name": "Console"
-    }
-},
-{
-    "model": "dcim.consoleporttemplate",
-    "pk": 7,
-    "fields": {
-        "device_type": 6,
-        "name": "Serial"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 1,
-    "fields": {
-        "device_type": 3,
-        "name": "Console"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 3,
-    "fields": {
-        "device_type": 4,
-        "name": "Console"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 4,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 1"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 5,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 2"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 6,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 3"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 7,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 4"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 8,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 5"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 9,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 6"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 10,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 7"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 11,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 8"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 12,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 9"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 13,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 10"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 14,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 11"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 15,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 12"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 16,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 13"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 17,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 14"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 18,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 15"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 19,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 16"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 20,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 17"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 21,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 18"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 22,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 19"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 23,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 20"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 24,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 21"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 25,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 22"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 26,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 23"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 27,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 24"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 28,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 25"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 29,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 26"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 30,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 27"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 31,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 28"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 32,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 29"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 33,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 30"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 34,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 31"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 35,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 32"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 36,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 33"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 37,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 34"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 38,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 35"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 39,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 36"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 40,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 37"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 41,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 38"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 42,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 39"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 43,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 40"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 44,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 41"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 45,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 42"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 46,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 43"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 47,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 44"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 48,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 45"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 49,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 46"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 50,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 47"
-    }
-},
-{
-    "model": "dcim.consoleserverporttemplate",
-    "pk": 51,
-    "fields": {
-        "device_type": 5,
-        "name": "Port 48"
-    }
-},
-{
-    "model": "dcim.powerporttemplate",
-    "pk": 1,
-    "fields": {
-        "device_type": 1,
-        "name": "PEM0"
-    }
-},
-{
-    "model": "dcim.powerporttemplate",
-    "pk": 2,
-    "fields": {
-        "device_type": 1,
-        "name": "PEM1"
-    }
-},
-{
-    "model": "dcim.powerporttemplate",
-    "pk": 3,
-    "fields": {
-        "device_type": 1,
-        "name": "PEM2"
-    }
-},
-{
-    "model": "dcim.powerporttemplate",
-    "pk": 4,
-    "fields": {
-        "device_type": 1,
-        "name": "PEM3"
-    }
-},
-{
-    "model": "dcim.powerporttemplate",
-    "pk": 5,
-    "fields": {
-        "device_type": 2,
-        "name": "PEM0"
-    }
-},
-{
-    "model": "dcim.powerporttemplate",
-    "pk": 6,
-    "fields": {
-        "device_type": 2,
-        "name": "PEM1"
-    }
-},
-{
-    "model": "dcim.powerporttemplate",
-    "pk": 7,
-    "fields": {
-        "device_type": 2,
-        "name": "PEM2"
-    }
-},
-{
-    "model": "dcim.powerporttemplate",
-    "pk": 8,
-    "fields": {
-        "device_type": 2,
-        "name": "PEM3"
-    }
-},
-{
-    "model": "dcim.powerporttemplate",
-    "pk": 9,
-    "fields": {
-        "device_type": 4,
-        "name": "PSU0"
-    }
-},
-{
-    "model": "dcim.powerporttemplate",
-    "pk": 11,
-    "fields": {
-        "device_type": 3,
-        "name": "PSU0"
-    }
-},
-{
-    "model": "dcim.powerporttemplate",
-    "pk": 12,
-    "fields": {
-        "device_type": 3,
-        "name": "PSU1"
-    }
-},
-{
-    "model": "dcim.powerporttemplate",
-    "pk": 13,
-    "fields": {
-        "device_type": 4,
-        "name": "PSU1"
-    }
-},
-{
-    "model": "dcim.powerporttemplate",
-    "pk": 14,
-    "fields": {
-        "device_type": 5,
-        "name": "PSU"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 4,
-    "fields": {
-        "device_type": 6,
-        "name": "AA1"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 5,
-    "fields": {
-        "device_type": 6,
-        "name": "AA2"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 6,
-    "fields": {
-        "device_type": 6,
-        "name": "AA3"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 7,
-    "fields": {
-        "device_type": 6,
-        "name": "AA4"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 8,
-    "fields": {
-        "device_type": 6,
-        "name": "AA5"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 9,
-    "fields": {
-        "device_type": 6,
-        "name": "AA6"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 10,
-    "fields": {
-        "device_type": 6,
-        "name": "AA7"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 11,
-    "fields": {
-        "device_type": 6,
-        "name": "AA8"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 12,
-    "fields": {
-        "device_type": 6,
-        "name": "AB1"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 13,
-    "fields": {
-        "device_type": 6,
-        "name": "AB2"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 14,
-    "fields": {
-        "device_type": 6,
-        "name": "AB3"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 15,
-    "fields": {
-        "device_type": 6,
-        "name": "AB4"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 16,
-    "fields": {
-        "device_type": 6,
-        "name": "AB5"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 17,
-    "fields": {
-        "device_type": 6,
-        "name": "AB6"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 18,
-    "fields": {
-        "device_type": 6,
-        "name": "AB7"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 19,
-    "fields": {
-        "device_type": 6,
-        "name": "AB8"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 20,
-    "fields": {
-        "device_type": 6,
-        "name": "AC1"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 21,
-    "fields": {
-        "device_type": 6,
-        "name": "AC2"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 22,
-    "fields": {
-        "device_type": 6,
-        "name": "AC3"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 23,
-    "fields": {
-        "device_type": 6,
-        "name": "AC4"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 24,
-    "fields": {
-        "device_type": 6,
-        "name": "AC5"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 25,
-    "fields": {
-        "device_type": 6,
-        "name": "AC6"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 26,
-    "fields": {
-        "device_type": 6,
-        "name": "AC7"
-    }
-},
-{
-    "model": "dcim.poweroutlettemplate",
-    "pk": 27,
-    "fields": {
-        "device_type": 6,
-        "name": "AC8"
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 1,
-    "fields": {
-        "device_type": 1,
-        "name": "fxp0 (RE0)",
-        "type": 1000,
-        "mgmt_only": true
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 2,
-    "fields": {
-        "device_type": 1,
-        "name": "fxp0 (RE1)",
-        "type": 800,
-        "mgmt_only": true
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 3,
-    "fields": {
-        "device_type": 1,
-        "name": "lo0",
-        "type": 0,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 4,
-    "fields": {
-        "device_type": 2,
-        "name": "fxp0 (RE0)",
-        "type": 1000,
-        "mgmt_only": true
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 5,
-    "fields": {
-        "device_type": 2,
-        "name": "fxp0 (RE1)",
-        "type": 1000,
-        "mgmt_only": true
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 6,
-    "fields": {
-        "device_type": 2,
-        "name": "lo0",
-        "type": 0,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 7,
-    "fields": {
-        "device_type": 3,
-        "name": "em0",
-        "type": 800,
-        "mgmt_only": true
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 8,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/0",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 9,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/1",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 10,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/2",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 11,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/3",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 12,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/4",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 13,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/5",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 14,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/6",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 15,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/7",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 16,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/8",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 17,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/9",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 18,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/10",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 19,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/11",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 20,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/12",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 21,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/13",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 22,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/14",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 23,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/15",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 24,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/16",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 25,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/17",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 26,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/18",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 27,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/19",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 28,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/20",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 29,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/21",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 30,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/0/22",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 31,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/1/0",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 32,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/1/1",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 33,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/1/2",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 34,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/1/3",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 35,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/2/0",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 36,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/2/1",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 37,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/2/2",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 38,
-    "fields": {
-        "device_type": 3,
-        "name": "et-0/2/3",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 138,
-    "fields": {
-        "device_type": 4,
-        "name": "em0",
-        "type": 1000,
-        "mgmt_only": true
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 139,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/0",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 140,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/1",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 141,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/2",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 142,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/3",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 143,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/4",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 144,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/5",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 145,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/6",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 146,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/7",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 147,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/8",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 148,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/9",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 149,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/10",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 150,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/11",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 151,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/12",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 152,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/13",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 153,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/14",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 154,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/15",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 155,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/16",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 156,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/17",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 157,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/18",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 158,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/19",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 159,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/20",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 160,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/21",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 161,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/22",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 162,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/23",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 163,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/24",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 164,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/25",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 165,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/26",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 166,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/27",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 167,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/28",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 168,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/29",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 169,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/30",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 170,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/31",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 171,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/32",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 172,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/33",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 173,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/34",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 174,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/35",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 175,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/36",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 176,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/37",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 177,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/38",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 178,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/39",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 179,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/40",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 180,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/41",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 181,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/42",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 182,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/43",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 183,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/44",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 184,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/45",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 185,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/46",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 186,
-    "fields": {
-        "device_type": 4,
-        "name": "xe-0/0/47",
-        "type": 1200,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 187,
-    "fields": {
-        "device_type": 4,
-        "name": "et-0/0/48",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 188,
-    "fields": {
-        "device_type": 4,
-        "name": "et-0/0/49",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 189,
-    "fields": {
-        "device_type": 4,
-        "name": "et-0/0/50",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 190,
-    "fields": {
-        "device_type": 4,
-        "name": "et-0/0/51",
-        "type": 1400,
-        "mgmt_only": false
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 191,
-    "fields": {
-        "device_type": 5,
-        "name": "eth0",
-        "type": 1000,
-        "mgmt_only": true
-    }
-},
-{
-    "model": "dcim.interfacetemplate",
-    "pk": 192,
-    "fields": {
-        "device_type": 6,
-        "name": "Net",
-        "type": 800,
-        "mgmt_only": true
-    }
-},
-{
-    "model": "dcim.devicerole",
-    "pk": 1,
-    "fields": {
-        "name": "Router",
-        "slug": "router",
-        "color": "purple"
-    }
-},
-{
-    "model": "dcim.devicerole",
-    "pk": 2,
-    "fields": {
-        "name": "Spine Switch",
-        "slug": "spine-switch",
-        "color": "green"
-    }
-},
-{
-    "model": "dcim.devicerole",
-    "pk": 3,
-    "fields": {
-        "name": "Core Switch",
-        "slug": "core-switch",
-        "color": "red"
-    }
-},
-{
-    "model": "dcim.devicerole",
-    "pk": 4,
-    "fields": {
-        "name": "Leaf Switch",
-        "slug": "leaf-switch",
-        "color": "teal"
-    }
-},
-{
-    "model": "dcim.devicerole",
-    "pk": 5,
-    "fields": {
-        "name": "OOB Switch",
-        "slug": "oob-switch",
-        "color": "purple"
-    }
-},
-{
-    "model": "dcim.devicerole",
-    "pk": 6,
-    "fields": {
-        "name": "PDU",
-        "slug": "pdu",
-        "color": "yellow"
-    }
-},
-{
-    "model": "dcim.platform",
-    "pk": 1,
-    "fields": {
-        "name": "Juniper Junos",
-        "slug": "juniper-junos"
-    }
-},
-{
-    "model": "dcim.platform",
-    "pk": 2,
-    "fields": {
-        "name": "Opengear",
-        "slug": "opengear"
-    }
-},
-{
-    "model": "dcim.device",
-    "pk": 1,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "device_type": 1,
-        "device_role": 1,
-        "platform": 1,
-        "name": "test1-edge1",
-        "serial": "5555555555",
-        "site": 1,
-        "rack": 1,
-        "position": 1,
-        "face": "front",
-        "status": true,
-        "primary_ip4": 1,
-        "primary_ip6": null,
-        "comments": ""
-    }
-},
-{
-    "model": "dcim.device",
-    "pk": 2,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "device_type": 2,
-        "device_role": 3,
-        "platform": 1,
-        "name": "test1-core1",
-        "serial": "",
-        "site": 1,
-        "rack": 1,
-        "position": 17,
-        "face": "rear",
-        "status": true,
-        "primary_ip4": 5,
-        "primary_ip6": null,
-        "comments": ""
-    }
-},
-{
-    "model": "dcim.device",
-    "pk": 3,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "device_type": 3,
-        "device_role": 2,
-        "platform": 1,
-        "name": "test1-spine1",
-        "serial": "",
-        "site": 1,
-        "rack": 1,
-        "position": 33,
-        "face": "rear",
-        "status": true,
-        "primary_ip4": null,
-        "primary_ip6": null,
-        "comments": ""
-    }
-},
-{
-    "model": "dcim.device",
-    "pk": 4,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "device_type": 4,
-        "device_role": 4,
-        "platform": 1,
-        "name": "test1-leaf1",
-        "serial": "",
-        "site": 1,
-        "rack": 1,
-        "position": 34,
-        "face": "rear",
-        "status": true,
-        "primary_ip4": null,
-        "primary_ip6": null,
-        "comments": ""
-    }
-},
-{
-    "model": "dcim.device",
-    "pk": 5,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "device_type": 4,
-        "device_role": 4,
-        "platform": 1,
-        "name": "test1-leaf2",
-        "serial": "9823478293748",
-        "site": 1,
-        "rack": 2,
-        "position": 34,
-        "face": "rear",
-        "status": true,
-        "primary_ip4": null,
-        "primary_ip6": null,
-        "comments": ""
-    }
-},
-{
-    "model": "dcim.device",
-    "pk": 6,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "device_type": 3,
-        "device_role": 2,
-        "platform": 1,
-        "name": "test1-spine2",
-        "serial": "45649818158",
-        "site": 1,
-        "rack": 2,
-        "position": 33,
-        "face": "rear",
-        "status": true,
-        "primary_ip4": null,
-        "primary_ip6": null,
-        "comments": ""
-    }
-},
-{
-    "model": "dcim.device",
-    "pk": 7,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "device_type": 1,
-        "device_role": 1,
-        "platform": 1,
-        "name": "test1-edge2",
-        "serial": "7567356345",
-        "site": 1,
-        "rack": 2,
-        "position": 1,
-        "face": "rear",
-        "status": true,
-        "primary_ip4": 3,
-        "primary_ip6": null,
-        "comments": ""
-    }
-},
-{
-    "model": "dcim.device",
-    "pk": 8,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "device_type": 2,
-        "device_role": 3,
-        "platform": 1,
-        "name": "test1-core2",
-        "serial": "67856734534",
-        "site": 1,
-        "rack": 2,
-        "position": 17,
-        "face": "rear",
-        "status": true,
-        "primary_ip4": 19,
-        "primary_ip6": null,
-        "comments": ""
-    }
-},
-{
-    "model": "dcim.device",
-    "pk": 9,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "device_type": 5,
-        "device_role": 5,
-        "platform": 2,
-        "name": "test1-oob1",
-        "serial": "98273942938",
-        "site": 1,
-        "rack": 1,
-        "position": 42,
-        "face": "rear",
-        "status": true,
-        "primary_ip4": null,
-        "primary_ip6": null,
-        "comments": ""
-    }
-},
-{
-    "model": "dcim.device",
-    "pk": 11,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "device_type": 6,
-        "device_role": 6,
-        "platform": null,
-        "name": "test1-pdu1",
-        "serial": "",
-        "site": 1,
-        "rack": 1,
-        "position": null,
-        "face": "",
-        "status": true,
-        "primary_ip4": null,
-        "primary_ip6": null,
-        "comments": ""
-    }
-},
-{
-    "model": "dcim.device",
-    "pk": 12,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "device_type": 6,
-        "device_role": 6,
-        "platform": null,
-        "name": "test1-pdu2",
-        "serial": "",
-        "site": 1,
-        "rack": 2,
-        "position": null,
-        "face": "",
-        "status": true,
-        "primary_ip4": null,
-        "primary_ip6": null,
-        "comments": ""
-    }
-},
-{
-    "model": "dcim.consoleport",
-    "pk": 1,
-    "fields": {
-        "device": 1,
-        "name": "Console (RE0)",
-        "connected_endpoint": 27,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.consoleport",
-    "pk": 2,
-    "fields": {
-        "device": 1,
-        "name": "Console (RE1)",
-        "connected_endpoint": 38,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.consoleport",
-    "pk": 3,
-    "fields": {
-        "device": 2,
-        "name": "Console (RE0)",
-        "connected_endpoint": 5,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.consoleport",
-    "pk": 4,
-    "fields": {
-        "device": 2,
-        "name": "Console (RE1)",
-        "connected_endpoint": 16,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.consoleport",
-    "pk": 5,
-    "fields": {
-        "device": 3,
-        "name": "Console",
-        "connected_endpoint": 49,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.consoleport",
-    "pk": 6,
-    "fields": {
-        "device": 4,
-        "name": "Console",
-        "connected_endpoint": 48,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.consoleport",
-    "pk": 7,
-    "fields": {
-        "device": 5,
-        "name": "Console",
-        "connected_endpoint": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.consoleport",
-    "pk": 8,
-    "fields": {
-        "device": 6,
-        "name": "Console",
-        "connected_endpoint": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.consoleport",
-    "pk": 9,
-    "fields": {
-        "device": 7,
-        "name": "Console (RE0)",
-        "connected_endpoint": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.consoleport",
-    "pk": 10,
-    "fields": {
-        "device": 7,
-        "name": "Console (RE1)",
-        "connected_endpoint": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.consoleport",
-    "pk": 11,
-    "fields": {
-        "device": 8,
-        "name": "Console (RE0)",
-        "connected_endpoint": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.consoleport",
-    "pk": 12,
-    "fields": {
-        "device": 8,
-        "name": "Console (RE1)",
-        "connected_endpoint": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.consoleport",
-    "pk": 13,
-    "fields": {
-        "device": 9,
-        "name": "Console",
-        "connected_endpoint": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.consoleport",
-    "pk": 15,
-    "fields": {
-        "device": 11,
-        "name": "Serial",
-        "connected_endpoint": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.consoleport",
-    "pk": 16,
-    "fields": {
-        "device": 12,
-        "name": "Serial",
-        "connected_endpoint": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 5,
-    "fields": {
-        "device": 9,
-        "name": "Port 1"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 6,
-    "fields": {
-        "device": 9,
-        "name": "Port 10"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 7,
-    "fields": {
-        "device": 9,
-        "name": "Port 11"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 8,
-    "fields": {
-        "device": 9,
-        "name": "Port 12"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 9,
-    "fields": {
-        "device": 9,
-        "name": "Port 13"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 10,
-    "fields": {
-        "device": 9,
-        "name": "Port 14"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 11,
-    "fields": {
-        "device": 9,
-        "name": "Port 15"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 12,
-    "fields": {
-        "device": 9,
-        "name": "Port 16"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 13,
-    "fields": {
-        "device": 9,
-        "name": "Port 17"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 14,
-    "fields": {
-        "device": 9,
-        "name": "Port 18"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 15,
-    "fields": {
-        "device": 9,
-        "name": "Port 19"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 16,
-    "fields": {
-        "device": 9,
-        "name": "Port 2"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 17,
-    "fields": {
-        "device": 9,
-        "name": "Port 20"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 18,
-    "fields": {
-        "device": 9,
-        "name": "Port 21"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 19,
-    "fields": {
-        "device": 9,
-        "name": "Port 22"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 20,
-    "fields": {
-        "device": 9,
-        "name": "Port 23"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 21,
-    "fields": {
-        "device": 9,
-        "name": "Port 24"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 22,
-    "fields": {
-        "device": 9,
-        "name": "Port 25"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 23,
-    "fields": {
-        "device": 9,
-        "name": "Port 26"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 24,
-    "fields": {
-        "device": 9,
-        "name": "Port 27"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 25,
-    "fields": {
-        "device": 9,
-        "name": "Port 28"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 26,
-    "fields": {
-        "device": 9,
-        "name": "Port 29"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 27,
-    "fields": {
-        "device": 9,
-        "name": "Port 3"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 28,
-    "fields": {
-        "device": 9,
-        "name": "Port 30"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 29,
-    "fields": {
-        "device": 9,
-        "name": "Port 31"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 30,
-    "fields": {
-        "device": 9,
-        "name": "Port 32"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 31,
-    "fields": {
-        "device": 9,
-        "name": "Port 33"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 32,
-    "fields": {
-        "device": 9,
-        "name": "Port 34"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 33,
-    "fields": {
-        "device": 9,
-        "name": "Port 35"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 34,
-    "fields": {
-        "device": 9,
-        "name": "Port 36"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 35,
-    "fields": {
-        "device": 9,
-        "name": "Port 37"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 36,
-    "fields": {
-        "device": 9,
-        "name": "Port 38"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 37,
-    "fields": {
-        "device": 9,
-        "name": "Port 39"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 38,
-    "fields": {
-        "device": 9,
-        "name": "Port 4"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 39,
-    "fields": {
-        "device": 9,
-        "name": "Port 40"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 40,
-    "fields": {
-        "device": 9,
-        "name": "Port 41"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 41,
-    "fields": {
-        "device": 9,
-        "name": "Port 42"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 42,
-    "fields": {
-        "device": 9,
-        "name": "Port 43"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 43,
-    "fields": {
-        "device": 9,
-        "name": "Port 44"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 44,
-    "fields": {
-        "device": 9,
-        "name": "Port 45"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 45,
-    "fields": {
-        "device": 9,
-        "name": "Port 46"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 46,
-    "fields": {
-        "device": 9,
-        "name": "Port 47"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 47,
-    "fields": {
-        "device": 9,
-        "name": "Port 48"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 48,
-    "fields": {
-        "device": 9,
-        "name": "Port 5"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 49,
-    "fields": {
-        "device": 9,
-        "name": "Port 6"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 50,
-    "fields": {
-        "device": 9,
-        "name": "Port 7"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 51,
-    "fields": {
-        "device": 9,
-        "name": "Port 8"
-    }
-},
-{
-    "model": "dcim.consoleserverport",
-    "pk": 52,
-    "fields": {
-        "device": 9,
-        "name": "Port 9"
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 1,
-    "fields": {
-        "device": 1,
-        "name": "PEM0",
-        "_connected_poweroutlet": 25,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 2,
-    "fields": {
-        "device": 1,
-        "name": "PEM1",
-        "_connected_poweroutlet": 49,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 3,
-    "fields": {
-        "device": 1,
-        "name": "PEM2",
-        "_connected_poweroutlet": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 4,
-    "fields": {
-        "device": 1,
-        "name": "PEM3",
-        "_connected_poweroutlet": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 5,
-    "fields": {
-        "device": 2,
-        "name": "PEM0",
-        "_connected_poweroutlet": 26,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 6,
-    "fields": {
-        "device": 2,
-        "name": "PEM1",
-        "_connected_poweroutlet": 50,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 7,
-    "fields": {
-        "device": 2,
-        "name": "PEM2",
-        "_connected_poweroutlet": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 8,
-    "fields": {
-        "device": 2,
-        "name": "PEM3",
-        "_connected_poweroutlet": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 9,
-    "fields": {
-        "device": 4,
-        "name": "PSU0",
-        "_connected_poweroutlet": 28,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 10,
-    "fields": {
-        "device": 4,
-        "name": "PSU1",
-        "_connected_poweroutlet": 52,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 11,
-    "fields": {
-        "device": 5,
-        "name": "PSU0",
-        "_connected_poweroutlet": 56,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 12,
-    "fields": {
-        "device": 5,
-        "name": "PSU1",
-        "_connected_poweroutlet": 32,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 13,
-    "fields": {
-        "device": 3,
-        "name": "PSU0",
-        "_connected_poweroutlet": 27,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 14,
-    "fields": {
-        "device": 3,
-        "name": "PSU1",
-        "_connected_poweroutlet": 51,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 15,
-    "fields": {
-        "device": 7,
-        "name": "PEM0",
-        "_connected_poweroutlet": 53,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 16,
-    "fields": {
-        "device": 7,
-        "name": "PEM1",
-        "_connected_poweroutlet": 29,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 17,
-    "fields": {
-        "device": 7,
-        "name": "PEM2",
-        "_connected_poweroutlet": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 18,
-    "fields": {
-        "device": 7,
-        "name": "PEM3",
-        "_connected_poweroutlet": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 19,
-    "fields": {
-        "device": 8,
-        "name": "PEM0",
-        "_connected_poweroutlet": 54,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 20,
-    "fields": {
-        "device": 8,
-        "name": "PEM1",
-        "_connected_poweroutlet": 30,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 21,
-    "fields": {
-        "device": 8,
-        "name": "PEM2",
-        "_connected_poweroutlet": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 22,
-    "fields": {
-        "device": 8,
-        "name": "PEM3",
-        "_connected_poweroutlet": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 23,
-    "fields": {
-        "device": 6,
-        "name": "PSU0",
-        "_connected_poweroutlet": 55,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 24,
-    "fields": {
-        "device": 6,
-        "name": "PSU1",
-        "_connected_poweroutlet": 31,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.powerport",
-    "pk": 25,
-    "fields": {
-        "device": 9,
-        "name": "PSU",
-        "_connected_poweroutlet": null,
-        "connection_status": true
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 25,
-    "fields": {
-        "device": 11,
-        "name": "AA1"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 26,
-    "fields": {
-        "device": 11,
-        "name": "AA2"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 27,
-    "fields": {
-        "device": 11,
-        "name": "AA3"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 28,
-    "fields": {
-        "device": 11,
-        "name": "AA4"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 29,
-    "fields": {
-        "device": 11,
-        "name": "AA5"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 30,
-    "fields": {
-        "device": 11,
-        "name": "AA6"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 31,
-    "fields": {
-        "device": 11,
-        "name": "AA7"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 32,
-    "fields": {
-        "device": 11,
-        "name": "AA8"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 33,
-    "fields": {
-        "device": 11,
-        "name": "AB1"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 34,
-    "fields": {
-        "device": 11,
-        "name": "AB2"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 35,
-    "fields": {
-        "device": 11,
-        "name": "AB3"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 36,
-    "fields": {
-        "device": 11,
-        "name": "AB4"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 37,
-    "fields": {
-        "device": 11,
-        "name": "AB5"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 38,
-    "fields": {
-        "device": 11,
-        "name": "AB6"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 39,
-    "fields": {
-        "device": 11,
-        "name": "AB7"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 40,
-    "fields": {
-        "device": 11,
-        "name": "AB8"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 41,
-    "fields": {
-        "device": 11,
-        "name": "AC1"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 42,
-    "fields": {
-        "device": 11,
-        "name": "AC2"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 43,
-    "fields": {
-        "device": 11,
-        "name": "AC3"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 44,
-    "fields": {
-        "device": 11,
-        "name": "AC4"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 45,
-    "fields": {
-        "device": 11,
-        "name": "AC5"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 46,
-    "fields": {
-        "device": 11,
-        "name": "AC6"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 47,
-    "fields": {
-        "device": 11,
-        "name": "AC7"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 48,
-    "fields": {
-        "device": 11,
-        "name": "AC8"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 49,
-    "fields": {
-        "device": 12,
-        "name": "AA1"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 50,
-    "fields": {
-        "device": 12,
-        "name": "AA2"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 51,
-    "fields": {
-        "device": 12,
-        "name": "AA3"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 52,
-    "fields": {
-        "device": 12,
-        "name": "AA4"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 53,
-    "fields": {
-        "device": 12,
-        "name": "AA5"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 54,
-    "fields": {
-        "device": 12,
-        "name": "AA6"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 55,
-    "fields": {
-        "device": 12,
-        "name": "AA7"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 56,
-    "fields": {
-        "device": 12,
-        "name": "AA8"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 57,
-    "fields": {
-        "device": 12,
-        "name": "AB1"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 58,
-    "fields": {
-        "device": 12,
-        "name": "AB2"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 59,
-    "fields": {
-        "device": 12,
-        "name": "AB3"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 60,
-    "fields": {
-        "device": 12,
-        "name": "AB4"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 61,
-    "fields": {
-        "device": 12,
-        "name": "AB5"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 62,
-    "fields": {
-        "device": 12,
-        "name": "AB6"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 63,
-    "fields": {
-        "device": 12,
-        "name": "AB7"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 64,
-    "fields": {
-        "device": 12,
-        "name": "AB8"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 65,
-    "fields": {
-        "device": 12,
-        "name": "AC1"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 66,
-    "fields": {
-        "device": 12,
-        "name": "AC2"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 67,
-    "fields": {
-        "device": 12,
-        "name": "AC3"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 68,
-    "fields": {
-        "device": 12,
-        "name": "AC4"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 69,
-    "fields": {
-        "device": 12,
-        "name": "AC5"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 70,
-    "fields": {
-        "device": 12,
-        "name": "AC6"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 71,
-    "fields": {
-        "device": 12,
-        "name": "AC7"
-    }
-},
-{
-    "model": "dcim.poweroutlet",
-    "pk": 72,
-    "fields": {
-        "device": 12,
-        "name": "AC8"
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 1,
-    "fields": {
-        "device": 1,
-        "name": "fxp0 (RE0)",
-        "type": 1000,
-        "mgmt_only": true,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 2,
-    "fields": {
-        "device": 1,
-        "name": "fxp0 (RE1)",
-        "type": 800,
-        "mgmt_only": true,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 3,
-    "fields": {
-        "device": 1,
-        "name": "lo0",
-        "type": 0,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 4,
-    "fields": {
-        "device": 1,
-        "name": "xe-0/0/0",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": "TEST"
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 5,
-    "fields": {
-        "device": 1,
-        "name": "xe-0/0/1",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 6,
-    "fields": {
-        "device": 1,
-        "name": "xe-0/0/2",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 7,
-    "fields": {
-        "device": 1,
-        "name": "xe-0/0/3",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 8,
-    "fields": {
-        "device": 1,
-        "name": "xe-0/0/4",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 9,
-    "fields": {
-        "device": 1,
-        "name": "xe-0/0/5",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 10,
-    "fields": {
-        "device": 2,
-        "name": "fxp0 (RE0)",
-        "type": 1000,
-        "mgmt_only": true,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 11,
-    "fields": {
-        "device": 2,
-        "name": "fxp0 (RE1)",
-        "type": 1000,
-        "mgmt_only": true,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 12,
-    "fields": {
-        "device": 2,
-        "name": "lo0",
-        "type": 0,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 13,
-    "fields": {
-        "device": 3,
-        "name": "em0",
-        "mac_address": "00-00-00-AA-BB-CC",
-        "type": 800,
-        "mgmt_only": true,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 14,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/0",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 15,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/1",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 16,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/10",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 17,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/11",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 18,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/12",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 19,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/13",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 20,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/14",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 21,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/15",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 22,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/16",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 23,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/17",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 24,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/18",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 25,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/19",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 26,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/2",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 27,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/20",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 28,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/21",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 29,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/22",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 30,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/3",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 31,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/4",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 32,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/5",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 33,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/6",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 34,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/7",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 35,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/8",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 36,
-    "fields": {
-        "device": 3,
-        "name": "et-0/0/9",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 37,
-    "fields": {
-        "device": 3,
-        "name": "et-0/1/0",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 38,
-    "fields": {
-        "device": 3,
-        "name": "et-0/1/1",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 39,
-    "fields": {
-        "device": 3,
-        "name": "et-0/1/2",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 40,
-    "fields": {
-        "device": 3,
-        "name": "et-0/1/3",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 41,
-    "fields": {
-        "device": 3,
-        "name": "et-0/2/0",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 42,
-    "fields": {
-        "device": 3,
-        "name": "et-0/2/1",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 43,
-    "fields": {
-        "device": 3,
-        "name": "et-0/2/2",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 44,
-    "fields": {
-        "device": 3,
-        "name": "et-0/2/3",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 45,
-    "fields": {
-        "device": 4,
-        "name": "em0",
-        "type": 1000,
-        "mac_address": "ff-ee-dd-33-22-11",
-        "mgmt_only": true,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 46,
-    "fields": {
-        "device": 4,
-        "name": "et-0/0/48",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 47,
-    "fields": {
-        "device": 4,
-        "name": "et-0/0/49",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 48,
-    "fields": {
-        "device": 4,
-        "name": "et-0/0/50",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 49,
-    "fields": {
-        "device": 4,
-        "name": "et-0/0/51",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 50,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/0",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 51,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/1",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 52,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/10",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 53,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/11",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 54,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/12",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 55,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/13",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 56,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/14",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 57,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/15",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 58,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/16",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 59,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/17",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 60,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/18",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 61,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/19",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 62,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/2",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 63,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/20",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 64,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/21",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 65,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/22",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 66,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/23",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 67,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/24",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 68,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/25",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 69,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/26",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 70,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/27",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 71,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/28",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 72,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/29",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 73,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/3",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 74,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/30",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 75,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/31",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 76,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/32",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 77,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/33",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 78,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/34",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 79,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/35",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 80,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/36",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 81,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/37",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 82,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/38",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 83,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/39",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 84,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/4",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 85,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/40",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 86,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/41",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 87,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/42",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 88,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/43",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 89,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/44",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 90,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/45",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 91,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/46",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 92,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/47",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 93,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/5",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 94,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/6",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 95,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/7",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 96,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/8",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 97,
-    "fields": {
-        "device": 4,
-        "name": "xe-0/0/9",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 98,
-    "fields": {
-        "device": 5,
-        "name": "em0",
-        "type": 1000,
-        "mgmt_only": true,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 99,
-    "fields": {
-        "device": 5,
-        "name": "et-0/0/48",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 100,
-    "fields": {
-        "device": 5,
-        "name": "et-0/0/49",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 101,
-    "fields": {
-        "device": 5,
-        "name": "et-0/0/50",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 102,
-    "fields": {
-        "device": 5,
-        "name": "et-0/0/51",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 103,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/0",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 104,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/1",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 105,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/10",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 106,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/11",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 107,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/12",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 108,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/13",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 109,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/14",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 110,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/15",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 111,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/16",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 112,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/17",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 113,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/18",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 114,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/19",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 115,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/2",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 116,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/20",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 117,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/21",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 118,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/22",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 119,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/23",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 120,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/24",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 121,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/25",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 122,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/26",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 123,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/27",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 124,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/28",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 125,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/29",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 126,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/3",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 127,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/30",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 128,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/31",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 129,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/32",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 130,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/33",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 131,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/34",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 132,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/35",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 133,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/36",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 134,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/37",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 135,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/38",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 136,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/39",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 137,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/4",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 138,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/40",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 139,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/41",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 140,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/42",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 141,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/43",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 142,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/44",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 143,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/45",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 144,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/46",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 145,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/47",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 146,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/5",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 147,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/6",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 148,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/7",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 149,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/8",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 150,
-    "fields": {
-        "device": 5,
-        "name": "xe-0/0/9",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 151,
-    "fields": {
-        "device": 6,
-        "name": "em0",
-        "type": 800,
-        "mgmt_only": true,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 152,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/0",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 153,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/1",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 154,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/10",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 155,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/11",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 156,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/12",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 157,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/13",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 158,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/14",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 159,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/15",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 160,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/16",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 161,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/17",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 162,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/18",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 163,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/19",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 164,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/2",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 165,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/20",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 166,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/21",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 167,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/22",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 168,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/3",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 169,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/4",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 170,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/5",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 171,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/6",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 172,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/7",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 173,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/8",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 174,
-    "fields": {
-        "device": 6,
-        "name": "et-0/0/9",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 175,
-    "fields": {
-        "device": 6,
-        "name": "et-0/1/0",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 176,
-    "fields": {
-        "device": 6,
-        "name": "et-0/1/1",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 177,
-    "fields": {
-        "device": 6,
-        "name": "et-0/1/2",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 178,
-    "fields": {
-        "device": 6,
-        "name": "et-0/1/3",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 179,
-    "fields": {
-        "device": 6,
-        "name": "et-0/2/0",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 180,
-    "fields": {
-        "device": 6,
-        "name": "et-0/2/1",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 181,
-    "fields": {
-        "device": 6,
-        "name": "et-0/2/2",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 182,
-    "fields": {
-        "device": 6,
-        "name": "et-0/2/3",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 183,
-    "fields": {
-        "device": 7,
-        "name": "fxp0 (RE0)",
-        "type": 1000,
-        "mgmt_only": true,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 184,
-    "fields": {
-        "device": 7,
-        "name": "fxp0 (RE1)",
-        "type": 800,
-        "mgmt_only": true,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 185,
-    "fields": {
-        "device": 7,
-        "name": "lo0",
-        "type": 0,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 186,
-    "fields": {
-        "device": 8,
-        "name": "fxp0 (RE0)",
-        "type": 1000,
-        "mgmt_only": true,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 187,
-    "fields": {
-        "device": 8,
-        "name": "fxp0 (RE1)",
-        "type": 1000,
-        "mgmt_only": true,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 188,
-    "fields": {
-        "device": 8,
-        "name": "lo0",
-        "type": 0,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 189,
-    "fields": {
-        "device": 2,
-        "name": "et-0/0/0",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 190,
-    "fields": {
-        "device": 2,
-        "name": "et-0/0/1",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 191,
-    "fields": {
-        "device": 2,
-        "name": "et-0/0/2",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 192,
-    "fields": {
-        "device": 2,
-        "name": "et-0/1/0",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 193,
-    "fields": {
-        "device": 2,
-        "name": "et-0/1/1",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 194,
-    "fields": {
-        "device": 2,
-        "name": "et-0/1/2",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 195,
-    "fields": {
-        "device": 8,
-        "name": "et-0/0/0",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 196,
-    "fields": {
-        "device": 8,
-        "name": "et-0/0/1",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 197,
-    "fields": {
-        "device": 8,
-        "name": "et-0/0/2",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 198,
-    "fields": {
-        "device": 8,
-        "name": "et-0/1/0",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 199,
-    "fields": {
-        "device": 8,
-        "name": "et-0/1/1",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 200,
-    "fields": {
-        "device": 8,
-        "name": "et-0/1/2",
-        "type": 1400,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 201,
-    "fields": {
-        "device": 2,
-        "name": "xe-0/0/0",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 202,
-    "fields": {
-        "device": 2,
-        "name": "xe-0/0/1",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 203,
-    "fields": {
-        "device": 2,
-        "name": "xe-0/0/2",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 204,
-    "fields": {
-        "device": 2,
-        "name": "xe-0/0/3",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 205,
-    "fields": {
-        "device": 2,
-        "name": "xe-0/0/4",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 206,
-    "fields": {
-        "device": 2,
-        "name": "xe-0/0/5",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 207,
-    "fields": {
-        "device": 8,
-        "name": "xe-0/0/0",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 208,
-    "fields": {
-        "device": 8,
-        "name": "xe-0/0/1",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 209,
-    "fields": {
-        "device": 8,
-        "name": "xe-0/0/2",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 210,
-    "fields": {
-        "device": 8,
-        "name": "xe-0/0/3",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 211,
-    "fields": {
-        "device": 8,
-        "name": "xe-0/0/4",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 212,
-    "fields": {
-        "device": 8,
-        "name": "xe-0/0/5",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 213,
-    "fields": {
-        "device": 7,
-        "name": "xe-0/0/0",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 214,
-    "fields": {
-        "device": 7,
-        "name": "xe-0/0/1",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 215,
-    "fields": {
-        "device": 7,
-        "name": "xe-0/0/2",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 216,
-    "fields": {
-        "device": 7,
-        "name": "xe-0/0/3",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 217,
-    "fields": {
-        "device": 7,
-        "name": "xe-0/0/4",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 218,
-    "fields": {
-        "device": 7,
-        "name": "xe-0/0/5",
-        "type": 1200,
-        "mgmt_only": false,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 219,
-    "fields": {
-        "device": 9,
-        "name": "eth0",
-        "type": 1000,
-        "mac_address": "44-55-66-77-88-99",
-        "mgmt_only": true,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 221,
-    "fields": {
-        "device": 11,
-        "name": "Net",
-        "type": 800,
-        "mgmt_only": true,
-        "description": ""
-    }
-},
-{
-    "model": "dcim.interface",
-    "pk": 222,
-    "fields": {
-        "device": 12,
-        "name": "Net",
-        "type": 800,
-        "mgmt_only": true,
-        "description": ""
-    }
-}
-]

+ 50 - 17
netbox/dcim/forms.py

@@ -5,7 +5,6 @@ from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.postgres.forms.array import SimpleArrayField
 from django.contrib.postgres.forms.array import SimpleArrayField
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.exceptions import ObjectDoesNotExist
-from django.db.models import Q
 from mptt.forms import TreeNodeChoiceField
 from mptt.forms import TreeNodeChoiceField
 from netaddr import EUI
 from netaddr import EUI
 from netaddr.core import AddrFormatError
 from netaddr.core import AddrFormatError
@@ -677,7 +676,8 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
     comments = CommentField(
     comments = CommentField(
-        widget=SmallTextarea
+        widget=SmallTextarea,
+        label='Comments'
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1301,8 +1301,8 @@ class RearPortTemplateCreateForm(ComponentForm):
         widget=StaticSelect2(),
         widget=StaticSelect2(),
     )
     )
     positions = forms.IntegerField(
     positions = forms.IntegerField(
-        min_value=1,
-        max_value=64,
+        min_value=REARPORT_POSITIONS_MIN,
+        max_value=REARPORT_POSITIONS_MAX,
         initial=1,
         initial=1,
         help_text='The number of front ports which may be mapped to each rear port'
         help_text='The number of front ports which may be mapped to each rear port'
     )
     )
@@ -1642,6 +1642,16 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
         if instance and instance.cluster is not None:
         if instance and instance.cluster is not None:
             kwargs['initial']['cluster_group'] = instance.cluster.group
             kwargs['initial']['cluster_group'] = instance.cluster.group
 
 
+        if 'device_type' in kwargs['initial'] and 'manufacturer' not in kwargs['initial']:
+            device_type_id = kwargs['initial']['device_type']
+            manufacturer_id = DeviceType.objects.filter(pk=device_type_id).values_list('manufacturer__pk', flat=True).first()
+            kwargs['initial']['manufacturer'] = manufacturer_id
+
+        if 'cluster' in kwargs['initial'] and 'cluster_group' not in kwargs['initial']:
+            cluster_id = kwargs['initial']['cluster']
+            cluster_group_id = Cluster.objects.filter(pk=cluster_id).values_list('group__pk', flat=True).first()
+            kwargs['initial']['cluster_group'] = cluster_group_id
+
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
         if self.instance.pk:
         if self.instance.pk:
@@ -2123,8 +2133,8 @@ class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
     )
     )
     mtu = forms.IntegerField(
     mtu = forms.IntegerField(
         required=False,
         required=False,
-        min_value=1,
-        max_value=32767,
+        min_value=INTERFACE_MTU_MIN,
+        max_value=INTERFACE_MTU_MAX,
         label='MTU'
         label='MTU'
     )
     )
     mgmt_only = forms.BooleanField(
     mgmt_only = forms.BooleanField(
@@ -2610,8 +2620,8 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
     )
     )
     mtu = forms.IntegerField(
     mtu = forms.IntegerField(
         required=False,
         required=False,
-        min_value=1,
-        max_value=32767,
+        min_value=INTERFACE_MTU_MIN,
+        max_value=INTERFACE_MTU_MAX,
         label='MTU'
         label='MTU'
     )
     )
     mac_address = forms.CharField(
     mac_address = forms.CharField(
@@ -2739,7 +2749,7 @@ class InterfaceCSVForm(forms.ModelForm):
             return self.cleaned_data['enabled']
             return self.cleaned_data['enabled']
 
 
 
 
-class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
+class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
         queryset=Interface.objects.all(),
         queryset=Interface.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
@@ -2765,8 +2775,8 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo
     )
     )
     mtu = forms.IntegerField(
     mtu = forms.IntegerField(
         required=False,
         required=False,
-        min_value=1,
-        max_value=32767,
+        min_value=INTERFACE_MTU_MIN,
+        max_value=INTERFACE_MTU_MAX,
         label='MTU'
         label='MTU'
     )
     )
     mgmt_only = forms.NullBooleanField(
     mgmt_only = forms.NullBooleanField(
@@ -2820,6 +2830,18 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo
         else:
         else:
             self.fields['lag'].choices = []
             self.fields['lag'].choices = []
 
 
+    def clean(self):
+
+        # Untagged interfaces cannot be assigned tagged VLANs
+        if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']:
+            raise forms.ValidationError({
+                'mode': "An access interface cannot have tagged VLANs assigned."
+            })
+
+        # Remove all tagged VLAN assignments from "tagged all" interfaces
+        elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL:
+            self.cleaned_data['tagged_vlans'] = []
+
 
 
 class InterfaceBulkRenameForm(BulkRenameForm):
 class InterfaceBulkRenameForm(BulkRenameForm):
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
@@ -3045,8 +3067,8 @@ class RearPortCreateForm(ComponentForm):
         widget=StaticSelect2(),
         widget=StaticSelect2(),
     )
     )
     positions = forms.IntegerField(
     positions = forms.IntegerField(
-        min_value=1,
-        max_value=64,
+        min_value=REARPORT_POSITIONS_MIN,
+        max_value=REARPORT_POSITIONS_MAX,
         initial=1,
         initial=1,
         help_text='The number of front ports which may be mapped to each rear port'
         help_text='The number of front ports which may be mapped to each rear port'
     )
     )
@@ -3168,6 +3190,11 @@ class ConnectCableToDeviceForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFo
             'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_id', 'type', 'status',
             'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_id', 'type', 'status',
             'label', 'color', 'length', 'length_unit',
             'label', 'color', 'length', 'length_unit',
         ]
         ]
+        widgets = {
+            'status': StaticSelect2,
+            'type': StaticSelect2,
+            'length_unit': StaticSelect2,
+        }
 
 
 
 
 class ConnectCableToConsolePortForm(ConnectCableToDeviceForm):
 class ConnectCableToConsolePortForm(ConnectCableToDeviceForm):
@@ -3363,6 +3390,11 @@ class CableForm(BootstrapMixin, forms.ModelForm):
         fields = [
         fields = [
             'type', 'status', 'label', 'color', 'length', 'length_unit',
             'type', 'status', 'label', 'color', 'length', 'length_unit',
         ]
         ]
+        widgets = {
+            'status': StaticSelect2,
+            'type': StaticSelect2,
+            'length_unit': StaticSelect2,
+        }
 
 
 
 
 class CableCSVForm(forms.ModelForm):
 class CableCSVForm(forms.ModelForm):
@@ -3513,7 +3545,7 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
         required=False
         required=False
     )
     )
     color = forms.CharField(
     color = forms.CharField(
-        max_length=6,
+        max_length=6,  # RGB color code
         required=False,
         required=False,
         widget=ColorSelect()
         widget=ColorSelect()
     )
     )
@@ -3592,7 +3624,7 @@ class CableFilterForm(BootstrapMixin, forms.Form):
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
     color = forms.CharField(
     color = forms.CharField(
-        max_length=6,
+        max_length=6,  # RGB color code
         required=False,
         required=False,
         widget=ColorSelect()
         widget=ColorSelect()
     )
     )
@@ -4387,8 +4419,9 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
     max_utilization = forms.IntegerField(
     max_utilization = forms.IntegerField(
         required=False
         required=False
     )
     )
-    comments = forms.CharField(
-        required=False
+    comments = CommentField(
+        widget=SmallTextarea,
+        label='Comments'
     )
     )
 
 
     class Meta:
     class Meta:

+ 25 - 14
netbox/dcim/models/__init__.py

@@ -405,7 +405,7 @@ class RackElevationHelperMixin:
 
 
     @staticmethod
     @staticmethod
     def _draw_device_rear(drawing, device, start, end, text):
     def _draw_device_rear(drawing, device, start, end, text):
-        rect = drawing.rect(start, end, class_="blocked")
+        rect = drawing.rect(start, end, class_="slot blocked")
         rect.set_desc('{} — {} ({}U) {} {}'.format(
         rect.set_desc('{} — {} ({}U) {} {}'.format(
             device.device_role, device.device_type.display_name,
             device.device_role, device.device_type.display_name,
             device.device_type.u_height, device.asset_tag or '', device.serial or ''
             device.device_type.u_height, device.asset_tag or '', device.serial or ''
@@ -414,7 +414,7 @@ class RackElevationHelperMixin:
         drawing.add(drawing.text(str(device), insert=text))
         drawing.add(drawing.text(str(device), insert=text))
 
 
     @staticmethod
     @staticmethod
-    def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_):
+    def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
         link = drawing.add(
         link = drawing.add(
             drawing.a(
             drawing.a(
                 href='{}?{}'.format(
                 href='{}?{}'.format(
@@ -424,6 +424,10 @@ class RackElevationHelperMixin:
                 target='_top'
                 target='_top'
             )
             )
         )
         )
+        if reservation:
+            link.set_desc('{} — {} · {}'.format(
+                reservation.description, reservation.user, reservation.created
+            ))
         link.add(drawing.rect(start, end, class_=class_))
         link.add(drawing.rect(start, end, class_=class_))
         link.add(drawing.text("add device", insert=text, class_='add-device'))
         link.add(drawing.text("add device", insert=text, class_='add-device'))
 
 
@@ -453,12 +457,13 @@ class RackElevationHelperMixin:
             else:
             else:
                 # Draw shallow devices, reservations, or empty units
                 # Draw shallow devices, reservations, or empty units
                 class_ = 'slot'
                 class_ = 'slot'
+                reservation = reserved_units.get(unit["id"])
                 if device:
                 if device:
                     class_ += ' occupied'
                     class_ += ' occupied'
-                if unit["id"] in reserved_units:
+                if reservation:
                     class_ += ' reserved'
                     class_ += ' reserved'
                 self._draw_empty(
                 self._draw_empty(
-                    drawing, self, start_cordinates, end_cordinates, text_cordinates, unit["id"], face, class_
+                    drawing, self, start_cordinates, end_cordinates, text_cordinates, unit["id"], face, class_, reservation
                 )
                 )
 
 
             unit_cursor += height
             unit_cursor += height
@@ -483,7 +488,12 @@ class RackElevationHelperMixin:
 
 
         return elevation
         return elevation
 
 
-    def get_elevation_svg(self, face=DeviceFaceChoices.FACE_FRONT, unit_width=230, unit_height=20):
+    def get_elevation_svg(
+            self,
+            face=DeviceFaceChoices.FACE_FRONT,
+            unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT,
+            unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT
+    ):
         """
         """
         Return an SVG of the rack elevation
         Return an SVG of the rack elevation
 
 
@@ -493,7 +503,7 @@ class RackElevationHelperMixin:
             height of the elevation
             height of the elevation
         """
         """
         elevation = self.merge_elevations(face)
         elevation = self.merge_elevations(face)
-        reserved_units = self.get_reserved_units().keys()
+        reserved_units = self.get_reserved_units()
 
 
         return self._draw_elevations(elevation, reserved_units, face, unit_width, unit_height)
         return self._draw_elevations(elevation, reserved_units, face, unit_width, unit_height)
 
 
@@ -569,7 +579,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
         help_text='Rail-to-rail width'
         help_text='Rail-to-rail width'
     )
     )
     u_height = models.PositiveSmallIntegerField(
     u_height = models.PositiveSmallIntegerField(
-        default=42,
+        default=RACK_U_HEIGHT_DEFAULT,
         verbose_name='Height (U)',
         verbose_name='Height (U)',
         validators=[MinValueValidator(1), MaxValueValidator(100)]
         validators=[MinValueValidator(1), MaxValueValidator(100)]
     )
     )
@@ -1445,10 +1455,11 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
         # Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary
         # Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary
         # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
         # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
         # of the uniqueness constraint without manual intervention.
         # of the uniqueness constraint without manual intervention.
-        if self.tenant is None and Device.objects.exclude(pk=self.pk).filter(name=self.name, tenant__isnull=True):
-            raise ValidationError({
-                'name': 'A device with this name already exists.'
-            })
+        if self.name and self.tenant is None:
+            if Device.objects.exclude(pk=self.pk).filter(name=self.name, tenant__isnull=True):
+                raise ValidationError({
+                    'name': 'A device with this name already exists.'
+                })
 
 
         super().validate_unique(exclude)
         super().validate_unique(exclude)
 
 
@@ -1858,15 +1869,15 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
     )
     )
     voltage = models.PositiveSmallIntegerField(
     voltage = models.PositiveSmallIntegerField(
         validators=[MinValueValidator(1)],
         validators=[MinValueValidator(1)],
-        default=120
+        default=POWERFEED_VOLTAGE_DEFAULT
     )
     )
     amperage = models.PositiveSmallIntegerField(
     amperage = models.PositiveSmallIntegerField(
         validators=[MinValueValidator(1)],
         validators=[MinValueValidator(1)],
-        default=20
+        default=POWERFEED_AMPERAGE_DEFAULT
     )
     )
     max_utilization = models.PositiveSmallIntegerField(
     max_utilization = models.PositiveSmallIntegerField(
         validators=[MinValueValidator(1), MaxValueValidator(100)],
         validators=[MinValueValidator(1), MaxValueValidator(100)],
-        default=80,
+        default=POWERFEED_MAX_UTILIZATION_DEFAULT,
         help_text="Maximum permissible draw (percentage)"
         help_text="Maximum permissible draw (percentage)"
     )
     )
     available_power = models.PositiveIntegerField(
     available_power = models.PositiveIntegerField(

+ 397 - 0
netbox/dcim/tests/test_api.py

@@ -4,6 +4,7 @@ from netaddr import IPNetwork
 from rest_framework import status
 from rest_framework import status
 
 
 from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
 from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
+from dcim.api import serializers
 from dcim.choices import *
 from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.models import (
 from dcim.models import (
@@ -595,6 +596,21 @@ class RackTest(APITestCase):
 
 
         self.assertEqual(response.data['count'], 42)
         self.assertEqual(response.data['count'], 42)
 
 
+    def test_get_rack_elevation(self):
+
+        url = reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk})
+        response = self.client.get(url, **self.header)
+
+        self.assertEqual(response.data['count'], 42)
+
+    def test_get_rack_elevation_svg(self):
+
+        url = '{}?render=svg'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk}))
+        response = self.client.get(url, **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(response.get('Content-Type'), 'image/svg+xml')
+
     def test_list_racks(self):
     def test_list_racks(self):
 
 
         url = reverse('dcim-api:rack-list')
         url = reverse('dcim-api:rack-list')
@@ -1900,6 +1916,31 @@ class DeviceTest(APITestCase):
         self.assertEqual(response.data['device_role']['id'], self.devicerole1.pk)
         self.assertEqual(response.data['device_role']['id'], self.devicerole1.pk)
         self.assertEqual(response.data['cluster']['id'], self.cluster1.pk)
         self.assertEqual(response.data['cluster']['id'], self.cluster1.pk)
 
 
+    def test_get_device_graphs(self):
+
+        device_ct = ContentType.objects.get_for_model(Device)
+        self.graph1 = Graph.objects.create(
+            type=device_ct,
+            name='Test Graph 1',
+            source='http://example.com/graphs.py?device={{ obj.name }}&foo=1'
+        )
+        self.graph2 = Graph.objects.create(
+            type=device_ct,
+            name='Test Graph 2',
+            source='http://example.com/graphs.py?device={{ obj.name }}&foo=2'
+        )
+        self.graph3 = Graph.objects.create(
+            type=device_ct,
+            name='Test Graph 3',
+            source='http://example.com/graphs.py?device={{ obj.name }}&foo=3'
+        )
+
+        url = reverse('dcim-api:device-graphs', kwargs={'pk': self.device1.pk})
+        response = self.client.get(url, **self.header)
+
+        self.assertEqual(len(response.data), 3)
+        self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?device=Test Device 1&foo=1')
+
     def test_list_devices(self):
     def test_list_devices(self):
 
 
         url = reverse('dcim-api:device-list')
         url = reverse('dcim-api:device-list')
@@ -2134,6 +2175,31 @@ class ConsolePortTest(APITestCase):
         self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
         self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
         self.assertEqual(ConsolePort.objects.count(), 2)
         self.assertEqual(ConsolePort.objects.count(), 2)
 
 
+    def test_trace_consoleport(self):
+
+        peer_device = Device.objects.create(
+            site=Site.objects.first(),
+            device_type=DeviceType.objects.first(),
+            device_role=DeviceRole.objects.first(),
+            name='Peer Device'
+        )
+        console_server_port = ConsoleServerPort.objects.create(
+            device=peer_device,
+            name='Console Server Port 1'
+        )
+        cable = Cable(termination_a=self.consoleport1, termination_b=console_server_port, label='Cable 1')
+        cable.save()
+
+        url = reverse('dcim-api:consoleport-trace', kwargs={'pk': self.consoleport1.pk})
+        response = self.client.get(url, **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(len(response.data), 1)
+        segment1 = response.data[0]
+        self.assertEqual(segment1[0]['name'], self.consoleport1.name)
+        self.assertEqual(segment1[1]['label'], cable.label)
+        self.assertEqual(segment1[2]['name'], console_server_port.name)
+
 
 
 class ConsoleServerPortTest(APITestCase):
 class ConsoleServerPortTest(APITestCase):
 
 
@@ -2245,6 +2311,31 @@ class ConsoleServerPortTest(APITestCase):
         self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
         self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
         self.assertEqual(ConsoleServerPort.objects.count(), 2)
         self.assertEqual(ConsoleServerPort.objects.count(), 2)
 
 
+    def test_trace_consoleserverport(self):
+
+        peer_device = Device.objects.create(
+            site=Site.objects.first(),
+            device_type=DeviceType.objects.first(),
+            device_role=DeviceRole.objects.first(),
+            name='Peer Device'
+        )
+        console_port = ConsolePort.objects.create(
+            device=peer_device,
+            name='Console Port 1'
+        )
+        cable = Cable(termination_a=self.consoleserverport1, termination_b=console_port, label='Cable 1')
+        cable.save()
+
+        url = reverse('dcim-api:consoleserverport-trace', kwargs={'pk': self.consoleserverport1.pk})
+        response = self.client.get(url, **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(len(response.data), 1)
+        segment1 = response.data[0]
+        self.assertEqual(segment1[0]['name'], self.consoleserverport1.name)
+        self.assertEqual(segment1[1]['label'], cable.label)
+        self.assertEqual(segment1[2]['name'], console_port.name)
+
 
 
 class PowerPortTest(APITestCase):
 class PowerPortTest(APITestCase):
 
 
@@ -2358,6 +2449,31 @@ class PowerPortTest(APITestCase):
         self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
         self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
         self.assertEqual(PowerPort.objects.count(), 2)
         self.assertEqual(PowerPort.objects.count(), 2)
 
 
+    def test_trace_powerport(self):
+
+        peer_device = Device.objects.create(
+            site=Site.objects.first(),
+            device_type=DeviceType.objects.first(),
+            device_role=DeviceRole.objects.first(),
+            name='Peer Device'
+        )
+        power_outlet = PowerOutlet.objects.create(
+            device=peer_device,
+            name='Power Outlet 1'
+        )
+        cable = Cable(termination_a=self.powerport1, termination_b=power_outlet, label='Cable 1')
+        cable.save()
+
+        url = reverse('dcim-api:powerport-trace', kwargs={'pk': self.powerport1.pk})
+        response = self.client.get(url, **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(len(response.data), 1)
+        segment1 = response.data[0]
+        self.assertEqual(segment1[0]['name'], self.powerport1.name)
+        self.assertEqual(segment1[1]['label'], cable.label)
+        self.assertEqual(segment1[2]['name'], power_outlet.name)
+
 
 
 class PowerOutletTest(APITestCase):
 class PowerOutletTest(APITestCase):
 
 
@@ -2469,6 +2585,31 @@ class PowerOutletTest(APITestCase):
         self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
         self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
         self.assertEqual(PowerOutlet.objects.count(), 2)
         self.assertEqual(PowerOutlet.objects.count(), 2)
 
 
+    def test_trace_poweroutlet(self):
+
+        peer_device = Device.objects.create(
+            site=Site.objects.first(),
+            device_type=DeviceType.objects.first(),
+            device_role=DeviceRole.objects.first(),
+            name='Peer Device'
+        )
+        power_port = PowerPort.objects.create(
+            device=peer_device,
+            name='Power Port 1'
+        )
+        cable = Cable(termination_a=self.poweroutlet1, termination_b=power_port, label='Cable 1')
+        cable.save()
+
+        url = reverse('dcim-api:poweroutlet-trace', kwargs={'pk': self.poweroutlet1.pk})
+        response = self.client.get(url, **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(len(response.data), 1)
+        segment1 = response.data[0]
+        self.assertEqual(segment1[0]['name'], self.poweroutlet1.name)
+        self.assertEqual(segment1[1]['label'], cable.label)
+        self.assertEqual(segment1[2]['name'], power_port.name)
+
 
 
 class InterfaceTest(APITestCase):
 class InterfaceTest(APITestCase):
 
 
@@ -2673,6 +2814,262 @@ class InterfaceTest(APITestCase):
         self.assertEqual(Interface.objects.count(), 2)
         self.assertEqual(Interface.objects.count(), 2)
 
 
 
 
+class FrontPortTest(APITestCase):
+
+    def setUp(self):
+
+        super().setUp()
+
+        site = Site.objects.create(name='Test Site 1', slug='test-site-1')
+        manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
+        devicetype = DeviceType.objects.create(
+            manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
+        )
+        devicerole = DeviceRole.objects.create(
+            name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
+        )
+        self.device = Device.objects.create(
+            device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site
+        )
+        rear_ports = RearPort.objects.bulk_create((
+            RearPort(device=self.device, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C),
+            RearPort(device=self.device, name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
+            RearPort(device=self.device, name='Rear Port 3', type=PortTypeChoices.TYPE_8P8C),
+            RearPort(device=self.device, name='Rear Port 4', type=PortTypeChoices.TYPE_8P8C),
+            RearPort(device=self.device, name='Rear Port 5', type=PortTypeChoices.TYPE_8P8C),
+            RearPort(device=self.device, name='Rear Port 6', type=PortTypeChoices.TYPE_8P8C),
+        ))
+        self.frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0])
+        self.frontport3 = FrontPort.objects.create(device=self.device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1])
+        self.frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[2])
+
+    def test_get_frontport(self):
+
+        url = reverse('dcim-api:frontport-detail', kwargs={'pk': self.frontport1.pk})
+        response = self.client.get(url, **self.header)
+
+        self.assertEqual(response.data['name'], self.frontport1.name)
+
+    def test_list_frontports(self):
+
+        url = reverse('dcim-api:frontport-list')
+        response = self.client.get(url, **self.header)
+
+        self.assertEqual(response.data['count'], 3)
+
+    def test_list_frontports_brief(self):
+
+        url = reverse('dcim-api:frontport-list')
+        response = self.client.get('{}?brief=1'.format(url), **self.header)
+
+        self.assertEqual(
+            sorted(response.data['results'][0]),
+            ['cable', 'device', 'id', 'name', 'url']
+        )
+
+    def test_create_frontport(self):
+
+        rear_port = RearPort.objects.get(name='Rear Port 4')
+        data = {
+            'device': self.device.pk,
+            'name': 'Front Port 4',
+            'type': PortTypeChoices.TYPE_8P8C,
+            'rear_port': rear_port.pk,
+            'rear_port_position': 1,
+        }
+
+        url = reverse('dcim-api:frontport-list')
+        response = self.client.post(url, data, format='json', **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_201_CREATED)
+        self.assertEqual(FrontPort.objects.count(), 4)
+        frontport4 = FrontPort.objects.get(pk=response.data['id'])
+        self.assertEqual(frontport4.device_id, data['device'])
+        self.assertEqual(frontport4.name, data['name'])
+
+    def test_create_frontport_bulk(self):
+
+        rear_ports = RearPort.objects.filter(frontports__isnull=True)
+        data = [
+            {
+                'device': self.device.pk,
+                'name': 'Front Port 4',
+                'type': PortTypeChoices.TYPE_8P8C,
+                'rear_port': rear_ports[0].pk,
+                'rear_port_position': 1,
+            },
+            {
+                'device': self.device.pk,
+                'name': 'Front Port 5',
+                'type': PortTypeChoices.TYPE_8P8C,
+                'rear_port': rear_ports[1].pk,
+                'rear_port_position': 1,
+            },
+            {
+                'device': self.device.pk,
+                'name': 'Front Port 6',
+                'type': PortTypeChoices.TYPE_8P8C,
+                'rear_port': rear_ports[2].pk,
+                'rear_port_position': 1,
+            },
+        ]
+
+        url = reverse('dcim-api:frontport-list')
+        response = self.client.post(url, data, format='json', **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_201_CREATED)
+        self.assertEqual(FrontPort.objects.count(), 6)
+        self.assertEqual(response.data[0]['name'], data[0]['name'])
+        self.assertEqual(response.data[1]['name'], data[1]['name'])
+        self.assertEqual(response.data[2]['name'], data[2]['name'])
+
+    def test_update_frontport(self):
+
+        rear_port = RearPort.objects.get(name='Rear Port 4')
+        data = {
+            'device': self.device.pk,
+            'name': 'Front Port X',
+            'type': PortTypeChoices.TYPE_110_PUNCH,
+            'rear_port': rear_port.pk,
+            'rear_port_position': 1,
+        }
+
+        url = reverse('dcim-api:frontport-detail', kwargs={'pk': self.frontport1.pk})
+        response = self.client.put(url, data, format='json', **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(FrontPort.objects.count(), 3)
+        frontport1 = FrontPort.objects.get(pk=response.data['id'])
+        self.assertEqual(frontport1.name, data['name'])
+        self.assertEqual(frontport1.type, data['type'])
+        self.assertEqual(frontport1.rear_port, rear_port)
+
+    def test_delete_frontport(self):
+
+        url = reverse('dcim-api:frontport-detail', kwargs={'pk': self.frontport1.pk})
+        response = self.client.delete(url, **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
+        self.assertEqual(FrontPort.objects.count(), 2)
+
+
+class RearPortTest(APITestCase):
+
+    def setUp(self):
+
+        super().setUp()
+
+        site = Site.objects.create(name='Test Site 1', slug='test-site-1')
+        manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
+        devicetype = DeviceType.objects.create(
+            manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
+        )
+        devicerole = DeviceRole.objects.create(
+            name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
+        )
+        self.device = Device.objects.create(
+            device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site
+        )
+        self.rearport1 = RearPort.objects.create(device=self.device, type=PortTypeChoices.TYPE_8P8C, name='Rear Port 1')
+        self.rearport3 = RearPort.objects.create(device=self.device, type=PortTypeChoices.TYPE_8P8C, name='Rear Port 2')
+        self.rearport1 = RearPort.objects.create(device=self.device, type=PortTypeChoices.TYPE_8P8C, name='Rear Port 3')
+
+    def test_get_rearport(self):
+
+        url = reverse('dcim-api:rearport-detail', kwargs={'pk': self.rearport1.pk})
+        response = self.client.get(url, **self.header)
+
+        self.assertEqual(response.data['name'], self.rearport1.name)
+
+    def test_list_rearports(self):
+
+        url = reverse('dcim-api:rearport-list')
+        response = self.client.get(url, **self.header)
+
+        self.assertEqual(response.data['count'], 3)
+
+    def test_list_rearports_brief(self):
+
+        url = reverse('dcim-api:rearport-list')
+        response = self.client.get('{}?brief=1'.format(url), **self.header)
+
+        self.assertEqual(
+            sorted(response.data['results'][0]),
+            ['cable', 'device', 'id', 'name', 'url']
+        )
+
+    def test_create_rearport(self):
+
+        data = {
+            'device': self.device.pk,
+            'name': 'Front Port 4',
+            'type': PortTypeChoices.TYPE_8P8C,
+        }
+
+        url = reverse('dcim-api:rearport-list')
+        response = self.client.post(url, data, format='json', **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_201_CREATED)
+        self.assertEqual(RearPort.objects.count(), 4)
+        rearport4 = RearPort.objects.get(pk=response.data['id'])
+        self.assertEqual(rearport4.device_id, data['device'])
+        self.assertEqual(rearport4.name, data['name'])
+
+    def test_create_rearport_bulk(self):
+
+        data = [
+            {
+                'device': self.device.pk,
+                'name': 'Rear Port 4',
+                'type': PortTypeChoices.TYPE_8P8C,
+            },
+            {
+                'device': self.device.pk,
+                'name': 'Rear Port 5',
+                'type': PortTypeChoices.TYPE_8P8C,
+            },
+            {
+                'device': self.device.pk,
+                'name': 'Rear Port 6',
+                'type': PortTypeChoices.TYPE_8P8C,
+            },
+        ]
+
+        url = reverse('dcim-api:rearport-list')
+        response = self.client.post(url, data, format='json', **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_201_CREATED)
+        self.assertEqual(RearPort.objects.count(), 6)
+        self.assertEqual(response.data[0]['name'], data[0]['name'])
+        self.assertEqual(response.data[1]['name'], data[1]['name'])
+        self.assertEqual(response.data[2]['name'], data[2]['name'])
+
+    def test_update_rearport(self):
+
+        data = {
+            'device': self.device.pk,
+            'name': 'Front Port X',
+            'type': PortTypeChoices.TYPE_110_PUNCH
+        }
+
+        url = reverse('dcim-api:rearport-detail', kwargs={'pk': self.rearport1.pk})
+        response = self.client.put(url, data, format='json', **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(RearPort.objects.count(), 3)
+        rearport1 = RearPort.objects.get(pk=response.data['id'])
+        self.assertEqual(rearport1.name, data['name'])
+        self.assertEqual(rearport1.type, data['type'])
+
+    def test_delete_rearport(self):
+
+        url = reverse('dcim-api:rearport-detail', kwargs={'pk': self.rearport1.pk})
+        response = self.client.delete(url, **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
+        self.assertEqual(RearPort.objects.count(), 2)
+
+
 class DeviceBayTest(APITestCase):
 class DeviceBayTest(APITestCase):
 
 
     def setUp(self):
     def setUp(self):

+ 80 - 42
netbox/dcim/tests/test_forms.py

@@ -2,6 +2,7 @@ from django.test import TestCase
 
 
 from dcim.forms import *
 from dcim.forms import *
 from dcim.models import *
 from dcim.models import *
+from virtualization.models import Cluster, ClusterGroup, ClusterType
 
 
 
 
 def get_id(model, slug):
 def get_id(model, slug):
@@ -10,71 +11,108 @@ def get_id(model, slug):
 
 
 class DeviceTestCase(TestCase):
 class DeviceTestCase(TestCase):
 
 
-    fixtures = ['dcim', 'ipam']
+    @classmethod
+    def setUpTestData(cls):
+
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        rack = Rack.objects.create(name='Rack 1', site=site)
+        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
+        device_type = DeviceType.objects.create(
+            manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', u_height=1
+        )
+        device_role = DeviceRole.objects.create(
+            name='Device Role 1', slug='device-role-1', color='ff0000'
+        )
+        Platform.objects.create(name='Platform 1', slug='platform-1')
+        Device.objects.create(
+            name='Device 1', device_type=device_type, device_role=device_role, site=site, rack=rack, position=1
+        )
+        cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
+        cluster_group = ClusterGroup.objects.create(name='Cluster Group 1', slug='cluster-group-1')
+        Cluster.objects.create(name='Cluster 1', type=cluster_type, group=cluster_group)
 
 
     def test_racked_device(self):
     def test_racked_device(self):
-        test = DeviceForm(data={
-            'name': 'test',
-            'device_role': get_id(DeviceRole, 'leaf-switch'),
+        form = DeviceForm(data={
+            'name': 'New Device',
+            'device_role': DeviceRole.objects.first().pk,
             'tenant': None,
             'tenant': None,
-            'manufacturer': get_id(Manufacturer, 'juniper'),
-            'device_type': get_id(DeviceType, 'qfx5100-48s'),
-            'site': get_id(Site, 'test1'),
-            'rack': '1',
+            'manufacturer': Manufacturer.objects.first().pk,
+            'device_type': DeviceType.objects.first().pk,
+            'site': Site.objects.first().pk,
+            'rack': Rack.objects.first().pk,
             'face': DeviceFaceChoices.FACE_FRONT,
             'face': DeviceFaceChoices.FACE_FRONT,
-            'position': 41,
-            'platform': get_id(Platform, 'juniper-junos'),
+            'position': 2,
+            'platform': Platform.objects.first().pk,
             'status': DeviceStatusChoices.STATUS_ACTIVE,
             'status': DeviceStatusChoices.STATUS_ACTIVE,
         })
         })
-        self.assertTrue(test.is_valid(), test.fields['position'].choices)
-        self.assertTrue(test.save())
+        self.assertTrue(form.is_valid())
+        self.assertTrue(form.save())
 
 
     def test_racked_device_occupied(self):
     def test_racked_device_occupied(self):
-        test = DeviceForm(data={
+        form = DeviceForm(data={
             'name': 'test',
             'name': 'test',
-            'device_role': get_id(DeviceRole, 'leaf-switch'),
+            'device_role': DeviceRole.objects.first().pk,
             'tenant': None,
             'tenant': None,
-            'manufacturer': get_id(Manufacturer, 'juniper'),
-            'device_type': get_id(DeviceType, 'qfx5100-48s'),
-            'site': get_id(Site, 'test1'),
-            'rack': '1',
+            'manufacturer': Manufacturer.objects.first().pk,
+            'device_type': DeviceType.objects.first().pk,
+            'site': Site.objects.first().pk,
+            'rack': Rack.objects.first().pk,
             'face': DeviceFaceChoices.FACE_FRONT,
             'face': DeviceFaceChoices.FACE_FRONT,
             'position': 1,
             'position': 1,
-            'platform': get_id(Platform, 'juniper-junos'),
+            'platform': Platform.objects.first().pk,
             'status': DeviceStatusChoices.STATUS_ACTIVE,
             'status': DeviceStatusChoices.STATUS_ACTIVE,
         })
         })
-        self.assertFalse(test.is_valid())
+        self.assertFalse(form.is_valid())
+        self.assertIn('position', form.errors)
 
 
     def test_non_racked_device(self):
     def test_non_racked_device(self):
-        test = DeviceForm(data={
-            'name': 'test',
-            'device_role': get_id(DeviceRole, 'pdu'),
+        form = DeviceForm(data={
+            'name': 'New Device',
+            'device_role': DeviceRole.objects.first().pk,
             'tenant': None,
             'tenant': None,
-            'manufacturer': get_id(Manufacturer, 'servertech'),
-            'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
-            'site': get_id(Site, 'test1'),
-            'rack': '1',
-            'face': '',
+            'manufacturer': Manufacturer.objects.first().pk,
+            'device_type': DeviceType.objects.first().pk,
+            'site': Site.objects.first().pk,
+            'rack': None,
+            'face': None,
             'position': None,
             'position': None,
-            'platform': None,
+            'platform': Platform.objects.first().pk,
             'status': DeviceStatusChoices.STATUS_ACTIVE,
             'status': DeviceStatusChoices.STATUS_ACTIVE,
         })
         })
-        self.assertTrue(test.is_valid())
-        self.assertTrue(test.save())
+        self.assertTrue(form.is_valid())
+        self.assertTrue(form.save())
 
 
-    def test_non_racked_device_with_face(self):
-        test = DeviceForm(data={
-            'name': 'test',
-            'device_role': get_id(DeviceRole, 'pdu'),
+    def test_non_racked_device_with_face_position(self):
+        form = DeviceForm(data={
+            'name': 'New Device',
+            'device_role': DeviceRole.objects.first().pk,
             'tenant': None,
             'tenant': None,
-            'manufacturer': get_id(Manufacturer, 'servertech'),
-            'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
-            'site': get_id(Site, 'test1'),
-            'rack': '1',
+            'manufacturer': Manufacturer.objects.first().pk,
+            'device_type': DeviceType.objects.first().pk,
+            'site': Site.objects.first().pk,
+            'rack': None,
             'face': DeviceFaceChoices.FACE_REAR,
             'face': DeviceFaceChoices.FACE_REAR,
-            'position': None,
+            'position': 10,
             'platform': None,
             'platform': None,
             'status': DeviceStatusChoices.STATUS_ACTIVE,
             'status': DeviceStatusChoices.STATUS_ACTIVE,
         })
         })
-        self.assertTrue(test.is_valid())
-        self.assertTrue(test.save())
+        self.assertFalse(form.is_valid())
+        self.assertIn('face', form.errors)
+        self.assertIn('position', form.errors)
+
+    def test_initial_data_population(self):
+        device_type = DeviceType.objects.first()
+        cluster = Cluster.objects.first()
+        test = DeviceForm(initial={
+            'device_type': device_type.pk,
+            'device_role': DeviceRole.objects.first().pk,
+            'status': DeviceStatusChoices.STATUS_ACTIVE,
+            'site': Site.objects.first().pk,
+            'cluster': cluster.pk,
+        })
+
+        # Check that the initial value for the manufacturer is set automatically when assigning the device type
+        self.assertEqual(test.initial['manufacturer'], device_type.manufacturer.pk)
+
+        # Check that the initial value for the cluster group is set automatically when assigning the cluster
+        self.assertEqual(test.initial['cluster_group'], cluster.group.pk)

+ 22 - 1
netbox/dcim/tests/test_models.py

@@ -285,7 +285,28 @@ class DeviceTestCase(TestCase):
             name='Device Bay 1'
             name='Device Bay 1'
         )
         )
 
 
-    def test_device_duplicate_name_per_site(self):
+    def test_multiple_unnamed_devices(self):
+
+        device1 = Device(
+            site=self.site,
+            device_type=self.device_type,
+            device_role=self.device_role,
+            name=''
+        )
+        device1.save()
+
+        device2 = Device(
+            site=device1.site,
+            device_type=device1.device_type,
+            device_role=device1.device_role,
+            name=''
+        )
+        device2.full_clean()
+        device2.save()
+
+        self.assertEqual(Device.objects.filter(name='').count(), 2)
+
+    def test_device_duplicate_names(self):
 
 
         device1 = Device(
         device1 = Device(
             site=self.site,
             site=self.site,

+ 11 - 5
netbox/dcim/views.py

@@ -30,6 +30,7 @@ from utilities.views import (
 )
 )
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
 from . import filters, forms, tables
 from . import filters, forms, tables
+from .choices import DeviceFaceChoices
 from .models import (
 from .models import (
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
     DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
@@ -376,16 +377,15 @@ class RackElevationListView(PermissionRequiredMixin, View):
             page = paginator.page(paginator.num_pages)
             page = paginator.page(paginator.num_pages)
 
 
         # Determine rack face
         # Determine rack face
-        if request.GET.get('face') == '1':
-            face_id = 1
-        else:
-            face_id = 0
+        rack_face = request.GET.get('face', DeviceFaceChoices.FACE_FRONT)
+        if rack_face not in DeviceFaceChoices.values():
+            rack_face = DeviceFaceChoices.FACE_FRONT
 
 
         return render(request, 'dcim/rack_elevation_list.html', {
         return render(request, 'dcim/rack_elevation_list.html', {
             'paginator': paginator,
             'paginator': paginator,
             'page': page,
             'page': page,
             'total_count': total_count,
             'total_count': total_count,
-            'face_id': face_id,
+            'rack_face': rack_face,
             'filter_form': forms.RackElevationFilterForm(request.GET),
             'filter_form': forms.RackElevationFilterForm(request.GET),
         })
         })
 
 
@@ -1945,6 +1945,12 @@ class CableCreateView(PermissionRequiredMixin, GetReturnURLMixin, View):
         # Parse initial data manually to avoid setting field values as lists
         # Parse initial data manually to avoid setting field values as lists
         initial_data = {k: request.GET[k] for k in request.GET}
         initial_data = {k: request.GET[k] for k in request.GET}
 
 
+        # Set initial site and rack based on side A termination (if not already set)
+        if 'termination_b_site' not in initial_data:
+            initial_data['termination_b_site'] = getattr(self.obj.termination_a.parent, 'site', None)
+        if 'termination_b_rack' not in initial_data:
+            initial_data['termination_b_rack'] = getattr(self.obj.termination_a.parent, 'rack', None)
+
         form = self.form_class(instance=self.obj, initial=initial_data)
         form = self.form_class(instance=self.obj, initial=initial_data)
 
 
         return render(request, self.template_name, {
         return render(request, self.template_name, {

+ 0 - 35
netbox/extras/fixtures/extras.json

@@ -1,35 +0,0 @@
-[
-{
-    "model": "extras.graph",
-    "pk": 1,
-    "fields": {
-        "type": 300,
-        "weight": 1000,
-        "name": "Site Test Graph",
-        "source": "http://localhost/na.png",
-        "link": ""
-    }
-},
-{
-    "model": "extras.graph",
-    "pk": 2,
-    "fields": {
-        "type": 200,
-        "weight": 1000,
-        "name": "Provider Test Graph",
-        "source": "http://localhost/provider_graph.png",
-        "link": ""
-    }
-},
-{
-    "model": "extras.graph",
-    "pk": 3,
-    "fields": {
-        "type": 100,
-        "weight": 1000,
-        "name": "Interface Test Graph",
-        "source": "http://localhost/interface_graph.png",
-        "link": ""
-    }
-}
-]

+ 32 - 11
netbox/extras/scripts.py

@@ -14,10 +14,10 @@ from django.db import transaction
 from mptt.forms import TreeNodeChoiceField, TreeNodeMultipleChoiceField
 from mptt.forms import TreeNodeChoiceField, TreeNodeMultipleChoiceField
 from mptt.models import MPTTModel
 from mptt.models import MPTTModel
 
 
-from ipam.formfields import IPFormField
-from utilities.exceptions import AbortTransaction
-from utilities.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator
+from ipam.formfields import IPAddressFormField, IPNetworkFormField
+from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
 from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_SUCCESS, LOG_WARNING
 from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_SUCCESS, LOG_WARNING
+from utilities.exceptions import AbortTransaction
 from .forms import ScriptForm
 from .forms import ScriptForm
 from .signals import purge_changelog
 from .signals import purge_changelog
 
 
@@ -27,6 +27,8 @@ __all__ = [
     'ChoiceVar',
     'ChoiceVar',
     'FileVar',
     'FileVar',
     'IntegerVar',
     'IntegerVar',
+    'IPAddressVar',
+    'IPAddressWithMaskVar',
     'IPNetworkVar',
     'IPNetworkVar',
     'MultiObjectVar',
     'MultiObjectVar',
     'ObjectVar',
     'ObjectVar',
@@ -48,15 +50,19 @@ class ScriptVariable:
 
 
     def __init__(self, label='', description='', default=None, required=True):
     def __init__(self, label='', description='', default=None, required=True):
 
 
-        # Default field attributes
-        self.field_attrs = {
-            'help_text': description,
-            'required': required
-        }
+        # Initialize field attributes
+        if not hasattr(self, 'field_attrs'):
+            self.field_attrs = {}
+        if description:
+            self.field_attrs['help_text'] = description
         if label:
         if label:
             self.field_attrs['label'] = label
             self.field_attrs['label'] = label
         if default:
         if default:
             self.field_attrs['initial'] = default
             self.field_attrs['initial'] = default
+        if required:
+            self.field_attrs['required'] = True
+        if 'validators' not in self.field_attrs:
+            self.field_attrs['validators'] = []
 
 
     def as_field(self):
     def as_field(self):
         """
         """
@@ -196,17 +202,32 @@ class FileVar(ScriptVariable):
     form_field = forms.FileField
     form_field = forms.FileField
 
 
 
 
+class IPAddressVar(ScriptVariable):
+    """
+    An IPv4 or IPv6 address without a mask.
+    """
+    form_field = IPAddressFormField
+
+
+class IPAddressWithMaskVar(ScriptVariable):
+    """
+    An IPv4 or IPv6 address with a mask.
+    """
+    form_field = IPNetworkFormField
+
+
 class IPNetworkVar(ScriptVariable):
 class IPNetworkVar(ScriptVariable):
     """
     """
     An IPv4 or IPv6 prefix.
     An IPv4 or IPv6 prefix.
     """
     """
-    form_field = IPFormField
+    form_field = IPNetworkFormField
+    field_attrs = {
+        'validators': [prefix_validator]
+    }
 
 
     def __init__(self, min_prefix_length=None, max_prefix_length=None, *args, **kwargs):
     def __init__(self, min_prefix_length=None, max_prefix_length=None, *args, **kwargs):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
-        self.field_attrs['validators'] = list()
-
         # Optional minimum/maximum prefix lengths
         # Optional minimum/maximum prefix lengths
         if min_prefix_length is not None:
         if min_prefix_length is not None:
             self.field_attrs['validators'].append(
             self.field_attrs['validators'].append(

+ 55 - 1
netbox/extras/tests/test_scripts.py

@@ -1,6 +1,6 @@
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.test import TestCase
 from django.test import TestCase
-from netaddr import IPNetwork
+from netaddr import IPAddress, IPNetwork
 
 
 from dcim.models import DeviceRole
 from dcim.models import DeviceRole
 from extras.scripts import *
 from extras.scripts import *
@@ -186,6 +186,54 @@ class ScriptVariablesTest(TestCase):
         self.assertTrue(form.is_valid())
         self.assertTrue(form.is_valid())
         self.assertEqual(form.cleaned_data['var1'], testfile)
         self.assertEqual(form.cleaned_data['var1'], testfile)
 
 
+    def test_ipaddressvar(self):
+
+        class TestScript(Script):
+
+            var1 = IPAddressVar()
+
+        # Validate IP network enforcement
+        data = {'var1': '1.2.3'}
+        form = TestScript().as_form(data, None)
+        self.assertFalse(form.is_valid())
+        self.assertIn('var1', form.errors)
+
+        # Validate IP mask exclusion
+        data = {'var1': '192.0.2.0/24'}
+        form = TestScript().as_form(data, None)
+        self.assertFalse(form.is_valid())
+        self.assertIn('var1', form.errors)
+
+        # Validate valid data
+        data = {'var1': '192.0.2.1'}
+        form = TestScript().as_form(data, None)
+        self.assertTrue(form.is_valid())
+        self.assertEqual(form.cleaned_data['var1'], IPAddress(data['var1']))
+
+    def test_ipaddresswithmaskvar(self):
+
+        class TestScript(Script):
+
+            var1 = IPAddressWithMaskVar()
+
+        # Validate IP network enforcement
+        data = {'var1': '1.2.3'}
+        form = TestScript().as_form(data, None)
+        self.assertFalse(form.is_valid())
+        self.assertIn('var1', form.errors)
+
+        # Validate IP mask requirement
+        data = {'var1': '192.0.2.0'}
+        form = TestScript().as_form(data, None)
+        self.assertFalse(form.is_valid())
+        self.assertIn('var1', form.errors)
+
+        # Validate valid data
+        data = {'var1': '192.0.2.0/24'}
+        form = TestScript().as_form(data, None)
+        self.assertTrue(form.is_valid())
+        self.assertEqual(form.cleaned_data['var1'], IPNetwork(data['var1']))
+
     def test_ipnetworkvar(self):
     def test_ipnetworkvar(self):
 
 
         class TestScript(Script):
         class TestScript(Script):
@@ -198,6 +246,12 @@ class ScriptVariablesTest(TestCase):
         self.assertFalse(form.is_valid())
         self.assertFalse(form.is_valid())
         self.assertIn('var1', form.errors)
         self.assertIn('var1', form.errors)
 
 
+        # Validate host IP check
+        data = {'var1': '192.0.2.1/24'}
+        form = TestScript().as_form(data, None)
+        self.assertFalse(form.is_valid())
+        self.assertIn('var1', form.errors)
+
         # Validate valid data
         # Validate valid data
         data = {'var1': '192.0.2.0/24'}
         data = {'var1': '192.0.2.0/24'}
         form = TestScript().as_form(data, None)
         form = TestScript().as_form(data, None)

+ 58 - 4
netbox/extras/tests/test_webhooks.py

@@ -1,11 +1,19 @@
+import json
+import uuid
+from unittest.mock import patch
+
 import django_rq
 import django_rq
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
+from django.http import HttpResponse
 from django.urls import reverse
 from django.urls import reverse
+from requests import Session
 from rest_framework import status
 from rest_framework import status
 
 
 from dcim.models import Site
 from dcim.models import Site
 from extras.choices import ObjectChangeActionChoices
 from extras.choices import ObjectChangeActionChoices
 from extras.models import Webhook
 from extras.models import Webhook
+from extras.webhooks import enqueue_webhooks, generate_signature
+from extras.webhooks_worker import process_webhook
 from utilities.testing import APITestCase
 from utilities.testing import APITestCase
 
 
 
 
@@ -22,11 +30,13 @@ class WebhookTest(APITestCase):
     def setUpTestData(cls):
     def setUpTestData(cls):
 
 
         site_ct = ContentType.objects.get_for_model(Site)
         site_ct = ContentType.objects.get_for_model(Site)
-        PAYLOAD_URL = "http://localhost/"
+        DUMMY_URL = "http://localhost/"
+        DUMMY_SECRET = "LOOKATMEIMASECRETSTRING"
+
         webhooks = Webhook.objects.bulk_create((
         webhooks = Webhook.objects.bulk_create((
-            Webhook(name='Site Create Webhook', type_create=True, payload_url=PAYLOAD_URL),
-            Webhook(name='Site Update Webhook', type_update=True, payload_url=PAYLOAD_URL),
-            Webhook(name='Site Delete Webhook', type_delete=True, payload_url=PAYLOAD_URL),
+            Webhook(name='Site Create Webhook', type_create=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET, additional_headers={'X-Foo': 'Bar'}),
+            Webhook(name='Site Update Webhook', type_update=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET),
+            Webhook(name='Site Delete Webhook', type_delete=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET),
         ))
         ))
         for webhook in webhooks:
         for webhook in webhooks:
             webhook.obj_type.set([site_ct])
             webhook.obj_type.set([site_ct])
@@ -87,3 +97,47 @@ class WebhookTest(APITestCase):
         self.assertEqual(job.args[1]['id'], site.pk)
         self.assertEqual(job.args[1]['id'], site.pk)
         self.assertEqual(job.args[2], 'site')
         self.assertEqual(job.args[2], 'site')
         self.assertEqual(job.args[3], ObjectChangeActionChoices.ACTION_DELETE)
         self.assertEqual(job.args[3], ObjectChangeActionChoices.ACTION_DELETE)
+
+    def test_webhooks_worker(self):
+
+        request_id = uuid.uuid4()
+
+        def dummy_send(_, request):
+            """
+            A dummy implementation of Session.send() to be used for testing.
+            Always returns a 200 HTTP response.
+            """
+            webhook = Webhook.objects.get(type_create=True)
+            signature = generate_signature(request.body, webhook.secret)
+
+            # Validate the outgoing request headers
+            self.assertEqual(request.headers['Content-Type'], webhook.http_content_type)
+            self.assertEqual(request.headers['X-Hook-Signature'], signature)
+            self.assertEqual(request.headers['X-Foo'], 'Bar')
+
+            # Validate the outgoing request body
+            body = json.loads(request.body)
+            self.assertEqual(body['event'], 'created')
+            self.assertEqual(body['timestamp'], job.args[4])
+            self.assertEqual(body['model'], 'site')
+            self.assertEqual(body['username'], 'testuser')
+            self.assertEqual(body['request_id'], str(request_id))
+            self.assertEqual(body['data']['name'], 'Site 1')
+
+            return HttpResponse()
+
+        # Enqueue a webhook for processing
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        enqueue_webhooks(
+            instance=site,
+            user=self.user,
+            request_id=request_id,
+            action=ObjectChangeActionChoices.ACTION_CREATE
+        )
+
+        # Retrieve the job from queue
+        job = self.queue.jobs[0]
+
+        # Patch the Session object with our dummy_send() method, then process the webhook for sending
+        with patch.object(Session, 'send', dummy_send) as mock_send:
+            process_webhook(*job.args)

+ 4 - 4
netbox/extras/urls.py

@@ -11,10 +11,10 @@ urlpatterns = [
     path(r'tags/', views.TagListView.as_view(), name='tag_list'),
     path(r'tags/', views.TagListView.as_view(), name='tag_list'),
     path(r'tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'),
     path(r'tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'),
     path(r'tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
     path(r'tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
-    path(r'tags/<slug:slug>/', views.TagView.as_view(), name='tag'),
-    path(r'tags/<slug:slug>/edit/', views.TagEditView.as_view(), name='tag_edit'),
-    path(r'tags/<slug:slug>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
-    path(r'tags/<slug:slug>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
+    path(r'tags/<str:slug>/', views.TagView.as_view(), name='tag'),
+    path(r'tags/<str:slug>/edit/', views.TagEditView.as_view(), name='tag_edit'),
+    path(r'tags/<str:slug>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
+    path(r'tags/<str:slug>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
 
 
     # Config contexts
     # Config contexts
     path(r'config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'),
     path(r'config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'),

+ 16 - 1
netbox/extras/webhooks.py

@@ -1,6 +1,9 @@
 import datetime
 import datetime
+import hashlib
+import hmac
 
 
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
+from django.utils import timezone
 
 
 from extras.models import Webhook
 from extras.models import Webhook
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
@@ -8,6 +11,18 @@ from .choices import *
 from .constants import *
 from .constants import *
 
 
 
 
+def generate_signature(request_body, secret):
+    """
+    Return a cryptographic signature that can be used to verify the authenticity of webhook data.
+    """
+    hmac_prep = hmac.new(
+        key=secret.encode('utf8'),
+        msg=request_body.encode('utf8'),
+        digestmod=hashlib.sha512
+    )
+    return hmac_prep.hexdigest()
+
+
 def enqueue_webhooks(instance, user, request_id, action):
 def enqueue_webhooks(instance, user, request_id, action):
     """
     """
     Find Webhook(s) assigned to this instance + action and enqueue them
     Find Webhook(s) assigned to this instance + action and enqueue them
@@ -48,7 +63,7 @@ def enqueue_webhooks(instance, user, request_id, action):
                 serializer.data,
                 serializer.data,
                 instance._meta.model_name,
                 instance._meta.model_name,
                 action,
                 action,
-                str(datetime.datetime.now()),
+                str(timezone.now()),
                 user.username,
                 user.username,
                 request_id
                 request_id
             )
             )

+ 4 - 10
netbox/extras/webhooks_worker.py

@@ -1,5 +1,3 @@
-import hashlib
-import hmac
 import json
 import json
 
 
 import requests
 import requests
@@ -7,6 +5,7 @@ from django_rq import job
 from rest_framework.utils.encoders import JSONEncoder
 from rest_framework.utils.encoders import JSONEncoder
 
 
 from .choices import ObjectChangeActionChoices, WebhookContentTypeChoices
 from .choices import ObjectChangeActionChoices, WebhookContentTypeChoices
+from .webhooks import generate_signature
 
 
 
 
 @job('default')
 @job('default')
@@ -23,7 +22,7 @@ def process_webhook(webhook, data, model_name, event, timestamp, username, reque
         'data': data
         'data': data
     }
     }
     headers = {
     headers = {
-        'Content-Type': webhook.get_http_content_type_display(),
+        'Content-Type': webhook.http_content_type,
     }
     }
     if webhook.additional_headers:
     if webhook.additional_headers:
         headers.update(webhook.additional_headers)
         headers.update(webhook.additional_headers)
@@ -43,12 +42,7 @@ def process_webhook(webhook, data, model_name, event, timestamp, username, reque
 
 
     if webhook.secret != '':
     if webhook.secret != '':
         # Sign the request with a hash of the secret key and its content.
         # Sign the request with a hash of the secret key and its content.
-        hmac_prep = hmac.new(
-            key=webhook.secret.encode('utf8'),
-            msg=prepared_request.body.encode('utf8'),
-            digestmod=hashlib.sha512
-        )
-        prepared_request.headers['X-Hook-Signature'] = hmac_prep.hexdigest()
+        prepared_request.headers['X-Hook-Signature'] = generate_signature(prepared_request.body, webhook.secret)
 
 
     with requests.Session() as session:
     with requests.Session() as session:
         session.verify = webhook.ssl_verification
         session.verify = webhook.ssl_verification
@@ -56,7 +50,7 @@ def process_webhook(webhook, data, model_name, event, timestamp, username, reque
             session.verify = webhook.ca_file_path
             session.verify = webhook.ca_file_path
         response = session.send(prepared_request)
         response = session.send(prepared_request)
 
 
-    if response.status_code >= 200 and response.status_code <= 299:
+    if 200 <= response.status_code <= 299:
         return 'Status {} returned, webhook successfully processed.'.format(response.status_code)
         return 'Status {} returned, webhook successfully processed.'.format(response.status_code)
     else:
     else:
         raise requests.exceptions.RequestException(
         raise requests.exceptions.RequestException(

+ 43 - 1
netbox/ipam/constants.py

@@ -4,10 +4,34 @@ from .choices import IPAddressRoleChoices
 BGP_ASN_MIN = 1
 BGP_ASN_MIN = 1
 BGP_ASN_MAX = 2**32 - 1
 BGP_ASN_MAX = 2**32 - 1
 
 
+
+#
+# VRFs
+#
+
+# Per RFC 4364 section 4.2, a route distinguisher may be encoded as one of the following:
+#   * Type 0 (16-bit AS number : 32-bit integer)
+#   * Type 1 (32-bit IPv4 address : 16-bit integer)
+#   * Type 2 (32-bit AS number : 16-bit integer)
+# 21 characters are sufficient to convey the longest possible string value (255.255.255.255:65535)
+VRF_RD_MAX_LENGTH = 21
+
+
+#
+# Prefixes
+#
+
+PREFIX_LENGTH_MIN = 1
+PREFIX_LENGTH_MAX = 127  # IPv6
+
+
 #
 #
-# IP addresses
+# IPAddresses
 #
 #
 
 
+IPADDRESS_MASK_LENGTH_MIN = 1
+IPADDRESS_MASK_LENGTH_MAX = 128  # IPv6
+
 IPADDRESS_ROLES_NONUNIQUE = (
 IPADDRESS_ROLES_NONUNIQUE = (
     # IPAddress roles which are exempt from unique address enforcement
     # IPAddress roles which are exempt from unique address enforcement
     IPAddressRoleChoices.ROLE_ANYCAST,
     IPAddressRoleChoices.ROLE_ANYCAST,
@@ -17,3 +41,21 @@ IPADDRESS_ROLES_NONUNIQUE = (
     IPAddressRoleChoices.ROLE_GLBP,
     IPAddressRoleChoices.ROLE_GLBP,
     IPAddressRoleChoices.ROLE_CARP,
     IPAddressRoleChoices.ROLE_CARP,
 )
 )
+
+
+#
+# VLANs
+#
+
+# 12-bit VLAN ID (values 0 and 4095 are reserved)
+VLAN_VID_MIN = 1
+VLAN_VID_MAX = 4094
+
+
+#
+# Services
+#
+
+# 16-bit port number
+SERVICE_PORT_MIN = 1
+SERVICE_PORT_MAX = 65535

+ 4 - 9
netbox/ipam/fields.py

@@ -2,13 +2,8 @@ from django.core.exceptions import ValidationError
 from django.db import models
 from django.db import models
 from netaddr import AddrFormatError, IPNetwork
 from netaddr import AddrFormatError, IPNetwork
 
 
-from . import lookups
-from .formfields import IPFormField
-
-
-def prefix_validator(prefix):
-    if prefix.ip != prefix.cidr.ip:
-        raise ValidationError("{} is not a valid prefix. Did you mean {}?".format(prefix, prefix.cidr))
+from . import lookups, validators
+from .formfields import IPNetworkFormField
 
 
 
 
 class BaseIPField(models.Field):
 class BaseIPField(models.Field):
@@ -38,7 +33,7 @@ class BaseIPField(models.Field):
         return str(self.to_python(value))
         return str(self.to_python(value))
 
 
     def form_class(self):
     def form_class(self):
-        return IPFormField
+        return IPNetworkFormField
 
 
     def formfield(self, **kwargs):
     def formfield(self, **kwargs):
         defaults = {'form_class': self.form_class()}
         defaults = {'form_class': self.form_class()}
@@ -51,7 +46,7 @@ class IPNetworkField(BaseIPField):
     IP prefix (network and mask)
     IP prefix (network and mask)
     """
     """
     description = "PostgreSQL CIDR field"
     description = "PostgreSQL CIDR field"
-    default_validators = [prefix_validator]
+    default_validators = [validators.prefix_validator]
 
 
     def db_type(self, connection):
     def db_type(self, connection):
         return 'cidr'
         return 'cidr'

+ 0 - 329
netbox/ipam/fixtures/ipam.json

@@ -1,329 +0,0 @@
-[
-{
-    "model": "ipam.rir",
-    "pk": 1,
-    "fields": {
-        "name": "RFC1918",
-        "slug": "rfc1918"
-    }
-},
-{
-    "model": "ipam.aggregate",
-    "pk": 1,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "prefix": "10.0.0.0/8",
-        "rir": 1,
-        "date_added": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.role",
-    "pk": 1,
-    "fields": {
-        "name": "Lab Network",
-        "slug": "lab-network",
-        "weight": 1000
-    }
-},
-{
-    "model": "ipam.prefix",
-    "pk": 1,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "prefix": "10.1.1.0/24",
-        "site": 1,
-        "vrf": null,
-        "vlan": null,
-        "status": "active",
-        "role": 1,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.prefix",
-    "pk": 2,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "prefix": "10.0.255.0/24",
-        "site": 1,
-        "vrf": null,
-        "vlan": null,
-        "status": "active",
-        "role": 1,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 1,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "10.0.255.1/32",
-        "vrf": null,
-        "interface_id": 3,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 2,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "169.254.254.1/31",
-        "vrf": null,
-        "interface_id": 4,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 3,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "10.0.255.2/32",
-        "vrf": null,
-        "interface_id": 185,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 4,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "169.254.1.1/31",
-        "vrf": null,
-        "interface_id": 213,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 5,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "10.0.254.1/24",
-        "vrf": null,
-        "interface_id": 12,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 8,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "10.15.21.1/31",
-        "vrf": null,
-        "interface_id": 218,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 9,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "10.15.21.2/31",
-        "vrf": null,
-        "interface_id": 9,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 10,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "10.15.22.1/31",
-        "vrf": null,
-        "interface_id": 8,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 11,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "10.15.20.1/31",
-        "vrf": null,
-        "interface_id": 7,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 12,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "10.16.20.1/31",
-        "vrf": null,
-        "interface_id": 216,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 13,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "10.15.22.2/31",
-        "vrf": null,
-        "interface_id": 206,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 14,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "10.16.22.1/31",
-        "vrf": null,
-        "interface_id": 217,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 15,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "10.16.22.2/31",
-        "vrf": null,
-        "interface_id": 205,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 16,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "10.16.20.2/31",
-        "vrf": null,
-        "interface_id": 211,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 17,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "10.15.22.2/31",
-        "vrf": null,
-        "interface_id": 212,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 19,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "10.0.254.2/32",
-        "vrf": null,
-        "interface_id": 188,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 20,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "169.254.1.1/31",
-        "vrf": null,
-        "interface_id": 200,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.ipaddress",
-    "pk": 21,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "family": 4,
-        "address": "169.254.1.2/31",
-        "vrf": null,
-        "interface_id": 194,
-        "nat_inside": null,
-        "description": ""
-    }
-},
-{
-    "model": "ipam.vlan",
-    "pk": 1,
-    "fields": {
-        "created": "2016-06-23",
-        "last_updated": "2016-06-23T03:19:56.521Z",
-        "site": 1,
-        "vid": 999,
-        "name": "TEST",
-        "status": "active",
-        "role": 1
-    }
-}
-]

+ 33 - 2
netbox/ipam/formfields.py

@@ -1,13 +1,44 @@
 from django import forms
 from django import forms
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
-from netaddr import IPNetwork, AddrFormatError
+from django.core.validators import validate_ipv4_address, validate_ipv6_address
+from netaddr import IPAddress, IPNetwork, AddrFormatError
 
 
 
 
 #
 #
 # Form fields
 # Form fields
 #
 #
 
 
-class IPFormField(forms.Field):
+class IPAddressFormField(forms.Field):
+    default_error_messages = {
+        'invalid': "Enter a valid IPv4 or IPv6 address (without a mask).",
+    }
+
+    def to_python(self, value):
+        if not value:
+            return None
+
+        if isinstance(value, IPAddress):
+            return value
+
+        # netaddr is a bit too liberal with what it accepts as a valid IP address. For example, '1.2.3' will become
+        # IPAddress('1.2.0.3'). Here, we employ Django's built-in IPv4 and IPv6 address validators as a sanity check.
+        try:
+            validate_ipv4_address(value)
+        except ValidationError:
+            try:
+                validate_ipv6_address(value)
+            except ValidationError:
+                raise ValidationError("Invalid IPv4/IPv6 address format: {}".format(value))
+
+        try:
+            return IPAddress(value)
+        except ValueError:
+            raise ValidationError('This field requires an IP address without a mask.')
+        except AddrFormatError:
+            raise ValidationError("Please specify a valid IPv4 or IPv6 address.")
+
+
+class IPNetworkFormField(forms.Field):
     default_error_messages = {
     default_error_messages = {
         'invalid': "Enter a valid IPv4 or IPv6 address (with CIDR mask).",
         'invalid': "Enter a valid IPv4 or IPv6 address (with CIDR mask).",
     }
     }

+ 28 - 16
netbox/ipam/forms.py

@@ -13,17 +13,18 @@ from utilities.forms import (
     SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
     SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
 )
 )
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
+from .constants import *
 from .choices import *
 from .choices import *
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 
 
-IP_FAMILY_CHOICES = [
-    ('', 'All'),
-    (4, 'IPv4'),
-    (6, 'IPv6'),
-]
 
 
-PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 128)])
-IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 129)])
+PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([
+    (i, i) for i in range(PREFIX_LENGTH_MIN, PREFIX_LENGTH_MAX + 1)
+])
+
+IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
+    (i, i) for i in range(IPADDRESS_MASK_LENGTH_MIN, IPADDRESS_MASK_LENGTH_MAX + 1)
+])
 
 
 
 
 #
 #
@@ -218,7 +219,7 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
     )
     )
     family = forms.ChoiceField(
     family = forms.ChoiceField(
         required=False,
         required=False,
-        choices=IP_FAMILY_CHOICES,
+        choices=add_blank_choice(IPAddressFamilyChoices),
         label='Address family',
         label='Address family',
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -450,8 +451,8 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
         )
         )
     )
     )
     prefix_length = forms.IntegerField(
     prefix_length = forms.IntegerField(
-        min_value=1,
-        max_value=127,
+        min_value=PREFIX_LENGTH_MIN,
+        max_value=PREFIX_LENGTH_MAX,
         required=False
         required=False
     )
     )
     tenant = forms.ModelChoiceField(
     tenant = forms.ModelChoiceField(
@@ -510,7 +511,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
     )
     )
     family = forms.ChoiceField(
     family = forms.ChoiceField(
         required=False,
         required=False,
-        choices=IP_FAMILY_CHOICES,
+        choices=add_blank_choice(IPAddressFamilyChoices),
         label='Address family',
         label='Address family',
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -634,6 +635,17 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
             }
             }
         )
         )
     )
     )
+    nat_vrf = forms.ModelChoiceField(
+        queryset=VRF.objects.all(),
+        required=False,
+        label='VRF',
+        widget=APISelect(
+            api_url="/api/ipam/vrfs/",
+            filter_for={
+                'nat_inside': 'vrf_id'
+            }
+        )
+    )
     nat_inside = ChainedModelChoiceField(
     nat_inside = ChainedModelChoiceField(
         queryset=IPAddress.objects.all(),
         queryset=IPAddress.objects.all(),
         chains=(
         chains=(
@@ -896,8 +908,8 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
         )
         )
     )
     )
     mask_length = forms.IntegerField(
     mask_length = forms.IntegerField(
-        min_value=1,
-        max_value=128,
+        min_value=IPADDRESS_MASK_LENGTH_MIN,
+        max_value=IPADDRESS_MASK_LENGTH_MAX,
         required=False
         required=False
     )
     )
     tenant = forms.ModelChoiceField(
     tenant = forms.ModelChoiceField(
@@ -969,7 +981,7 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
     )
     )
     family = forms.ChoiceField(
     family = forms.ChoiceField(
         required=False,
         required=False,
-        choices=IP_FAMILY_CHOICES,
+        choices=add_blank_choice(IPAddressFamilyChoices),
         label='Address family',
         label='Address family',
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
@@ -1300,8 +1312,8 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
 
 
 class ServiceForm(BootstrapMixin, CustomFieldForm):
 class ServiceForm(BootstrapMixin, CustomFieldForm):
     port = forms.IntegerField(
     port = forms.IntegerField(
-        min_value=1,
-        max_value=65535
+        min_value=SERVICE_PORT_MIN,
+        max_value=SERVICE_PORT_MAX
     )
     )
     tags = TagField(
     tags = TagField(
         required=False
         required=False

+ 1 - 1
netbox/ipam/migrations/0029_3569_ipaddress_fields.py

@@ -2,10 +2,10 @@ from django.db import migrations, models
 
 
 
 
 IPADDRESS_STATUS_CHOICES = (
 IPADDRESS_STATUS_CHOICES = (
-    (0, 'container'),
     (1, 'active'),
     (1, 'active'),
     (2, 'reserved'),
     (2, 'reserved'),
     (3, 'deprecated'),
     (3, 'deprecated'),
+    (5, 'dhcp'),
 )
 )
 
 
 IPADDRESS_ROLE_CHOICES = (
 IPADDRESS_ROLE_CHOICES = (

+ 21 - 0
netbox/ipam/migrations/0034_fix_ipaddress_status_dhcp.py

@@ -0,0 +1,21 @@
+from django.db import migrations
+
+
+def ipaddress_status_dhcp_to_slug(apps, schema_editor):
+    IPAddress = apps.get_model('ipam', 'IPAddress')
+    IPAddress.objects.filter(status='5').update(status='dhcp')
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('ipam', '0033_deterministic_ordering'),
+    ]
+
+    operations = [
+        # Fixes a missed integer substitution from #3569; see bug #4027. The original migration has also been fixed,
+        # so this can be omitted when squashing in the future.
+        migrations.RunPython(
+            code=ipaddress_status_dhcp_to_slug
+        ),
+    ]

+ 3 - 3
netbox/ipam/models.py

@@ -14,7 +14,7 @@ from utilities.models import ChangeLoggedModel
 from utilities.utils import serialize_object
 from utilities.utils import serialize_object
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
 from .choices import *
 from .choices import *
-from .constants import IPADDRESS_ROLES_NONUNIQUE
+from .constants import *
 from .fields import IPNetworkField, IPAddressField
 from .fields import IPNetworkField, IPAddressField
 from .managers import IPAddressManager
 from .managers import IPAddressManager
 from .querysets import PrefixQuerySet
 from .querysets import PrefixQuerySet
@@ -44,7 +44,7 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
         max_length=50
         max_length=50
     )
     )
     rd = models.CharField(
     rd = models.CharField(
-        max_length=21,
+        max_length=VRF_RD_MAX_LENGTH,
         unique=True,
         unique=True,
         blank=True,
         blank=True,
         null=True,
         null=True,
@@ -1006,7 +1006,7 @@ class Service(ChangeLoggedModel, CustomFieldModel):
         choices=ServiceProtocolChoices
         choices=ServiceProtocolChoices
     )
     )
     port = models.PositiveIntegerField(
     port = models.PositiveIntegerField(
-        validators=[MinValueValidator(1), MaxValueValidator(65535)],
+        validators=[MinValueValidator(SERVICE_PORT_MIN), MaxValueValidator(SERVICE_PORT_MAX)],
         verbose_name='Port number'
         verbose_name='Port number'
     )
     )
     ipaddresses = models.ManyToManyField(
     ipaddresses = models.ManyToManyField(

+ 23 - 1
netbox/ipam/validators.py

@@ -1,4 +1,26 @@
-from django.core.validators import RegexValidator
+from django.core.exceptions import ValidationError
+from django.core.validators import BaseValidator, RegexValidator
+
+
+def prefix_validator(prefix):
+    if prefix.ip != prefix.cidr.ip:
+        raise ValidationError("{} is not a valid prefix. Did you mean {}?".format(prefix, prefix.cidr))
+
+
+class MaxPrefixLengthValidator(BaseValidator):
+    message = 'The prefix length must be less than or equal to %(limit_value)s.'
+    code = 'max_prefix_length'
+
+    def compare(self, a, b):
+        return a.prefixlen > b
+
+
+class MinPrefixLengthValidator(BaseValidator):
+    message = 'The prefix length must be greater than or equal to %(limit_value)s.'
+    code = 'min_prefix_length'
+
+    def compare(self, a, b):
+        return a.prefixlen < b
 
 
 
 
 DNSValidator = RegexValidator(
 DNSValidator = RegexValidator(

+ 7 - 9
netbox/ipam/views.py

@@ -15,6 +15,7 @@ from utilities.views import (
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
 from . import filters, forms, tables
 from . import filters, forms, tables
 from .choices import *
 from .choices import *
+from .constants import *
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 
 
 
 
@@ -86,23 +87,20 @@ def add_available_vlans(vlan_group, vlans):
     """
     """
     Create fake records for all gaps between used VLANs
     Create fake records for all gaps between used VLANs
     """
     """
-    MIN_VLAN = 1
-    MAX_VLAN = 4094
-
     if not vlans:
     if not vlans:
-        return [{'vid': MIN_VLAN, 'available': MAX_VLAN - MIN_VLAN + 1}]
+        return [{'vid': VLAN_VID_MIN, 'available': VLAN_VID_MAX - VLAN_VID_MIN + 1}]
 
 
-    prev_vid = MAX_VLAN
+    prev_vid = VLAN_VID_MAX
     new_vlans = []
     new_vlans = []
     for vlan in vlans:
     for vlan in vlans:
         if vlan.vid - prev_vid > 1:
         if vlan.vid - prev_vid > 1:
             new_vlans.append({'vid': prev_vid + 1, 'available': vlan.vid - prev_vid - 1})
             new_vlans.append({'vid': prev_vid + 1, 'available': vlan.vid - prev_vid - 1})
         prev_vid = vlan.vid
         prev_vid = vlan.vid
 
 
-    if vlans[0].vid > MIN_VLAN:
-        new_vlans.append({'vid': MIN_VLAN, 'available': vlans[0].vid - MIN_VLAN})
-    if prev_vid < MAX_VLAN:
-        new_vlans.append({'vid': prev_vid + 1, 'available': MAX_VLAN - prev_vid})
+    if vlans[0].vid > VLAN_VID_MIN:
+        new_vlans.append({'vid': VLAN_VID_MIN, 'available': vlans[0].vid - VLAN_VID_MIN})
+    if prev_vid < VLAN_VID_MAX:
+        new_vlans.append({'vid': prev_vid + 1, 'available': VLAN_VID_MAX - prev_vid})
 
 
     vlans = list(vlans) + new_vlans
     vlans = list(vlans) + new_vlans
     vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid'])
     vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid'])

+ 2 - 1
netbox/netbox/settings.py

@@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
 # Environment setup
 # Environment setup
 #
 #
 
 
-VERSION = '2.7.2'
+VERSION = '2.7.3'
 
 
 # Hostname
 # Hostname
 HOSTNAME = platform.node()
 HOSTNAME = platform.node()
@@ -503,6 +503,7 @@ SWAGGER_SETTINGS = {
         'utilities.custom_inspectors.IdInFilterInspector',
         'utilities.custom_inspectors.IdInFilterInspector',
         'drf_yasg.inspectors.CoreAPICompatInspector',
         'drf_yasg.inspectors.CoreAPICompatInspector',
     ],
     ],
+    'DEFAULT_INFO': 'netbox.urls.openapi_info',
     'DEFAULT_MODEL_DEPTH': 1,
     'DEFAULT_MODEL_DEPTH': 1,
     'DEFAULT_PAGINATOR_INSPECTORS': [
     'DEFAULT_PAGINATOR_INSPECTORS': [
         'utilities.custom_inspectors.NullablePaginatorInspector',
         'utilities.custom_inspectors.NullablePaginatorInspector',

+ 0 - 0
netbox/netbox/tests/__init__.py


+ 13 - 0
netbox/netbox/tests/test_api.py

@@ -0,0 +1,13 @@
+from django.urls import reverse
+
+from utilities.testing import APITestCase
+
+
+class AppTest(APITestCase):
+
+    def test_root(self):
+
+        url = reverse('api-root')
+        response = self.client.get('{}?format=api'.format(url), **self.header)
+
+        self.assertEqual(response.status_code, 200)

+ 24 - 0
netbox/netbox/tests/test_views.py

@@ -0,0 +1,24 @@
+import urllib.parse
+
+from django.test import TestCase
+from django.urls import reverse
+
+
+class HomeViewTestCase(TestCase):
+
+    def test_home(self):
+
+        url = reverse('home')
+
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+
+    def test_search(self):
+
+        url = reverse('search')
+        params = {
+            'q': 'foo',
+        }
+
+        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
+        self.assertEqual(response.status_code, 200)

+ 9 - 7
netbox/netbox/urls.py

@@ -9,14 +9,16 @@ from netbox.views import APIRootView, HomeView, SearchView
 from users.views import LoginView, LogoutView
 from users.views import LoginView, LogoutView
 from .admin import admin_site
 from .admin import admin_site
 
 
+openapi_info = openapi.Info(
+    title="NetBox API",
+    default_version='v2',
+    description="API to access NetBox",
+    terms_of_service="https://github.com/netbox-community/netbox",
+    license=openapi.License(name="Apache v2 License"),
+)
+
 schema_view = get_schema_view(
 schema_view = get_schema_view(
-    openapi.Info(
-        title="NetBox API",
-        default_version='v2',
-        description="API to access NetBox",
-        terms_of_service="https://github.com/netbox-community/netbox",
-        license=openapi.License(name="Apache v2 License"),
-    ),
+    openapi_info,
     validators=['flex', 'ssv'],
     validators=['flex', 'ssv'],
     public=True,
     public=True,
 )
 )

+ 7 - 4
netbox/project-static/js/forms.js

@@ -158,14 +158,17 @@ $(document).ready(function() {
 
 
                 filter_for_elements.each(function(index, filter_for_element) {
                 filter_for_elements.each(function(index, filter_for_element) {
                     var param_name = $(filter_for_element).attr(attr_name);
                     var param_name = $(filter_for_element).attr(attr_name);
+                    var is_required = $(filter_for_element).attr("required");
                     var is_nullable = $(filter_for_element).attr("nullable");
                     var is_nullable = $(filter_for_element).attr("nullable");
                     var is_visible = $(filter_for_element).is(":visible");
                     var is_visible = $(filter_for_element).is(":visible");
                     var value = $(filter_for_element).val();
                     var value = $(filter_for_element).val();
 
 
-                    if (param_name && is_visible && value) {
-                        parameters[param_name] = value;
-                    } else if (param_name && is_visible && is_nullable) {
-                        parameters[param_name] = "null";
+                    if (param_name && is_visible) {
+                        if (value) {
+                            parameters[param_name] = value;
+                        } else if (is_required && is_nullable) {
+                            parameters[param_name] = "null";
+                        }
                     }
                     }
                 });
                 });
 
 

+ 9 - 4
netbox/project-static/js/interface_toggles.js

@@ -2,9 +2,9 @@
 $('button.toggle-ips').click(function() {
 $('button.toggle-ips').click(function() {
     var selected = $(this).attr('selected');
     var selected = $(this).attr('selected');
     if (selected) {
     if (selected) {
-        $('#interfaces_table tr.ipaddresses').hide();
+        $('#interfaces_table tr.interface:visible + tr.ipaddresses').hide();
     } else {
     } else {
-        $('#interfaces_table tr.ipaddresses').show();
+        $('#interfaces_table tr.interface:visible + tr.ipaddresses').show();
     }
     }
     $(this).attr('selected', !selected);
     $(this).attr('selected', !selected);
     $(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
     $(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
@@ -14,17 +14,22 @@ $('button.toggle-ips').click(function() {
 // Inteface filtering
 // Inteface filtering
 $('input.interface-filter').on('input', function() {
 $('input.interface-filter').on('input', function() {
     var filter = new RegExp(this.value);
     var filter = new RegExp(this.value);
+    var interface;
 
 
-    for (interface of $(this).closest('div.panel').find('tbody > tr')) {
+    for (interface of $('#interfaces_table > tbody > tr.interface')) {
         // Slice off 'interface_' at the start of the ID
         // Slice off 'interface_' at the start of the ID
-        if (filter && filter.test(interface.id.slice(10))) {
+        if (filter.test(interface.id.slice(10))) {
             // Match the toggle in case the filter now matches the interface
             // Match the toggle in case the filter now matches the interface
             $(interface).find('input:checkbox[name=pk]').prop('checked', $('input.toggle').prop('checked'));
             $(interface).find('input:checkbox[name=pk]').prop('checked', $('input.toggle').prop('checked'));
             $(interface).show();
             $(interface).show();
+            if ($('button.toggle-ips').attr('selected')) {
+                $(interface).next('tr.ipaddresses').show();
+            }
         } else {
         } else {
             // Uncheck to prevent actions from including it when it doesn't match
             // Uncheck to prevent actions from including it when it doesn't match
             $(interface).find('input:checkbox[name=pk]').prop('checked', false);
             $(interface).find('input:checkbox[name=pk]').prop('checked', false);
             $(interface).hide();
             $(interface).hide();
+            $(interface).next('tr.ipaddresses').hide();
         }
         }
     }
     }
 });
 });

+ 5 - 0
netbox/secrets/constants.py

@@ -0,0 +1,5 @@
+#
+# Secrets
+#
+
+SECRET_PLAINTEXT_MAX_LENGTH = 65535

+ 3 - 2
netbox/secrets/forms.py

@@ -9,6 +9,7 @@ from utilities.forms import (
     APISelect, APISelectMultiple, BootstrapMixin, FilterChoiceField, FlexibleModelChoiceField, SlugField,
     APISelect, APISelectMultiple, BootstrapMixin, FilterChoiceField, FlexibleModelChoiceField, SlugField,
     StaticSelect2Multiple
     StaticSelect2Multiple
 )
 )
+from .constants import *
 from .models import Secret, SecretRole, UserKey
 from .models import Secret, SecretRole, UserKey
 
 
 
 
@@ -69,7 +70,7 @@ class SecretRoleCSVForm(forms.ModelForm):
 
 
 class SecretForm(BootstrapMixin, CustomFieldForm):
 class SecretForm(BootstrapMixin, CustomFieldForm):
     plaintext = forms.CharField(
     plaintext = forms.CharField(
-        max_length=65535,
+        max_length=SECRET_PLAINTEXT_MAX_LENGTH,
         required=False,
         required=False,
         label='Plaintext',
         label='Plaintext',
         widget=forms.PasswordInput(
         widget=forms.PasswordInput(
@@ -79,7 +80,7 @@ class SecretForm(BootstrapMixin, CustomFieldForm):
         )
         )
     )
     )
     plaintext2 = forms.CharField(
     plaintext2 = forms.CharField(
-        max_length=65535,
+        max_length=SECRET_PLAINTEXT_MAX_LENGTH,
         required=False,
         required=False,
         label='Plaintext (verify)',
         label='Plaintext (verify)',
         widget=forms.PasswordInput()
         widget=forms.PasswordInput()

+ 0 - 1
netbox/secrets/tests/test_form.py

@@ -29,5 +29,4 @@ class UserKeyFormTestCase(TestCase):
             data={'public_key': SSH_PUBLIC_KEY},
             data={'public_key': SSH_PUBLIC_KEY},
             instance=self.userkey,
             instance=self.userkey,
         )
         )
-        print(form.is_valid())
         self.assertFalse(form.is_valid())
         self.assertFalse(form.is_valid())

+ 2 - 19
netbox/templates/dcim/cable_connect.html

@@ -144,25 +144,8 @@
             </div>
             </div>
         </div>
         </div>
         <div class="row">
         <div class="row">
-            <div class="col-md-4 col-md-offset-4">
-                <div class="panel panel-default">
-                    <div class="panel-heading"><strong>Cable</strong></div>
-                    <div class="panel-body">
-                        {% render_field form.status %}
-                        {% render_field form.type %}
-                        {% render_field form.label %}
-                        {% render_field form.color %}
-                        <div class="form-group">
-                            <label class="col-md-3 control-label" for="id_length">{{ form.length.label }}</label>
-                            <div class="col-md-6">
-                                {{ form.length }}
-                            </div>
-                            <div class="col-md-3">
-                                {{ form.length_unit }}
-                            </div>
-                        </div>
-                    </div>
-                </div>
+            <div class="col-md-6 col-md-offset-3">
+                {% include 'dcim/inc/cable_form.html' %}
             </div>
             </div>
         </div>
         </div>
         <div class="form-group">
         <div class="form-group">

+ 1 - 19
netbox/templates/dcim/cable_edit.html

@@ -1,23 +1,5 @@
 {% extends 'utilities/obj_edit.html' %}
 {% extends 'utilities/obj_edit.html' %}
-{% load form_helpers %}
 
 
 {% block form %}
 {% block form %}
-    <div class="panel panel-default">
-        <div class="panel-heading"><strong>Cable</strong></div>
-        <div class="panel-body">
-            {% render_field form.type %}
-            {% render_field form.status %}
-            {% render_field form.label %}
-            {% render_field form.color %}
-            <div class="form-group">
-                <label class="col-md-3 control-label" for="id_length">{{ form.length.label }}</label>
-                <div class="col-md-6">
-                    {{ form.length }}
-                </div>
-                <div class="col-md-3">
-                    {{ form.length_unit }}
-                </div>
-            </div>
-        </div>
-    </div>
+    {% include 'dcim/inc/cable_form.html' %}
 {% endblock %}
 {% endblock %}

+ 1 - 1
netbox/templates/dcim/cable_trace.html

@@ -32,7 +32,7 @@
                             {% if cable.label %}<code>{{ cable.label }}</code>{% else %}Cable #{{ cable.pk }}{% endif %}
                             {% if cable.label %}<code>{{ cable.label }}</code>{% else %}Cable #{{ cable.pk }}{% endif %}
                         </a>
                         </a>
                     </h4>
                     </h4>
-                    <p><span class="label label-{% if cable.status %}success{% else %}info{% endif %}">{{ cable.get_status_display }}</span></p>
+                    <p><span class="label label-{{ cable.get_status_class }}">{{ cable.get_status_display }}</span></p>
                     <p>{{ cable.get_type_display|default:"" }}</p>
                     <p>{{ cable.get_type_display|default:"" }}</p>
                     {% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %}
                     {% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %}
                     {% if cable.color %}
                     {% if cable.color %}

+ 19 - 0
netbox/templates/dcim/inc/cable_form.html

@@ -0,0 +1,19 @@
+{% load form_helpers %}
+<div class="panel panel-default">
+    <div class="panel-heading"><strong>Cable</strong></div>
+    <div class="panel-body">
+        {% render_field form.status %}
+        {% render_field form.type %}
+        {% render_field form.label %}
+        {% render_field form.color %}
+        <div class="form-group">
+            <label class="col-md-3 control-label" for="id_length">{{ form.length.label }}</label>
+            <div class="col-md-5">
+                {{ form.length }}
+            </div>
+            <div class="col-md-4">
+                {{ form.length_unit }}
+            </div>
+        </div>
+    </div>
+</div>

+ 1 - 1
netbox/templates/dcim/inc/cable_toggle_buttons.html

@@ -1,5 +1,5 @@
 {% if perms.dcim.change_cable %}
 {% if perms.dcim.change_cable %}
-    {% if cable.status %}
+    {% if cable.status == 'connected' %}
         <a href="#" class="btn btn-warning btn-xs cable-toggle connected" title="Mark planned" data="{{ cable.pk }}">
         <a href="#" class="btn btn-warning btn-xs cable-toggle connected" title="Mark planned" data="{{ cable.pk }}">
             <i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
             <i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
         </a>
         </a>

+ 1 - 1
netbox/templates/dcim/inc/consoleport.html

@@ -1,4 +1,4 @@
-<tr class="consoleport{% if cp.cable.status %} success{% elif cp.cable %} info{% endif %}">
+<tr class="consoleport{% if cp.cable %} {{ cp.cable.get_status_class }}{% endif %}">
 
 
     {# Name #}
     {# Name #}
     <td>
     <td>

+ 1 - 1
netbox/templates/dcim/inc/consoleserverport.html

@@ -1,6 +1,6 @@
 {% load helpers %}
 {% load helpers %}
 
 
-<tr class="consoleserverport{% if csp.cable.status %} success{% elif csp.cable %} info{% endif %}">
+<tr class="consoleserverport{% if csp.cable %} {{ csp.cable.get_status_class }}{% endif %}">
 
 
     {# Checkbox #}
     {# Checkbox #}
     {% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
     {% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}

+ 1 - 1
netbox/templates/dcim/inc/frontport.html

@@ -1,5 +1,5 @@
 {% load helpers %}
 {% load helpers %}
-<tr class="frontport{% if frontport.cable.status %} success{% elif frontport.cable %} info{% endif %}">
+<tr class="frontport{% if frontport.cable %} {{ frontport.cable.get_status_class }}{% endif %}">
 
 
     {# Checkbox #}
     {# Checkbox #}
     {% if perms.dcim.change_frontport or perms.dcim.delete_frontport %}
     {% if perms.dcim.change_frontport or perms.dcim.delete_frontport %}

+ 1 - 1
netbox/templates/dcim/inc/interface.html

@@ -1,5 +1,5 @@
 {% load helpers %}
 {% load helpers %}
-<tr class="interface{% if not iface.enabled %} danger{% elif iface.cable.status %} success{% elif iface.cable %} info{% elif iface.is_virtual %} warning{% endif %}" id="interface_{{ iface.name }}">
+<tr class="interface{% if not iface.enabled %} danger{% elif iface.cable %} {{ iface.cable.get_status_class }}{% elif iface.is_virtual %} warning{% endif %}" id="interface_{{ iface.name }}">
 
 
     {# Checkbox #}
     {# Checkbox #}
     {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
     {% if perms.dcim.change_interface or perms.dcim.delete_interface %}

+ 1 - 1
netbox/templates/dcim/inc/poweroutlet.html

@@ -1,6 +1,6 @@
 {% load helpers %}
 {% load helpers %}
 
 
-<tr class="poweroutlet{% if po.cable.status %} success{% elif po.cable %} info{% endif %}">
+<tr class="poweroutlet{% if po.cable %} {{ po.cable.get_status_class }}{% endif %}">
 
 
     {# Checkbox #}
     {# Checkbox #}
     {% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
     {% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}

+ 1 - 1
netbox/templates/dcim/inc/powerport.html

@@ -1,4 +1,4 @@
-<tr class="powerport{% if pp.cable.status %} success{% elif pp.cable %} info{% endif %}">
+<tr class="powerport{% if pp.cable %} {{ pp.cable.get_status_class }}{% endif %}">
 
 
     {# Name #}
     {# Name #}
     <td>
     <td>

+ 1 - 1
netbox/templates/dcim/inc/rearport.html

@@ -1,5 +1,5 @@
 {% load helpers %}
 {% load helpers %}
-<tr class="rearport{% if rearport.cable.status %} success{% elif rearport.cable %} info{% endif %}">
+<tr class="rearport{% if rearport.cable %} {{ rearport.cable.get_status_class }}{% endif %}">
 
 
     {# Checkbox #}
     {# Checkbox #}
     {% if perms.dcim.change_rearport or perms.dcim.delete_rearport %}
     {% if perms.dcim.change_rearport or perms.dcim.delete_rearport %}

+ 3 - 7
netbox/templates/dcim/rack_elevation_list.html

@@ -3,8 +3,8 @@
 
 
 {% block content %}
 {% block content %}
 <div class="btn-group pull-right noprint" role="group">
 <div class="btn-group pull-right noprint" role="group">
-    <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face=0 %}" class="btn btn-default{% if request.GET.face != '1' %} active{% endif %}">Front</a>
-    <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face=1 %}" class="btn btn-default{% if request.GET.face == '1' %} active{% endif %}">Rear</a>
+    <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-default{% if rack_face == 'front' %} active{% endif %}">Front</a>
+    <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-default{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
 </div>
 </div>
 <h1>{% block title %}Rack Elevations{% endblock %}</h1>
 <h1>{% block title %}Rack Elevations{% endblock %}</h1>
 <div class="row">
 <div class="row">
@@ -17,11 +17,7 @@
                             <strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name|truncatechars:"25" }}</a></strong>
                             <strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name|truncatechars:"25" }}</a></strong>
                             <p><small class="text-muted">{{ rack.facility_id|truncatechars:"30" }}</small></p>
                             <p><small class="text-muted">{{ rack.facility_id|truncatechars:"30" }}</small></p>
                         </div>
                         </div>
-                        {% if face_id %}
-                            {% include 'dcim/inc/rack_elevation.html' with face='rear' %}
-                        {% else %}
-                            {% include 'dcim/inc/rack_elevation.html' with face='front' %}
-                        {% endif %}
+                        {% include 'dcim/inc/rack_elevation.html' with face=rack_face %}
                         <div class="clearfix"></div>
                         <div class="clearfix"></div>
                         <div class="rack_header">
                         <div class="rack_header">
                             <strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name|truncatechars:"25" }}</a></strong>
                             <strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name|truncatechars:"25" }}</a></strong>

+ 1 - 1
netbox/templates/ipam/ipaddress_edit.html

@@ -61,7 +61,7 @@
                     {% render_field form.nat_device %}
                     {% render_field form.nat_device %}
                 </div>
                 </div>
                 <div class="tab-pane" id="search">
                 <div class="tab-pane" id="search">
-                    &nbsp;
+                  {% render_field form.nat_vrf %}
                 </div>
                 </div>
             </div>
             </div>
             {% render_field form.nat_inside %}
             {% render_field form.nat_inside %}

+ 0 - 1
netbox/templates/virtualization/cluster_edit.html

@@ -8,7 +8,6 @@
             {% render_field form.name %}
             {% render_field form.name %}
             {% render_field form.type %}
             {% render_field form.type %}
             {% render_field form.group %}
             {% render_field form.group %}
-            {% render_field form.tenant %}
             {% render_field form.site %}
             {% render_field form.site %}
         </div>
         </div>
     </div>
     </div>

+ 0 - 1
netbox/utilities/api.py

@@ -13,7 +13,6 @@ from rest_framework.response import Response
 from rest_framework.serializers import Field, ModelSerializer, ValidationError
 from rest_framework.serializers import Field, ModelSerializer, ValidationError
 from rest_framework.viewsets import ModelViewSet as _ModelViewSet, ViewSet
 from rest_framework.viewsets import ModelViewSet as _ModelViewSet, ViewSet
 
 
-from utilities.choices import ChoiceSet
 from .utils import dict_to_filter_params, dynamic_import
 from .utils import dict_to_filter_params, dynamic_import
 
 
 
 

+ 1 - 1
netbox/utilities/choices.py

@@ -18,7 +18,7 @@ class ChoiceSet(metaclass=ChoiceSetMeta):
 
 
     @classmethod
     @classmethod
     def values(cls):
     def values(cls):
-        return [c[0] for c in cls.CHOICES]
+        return [c[0] for c in unpack_grouped_choices(cls.CHOICES)]
 
 
     @classmethod
     @classmethod
     def as_dict(cls):
     def as_dict(cls):

+ 1 - 4
netbox/utilities/middleware.py

@@ -7,9 +7,6 @@ from django.urls import reverse
 
 
 from .views import server_error
 from .views import server_error
 
 
-BASE_PATH = getattr(settings, 'BASE_PATH', False)
-LOGIN_REQUIRED = getattr(settings, 'LOGIN_REQUIRED', False)
-
 
 
 class LoginRequiredMiddleware(object):
 class LoginRequiredMiddleware(object):
     """
     """
@@ -19,7 +16,7 @@ class LoginRequiredMiddleware(object):
         self.get_response = get_response
         self.get_response = get_response
 
 
     def __call__(self, request):
     def __call__(self, request):
-        if LOGIN_REQUIRED and not request.user.is_authenticated:
+        if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
             # Redirect unauthenticated requests to the login page. API requests are exempt from redirection as the API
             # Redirect unauthenticated requests to the login page. API requests are exempt from redirection as the API
             # performs its own authentication. Also metrics can be read without login.
             # performs its own authentication. Also metrics can be read without login.
             api_path = reverse('api-root')
             api_path = reverse('api-root')

+ 50 - 0
netbox/utilities/tests/test_choices.py

@@ -0,0 +1,50 @@
+from django.test import TestCase
+
+from utilities.choices import ChoiceSet
+
+
+class ExampleChoices(ChoiceSet):
+
+    CHOICE_A = 'a'
+    CHOICE_B = 'b'
+    CHOICE_C = 'c'
+    CHOICE_1 = 1
+    CHOICE_2 = 2
+    CHOICE_3 = 3
+    CHOICES = (
+        ('Letters', (
+            (CHOICE_A, 'A'),
+            (CHOICE_B, 'B'),
+            (CHOICE_C, 'C'),
+        )),
+        ('Digits', (
+            (CHOICE_1, 'One'),
+            (CHOICE_2, 'Two'),
+            (CHOICE_3, 'Three'),
+        )),
+    )
+    LEGACY_MAP = {
+        CHOICE_A: 101,
+        CHOICE_B: 102,
+        CHOICE_C: 103,
+        CHOICE_1: 201,
+        CHOICE_2: 202,
+        CHOICE_3: 203,
+    }
+
+
+class ChoiceSetTestCase(TestCase):
+
+    def test_values(self):
+        self.assertListEqual(ExampleChoices.values(), ['a', 'b', 'c', 1, 2, 3])
+
+    def test_as_dict(self):
+        self.assertEqual(ExampleChoices.as_dict(), {
+            'a': 'A', 'b': 'B', 'c': 'C', 1: 'One', 2: 'Two', 3: 'Three'
+        })
+
+    def test_slug_to_id(self):
+        self.assertEqual(ExampleChoices.slug_to_id('a'), 101)
+
+    def test_id_to_slug(self):
+        self.assertEqual(ExampleChoices.id_to_slug(101), 'a')

+ 1 - 17
netbox/utilities/validators.py

@@ -1,6 +1,6 @@
 import re
 import re
 
 
-from django.core.validators import _lazy_re_compile, BaseValidator, URLValidator
+from django.core.validators import _lazy_re_compile, URLValidator
 
 
 
 
 class EnhancedURLValidator(URLValidator):
 class EnhancedURLValidator(URLValidator):
@@ -26,19 +26,3 @@ class EnhancedURLValidator(URLValidator):
         r'(?:[/?#][^\s]*)?'                 # Path
         r'(?:[/?#][^\s]*)?'                 # Path
         r'\Z', re.IGNORECASE)
         r'\Z', re.IGNORECASE)
     schemes = AnyURLScheme()
     schemes = AnyURLScheme()
-
-
-class MaxPrefixLengthValidator(BaseValidator):
-    message = 'The prefix length must be less than or equal to %(limit_value)s.'
-    code = 'max_prefix_length'
-
-    def compare(self, a, b):
-        return a.prefixlen > b
-
-
-class MinPrefixLengthValidator(BaseValidator):
-    message = 'The prefix length must be greater than or equal to %(limit_value)s.'
-    code = 'min_prefix_length'
-
-    def compare(self, a, b):
-        return a.prefixlen < b

+ 11 - 8
netbox/virtualization/forms.py

@@ -3,6 +3,7 @@ from django.core.exceptions import ValidationError
 from taggit.forms import TagField
 from taggit.forms import TagField
 
 
 from dcim.choices import InterfaceModeChoices
 from dcim.choices import InterfaceModeChoices
+from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
 from dcim.forms import INTERFACE_MODE_HELP_TEXT
 from dcim.forms import INTERFACE_MODE_HELP_TEXT
 from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
 from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
 from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
 from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
@@ -170,7 +171,8 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
         )
         )
     )
     )
     comments = CommentField(
     comments = CommentField(
-        widget=SmallTextarea()
+        widget=SmallTextarea,
+        label='Comments'
     )
     )
 
 
     class Meta:
     class Meta:
@@ -534,7 +536,8 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB
         label='Disk (GB)'
         label='Disk (GB)'
     )
     )
     comments = CommentField(
     comments = CommentField(
-        widget=SmallTextarea()
+        widget=SmallTextarea,
+        label='Comments'
     )
     )
 
 
     class Meta:
     class Meta:
@@ -745,8 +748,8 @@ class InterfaceCreateForm(ComponentForm):
     )
     )
     mtu = forms.IntegerField(
     mtu = forms.IntegerField(
         required=False,
         required=False,
-        min_value=1,
-        max_value=32767,
+        min_value=INTERFACE_MTU_MIN,
+        max_value=INTERFACE_MTU_MAX,
         label='MTU'
         label='MTU'
     )
     )
     mac_address = forms.CharField(
     mac_address = forms.CharField(
@@ -834,8 +837,8 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
     )
     )
     mtu = forms.IntegerField(
     mtu = forms.IntegerField(
         required=False,
         required=False,
-        min_value=1,
-        max_value=32767,
+        min_value=INTERFACE_MTU_MIN,
+        max_value=INTERFACE_MTU_MAX,
         label='MTU'
         label='MTU'
     )
     )
     description = forms.CharField(
     description = forms.CharField(
@@ -931,8 +934,8 @@ class VirtualMachineBulkAddInterfaceForm(VirtualMachineBulkAddComponentForm):
     )
     )
     mtu = forms.IntegerField(
     mtu = forms.IntegerField(
         required=False,
         required=False,
-        min_value=1,
-        max_value=32767,
+        min_value=INTERFACE_MTU_MIN,
+        max_value=INTERFACE_MTU_MAX,
         label='MTU'
         label='MTU'
     )
     )
     description = forms.CharField(
     description = forms.CharField(