mirror of
				https://github.com/netbox-community/netbox.git
				synced 2024-05-10 07:54:54 +00:00 
			
		
		
		
	* Initial work to support rackless devices * Updated device component connection forms * Updated IP address assignment form * Updated circuit termination form * Formatting cleanup * Fixed tests
This commit is contained in:
		@@ -143,19 +143,49 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
 | 
			
		||||
    site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
 | 
			
		||||
    rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack',
 | 
			
		||||
                                  widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}',
 | 
			
		||||
                                                   attrs={'filter-for': 'device'}))
 | 
			
		||||
    device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
 | 
			
		||||
                                    widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
 | 
			
		||||
                                                     display_field='display_name', attrs={'filter-for': 'interface'}))
 | 
			
		||||
    livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
 | 
			
		||||
        query_key='q', query_url='dcim-api:device_list', field_to_update='device')
 | 
			
		||||
    site = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Site.objects.all(),
 | 
			
		||||
        widget=forms.Select(
 | 
			
		||||
            attrs={'filter-for': 'rack'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    rack = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Rack.objects.all(),
 | 
			
		||||
        required=False,
 | 
			
		||||
        label='Rack',
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/racks/?site_id={{site}}',
 | 
			
		||||
            attrs={'filter-for': 'device', 'nullable': 'true'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    device = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Device.objects.all(),
 | 
			
		||||
        required=False,
 | 
			
		||||
        label='Device',
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
 | 
			
		||||
            display_field='display_name',
 | 
			
		||||
            attrs={'filter-for': 'interface'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    livesearch = forms.CharField(
 | 
			
		||||
        required=False,
 | 
			
		||||
        label='Device',
 | 
			
		||||
        widget=Livesearch(
 | 
			
		||||
            query_key='q',
 | 
			
		||||
            query_url='dcim-api:device_list',
 | 
			
		||||
            field_to_update='device'
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    interface = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Interface.objects.all(),
 | 
			
		||||
        required=False,
 | 
			
		||||
        label='Interface',
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
 | 
			
		||||
            disabled_indicator='is_connected'
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    interface = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Interface',
 | 
			
		||||
                                       widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
 | 
			
		||||
                                                        disabled_indicator='is_connected'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = CircuitTermination
 | 
			
		||||
 
 | 
			
		||||
@@ -275,6 +275,7 @@ class DeviceSerializer(CustomFieldSerializer, serializers.ModelSerializer):
 | 
			
		||||
    device_role = DeviceRoleNestedSerializer()
 | 
			
		||||
    tenant = TenantNestedSerializer()
 | 
			
		||||
    platform = PlatformNestedSerializer()
 | 
			
		||||
    site = SiteNestedSerializer()
 | 
			
		||||
    rack = RackNestedSerializer()
 | 
			
		||||
    primary_ip = DeviceIPAddressNestedSerializer()
 | 
			
		||||
    primary_ip4 = DeviceIPAddressNestedSerializer()
 | 
			
		||||
@@ -283,9 +284,11 @@ class DeviceSerializer(CustomFieldSerializer, serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Device
 | 
			
		||||
        fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial',
 | 
			
		||||
                  'asset_tag', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
 | 
			
		||||
                  'primary_ip6', 'comments', 'custom_fields']
 | 
			
		||||
        fields = [
 | 
			
		||||
            'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
 | 
			
		||||
            'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
 | 
			
		||||
            'comments', 'custom_fields',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def get_parent_device(self, obj):
 | 
			
		||||
        try:
 | 
			
		||||
 
 | 
			
		||||
@@ -175,12 +175,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
 | 
			
		||||
        label='MAC address',
 | 
			
		||||
    )
 | 
			
		||||
    site_id = django_filters.ModelMultipleChoiceFilter(
 | 
			
		||||
        name='rack__site',
 | 
			
		||||
        name='site',
 | 
			
		||||
        queryset=Site.objects.all(),
 | 
			
		||||
        label='Site (ID)',
 | 
			
		||||
    )
 | 
			
		||||
    site = django_filters.ModelMultipleChoiceFilter(
 | 
			
		||||
        name='rack__site__slug',
 | 
			
		||||
        name='site__slug',
 | 
			
		||||
        queryset=Site.objects.all(),
 | 
			
		||||
        to_field_name='slug',
 | 
			
		||||
        label='Site name (slug)',
 | 
			
		||||
@@ -190,7 +190,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
 | 
			
		||||
        queryset=RackGroup.objects.all(),
 | 
			
		||||
        label='Rack group (ID)',
 | 
			
		||||
    )
 | 
			
		||||
    rack_id = django_filters.ModelMultipleChoiceFilter(
 | 
			
		||||
    rack_id = NullableModelMultipleChoiceFilter(
 | 
			
		||||
        name='rack',
 | 
			
		||||
        queryset=Rack.objects.all(),
 | 
			
		||||
        label='Rack (ID)',
 | 
			
		||||
 
 | 
			
		||||
@@ -1915,6 +1915,7 @@
 | 
			
		||||
        "platform": 1,
 | 
			
		||||
        "name": "test1-edge1",
 | 
			
		||||
        "serial": "5555555555",
 | 
			
		||||
        "site": 1,
 | 
			
		||||
        "rack": 1,
 | 
			
		||||
        "position": 1,
 | 
			
		||||
        "face": 0,
 | 
			
		||||
@@ -1935,6 +1936,7 @@
 | 
			
		||||
        "platform": 1,
 | 
			
		||||
        "name": "test1-core1",
 | 
			
		||||
        "serial": "",
 | 
			
		||||
        "site": 1,
 | 
			
		||||
        "rack": 1,
 | 
			
		||||
        "position": 17,
 | 
			
		||||
        "face": 0,
 | 
			
		||||
@@ -1955,6 +1957,7 @@
 | 
			
		||||
        "platform": 1,
 | 
			
		||||
        "name": "test1-spine1",
 | 
			
		||||
        "serial": "",
 | 
			
		||||
        "site": 1,
 | 
			
		||||
        "rack": 1,
 | 
			
		||||
        "position": 33,
 | 
			
		||||
        "face": 0,
 | 
			
		||||
@@ -1975,6 +1978,7 @@
 | 
			
		||||
        "platform": 1,
 | 
			
		||||
        "name": "test1-leaf1",
 | 
			
		||||
        "serial": "",
 | 
			
		||||
        "site": 1,
 | 
			
		||||
        "rack": 1,
 | 
			
		||||
        "position": 34,
 | 
			
		||||
        "face": 0,
 | 
			
		||||
@@ -1995,6 +1999,7 @@
 | 
			
		||||
        "platform": 1,
 | 
			
		||||
        "name": "test1-leaf2",
 | 
			
		||||
        "serial": "9823478293748",
 | 
			
		||||
        "site": 1,
 | 
			
		||||
        "rack": 2,
 | 
			
		||||
        "position": 34,
 | 
			
		||||
        "face": 0,
 | 
			
		||||
@@ -2015,6 +2020,7 @@
 | 
			
		||||
        "platform": 1,
 | 
			
		||||
        "name": "test1-spine2",
 | 
			
		||||
        "serial": "45649818158",
 | 
			
		||||
        "site": 1,
 | 
			
		||||
        "rack": 2,
 | 
			
		||||
        "position": 33,
 | 
			
		||||
        "face": 0,
 | 
			
		||||
@@ -2035,6 +2041,7 @@
 | 
			
		||||
        "platform": 1,
 | 
			
		||||
        "name": "test1-edge2",
 | 
			
		||||
        "serial": "7567356345",
 | 
			
		||||
        "site": 1,
 | 
			
		||||
        "rack": 2,
 | 
			
		||||
        "position": 1,
 | 
			
		||||
        "face": 0,
 | 
			
		||||
@@ -2055,6 +2062,7 @@
 | 
			
		||||
        "platform": 1,
 | 
			
		||||
        "name": "test1-core2",
 | 
			
		||||
        "serial": "67856734534",
 | 
			
		||||
        "site": 1,
 | 
			
		||||
        "rack": 2,
 | 
			
		||||
        "position": 17,
 | 
			
		||||
        "face": 0,
 | 
			
		||||
@@ -2075,6 +2083,7 @@
 | 
			
		||||
        "platform": 2,
 | 
			
		||||
        "name": "test1-oob1",
 | 
			
		||||
        "serial": "98273942938",
 | 
			
		||||
        "site": 1,
 | 
			
		||||
        "rack": 1,
 | 
			
		||||
        "position": 42,
 | 
			
		||||
        "face": 0,
 | 
			
		||||
@@ -2095,6 +2104,7 @@
 | 
			
		||||
        "platform": null,
 | 
			
		||||
        "name": "test1-pdu1",
 | 
			
		||||
        "serial": "",
 | 
			
		||||
        "site": 1,
 | 
			
		||||
        "rack": 1,
 | 
			
		||||
        "position": null,
 | 
			
		||||
        "face": null,
 | 
			
		||||
@@ -2115,6 +2125,7 @@
 | 
			
		||||
        "platform": null,
 | 
			
		||||
        "name": "test1-pdu2",
 | 
			
		||||
        "serial": "",
 | 
			
		||||
        "site": 1,
 | 
			
		||||
        "rack": 2,
 | 
			
		||||
        "position": null,
 | 
			
		||||
        "face": null,
 | 
			
		||||
 
 | 
			
		||||
@@ -445,7 +445,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
 | 
			
		||||
 | 
			
		||||
class DeviceForm(BootstrapMixin, CustomFieldForm):
 | 
			
		||||
    site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
 | 
			
		||||
    rack = forms.ModelChoiceField(queryset=Rack.objects.all(), widget=APISelect(
 | 
			
		||||
    rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, widget=APISelect(
 | 
			
		||||
        api_url='/api/dcim/racks/?site_id={{site}}',
 | 
			
		||||
        display_field='display_name',
 | 
			
		||||
        attrs={'filter-for': 'position'}
 | 
			
		||||
@@ -549,7 +549,7 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
 | 
			
		||||
        if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
 | 
			
		||||
            self.fields['site'].disabled = True
 | 
			
		||||
            self.fields['rack'].disabled = True
 | 
			
		||||
            self.initial['site'] = self.instance.parent_bay.device.rack.site_id
 | 
			
		||||
            self.initial['site'] = self.instance.parent_bay.device.site_id
 | 
			
		||||
            self.initial['rack'] = self.instance.parent_bay.device.rack_id
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -585,7 +585,7 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):
 | 
			
		||||
    site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
 | 
			
		||||
        'invalid_choice': 'Invalid site name.',
 | 
			
		||||
    })
 | 
			
		||||
    rack_name = forms.CharField()
 | 
			
		||||
    rack_name = forms.CharField(required=False)
 | 
			
		||||
    face = forms.CharField(required=False)
 | 
			
		||||
 | 
			
		||||
    class Meta(BaseDeviceFromCSVForm.Meta):
 | 
			
		||||
@@ -748,9 +748,13 @@ class ConsolePortCreateForm(BootstrapMixin, forms.Form):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ConsoleConnectionCSVForm(forms.Form):
 | 
			
		||||
    console_server = FlexibleModelChoiceField(queryset=Device.objects.filter(device_type__is_console_server=True),
 | 
			
		||||
                                              to_field_name='name',
 | 
			
		||||
                                              error_messages={'invalid_choice': 'Console server not found'})
 | 
			
		||||
    console_server = FlexibleModelChoiceField(
 | 
			
		||||
        queryset=Device.objects.filter(device_type__is_console_server=True),
 | 
			
		||||
        to_field_name='name',
 | 
			
		||||
        error_messages={
 | 
			
		||||
            'invalid_choice': 'Console server not found',
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    cs_port = forms.CharField()
 | 
			
		||||
    device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
 | 
			
		||||
                                      error_messages={'invalid_choice': 'Device not found'})
 | 
			
		||||
@@ -815,22 +819,49 @@ class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
 | 
			
		||||
    rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
 | 
			
		||||
                                  widget=forms.Select(attrs={'filter-for': 'console_server'}))
 | 
			
		||||
    console_server = forms.ModelChoiceField(queryset=Device.objects.all(), label='Console Server', required=False,
 | 
			
		||||
                                            widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_console_server=True',
 | 
			
		||||
                                                             display_field='display_name',
 | 
			
		||||
                                                             attrs={'filter-for': 'cs_port'}))
 | 
			
		||||
    livesearch = forms.CharField(required=False, label='Console Server', widget=Livesearch(
 | 
			
		||||
        query_key='q', query_url='dcim-api:device_list', field_to_update='console_server')
 | 
			
		||||
    site = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Site.objects.all(),
 | 
			
		||||
        widget=forms.HiddenInput(),
 | 
			
		||||
    )
 | 
			
		||||
    rack = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Rack.objects.all(),
 | 
			
		||||
        label='Rack',
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=forms.Select(
 | 
			
		||||
            attrs={'filter-for': 'console_server', 'nullable': 'true'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    console_server = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Device.objects.all(),
 | 
			
		||||
        label='Console Server',
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}&is_console_server=True',
 | 
			
		||||
            display_field='display_name',
 | 
			
		||||
            attrs={'filter-for': 'cs_port'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    livesearch = forms.CharField(
 | 
			
		||||
        required=False,
 | 
			
		||||
        label='Console Server',
 | 
			
		||||
        widget=Livesearch(
 | 
			
		||||
            query_key='q',
 | 
			
		||||
            query_url='dcim-api:device_list',
 | 
			
		||||
            field_to_update='console_server',
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    cs_port = forms.ModelChoiceField(
 | 
			
		||||
        queryset=ConsoleServerPort.objects.all(),
 | 
			
		||||
        label='Port',
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/devices/{{console_server}}/console-server-ports/',
 | 
			
		||||
            disabled_indicator='connected_console',
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    cs_port = forms.ModelChoiceField(queryset=ConsoleServerPort.objects.all(), label='Port',
 | 
			
		||||
                                     widget=APISelect(api_url='/api/dcim/devices/{{console_server}}/console-server-ports/',
 | 
			
		||||
                                                      disabled_indicator='connected_console'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = ConsolePort
 | 
			
		||||
        fields = ['rack', 'console_server', 'livesearch', 'cs_port', 'connection_status']
 | 
			
		||||
        fields = ['site', 'rack', 'console_server', 'livesearch', 'cs_port', 'connection_status']
 | 
			
		||||
        labels = {
 | 
			
		||||
            'cs_port': 'Port',
 | 
			
		||||
            'connection_status': 'Status',
 | 
			
		||||
@@ -843,17 +874,22 @@ class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
 | 
			
		||||
        if not self.instance.pk:
 | 
			
		||||
            raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.")
 | 
			
		||||
 | 
			
		||||
        self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.rack.site)
 | 
			
		||||
        self.initial['site'] = self.instance.device.site
 | 
			
		||||
        self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.site)
 | 
			
		||||
        self.fields['cs_port'].required = True
 | 
			
		||||
        self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
 | 
			
		||||
 | 
			
		||||
        # Initialize console server choices
 | 
			
		||||
        if self.is_bound and self.data.get('rack'):
 | 
			
		||||
            self.fields['console_server'].queryset = Device.objects.filter(rack=self.data['rack'], device_type__is_console_server=True)
 | 
			
		||||
            self.fields['console_server'].queryset = Device.objects.filter(rack=self.data['rack'],
 | 
			
		||||
                                                                           device_type__is_console_server=True)
 | 
			
		||||
        elif self.initial.get('rack'):
 | 
			
		||||
            self.fields['console_server'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_console_server=True)
 | 
			
		||||
            self.fields['console_server'].queryset = Device.objects.filter(rack=self.initial['rack'],
 | 
			
		||||
                                                                           device_type__is_console_server=True)
 | 
			
		||||
        else:
 | 
			
		||||
            self.fields['console_server'].choices = []
 | 
			
		||||
            self.fields['console_server'].queryset = Device.objects.filter(site=self.instance.device.site,
 | 
			
		||||
                                                                           rack__isnull=True,
 | 
			
		||||
                                                                           device_type__is_console_server=True)
 | 
			
		||||
 | 
			
		||||
        # Initialize CS port choices
 | 
			
		||||
        if self.is_bound:
 | 
			
		||||
@@ -883,22 +919,56 @@ class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
 | 
			
		||||
    rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
 | 
			
		||||
                                  widget=forms.Select(attrs={'filter-for': 'device'}))
 | 
			
		||||
    device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
 | 
			
		||||
                                    widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
 | 
			
		||||
                                                     display_field='display_name', attrs={'filter-for': 'port'}))
 | 
			
		||||
    livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
 | 
			
		||||
        query_key='q', query_url='dcim-api:device_list', field_to_update='device')
 | 
			
		||||
    site = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Site.objects.all(),
 | 
			
		||||
        widget=forms.HiddenInput(),
 | 
			
		||||
    )
 | 
			
		||||
    rack = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Rack.objects.all(),
 | 
			
		||||
        label='Rack',
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=forms.Select(
 | 
			
		||||
            attrs={'filter-for': 'device', 'nullable': 'true'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    device = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Device.objects.all(),
 | 
			
		||||
        label='Device',
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
 | 
			
		||||
            display_field='display_name',
 | 
			
		||||
            attrs={'filter-for': 'port'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    livesearch = forms.CharField(
 | 
			
		||||
        required=False,
 | 
			
		||||
        label='Device',
 | 
			
		||||
        widget=Livesearch(
 | 
			
		||||
            query_key='q',
 | 
			
		||||
            query_url='dcim-api:device_list',
 | 
			
		||||
            field_to_update='device'
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    port = forms.ModelChoiceField(
 | 
			
		||||
        queryset=ConsolePort.objects.all(),
 | 
			
		||||
        label='Port',
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/devices/{{device}}/console-ports/',
 | 
			
		||||
            disabled_indicator='cs_port'
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    connection_status = forms.BooleanField(
 | 
			
		||||
        required=False,
 | 
			
		||||
        initial=CONNECTION_STATUS_CONNECTED,
 | 
			
		||||
        label='Status',
 | 
			
		||||
        widget=forms.Select(
 | 
			
		||||
            choices=CONNECTION_STATUS_CHOICES
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    port = forms.ModelChoiceField(queryset=ConsolePort.objects.all(), label='Port',
 | 
			
		||||
                                  widget=APISelect(api_url='/api/dcim/devices/{{device}}/console-ports/',
 | 
			
		||||
                                                   disabled_indicator='cs_port'))
 | 
			
		||||
    connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status',
 | 
			
		||||
                                           widget=forms.Select(choices=CONNECTION_STATUS_CHOICES))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
 | 
			
		||||
        fields = ['site', 'rack', 'device', 'livesearch', 'port', 'connection_status']
 | 
			
		||||
        labels = {
 | 
			
		||||
            'connection_status': 'Status',
 | 
			
		||||
        }
 | 
			
		||||
@@ -907,7 +977,8 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
 | 
			
		||||
 | 
			
		||||
        super(ConsoleServerPortConnectionForm, self).__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        self.fields['rack'].queryset = Rack.objects.filter(site=consoleserverport.device.rack.site)
 | 
			
		||||
        self.initial['site'] = consoleserverport.device.site
 | 
			
		||||
        self.fields['rack'].queryset = Rack.objects.filter(site=consoleserverport.device.site)
 | 
			
		||||
 | 
			
		||||
        # Initialize device choices
 | 
			
		||||
        if self.is_bound and self.data.get('rack'):
 | 
			
		||||
@@ -915,7 +986,8 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
 | 
			
		||||
        elif self.initial.get('rack', None):
 | 
			
		||||
            self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
 | 
			
		||||
        else:
 | 
			
		||||
            self.fields['device'].choices = []
 | 
			
		||||
            self.fields['device'].queryset = Device.objects.filter(site=consoleserverport.device.site,
 | 
			
		||||
                                                                   rack__isnull=True)
 | 
			
		||||
 | 
			
		||||
        # Initialize port choices
 | 
			
		||||
        if self.is_bound:
 | 
			
		||||
@@ -945,8 +1017,13 @@ class PowerPortCreateForm(BootstrapMixin, forms.Form):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PowerConnectionCSVForm(forms.Form):
 | 
			
		||||
    pdu = FlexibleModelChoiceField(queryset=Device.objects.filter(device_type__is_pdu=True), to_field_name='name',
 | 
			
		||||
                                   error_messages={'invalid_choice': 'PDU not found.'})
 | 
			
		||||
    pdu = FlexibleModelChoiceField(
 | 
			
		||||
        queryset=Device.objects.filter(device_type__is_pdu=True),
 | 
			
		||||
        to_field_name='name',
 | 
			
		||||
        error_messages={
 | 
			
		||||
            'invalid_choice': 'PDU not found.',
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    power_outlet = forms.CharField()
 | 
			
		||||
    device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
 | 
			
		||||
                                      error_messages={'invalid_choice': 'Device not found'})
 | 
			
		||||
@@ -1012,21 +1089,46 @@ class PowerConnectionImportForm(BootstrapMixin, BulkImportForm):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
 | 
			
		||||
    rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
 | 
			
		||||
                                  widget=forms.Select(attrs={'filter-for': 'pdu'}))
 | 
			
		||||
    pdu = forms.ModelChoiceField(queryset=Device.objects.all(), label='PDU', required=False,
 | 
			
		||||
                                 widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_pdu=True',
 | 
			
		||||
                                                  display_field='display_name', attrs={'filter-for': 'power_outlet'}))
 | 
			
		||||
    livesearch = forms.CharField(required=False, label='PDU', widget=Livesearch(
 | 
			
		||||
        query_key='q', query_url='dcim-api:device_list', field_to_update='pdu')
 | 
			
		||||
    site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.HiddenInput())
 | 
			
		||||
    rack = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Rack.objects.all(),
 | 
			
		||||
        label='Rack',
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=forms.Select(
 | 
			
		||||
            attrs={'filter-for': 'pdu', 'nullable': 'true'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    pdu = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Device.objects.all(),
 | 
			
		||||
        label='PDU',
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}&is_pdu=True',
 | 
			
		||||
            display_field='display_name',
 | 
			
		||||
            attrs={'filter-for': 'power_outlet'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    livesearch = forms.CharField(
 | 
			
		||||
        required=False,
 | 
			
		||||
        label='PDU',
 | 
			
		||||
        widget=Livesearch(
 | 
			
		||||
            query_key='q',
 | 
			
		||||
            query_url='dcim-api:device_list',
 | 
			
		||||
            field_to_update='pdu'
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    power_outlet = forms.ModelChoiceField(
 | 
			
		||||
        queryset=PowerOutlet.objects.all(),
 | 
			
		||||
        label='Outlet',
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/devices/{{pdu}}/power-outlets/',
 | 
			
		||||
            disabled_indicator='connected_port'
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    power_outlet = forms.ModelChoiceField(queryset=PowerOutlet.objects.all(), label='Outlet',
 | 
			
		||||
                                          widget=APISelect(api_url='/api/dcim/devices/{{pdu}}/power-outlets/',
 | 
			
		||||
                                                           disabled_indicator='connected_port'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = PowerPort
 | 
			
		||||
        fields = ['rack', 'pdu', 'livesearch', 'power_outlet', 'connection_status']
 | 
			
		||||
        fields = ['site', 'rack', 'pdu', 'livesearch', 'power_outlet', 'connection_status']
 | 
			
		||||
        labels = {
 | 
			
		||||
            'power_outlet': 'Outlet',
 | 
			
		||||
            'connection_status': 'Status',
 | 
			
		||||
@@ -1039,17 +1141,22 @@ class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
 | 
			
		||||
        if not self.instance.pk:
 | 
			
		||||
            raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.")
 | 
			
		||||
 | 
			
		||||
        self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.rack.site)
 | 
			
		||||
        self.initial['site'] = self.instance.device.site
 | 
			
		||||
        self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.site)
 | 
			
		||||
        self.fields['power_outlet'].required = True
 | 
			
		||||
        self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
 | 
			
		||||
 | 
			
		||||
        # Initialize PDU choices
 | 
			
		||||
        if self.is_bound and self.data.get('rack'):
 | 
			
		||||
            self.fields['pdu'].queryset = Device.objects.filter(rack=self.data['rack'], device_type__is_pdu=True)
 | 
			
		||||
            self.fields['pdu'].queryset = Device.objects.filter(rack=self.data['rack'],
 | 
			
		||||
                                                                device_type__is_pdu=True)
 | 
			
		||||
        elif self.initial.get('rack', None):
 | 
			
		||||
            self.fields['pdu'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_pdu=True)
 | 
			
		||||
            self.fields['pdu'].queryset = Device.objects.filter(rack=self.initial['rack'],
 | 
			
		||||
                                                                device_type__is_pdu=True)
 | 
			
		||||
        else:
 | 
			
		||||
            self.fields['pdu'].choices = []
 | 
			
		||||
            self.fields['pdu'].queryset = Device.objects.filter(site=self.instance.device.site,
 | 
			
		||||
                                                                rack__isnull=True,
 | 
			
		||||
                                                                device_type__is_pdu=True)
 | 
			
		||||
 | 
			
		||||
        # Initialize power outlet choices
 | 
			
		||||
        if self.is_bound:
 | 
			
		||||
@@ -1079,22 +1186,56 @@ class PowerOutletCreateForm(BootstrapMixin, forms.Form):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
 | 
			
		||||
    rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
 | 
			
		||||
                                  widget=forms.Select(attrs={'filter-for': 'device'}))
 | 
			
		||||
    device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
 | 
			
		||||
                                    widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
 | 
			
		||||
                                                     display_field='display_name', attrs={'filter-for': 'port'}))
 | 
			
		||||
    livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
 | 
			
		||||
        query_key='q', query_url='dcim-api:device_list', field_to_update='device')
 | 
			
		||||
    site = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Site.objects.all(),
 | 
			
		||||
        widget=forms.HiddenInput()
 | 
			
		||||
    )
 | 
			
		||||
    rack = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Rack.objects.all(),
 | 
			
		||||
        label='Rack',
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=forms.Select(
 | 
			
		||||
            attrs={'filter-for': 'device', 'nullable': 'true'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    device = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Device.objects.all(),
 | 
			
		||||
        label='Device',
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
 | 
			
		||||
            display_field='display_name',
 | 
			
		||||
            attrs={'filter-for': 'port'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    livesearch = forms.CharField(
 | 
			
		||||
        required=False,
 | 
			
		||||
        label='Device',
 | 
			
		||||
        widget=Livesearch(
 | 
			
		||||
            query_key='q',
 | 
			
		||||
            query_url='dcim-api:device_list',
 | 
			
		||||
            field_to_update='device'
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    port = forms.ModelChoiceField(
 | 
			
		||||
        queryset=PowerPort.objects.all(),
 | 
			
		||||
        label='Port',
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/devices/{{device}}/power-ports/',
 | 
			
		||||
            disabled_indicator='power_outlet'
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    connection_status = forms.BooleanField(
 | 
			
		||||
        required=False,
 | 
			
		||||
        initial=CONNECTION_STATUS_CONNECTED,
 | 
			
		||||
        label='Status',
 | 
			
		||||
        widget=forms.Select(
 | 
			
		||||
            choices=CONNECTION_STATUS_CHOICES
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    port = forms.ModelChoiceField(queryset=PowerPort.objects.all(), label='Port',
 | 
			
		||||
                                  widget=APISelect(api_url='/api/dcim/devices/{{device}}/power-ports/',
 | 
			
		||||
                                                   disabled_indicator='power_outlet'))
 | 
			
		||||
    connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status',
 | 
			
		||||
                                           widget=forms.Select(choices=CONNECTION_STATUS_CHOICES))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
 | 
			
		||||
        fields = ['site', 'rack', 'device', 'livesearch', 'port', 'connection_status']
 | 
			
		||||
        labels = {
 | 
			
		||||
            'connection_status': 'Status',
 | 
			
		||||
        }
 | 
			
		||||
@@ -1103,7 +1244,8 @@ class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
 | 
			
		||||
 | 
			
		||||
        super(PowerOutletConnectionForm, self).__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        self.fields['rack'].queryset = Rack.objects.filter(site=poweroutlet.device.rack.site)
 | 
			
		||||
        self.initial['site'] = poweroutlet.device.site
 | 
			
		||||
        self.fields['rack'].queryset = Rack.objects.filter(site=poweroutlet.device.site)
 | 
			
		||||
 | 
			
		||||
        # Initialize device choices
 | 
			
		||||
        if self.is_bound and self.data.get('rack'):
 | 
			
		||||
@@ -1111,7 +1253,8 @@ class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
 | 
			
		||||
        elif self.initial.get('rack', None):
 | 
			
		||||
            self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
 | 
			
		||||
        else:
 | 
			
		||||
            self.fields['device'].choices = []
 | 
			
		||||
            self.fields['device'].queryset = Device.objects.filter(site=poweroutlet.device.site,
 | 
			
		||||
                                                                   rack__isnull=True)
 | 
			
		||||
 | 
			
		||||
        # Initialize port choices
 | 
			
		||||
        if self.is_bound:
 | 
			
		||||
@@ -1158,22 +1301,55 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
 | 
			
		||||
    interface_a = forms.ChoiceField(choices=[], widget=SelectWithDisabled, label='Interface')
 | 
			
		||||
    site_b = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False,
 | 
			
		||||
                                    widget=forms.Select(attrs={'filter-for': 'rack_b'}))
 | 
			
		||||
    rack_b = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
 | 
			
		||||
                                    widget=APISelect(api_url='/api/dcim/racks/?site_id={{site_b}}',
 | 
			
		||||
                                                     attrs={'filter-for': 'device_b'}))
 | 
			
		||||
    device_b = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
 | 
			
		||||
                                      widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack_b}}',
 | 
			
		||||
                                                       display_field='display_name',
 | 
			
		||||
                                                       attrs={'filter-for': 'interface_b'}))
 | 
			
		||||
    livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
 | 
			
		||||
        query_key='q', query_url='dcim-api:device_list', field_to_update='device_b')
 | 
			
		||||
    interface_a = forms.ChoiceField(
 | 
			
		||||
        choices=[],
 | 
			
		||||
        widget=SelectWithDisabled,
 | 
			
		||||
        label='Interface'
 | 
			
		||||
    )
 | 
			
		||||
    site_b = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Site.objects.all(),
 | 
			
		||||
        label='Site',
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=forms.Select(
 | 
			
		||||
            attrs={'filter-for': 'rack_b'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    rack_b = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Rack.objects.all(),
 | 
			
		||||
        label='Rack',
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/racks/?site_id={{site_b}}',
 | 
			
		||||
            attrs={'filter-for': 'device_b', 'nullable': 'true'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    device_b = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Device.objects.all(),
 | 
			
		||||
        label='Device',
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/devices/?site_id={{site_b}}&rack_id={{rack_b}}',
 | 
			
		||||
            display_field='display_name',
 | 
			
		||||
            attrs={'filter-for': 'interface_b'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    livesearch = forms.CharField(
 | 
			
		||||
        required=False,
 | 
			
		||||
        label='Device',
 | 
			
		||||
        widget=Livesearch(
 | 
			
		||||
            query_key='q',
 | 
			
		||||
            query_url='dcim-api:device_list',
 | 
			
		||||
            field_to_update='device_b'
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    interface_b = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Interface.objects.all(),
 | 
			
		||||
        label='Interface',
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/devices/{{device_b}}/interfaces/?type=physical',
 | 
			
		||||
            disabled_indicator='is_connected'
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    interface_b = forms.ModelChoiceField(queryset=Interface.objects.all(), label='Interface',
 | 
			
		||||
                                         widget=APISelect(api_url='/api/dcim/devices/{{device_b}}/interfaces/?type=physical',
 | 
			
		||||
                                                          disabled_indicator='is_connected'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = InterfaceConnection
 | 
			
		||||
@@ -1198,11 +1374,15 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
 | 
			
		||||
        else:
 | 
			
		||||
            self.fields['rack_b'].choices = []
 | 
			
		||||
 | 
			
		||||
        # Initialize device_b choices if rack_b is set
 | 
			
		||||
        # Initialize device_b choices if rack_b or site_b is set
 | 
			
		||||
        if self.is_bound and self.data.get('rack_b'):
 | 
			
		||||
            self.fields['device_b'].queryset = Device.objects.filter(rack__pk=self.data['rack_b'])
 | 
			
		||||
        elif self.is_bound and self.data.get('site_b'):
 | 
			
		||||
            self.fields['device_b'].queryset = Device.objects.filter(site__pk=self.data['site_b'], rack__isnull=True)
 | 
			
		||||
        elif self.initial.get('rack_b'):
 | 
			
		||||
            self.fields['device_b'].queryset = Device.objects.filter(rack=self.initial['rack_b'])
 | 
			
		||||
        elif self.initial.get('site_b'):
 | 
			
		||||
            self.fields['device_b'].queryset = Device.objects.filter(site=self.initial['site_b'], rack__isnull=True)
 | 
			
		||||
        else:
 | 
			
		||||
            self.fields['device_b'].choices = []
 | 
			
		||||
 | 
			
		||||
@@ -1223,13 +1403,21 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InterfaceConnectionCSVForm(forms.Form):
 | 
			
		||||
    device_a = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
 | 
			
		||||
                                        error_messages={'invalid_choice': 'Device A not found.'})
 | 
			
		||||
    device_a = FlexibleModelChoiceField(
 | 
			
		||||
        queryset=Device.objects.all(),
 | 
			
		||||
        to_field_name='name',
 | 
			
		||||
        error_messages={'invalid_choice': 'Device A not found.'}
 | 
			
		||||
    )
 | 
			
		||||
    interface_a = forms.CharField()
 | 
			
		||||
    device_b = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
 | 
			
		||||
                                        error_messages={'invalid_choice': 'Device B not found.'})
 | 
			
		||||
    device_b = FlexibleModelChoiceField(
 | 
			
		||||
        queryset=Device.objects.all(),
 | 
			
		||||
        to_field_name='name',
 | 
			
		||||
        error_messages={'invalid_choice': 'Device B not found.'}
 | 
			
		||||
    )
 | 
			
		||||
    interface_b = forms.CharField()
 | 
			
		||||
    status = forms.CharField(validators=[validate_connection_status])
 | 
			
		||||
    status = forms.CharField(
 | 
			
		||||
        validators=[validate_connection_status]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								netbox/dcim/migrations/0027_device_add_site.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								netbox/dcim/migrations/0027_device_add_site.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
# Generated by Django 1.10.4 on 2017-02-16 21:21
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('dcim', '0026_add_rack_reservations'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='device',
 | 
			
		||||
            name='site',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										23
									
								
								netbox/dcim/migrations/0028_device_copy_rack_to_site.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								netbox/dcim/migrations/0028_device_copy_rack_to_site.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
# Generated by Django 1.10.4 on 2017-02-16 21:23
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def copy_site_from_rack(apps, schema_editor):
 | 
			
		||||
    Device = apps.get_model('dcim', 'Device')
 | 
			
		||||
    for device in Device.objects.all():
 | 
			
		||||
        device.site = device.rack.site
 | 
			
		||||
        device.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('dcim', '0027_device_add_site'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RunPython(copy_site_from_rack),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										26
									
								
								netbox/dcim/migrations/0029_allow_rackless_devices.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								netbox/dcim/migrations/0029_allow_rackless_devices.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
# Generated by Django 1.10.4 on 2017-02-16 21:25
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('dcim', '0028_device_copy_rack_to_site'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='device',
 | 
			
		||||
            name='rack',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Rack'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='device',
 | 
			
		||||
            name='site',
 | 
			
		||||
            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -370,6 +370,19 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
 | 
			
		||||
                        )
 | 
			
		||||
                    })
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
        # Record the original site assignment for this rack.
 | 
			
		||||
        _site_id = None
 | 
			
		||||
        if self.pk:
 | 
			
		||||
            _site_id = Rack.objects.get(pk=self.pk).site_id
 | 
			
		||||
 | 
			
		||||
        super(Rack, self).save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # Update racked devices if the assigned Site has been changed.
 | 
			
		||||
        if _site_id is not None and self.site_id != _site_id:
 | 
			
		||||
            Device.objects.filter(rack=self).update(site_id=self.site.pk)
 | 
			
		||||
 | 
			
		||||
    def to_csv(self):
 | 
			
		||||
        return csv_format([
 | 
			
		||||
            self.site.name,
 | 
			
		||||
@@ -871,7 +884,8 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
 | 
			
		||||
    serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
 | 
			
		||||
    asset_tag = NullableCharField(max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
 | 
			
		||||
                                  help_text='A unique tag used to identify this device')
 | 
			
		||||
    rack = models.ForeignKey('Rack', related_name='devices', on_delete=models.PROTECT)
 | 
			
		||||
    site = models.ForeignKey('Site', related_name='devices', on_delete=models.PROTECT)
 | 
			
		||||
    rack = models.ForeignKey('Rack', related_name='devices', blank=True, null=True, on_delete=models.PROTECT)
 | 
			
		||||
    position = models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(1)],
 | 
			
		||||
                                                verbose_name='Position (U)',
 | 
			
		||||
                                                help_text='The lowest-numbered unit occupied by the device')
 | 
			
		||||
@@ -898,41 +912,59 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
 | 
			
		||||
        # Validate site/rack combination
 | 
			
		||||
        if self.rack and self.site != self.rack.site:
 | 
			
		||||
            raise ValidationError({
 | 
			
		||||
                'rack': "Rack {} does not belong to site {}.".format(self.rack, self.site),
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
        if self.rack is None:
 | 
			
		||||
            if self.face is not None:
 | 
			
		||||
                raise ValidationError({
 | 
			
		||||
                    'face': "Cannot select a rack face without assigning a rack.",
 | 
			
		||||
                })
 | 
			
		||||
            if self.position:
 | 
			
		||||
                raise ValidationError({
 | 
			
		||||
                    'face': "Cannot select a rack position without assigning a rack.",
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
        # Validate position/face combination
 | 
			
		||||
        if self.position and self.face is None:
 | 
			
		||||
            raise ValidationError({
 | 
			
		||||
                'face': "Must specify rack face when defining rack position."
 | 
			
		||||
                'face': "Must specify rack face when defining rack position.",
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            # Child devices cannot be assigned to a rack face/unit
 | 
			
		||||
            if self.device_type.is_child_device and self.face is not None:
 | 
			
		||||
                raise ValidationError({
 | 
			
		||||
                    'face': "Child device types cannot be assigned to a rack face. This is an attribute of the parent "
 | 
			
		||||
                            "device."
 | 
			
		||||
                })
 | 
			
		||||
            if self.device_type.is_child_device and self.position:
 | 
			
		||||
                raise ValidationError({
 | 
			
		||||
                    'position': "Child device types cannot be assigned to a rack position. This is an attribute of the "
 | 
			
		||||
                                "parent device."
 | 
			
		||||
                })
 | 
			
		||||
        if self.rack:
 | 
			
		||||
 | 
			
		||||
            # Validate rack space
 | 
			
		||||
            rack_face = self.face if not self.device_type.is_full_depth else None
 | 
			
		||||
            exclude_list = [self.pk] if self.pk else []
 | 
			
		||||
            try:
 | 
			
		||||
                available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
 | 
			
		||||
                                                                exclude=exclude_list)
 | 
			
		||||
                if self.position and self.position not in available_units:
 | 
			
		||||
                # Child devices cannot be assigned to a rack face/unit
 | 
			
		||||
                if self.device_type.is_child_device and self.face is not None:
 | 
			
		||||
                    raise ValidationError({
 | 
			
		||||
                        'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) {} "
 | 
			
		||||
                                    "({}U).".format(self.position, self.device_type, self.device_type.u_height)
 | 
			
		||||
                        'face': "Child device types cannot be assigned to a rack face. This is an attribute of the parent "
 | 
			
		||||
                                "device."
 | 
			
		||||
                    })
 | 
			
		||||
                if self.device_type.is_child_device and self.position:
 | 
			
		||||
                    raise ValidationError({
 | 
			
		||||
                        'position': "Child device types cannot be assigned to a rack position. This is an attribute of the "
 | 
			
		||||
                                    "parent device."
 | 
			
		||||
                    })
 | 
			
		||||
            except Rack.DoesNotExist:
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
        except DeviceType.DoesNotExist:
 | 
			
		||||
            pass
 | 
			
		||||
                # Validate rack space
 | 
			
		||||
                rack_face = self.face if not self.device_type.is_full_depth else None
 | 
			
		||||
                exclude_list = [self.pk] if self.pk else []
 | 
			
		||||
                try:
 | 
			
		||||
                    available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
 | 
			
		||||
                                                                    exclude=exclude_list)
 | 
			
		||||
                    if self.position and self.position not in available_units:
 | 
			
		||||
                        raise ValidationError({
 | 
			
		||||
                            'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) {} "
 | 
			
		||||
                                        "({}U).".format(self.position, self.device_type, self.device_type.u_height)
 | 
			
		||||
                        })
 | 
			
		||||
                except Rack.DoesNotExist:
 | 
			
		||||
                    pass
 | 
			
		||||
 | 
			
		||||
            except DeviceType.DoesNotExist:
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
@@ -980,8 +1012,8 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
 | 
			
		||||
            self.platform.name if self.platform else None,
 | 
			
		||||
            self.serial,
 | 
			
		||||
            self.asset_tag,
 | 
			
		||||
            self.rack.site.name,
 | 
			
		||||
            self.rack.name,
 | 
			
		||||
            self.site.name,
 | 
			
		||||
            self.rack.name if self.rack else None,
 | 
			
		||||
            self.position,
 | 
			
		||||
            self.get_face_display(),
 | 
			
		||||
        ])
 | 
			
		||||
 
 | 
			
		||||
@@ -311,8 +311,7 @@ class DeviceTable(BaseTable):
 | 
			
		||||
    status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
 | 
			
		||||
    name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
 | 
			
		||||
    tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
 | 
			
		||||
    site = tables.LinkColumn('dcim:site', accessor=Accessor('rack.site'), args=[Accessor('rack.site.slug')],
 | 
			
		||||
                             verbose_name='Site')
 | 
			
		||||
    site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
 | 
			
		||||
    rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
 | 
			
		||||
    device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
 | 
			
		||||
    device_type = tables.LinkColumn('dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
 | 
			
		||||
@@ -328,8 +327,7 @@ class DeviceTable(BaseTable):
 | 
			
		||||
class DeviceImportTable(BaseTable):
 | 
			
		||||
    name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
 | 
			
		||||
    tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
 | 
			
		||||
    site = tables.LinkColumn('dcim:site', accessor=Accessor('rack.site'), args=[Accessor('rack.site.slug')],
 | 
			
		||||
                             verbose_name='Site')
 | 
			
		||||
    site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
 | 
			
		||||
    rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
 | 
			
		||||
    position = tables.Column(verbose_name='Position')
 | 
			
		||||
    device_role = tables.Column(verbose_name='Role')
 | 
			
		||||
 
 | 
			
		||||
@@ -346,6 +346,7 @@ class DeviceTest(APITestCase):
 | 
			
		||||
        'platform',
 | 
			
		||||
        'serial',
 | 
			
		||||
        'asset_tag',
 | 
			
		||||
        'site',
 | 
			
		||||
        'rack',
 | 
			
		||||
        'position',
 | 
			
		||||
        'face',
 | 
			
		||||
@@ -417,6 +418,9 @@ class DeviceTest(APITestCase):
 | 
			
		||||
            'primary_ip4_family',
 | 
			
		||||
            'primary_ip4_id',
 | 
			
		||||
            'primary_ip6',
 | 
			
		||||
            'site_id',
 | 
			
		||||
            'site_name',
 | 
			
		||||
            'site_slug',
 | 
			
		||||
            'rack_display_name',
 | 
			
		||||
            'rack_facility_id',
 | 
			
		||||
            'rack_id',
 | 
			
		||||
 
 | 
			
		||||
@@ -6,14 +6,14 @@ class RackTestCase(TestCase):
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
 | 
			
		||||
        site = Site.objects.create(
 | 
			
		||||
        self.site = Site.objects.create(
 | 
			
		||||
            name='TestSite1',
 | 
			
		||||
            slug='my-test-site'
 | 
			
		||||
        )
 | 
			
		||||
        self.rack = Rack.objects.create(
 | 
			
		||||
            name='TestRack1',
 | 
			
		||||
            facility_id='A101',
 | 
			
		||||
            site=site,
 | 
			
		||||
            site=self.site,
 | 
			
		||||
            u_height=42
 | 
			
		||||
        )
 | 
			
		||||
        self.manufacturer = Manufacturer.objects.create(
 | 
			
		||||
@@ -56,29 +56,29 @@ class RackTestCase(TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_mount_single_device(self):
 | 
			
		||||
 | 
			
		||||
        rack1 = Rack.objects.get(name='TestRack1')
 | 
			
		||||
        device1 = Device(
 | 
			
		||||
            name='TestSwitch1',
 | 
			
		||||
            device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'),
 | 
			
		||||
            device_role=DeviceRole.objects.get(slug='switch'),
 | 
			
		||||
            rack=rack1,
 | 
			
		||||
            site=self.site,
 | 
			
		||||
            rack=self.rack,
 | 
			
		||||
            position=10,
 | 
			
		||||
            face=RACK_FACE_REAR,
 | 
			
		||||
        )
 | 
			
		||||
        device1.save()
 | 
			
		||||
 | 
			
		||||
        # Validate rack height
 | 
			
		||||
        self.assertEqual(list(rack1.units), list(reversed(range(1, 43))))
 | 
			
		||||
        self.assertEqual(list(self.rack.units), list(reversed(range(1, 43))))
 | 
			
		||||
 | 
			
		||||
        # Validate inventory (front face)
 | 
			
		||||
        rack1_inventory_front = rack1.get_front_elevation()
 | 
			
		||||
        rack1_inventory_front = self.rack.get_front_elevation()
 | 
			
		||||
        self.assertEqual(rack1_inventory_front[-10]['device'], device1)
 | 
			
		||||
        del(rack1_inventory_front[-10])
 | 
			
		||||
        for u in rack1_inventory_front:
 | 
			
		||||
            self.assertIsNone(u['device'])
 | 
			
		||||
 | 
			
		||||
        # Validate inventory (rear face)
 | 
			
		||||
        rack1_inventory_rear = rack1.get_rear_elevation()
 | 
			
		||||
        rack1_inventory_rear = self.rack.get_rear_elevation()
 | 
			
		||||
        self.assertEqual(rack1_inventory_rear[-10]['device'], device1)
 | 
			
		||||
        del(rack1_inventory_rear[-10])
 | 
			
		||||
        for u in rack1_inventory_rear:
 | 
			
		||||
@@ -89,6 +89,7 @@ class RackTestCase(TestCase):
 | 
			
		||||
            name='TestPDU',
 | 
			
		||||
            device_role=self.role.get('PDU'),
 | 
			
		||||
            device_type=self.device_type.get('cc5000'),
 | 
			
		||||
            site=self.site,
 | 
			
		||||
            rack=self.rack,
 | 
			
		||||
            position=None,
 | 
			
		||||
            face=None,
 | 
			
		||||
 
 | 
			
		||||
@@ -627,7 +627,7 @@ class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
class DeviceListView(ObjectListView):
 | 
			
		||||
    queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'rack__site',
 | 
			
		||||
    queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack',
 | 
			
		||||
                                             'primary_ip4', 'primary_ip6')
 | 
			
		||||
    filter = filters.DeviceFilter
 | 
			
		||||
    filter_form = forms.DeviceFilterForm
 | 
			
		||||
@@ -1411,7 +1411,7 @@ def interfaceconnection_add(request, pk):
 | 
			
		||||
    else:
 | 
			
		||||
        form = forms.InterfaceConnectionForm(device, initial={
 | 
			
		||||
            'interface_a': request.GET.get('interface_a', None),
 | 
			
		||||
            'site_b': request.GET.get('site_b', device.rack.site),
 | 
			
		||||
            'site_b': request.GET.get('site_b', device.site),
 | 
			
		||||
            'rack_b': request.GET.get('rack_b', None),
 | 
			
		||||
            'device_b': request.GET.get('device_b', None),
 | 
			
		||||
            'interface_b': request.GET.get('interface_b', None),
 | 
			
		||||
 
 | 
			
		||||
@@ -307,10 +307,10 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
 | 
			
		||||
            nat_inside = self.instance.nat_inside
 | 
			
		||||
            # If the IP is assigned to an interface, populate site/device fields accordingly
 | 
			
		||||
            if self.instance.nat_inside.interface:
 | 
			
		||||
                self.initial['nat_site'] = self.instance.nat_inside.interface.device.rack.site.pk
 | 
			
		||||
                self.initial['nat_site'] = self.instance.nat_inside.interface.device.site.pk
 | 
			
		||||
                self.initial['nat_device'] = self.instance.nat_inside.interface.device.pk
 | 
			
		||||
                self.fields['nat_device'].queryset = Device.objects.filter(
 | 
			
		||||
                    rack__site=nat_inside.interface.device.rack.site)
 | 
			
		||||
                    rack__site=nat_inside.interface.device.site)
 | 
			
		||||
                self.fields['nat_inside'].queryset = IPAddress.objects.filter(
 | 
			
		||||
                    interface__device=nat_inside.interface.device)
 | 
			
		||||
            else:
 | 
			
		||||
@@ -346,20 +346,54 @@ class IPAddressBulkAddForm(BootstrapMixin, forms.Form):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IPAddressAssignForm(BootstrapMixin, forms.Form):
 | 
			
		||||
    site = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False,
 | 
			
		||||
                                  widget=forms.Select(attrs={'filter-for': 'rack'}))
 | 
			
		||||
    rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
 | 
			
		||||
                                  widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}',
 | 
			
		||||
                                                   display_field='display_name', attrs={'filter-for': 'device'}))
 | 
			
		||||
    device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
 | 
			
		||||
                                    widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
 | 
			
		||||
                                                     display_field='display_name', attrs={'filter-for': 'interface'}))
 | 
			
		||||
    livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
 | 
			
		||||
        query_key='q', query_url='dcim-api:device_list', field_to_update='device')
 | 
			
		||||
    site = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Site.objects.all(),
 | 
			
		||||
        label='Site',
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=forms.Select(
 | 
			
		||||
            attrs={'filter-for': 'rack'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    rack = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Rack.objects.all(),
 | 
			
		||||
        label='Rack',
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/racks/?site_id={{site}}',
 | 
			
		||||
            display_field='display_name',
 | 
			
		||||
            attrs={'filter-for': 'device', 'nullable': 'true'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    device = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Device.objects.all(),
 | 
			
		||||
        label='Device',
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
 | 
			
		||||
            display_field='display_name',
 | 
			
		||||
            attrs={'filter-for': 'interface'}
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    livesearch = forms.CharField(
 | 
			
		||||
        required=False,
 | 
			
		||||
        label='Device',
 | 
			
		||||
        widget=Livesearch(
 | 
			
		||||
            query_key='q',
 | 
			
		||||
            query_url='dcim-api:device_list',
 | 
			
		||||
            field_to_update='device'
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    interface = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Interface.objects.all(),
 | 
			
		||||
        label='Interface',
 | 
			
		||||
        widget=APISelect(
 | 
			
		||||
            api_url='/api/dcim/devices/{{device}}/interfaces/'
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    set_as_primary = forms.BooleanField(
 | 
			
		||||
        label='Set as primary IP for device',
 | 
			
		||||
        required=False
 | 
			
		||||
    )
 | 
			
		||||
    interface = forms.ModelChoiceField(queryset=Interface.objects.all(), label='Interface',
 | 
			
		||||
                                       widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/'))
 | 
			
		||||
    set_as_primary = forms.BooleanField(label='Set as primary IP for device', required=False)
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -559,6 +559,8 @@ def ipaddress_assign(request, pk):
 | 
			
		||||
                device.save()
 | 
			
		||||
 | 
			
		||||
            return redirect('ipam:ipaddress', pk=ipaddress.pk)
 | 
			
		||||
        else:
 | 
			
		||||
            assert False, form.errors
 | 
			
		||||
 | 
			
		||||
    else:
 | 
			
		||||
        form = forms.IPAddressAssignForm()
 | 
			
		||||
 
 | 
			
		||||
@@ -68,38 +68,38 @@ $(document).ready(function() {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // API select widget
 | 
			
		||||
    $('select[filter-for]').change(function () {
 | 
			
		||||
    $('select[filter-for]').change(function() {
 | 
			
		||||
 | 
			
		||||
        // Resolve child field by ID specified in parent
 | 
			
		||||
        var child_name = $(this).attr('filter-for');
 | 
			
		||||
        var child_field = $('#id_' + child_name);
 | 
			
		||||
        var child_selected = child_field.val();
 | 
			
		||||
 | 
			
		||||
        // Wipe out any existing options within the child field
 | 
			
		||||
        // Wipe out any existing options within the child field and create a default option
 | 
			
		||||
        child_field.empty();
 | 
			
		||||
        child_field.append($("<option></option>").attr("value", "").text(""));
 | 
			
		||||
 | 
			
		||||
        if ($(this).val()) {
 | 
			
		||||
        child_field.append($("<option></option>").attr("value", "").text("---------"));
 | 
			
		||||
 | 
			
		||||
        if ($(this).val() || $(this).attr('nullable') == 'true') {
 | 
			
		||||
            var api_url = child_field.attr('api-url');
 | 
			
		||||
            var disabled_indicator = child_field.attr('disabled-indicator');
 | 
			
		||||
            var initial_value = child_field.attr('initial');
 | 
			
		||||
            var display_field = child_field.attr('display-field') || 'name';
 | 
			
		||||
 | 
			
		||||
            // Gather the values of all other filter fields for this child
 | 
			
		||||
            $("select[filter-for='" + child_name + "']").each(function() {
 | 
			
		||||
                var filter_field = $(this);
 | 
			
		||||
            // Determine the filter fields needed to make an API call
 | 
			
		||||
            var filter_regex = /\{\{([a-z_]+)\}\}/g;
 | 
			
		||||
            var match;
 | 
			
		||||
            while (match = filter_regex.exec(api_url)) {
 | 
			
		||||
                var filter_field = $('#id_' + match[1]);
 | 
			
		||||
                if (filter_field.val()) {
 | 
			
		||||
                    api_url = api_url.replace('{{' + filter_field.attr('name') + '}}', filter_field.val());
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Not all filters have been selected yet
 | 
			
		||||
                    return false;
 | 
			
		||||
                    api_url = api_url.replace(match[0], filter_field.val());
 | 
			
		||||
                } else if ($(this).attr('nullable') == 'true') {
 | 
			
		||||
                    api_url = api_url.replace(match[0], '0');
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // If all URL variables have been replaced, make the API call
 | 
			
		||||
            if (api_url.search('{{') < 0) {
 | 
			
		||||
                console.log(child_name + ": Fetching " + api_url);
 | 
			
		||||
                $.ajax({
 | 
			
		||||
                    url: api_url,
 | 
			
		||||
                    dataType: 'json',
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,9 @@
 | 
			
		||||
{% block content %}
 | 
			
		||||
<form action="." method="post" class="form form-horizontal">
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    {% for field in form.hidden_fields %}
 | 
			
		||||
        {{ field }}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-md-6 col-md-offset-3">
 | 
			
		||||
            {% if form.non_field_errors %}
 | 
			
		||||
@@ -29,6 +32,12 @@
 | 
			
		||||
                            {% render_field form.livesearch %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="tab-pane" id="select">
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label class="col-md-3 control-label">Site</label>
 | 
			
		||||
                                <div class="col-md-9">
 | 
			
		||||
                                    <p class="form-control-static">{{ consoleport.device.site }}</p>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {% render_field form.rack %}
 | 
			
		||||
                            {% render_field form.console_server %}
 | 
			
		||||
                        </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,10 @@
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<form action="." method="post" class="form form-horizontal">
 | 
			
		||||
{% csrf_token %}
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    {% for field in form.hidden_fields %}
 | 
			
		||||
        {{ field }}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-md-6 col-md-offset-3">
 | 
			
		||||
            {% if form.non_field_errors %}
 | 
			
		||||
@@ -29,6 +32,12 @@
 | 
			
		||||
                            {% render_field form.livesearch %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="tab-pane" id="select">
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label class="col-md-3 control-label">Site</label>
 | 
			
		||||
                                <div class="col-md-9">
 | 
			
		||||
                                    <p class="form-control-static">{{ consoleserverport.device.site }}</p>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {% render_field form.rack %}
 | 
			
		||||
                            {% render_field form.device %}
 | 
			
		||||
                        </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -27,13 +27,17 @@
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>Site</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <a href="{% url 'dcim:site' slug=device.rack.site.slug %}">{{ device.rack.site }}</a>
 | 
			
		||||
                        <a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>Rack</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <span><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %}</span>
 | 
			
		||||
                        {% if device.rack %}
 | 
			
		||||
                            <span><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %}</span>
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            <span class="text-muted">None</span>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
@@ -44,9 +48,9 @@
 | 
			
		||||
                                <span>U{{ parent.position }} / {{ parent.get_face_display }}
 | 
			
		||||
                                (<a href="{{ parent.get_absolute_url }}">{{ parent }}</a> - {{ device.parent_bay.name }})</span>
 | 
			
		||||
                            {% endwith %}
 | 
			
		||||
                        {% elif device.position %}
 | 
			
		||||
                        {% elif device.rack and device.position %}
 | 
			
		||||
                            <span>U{{ device.position }} / {{ device.get_face_display }}</span>
 | 
			
		||||
                        {% elif device.device_type.u_height %}
 | 
			
		||||
                        {% elif device.rack and device.device_type.u_height %}
 | 
			
		||||
                            <span class="label label-warning">Not racked</span>
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            <span class="text-muted">N/A</span>
 | 
			
		||||
@@ -314,7 +318,11 @@
 | 
			
		||||
                                <a href="{% url 'dcim:device' pk=rd.pk %}">{{ rd }}</a>
 | 
			
		||||
                            </td>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                <a href="{% url 'dcim:rack' pk=rd.rack.pk %}">Rack {{ rd.rack }}</a>
 | 
			
		||||
                                {% if rd.rack %}
 | 
			
		||||
                                    <a href="{% url 'dcim:rack' pk=rd.rack.pk %}">Rack {{ rd.rack }}</a>
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                                    <span class="text-muted">—</span>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </td>
 | 
			
		||||
                            <td>{{ rd.device_type.full_name }}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,17 @@
 | 
			
		||||
<div class="row">
 | 
			
		||||
    <div class="col-sm-8 col-md-9">
 | 
			
		||||
    {% if device.rack %}
 | 
			
		||||
        <ol class="breadcrumb">
 | 
			
		||||
            <li><a href="{% url 'dcim:site' slug=device.rack.site.slug %}">{{ device.rack.site }}</a></li>
 | 
			
		||||
            <li><a href="{% url 'dcim:rack_list' %}?site={{ device.rack.site.slug }}">Racks</a></li>
 | 
			
		||||
    <ol class="breadcrumb">
 | 
			
		||||
        <li><a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a></li>
 | 
			
		||||
        {% if device.rack %}
 | 
			
		||||
            <li><a href="{% url 'dcim:rack_list' %}?site={{ device.site.slug }}">Racks</a></li>
 | 
			
		||||
            <li><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a></li>
 | 
			
		||||
            {% if device.parent_bay %}
 | 
			
		||||
                <li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
 | 
			
		||||
                <li>{{ device.parent_bay.name }}</li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            <li>{{ device }}</li>
 | 
			
		||||
        </ol>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% if device.parent_bay %}
 | 
			
		||||
            <li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
 | 
			
		||||
            <li>{{ device.parent_bay.name }}</li>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        <li>{{ device }}</li>
 | 
			
		||||
    </ol>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="col-sm-4 col-md-3">
 | 
			
		||||
        <form action="{% url 'dcim:device_list' %}" method="get">
 | 
			
		||||
 
 | 
			
		||||
@@ -7,88 +7,88 @@
 | 
			
		||||
{% block content %}
 | 
			
		||||
<h1>Connect Interfaces</h1>
 | 
			
		||||
<form action="." method="post" class="form form-horizontal">
 | 
			
		||||
{% csrf_token %}
 | 
			
		||||
<div class="row">
 | 
			
		||||
    <div class="col-md-6 col-md-offset-3">
 | 
			
		||||
        {% if form.non_field_errors %}
 | 
			
		||||
            <div class="panel panel-danger">
 | 
			
		||||
                <div class="panel-heading"><strong>Errors</strong></div>
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-md-6 col-md-offset-3">
 | 
			
		||||
            {% if form.non_field_errors %}
 | 
			
		||||
                <div class="panel panel-danger">
 | 
			
		||||
                    <div class="panel-heading"><strong>Errors</strong></div>
 | 
			
		||||
                    <div class="panel-body">
 | 
			
		||||
                        {{ form.non_field_errors }}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-md-5">
 | 
			
		||||
            <div class="panel panel-default">
 | 
			
		||||
                <div class="panel-heading text-center">
 | 
			
		||||
                    <strong>A Side</strong>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="panel-body">
 | 
			
		||||
                    {{ form.non_field_errors }}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="row">
 | 
			
		||||
	<div class="col-md-5">
 | 
			
		||||
        <div class="panel panel-default">
 | 
			
		||||
            <div class="panel-heading text-center">
 | 
			
		||||
                <strong>A Side</strong>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="panel-body">
 | 
			
		||||
                <div class="form-group">
 | 
			
		||||
                    <label class="col-md-3 control-label required">Site</label>
 | 
			
		||||
                    <div class="col-md-9">
 | 
			
		||||
                        <p class="form-control-static">{{ device.rack.site }}</p>
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label class="col-md-3 control-label required">Site</label>
 | 
			
		||||
                        <div class="col-md-9">
 | 
			
		||||
                            <p class="form-control-static">{{ device.site }}</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="form-group">
 | 
			
		||||
                    <label class="col-md-3 control-label required">Rack</label>
 | 
			
		||||
                    <div class="col-md-9">
 | 
			
		||||
                        <p class="form-control-static">{{ device.rack }}</p>
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label class="col-md-3 control-label required">Rack</label>
 | 
			
		||||
                        <div class="col-md-9">
 | 
			
		||||
                            <p class="form-control-static">{{ device.rack|default:"None" }}</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="form-group">
 | 
			
		||||
                    <label class="col-md-3 control-label required">Device</label>
 | 
			
		||||
                    <div class="col-md-9">
 | 
			
		||||
                        <p class="form-control-static">{{ device }}</p>
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label class="col-md-3 control-label required">Device</label>
 | 
			
		||||
                        <div class="col-md-9">
 | 
			
		||||
                            <p class="form-control-static">{{ device }}</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% render_field form.interface_a %}
 | 
			
		||||
                </div>
 | 
			
		||||
                {% render_field form.interface_a %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="col-md-2 text-center" style="padding-top: 90px;">
 | 
			
		||||
        <i class="fa fa-exchange fa-4x"></i>
 | 
			
		||||
    </div>
 | 
			
		||||
	<div class="col-md-5">
 | 
			
		||||
        <div class="panel panel-default">
 | 
			
		||||
            <div class="panel-heading text-center">
 | 
			
		||||
                <strong>B Side</strong>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="panel-body">
 | 
			
		||||
                <ul class="nav nav-tabs" role="tablist">
 | 
			
		||||
                    <li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
 | 
			
		||||
                    <li role="presentation"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
 | 
			
		||||
                </ul>
 | 
			
		||||
                <div class="tab-content">
 | 
			
		||||
                    <div class="tab-pane active" id="search">
 | 
			
		||||
                        {% render_field form.livesearch %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="tab-pane" id="select">
 | 
			
		||||
                        {% render_field form.site_b %}
 | 
			
		||||
                        {% render_field form.rack_b %}
 | 
			
		||||
                        {% render_field form.device_b %}
 | 
			
		||||
                    </div>
 | 
			
		||||
        <div class="col-md-2 text-center" style="padding-top: 90px;">
 | 
			
		||||
            <i class="fa fa-exchange fa-4x"></i>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-5">
 | 
			
		||||
            <div class="panel panel-default">
 | 
			
		||||
                <div class="panel-heading text-center">
 | 
			
		||||
                    <strong>B Side</strong>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="panel-body">
 | 
			
		||||
                    <ul class="nav nav-tabs" role="tablist">
 | 
			
		||||
                        <li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
 | 
			
		||||
                        <li role="presentation"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                    <div class="tab-content">
 | 
			
		||||
                        <div class="tab-pane active" id="search">
 | 
			
		||||
                            {% render_field form.livesearch %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="tab-pane" id="select">
 | 
			
		||||
                            {% render_field form.site_b %}
 | 
			
		||||
                            {% render_field form.rack_b %}
 | 
			
		||||
                            {% render_field form.device_b %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% render_field form.interface_b %}
 | 
			
		||||
                </div>
 | 
			
		||||
                {% render_field form.interface_b %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="row">
 | 
			
		||||
    <div class="col-md-4 col-md-offset-4">
 | 
			
		||||
        {% render_field form.connection_status %}
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="text-center">
 | 
			
		||||
    <div class="form-group">
 | 
			
		||||
        <button type="submit" name="_create" class="btn btn-primary">Connect</button>
 | 
			
		||||
        <button type="submit" name="_addanother" class="btn btn-primary">Connect and Add Another</button>
 | 
			
		||||
        <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-md-4 col-md-offset-4">
 | 
			
		||||
            {% render_field form.connection_status %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="text-center">
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
            <button type="submit" name="_create" class="btn btn-primary">Connect</button>
 | 
			
		||||
            <button type="submit" name="_addanother" class="btn btn-primary">Connect and Add Another</button>
 | 
			
		||||
            <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
</form>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,10 @@
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<form action="." method="post" class="form form-horizontal">
 | 
			
		||||
{% csrf_token %}
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    {% for field in form.hidden_fields %}
 | 
			
		||||
        {{ field }}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-md-6 col-md-offset-3">
 | 
			
		||||
            {% if form.non_field_errors %}
 | 
			
		||||
@@ -29,6 +32,12 @@
 | 
			
		||||
                            {% render_field form.livesearch %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="tab-pane" id="select">
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label class="col-md-3 control-label">Site</label>
 | 
			
		||||
                                <div class="col-md-9">
 | 
			
		||||
                                    <p class="form-control-static">{{ poweroutlet.device.site }}</p>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {% render_field form.rack %}
 | 
			
		||||
                            {% render_field form.device %}
 | 
			
		||||
                        </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,10 @@
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<form action="." method="post" class="form form-horizontal">
 | 
			
		||||
{% csrf_token %}
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    {% for field in form.hidden_fields %}
 | 
			
		||||
        {{ field }}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-md-6 col-md-offset-3">
 | 
			
		||||
            {% if form.non_field_errors %}
 | 
			
		||||
@@ -29,6 +32,12 @@
 | 
			
		||||
                            {% render_field form.livesearch %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="tab-pane" id="select">
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label class="col-md-3 control-label">Site</label>
 | 
			
		||||
                                <div class="col-md-9">
 | 
			
		||||
                                    <p class="form-control-static">{{ powerport.device.site }}</p>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {% render_field form.rack %}
 | 
			
		||||
                            {% render_field form.pdu %}
 | 
			
		||||
                        </div>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user