mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 18:08:38 -06:00
Merge branch 'develop'
This commit is contained in:
commit
4e4996e88f
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ configuration.py
|
|||||||
/*.sh
|
/*.sh
|
||||||
!upgrade.sh
|
!upgrade.sh
|
||||||
fabfile.py
|
fabfile.py
|
||||||
|
*.swp
|
||||||
|
@ -51,6 +51,10 @@ Even if it's not quite right for NetBox, we may be able to point you to a tool b
|
|||||||
|
|
||||||
## Submitting Pull Requests
|
## Submitting Pull Requests
|
||||||
|
|
||||||
|
* Be sure to open an issue before starting work on a pull request, and discuss your idea with the NetBox maintainers
|
||||||
|
before beginning work. This will help prevent wasting time on something that might we might not be able to implement.
|
||||||
|
When suggesting a new feature, also make sure it won't conflict with any work that's already in progress.
|
||||||
|
|
||||||
* When submitting a pull request, please be sure to work off of branch `develop`, rather than branch `master`.
|
* When submitting a pull request, please be sure to work off of branch `develop`, rather than branch `master`.
|
||||||
In NetBox, the `develop` branch is used for ongoing development, while `master` is used for tagging new
|
In NetBox, the `develop` branch is used for ongoing development, while `master` is used for tagging new
|
||||||
stable releases.
|
stable releases.
|
||||||
|
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
FROM ubuntu:14.04
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
python2.7 \
|
||||||
|
python-dev \
|
||||||
|
git \
|
||||||
|
python-pip \
|
||||||
|
libxml2-dev \
|
||||||
|
libxslt1-dev \
|
||||||
|
libffi-dev \
|
||||||
|
graphviz \
|
||||||
|
libpq-dev \
|
||||||
|
build-essential \
|
||||||
|
gunicorn \
|
||||||
|
--no-install-recommends \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& mkdir -p /opt/netbox \
|
||||||
|
&& cd /opt/netbox \
|
||||||
|
&& git clone --depth 1 https://github.com/digitalocean/netbox.git -b master . \
|
||||||
|
&& pip install -r requirements.txt \
|
||||||
|
&& apt-get purge -y --auto-remove git build-essential
|
||||||
|
|
||||||
|
ADD docker/docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
|
ADD netbox/netbox/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/docker-entrypoint.sh" ]
|
||||||
|
|
||||||
|
ADD docker/gunicorn_config.py /opt/netbox/
|
||||||
|
ADD docker/nginx.conf /etc/netbox-nginx/
|
||||||
|
VOLUME ["/etc/netbox-nginx/"]
|
52
docker-compose.yml
Normal file
52
docker-compose.yml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:9.6
|
||||||
|
container_name: postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: netbox
|
||||||
|
POSTGRES_PASSWORD: J5brHrAXFLQSif0K
|
||||||
|
POSTGRES_DB: netbox
|
||||||
|
netbox:
|
||||||
|
image: digitalocean/netbox
|
||||||
|
links:
|
||||||
|
- postgres
|
||||||
|
container_name: netbox
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
environment:
|
||||||
|
SUPERUSER_NAME: admin
|
||||||
|
SUPERUSER_EMAIL: admin@example.com
|
||||||
|
SUPERUSER_PASSWORD: admin
|
||||||
|
ALLOWED_HOSTS: localhost
|
||||||
|
DB_NAME: netbox
|
||||||
|
DB_USER: netbox
|
||||||
|
DB_PASSWORD: J5brHrAXFLQSif0K
|
||||||
|
DB_HOST: postgres
|
||||||
|
SECRET_KEY: r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj
|
||||||
|
EMAIL_SERVER: localhost
|
||||||
|
EMAIL_PORT: 25
|
||||||
|
EMAIL_USERNAME: foo
|
||||||
|
EMAIL_PASSWORD: bar
|
||||||
|
EMAIL_TIMEOUT: 10
|
||||||
|
EMAIL_FROM: netbox@bar.com
|
||||||
|
NETBOX_USERNAME: guest
|
||||||
|
NETBOX_PASSWORD: guest
|
||||||
|
volumes:
|
||||||
|
- netbox-static-files:/opt/netbox/netbox/static
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.11.1-alpine
|
||||||
|
links:
|
||||||
|
- netbox
|
||||||
|
container_name: nginx
|
||||||
|
command: nginx -g 'daemon off;' -c /etc/netbox-nginx/nginx.conf
|
||||||
|
depends_on:
|
||||||
|
- netbox
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
volumes_from:
|
||||||
|
- netbox
|
||||||
|
volumes:
|
||||||
|
netbox-static-files:
|
||||||
|
driver: local
|
22
docker/docker-entrypoint.sh
Executable file
22
docker/docker-entrypoint.sh
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# run db migrations (retry on error)
|
||||||
|
while ! /opt/netbox/netbox/manage.py migrate 2>&1; do
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
# create superuser silently
|
||||||
|
if [[ -z ${SUPERUSER_NAME} || -z ${SUPERUSER_EMAIL} || -z ${SUPERUSER_PASSWORD} ]]; then
|
||||||
|
SUPERUSER_NAME='admin'
|
||||||
|
SUPERUSER_EMAIL='admin@example.com'
|
||||||
|
SUPERUSER_PASSWORD='admin'
|
||||||
|
echo "Using defaults: Username: ${SUPERUSER_NAME}, E-Mail: ${SUPERUSER_EMAIL}, Password: ${SUPERUSER_PASSWORD}"
|
||||||
|
fi
|
||||||
|
echo "from django.contrib.auth.models import User; User.objects.create_superuser('${SUPERUSER_NAME}', '${SUPERUSER_EMAIL}', '${SUPERUSER_PASSWORD}')" | python /opt/netbox/netbox/manage.py shell
|
||||||
|
|
||||||
|
# copy static files
|
||||||
|
/opt/netbox/netbox/manage.py collectstatic --no-input
|
||||||
|
|
||||||
|
# start unicorn
|
||||||
|
gunicorn --log-level debug --debug --error-logfile /dev/stderr --log-file /dev/stdout -c /opt/netbox/gunicorn_config.py netbox.wsgi
|
5
docker/gunicorn_config.py
Normal file
5
docker/gunicorn_config.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
command = '/usr/bin/gunicorn'
|
||||||
|
pythonpath = '/opt/netbox/netbox'
|
||||||
|
bind = '0.0.0.0:8001'
|
||||||
|
workers = 3
|
||||||
|
user = 'root'
|
35
docker/nginx.conf
Normal file
35
docker/nginx.conf
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
worker_processes 1;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
gzip on;
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
access_log off;
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
alias /opt/netbox/netbox/static/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://netbox:8001;
|
||||||
|
proxy_set_header X-Forwarded-Host $server_name;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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.
|
||||||
|
54
docs/getting-started-docker.md
Normal file
54
docs/getting-started-docker.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<h1>Getting Started with NetBox and Docker</h1>
|
||||||
|
|
||||||
|
This guide assumes that the latest versions of [Docker](https://www.docker.com/) and [docker-compose](https://docs.docker.com/compose/) are already installed in your host.
|
||||||
|
|
||||||
|
# Quickstart
|
||||||
|
|
||||||
|
To get NetBox up and running:
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://github.com/digitalocean/netbox.git
|
||||||
|
cd netbox
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available on http://localhost/ after a few minutes.
|
||||||
|
|
||||||
|
Default credentials:
|
||||||
|
* user: admin
|
||||||
|
* password: admin
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
You can configure the app at runtime using variables (see docker-compose.yml).
|
||||||
|
|
||||||
|
Possible environment variables:
|
||||||
|
|
||||||
|
* SUPERUSER_NAME
|
||||||
|
* SUPERUSER_EMAIL
|
||||||
|
* SUPERUSER_PASSWORD
|
||||||
|
* ALLOWED_HOSTS
|
||||||
|
* DB_NAME
|
||||||
|
* DB_USER
|
||||||
|
* DB_PASSWORD
|
||||||
|
* DB_HOST
|
||||||
|
* DB_PORT
|
||||||
|
* SECRET_KEY
|
||||||
|
* EMAIL_SERVER
|
||||||
|
* EMAIL_PORT
|
||||||
|
* EMAIL_USERNAME
|
||||||
|
* EMAIL_PASSWORD
|
||||||
|
* EMAIL_TIMEOUT
|
||||||
|
* EMAIL_FROM
|
||||||
|
* LOGIN_REQUIRED
|
||||||
|
* MAINTENANCE_MODE
|
||||||
|
* NETBOX_USERNAME
|
||||||
|
* NETBOX_PASSWORD
|
||||||
|
* PAGINATE_COUNT
|
||||||
|
* TIME_ZONE
|
||||||
|
* DATE_FORMAT
|
||||||
|
* SHORT_DATE_FORMAT
|
||||||
|
* TIME_FORMAT
|
||||||
|
* SHORT_TIME_FORMAT
|
||||||
|
* DATETIME_FORMAT
|
||||||
|
* SHORT_DATETIME_FORMAT
|
||||||
|
|
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,
|
||||||
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
|
||||||
#
|
#
|
||||||
@ -305,5 +319,5 @@ class InterfaceConnectionTable(BaseTable):
|
|||||||
interface_b = tables.Column(verbose_name='Interface B')
|
interface_b = tables.Column(verbose_name='Interface B')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = PowerPort
|
model = Interface
|
||||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
|
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
|
||||||
|
@ -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
|
||||||
#
|
#
|
||||||
|
75
netbox/netbox/configuration.docker.py
Normal file
75
netbox/netbox/configuration.docker.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import os
|
||||||
|
#########################
|
||||||
|
# #
|
||||||
|
# Required settings #
|
||||||
|
# #
|
||||||
|
#########################
|
||||||
|
|
||||||
|
# This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write
|
||||||
|
# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
|
||||||
|
#
|
||||||
|
# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
|
||||||
|
ALLOWED_HOSTS = [os.environ.get('ALLOWED_HOSTS', '')]
|
||||||
|
|
||||||
|
# PostgreSQL database configuration.
|
||||||
|
DATABASE = {
|
||||||
|
'NAME': os.environ.get('DB_NAME', 'netbox'), # Database name
|
||||||
|
'USER': os.environ.get('DB_USER', ''), # PostgreSQL username
|
||||||
|
'PASSWORD': os.environ.get('DB_PASSWORD', ''), # PostgreSQL password
|
||||||
|
'HOST': os.environ.get('DB_HOST', 'localhost'), # Database server
|
||||||
|
'PORT': os.environ.get('DB_PORT', ''), # Database port (leave blank for default)
|
||||||
|
}
|
||||||
|
|
||||||
|
# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file.
|
||||||
|
# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and
|
||||||
|
# symbols. NetBox will not run without this defined. For more information, see
|
||||||
|
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SECRET_KEY
|
||||||
|
SECRET_KEY = os.environ.get('SECRET_KEY', '')
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# #
|
||||||
|
# Optional settings #
|
||||||
|
# #
|
||||||
|
#########################
|
||||||
|
|
||||||
|
# Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of
|
||||||
|
# application errors (assuming correct email settings are provided).
|
||||||
|
ADMINS = [
|
||||||
|
# ['John Doe', 'jdoe@example.com'],
|
||||||
|
]
|
||||||
|
|
||||||
|
# Email settings
|
||||||
|
EMAIL = {
|
||||||
|
'SERVER': os.environ.get('EMAIL_SERVER', 'localhost'),
|
||||||
|
'PORT': os.environ.get('EMAIL_PORT', 25),
|
||||||
|
'USERNAME': os.environ.get('EMAIL_USERNAME', ''),
|
||||||
|
'PASSWORD': os.environ.get('EMAIL_PASSWORD', ''),
|
||||||
|
'TIMEOUT': os.environ.get('EMAIL_TIMEOUT', 10), # seconds
|
||||||
|
'FROM_EMAIL': os.environ.get('EMAIL_FROM', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
|
||||||
|
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
|
||||||
|
LOGIN_REQUIRED = os.environ.get('LOGIN_REQUIRED', False)
|
||||||
|
|
||||||
|
# Setting this to True will display a "maintenance mode" banner at the top of every page.
|
||||||
|
MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', False)
|
||||||
|
|
||||||
|
# Credentials that NetBox will use to access live devices.
|
||||||
|
NETBOX_USERNAME = os.environ.get('NETBOX_USERNAME', '')
|
||||||
|
NETBOX_PASSWORD = os.environ.get('NETBOX_PASSWORD', '')
|
||||||
|
|
||||||
|
# Determine how many objects to display per page within a list. (Default: 50)
|
||||||
|
PAGINATE_COUNT = os.environ.get('PAGINATE_COUNT', 50)
|
||||||
|
|
||||||
|
# Time zone (default: UTC)
|
||||||
|
TIME_ZONE = os.environ.get('TIME_ZONE', 'UTC')
|
||||||
|
|
||||||
|
# Date/time formatting. See the following link for supported formats:
|
||||||
|
# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
|
||||||
|
DATE_FORMAT = os.environ.get('DATE_FORMAT', 'N j, Y')
|
||||||
|
SHORT_DATE_FORMAT = os.environ.get('SHORT_DATE_FORMAT', 'Y-m-d')
|
||||||
|
TIME_FORMAT = os.environ.get('TIME_FORMAT', 'g:i a')
|
||||||
|
SHORT_TIME_FORMAT = os.environ.get('SHORT_TIME_FORMAT', 'H:i:s')
|
||||||
|
DATETIME_FORMAT = os.environ.get('DATETIME_FORMAT', 'N j, Y g:i a')
|
||||||
|
SHORT_DATETIME_FORMAT = os.environ.get('SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
@ -11,7 +11,7 @@ except ImportError:
|
|||||||
"the documentation.")
|
"the documentation.")
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.0.7-r1'
|
VERSION = '1.1.0'
|
||||||
|
|
||||||
# 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,14 +109,15 @@ 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):
|
||||||
master_key = uk.get_master_key(private_key)
|
raise PermissionDenied(detail="You do not have permission to decrypt this secret.")
|
||||||
if master_key is None:
|
master_key = uk.get_master_key(private_key)
|
||||||
return Response(
|
if master_key is None:
|
||||||
{'error': ERR_PRIVKEY_INVALID},
|
return Response(
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
{'error': ERR_PRIVKEY_INVALID},
|
||||||
)
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
secret.decrypt(master_key)
|
)
|
||||||
|
secret.decrypt(master_key)
|
||||||
|
|
||||||
serializer = self.get_serializer(secret)
|
serializer = self.get_serializer(secret)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
@ -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,23 +14,27 @@
|
|||||||
</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">
|
||||||
<a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
|
{% if perms.dcim.change_devicetype %}
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
<a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
|
||||||
Edit this device type
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
||||||
</a>
|
Edit this device type
|
||||||
{% endif %}
|
</a>
|
||||||
{% if perms.dcim.delete_devicetype %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:devicetype_delete' pk=devicetype.pk %}" class="btn btn-danger">
|
{% if perms.dcim.delete_devicetype %}
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
<a href="{% url 'dcim:devicetype_delete' pk=devicetype.pk %}" class="btn btn-danger">
|
||||||
Delete this device type
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
||||||
</a>
|
Delete this device type
|
||||||
|
</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">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Chassis</strong>
|
<strong>Chassis</strong>
|
||||||
@ -76,10 +80,19 @@
|
|||||||
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %}
|
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %}
|
||||||
{% 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">
|
||||||
{% 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' %}
|
{% if devicetype.is_network_device %}
|
||||||
{% 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=devicebay_table title='Device Bays' add_url='dcim:devicetype_add_devicebay' delete_url='dcim:devicetype_delete_devicebay' %}
|
||||||
{% 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 %}
|
||||||
|
{% 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' %}
|
||||||
|
{% 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' %}
|
||||||
|
{% 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' %}
|
||||||
|
{% 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">
|
||||||
<button class="btn btn-xs btn-success unlock-secret" secret-id="{{ secret.pk }}">
|
{% if secret|decryptable_by:request.user %}
|
||||||
<i class="fa fa-lock"></i> Unlock
|
<button class="btn btn-xs btn-success unlock-secret" secret-id="{{ secret.pk }}">
|
||||||
</button>
|
<i class="fa fa-lock"></i> Unlock
|
||||||
<button class="btn btn-xs btn-danger lock-secret collapse" secret-id="{{ secret.pk }}">
|
</button>
|
||||||
<i class="fa fa-unlock-alt"></i> Lock
|
<button class="btn btn-xs btn-danger lock-secret collapse" secret-id="{{ secret.pk }}">
|
||||||
</button>
|
<i class="fa fa-unlock-alt"></i> Lock
|
||||||
|
</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,28 +68,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
{% if secret|decryptable_by:request.user %}
|
||||||
<div class="panel-heading">
|
<div class="panel panel-default">
|
||||||
<strong>Secret Data</strong>
|
<div class="panel-heading">
|
||||||
</div>
|
<strong>Secret Data</strong>
|
||||||
<div class="panel-body">
|
</div>
|
||||||
<form id="secret_form">
|
<div class="panel-body">
|
||||||
{% csrf_token %}
|
<form id="secret_form">
|
||||||
</form>
|
{% csrf_token %}
|
||||||
<div class="row">
|
</form>
|
||||||
<div class="col-md-2">Secret</div>
|
<div class="row">
|
||||||
<div class="col-md-8" id="secret_{{ secret.pk }}">********</div>
|
<div class="col-md-2">Secret</div>
|
||||||
<div class="col-md-2 text-right">
|
<div class="col-md-8" id="secret_{{ secret.pk }}">********</div>
|
||||||
<button class="btn btn-xs btn-success unlock-secret" secret-id="{{ secret.pk }}">
|
<div class="col-md-2 text-right">
|
||||||
<i class="fa fa-lock"></i> Unlock
|
<button class="btn btn-xs btn-success unlock-secret" secret-id="{{ secret.pk }}">
|
||||||
</button>
|
<i class="fa fa-lock"></i> Unlock
|
||||||
<button class="btn btn-xs btn-danger lock-secret collapse" secret-id="{{ secret.pk }}">
|
</button>
|
||||||
<i class="fa fa-unlock-alt"></i> Lock
|
<button class="btn btn-xs btn-danger lock-secret collapse" secret-id="{{ secret.pk }}">
|
||||||
</button>
|
<i class="fa fa-unlock-alt"></i> Lock
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ class ObjectEditView(View):
|
|||||||
'obj': obj,
|
'obj': obj,
|
||||||
'obj_type': self.model._meta.verbose_name,
|
'obj_type': self.model._meta.verbose_name,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': obj.get_absolute_url() if obj else reverse(self.cancel_url),
|
'cancel_url': obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else reverse(self.cancel_url),
|
||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
@ -157,7 +157,7 @@ class ObjectEditView(View):
|
|||||||
'obj': obj,
|
'obj': obj,
|
||||||
'obj_type': self.model._meta.verbose_name,
|
'obj_type': self.model._meta.verbose_name,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': obj.get_absolute_url() if obj else reverse(self.cancel_url),
|
'cancel_url': obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else reverse(self.cancel_url),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -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)
|
||||||
msg = 'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural)
|
if updated_count:
|
||||||
messages.success(self.request, msg)
|
msg = 'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural)
|
||||||
UserAction.objects.log_bulk_edit(request.user, ContentType.objects.get_for_model(self.cls), msg)
|
messages.success(self.request, 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:
|
||||||
|
17
upgrade.sh
17
upgrade.sh
@ -1,13 +1,24 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
# This script will prepare NetBox to run after the code has been upgraded to
|
# This script will prepare NetBox to run after the code has been upgraded to
|
||||||
# its most recent release.
|
# its most recent release.
|
||||||
#
|
#
|
||||||
# Once the script completes, remember to restart the WSGI service (e.g.
|
# Once the script completes, remember to restart the WSGI service (e.g.
|
||||||
# gunicorn or uWSGI).
|
# gunicorn or uWSGI).
|
||||||
|
|
||||||
|
# Optionally use sudo if not already root, and always prompt for password
|
||||||
|
# before running the command
|
||||||
|
PREFIX="sudo -k "
|
||||||
|
if [ "$(whoami)" = "root" ]; then
|
||||||
|
# When running upgrade as root, ask user to confirm if they wish to
|
||||||
|
# continue
|
||||||
|
read -n1 -rsp $'Running NetBox upgrade as root, press any key to continue or ^C to cancel\n'
|
||||||
|
PREFIX=""
|
||||||
|
fi
|
||||||
|
|
||||||
# Install any new Python packages
|
# Install any new Python packages
|
||||||
echo "Updating required Python packages (pip install -r requirements.txt --upgrade)..."
|
COMMAND="${PREFIX}pip install -r requirements.txt --upgrade"
|
||||||
sudo pip install -r requirements.txt --upgrade
|
echo "Updating required Python packages ($COMMAND)..."
|
||||||
|
eval $COMMAND
|
||||||
|
|
||||||
# Apply any database migrations
|
# Apply any database migrations
|
||||||
./netbox/manage.py migrate
|
./netbox/manage.py migrate
|
||||||
|
Loading…
Reference in New Issue
Block a user