mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-27 02:48:38 -06:00
Merge https://github.com/digitalocean/netbox into HEAD
This commit is contained in:
commit
cdb51e42f3
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ configuration.py
|
|||||||
/*.sh
|
/*.sh
|
||||||
!upgrade.sh
|
!upgrade.sh
|
||||||
fabfile.py
|
fabfile.py
|
||||||
|
*.swp
|
||||||
|
@ -21,5 +21,10 @@ RUN apt-get update && apt-get install -y \
|
|||||||
&& apt-get purge -y --auto-remove git build-essential
|
&& apt-get purge -y --auto-remove git build-essential
|
||||||
|
|
||||||
ADD docker/docker-entrypoint.sh /docker-entrypoint.sh
|
ADD docker/docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
|
ADD netbox/netbox/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py
|
||||||
|
|
||||||
ENTRYPOINT [ "/docker-entrypoint.sh" ]
|
ENTRYPOINT [ "/docker-entrypoint.sh" ]
|
||||||
|
|
||||||
|
ADD docker/gunicorn_config.py /opt/netbox/
|
||||||
|
ADD docker/nginx.conf /etc/netbox-nginx/
|
||||||
|
VOLUME ["/etc/netbox-nginx/"]
|
||||||
|
@ -9,7 +9,7 @@ services:
|
|||||||
POSTGRES_PASSWORD: J5brHrAXFLQSif0K
|
POSTGRES_PASSWORD: J5brHrAXFLQSif0K
|
||||||
POSTGRES_DB: netbox
|
POSTGRES_DB: netbox
|
||||||
netbox:
|
netbox:
|
||||||
build: .
|
image: digitalocean/netbox
|
||||||
links:
|
links:
|
||||||
- postgres
|
- postgres
|
||||||
container_name: netbox
|
container_name: netbox
|
||||||
@ -34,20 +34,17 @@ services:
|
|||||||
NETBOX_USERNAME: guest
|
NETBOX_USERNAME: guest
|
||||||
NETBOX_PASSWORD: guest
|
NETBOX_PASSWORD: guest
|
||||||
volumes:
|
volumes:
|
||||||
- $PWD/netbox/netbox/configuration.docker.py:/opt/netbox/netbox/netbox/configuration.py:ro
|
|
||||||
- $PWD/docker/gunicorn_config.py:/opt/netbox/gunicorn_config.py:ro
|
|
||||||
- netbox-static-files:/opt/netbox/netbox/static
|
- netbox-static-files:/opt/netbox/netbox/static
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:1.11.1-alpine
|
image: nginx:1.11.1-alpine
|
||||||
links:
|
links:
|
||||||
- netbox
|
- netbox
|
||||||
container_name: nginx
|
container_name: nginx
|
||||||
|
command: nginx -g 'daemon off;' -c /etc/netbox-nginx/nginx.conf
|
||||||
depends_on:
|
depends_on:
|
||||||
- netbox
|
- netbox
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
volumes:
|
|
||||||
- $PWD/docker/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
||||||
volumes_from:
|
volumes_from:
|
||||||
- netbox
|
- netbox
|
||||||
volumes:
|
volumes:
|
||||||
|
10
docs/dcim.md
10
docs/dcim.md
@ -43,6 +43,7 @@ Each device type is assigned a number of component templates which describe the
|
|||||||
* Power port templates
|
* Power port templates
|
||||||
* Power outlet templates
|
* Power outlet templates
|
||||||
* Interface templates
|
* Interface templates
|
||||||
|
* Device bay templates
|
||||||
|
|
||||||
Whenever a new device is created, it is automatically assigned console, power, and interface components per the templates assigned to its device type. For example, suppose your network employs Juniper EX4300-48T switches. You would create a device type with a model name "EX4300-48T" and assign it to the manufacturer "Juniper." You might then also create the following templates for it:
|
Whenever a new device is created, it is automatically assigned console, power, and interface components per the templates assigned to its device type. For example, suppose your network employs Juniper EX4300-48T switches. You would create a device type with a model name "EX4300-48T" and assign it to the manufacturer "Juniper." You might then also create the following templates for it:
|
||||||
|
|
||||||
@ -81,16 +82,19 @@ A device can be assigned modules which represent internal components. Currently,
|
|||||||
|
|
||||||
### Components
|
### Components
|
||||||
|
|
||||||
There are five types of device components which comprise all of the interconnection logic with NetBox:
|
There are six types of device components which comprise all of the interconnection logic with NetBox:
|
||||||
|
|
||||||
* Console ports
|
* Console ports
|
||||||
* Console server ports
|
* Console server ports
|
||||||
* Power ports
|
* Power ports
|
||||||
* Power outlets
|
* Power outlets
|
||||||
* Interfaces
|
* Interfaces
|
||||||
|
* Device bays
|
||||||
|
|
||||||
Console ports connect only to console server ports, and power ports connect only to power outlets. Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. (The relationship between two interfaces is actually represented in the database by an InterfaceConnection object, but this is transparent to the user.)
|
Console ports connect only to console server ports, and power ports connect only to power outlets. Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. (The relationship between two interfaces is actually represented in the database by an InterfaceConnection object, but this is transparent to the user.)
|
||||||
|
|
||||||
Each type of connection can be defined as either *planned* or *connected*. This allows for easily denoting connections which have not yet been installed.
|
Each type of connection can be classified as either *planned* or *connected*. This allows for easily denoting connections which have not yet been installed. In addition to a connecting peer, interfaces are also assigned a form factor and may be designated as management-only (for out-of-band management). Interfaces may also be assigned a short description.
|
||||||
|
|
||||||
In addition to a connecting peer, interfaces are also assigned a form factor and may be designated as management-only (for out-of-band management). Interfaces may also be assigned a short description.
|
Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear on rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
|
||||||
|
|
||||||
|
Note that child devices differ from modules in that they are still treated as independent devices, with their own console/power/data components, modules, and IP addresses. Modules, on the other hand, are parts within a device, such as a hard disk or power supply.
|
||||||
|
@ -438,7 +438,52 @@ You should now have netbox running on a SSL protected connection.
|
|||||||
|
|
||||||
# Upgrading
|
# Upgrading
|
||||||
|
|
||||||
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by cloning the `master` branch of the git repository. Once the new code is in place, run the upgrade script (which may need to be run as root depending on how your environment is configured).
|
## Installation of Upgrade
|
||||||
|
|
||||||
|
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by cloning the `master` branch of the git repository.
|
||||||
|
|
||||||
|
### Option A: Download a Release
|
||||||
|
|
||||||
|
Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`. For this guide we are using 1.0.4 as the old version and 1.0.7 as the new version.
|
||||||
|
|
||||||
|
Download & extract latest version:
|
||||||
|
```
|
||||||
|
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
|
||||||
|
# tar -xzf vX.Y.Z.tar.gz -C /opt
|
||||||
|
# cd /opt/
|
||||||
|
# ln -sf netbox-1.0.7/ netbox
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the 'configuration.py' you created when first installing to the new version:
|
||||||
|
```
|
||||||
|
# cp /opt/netbox-1.0.4/configuration.py /opt/netbox/configuration.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option B: Clone the Git Repository (latest master release)
|
||||||
|
|
||||||
|
For this guide, we'll use `/opt/netbox`.
|
||||||
|
|
||||||
|
Check that your git branch is up to date & is set to master:
|
||||||
|
```
|
||||||
|
# cd /opt/netbox
|
||||||
|
# git status
|
||||||
|
```
|
||||||
|
|
||||||
|
If not on branch master, set it and verify status:
|
||||||
|
```
|
||||||
|
# git checkout master
|
||||||
|
# git status
|
||||||
|
```
|
||||||
|
|
||||||
|
Pull down the set branch from git status above:
|
||||||
|
```
|
||||||
|
# git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Upgrade Script & Netbox Restart
|
||||||
|
|
||||||
|
Once the new code is in place, run the upgrade script (which may need to be run as root depending on how your environment is configured).
|
||||||
|
|
||||||
```
|
```
|
||||||
# ./upgrade.sh
|
# ./upgrade.sh
|
||||||
|
4391
docs/schema.sql
4391
docs/schema.sql
File diff suppressed because it is too large
Load Diff
@ -20,6 +20,8 @@ Each secret is assigned a functional role which indicates what it is used for. T
|
|||||||
* IKE key strings
|
* IKE key strings
|
||||||
* Routing protocol shared secrets
|
* Routing protocol shared secrets
|
||||||
|
|
||||||
|
Roles are also used to control access to secrets. Each role is assigned an arbitrary number of groups and/or users. Only the users associated with a role have permission to decrypt the secrets assigned to that role. (A superuser has permission to decrypt all secrets, provided they have an active user key.)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# User Keys
|
# User Keys
|
||||||
|
@ -2,9 +2,9 @@ from django.contrib import admin
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
Interface, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
|
||||||
PowerPortTemplate, Rack, RackGroup, Site,
|
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -61,6 +61,10 @@ class InterfaceTemplateAdmin(admin.TabularInline):
|
|||||||
model = InterfaceTemplate
|
model = InterfaceTemplate
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBayTemplateAdmin(admin.TabularInline):
|
||||||
|
model = DeviceBayTemplate
|
||||||
|
|
||||||
|
|
||||||
@admin.register(DeviceType)
|
@admin.register(DeviceType)
|
||||||
class DeviceTypeAdmin(admin.ModelAdmin):
|
class DeviceTypeAdmin(admin.ModelAdmin):
|
||||||
prepopulated_fields = {
|
prepopulated_fields = {
|
||||||
@ -72,9 +76,10 @@ class DeviceTypeAdmin(admin.ModelAdmin):
|
|||||||
PowerPortTemplateAdmin,
|
PowerPortTemplateAdmin,
|
||||||
PowerOutletTemplateAdmin,
|
PowerOutletTemplateAdmin,
|
||||||
InterfaceTemplateAdmin,
|
InterfaceTemplateAdmin,
|
||||||
|
DeviceBayTemplateAdmin,
|
||||||
]
|
]
|
||||||
list_display = ['model', 'manufacturer', 'slug', 'u_height', 'console_ports', 'console_server_ports', 'power_ports',
|
list_display = ['model', 'manufacturer', 'slug', 'u_height', 'console_ports', 'console_server_ports', 'power_ports',
|
||||||
'power_outlets', 'interfaces']
|
'power_outlets', 'interfaces', 'device_bays']
|
||||||
list_filter = ['manufacturer']
|
list_filter = ['manufacturer']
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
@ -84,6 +89,7 @@ class DeviceTypeAdmin(admin.ModelAdmin):
|
|||||||
power_port_count=Count('power_port_templates', distinct=True),
|
power_port_count=Count('power_port_templates', distinct=True),
|
||||||
power_outlet_count=Count('power_outlet_templates', distinct=True),
|
power_outlet_count=Count('power_outlet_templates', distinct=True),
|
||||||
interface_count=Count('interface_templates', distinct=True),
|
interface_count=Count('interface_templates', distinct=True),
|
||||||
|
devicebay_count=Count('devicebay_templates', distinct=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
def console_ports(self, instance):
|
def console_ports(self, instance):
|
||||||
@ -101,6 +107,9 @@ class DeviceTypeAdmin(admin.ModelAdmin):
|
|||||||
def interfaces(self, instance):
|
def interfaces(self, instance):
|
||||||
return instance.interface_count
|
return instance.interface_count
|
||||||
|
|
||||||
|
def device_bays(self, instance):
|
||||||
|
return instance.devicebay_count
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Devices
|
# Devices
|
||||||
@ -144,6 +153,12 @@ class InterfaceAdmin(admin.TabularInline):
|
|||||||
model = Interface
|
model = Interface
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBayAdmin(admin.TabularInline):
|
||||||
|
model = DeviceBay
|
||||||
|
fk_name = 'device'
|
||||||
|
readonly_fields = ['installed_device']
|
||||||
|
|
||||||
|
|
||||||
class ModuleAdmin(admin.TabularInline):
|
class ModuleAdmin(admin.TabularInline):
|
||||||
model = Module
|
model = Module
|
||||||
readonly_fields = ['parent', 'discovered']
|
readonly_fields = ['parent', 'discovered']
|
||||||
@ -157,6 +172,7 @@ class DeviceAdmin(admin.ModelAdmin):
|
|||||||
PowerPortAdmin,
|
PowerPortAdmin,
|
||||||
PowerOutletAdmin,
|
PowerOutletAdmin,
|
||||||
InterfaceAdmin,
|
InterfaceAdmin,
|
||||||
|
DeviceBayAdmin,
|
||||||
ModuleAdmin,
|
ModuleAdmin,
|
||||||
]
|
]
|
||||||
list_display = ['display_name', 'device_type', 'device_role', 'primary_ip', 'rack', 'position', 'serial']
|
list_display = ['display_name', 'device_type', 'device_role', 'primary_ip', 'rack', 'position', 'serial']
|
||||||
|
@ -2,9 +2,9 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceType, DeviceRole,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
|
||||||
Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate,
|
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
|
||||||
PowerPort, PowerPortTemplate, Rack, RackGroup, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
|
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -221,16 +221,31 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
platform = PlatformNestedSerializer()
|
platform = PlatformNestedSerializer()
|
||||||
rack = RackNestedSerializer()
|
rack = RackNestedSerializer()
|
||||||
primary_ip = DeviceIPAddressNestedSerializer()
|
primary_ip = DeviceIPAddressNestedSerializer()
|
||||||
|
parent_device = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'platform', 'serial', 'rack', 'position',
|
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'platform', 'serial', 'rack', 'position',
|
||||||
'face', 'status', 'primary_ip', 'comments']
|
'face', 'parent_device', 'status', 'primary_ip', 'comments']
|
||||||
|
|
||||||
|
def get_parent_device(self, obj):
|
||||||
|
try:
|
||||||
|
device_bay = obj.parent_bay
|
||||||
|
except DeviceBay.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
'id': device_bay.device.pk,
|
||||||
|
'name': device_bay.device.name,
|
||||||
|
'device_bay': {
|
||||||
|
'id': device_bay.pk,
|
||||||
|
'name': device_bay.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceNestedSerializer(DeviceSerializer):
|
class DeviceNestedSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta(DeviceSerializer.Meta):
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = ['id', 'name', 'display_name']
|
fields = ['id', 'name', 'display_name']
|
||||||
|
|
||||||
@ -319,7 +334,7 @@ class InterfaceSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['id', 'device', 'name', 'form_factor', 'mgmt_only', 'description', 'is_connected']
|
fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceNestedSerializer(InterfaceSerializer):
|
class InterfaceNestedSerializer(InterfaceSerializer):
|
||||||
@ -333,10 +348,36 @@ class InterfaceDetailSerializer(InterfaceSerializer):
|
|||||||
connected_interface = InterfaceSerializer(source='get_connected_interface')
|
connected_interface = InterfaceSerializer(source='get_connected_interface')
|
||||||
|
|
||||||
class Meta(InterfaceSerializer.Meta):
|
class Meta(InterfaceSerializer.Meta):
|
||||||
fields = ['id', 'device', 'name', 'form_factor', 'mgmt_only', 'description', 'is_connected',
|
fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected',
|
||||||
'connected_interface']
|
'connected_interface']
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device bays
|
||||||
|
#
|
||||||
|
|
||||||
|
class DeviceBaySerializer(serializers.ModelSerializer):
|
||||||
|
device = DeviceNestedSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DeviceBay
|
||||||
|
fields = ['id', 'device', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBayNestedSerializer(DeviceBaySerializer):
|
||||||
|
installed_device = DeviceNestedSerializer()
|
||||||
|
|
||||||
|
class Meta(DeviceBaySerializer.Meta):
|
||||||
|
fields = ['id', 'name', 'installed_device']
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBayDetailSerializer(DeviceBaySerializer):
|
||||||
|
installed_device = DeviceNestedSerializer()
|
||||||
|
|
||||||
|
class Meta(DeviceBaySerializer.Meta):
|
||||||
|
fields = ['id', 'device', 'name', 'installed_device']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interface connections
|
# Interface connections
|
||||||
#
|
#
|
||||||
|
@ -49,6 +49,7 @@ urlpatterns = [
|
|||||||
url(r'^devices/(?P<pk>\d+)/power-ports/$', PowerPortListView.as_view(), name='device_powerports'),
|
url(r'^devices/(?P<pk>\d+)/power-ports/$', PowerPortListView.as_view(), name='device_powerports'),
|
||||||
url(r'^devices/(?P<pk>\d+)/power-outlets/$', PowerOutletListView.as_view(), name='device_poweroutlets'),
|
url(r'^devices/(?P<pk>\d+)/power-outlets/$', PowerOutletListView.as_view(), name='device_poweroutlets'),
|
||||||
url(r'^devices/(?P<pk>\d+)/interfaces/$', InterfaceListView.as_view(), name='device_interfaces'),
|
url(r'^devices/(?P<pk>\d+)/interfaces/$', InterfaceListView.as_view(), name='device_interfaces'),
|
||||||
|
url(r'^devices/(?P<pk>\d+)/device-bays/$', DeviceBayListView.as_view(), name='device_devicebays'),
|
||||||
|
|
||||||
# Console ports
|
# Console ports
|
||||||
url(r'^console-ports/(?P<pk>\d+)/$', ConsolePortView.as_view(), name='consoleport'),
|
url(r'^console-ports/(?P<pk>\d+)/$', ConsolePortView.as_view(), name='consoleport'),
|
||||||
|
@ -9,8 +9,8 @@ from django.http import Http404
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface, InterfaceConnection,
|
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface,
|
||||||
Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site,
|
InterfaceConnection, Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site,
|
||||||
)
|
)
|
||||||
from dcim import filters
|
from dcim import filters
|
||||||
from .exceptions import MissingFilterException
|
from .exceptions import MissingFilterException
|
||||||
@ -326,6 +326,33 @@ class InterfaceConnectionView(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
queryset = InterfaceConnection.objects.all()
|
queryset = InterfaceConnection.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device bays
|
||||||
|
#
|
||||||
|
|
||||||
|
class DeviceBayListView(generics.ListAPIView):
|
||||||
|
"""
|
||||||
|
List device bays (by device)
|
||||||
|
"""
|
||||||
|
serializer_class = serializers.DeviceBayNestedSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
|
||||||
|
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||||
|
queryset = DeviceBay.objects.filter(device=device).select_related('installed_device')
|
||||||
|
|
||||||
|
# Filter by type (physical or virtual)
|
||||||
|
iface_type = self.request.query_params.get('type')
|
||||||
|
if iface_type == 'physical':
|
||||||
|
queryset = queryset.exclude(form_factor=IFACE_FF_VIRTUAL)
|
||||||
|
elif iface_type == 'virtual':
|
||||||
|
queryset = queryset.filter(form_factor=IFACE_FF_VIRTUAL)
|
||||||
|
elif iface_type is not None:
|
||||||
|
queryset = queryset.empty()
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Live queries
|
# Live queries
|
||||||
#
|
#
|
||||||
|
44
netbox/dcim/fields.py
Normal file
44
netbox/dcim/fields.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from netaddr import EUI, mac_unix_expanded
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from .formfields import MACAddressFormField
|
||||||
|
|
||||||
|
|
||||||
|
class mac_unix_expanded_uppercase(mac_unix_expanded):
|
||||||
|
word_fmt = '%.2X'
|
||||||
|
|
||||||
|
|
||||||
|
class MACAddressField(models.Field):
|
||||||
|
description = "PostgreSQL MAC Address field"
|
||||||
|
|
||||||
|
def python_type(self):
|
||||||
|
return EUI
|
||||||
|
|
||||||
|
def from_db_value(self, value, expression, connection, context):
|
||||||
|
return self.to_python(value)
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValidationError(e)
|
||||||
|
|
||||||
|
def db_type(self, connection):
|
||||||
|
return 'macaddr'
|
||||||
|
|
||||||
|
def get_prep_value(self, value):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
return str(self.to_python(value))
|
||||||
|
|
||||||
|
def form_class(self):
|
||||||
|
return MACAddressFormField
|
||||||
|
|
||||||
|
def formfield(self, **kwargs):
|
||||||
|
defaults = {'form_class': self.form_class()}
|
||||||
|
defaults.update(kwargs)
|
||||||
|
return super(MACAddressField, self).formfield(**defaults)
|
@ -3419,6 +3419,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"device": 3,
|
"device": 3,
|
||||||
"name": "em0",
|
"name": "em0",
|
||||||
|
"mac_address": "00-00-00-AA-BB-CC",
|
||||||
"form_factor": 800,
|
"form_factor": 800,
|
||||||
"mgmt_only": true,
|
"mgmt_only": true,
|
||||||
"description": ""
|
"description": ""
|
||||||
@ -3772,6 +3773,7 @@
|
|||||||
"device": 4,
|
"device": 4,
|
||||||
"name": "em0",
|
"name": "em0",
|
||||||
"form_factor": 1000,
|
"form_factor": 1000,
|
||||||
|
"mac_address": "ff-ee-dd-33-22-11",
|
||||||
"mgmt_only": true,
|
"mgmt_only": true,
|
||||||
"description": ""
|
"description": ""
|
||||||
}
|
}
|
||||||
@ -5686,6 +5688,7 @@
|
|||||||
"device": 9,
|
"device": 9,
|
||||||
"name": "eth0",
|
"name": "eth0",
|
||||||
"form_factor": 1000,
|
"form_factor": 1000,
|
||||||
|
"mac_address": "44-55-66-77-88-99",
|
||||||
"mgmt_only": true,
|
"mgmt_only": true,
|
||||||
"description": ""
|
"description": ""
|
||||||
}
|
}
|
||||||
|
26
netbox/dcim/formfields.py
Normal file
26
netbox/dcim/formfields.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from netaddr import EUI, AddrFormatError
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Form fields
|
||||||
|
#
|
||||||
|
|
||||||
|
class MACAddressFormField(forms.Field):
|
||||||
|
default_error_messages = {
|
||||||
|
'invalid': "Enter a valid MAC address.",
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(value, EUI):
|
||||||
|
return value
|
||||||
|
|
||||||
|
try:
|
||||||
|
return EUI(value, version=48)
|
||||||
|
except AddrFormatError:
|
||||||
|
raise ValidationError("Please specify a valid MAC address.")
|
@ -10,10 +10,10 @@ from utilities.forms import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate,
|
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
||||||
ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface, IFACE_FF_VIRTUAL,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||||
InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
Interface, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
|
||||||
PowerPortTemplate, Rack, RackGroup, Site, STATUS_CHOICES
|
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -216,7 +216,7 @@ class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
fields = ['manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||||
'is_network_device']
|
'is_network_device', 'subdevice_role']
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
|
class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
|
||||||
@ -283,6 +283,14 @@ class InterfaceTemplateForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['name_pattern', 'form_factor', 'mgmt_only']
|
fields = ['name_pattern', 'form_factor', 'mgmt_only']
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBayTemplateForm(forms.ModelForm, BootstrapMixin):
|
||||||
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DeviceBayTemplate
|
||||||
|
fields = ['name_pattern']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device roles
|
# Device roles
|
||||||
#
|
#
|
||||||
@ -917,7 +925,7 @@ class InterfaceForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['device', 'name', 'form_factor', 'mgmt_only', 'description']
|
fields = ['device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description']
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
}
|
}
|
||||||
@ -928,7 +936,7 @@ class InterfaceCreateForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['name_pattern', 'form_factor', 'mgmt_only', 'description']
|
fields = ['name_pattern', 'form_factor', 'mac_address', 'mgmt_only', 'description']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkCreateForm(InterfaceCreateForm, BootstrapMixin):
|
class InterfaceBulkCreateForm(InterfaceCreateForm, BootstrapMixin):
|
||||||
@ -1080,6 +1088,41 @@ class InterfaceConnectionDeletionForm(forms.Form, BootstrapMixin):
|
|||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
|
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device bays
|
||||||
|
#
|
||||||
|
|
||||||
|
class DeviceBayForm(forms.ModelForm, BootstrapMixin):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DeviceBay
|
||||||
|
fields = ['device', 'name']
|
||||||
|
widgets = {
|
||||||
|
'device': forms.HiddenInput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBayCreateForm(forms.Form, BootstrapMixin):
|
||||||
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
|
class PopulateDeviceBayForm(forms.Form, BootstrapMixin):
|
||||||
|
installed_device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Child Device',
|
||||||
|
help_text="Child devices must first be created within the rack occupied "
|
||||||
|
"by the parent device. Then they can be assigned to a bay.")
|
||||||
|
|
||||||
|
def __init__(self, device_bay, *args, **kwargs):
|
||||||
|
|
||||||
|
super(PopulateDeviceBayForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
children_queryset = Device.objects.filter(rack=device_bay.device.rack,
|
||||||
|
parent_bay__isnull=True,
|
||||||
|
device_type__u_height=0,
|
||||||
|
device_type__subdevice_role=SUBDEVICE_ROLE_CHILD)\
|
||||||
|
.exclude(pk=device_bay.device.pk)
|
||||||
|
self.fields['installed_device'].queryset = children_queryset
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Connections
|
# Connections
|
||||||
#
|
#
|
||||||
|
56
netbox/dcim/migrations/0004_auto_20160701_2049.py
Normal file
56
netbox/dcim/migrations/0004_auto_20160701_2049.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-07-01 20:49
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0003_auto_20160628_1721'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DeviceBay',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=50, verbose_name=b'Name')),
|
||||||
|
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bays', to='dcim.Device')),
|
||||||
|
('installed_device', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_bay', to='dcim.Device')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['device', 'name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DeviceBayTemplate',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=30)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['device_type', 'name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='subdevice_role',
|
||||||
|
field=models.NullBooleanField(choices=[(None, b'N/A'), (True, b'Parent'), (False, b'Child')], default=None, help_text=b'Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name=b'Parent/child status'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicebaytemplate',
|
||||||
|
name='device_type',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bay_templates', to='dcim.DeviceType'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='devicebaytemplate',
|
||||||
|
unique_together=set([('device_type', 'name')]),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='devicebay',
|
||||||
|
unique_together=set([('device', 'name')]),
|
||||||
|
),
|
||||||
|
]
|
26
netbox/dcim/migrations/0005_auto_20160706_1722.py
Normal file
26
netbox/dcim/migrations/0005_auto_20160706_1722.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-07-06 17:22
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import dcim.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0004_auto_20160701_2049'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='mac_address',
|
||||||
|
field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name=b'MAC Address'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='subdevice_role',
|
||||||
|
field=models.NullBooleanField(choices=[(None, b'None'), (True, b'Parent'), (False, b'Child')], default=None, help_text=b'Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name=b'Parent/child status'),
|
||||||
|
),
|
||||||
|
]
|
@ -4,12 +4,13 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q, ObjectDoesNotExist
|
from django.db.models import Count, Q, ObjectDoesNotExist
|
||||||
|
|
||||||
from extras.rpc import RPC_CLIENTS
|
from extras.rpc import RPC_CLIENTS
|
||||||
from utilities.fields import NullableCharField
|
from utilities.fields import NullableCharField
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
|
|
||||||
|
from .fields import MACAddressField
|
||||||
|
|
||||||
RACK_FACE_FRONT = 0
|
RACK_FACE_FRONT = 0
|
||||||
RACK_FACE_REAR = 1
|
RACK_FACE_REAR = 1
|
||||||
@ -18,6 +19,14 @@ RACK_FACE_CHOICES = [
|
|||||||
[RACK_FACE_REAR, 'Rear'],
|
[RACK_FACE_REAR, 'Rear'],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
SUBDEVICE_ROLE_PARENT = True
|
||||||
|
SUBDEVICE_ROLE_CHILD = False
|
||||||
|
SUBDEVICE_ROLE_CHOICES = (
|
||||||
|
(None, 'None'),
|
||||||
|
(SUBDEVICE_ROLE_PARENT, 'Parent'),
|
||||||
|
(SUBDEVICE_ROLE_CHILD, 'Child'),
|
||||||
|
)
|
||||||
|
|
||||||
COLOR_TEAL = 'teal'
|
COLOR_TEAL = 'teal'
|
||||||
COLOR_GREEN = 'green'
|
COLOR_GREEN = 'green'
|
||||||
COLOR_BLUE = 'blue'
|
COLOR_BLUE = 'blue'
|
||||||
@ -274,6 +283,7 @@ class Rack(CreatedUpdatedModel):
|
|||||||
# Add devices to rack units list
|
# Add devices to rack units list
|
||||||
if self.pk:
|
if self.pk:
|
||||||
for device in Device.objects.select_related('device_type__manufacturer', 'device_role')\
|
for device in Device.objects.select_related('device_type__manufacturer', 'device_role')\
|
||||||
|
.annotate(devicebay_count=Count('device_bays'))\
|
||||||
.exclude(pk=exclude)\
|
.exclude(pk=exclude)\
|
||||||
.filter(rack=self, position__gt=0)\
|
.filter(rack=self, position__gt=0)\
|
||||||
.filter(Q(face=face) | Q(device_type__is_full_depth=True)):
|
.filter(Q(face=face) | Q(device_type__is_full_depth=True)):
|
||||||
@ -380,6 +390,10 @@ class DeviceType(models.Model):
|
|||||||
help_text="This type of device has power outlets")
|
help_text="This type of device has power outlets")
|
||||||
is_network_device = models.BooleanField(default=True, verbose_name='Is a network device',
|
is_network_device = models.BooleanField(default=True, verbose_name='Is a network device',
|
||||||
help_text="This type of device has network interfaces")
|
help_text="This type of device has network interfaces")
|
||||||
|
subdevice_role = models.NullBooleanField(default=None, verbose_name='Parent/child status',
|
||||||
|
choices=SUBDEVICE_ROLE_CHOICES,
|
||||||
|
help_text="Parent devices house child devices in device bays. Select "
|
||||||
|
"\"None\" if this device type is neither a parent nor a child.")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['manufacturer', 'model']
|
ordering = ['manufacturer', 'model']
|
||||||
@ -389,11 +403,40 @@ class DeviceType(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return "{0} {1}".format(self.manufacturer, self.model)
|
return "{} {}".format(self.manufacturer, self.model)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:devicetype', args=[self.pk])
|
return reverse('dcim:devicetype', args=[self.pk])
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
if not self.is_console_server and self.cs_port_templates.count():
|
||||||
|
raise ValidationError("Must delete all console server port templates associated with this device before "
|
||||||
|
"declassifying it as a console server.")
|
||||||
|
|
||||||
|
if not self.is_pdu and self.power_outlet_templates.count():
|
||||||
|
raise ValidationError("Must delete all power outlet templates associated with this device before "
|
||||||
|
"declassifying it as a PDU.")
|
||||||
|
|
||||||
|
if not self.is_network_device and self.interface_templates.filter(mgmt_only=False).count():
|
||||||
|
raise ValidationError("Must delete all non-management-only interface templates associated with this device "
|
||||||
|
"before declassifying it as a network device.")
|
||||||
|
|
||||||
|
if self.subdevice_role != SUBDEVICE_ROLE_PARENT and self.device_bay_templates.count():
|
||||||
|
raise ValidationError("Must delete all device bay templates associated with this device before "
|
||||||
|
"declassifying it as a parent device.")
|
||||||
|
|
||||||
|
if self.u_height and self.subdevice_role == SUBDEVICE_ROLE_CHILD:
|
||||||
|
raise ValidationError("Child device types must be 0U.")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_parent_device(self):
|
||||||
|
return bool(self.subdevice_role)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_child_device(self):
|
||||||
|
return bool(self.subdevice_role is False)
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTemplate(models.Model):
|
class ConsolePortTemplate(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -481,6 +524,21 @@ class InterfaceTemplate(models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBayTemplate(models.Model):
|
||||||
|
"""
|
||||||
|
A template for a DeviceBay to be created for a new parent Device.
|
||||||
|
"""
|
||||||
|
device_type = models.ForeignKey('DeviceType', related_name='device_bay_templates', on_delete=models.CASCADE)
|
||||||
|
name = models.CharField(max_length=30)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['device_type', 'name']
|
||||||
|
unique_together = ['device_type', 'name']
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
@ -563,6 +621,10 @@ class Device(CreatedUpdatedModel):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
|
# Child devices cannot be assigned to a rack face/unit
|
||||||
|
if self.device_type.is_child_device and (self.face is not None or self.position):
|
||||||
|
raise ValidationError("Child device types cannot be assigned a rack face or position.")
|
||||||
|
|
||||||
# Validate position/face combination
|
# Validate position/face combination
|
||||||
if self.position and self.face is None:
|
if self.position and self.face is None:
|
||||||
raise ValidationError("Must specify rack face with rack position.")
|
raise ValidationError("Must specify rack face with rack position.")
|
||||||
@ -610,6 +672,10 @@ class Device(CreatedUpdatedModel):
|
|||||||
[Interface(device=self, name=template.name, form_factor=template.form_factor,
|
[Interface(device=self, name=template.name, form_factor=template.form_factor,
|
||||||
mgmt_only=template.mgmt_only) for template in self.device_type.interface_templates.all()]
|
mgmt_only=template.mgmt_only) for template in self.device_type.interface_templates.all()]
|
||||||
)
|
)
|
||||||
|
DeviceBay.objects.bulk_create(
|
||||||
|
[DeviceBay(device=self, name=template.name) for template in
|
||||||
|
self.device_type.device_bay_templates.all()]
|
||||||
|
)
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return ','.join([
|
return ','.join([
|
||||||
@ -643,6 +709,12 @@ class Device(CreatedUpdatedModel):
|
|||||||
return self.name
|
return self.name
|
||||||
return '{{{}}}'.format(self.pk)
|
return '{{{}}}'.format(self.pk)
|
||||||
|
|
||||||
|
def get_children(self):
|
||||||
|
"""
|
||||||
|
Return the set of child Devices installed in DeviceBays within this Device.
|
||||||
|
"""
|
||||||
|
return Device.objects.filter(parent_bay__device=self.pk)
|
||||||
|
|
||||||
def get_rpc_client(self):
|
def get_rpc_client(self):
|
||||||
"""
|
"""
|
||||||
Return the appropriate RPC (e.g. NETCONF, ssh, etc.) client for this device's platform, if one is defined.
|
Return the appropriate RPC (e.g. NETCONF, ssh, etc.) client for this device's platform, if one is defined.
|
||||||
@ -785,6 +857,7 @@ class Interface(models.Model):
|
|||||||
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
|
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
|
||||||
name = models.CharField(max_length=30)
|
name = models.CharField(max_length=30)
|
||||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS)
|
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS)
|
||||||
|
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
|
||||||
mgmt_only = models.BooleanField(default=False, verbose_name='OOB Management',
|
mgmt_only = models.BooleanField(default=False, verbose_name='OOB Management',
|
||||||
help_text="This interface is used only for out-of-band management")
|
help_text="This interface is used only for out-of-band management")
|
||||||
description = models.CharField(max_length=100, blank=True)
|
description = models.CharField(max_length=100, blank=True)
|
||||||
@ -860,6 +933,33 @@ class InterfaceConnection(models.Model):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBay(models.Model):
|
||||||
|
"""
|
||||||
|
An empty space within a Device which can house a child device
|
||||||
|
"""
|
||||||
|
device = models.ForeignKey('Device', related_name='device_bays', on_delete=models.CASCADE)
|
||||||
|
name = models.CharField(max_length=50, verbose_name='Name')
|
||||||
|
installed_device = models.OneToOneField('Device', related_name='parent_bay', blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['device', 'name']
|
||||||
|
unique_together = ['device', 'name']
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return '{} - {}'.format(self.device.name, self.name)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Validate that the parent Device can have DeviceBays
|
||||||
|
if not self.device.device_type.is_parent_device:
|
||||||
|
raise ValidationError("This type of device ({}) does not support device bays."
|
||||||
|
.format(self.device.device_type))
|
||||||
|
|
||||||
|
# Cannot install a device into itself, obviously
|
||||||
|
if self.device == self.installed_device:
|
||||||
|
raise ValidationError("Cannot install a device into itself.")
|
||||||
|
|
||||||
|
|
||||||
class Module(models.Model):
|
class Module(models.Model):
|
||||||
"""
|
"""
|
||||||
A Module represents a piece of hardware within a Device, such as a line card or power supply. Modules are used only
|
A Module represents a piece of hardware within a Device, such as a line card or power supply. Modules are used only
|
||||||
|
@ -4,8 +4,9 @@ from django_tables2.utils import Accessor
|
|||||||
from utilities.tables import BaseTable, ToggleColumn
|
from utilities.tables import BaseTable, ToggleColumn
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, InterfaceTemplate,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
||||||
Interface, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site,
|
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||||
|
RackGroup, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -201,6 +202,19 @@ class InterfaceTemplateTable(tables.Table):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBayTemplateTable(tables.Table):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DeviceBayTemplate
|
||||||
|
fields = ('pk', 'name')
|
||||||
|
empty_text = "None"
|
||||||
|
show_header = False
|
||||||
|
attrs = {
|
||||||
|
'class': 'table table-hover panel-body',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device roles
|
# Device roles
|
||||||
#
|
#
|
||||||
|
@ -315,6 +315,7 @@ class DeviceTest(APITestCase):
|
|||||||
'rack',
|
'rack',
|
||||||
'position',
|
'position',
|
||||||
'face',
|
'face',
|
||||||
|
'parent_device',
|
||||||
'status',
|
'status',
|
||||||
'primary_ip',
|
'primary_ip',
|
||||||
'comments',
|
'comments',
|
||||||
@ -366,6 +367,7 @@ class DeviceTest(APITestCase):
|
|||||||
'face',
|
'face',
|
||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
|
'parent_device',
|
||||||
'platform_id',
|
'platform_id',
|
||||||
'platform_name',
|
'platform_name',
|
||||||
'platform_slug',
|
'platform_slug',
|
||||||
@ -527,6 +529,7 @@ class InterfaceTest(APITestCase):
|
|||||||
'device',
|
'device',
|
||||||
'name',
|
'name',
|
||||||
'form_factor',
|
'form_factor',
|
||||||
|
'mac_address',
|
||||||
'mgmt_only',
|
'mgmt_only',
|
||||||
'description',
|
'description',
|
||||||
'is_connected'
|
'is_connected'
|
||||||
@ -539,6 +542,7 @@ class InterfaceTest(APITestCase):
|
|||||||
'device',
|
'device',
|
||||||
'name',
|
'name',
|
||||||
'form_factor',
|
'form_factor',
|
||||||
|
'mac_address',
|
||||||
'mgmt_only',
|
'mgmt_only',
|
||||||
'description',
|
'description',
|
||||||
'is_connected',
|
'is_connected',
|
||||||
|
@ -4,7 +4,8 @@ from secrets.views import secret_add
|
|||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePortTemplate, ConsoleServerPortTemplate, PowerPortTemplate, PowerOutletTemplate, InterfaceTemplate,
|
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, PowerPortTemplate, PowerOutletTemplate,
|
||||||
|
InterfaceTemplate,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -70,6 +71,10 @@ urlpatterns = [
|
|||||||
name='devicetype_add_interface'),
|
name='devicetype_add_interface'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.component_template_delete,
|
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.component_template_delete,
|
||||||
{'model': InterfaceTemplate}, name='devicetype_delete_interface'),
|
{'model': InterfaceTemplate}, name='devicetype_delete_interface'),
|
||||||
|
url(r'^device-types/(?P<pk>\d+)/device-bays/add/$', views.DeviceBayTemplateAddView.as_view(),
|
||||||
|
name='devicetype_add_devicebay'),
|
||||||
|
url(r'^device-types/(?P<pk>\d+)/device-bays/delete/$', views.component_template_delete,
|
||||||
|
{'model': DeviceBayTemplate}, name='devicetype_delete_devicebay'),
|
||||||
|
|
||||||
# Device roles
|
# Device roles
|
||||||
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||||
@ -125,6 +130,13 @@ urlpatterns = [
|
|||||||
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.poweroutlet_edit, name='poweroutlet_edit'),
|
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.poweroutlet_edit, name='poweroutlet_edit'),
|
||||||
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.poweroutlet_delete, name='poweroutlet_delete'),
|
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.poweroutlet_delete, name='poweroutlet_delete'),
|
||||||
|
|
||||||
|
# Device bays
|
||||||
|
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.devicebay_add, name='devicebay_add'),
|
||||||
|
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.devicebay_edit, name='devicebay_edit'),
|
||||||
|
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.devicebay_delete, name='devicebay_delete'),
|
||||||
|
url(r'^device-bays/(?P<pk>\d+)/populate/$', views.devicebay_populate, name='devicebay_populate'),
|
||||||
|
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'),
|
||||||
|
|
||||||
# Console/power/interface connections
|
# Console/power/interface connections
|
||||||
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
|
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
|
||||||
url(r'^console-connections/import/$', views.ConsoleConnectionsBulkImportView.as_view(), name='console_connections_import'),
|
url(r'^console-connections/import/$', views.ConsoleConnectionsBulkImportView.as_view(), name='console_connections_import'),
|
||||||
|
@ -24,8 +24,9 @@ from utilities.views import (
|
|||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
from .models import (
|
from .models import (
|
||||||
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||||
DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform,
|
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
||||||
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site,
|
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||||
|
Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -153,7 +154,8 @@ def rack(request, pk):
|
|||||||
|
|
||||||
rack = get_object_or_404(Rack, pk=pk)
|
rack = get_object_or_404(Rack, pk=pk)
|
||||||
|
|
||||||
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True)
|
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True)\
|
||||||
|
.select_related('device_type__manufacturer')
|
||||||
next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
|
next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
|
||||||
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
|
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
|
||||||
|
|
||||||
@ -263,12 +265,14 @@ def devicetype(request, pk):
|
|||||||
powerport_table = tables.PowerPortTemplateTable(PowerPortTemplate.objects.filter(device_type=devicetype))
|
powerport_table = tables.PowerPortTemplateTable(PowerPortTemplate.objects.filter(device_type=devicetype))
|
||||||
poweroutlet_table = tables.PowerOutletTemplateTable(PowerOutletTemplate.objects.filter(device_type=devicetype))
|
poweroutlet_table = tables.PowerOutletTemplateTable(PowerOutletTemplate.objects.filter(device_type=devicetype))
|
||||||
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype))
|
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype))
|
||||||
|
devicebay_table = tables.DeviceBayTemplateTable(DeviceBayTemplate.objects.filter(device_type=devicetype))
|
||||||
if request.user.has_perm('dcim.change_devicetype'):
|
if request.user.has_perm('dcim.change_devicetype'):
|
||||||
consoleport_table.base_columns['pk'].visible = True
|
consoleport_table.base_columns['pk'].visible = True
|
||||||
consoleserverport_table.base_columns['pk'].visible = True
|
consoleserverport_table.base_columns['pk'].visible = True
|
||||||
powerport_table.base_columns['pk'].visible = True
|
powerport_table.base_columns['pk'].visible = True
|
||||||
poweroutlet_table.base_columns['pk'].visible = True
|
poweroutlet_table.base_columns['pk'].visible = True
|
||||||
interface_table.base_columns['pk'].visible = True
|
interface_table.base_columns['pk'].visible = True
|
||||||
|
devicebay_table.base_columns['pk'].visible = True
|
||||||
|
|
||||||
return render(request, 'dcim/devicetype.html', {
|
return render(request, 'dcim/devicetype.html', {
|
||||||
'devicetype': devicetype,
|
'devicetype': devicetype,
|
||||||
@ -277,6 +281,7 @@ def devicetype(request, pk):
|
|||||||
'powerport_table': powerport_table,
|
'powerport_table': powerport_table,
|
||||||
'poweroutlet_table': poweroutlet_table,
|
'poweroutlet_table': poweroutlet_table,
|
||||||
'interface_table': interface_table,
|
'interface_table': interface_table,
|
||||||
|
'devicebay_table': devicebay_table,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -395,6 +400,11 @@ class InterfaceTemplateAddView(ComponentTemplateCreateView):
|
|||||||
form = forms.InterfaceTemplateForm
|
form = forms.InterfaceTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBayTemplateAddView(ComponentTemplateCreateView):
|
||||||
|
model = DeviceBayTemplate
|
||||||
|
form = forms.DeviceBayTemplateForm
|
||||||
|
|
||||||
|
|
||||||
def component_template_delete(request, pk, model):
|
def component_template_delete(request, pk, model):
|
||||||
|
|
||||||
devicetype = get_object_or_404(DeviceType, pk=pk)
|
devicetype = get_object_or_404(DeviceType, pk=pk)
|
||||||
@ -421,7 +431,7 @@ def component_template_delete(request, pk, model):
|
|||||||
else:
|
else:
|
||||||
form = ComponentTemplateBulkDeleteForm(initial={'pk': request.POST.getlist('pk')})
|
form = ComponentTemplateBulkDeleteForm(initial={'pk': request.POST.getlist('pk')})
|
||||||
|
|
||||||
selected_objects = model.objects.filter(pk__in=form.initial.get('pk'))
|
selected_objects = model.objects.filter(pk__in=request.POST.getlist('pk'))
|
||||||
if not selected_objects:
|
if not selected_objects:
|
||||||
messages.warning(request, "No {} were selected for deletion.".format(model._meta.verbose_name_plural))
|
messages.warning(request, "No {} were selected for deletion.".format(model._meta.verbose_name_plural))
|
||||||
return redirect('dcim:devicetype', pk=devicetype.pk)
|
return redirect('dcim:devicetype', pk=devicetype.pk)
|
||||||
@ -510,6 +520,7 @@ def device(request, pk):
|
|||||||
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
||||||
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\
|
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\
|
||||||
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
||||||
|
device_bays = DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer')
|
||||||
|
|
||||||
# Gather any secrets which belong to this device
|
# Gather any secrets which belong to this device
|
||||||
secrets = device.secrets.all()
|
secrets = device.secrets.all()
|
||||||
@ -540,6 +551,7 @@ def device(request, pk):
|
|||||||
'power_outlets': power_outlets,
|
'power_outlets': power_outlets,
|
||||||
'interfaces': interfaces,
|
'interfaces': interfaces,
|
||||||
'mgmt_interfaces': mgmt_interfaces,
|
'mgmt_interfaces': mgmt_interfaces,
|
||||||
|
'device_bays': device_bays,
|
||||||
'ip_addresses': ip_addresses,
|
'ip_addresses': ip_addresses,
|
||||||
'secrets': secrets,
|
'secrets': secrets,
|
||||||
'related_devices': related_devices,
|
'related_devices': related_devices,
|
||||||
@ -550,7 +562,7 @@ class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'dcim.change_device'
|
permission_required = 'dcim.change_device'
|
||||||
model = Device
|
model = Device
|
||||||
form_class = forms.DeviceForm
|
form_class = forms.DeviceForm
|
||||||
fields_initial = ['site', 'rack', 'position', 'face']
|
fields_initial = ['site', 'rack', 'position', 'face', 'device_bay']
|
||||||
template_name = 'dcim/device_edit.html'
|
template_name = 'dcim/device_edit.html'
|
||||||
cancel_url = 'dcim:device_list'
|
cancel_url = 'dcim:device_list'
|
||||||
|
|
||||||
@ -1240,6 +1252,7 @@ def interface_add(request, pk):
|
|||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
'name': name,
|
'name': name,
|
||||||
'form_factor': form.cleaned_data['form_factor'],
|
'form_factor': form.cleaned_data['form_factor'],
|
||||||
|
'mac_address': form.cleaned_data['mac_address'],
|
||||||
'mgmt_only': form.cleaned_data['mgmt_only'],
|
'mgmt_only': form.cleaned_data['mgmt_only'],
|
||||||
'description': form.cleaned_data['description'],
|
'description': form.cleaned_data['description'],
|
||||||
})
|
})
|
||||||
@ -1327,6 +1340,7 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
|
|||||||
iface_form = forms.InterfaceForm({
|
iface_form = forms.InterfaceForm({
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
'name': name,
|
'name': name,
|
||||||
|
'mac_address': form.cleaned_data['mac_address'],
|
||||||
'form_factor': form.cleaned_data['form_factor'],
|
'form_factor': form.cleaned_data['form_factor'],
|
||||||
'mgmt_only': form.cleaned_data['mgmt_only'],
|
'mgmt_only': form.cleaned_data['mgmt_only'],
|
||||||
'description': form.cleaned_data['description'],
|
'description': form.cleaned_data['description'],
|
||||||
@ -1342,6 +1356,143 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
|
|||||||
len(selected_devices)))
|
len(selected_devices)))
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device bays
|
||||||
|
#
|
||||||
|
|
||||||
|
@permission_required('dcim.add_devicebay')
|
||||||
|
def devicebay_add(request, pk):
|
||||||
|
|
||||||
|
device = get_object_or_404(Device, pk=pk)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = forms.DeviceBayCreateForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
device_bays = []
|
||||||
|
for name in form.cleaned_data['name_pattern']:
|
||||||
|
devicebay_form = forms.DeviceBayForm({
|
||||||
|
'device': device.pk,
|
||||||
|
'name': name,
|
||||||
|
})
|
||||||
|
if devicebay_form.is_valid():
|
||||||
|
device_bays.append(devicebay_form.save(commit=False))
|
||||||
|
else:
|
||||||
|
for err in devicebay_form.errors.get('__all__', []):
|
||||||
|
form.add_error('name_pattern', err)
|
||||||
|
|
||||||
|
if not form.errors:
|
||||||
|
DeviceBay.objects.bulk_create(device_bays)
|
||||||
|
messages.success(request, "Added {} device bay(s) to {}".format(len(device_bays), device))
|
||||||
|
if '_addanother' in request.POST:
|
||||||
|
return redirect('dcim:devicebay_add', pk=device.pk)
|
||||||
|
else:
|
||||||
|
return redirect('dcim:device', pk=device.pk)
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = forms.DeviceBayCreateForm()
|
||||||
|
|
||||||
|
return render(request, 'dcim/devicebay_edit.html', {
|
||||||
|
'device': device,
|
||||||
|
'form': form,
|
||||||
|
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@permission_required('dcim.change_devicebay')
|
||||||
|
def devicebay_edit(request, pk):
|
||||||
|
|
||||||
|
devicebay = get_object_or_404(DeviceBay, pk=pk)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = forms.DeviceBayForm(request.POST, instance=devicebay)
|
||||||
|
if form.is_valid():
|
||||||
|
devicebay = form.save()
|
||||||
|
messages.success(request, "Modified {} bay {}".format(devicebay.device.name, devicebay.name))
|
||||||
|
return redirect('dcim:device', pk=devicebay.device.pk)
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = forms.DeviceBayForm(instance=devicebay)
|
||||||
|
|
||||||
|
return render(request, 'dcim/devicebay_edit.html', {
|
||||||
|
'devicebay': devicebay,
|
||||||
|
'form': form,
|
||||||
|
'cancel_url': reverse('dcim:device', kwargs={'pk': devicebay.device.pk}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@permission_required('dcim.delete_devicebay')
|
||||||
|
def devicebay_delete(request, pk):
|
||||||
|
|
||||||
|
devicebay = get_object_or_404(DeviceBay, pk=pk)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = ConfirmationForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
devicebay.delete()
|
||||||
|
messages.success(request, "Device bay {} has been deleted from {}".format(devicebay, devicebay.device))
|
||||||
|
return redirect('dcim:device', pk=devicebay.device.pk)
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = ConfirmationForm()
|
||||||
|
|
||||||
|
return render(request, 'dcim/devicebay_delete.html', {
|
||||||
|
'devicebay': devicebay,
|
||||||
|
'form': form,
|
||||||
|
'cancel_url': reverse('dcim:device', kwargs={'pk': devicebay.device.pk}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@permission_required('dcim.change_devicebay')
|
||||||
|
def devicebay_populate(request, pk):
|
||||||
|
|
||||||
|
device_bay = get_object_or_404(DeviceBay, pk=pk)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = forms.PopulateDeviceBayForm(device_bay, request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
device_bay.installed_device = form.cleaned_data['installed_device']
|
||||||
|
device_bay.save()
|
||||||
|
|
||||||
|
if not form.errors:
|
||||||
|
messages.success(request, "Added {} to {}".format(device_bay.installed_device, device_bay))
|
||||||
|
return redirect('dcim:device', pk=device_bay.device.pk)
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = forms.PopulateDeviceBayForm(device_bay)
|
||||||
|
|
||||||
|
return render(request, 'dcim/devicebay_populate.html', {
|
||||||
|
'device_bay': device_bay,
|
||||||
|
'form': form,
|
||||||
|
'cancel_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@permission_required('dcim.change_devicebay')
|
||||||
|
def devicebay_depopulate(request, pk):
|
||||||
|
|
||||||
|
device_bay = get_object_or_404(DeviceBay, pk=pk)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = ConfirmationForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
removed_device = device_bay.installed_device
|
||||||
|
device_bay.installed_device = None
|
||||||
|
device_bay.save()
|
||||||
|
messages.success(request, "{} has been removed from {}".format(removed_device, device_bay))
|
||||||
|
return redirect('dcim:device', pk=device_bay.device.pk)
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = ConfirmationForm()
|
||||||
|
|
||||||
|
return render(request, 'dcim/devicebay_depopulate.html', {
|
||||||
|
'device_bay': device_bay,
|
||||||
|
'form': form,
|
||||||
|
'cancel_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interface connections
|
# Interface connections
|
||||||
#
|
#
|
||||||
|
@ -16,7 +16,6 @@ def prefix_validator(prefix):
|
|||||||
|
|
||||||
|
|
||||||
class BaseIPField(models.Field):
|
class BaseIPField(models.Field):
|
||||||
default_validators = [prefix_validator]
|
|
||||||
|
|
||||||
def python_type(self):
|
def python_type(self):
|
||||||
return IPNetwork
|
return IPNetwork
|
||||||
@ -51,6 +50,7 @@ class IPNetworkField(BaseIPField):
|
|||||||
IP prefix (network and mask)
|
IP prefix (network and mask)
|
||||||
"""
|
"""
|
||||||
description = "PostgreSQL CIDR field"
|
description = "PostgreSQL CIDR field"
|
||||||
|
default_validators = [prefix_validator]
|
||||||
|
|
||||||
def db_type(self, connection):
|
def db_type(self, connection):
|
||||||
return 'cidr'
|
return 'cidr'
|
||||||
|
@ -11,7 +11,7 @@ except ImportError:
|
|||||||
"the documentation.")
|
"the documentation.")
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.0.8-dev'
|
VERSION = '1.1.1-dev'
|
||||||
|
|
||||||
# Import local configuration
|
# Import local configuration
|
||||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||||
|
@ -41,7 +41,7 @@ def home(request):
|
|||||||
|
|
||||||
return render(request, 'home.html', {
|
return render(request, 'home.html', {
|
||||||
'stats': stats,
|
'stats': stats,
|
||||||
'recent_activity': UserAction.objects.all()[:15]
|
'recent_activity': UserAction.objects.select_related('user')[:15]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ from django.shortcuts import get_object_or_404
|
|||||||
|
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -108,7 +109,8 @@ class SecretDetailView(generics.GenericAPIView):
|
|||||||
{'error': ERR_USERKEY_INACTIVE},
|
{'error': ERR_USERKEY_INACTIVE},
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
if secret.decryptable_by(request.user):
|
if not secret.decryptable_by(request.user):
|
||||||
|
raise PermissionDenied(detail="You do not have permission to decrypt this secret.")
|
||||||
master_key = uk.get_master_key(private_key)
|
master_key = uk.get_master_key(private_key)
|
||||||
if master_key is None:
|
if master_key is None:
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -182,6 +182,14 @@ class SecretRole(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return "{}?role={}".format(reverse('secrets:secret_list'), self.slug)
|
return "{}?role={}".format(reverse('secrets:secret_list'), self.slug)
|
||||||
|
|
||||||
|
def has_member(self, user):
|
||||||
|
"""
|
||||||
|
Check whether the given user has belongs to this SecretRole. Note that superusers belong to all roles.
|
||||||
|
"""
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
return user in self.users.all() or user.groups.filter(pk__in=self.groups.all()).exists()
|
||||||
|
|
||||||
|
|
||||||
class Secret(CreatedUpdatedModel):
|
class Secret(CreatedUpdatedModel):
|
||||||
"""
|
"""
|
||||||
@ -304,4 +312,4 @@ class Secret(CreatedUpdatedModel):
|
|||||||
"""
|
"""
|
||||||
Check whether the given user has permission to decrypt this Secret.
|
Check whether the given user has permission to decrypt this Secret.
|
||||||
"""
|
"""
|
||||||
return user in self.role.users.all() or user.groups.filter(pk__in=self.role.groups.all()).exists()
|
return self.role.has_member(user)
|
||||||
|
0
netbox/secrets/templatetags/__init__.py
Normal file
0
netbox/secrets/templatetags/__init__.py
Normal file
12
netbox/secrets/templatetags/secret_helpers.py
Normal file
12
netbox/secrets/templatetags/secret_helpers.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from django import template
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter()
|
||||||
|
def decryptable_by(secret, user):
|
||||||
|
"""
|
||||||
|
Determine whether a given User is permitted to decrypt a Secret.
|
||||||
|
"""
|
||||||
|
return secret.decryptable_by(user)
|
@ -31,7 +31,12 @@
|
|||||||
<li class="occupied h{{ u.device.device_type.u_height }}u{% ifequal u.device.face face_id %} {{ u.device.device_role.color }}{% endifequal %}">
|
<li class="occupied h{{ u.device.device_type.u_height }}u{% ifequal u.device.face face_id %} {{ u.device.device_role.color }}{% endifequal %}">
|
||||||
{% ifequal u.device.face face_id %}
|
{% ifequal u.device.face face_id %}
|
||||||
<a href="{% url 'dcim:device' pk=u.device.pk %}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true"
|
<a href="{% url 'dcim:device' pk=u.device.pk %}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true"
|
||||||
data-content="{{ u.device.device_role }}<br />{{ u.device.device_type }} ({{ u.device.device_type.u_height }}U)">{{ u.device.name|default:u.device.device_role }}</a>
|
data-content="{{ u.device.device_role }}<br />{{ u.device.device_type }} ({{ u.device.device_type.u_height }}U)">
|
||||||
|
{{ u.device.name|default:u.device.device_role }}
|
||||||
|
{% if u.device.devicebay_count %}
|
||||||
|
({{ u.device.get_children.count }}/{{ u.device.devicebay_count }})
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span>{{ u.device.name|default:u.device.device_role }}</span>
|
<span>{{ u.device.name|default:u.device.device_role }}</span>
|
||||||
{% endifequal %}
|
{% endifequal %}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{% extends 'utilities/confirmation_form.html' %}
|
{% extends 'utilities/confirmation_form.html' %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Delete devie type components?{% endblock %}
|
{% block title %}Delete device type components?{% endblock %}
|
||||||
|
|
||||||
{% block message %}
|
{% block message %}
|
||||||
<p>Are you sure you want to delete these components from <strong>{{ devicetype }}</strong>?</p>
|
<p>Are you sure you want to delete these components from <strong>{{ devicetype }}</strong>?</p>
|
||||||
|
@ -29,7 +29,12 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Position</td>
|
<td>Position</td>
|
||||||
<td>
|
<td>
|
||||||
{% if device.position %}
|
{% if device.parent_bay %}
|
||||||
|
{% with device.parent_bay.device as parent %}
|
||||||
|
<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 %}
|
||||||
<span>U{{ device.position }} / {{ device.get_face_display }}</span>
|
<span>U{{ device.position }} / {{ device.get_face_display }}</span>
|
||||||
{% elif device.device_type.u_height %}
|
{% elif device.device_type.u_height %}
|
||||||
<span class="label label-warning">Not racked</span>
|
<span class="label label-warning">Not racked</span>
|
||||||
@ -160,7 +165,7 @@
|
|||||||
<div class="panel-footer text-right">
|
<div class="panel-footer text-right">
|
||||||
<a href="{% url 'dcim:ipaddress_assign' pk=device.pk %}" class="btn btn-xs btn-primary">
|
<a href="{% url 'dcim:ipaddress_assign' pk=device.pk %}" class="btn btn-xs btn-primary">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||||
Assign IP Address
|
Assign IP address
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -174,7 +179,7 @@
|
|||||||
{% include 'dcim/inc/_interface.html' with icon='wrench' %}
|
{% include 'dcim/inc/_interface.html' with icon='wrench' %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="alert-warning">
|
<td colspan="5" class="alert-warning">
|
||||||
<i class="fa fa-fw fa-warning"></i> No management interfaces defined!
|
<i class="fa fa-fw fa-warning"></i> No management interfaces defined!
|
||||||
{% if perms.dcim.add_interface %}
|
{% 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>
|
<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>
|
||||||
@ -186,7 +191,7 @@
|
|||||||
{% include 'dcim/inc/_consoleport.html' %}
|
{% include 'dcim/inc/_consoleport.html' %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="alert-warning">
|
<td colspan="5" class="alert-warning">
|
||||||
<i class="fa fa-fw fa-warning"></i> No console ports defined!
|
<i class="fa fa-fw fa-warning"></i> No console ports defined!
|
||||||
{% if perms.dcim.add_consoleport %}
|
{% 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>
|
<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>
|
||||||
@ -199,7 +204,7 @@
|
|||||||
{% empty %}
|
{% empty %}
|
||||||
{% if not device.device_type.is_pdu %}
|
{% if not device.device_type.is_pdu %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="alert-warning">
|
<td colspan="5" class="alert-warning">
|
||||||
<i class="fa fa-fw fa-warning"></i> No power ports defined!
|
<i class="fa fa-fw fa-warning"></i> No power ports defined!
|
||||||
{% if perms.dcim.add_powerport %}
|
{% 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>
|
<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>
|
||||||
@ -268,12 +273,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
{% if device_bays or device.device_type.is_parent_device %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Device Bays</strong>
|
||||||
|
</div>
|
||||||
|
<table class="table table-hover panel-body">
|
||||||
|
{% for devicebay in device_bays %}
|
||||||
|
{% include 'dcim/inc/_devicebay.html' %}
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">No device bays defined</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% if perms.dcim.add_devicebay %}
|
||||||
|
<div class="panel-footer text-right">
|
||||||
|
<a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||||
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||||
|
Add device bays
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if interfaces or device.device_type.is_network_device %}
|
{% if interfaces or device.device_type.is_network_device %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
{% if perms.dcim.add_interface %}
|
|
||||||
<a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Interfaces</a>
|
|
||||||
{% endif %}
|
|
||||||
<strong>Interfaces</strong>
|
<strong>Interfaces</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
@ -285,14 +311,19 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
{% if perms.dcim.add_interface %}
|
||||||
|
<div class="panel-footer text-right">
|
||||||
|
<a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||||
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||||
|
Add interface
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if cs_ports or device.device_type.is_console_server %}
|
{% if cs_ports or device.device_type.is_console_server %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
{% if perms.dcim.add_consoleserverport %}
|
|
||||||
<a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Console Server Ports</a>
|
|
||||||
{% endif %}
|
|
||||||
<strong>Console Server Ports</strong>
|
<strong>Console Server Ports</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
@ -304,14 +335,19 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
{% if perms.dcim.add_consoleserverport %}
|
||||||
|
<div class="panel-footer text-right">
|
||||||
|
<a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||||
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||||
|
Add console server ports
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if power_outlets or device.device_type.is_pdu %}
|
{% if power_outlets or device.device_type.is_pdu %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
{% if perms.dcim.add_poweroutlet %}
|
|
||||||
<a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Power Outlets</a>
|
|
||||||
{% endif %}
|
|
||||||
<strong>Power Outlets</strong>
|
<strong>Power Outlets</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
@ -323,6 +359,14 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
{% if perms.dcim.add_poweroutlet %}
|
||||||
|
<div class="panel-footer text-right">
|
||||||
|
<a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||||
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||||
|
Add power outlets
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
8
netbox/templates/dcim/devicebay_delete.html
Normal file
8
netbox/templates/dcim/devicebay_delete.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{% extends 'utilities/confirmation_form.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block title %}Delete device bay {{ devicebay }}?{% endblock %}
|
||||||
|
|
||||||
|
{% block message %}
|
||||||
|
<p>Are you sure you want to delete this device bay from <strong>{{ devicebay.device }}</strong>?</p>
|
||||||
|
{% endblock %}
|
8
netbox/templates/dcim/devicebay_depopulate.html
Normal file
8
netbox/templates/dcim/devicebay_depopulate.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{% extends 'utilities/confirmation_form.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block title %}Remove {{ device_bay.installed_device }} from {{ device_bay }}?{% endblock %}
|
||||||
|
|
||||||
|
{% block message %}
|
||||||
|
<p>Are you sure you want to remove <strong>{{ device_bay.installed_device }}</strong> from <strong>{{ device_bay }}</strong>?</p>
|
||||||
|
{% endblock %}
|
51
netbox/templates/dcim/devicebay_edit.html
Normal file
51
netbox/templates/dcim/devicebay_edit.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block title %}{% if devicebay.pk %}Editing {{ devicebay.device }} {{ devicebay }}{% else %}Add a Device Bay ({{ device }}){% endif %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<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>
|
||||||
|
<div class="panel-body">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
{% if poweroutlet.pk %}
|
||||||
|
<strong>Editing {{ devicebay }}</strong>
|
||||||
|
{% else %}
|
||||||
|
<strong>Add a Device Bay</strong>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label required">Device</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">{% if devicebay %}{{ devicebay.device }}{% else %}{{ device }}{% endif %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% render_form form %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-9 col-md-offset-3">
|
||||||
|
{% if devicebay.pk %}
|
||||||
|
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||||
|
{% else %}
|
||||||
|
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||||
|
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
46
netbox/templates/dcim/devicebay_populate.html
Normal file
46
netbox/templates/dcim/devicebay_populate.html
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block title %}Populate {{ device_bay }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<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>
|
||||||
|
<div class="panel-body">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">Populate {{ device_bay }}</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label required">Parent Device</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">{{ device_bay.device }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label required">Bay</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">{{ device_bay.name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% render_form form %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-9 col-md-offset-3">
|
||||||
|
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||||
|
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
@ -14,8 +14,10 @@
|
|||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if perms.dcim.change_devicetype %}
|
|
||||||
|
{% if perms.dcim.change_devicetype or perms.dcim.delete_devicetype %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
|
{% if perms.dcim.change_devicetype %}
|
||||||
<a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
|
<a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
||||||
Edit this device type
|
Edit this device type
|
||||||
@ -26,8 +28,10 @@
|
|||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
||||||
Delete this device type
|
Delete this device type
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h1>{{ devicetype }}</h1>
|
<h1>{{ devicetype }}</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@ -77,9 +81,18 @@
|
|||||||
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
|
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
{% if devicetype.is_parent_device %}
|
||||||
|
{% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicetype_add_devicebay' delete_url='dcim:devicetype_delete_devicebay' %}
|
||||||
|
{% endif %}
|
||||||
|
{% if devicetype.is_network_device %}
|
||||||
{% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' delete_url='dcim:devicetype_delete_interface' %}
|
{% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' delete_url='dcim:devicetype_delete_interface' %}
|
||||||
|
{% endif %}
|
||||||
|
{% if devicetype.is_console_server %}
|
||||||
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:devicetype_add_consoleserverport' delete_url='dcim:devicetype_delete_consoleserverport' %}
|
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:devicetype_add_consoleserverport' delete_url='dcim:devicetype_delete_consoleserverport' %}
|
||||||
|
{% endif %}
|
||||||
|
{% if devicetype.is_pdu %}
|
||||||
{% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' add_url='dcim:devicetype_add_poweroutlet' delete_url='dcim:devicetype_delete_poweroutlet' %}
|
{% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' add_url='dcim:devicetype_add_poweroutlet' delete_url='dcim:devicetype_delete_poweroutlet' %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
|
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
|
||||||
</td>
|
</td>
|
||||||
|
<td></td>
|
||||||
{% if cp.cs_port %}
|
{% if cp.cs_port %}
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
|
<a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
|
||||||
@ -10,7 +11,9 @@
|
|||||||
{{ cp.cs_port.name }}
|
{{ cp.cs_port.name }}
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2">Not connected</td>
|
<td colspan="2">
|
||||||
|
<span class="text-muted">Not connected</span>
|
||||||
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.dcim.change_consoleport %}
|
{% if perms.dcim.change_consoleport %}
|
||||||
|
@ -10,7 +10,9 @@
|
|||||||
{{ csp.connected_console.name }}
|
{{ csp.connected_console.name }}
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2">Not connected</td>
|
<td colspan="2">
|
||||||
|
<span class="text-muted">Not connected</span>
|
||||||
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.dcim.change_consoleserverport %}
|
{% if perms.dcim.change_consoleserverport %}
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
<li><a href="{% url 'dcim:site' slug=device.rack.site.slug %}">{{ device.rack.site }}</a></li>
|
<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>
|
<li><a href="{% url 'dcim:rack_list' %}?site={{ device.rack.site.slug }}">Racks</a></li>
|
||||||
<li><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</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>
|
<li>{{ device }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
44
netbox/templates/dcim/inc/_devicebay.html
Normal file
44
netbox/templates/dcim/inc/_devicebay.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }}
|
||||||
|
</td>
|
||||||
|
{% if devicebay.installed_device %}
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'dcim:device' pk=devicebay.installed_device.pk %}">{{ devicebay.installed_device }}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span>{{ devicebay.installed_device.device_type }}</span>
|
||||||
|
</td>
|
||||||
|
{% else %}
|
||||||
|
<td colspan="2">
|
||||||
|
<span class="text-muted">Vacant</span>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td class="text-right">
|
||||||
|
{% if perms.dcim.change_devicebay %}
|
||||||
|
{% if devicebay.installed_device %}
|
||||||
|
<a href="{% url 'dcim:devicebay_depopulate' pk=devicebay.pk %}" class="btn btn-danger btn-xs">
|
||||||
|
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Remove device"></i>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'dcim:devicebay_populate' pk=devicebay.pk %}" class="btn btn-success btn-xs">
|
||||||
|
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Install device"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'dcim:devicebay_edit' pk=devicebay.pk %}" class="btn btn-info btn-xs">
|
||||||
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit device bay"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.delete_devicebay %}
|
||||||
|
{% if devicebay.installed_device %}
|
||||||
|
<button class="btn btn-danger btn-xs" disabled="disabled">
|
||||||
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'dcim:devicebay_delete' pk=devicebay.pk %}" class="btn btn-danger btn-xs">
|
||||||
|
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete device bay"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
@ -5,6 +5,9 @@
|
|||||||
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
|
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<small>{{ iface.mac_address|default:'' }}</small>
|
||||||
|
</td>
|
||||||
{% if not iface.is_physical %}
|
{% if not iface.is_physical %}
|
||||||
<td colspan="2">Virtual</td>
|
<td colspan="2">Virtual</td>
|
||||||
{% elif iface.connection %}
|
{% elif iface.connection %}
|
||||||
@ -21,7 +24,9 @@
|
|||||||
<a href="{% url 'circuits:circuit' pk=iface.circuit.pk %}">{{ iface.circuit }}</a>
|
<a href="{% url 'circuits:circuit' pk=iface.circuit.pk %}">{{ iface.circuit }}</a>
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2">Not connected</td>
|
<td colspan="2">
|
||||||
|
<span class="text-muted">Not connected</span>
|
||||||
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if iface.circuit or iface.connection %}
|
{% if iface.circuit or iface.connection %}
|
||||||
|
@ -10,7 +10,9 @@
|
|||||||
{{ po.connected_port.name }}
|
{{ po.connected_port.name }}
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2">Not connected</td>
|
<td colspan="2">
|
||||||
|
<span class="text-muted">Not connected</span>
|
||||||
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.dcim.change_poweroutlet %}
|
{% if perms.dcim.change_poweroutlet %}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
|
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
|
||||||
</td>
|
</td>
|
||||||
|
<td></td>
|
||||||
{% if pp.power_outlet %}
|
{% if pp.power_outlet %}
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
|
<a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
|
||||||
@ -10,7 +11,9 @@
|
|||||||
{{ pp.power_outlet.name }}
|
{{ pp.power_outlet.name }}
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2">Not connected</td>
|
<td colspan="2">
|
||||||
|
<span class="text-muted">Not connected</span>
|
||||||
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.dcim.change_powerport %}
|
{% if perms.dcim.change_powerport %}
|
||||||
|
@ -112,6 +112,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if nonracked_devices %}
|
{% if nonracked_devices %}
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Parent</th>
|
||||||
|
</tr>
|
||||||
{% for device in nonracked_devices %}
|
{% for device in nonracked_devices %}
|
||||||
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
|
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
|
||||||
<td>
|
<td>
|
||||||
@ -119,6 +125,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{{ device.device_role }}</td>
|
<td>{{ device.device_role }}</td>
|
||||||
<td>{{ device.device_type }}</td>
|
<td>{{ device.device_type }}</td>
|
||||||
|
<td>{% if device.parent_bay %}<a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay }}</a>{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
@ -1,13 +1,20 @@
|
|||||||
|
{% load secret_helpers %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{% url 'secrets:secret' pk=secret.pk %}">{{ secret.role }}</a></td>
|
<td><a href="{% url 'secrets:secret' pk=secret.pk %}">{{ secret.role }}</a></td>
|
||||||
<td>{{ secret.name }}</td>
|
<td>{{ secret.name }}</td>
|
||||||
<td id="secret_{{ secret.pk }}">********</td>
|
<td id="secret_{{ secret.pk }}">********</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
|
{% if secret|decryptable_by:request.user %}
|
||||||
<button class="btn btn-xs btn-success unlock-secret" secret-id="{{ secret.pk }}">
|
<button class="btn btn-xs btn-success unlock-secret" secret-id="{{ secret.pk }}">
|
||||||
<i class="fa fa-lock"></i> Unlock
|
<i class="fa fa-lock"></i> Unlock
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-xs btn-danger lock-secret collapse" secret-id="{{ secret.pk }}">
|
<button class="btn btn-xs btn-danger lock-secret collapse" secret-id="{{ secret.pk }}">
|
||||||
<i class="fa fa-unlock-alt"></i> Lock
|
<i class="fa fa-unlock-alt"></i> Lock
|
||||||
</button>
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-xs btn-default" disabled="disabled" title="Permission denied">
|
||||||
|
<i class="fa fa-lock"></i> Unlock
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
{% load static from staticfiles %}
|
{% load static from staticfiles %}
|
||||||
|
{% load secret_helpers %}
|
||||||
|
|
||||||
{% block title %}Secret: {{ secret }}{% endblock %}
|
{% block title %}Secret: {{ secret }}{% endblock %}
|
||||||
|
|
||||||
@ -67,6 +68,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
{% if secret|decryptable_by:request.user %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Secret Data</strong>
|
<strong>Secret Data</strong>
|
||||||
@ -89,6 +91,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></i>
|
||||||
|
You do not have permission to decrypt this secret.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -280,10 +280,10 @@ class BulkEditView(View):
|
|||||||
form = self.form(request.POST)
|
form = self.form(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
updated_count = self.update_objects(pk_list, form)
|
updated_count = self.update_objects(pk_list, form)
|
||||||
|
if updated_count:
|
||||||
msg = 'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural)
|
msg = 'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural)
|
||||||
messages.success(self.request, msg)
|
messages.success(self.request, msg)
|
||||||
UserAction.objects.log_bulk_edit(request.user, ContentType.objects.get_for_model(self.cls), msg)
|
UserAction.objects.log_bulk_edit(request.user, ContentType.objects.get_for_model(self.cls), msg)
|
||||||
|
|
||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
Loading…
Reference in New Issue
Block a user