mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into api2
Conflicts: netbox/dcim/api/serializers.py
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
|
||||
|
@ -31,7 +31,8 @@ class ProviderListView(ObjectListView):
|
||||
def provider(request, slug):
|
||||
|
||||
provider = get_object_or_404(Provider, slug=slug)
|
||||
circuits = Circuit.objects.filter(provider=provider)
|
||||
circuits = Circuit.objects.filter(provider=provider).select_related('type', 'tenant')\
|
||||
.prefetch_related('terminations__site')
|
||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
||||
|
||||
return render(request, 'circuits/provider.html', {
|
||||
|
@ -371,6 +371,7 @@ class DeviceSerializer(CustomFieldModelSerializer):
|
||||
device_role = NestedDeviceRoleSerializer()
|
||||
tenant = NestedTenantSerializer()
|
||||
platform = NestedPlatformSerializer()
|
||||
site = NestedSiteSerializer()
|
||||
rack = NestedRackSerializer()
|
||||
face = ChoiceFieldSerializer(choices=RACK_FACE_CHOICES)
|
||||
status = ChoiceFieldSerializer(choices=STATUS_CHOICES)
|
||||
@ -383,7 +384,7 @@ class DeviceSerializer(CustomFieldModelSerializer):
|
||||
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',
|
||||
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
||||
'comments', 'custom_fields',
|
||||
]
|
||||
|
||||
|
@ -233,12 +233,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)',
|
||||
@ -248,7 +248,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'}
|
||||
@ -482,7 +482,7 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
|
||||
if self.instance.pk:
|
||||
|
||||
# Initialize helper selections
|
||||
self.initial['site'] = self.instance.rack.site
|
||||
self.initial['site'] = self.instance.site
|
||||
self.initial['manufacturer'] = self.instance.device_type.manufacturer
|
||||
|
||||
# Compile list of choices for primary IPv4 and IPv6 addresses
|
||||
@ -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(),
|
||||
])
|
||||
@ -1110,8 +1142,8 @@ class PowerPort(models.Model):
|
||||
return self.name
|
||||
|
||||
# Used for connections export
|
||||
def csv_format(self):
|
||||
return ','.join([
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
self.power_outlet.device.identifier if self.power_outlet else None,
|
||||
self.power_outlet.name if self.power_outlet else None,
|
||||
self.device.identifier,
|
||||
|
@ -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')
|
||||
|
@ -331,6 +331,7 @@ class DeviceTest(APITestCase):
|
||||
'platform',
|
||||
'serial',
|
||||
'asset_tag',
|
||||
'site',
|
||||
'rack',
|
||||
'position',
|
||||
'face',
|
||||
@ -402,6 +403,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',
|
||||
|
@ -48,7 +48,7 @@
|
||||
<h1>{{ provider }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=provider %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Provider</strong>
|
||||
@ -104,6 +104,12 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Circuits</td>
|
||||
<td>
|
||||
<a href="{% url 'circuits:circuit_list' %}?provider={{ provider.slug }}">{{ provider.circuits.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% with provider.get_custom_fields as custom_fields %}
|
||||
@ -122,12 +128,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-8">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Circuits</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<tr>
|
||||
<th>Circuit ID</th>
|
||||
<th>Type</th>
|
||||
<th>Tenant</th>
|
||||
<th>A Side</th>
|
||||
<th>Z Side</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% for c in circuits %}
|
||||
<tr>
|
||||
<td>
|
||||
@ -136,6 +150,34 @@
|
||||
<td>
|
||||
<a href="{% url 'circuits:circuit_list' %}?type={{ c.type.slug }}">{{ c.type }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if c.tenant %}
|
||||
<a href="{% url 'tenants:tenant' slug=c.tenant.slug %}">{{ c.tenant }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if c.termination_a %}
|
||||
<a href="{% url 'dcim:site' slug=c.termination_a.site.slug %}">{{ c.termination_a.site }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if c.termination_z %}
|
||||
<a href="{% url 'dcim:site' slug=c.termination_z.site.slug %}">{{ c.termination_z.site }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if c.description %}
|
||||
{{ c.description }}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
|
@ -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>
|
||||
@ -236,38 +240,44 @@
|
||||
{% for iface in mgmt_interfaces %}
|
||||
{% include 'dcim/inc/interface.html' with icon='wrench' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="alert-warning">
|
||||
<i class="fa fa-fw fa-warning"></i> No management interfaces defined
|
||||
{% if perms.dcim.add_interface %}
|
||||
<a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if device.device_type.interface_templates.exists %}
|
||||
<tr>
|
||||
<td colspan="6" class="alert-warning">
|
||||
<i class="fa fa-fw fa-warning"></i> No management interfaces defined
|
||||
{% if perms.dcim.add_interface %}
|
||||
<a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for cp in console_ports %}
|
||||
{% include 'dcim/inc/consoleport.html' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="alert-warning">
|
||||
<i class="fa fa-fw fa-warning"></i> No console ports defined
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if device.device_type.console_port_templates.exists %}
|
||||
<tr>
|
||||
<td colspan="6" class="alert-warning">
|
||||
<i class="fa fa-fw fa-warning"></i> No console ports defined
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for pp in power_ports %}
|
||||
{% include 'dcim/inc/powerport.html' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="alert-warning">
|
||||
<i class="fa fa-fw fa-warning"></i> No power ports defined
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if device.device_type.power_port_templates.exists %}
|
||||
<tr>
|
||||
<td colspan="6" class="alert-warning">
|
||||
<i class="fa fa-fw fa-warning"></i> No power ports defined
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
|
||||
@ -314,7 +324,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