1
0
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:
Jeremy Stretch
2017-02-17 15:12:53 -05:00
25 changed files with 759 additions and 295 deletions

View File

@ -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

View File

@ -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', {

View File

@ -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',
]

View File

@ -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)',

View File

@ -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,

View File

@ -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):

View 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'),
),
]

View 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),
]

View 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'),
),
]

View File

@ -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,

View File

@ -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')

View File

@ -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',

View File

@ -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,

View File

@ -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),

View File

@ -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):

View File

@ -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()

View File

@ -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',

View File

@ -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">&mdash;</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">&mdash;</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">&mdash;</span>
{% endif %}
</td>
<td>
{% if c.description %}
{{ c.description }}
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr>

View File

@ -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>

View File

@ -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>

View File

@ -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">&mdash;</span>
{% endif %}
</td>
<td>{{ rd.device_type.full_name }}</td>
</tr>

View File

@ -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">

View File

@ -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 %}

View File

@ -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>

View File

@ -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>