Sfoglia il codice sorgente

Merge branch 'develop' into develop-2.10

Jeremy Stretch 5 anni fa
parent
commit
23cce55246

+ 0 - 23
.github/lock.yml

@@ -1,23 +0,0 @@
-# Configuration for Lock (https://github.com/apps/lock)
-
-# Number of days of inactivity before a closed issue or pull request is locked
-daysUntilLock: 90
-
-# Skip issues and pull requests created before a given timestamp. Timestamp must
-# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
-skipCreatedBefore: false
-
-# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
-exemptLabels: []
-
-# Label to add before locking, such as `outdated`. Set to `false` to disable
-lockLabel: false
-
-# Comment to post before locking. Set to `false` to disable
-lockComment: false
-
-# Assign `resolved` as the reason for locking. Set to `false` to disable
-setLockReason: true
-
-# Limit to only `issues` or `pulls`
-# only: issues

+ 21 - 0
.github/workflows/lock.yml

@@ -0,0 +1,21 @@
+# lock-threads (https://github.com/marketplace/actions/lock-threads)
+name: 'Lock threads'
+
+on:
+  schedule:
+    - cron: '0 3 * * *'
+
+jobs:
+  lock:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: dessant/lock-threads@v2
+        with:
+          github-token: ${{ github.token }}
+          issue-lock-inactive-days: '90'
+          issue-exclude-created-before: ''
+          issue-exclude-labels: ''
+          issue-lock-labels: ''
+          issue-lock-comment: ''
+          issue-lock-reason: 'resolved'
+          process-only: 'issues'

+ 3 - 2
docs/administration/replicating-netbox.md

@@ -73,8 +73,9 @@ tar -xf netbox_media.tar.gz
 
 
 ## Cache Invalidation
 ## Cache Invalidation
 
 
-If you are migrating your instance of NetBox to a different machine, be sure to first invalidate the cache by performing this command:
+If you are migrating your instance of NetBox to a different machine, be sure to first invalidate the cache on the original instance by issuing the `invalidate all` management command (within the Python virtual environment):
 
 
 ```no-highlight
 ```no-highlight
-python3 manage.py invalidate all
+# source /opt/netbox/venv/bin/activate
+(venv) # python3 manage.py invalidate all
 ```
 ```

+ 14 - 0
docs/release-notes/version-2.9.md

@@ -1,5 +1,19 @@
 # NetBox v2.9
 # NetBox v2.9
 
 
+## v2.9.9 (FUTURE)
+
+### Enhancements
+
+* [#5304](https://github.com/netbox-community/netbox/issues/5304) - Return server error messages as JSON when handling REST API requests
+* [#5310](https://github.com/netbox-community/netbox/issues/5310) - Link to rack groups within rack list table
+
+### Bug Fixes
+
+* [#5271](https://github.com/netbox-community/netbox/issues/5271) - Fix auto-population of region field when editing a device
+
+
+---
+
 ## v2.9.8 (2020-10-30)
 ## v2.9.8 (2020-10-30)
 
 
 ### Enhancements
 ### Enhancements

+ 12 - 2
netbox/circuits/forms.py

@@ -303,14 +303,24 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
 #
 #
 
 
 class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
 class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        initial_params={
+            'sites': '$site'
+        }
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
-        queryset=Site.objects.all()
+        queryset=Site.objects.all(),
+        query_params={
+            'region_id': '$region'
+        }
     )
     )
 
 
     class Meta:
     class Meta:
         model = CircuitTermination
         model = CircuitTermination
         fields = [
         fields = [
-            'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description',
+            'term_side', 'region', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description',
         ]
         ]
         help_texts = {
         help_texts = {
             'port_speed': "Physical circuit speed",
             'port_speed': "Physical circuit speed",

+ 199 - 66
netbox/dcim/forms.py

@@ -352,8 +352,18 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
 #
 #
 
 
 class RackGroupForm(BootstrapMixin, forms.ModelForm):
 class RackGroupForm(BootstrapMixin, forms.ModelForm):
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        initial_params={
+            'sites': '$site'
+        }
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
-        queryset=Site.objects.all()
+        queryset=Site.objects.all(),
+        query_params={
+            'region_id': '$region'
+        }
     )
     )
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         queryset=RackGroup.objects.all(),
         queryset=RackGroup.objects.all(),
@@ -367,7 +377,7 @@ class RackGroupForm(BootstrapMixin, forms.ModelForm):
     class Meta:
     class Meta:
         model = RackGroup
         model = RackGroup
         fields = (
         fields = (
-            'site', 'parent', 'name', 'slug', 'description',
+            'region', 'site', 'parent', 'name', 'slug', 'description',
         )
         )
 
 
 
 
@@ -447,14 +457,17 @@ class RackRoleCSVForm(CSVModelForm):
 #
 #
 
 
 class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
 class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
-    site = DynamicModelChoiceField(
-        queryset=Site.objects.all()
-    )
-    group = DynamicModelChoiceField(
-        queryset=RackGroup.objects.all(),
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
         required=False,
         required=False,
+        initial_params={
+            'sites': '$site'
+        }
+    )
+    site = DynamicModelChoiceField(
+        queryset=Site.objects.all(),
         query_params={
         query_params={
-            'site_id': '$site'
+            'region_id': '$region'
         }
         }
     )
     )
     role = DynamicModelChoiceField(
     role = DynamicModelChoiceField(
@@ -470,8 +483,9 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
     class Meta:
     class Meta:
         model = Rack
         model = Rack
         fields = [
         fields = [
-            'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial', 'asset_tag',
-            'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', 'tags',
+            'region', 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
+            'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
+            'comments', 'tags',
         ]
         ]
         help_texts = {
         help_texts = {
             'site': "The site at which the rack exists",
             'site': "The site at which the rack exists",
@@ -548,9 +562,19 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         queryset=Rack.objects.all(),
         queryset=Rack.objects.all(),
         widget=forms.MultipleHiddenInput
         widget=forms.MultipleHiddenInput
     )
     )
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        initial_params={
+            'sites': '$site'
+        }
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        required=False
+        required=False,
+        query_params={
+            'region_id': '$region'
+        }
     )
     )
     group = DynamicModelChoiceField(
     group = DynamicModelChoiceField(
         queryset=RackGroup.objects.all(),
         queryset=RackGroup.objects.all(),
@@ -691,9 +715,19 @@ class RackElevationFilterForm(RackFilterForm):
 #
 #
 
 
 class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
 class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        initial_params={
+            'sites': '$site'
+        }
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        required=False
+        required=False,
+        query_params={
+            'region_id': '$region'
+        }
     )
     )
     rack_group = DynamicModelChoiceField(
     rack_group = DynamicModelChoiceField(
         queryset=RackGroup.objects.all(),
         queryset=RackGroup.objects.all(),
@@ -707,7 +741,7 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         display_field='display_name',
         display_field='display_name',
         query_params={
         query_params={
             'site_id': '$site',
             'site_id': '$site',
-            'group_id': 'rack',
+            'group_id': '$rack',
         }
         }
     )
     )
     units = NumericArrayField(
     units = NumericArrayField(
@@ -809,15 +843,23 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditFor
 
 
 class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
 class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
     model = RackReservation
     model = RackReservation
-    field_order = ['q', 'site', 'group_id', 'tenant_group', 'tenant']
+    field_order = ['q', 'region', 'site', 'group_id', 'tenant_group', 'tenant']
     q = forms.CharField(
     q = forms.CharField(
         required=False,
         required=False,
         label='Search'
         label='Search'
     )
     )
+    region = DynamicModelMultipleChoiceField(
+        queryset=Region.objects.all(),
+        to_field_name='slug',
+        required=False
+    )
     site = DynamicModelMultipleChoiceField(
     site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
-        required=False
+        required=False,
+        query_params={
+            'region': '$region'
+        }
     )
     )
     group_id = DynamicModelMultipleChoiceField(
     group_id = DynamicModelMultipleChoiceField(
         queryset=RackGroup.objects.prefetch_related('site'),
         queryset=RackGroup.objects.prefetch_related('site'),
@@ -1672,7 +1714,10 @@ class PlatformCSVForm(CSVModelForm):
 class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
 class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
     region = DynamicModelChoiceField(
     region = DynamicModelChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        required=False
+        required=False,
+        initial_params={
+            'sites': '$site'
+        }
     )
     )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -1686,6 +1731,9 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         display_field='display_name',
         display_field='display_name',
         query_params={
         query_params={
             'site_id': '$site'
             'site_id': '$site'
+        },
+        initial_params={
+            'racks': '$rack'
         }
         }
     )
     )
     rack = DynamicModelChoiceField(
     rack = DynamicModelChoiceField(
@@ -1711,7 +1759,10 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
     )
     )
     manufacturer = DynamicModelChoiceField(
     manufacturer = DynamicModelChoiceField(
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
-        required=False
+        required=False,
+        initial_params={
+            'device_types': '$device_type'
+        }
     )
     )
     device_type = DynamicModelChoiceField(
     device_type = DynamicModelChoiceField(
         queryset=DeviceType.objects.all(),
         queryset=DeviceType.objects.all(),
@@ -1733,7 +1784,10 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
     cluster_group = DynamicModelChoiceField(
     cluster_group = DynamicModelChoiceField(
         queryset=ClusterGroup.objects.all(),
         queryset=ClusterGroup.objects.all(),
         required=False,
         required=False,
-        null_option='None'
+        null_option='None',
+        initial_params={
+            'clusters': '$cluster'
+        }
     )
     )
     cluster = DynamicModelChoiceField(
     cluster = DynamicModelChoiceField(
         queryset=Cluster.objects.all(),
         queryset=Cluster.objects.all(),
@@ -1772,27 +1826,6 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         }
         }
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
-
-        # Initialize helper selectors
-        instance = kwargs.get('instance')
-        if 'initial' not in kwargs:
-            kwargs['initial'] = {}
-        # Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field
-        if instance and hasattr(instance, 'device_type'):
-            kwargs['initial']['manufacturer'] = instance.device_type.manufacturer
-        if instance and instance.cluster is not None:
-            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:
@@ -3441,10 +3474,18 @@ class ConnectCableToDeviceForm(BootstrapMixin, forms.ModelForm):
     """
     """
     Base form for connecting a Cable to a Device component
     Base form for connecting a Cable to a Device component
     """
     """
+    termination_b_region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        label='Region',
+        required=False
+    )
     termination_b_site = DynamicModelChoiceField(
     termination_b_site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         label='Site',
         label='Site',
-        required=False
+        required=False,
+        query_params={
+            'region_id': '$termination_b_region'
+        }
     )
     )
     termination_b_rack = DynamicModelChoiceField(
     termination_b_rack = DynamicModelChoiceField(
         queryset=Rack.objects.all(),
         queryset=Rack.objects.all(),
@@ -3470,8 +3511,8 @@ class ConnectCableToDeviceForm(BootstrapMixin, forms.ModelForm):
     class Meta:
     class Meta:
         model = Cable
         model = Cable
         fields = [
         fields = [
-            'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_id', 'type', 'status',
-            'label', 'color', 'length', 'length_unit',
+            'termination_b_region', 'termination_b_site', 'termination_b_rack', 'termination_b_device',
+            'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit',
         ]
         ]
         widgets = {
         widgets = {
             'status': StaticSelect2,
             'status': StaticSelect2,
@@ -3568,10 +3609,18 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm):
         label='Provider',
         label='Provider',
         required=False
         required=False
     )
     )
+    termination_b_region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        label='Region',
+        required=False
+    )
     termination_b_site = DynamicModelChoiceField(
     termination_b_site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         label='Site',
         label='Site',
-        required=False
+        required=False,
+        query_params={
+            'region_id': '$termination_b_region'
+        }
     )
     )
     termination_b_circuit = DynamicModelChoiceField(
     termination_b_circuit = DynamicModelChoiceField(
         queryset=Circuit.objects.all(),
         queryset=Circuit.objects.all(),
@@ -3595,8 +3644,8 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm):
     class Meta:
     class Meta:
         model = Cable
         model = Cable
         fields = [
         fields = [
-            'termination_b_provider', 'termination_b_site', 'termination_b_circuit', 'termination_b_id', 'type',
-            'status', 'label', 'color', 'length', 'length_unit',
+            'termination_b_provider', 'termination_b_region', 'termination_b_site', 'termination_b_circuit',
+            'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit',
         ]
         ]
 
 
     def clean_termination_b_id(self):
     def clean_termination_b_id(self):
@@ -3605,11 +3654,18 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm):
 
 
 
 
 class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm):
 class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm):
+    termination_b_region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        label='Region',
+        required=False
+    )
     termination_b_site = DynamicModelChoiceField(
     termination_b_site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         label='Site',
         label='Site',
         required=False,
         required=False,
-        display_field='cid'
+        query_params={
+            'region_id': '$termination_b_region'
+        }
     )
     )
     termination_b_rackgroup = DynamicModelChoiceField(
     termination_b_rackgroup = DynamicModelChoiceField(
         queryset=RackGroup.objects.all(),
         queryset=RackGroup.objects.all(),
@@ -3851,10 +3907,18 @@ class CableFilterForm(BootstrapMixin, forms.Form):
         required=False,
         required=False,
         label='Search'
         label='Search'
     )
     )
+    region = DynamicModelMultipleChoiceField(
+        queryset=Region.objects.all(),
+        to_field_name='slug',
+        required=False
+    )
     site = DynamicModelMultipleChoiceField(
     site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
-        required=False
+        required=False,
+        query_params={
+            'region': '$region'
+        }
     )
     )
     tenant = DynamicModelMultipleChoiceField(
     tenant = DynamicModelMultipleChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
@@ -3903,10 +3967,18 @@ class CableFilterForm(BootstrapMixin, forms.Form):
 #
 #
 
 
 class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
 class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
+    region = DynamicModelMultipleChoiceField(
+        queryset=Region.objects.all(),
+        to_field_name='slug',
+        required=False
+    )
     site = DynamicModelMultipleChoiceField(
     site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
-        required=False
+        required=False,
+        query_params={
+            'region': '$region'
+        }
     )
     )
     device_id = DynamicModelMultipleChoiceField(
     device_id = DynamicModelMultipleChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -3919,10 +3991,18 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
 
 
 
 
 class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
 class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
+    region = DynamicModelMultipleChoiceField(
+        queryset=Region.objects.all(),
+        to_field_name='slug',
+        required=False
+    )
     site = DynamicModelMultipleChoiceField(
     site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
-        required=False
+        required=False,
+        query_params={
+            'region': '$region'
+        }
     )
     )
     device_id = DynamicModelMultipleChoiceField(
     device_id = DynamicModelMultipleChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -3935,10 +4015,18 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
 
 
 
 
 class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
 class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
+    region = DynamicModelMultipleChoiceField(
+        queryset=Region.objects.all(),
+        to_field_name='slug',
+        required=False
+    )
     site = DynamicModelMultipleChoiceField(
     site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
-        required=False
+        required=False,
+        query_params={
+            'region': '$region'
+        }
     )
     )
     device_id = DynamicModelMultipleChoiceField(
     device_id = DynamicModelMultipleChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -3962,9 +4050,19 @@ class DeviceSelectionForm(forms.Form):
 
 
 
 
 class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
 class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        initial_params={
+            'sites': '$site'
+        }
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        required=False
+        required=False,
+        query_params={
+            'region_id': '$region'
+        }
     )
     )
     rack = DynamicModelChoiceField(
     rack = DynamicModelChoiceField(
         queryset=Rack.objects.all(),
         queryset=Rack.objects.all(),
@@ -3997,7 +4095,7 @@ class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
     class Meta:
     class Meta:
         model = VirtualChassis
         model = VirtualChassis
         fields = [
         fields = [
-            'name', 'domain', 'site', 'rack', 'members', 'initial_position', 'tags',
+            'name', 'domain', 'region', 'site', 'rack', 'members', 'initial_position', 'tags',
         ]
         ]
 
 
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
@@ -4094,9 +4192,19 @@ class DeviceVCMembershipForm(forms.ModelForm):
 
 
 
 
 class VCMemberSelectForm(BootstrapMixin, forms.Form):
 class VCMemberSelectForm(BootstrapMixin, forms.Form):
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        initial_params={
+            'sites': '$site'
+        }
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        required=False
+        required=False,
+        query_params={
+            'region_id': '$region'
+        }
     )
     )
     rack = DynamicModelChoiceField(
     rack = DynamicModelChoiceField(
         queryset=Rack.objects.all(),
         queryset=Rack.objects.all(),
@@ -4195,8 +4303,18 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
 #
 #
 
 
 class PowerPanelForm(BootstrapMixin, CustomFieldModelForm):
 class PowerPanelForm(BootstrapMixin, CustomFieldModelForm):
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        initial_params={
+            'sites': '$site'
+        }
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
-        queryset=Site.objects.all()
+        queryset=Site.objects.all(),
+        query_params={
+            'region_id': '$region'
+        }
     )
     )
     rack_group = DynamicModelChoiceField(
     rack_group = DynamicModelChoiceField(
         queryset=RackGroup.objects.all(),
         queryset=RackGroup.objects.all(),
@@ -4213,7 +4331,7 @@ class PowerPanelForm(BootstrapMixin, CustomFieldModelForm):
     class Meta:
     class Meta:
         model = PowerPanel
         model = PowerPanel
         fields = [
         fields = [
-            'site', 'rack_group', 'name', 'tags',
+            'region', 'site', 'rack_group', 'name', 'tags',
         ]
         ]
 
 
 
 
@@ -4248,9 +4366,19 @@ class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
         queryset=PowerPanel.objects.all(),
         queryset=PowerPanel.objects.all(),
         widget=forms.MultipleHiddenInput
         widget=forms.MultipleHiddenInput
     )
     )
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        initial_params={
+            'sites': '$site'
+        }
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        required=False
+        required=False,
+        query_params={
+            'region_id': '$region'
+        }
     )
     )
     rack_group = DynamicModelChoiceField(
     rack_group = DynamicModelChoiceField(
         queryset=RackGroup.objects.all(),
         queryset=RackGroup.objects.all(),
@@ -4302,9 +4430,22 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
 #
 #
 
 
 class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
 class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        initial_params={
+            'sites__powerpanel': '$power_panel'
+        }
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        required=False
+        required=False,
+        initial_params={
+            'powerpanel': '$power_panel'
+        },
+        query_params={
+            'region_id': '$region'
+        }
     )
     )
     power_panel = DynamicModelChoiceField(
     power_panel = DynamicModelChoiceField(
         queryset=PowerPanel.objects.all(),
         queryset=PowerPanel.objects.all(),
@@ -4329,7 +4470,7 @@ class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
     class Meta:
     class Meta:
         model = PowerFeed
         model = PowerFeed
         fields = [
         fields = [
-            'site', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
+            'region', 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
             'max_utilization', 'comments', 'tags',
             'max_utilization', 'comments', 'tags',
         ]
         ]
         widgets = {
         widgets = {
@@ -4339,14 +4480,6 @@ class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
             'phase': StaticSelect2(),
             'phase': StaticSelect2(),
         }
         }
 
 
-    def __init__(self, *args, **kwargs):
-
-        super().__init__(*args, **kwargs)
-
-        # Initialize site field
-        if self.instance and hasattr(self.instance, 'power_panel'):
-            self.initial['site'] = self.instance.power_panel.site
-
 
 
 class PowerFeedCSVForm(CustomFieldModelCSVForm):
 class PowerFeedCSVForm(CustomFieldModelCSVForm):
     site = CSVModelChoiceField(
     site = CSVModelChoiceField(

+ 1100 - 0
netbox/dcim/tables.py

@@ -0,0 +1,1100 @@
+import django_tables2 as tables
+from django_tables2.utils import Accessor
+
+from tenancy.tables import COL_TENANT
+from utilities.tables import (
+    BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, ColoredLabelColumn, TagColumn, ToggleColumn,
+)
+from .models import (
+    Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
+    DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
+    InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
+    PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
+    VirtualChassis,
+)
+
+MPTT_LINK = """
+{% if record.get_children %}
+    <span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>
+{% else %}
+    <span style="padding-left: {{ record.get_ancestors|length }}9px">
+{% endif %}
+    <a href="{{ record.get_absolute_url }}">{{ record.name }}</a>
+</span>
+"""
+
+SITE_REGION_LINK = """
+{% if record.region %}
+    <a href="{% url 'dcim:site_list' %}?region={{ record.region.slug }}">{{ record.region }}</a>
+{% else %}
+    &mdash;
+{% endif %}
+"""
+
+COLOR_LABEL = """
+{% load helpers %}
+<label class="label" style="color: {{ record.color|fgcolor }}; background-color: #{{ record.color }}">{{ record }}</label>
+"""
+
+DEVICE_LINK = """
+<a href="{% url 'dcim:device' pk=record.pk %}">
+    {{ record.name|default:'<span class="label label-info">Unnamed device</span>' }}
+</a>
+"""
+
+RACKGROUP_ELEVATIONS = """
+<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&group_id={{ record.pk }}" class="btn btn-xs btn-primary" title="View elevations">
+    <i class="fa fa-eye"></i>
+</a>
+"""
+
+RACK_DEVICE_COUNT = """
+<a href="{% url 'dcim:device_list' %}?rack_id={{ record.pk }}">{{ value }}</a>
+"""
+
+DEVICE_COUNT = """
+<a href="{% url 'dcim:device_list' %}?role={{ record.slug }}">{{ value|default:0 }}</a>
+"""
+
+RACKRESERVATION_ACTIONS = """
+<a href="{% url 'dcim:rackreservation_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
+    <i class="fa fa-history"></i>
+</a>
+{% if perms.dcim.change_rackreservation %}
+    <a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
+{% endif %}
+"""
+
+MANUFACTURER_ACTIONS = """
+<a href="{% url 'dcim:manufacturer_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
+    <i class="fa fa-history"></i>
+</a>
+{% if perms.dcim.change_manufacturer %}
+    <a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
+{% endif %}
+"""
+
+DEVICEROLE_DEVICE_COUNT = """
+<a href="{% url 'dcim:device_list' %}?role={{ record.slug }}">{{ value|default:0 }}</a>
+"""
+
+DEVICEROLE_VM_COUNT = """
+<a href="{% url 'virtualization:virtualmachine_list' %}?role={{ record.slug }}">{{ value|default:0 }}</a>
+"""
+
+DEVICEROLE_ACTIONS = """
+<a href="{% url 'dcim:devicerole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
+    <i class="fa fa-history"></i>
+</a>
+{% if perms.dcim.change_devicerole %}
+    <a href="{% url 'dcim:devicerole_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
+{% endif %}
+"""
+
+PLATFORM_DEVICE_COUNT = """
+<a href="{% url 'dcim:device_list' %}?platform={{ record.slug }}">{{ value|default:0 }}</a>
+"""
+
+PLATFORM_VM_COUNT = """
+<a href="{% url 'virtualization:virtualmachine_list' %}?platform={{ record.slug }}">{{ value|default:0 }}</a>
+"""
+
+STATUS_LABEL = """
+<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
+"""
+
+TYPE_LABEL = """
+<span class="label label-{{ record.get_type_class }}">{{ record.get_type_display }}</span>
+"""
+
+DEVICE_PRIMARY_IP = """
+{{ record.primary_ip6.address.ip|default:"" }}
+{% if record.primary_ip6 and record.primary_ip4 %}<br />{% endif %}
+{{ record.primary_ip4.address.ip|default:"" }}
+"""
+
+DEVICETYPE_INSTANCES_TEMPLATE = """
+<a href="{% url 'dcim:device_list' %}?manufacturer_id={{ record.manufacturer_id }}&device_type_id={{ record.pk }}">{{ record.instance_count }}</a>
+"""
+
+UTILIZATION_GRAPH = """
+{% load helpers %}
+{% utilization_graph value %}
+"""
+
+CABLE_TERMINATION_PARENT = """
+{% if value.device %}
+    <a href="{{ value.device.get_absolute_url }}">{{ value.device }}</a>
+{% elif value.circuit %}
+    <a href="{{ value.circuit.get_absolute_url }}">{{ value.circuit }}</a>
+{% elif value.power_panel %}
+    <a href="{{ value.power_panel.get_absolute_url }}">{{ value.power_panel }}</a>
+{% endif %}
+"""
+
+CABLE_LENGTH = """
+{% if record.length %}{{ record.length }} {{ record.get_length_unit_display }}{% else %}&mdash;{% endif %}
+"""
+
+POWERPANEL_POWERFEED_COUNT = """
+<a href="{% url 'dcim:powerfeed_list' %}?power_panel_id={{ record.pk }}">{{ value }}</a>
+"""
+
+INTERFACE_IPADDRESSES = """
+{% for ip in record.ip_addresses.unrestricted %}
+    <a href="{{ ip.get_absolute_url }}">{{ ip }}</a><br />
+{% endfor %}
+"""
+
+INTERFACE_TAGGED_VLANS = """
+{% for vlan in record.tagged_vlans.unrestricted %}
+    <a href="{{ vlan.get_absolute_url }}">{{ vlan }}</a><br />
+{% endfor %}
+"""
+
+CONNECTION_STATUS = """
+<span class="label label-{% if record.connection_status %}success{% else %}danger{% endif %}">{{ record.get_connection_status_display }}</span>
+"""
+
+
+#
+# Regions
+#
+
+class RegionTable(BaseTable):
+    pk = ToggleColumn()
+    name = tables.TemplateColumn(
+        template_code=MPTT_LINK,
+        orderable=False
+    )
+    site_count = tables.Column(
+        verbose_name='Sites'
+    )
+    actions = ButtonsColumn(Region)
+
+    class Meta(BaseTable.Meta):
+        model = Region
+        fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions')
+        default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
+
+
+#
+# Sites
+#
+
+class SiteTable(BaseTable):
+    pk = ToggleColumn()
+    name = tables.LinkColumn(
+        order_by=('_name',)
+    )
+    status = tables.TemplateColumn(
+        template_code=STATUS_LABEL
+    )
+    region = tables.TemplateColumn(
+        template_code=SITE_REGION_LINK
+    )
+    tenant = tables.TemplateColumn(
+        template_code=COL_TENANT
+    )
+    tags = TagColumn(
+        url_name='dcim:site_list'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = Site
+        fields = (
+            'pk', 'name', 'slug', 'status', 'facility', 'region', 'tenant', 'asn', 'time_zone', 'description',
+            'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
+            'contact_email', 'tags',
+        )
+        default_columns = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description')
+
+
+#
+# Rack groups
+#
+
+class RackGroupTable(BaseTable):
+    pk = ToggleColumn()
+    name = tables.TemplateColumn(
+        template_code=MPTT_LINK,
+        orderable=False
+    )
+    site = tables.LinkColumn(
+        viewname='dcim:site',
+        args=[Accessor('site__slug')],
+        verbose_name='Site'
+    )
+    rack_count = tables.Column(
+        verbose_name='Racks'
+    )
+    actions = ButtonsColumn(
+        model=RackGroup,
+        prepend_template=RACKGROUP_ELEVATIONS
+    )
+
+    class Meta(BaseTable.Meta):
+        model = RackGroup
+        fields = ('pk', 'name', 'site', 'rack_count', 'description', 'slug', 'actions')
+        default_columns = ('pk', 'name', 'site', 'rack_count', 'description', 'actions')
+
+
+#
+# Rack roles
+#
+
+class RackRoleTable(BaseTable):
+    pk = ToggleColumn()
+    name = tables.Column(linkify=True)
+    rack_count = tables.Column(verbose_name='Racks')
+    color = tables.TemplateColumn(COLOR_LABEL)
+    actions = ButtonsColumn(RackRole)
+
+    class Meta(BaseTable.Meta):
+        model = RackRole
+        fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions')
+        default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions')
+
+
+#
+# Racks
+#
+
+class RackTable(BaseTable):
+    pk = ToggleColumn()
+    name = tables.Column(
+        order_by=('_name',),
+        linkify=True
+    )
+    group = tables.Column(
+        linkify=True
+    )
+    site = tables.Column(
+        linkify=True
+    )
+    tenant = tables.TemplateColumn(
+        template_code=COL_TENANT
+    )
+    status = tables.TemplateColumn(
+        template_code=STATUS_LABEL
+    )
+    role = ColoredLabelColumn()
+    u_height = tables.TemplateColumn(
+        template_code="{{ record.u_height }}U",
+        verbose_name='Height'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = Rack
+        fields = (
+            'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
+            'width', 'u_height',
+        )
+        default_columns = ('pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height')
+
+
+class RackDetailTable(RackTable):
+    device_count = tables.TemplateColumn(
+        template_code=RACK_DEVICE_COUNT,
+        verbose_name='Devices'
+    )
+    get_utilization = tables.TemplateColumn(
+        template_code=UTILIZATION_GRAPH,
+        orderable=False,
+        verbose_name='Space'
+    )
+    get_power_utilization = tables.TemplateColumn(
+        template_code=UTILIZATION_GRAPH,
+        orderable=False,
+        verbose_name='Power'
+    )
+    tags = TagColumn(
+        url_name='dcim:rack_list'
+    )
+
+    class Meta(RackTable.Meta):
+        fields = (
+            'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
+            'width', 'u_height', 'device_count', 'get_utilization', 'get_power_utilization', 'tags',
+        )
+        default_columns = (
+            'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
+            'get_utilization', 'get_power_utilization',
+        )
+
+
+#
+# Rack reservations
+#
+
+class RackReservationTable(BaseTable):
+    pk = ToggleColumn()
+    reservation = tables.Column(
+        accessor='pk',
+        linkify=True
+    )
+    site = tables.Column(
+        accessor=Accessor('rack__site'),
+        linkify=True
+    )
+    tenant = tables.TemplateColumn(
+        template_code=COL_TENANT
+    )
+    rack = tables.Column(
+        linkify=True
+    )
+    unit_list = tables.Column(
+        orderable=False,
+        verbose_name='Units'
+    )
+    tags = TagColumn(
+        url_name='dcim:rackreservation_list'
+    )
+    actions = ButtonsColumn(RackReservation)
+
+    class Meta(BaseTable.Meta):
+        model = RackReservation
+        fields = (
+            'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags',
+            'actions',
+        )
+        default_columns = (
+            'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description', 'actions',
+        )
+
+
+#
+# Manufacturers
+#
+
+class ManufacturerTable(BaseTable):
+    pk = ToggleColumn()
+    name = tables.LinkColumn()
+    devicetype_count = tables.Column(
+        verbose_name='Device Types'
+    )
+    inventoryitem_count = tables.Column(
+        verbose_name='Inventory Items'
+    )
+    platform_count = tables.Column(
+        verbose_name='Platforms'
+    )
+    slug = tables.Column()
+    actions = ButtonsColumn(Manufacturer, pk_field='slug')
+
+    class Meta(BaseTable.Meta):
+        model = Manufacturer
+        fields = (
+            'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',
+        )
+
+
+#
+# Device types
+#
+
+class DeviceTypeTable(BaseTable):
+    pk = ToggleColumn()
+    model = tables.Column(
+        linkify=True,
+        verbose_name='Device Type'
+    )
+    is_full_depth = BooleanColumn(
+        verbose_name='Full Depth'
+    )
+    instance_count = tables.TemplateColumn(
+        template_code=DEVICETYPE_INSTANCES_TEMPLATE,
+        verbose_name='Instances'
+    )
+    tags = TagColumn(
+        url_name='dcim:devicetype_list'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = DeviceType
+        fields = (
+            'pk', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
+            'instance_count', 'tags',
+        )
+        default_columns = (
+            'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
+        )
+
+
+#
+# Device type components
+#
+
+class ComponentTemplateTable(BaseTable):
+    pk = ToggleColumn()
+    name = tables.Column(
+        order_by=('_name',)
+    )
+
+
+class ConsolePortTemplateTable(ComponentTemplateTable):
+    actions = ButtonsColumn(
+        model=ConsolePortTemplate,
+        buttons=('edit', 'delete'),
+        return_url_extra='%23tab_consoleports'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = ConsolePortTemplate
+        fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
+        empty_text = "None"
+
+
+class ConsoleServerPortTemplateTable(ComponentTemplateTable):
+    actions = ButtonsColumn(
+        model=ConsoleServerPortTemplate,
+        buttons=('edit', 'delete'),
+        return_url_extra='%23tab_consoleserverports'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = ConsoleServerPortTemplate
+        fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
+        empty_text = "None"
+
+
+class PowerPortTemplateTable(ComponentTemplateTable):
+    actions = ButtonsColumn(
+        model=PowerPortTemplate,
+        buttons=('edit', 'delete'),
+        return_url_extra='%23tab_powerports'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = PowerPortTemplate
+        fields = ('pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'actions')
+        empty_text = "None"
+
+
+class PowerOutletTemplateTable(ComponentTemplateTable):
+    actions = ButtonsColumn(
+        model=PowerOutletTemplate,
+        buttons=('edit', 'delete'),
+        return_url_extra='%23tab_poweroutlets'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = PowerOutletTemplate
+        fields = ('pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'actions')
+        empty_text = "None"
+
+
+class InterfaceTemplateTable(ComponentTemplateTable):
+    mgmt_only = BooleanColumn(
+        verbose_name='Management Only'
+    )
+    actions = ButtonsColumn(
+        model=InterfaceTemplate,
+        buttons=('edit', 'delete'),
+        return_url_extra='%23tab_interfaces'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = InterfaceTemplate
+        fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'actions')
+        empty_text = "None"
+
+
+class FrontPortTemplateTable(ComponentTemplateTable):
+    rear_port_position = tables.Column(
+        verbose_name='Position'
+    )
+    actions = ButtonsColumn(
+        model=FrontPortTemplate,
+        buttons=('edit', 'delete'),
+        return_url_extra='%23tab_frontports'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = FrontPortTemplate
+        fields = ('pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'actions')
+        empty_text = "None"
+
+
+class RearPortTemplateTable(ComponentTemplateTable):
+    actions = ButtonsColumn(
+        model=RearPortTemplate,
+        buttons=('edit', 'delete'),
+        return_url_extra='%23tab_rearports'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = RearPortTemplate
+        fields = ('pk', 'name', 'label', 'type', 'positions', 'description', 'actions')
+        empty_text = "None"
+
+
+class DeviceBayTemplateTable(ComponentTemplateTable):
+    actions = ButtonsColumn(
+        model=DeviceBayTemplate,
+        buttons=('edit', 'delete'),
+        return_url_extra='%23tab_devicebays'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = DeviceBayTemplate
+        fields = ('pk', 'name', 'label', 'description', 'actions')
+        empty_text = "None"
+
+
+#
+# Device roles
+#
+
+class DeviceRoleTable(BaseTable):
+    pk = ToggleColumn()
+    device_count = tables.TemplateColumn(
+        template_code=DEVICEROLE_DEVICE_COUNT,
+        verbose_name='Devices'
+    )
+    vm_count = tables.TemplateColumn(
+        template_code=DEVICEROLE_VM_COUNT,
+        verbose_name='VMs'
+    )
+    color = tables.TemplateColumn(
+        template_code=COLOR_LABEL,
+        verbose_name='Label'
+    )
+    vm_role = BooleanColumn()
+    actions = ButtonsColumn(DeviceRole, pk_field='slug')
+
+    class Meta(BaseTable.Meta):
+        model = DeviceRole
+        fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions')
+        default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions')
+
+
+#
+# Platforms
+#
+
+class PlatformTable(BaseTable):
+    pk = ToggleColumn()
+    device_count = tables.TemplateColumn(
+        template_code=PLATFORM_DEVICE_COUNT,
+        verbose_name='Devices'
+    )
+    vm_count = tables.TemplateColumn(
+        template_code=PLATFORM_VM_COUNT,
+        verbose_name='VMs'
+    )
+    actions = ButtonsColumn(Platform, pk_field='slug')
+
+    class Meta(BaseTable.Meta):
+        model = Platform
+        fields = (
+            'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
+            'description', 'actions',
+        )
+        default_columns = (
+            'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions',
+        )
+
+
+#
+# Devices
+#
+
+class DeviceTable(BaseTable):
+    pk = ToggleColumn()
+    name = tables.TemplateColumn(
+        order_by=('_name',),
+        template_code=DEVICE_LINK
+    )
+    status = tables.TemplateColumn(
+        template_code=STATUS_LABEL
+    )
+    tenant = tables.TemplateColumn(
+        template_code=COL_TENANT
+    )
+    site = tables.Column(
+        linkify=True
+    )
+    rack = tables.Column(
+        linkify=True
+    )
+    device_role = ColoredLabelColumn(
+        verbose_name='Role'
+    )
+    device_type = tables.LinkColumn(
+        viewname='dcim:devicetype',
+        args=[Accessor('device_type__pk')],
+        verbose_name='Type',
+        text=lambda record: record.device_type.display_name
+    )
+    primary_ip = tables.TemplateColumn(
+        template_code=DEVICE_PRIMARY_IP,
+        orderable=False,
+        verbose_name='IP Address'
+    )
+    primary_ip4 = tables.Column(
+        linkify=True,
+        verbose_name='IPv4 Address'
+    )
+    primary_ip6 = tables.Column(
+        linkify=True,
+        verbose_name='IPv6 Address'
+    )
+    cluster = tables.LinkColumn(
+        viewname='virtualization:cluster',
+        args=[Accessor('cluster__pk')]
+    )
+    virtual_chassis = tables.LinkColumn(
+        viewname='dcim:virtualchassis',
+        args=[Accessor('virtual_chassis__pk')]
+    )
+    vc_position = tables.Column(
+        verbose_name='VC Position'
+    )
+    vc_priority = tables.Column(
+        verbose_name='VC Priority'
+    )
+    tags = TagColumn(
+        url_name='dcim:device_list'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = Device
+        fields = (
+            'pk', 'name', 'status', 'tenant', 'device_role', 'device_type', 'platform', 'serial', 'asset_tag', 'site',
+            'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis',
+            'vc_position', 'vc_priority', 'tags',
+        )
+        default_columns = (
+            'pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip',
+        )
+
+
+class DeviceImportTable(BaseTable):
+    name = tables.TemplateColumn(
+        template_code=DEVICE_LINK
+    )
+    status = tables.TemplateColumn(
+        template_code=STATUS_LABEL
+    )
+    tenant = tables.TemplateColumn(
+        template_code=COL_TENANT
+    )
+    site = tables.Column(
+        linkify=True
+    )
+    rack = tables.Column(
+        linkify=True
+    )
+    device_role = tables.Column(
+        verbose_name='Role'
+    )
+    device_type = tables.Column(
+        verbose_name='Type'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = Device
+        fields = ('name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
+        empty_text = False
+
+
+#
+# Device components
+#
+
+class DeviceComponentTable(BaseTable):
+    pk = ToggleColumn()
+    device = tables.Column(
+        linkify=True
+    )
+    name = tables.Column(
+        linkify=True,
+        order_by=('_name',)
+    )
+    cable = tables.Column(
+        linkify=True
+    )
+
+    class Meta(BaseTable.Meta):
+        order_by = ('device', 'name')
+
+
+class ConsolePortTable(DeviceComponentTable):
+    tags = TagColumn(
+        url_name='dcim:consoleport_list'
+    )
+
+    class Meta(DeviceComponentTable.Meta):
+        model = ConsolePort
+        fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'tags')
+        default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
+
+
+class ConsoleServerPortTable(DeviceComponentTable):
+    tags = TagColumn(
+        url_name='dcim:consoleserverport_list'
+    )
+
+    class Meta(DeviceComponentTable.Meta):
+        model = ConsoleServerPort
+        fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'tags')
+        default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
+
+
+class PowerPortTable(DeviceComponentTable):
+    tags = TagColumn(
+        url_name='dcim:powerport_list'
+    )
+
+    class Meta(DeviceComponentTable.Meta):
+        model = PowerPort
+        fields = (
+            'pk', 'device', 'name', 'label', 'type', 'description', 'maximum_draw', 'allocated_draw', 'cable', 'tags',
+        )
+        default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
+
+
+class PowerOutletTable(DeviceComponentTable):
+    tags = TagColumn(
+        url_name='dcim:poweroutlet_list'
+    )
+
+    class Meta(DeviceComponentTable.Meta):
+        model = PowerOutlet
+        fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable', 'tags')
+        default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')
+
+
+class BaseInterfaceTable(BaseTable):
+    enabled = BooleanColumn()
+    ip_addresses = tables.TemplateColumn(
+        template_code=INTERFACE_IPADDRESSES,
+        orderable=False,
+        verbose_name='IP Addresses'
+    )
+    untagged_vlan = tables.Column(linkify=True)
+    tagged_vlans = tables.TemplateColumn(
+        template_code=INTERFACE_TAGGED_VLANS,
+        orderable=False,
+        verbose_name='Tagged VLANs'
+    )
+
+
+class InterfaceTable(DeviceComponentTable, BaseInterfaceTable):
+    tags = TagColumn(
+        url_name='dcim:interface_list'
+    )
+
+    class Meta(DeviceComponentTable.Meta):
+        model = Interface
+        fields = (
+            'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
+            'description', 'cable', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
+        )
+        default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
+
+
+class FrontPortTable(DeviceComponentTable):
+    rear_port_position = tables.Column(
+        verbose_name='Position'
+    )
+    tags = TagColumn(
+        url_name='dcim:frontport_list'
+    )
+
+    class Meta(DeviceComponentTable.Meta):
+        model = FrontPort
+        fields = (
+            'pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'tags',
+        )
+        default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
+
+
+class RearPortTable(DeviceComponentTable):
+    tags = TagColumn(
+        url_name='dcim:rearport_list'
+    )
+
+    class Meta(DeviceComponentTable.Meta):
+        model = RearPort
+        fields = ('pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'tags')
+        default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
+
+
+class DeviceBayTable(DeviceComponentTable):
+    installed_device = tables.Column(
+        linkify=True
+    )
+    tags = TagColumn(
+        url_name='dcim:devicebay_list'
+    )
+
+    class Meta(DeviceComponentTable.Meta):
+        model = DeviceBay
+        fields = ('pk', 'device', 'name', 'label', 'installed_device', 'description', 'tags')
+        default_columns = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
+
+
+class InventoryItemTable(DeviceComponentTable):
+    manufacturer = tables.Column(
+        linkify=True
+    )
+    discovered = BooleanColumn()
+    tags = TagColumn(
+        url_name='dcim:inventoryitem_list'
+    )
+    cable = None  # Override DeviceComponentTable
+
+    class Meta(DeviceComponentTable.Meta):
+        model = InventoryItem
+        fields = (
+            'pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
+            'discovered', 'tags',
+        )
+        default_columns = ('pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
+
+
+#
+# Cables
+#
+
+class CableTable(BaseTable):
+    pk = ToggleColumn()
+    id = tables.Column(
+        linkify=True,
+        verbose_name='ID'
+    )
+    termination_a_parent = tables.TemplateColumn(
+        template_code=CABLE_TERMINATION_PARENT,
+        accessor=Accessor('termination_a'),
+        orderable=False,
+        verbose_name='Side A'
+    )
+    termination_a = tables.LinkColumn(
+        accessor=Accessor('termination_a'),
+        orderable=False,
+        verbose_name='Termination A'
+    )
+    termination_b_parent = tables.TemplateColumn(
+        template_code=CABLE_TERMINATION_PARENT,
+        accessor=Accessor('termination_b'),
+        orderable=False,
+        verbose_name='Side B'
+    )
+    termination_b = tables.LinkColumn(
+        accessor=Accessor('termination_b'),
+        orderable=False,
+        verbose_name='Termination B'
+    )
+    status = tables.TemplateColumn(
+        template_code=STATUS_LABEL
+    )
+    length = tables.TemplateColumn(
+        template_code=CABLE_LENGTH,
+        order_by='_abs_length'
+    )
+    color = ColorColumn()
+    tags = TagColumn(
+        url_name='dcim:cable_list'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = Cable
+        fields = (
+            'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
+            'status', 'type', 'color', 'length', 'tags',
+        )
+        default_columns = (
+            'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
+            'status', 'type',
+        )
+
+
+#
+# Device connections
+#
+
+class ConsoleConnectionTable(BaseTable):
+    console_server = tables.LinkColumn(
+        viewname='dcim:device',
+        accessor=Accessor('connected_endpoint__device'),
+        args=[Accessor('connected_endpoint__device__pk')],
+        verbose_name='Console Server'
+    )
+    connected_endpoint = tables.Column(
+        linkify=True,
+        verbose_name='Port'
+    )
+    device = tables.Column(
+        linkify=True
+    )
+    name = tables.Column(
+        linkify=True,
+        verbose_name='Console Port'
+    )
+    connection_status = tables.TemplateColumn(
+        template_code=CONNECTION_STATUS,
+        verbose_name='Status'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = ConsolePort
+        fields = ('console_server', 'connected_endpoint', 'device', 'name', 'connection_status')
+
+
+class PowerConnectionTable(BaseTable):
+    pdu = tables.LinkColumn(
+        viewname='dcim:device',
+        accessor=Accessor('connected_endpoint__device'),
+        args=[Accessor('connected_endpoint__device__pk')],
+        order_by='_connected_poweroutlet__device',
+        verbose_name='PDU'
+    )
+    outlet = tables.Column(
+        accessor=Accessor('_connected_poweroutlet'),
+        linkify=True,
+        verbose_name='Outlet'
+    )
+    device = tables.Column(
+        linkify=True
+    )
+    name = tables.Column(
+        linkify=True,
+        verbose_name='Power Port'
+    )
+    connection_status = tables.TemplateColumn(
+        template_code=CONNECTION_STATUS,
+        verbose_name='Status'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = PowerPort
+        fields = ('pdu', 'outlet', 'device', 'name', 'connection_status')
+
+
+class InterfaceConnectionTable(BaseTable):
+    device_a = tables.LinkColumn(
+        viewname='dcim:device',
+        accessor=Accessor('device'),
+        args=[Accessor('device__pk')],
+        verbose_name='Device A'
+    )
+    interface_a = tables.LinkColumn(
+        viewname='dcim:interface',
+        accessor=Accessor('name'),
+        args=[Accessor('pk')],
+        verbose_name='Interface A'
+    )
+    device_b = tables.LinkColumn(
+        viewname='dcim:device',
+        accessor=Accessor('_connected_interface__device'),
+        args=[Accessor('_connected_interface__device__pk')],
+        verbose_name='Device B'
+    )
+    interface_b = tables.LinkColumn(
+        viewname='dcim:interface',
+        accessor=Accessor('_connected_interface'),
+        args=[Accessor('_connected_interface__pk')],
+        verbose_name='Interface B'
+    )
+    connection_status = tables.TemplateColumn(
+        template_code=CONNECTION_STATUS,
+        verbose_name='Status'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = Interface
+        fields = (
+            'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status',
+        )
+
+
+#
+# Virtual chassis
+#
+
+class VirtualChassisTable(BaseTable):
+    pk = ToggleColumn()
+    name = tables.Column(
+        linkify=True
+    )
+    master = tables.Column(
+        linkify=True
+    )
+    member_count = tables.Column(
+        verbose_name='Members'
+    )
+    tags = TagColumn(
+        url_name='dcim:virtualchassis_list'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = VirtualChassis
+        fields = ('pk', 'name', 'domain', 'master', 'member_count', 'tags')
+        default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
+
+
+#
+# Power panels
+#
+
+class PowerPanelTable(BaseTable):
+    pk = ToggleColumn()
+    name = tables.LinkColumn()
+    site = tables.LinkColumn(
+        viewname='dcim:site',
+        args=[Accessor('site__slug')]
+    )
+    powerfeed_count = tables.TemplateColumn(
+        template_code=POWERPANEL_POWERFEED_COUNT,
+        verbose_name='Feeds'
+    )
+    tags = TagColumn(
+        url_name='dcim:powerpanel_list'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = PowerPanel
+        fields = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count', 'tags')
+        default_columns = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count')
+
+
+#
+# Power feeds
+#
+
+class PowerFeedTable(BaseTable):
+    pk = ToggleColumn()
+    name = tables.LinkColumn()
+    power_panel = tables.Column(
+        linkify=True
+    )
+    rack = tables.Column(
+        linkify=True
+    )
+    status = tables.TemplateColumn(
+        template_code=STATUS_LABEL
+    )
+    type = tables.TemplateColumn(
+        template_code=TYPE_LABEL
+    )
+    max_utilization = tables.TemplateColumn(
+        template_code="{{ value }}%"
+    )
+    available_power = tables.Column(
+        verbose_name='Available power (VA)'
+    )
+    tags = TagColumn(
+        url_name='dcim:powerfeed_list'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = PowerFeed
+        fields = (
+            'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
+            'max_utilization', 'available_power', 'tags',
+        )
+        default_columns = (
+            'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
+        )

+ 8 - 5
netbox/dcim/tables/racks.py

@@ -70,12 +70,15 @@ class RackRoleTable(BaseTable):
 
 
 class RackTable(BaseTable):
 class RackTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
-    name = tables.LinkColumn(
-        order_by=('_name',)
+    name = tables.Column(
+        order_by=('_name',),
+        linkify=True
     )
     )
-    site = tables.LinkColumn(
-        viewname='dcim:site',
-        args=[Accessor('site__slug')]
+    group = tables.Column(
+        linkify=True
+    )
+    site = tables.Column(
+        linkify=True
     )
     )
     tenant = tables.TemplateColumn(
     tenant = tables.TemplateColumn(
         template_code=COL_TENANT
         template_code=COL_TENANT

+ 0 - 17
netbox/dcim/tests/test_forms.py

@@ -100,23 +100,6 @@ class DeviceTestCase(TestCase):
         self.assertIn('face', form.errors)
         self.assertIn('face', form.errors)
         self.assertIn('position', 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)
-
 
 
 class LabelTestCase(TestCase):
 class LabelTestCase(TestCase):
 
 

+ 4 - 1
netbox/dcim/views.py

@@ -2089,8 +2089,11 @@ class CableCreateView(ObjectEditView):
         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)
         # Set initial site and rack based on side A termination (if not already set)
+        termination_a_site = getattr(obj.termination_a.parent, 'site', None)
+        if termination_a_site and 'termination_b_region' not in initial_data:
+            initial_data['termination_b_region'] = termination_a_site.region
         if 'termination_b_site' not in initial_data:
         if 'termination_b_site' not in initial_data:
-            initial_data['termination_b_site'] = getattr(obj.termination_a.parent, 'site', None)
+            initial_data['termination_b_site'] = termination_a_site
         if 'termination_b_rack' not in initial_data:
         if 'termination_b_rack' not in initial_data:
             initial_data['termination_b_rack'] = getattr(obj.termination_a.parent, 'rack', None)
             initial_data['termination_b_rack'] = getattr(obj.termination_a.parent, 'rack', None)
 
 

+ 75 - 19
netbox/ipam/forms.py

@@ -351,10 +351,20 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         label='VRF',
         label='VRF',
         display_field='display_name'
         display_field='display_name'
     )
     )
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        initial_params={
+            'sites': '$site'
+        }
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         required=False,
         required=False,
-        null_option='None'
+        null_option='None',
+        query_params={
+            'region_id': '$region'
+        }
     )
     )
     vlan_group = DynamicModelChoiceField(
     vlan_group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         queryset=VLANGroup.objects.all(),
@@ -363,6 +373,9 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         null_option='None',
         null_option='None',
         query_params={
         query_params={
             'site_id': '$site'
             'site_id': '$site'
+        },
+        initial_params={
+            'vlans': '$vlan'
         }
         }
     )
     )
     vlan = DynamicModelChoiceField(
     vlan = DynamicModelChoiceField(
@@ -395,14 +408,6 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         }
         }
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
-
-        # Initialize helper selectors
-        instance = kwargs.get('instance')
-        initial = kwargs.get('initial', {}).copy()
-        if instance and instance.vlan is not None:
-            initial['vlan_group'] = instance.vlan.group
-        kwargs['initial'] = initial
-
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
         self.fields['vrf'].empty_label = 'Global'
         self.fields['vrf'].empty_label = 'Global'
@@ -472,9 +477,17 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
         queryset=Prefix.objects.all(),
         queryset=Prefix.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        to_field_name='slug'
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        required=False
+        required=False,
+        query_params={
+            'region': '$region'
+        }
     )
     )
     vrf = DynamicModelChoiceField(
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
@@ -604,7 +617,10 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
     device = DynamicModelChoiceField(
     device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         required=False,
         required=False,
-        display_field='display_name'
+        display_field='display_name',
+        initial_params={
+            'interfaces': '$interface'
+        }
     )
     )
     interface = DynamicModelChoiceField(
     interface = DynamicModelChoiceField(
         queryset=Interface.objects.all(),
         queryset=Interface.objects.all(),
@@ -615,7 +631,10 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
     )
     )
     virtual_machine = DynamicModelChoiceField(
     virtual_machine = DynamicModelChoiceField(
         queryset=VirtualMachine.objects.all(),
         queryset=VirtualMachine.objects.all(),
-        required=False
+        required=False,
+        initial_params={
+            'interfaces': '$vminterface'
+        }
     )
     )
     vminterface = DynamicModelChoiceField(
     vminterface = DynamicModelChoiceField(
         queryset=VMInterface.objects.all(),
         queryset=VMInterface.objects.all(),
@@ -631,10 +650,21 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
         label='VRF',
         label='VRF',
         display_field='display_name'
         display_field='display_name'
     )
     )
+    nat_region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        label='Region',
+        initial_params={
+            'sites': '$nat_site'
+        }
+    )
     nat_site = DynamicModelChoiceField(
     nat_site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         required=False,
         required=False,
-        label='Site'
+        label='Site',
+        query_params={
+            'region_id': '$nat_region'
+        }
     )
     )
     nat_rack = DynamicModelChoiceField(
     nat_rack = DynamicModelChoiceField(
         queryset=Rack.objects.all(),
         queryset=Rack.objects.all(),
@@ -714,10 +744,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
         initial = kwargs.get('initial', {}).copy()
         initial = kwargs.get('initial', {}).copy()
         if instance:
         if instance:
             if type(instance.assigned_object) is Interface:
             if type(instance.assigned_object) is Interface:
-                initial['device'] = instance.assigned_object.device
                 initial['interface'] = instance.assigned_object
                 initial['interface'] = instance.assigned_object
             elif type(instance.assigned_object) is VMInterface:
             elif type(instance.assigned_object) is VMInterface:
-                initial['virtual_machine'] = instance.assigned_object.virtual_machine
                 initial['vminterface'] = instance.assigned_object
                 initial['vminterface'] = instance.assigned_object
             if instance.nat_inside:
             if instance.nat_inside:
                 nat_inside_parent = instance.nat_inside.assigned_object
                 nat_inside_parent = instance.nat_inside.assigned_object
@@ -1028,16 +1056,26 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
 #
 #
 
 
 class VLANGroupForm(BootstrapMixin, forms.ModelForm):
 class VLANGroupForm(BootstrapMixin, forms.ModelForm):
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        initial_params={
+            'sites': '$site'
+        }
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        required=False
+        required=False,
+        query_params={
+            'region_id': '$region'
+        }
     )
     )
     slug = SlugField()
     slug = SlugField()
 
 
     class Meta:
     class Meta:
         model = VLANGroup
         model = VLANGroup
         fields = [
         fields = [
-            'site', 'name', 'slug', 'description',
+            'region', 'site', 'name', 'slug', 'description',
         ]
         ]
 
 
 
 
@@ -1077,10 +1115,20 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
 #
 #
 
 
 class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
 class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        initial_params={
+            'sites': '$site'
+        }
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         required=False,
         required=False,
-        null_option='None'
+        null_option='None',
+        query_params={
+            'region_id': '$region'
+        }
     )
     )
     group = DynamicModelChoiceField(
     group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         queryset=VLANGroup.objects.all(),
@@ -1169,9 +1217,17 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        to_field_name='slug'
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        required=False
+        required=False,
+        query_params={
+            'region': '$region'
+        }
     )
     )
     group = DynamicModelChoiceField(
     group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         queryset=VLANGroup.objects.all(),

+ 1 - 0
netbox/templates/circuits/circuittermination_edit.html

@@ -40,6 +40,7 @@
                                 <p class="form-control-static">{{ form.term_side.value }}</p>
                                 <p class="form-control-static">{{ form.term_side.value }}</p>
                             </div>
                             </div>
                         </div>
                         </div>
+                        {% render_field form.region %}
                         {% render_field form.site %}
                         {% render_field form.site %}
                     </div>
                     </div>
                 </div>
                 </div>

+ 9 - 0
netbox/templates/dcim/cable_connect.html

@@ -32,6 +32,12 @@
                     <div class="panel-body">
                     <div class="panel-body">
                         {% if termination_a.device %}
                         {% if termination_a.device %}
                             {# Device component #}
                             {# Device component #}
+                            <div class="form-group">
+                                <label class="col-md-3 control-label required">Region</label>
+                                <div class="col-md-9">
+                                    <p class="form-control-static">{{ termination_a.device.site.region }}</p>
+                                </div>
+                            </div>
                             <div class="form-group">
                             <div class="form-group">
                                 <label class="col-md-3 control-label required">Site</label>
                                 <label class="col-md-3 control-label required">Site</label>
                                 <div class="col-md-9">
                                 <div class="col-md-9">
@@ -111,6 +117,9 @@
                         {% if 'termination_b_provider' in form.fields %}
                         {% if 'termination_b_provider' in form.fields %}
                             {% render_field form.termination_b_provider %}
                             {% render_field form.termination_b_provider %}
                         {% endif %}
                         {% endif %}
+                        {% if 'termination_b_region' in form.fields %}
+                            {% render_field form.termination_b_region %}
+                        {% endif %}
                         {% if 'termination_b_site' in form.fields %}
                         {% if 'termination_b_site' in form.fields %}
                             {% render_field form.termination_b_site %}
                             {% render_field form.termination_b_site %}
                         {% endif %}
                         {% endif %}

+ 7 - 1
netbox/templates/dcim/powerfeed_edit.html

@@ -3,10 +3,16 @@
 
 
 {% block form %}
 {% block form %}
     <div class="panel panel-default">
     <div class="panel panel-default">
-        <div class="panel-heading"><strong>Power Feed</strong></div>
+        <div class="panel-heading"><strong>Power Panel</strong></div>
         <div class="panel-body">
         <div class="panel-body">
+            {% render_field form.region %}
             {% render_field form.site %}
             {% render_field form.site %}
             {% render_field form.power_panel %}
             {% render_field form.power_panel %}
+        </div>
+    </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Power Feed</strong></div>
+        <div class="panel-body">
             {% render_field form.rack %}
             {% render_field form.rack %}
             {% render_field form.name %}
             {% render_field form.name %}
             {% render_field form.status %}
             {% render_field form.status %}

+ 1 - 0
netbox/templates/dcim/rack_edit.html

@@ -5,6 +5,7 @@
     <div class="panel panel-default">
     <div class="panel panel-default">
         <div class="panel-heading"><strong>Rack</strong></div>
         <div class="panel-heading"><strong>Rack</strong></div>
         <div class="panel-body">
         <div class="panel-body">
+            {% render_field form.region %}
             {% render_field form.site %}
             {% render_field form.site %}
             {% render_field form.name %}
             {% render_field form.name %}
             {% render_field form.facility_id %}
             {% render_field form.facility_id %}

+ 1 - 0
netbox/templates/dcim/rackreservation_edit.html

@@ -5,6 +5,7 @@
     <div class="panel panel-default">
     <div class="panel panel-default">
         <div class="panel-heading"><strong>Rack Reservation</strong></div>
         <div class="panel-heading"><strong>Rack Reservation</strong></div>
         <div class="panel-body">
         <div class="panel-body">
+            {% render_field form.region %}
             {% render_field form.site %}
             {% render_field form.site %}
             {% render_field form.rack_group %}
             {% render_field form.rack_group %}
             {% render_field form.rack %}
             {% render_field form.rack %}

+ 1 - 0
netbox/templates/dcim/virtualchassis_add.html

@@ -13,6 +13,7 @@
     <div class="panel panel-default">
     <div class="panel panel-default">
         <div class="panel-heading"><strong>Member Devices</strong></div>
         <div class="panel-heading"><strong>Member Devices</strong></div>
         <div class="panel-body">
         <div class="panel-body">
+            {% render_field form.region %}
             {% render_field form.site %}
             {% render_field form.site %}
             {% render_field form.rack %}
             {% render_field form.rack %}
             {% render_field form.members %}
             {% render_field form.members %}

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

@@ -62,6 +62,7 @@
             </ul>
             </ul>
             <div class="tab-content">
             <div class="tab-content">
                 <div class="tab-pane active" id="by_device">
                 <div class="tab-pane active" id="by_device">
+                    {% render_field form.nat_region %}
                     {% render_field form.nat_site %}
                     {% render_field form.nat_site %}
                     {% render_field form.nat_rack %}
                     {% render_field form.nat_rack %}
                     {% render_field form.nat_device %}
                     {% render_field form.nat_device %}

+ 1 - 0
netbox/templates/ipam/prefix_edit.html

@@ -16,6 +16,7 @@
     <div class="panel panel-default">
     <div class="panel panel-default">
         <div class="panel-heading"><strong>Site/VLAN Assignment</strong></div>
         <div class="panel-heading"><strong>Site/VLAN Assignment</strong></div>
         <div class="panel-body">
         <div class="panel-body">
+            {% render_field form.region %}
             {% render_field form.site %}
             {% render_field form.site %}
             {% render_field form.vlan_group %}
             {% render_field form.vlan_group %}
             {% render_field form.vlan %}
             {% render_field form.vlan %}

+ 8 - 2
netbox/templates/ipam/vlan_edit.html

@@ -8,12 +8,18 @@
             {% render_field form.vid %}
             {% render_field form.vid %}
             {% render_field form.name %}
             {% render_field form.name %}
             {% render_field form.status %}
             {% render_field form.status %}
-            {% render_field form.site %}
-            {% render_field form.group %}
             {% render_field form.role %}
             {% render_field form.role %}
             {% render_field form.description %}
             {% render_field form.description %}
         </div>
         </div>
     </div>
     </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Assignment</strong></div>
+        <div class="panel-body">
+            {% render_field form.region %}
+            {% render_field form.site %}
+            {% render_field form.group %}
+        </div>
+    </div>
     <div class="panel panel-default">
     <div class="panel panel-default">
         <div class="panel-heading"><strong>Tenancy</strong></div>
         <div class="panel-heading"><strong>Tenancy</strong></div>
         <div class="panel-body">
         <div class="panel-body">

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

@@ -8,6 +8,7 @@
             {% 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.region %}
             {% render_field form.site %}
             {% render_field form.site %}
         </div>
         </div>
     </div>
     </div>

+ 4 - 12
netbox/tenancy/forms.py

@@ -119,7 +119,10 @@ class TenancyForm(forms.Form):
     tenant_group = DynamicModelChoiceField(
     tenant_group = DynamicModelChoiceField(
         queryset=TenantGroup.objects.all(),
         queryset=TenantGroup.objects.all(),
         required=False,
         required=False,
-        null_option='None'
+        null_option='None',
+        initial_params={
+            'tenants': '$tenant'
+        }
     )
     )
     tenant = DynamicModelChoiceField(
     tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
@@ -129,17 +132,6 @@ class TenancyForm(forms.Form):
         }
         }
     )
     )
 
 
-    def __init__(self, *args, **kwargs):
-
-        # Initialize helper selector
-        instance = kwargs.get('instance')
-        if instance and instance.tenant is not None:
-            initial = kwargs.get('initial', {}).copy()
-            initial['tenant_group'] = instance.tenant.group
-            kwargs['initial'] = initial
-
-        super().__init__(*args, **kwargs)
-
 
 
 class TenancyFilterForm(forms.Form):
 class TenancyFilterForm(forms.Form):
     tenant_group = DynamicModelMultipleChoiceField(
     tenant_group = DynamicModelMultipleChoiceField(

+ 14 - 2
netbox/utilities/forms/fields.py

@@ -241,6 +241,7 @@ class DynamicModelChoiceMixin:
     """
     """
     :param display_field: The name of the attribute of an API response object to display in the selection list
     :param display_field: The name of the attribute of an API response object to display in the selection list
     :param query_params: A dictionary of additional key/value pairs to attach to the API request
     :param query_params: A dictionary of additional key/value pairs to attach to the API request
+    :param initial_params: A dictionary of child field references to use for selecting a parent field's initial value
     :param null_option: The string used to represent a null selection (if any)
     :param null_option: The string used to represent a null selection (if any)
     :param disabled_indicator: The name of the field which, if populated, will disable selection of the
     :param disabled_indicator: The name of the field which, if populated, will disable selection of the
         choice (optional)
         choice (optional)
@@ -249,10 +250,11 @@ class DynamicModelChoiceMixin:
     filter = django_filters.ModelChoiceFilter
     filter = django_filters.ModelChoiceFilter
     widget = widgets.APISelect
     widget = widgets.APISelect
 
 
-    def __init__(self, display_field='name', query_params=None, null_option=None, disabled_indicator=None,
-                 brief_mode=True, *args, **kwargs):
+    def __init__(self, display_field='name', query_params=None, initial_params=None, null_option=None,
+                 disabled_indicator=None, brief_mode=True, *args, **kwargs):
         self.display_field = display_field
         self.display_field = display_field
         self.query_params = query_params or {}
         self.query_params = query_params or {}
+        self.initial_params = initial_params or {}
         self.null_option = null_option
         self.null_option = null_option
         self.disabled_indicator = disabled_indicator
         self.disabled_indicator = disabled_indicator
         self.brief_mode = brief_mode
         self.brief_mode = brief_mode
@@ -293,6 +295,16 @@ class DynamicModelChoiceMixin:
     def get_bound_field(self, form, field_name):
     def get_bound_field(self, form, field_name):
         bound_field = BoundField(form, self, field_name)
         bound_field = BoundField(form, self, field_name)
 
 
+        # Set initial value based on prescribed child fields (if not already set)
+        if not self.initial and self.initial_params:
+            filter_kwargs = {}
+            for kwarg, child_field in self.initial_params.items():
+                value = form.initial.get(child_field.lstrip('$'))
+                if value:
+                    filter_kwargs[kwarg] = value
+            if filter_kwargs:
+                self.initial = self.queryset.filter(**filter_kwargs).first()
+
         # Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
         # Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
         # will be populated on-demand via the APISelect widget.
         # will be populated on-demand via the APISelect widget.
         data = bound_field.value()
         data = bound_field.value()

+ 5 - 1
netbox/utilities/middleware.py

@@ -7,7 +7,7 @@ from django.http import Http404, HttpResponseRedirect
 from django.urls import reverse
 from django.urls import reverse
 
 
 from .api import is_api_request
 from .api import is_api_request
-from .views import server_error
+from .views import server_error, rest_api_server_error
 
 
 
 
 class LoginRequiredMiddleware(object):
 class LoginRequiredMiddleware(object):
@@ -86,6 +86,10 @@ class ExceptionHandlingMiddleware(object):
         if isinstance(exception, Http404):
         if isinstance(exception, Http404):
             return
             return
 
 
+        # Handle exceptions that occur from REST API requests
+        if is_api_request(request):
+            return rest_api_server_error(request)
+
         # Determine the type of exception. If it's a common issue, return a custom error page with instructions.
         # Determine the type of exception. If it's a common issue, return a custom error page with instructions.
         custom_template = None
         custom_template = None
         if isinstance(exception, ProgrammingError):
         if isinstance(exception, ProgrammingError):

+ 19 - 4
netbox/utilities/views.py

@@ -13,7 +13,7 @@ from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured, Obje
 from django.db import transaction, IntegrityError
 from django.db import transaction, IntegrityError
 from django.db.models import ManyToManyField, ProtectedError
 from django.db.models import ManyToManyField, ProtectedError
 from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
 from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
-from django.http import HttpResponse, HttpResponseServerError
+from django.http import HttpResponse, HttpResponseServerError, JsonResponse
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django.template import loader
 from django.template import loader
 from django.template.exceptions import TemplateDoesNotExist
 from django.template.exceptions import TemplateDoesNotExist
@@ -27,6 +27,7 @@ from django.views.decorators.csrf import requires_csrf_token
 from django.views.defaults import ERROR_500_TEMPLATE_NAME
 from django.views.defaults import ERROR_500_TEMPLATE_NAME
 from django.views.generic import View
 from django.views.generic import View
 from django_tables2 import RequestConfig
 from django_tables2 import RequestConfig
+from rest_framework import status
 
 
 from extras.models import CustomField, ExportTemplate
 from extras.models import CustomField, ExportTemplate
 from utilities.exceptions import AbortTransaction
 from utilities.exceptions import AbortTransaction
@@ -1367,8 +1368,22 @@ def server_error(request, template_name=ERROR_500_TEMPLATE_NAME):
     type_, error, traceback = sys.exc_info()
     type_, error, traceback = sys.exc_info()
 
 
     return HttpResponseServerError(template.render({
     return HttpResponseServerError(template.render({
-        'python_version': platform.python_version(),
-        'netbox_version': settings.VERSION,
-        'exception': str(type_),
         'error': error,
         'error': error,
+        'exception': str(type_),
+        'netbox_version': settings.VERSION,
+        'python_version': platform.python_version(),
     }))
     }))
+
+
+def rest_api_server_error(request, *args, **kwargs):
+    """
+    Handle exceptions and return a useful error message for REST API requests.
+    """
+    type_, error, traceback = sys.exc_info()
+    data = {
+        'error': str(error),
+        'exception': type_.__name__,
+        'netbox_version': settings.VERSION,
+        'python_version': platform.python_version(),
+    }
+    return JsonResponse(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

+ 25 - 12
netbox/virtualization/forms.py

@@ -79,9 +79,19 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         queryset=ClusterGroup.objects.all(),
         queryset=ClusterGroup.objects.all(),
         required=False
         required=False
     )
     )
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        initial_params={
+            'sites': '$site'
+        }
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        required=False
+        required=False,
+        query_params={
+            'region_id': '$region'
+        }
     )
     )
     comments = CommentField()
     comments = CommentField()
     tags = DynamicModelMultipleChoiceField(
     tags = DynamicModelMultipleChoiceField(
@@ -92,7 +102,7 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
     class Meta:
     class Meta:
         model = Cluster
         model = Cluster
         fields = (
         fields = (
-            'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags',
+            'name', 'type', 'group', 'tenant', 'region', 'site', 'comments', 'tags',
         )
         )
 
 
 
 
@@ -143,9 +153,17 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         required=False
         required=False
     )
     )
+    region = DynamicModelChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        to_field_name='slug'
+    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        required=False
+        required=False,
+        query_params={
+            'region': '$region'
+        }
     )
     )
     comments = CommentField(
     comments = CommentField(
         widget=SmallTextarea,
         widget=SmallTextarea,
@@ -266,7 +284,10 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
     cluster_group = DynamicModelChoiceField(
     cluster_group = DynamicModelChoiceField(
         queryset=ClusterGroup.objects.all(),
         queryset=ClusterGroup.objects.all(),
         required=False,
         required=False,
-        null_option='None'
+        null_option='None',
+        initial_params={
+            'clusters': '$cluster'
+        }
     )
     )
     cluster = DynamicModelChoiceField(
     cluster = DynamicModelChoiceField(
         queryset=Cluster.objects.all(),
         queryset=Cluster.objects.all(),
@@ -311,14 +332,6 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         }
         }
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
-
-        # Initialize helper selector
-        instance = kwargs.get('instance')
-        if instance.pk and instance.cluster is not None:
-            initial = kwargs.get('initial', {}).copy()
-            initial['cluster_group'] = instance.cluster.group
-            kwargs['initial'] = initial
-
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
         if self.instance.pk:
         if self.instance.pk: