mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-29 16:47:46 -06:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf44e512ff | ||
|
|
026403ed38 | ||
|
|
f6bd1f0c48 | ||
|
|
66489438b9 | ||
|
|
e5a6a4f05e | ||
|
|
9e4aa9c056 | ||
|
|
4ce40891f0 | ||
|
|
46b1ac23af | ||
|
|
a5f6e64849 | ||
|
|
b9db1ac7f7 | ||
|
|
124c2acad7 | ||
|
|
2691590aa1 | ||
|
|
51cc0d5083 | ||
|
|
9c32943d73 | ||
|
|
4483ba55dd | ||
|
|
f20e0edb35 | ||
|
|
aed2180142 | ||
|
|
4913d25d18 | ||
|
|
9e181c20c7 | ||
|
|
404d934736 | ||
|
|
024c7da15b | ||
|
|
d3a5b82d93 | ||
|
|
1e3a03c463 | ||
|
|
bafbc052e2 | ||
|
|
9421ec040c | ||
|
|
07fc2e5502 | ||
|
|
9098001bcb | ||
|
|
d9bf199e75 | ||
|
|
6f1ed9fc16 | ||
|
|
96b496ffa8 | ||
|
|
f1b6f0cfee | ||
|
|
e19ce043d6 | ||
|
|
35a2671525 | ||
|
|
03542b400d | ||
|
|
73d24532c9 | ||
|
|
b60f964835 | ||
|
|
8e7e02a622 | ||
|
|
2c23ca33a2 | ||
|
|
69affb7a6e | ||
|
|
6a6cf14a38 | ||
|
|
e1da3b8f10 | ||
|
|
da50cd0f03 | ||
|
|
d80ffd2308 | ||
|
|
18846cf40a | ||
|
|
e81a2094df | ||
|
|
173a4cde8b | ||
|
|
d9867423de |
@@ -27,9 +27,10 @@ IRC.
|
||||
|
||||
## Feature Requests
|
||||
|
||||
* First, check the [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you'd like to see
|
||||
has already been requested (and possibly rejected). If it is, be sure to comment with a "+1" and any additional
|
||||
justification you have for the feature.
|
||||
* First, check the [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're requesting
|
||||
has already been requested (and possibly rejected). If it has, click "add a reaction" in the top right corner of the
|
||||
issue and add a thumbs up (+1). This ensures that the issue has a better chance of making it onto the roadmap. Also feel
|
||||
free to add a comment with any additional justification for the feature.
|
||||
|
||||
* While discussion of new features is welcome, it's important to limit the scope of NetBox's feature set to avoid
|
||||
feature creep. For example, the following features would be firmly out of scope for NetBox:
|
||||
|
||||
@@ -4,6 +4,8 @@ NetBox is an IP address management (IPAM) and data center infrastructure managem
|
||||
|
||||
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/) Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox).
|
||||
|
||||
The complete documentation for Netbox can be found at [Read the Docs](http://netbox.readthedocs.io/en/latest/).
|
||||
|
||||
Questions? Comments? Please join us on IRC in **#netbox** on **irc.freenode.net**!
|
||||
|
||||
### Build Status
|
||||
|
||||
@@ -13,11 +13,24 @@ ADMINS = [
|
||||
|
||||
---
|
||||
|
||||
## BANNER_TOP
|
||||
|
||||
## BANNER_BOTTOM
|
||||
|
||||
Setting these variables will display content in a banner at the top and/or bottom of the page, respectively. To replicate the content of the top banner in the bottom banner, set:
|
||||
|
||||
```
|
||||
BANNER_TOP = 'Your banner text'
|
||||
BANNER_BOTTOM = BANNER_TOP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DEBUG
|
||||
|
||||
Default: False
|
||||
|
||||
This setting enables debugging. This should be done only during development or troubleshooting. Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users.
|
||||
This setting enables debugging. This should be done only during development or troubleshooting. Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users.
|
||||
|
||||
---
|
||||
|
||||
@@ -66,6 +79,14 @@ Determine how many objects to display per page within each list of objects.
|
||||
|
||||
---
|
||||
|
||||
## PREFER_IPV4
|
||||
|
||||
Default: False
|
||||
|
||||
When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to prefer IPv4 instead.
|
||||
|
||||
---
|
||||
|
||||
## TIME_ZONE
|
||||
|
||||
Default: UTC
|
||||
|
||||
@@ -112,6 +112,9 @@ Generate a random secret key of at least 50 alphanumeric characters. This key mu
|
||||
|
||||
You may use the script located at `netbox/generate_secret_key.py` to generate a suitable key.
|
||||
|
||||
!!! note
|
||||
In the case of a highly available installation with multiple web servers, `SECRET_KEY` must be identical among all servers in order to maintain a persistent user session state.
|
||||
|
||||
# Run Database Migrations
|
||||
|
||||
Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
|
||||
|
||||
@@ -4,42 +4,40 @@ As with the initial installation, you can upgrade NetBox by either downloading t
|
||||
|
||||
## Option A: Download a Release
|
||||
|
||||
Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`. For this guide we are using 1.0.4 as the old version and 1.0.7 as the new version.
|
||||
Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`.
|
||||
|
||||
Download and extract the latest version:
|
||||
|
||||
Download & extract latest version:
|
||||
```
|
||||
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
|
||||
# tar -xzf vX.Y.Z.tar.gz -C /opt
|
||||
# cd /opt/
|
||||
# ln -sf netbox-1.0.7/ netbox
|
||||
# ln -sf netbox-X.Y.Z/ netbox
|
||||
```
|
||||
|
||||
Copy the 'configuration.py' you created when first installing to the new version:
|
||||
|
||||
```
|
||||
# cp /opt/netbox-1.0.4/configuration.py /opt/netbox/configuration.py
|
||||
# cp /opt/netbox-X.Y.Z/configuration.py /opt/netbox/configuration.py
|
||||
```
|
||||
|
||||
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
|
||||
|
||||
```
|
||||
# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py
|
||||
```
|
||||
|
||||
## Option B: Clone the Git Repository (latest master release)
|
||||
|
||||
For this guide, we'll use `/opt/netbox`.
|
||||
This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
|
||||
|
||||
Check that your git branch is up to date & is set to master:
|
||||
```
|
||||
# cd /opt/netbox
|
||||
# git status
|
||||
```
|
||||
|
||||
If not on branch master, set it and verify status:
|
||||
```
|
||||
# git checkout master
|
||||
# git pull origin master
|
||||
# git status
|
||||
```
|
||||
|
||||
Pull down the set branch from git status above:
|
||||
```
|
||||
# git pull
|
||||
```
|
||||
|
||||
# Run the Upgrade Script
|
||||
|
||||
Once the new code is in place, run the upgrade script (which may need to be run as root depending on how your environment is configured).
|
||||
|
||||
@@ -1,9 +1,40 @@
|
||||
import django_filters
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.models import Site
|
||||
from .models import Provider, Circuit, CircuitType
|
||||
|
||||
|
||||
class ProviderFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='circuits__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='circuits__site',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['q', 'name', 'account', 'asn']
|
||||
|
||||
def search(self, queryset, value):
|
||||
value = value.strip()
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(account__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class CircuitFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
|
||||
@@ -59,6 +59,16 @@ class ProviderBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def provider_site_choices():
|
||||
site_choices = Site.objects.all()
|
||||
return [(s.slug, s.name) for s in site_choices]
|
||||
|
||||
|
||||
class ProviderFilterForm(forms.Form, BootstrapMixin):
|
||||
site = forms.MultipleChoiceField(required=False, choices=provider_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
|
||||
|
||||
#
|
||||
# Circuit types
|
||||
#
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-07-13 19:24
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0002_auto_20160622_1821'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='asn',
|
||||
field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'),
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
|
||||
from dcim.fields import ASNField
|
||||
from dcim.models import Site, Interface
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
|
||||
@@ -12,7 +13,7 @@ class Provider(CreatedUpdatedModel):
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
asn = models.PositiveIntegerField(blank=True, null=True, verbose_name='ASN')
|
||||
asn = ASNField(blank=True, null=True, verbose_name='ASN')
|
||||
account = models.CharField(max_length=30, blank=True, verbose_name='Account number')
|
||||
portal_url = models.URLField(blank=True, verbose_name='Portal')
|
||||
noc_contact = models.TextField(blank=True, verbose_name='NOC contact')
|
||||
|
||||
@@ -16,6 +16,8 @@ from .models import Circuit, CircuitType, Provider
|
||||
|
||||
class ProviderListView(ObjectListView):
|
||||
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
|
||||
filter = filters.ProviderFilter
|
||||
filter_form = forms.ProviderFilterForm
|
||||
table = tables.ProviderTable
|
||||
edit_permissions = ['circuits.change_provider', 'circuits.delete_provider']
|
||||
template_name = 'circuits/provider_list.html'
|
||||
|
||||
@@ -1 +1 @@
|
||||
default_app_config = 'dcim.apps.IPAMConfig'
|
||||
default_app_config = 'dcim.apps.DCIMConfig'
|
||||
|
||||
@@ -180,4 +180,4 @@ class DeviceAdmin(admin.ModelAdmin):
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(DeviceAdmin, self).get_queryset(request)
|
||||
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip', 'rack')
|
||||
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip4', 'primary_ip6', 'rack')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class IPAMConfig(AppConfig):
|
||||
class DCIMConfig(AppConfig):
|
||||
name = "dcim"
|
||||
verbose_name = "DCIM"
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
from netaddr import EUI, mac_unix_expanded
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.db import models
|
||||
|
||||
from .formfields import MACAddressFormField
|
||||
|
||||
|
||||
class ASNField(models.BigIntegerField):
|
||||
description = "32-bit ASN field"
|
||||
default_validators = [
|
||||
MinValueValidator(1),
|
||||
MaxValueValidator(4294967295),
|
||||
]
|
||||
|
||||
|
||||
class mac_unix_expanded_uppercase(mac_unix_expanded):
|
||||
word_fmt = '%.2X'
|
||||
|
||||
|
||||
@@ -122,6 +122,11 @@ class DeviceFilter(django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
rack_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Rack group (ID)',
|
||||
)
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack',
|
||||
queryset=Rack.objects.all(),
|
||||
|
||||
@@ -186,7 +186,7 @@ def rack_group_choices():
|
||||
class RackFilterForm(forms.Form, BootstrapMixin):
|
||||
site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices,
|
||||
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, label='Rack Group',
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
|
||||
|
||||
@@ -502,6 +502,11 @@ def device_site_choices():
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.device_count)) for s in site_choices]
|
||||
|
||||
|
||||
def device_rack_group_choices():
|
||||
group_choices = RackGroup.objects.select_related('site').annotate(device_count=Count('racks__devices'))
|
||||
return [(g.pk, '{} ({})'.format(g, g.device_count)) for g in group_choices]
|
||||
|
||||
|
||||
def device_role_choices():
|
||||
role_choices = DeviceRole.objects.annotate(device_count=Count('devices'))
|
||||
return [(r.slug, '{} ({})'.format(r.name, r.device_count)) for r in role_choices]
|
||||
@@ -520,6 +525,8 @@ def device_platform_choices():
|
||||
class DeviceFilterForm(forms.Form, BootstrapMixin):
|
||||
site = forms.MultipleChoiceField(required=False, choices=device_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
rack_group_id = forms.MultipleChoiceField(required=False, choices=device_rack_group_choices, label='Rack Group',
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type',
|
||||
|
||||
21
netbox/dcim/migrations/0009_site_32bit_asn_support.py
Normal file
21
netbox/dcim/migrations/0009_site_32bit_asn_support.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-07-13 19:24
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0008_device_remove_primary_ip'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='asn',
|
||||
field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,6 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import MinValueValidator
|
||||
@@ -10,7 +11,7 @@ from extras.rpc import RPC_CLIENTS
|
||||
from utilities.fields import NullableCharField
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
|
||||
from .fields import MACAddressField
|
||||
from .fields import ASNField, MACAddressField
|
||||
|
||||
RACK_FACE_FRONT = 0
|
||||
RACK_FACE_REAR = 1
|
||||
@@ -144,7 +145,7 @@ class Site(CreatedUpdatedModel):
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
facility = models.CharField(max_length=50, blank=True)
|
||||
asn = models.PositiveIntegerField(blank=True, null=True, verbose_name='ASN')
|
||||
asn = ASNField(blank=True, null=True, verbose_name='ASN')
|
||||
physical_address = models.CharField(max_length=200, blank=True)
|
||||
shipping_address = models.CharField(max_length=200, blank=True)
|
||||
comments = models.TextField(blank=True)
|
||||
@@ -713,7 +714,9 @@ class Device(CreatedUpdatedModel):
|
||||
|
||||
@property
|
||||
def primary_ip(self):
|
||||
if self.primary_ip6:
|
||||
if settings.PREFER_IPV4 and self.primary_ip4:
|
||||
return self.primary_ip4
|
||||
elif self.primary_ip6:
|
||||
return self.primary_ip6
|
||||
elif self.primary_ip4:
|
||||
return self.primary_ip4
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import re
|
||||
from natsort import natsorted
|
||||
from operator import attrgetter
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
@@ -259,18 +261,31 @@ def devicetype(request, pk):
|
||||
devicetype = get_object_or_404(DeviceType, pk=pk)
|
||||
|
||||
# Component tables
|
||||
consoleport_table = tables.ConsolePortTemplateTable(ConsolePortTemplate.objects.filter(device_type=devicetype))
|
||||
consoleserverport_table = tables.ConsoleServerPortTemplateTable(ConsoleServerPortTemplate.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))
|
||||
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype))
|
||||
devicebay_table = tables.DeviceBayTemplateTable(DeviceBayTemplate.objects.filter(device_type=devicetype))
|
||||
consoleport_table = tables.ConsolePortTemplateTable(
|
||||
natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||
)
|
||||
consoleserverport_table = tables.ConsoleServerPortTemplateTable(
|
||||
natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||
)
|
||||
powerport_table = tables.PowerPortTemplateTable(
|
||||
natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||
)
|
||||
poweroutlet_table = tables.PowerOutletTemplateTable(
|
||||
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||
)
|
||||
mgmt_interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype,
|
||||
mgmt_only=True))
|
||||
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype,
|
||||
mgmt_only=False))
|
||||
devicebay_table = tables.DeviceBayTemplateTable(
|
||||
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||
)
|
||||
if request.user.has_perm('dcim.change_devicetype'):
|
||||
consoleport_table.base_columns['pk'].visible = True
|
||||
consoleserverport_table.base_columns['pk'].visible = True
|
||||
powerport_table.base_columns['pk'].visible = True
|
||||
poweroutlet_table.base_columns['pk'].visible = True
|
||||
mgmt_interface_table.base_columns['pk'].visible = True
|
||||
interface_table.base_columns['pk'].visible = True
|
||||
devicebay_table.base_columns['pk'].visible = True
|
||||
|
||||
@@ -280,6 +295,7 @@ def devicetype(request, pk):
|
||||
'consoleserverport_table': consoleserverport_table,
|
||||
'powerport_table': powerport_table,
|
||||
'poweroutlet_table': poweroutlet_table,
|
||||
'mgmt_interface_table': mgmt_interface_table,
|
||||
'interface_table': interface_table,
|
||||
'devicebay_table': devicebay_table,
|
||||
})
|
||||
@@ -337,7 +353,7 @@ class ComponentTemplateCreateView(View):
|
||||
return render(request, 'dcim/component_template_add.html', {
|
||||
'devicetype': devicetype,
|
||||
'component_type': self.model._meta.verbose_name,
|
||||
'form': self.form(),
|
||||
'form': self.form(initial=request.GET),
|
||||
'cancel_url': reverse('dcim:devicetype', kwargs={'pk': devicetype.pk}),
|
||||
})
|
||||
|
||||
@@ -513,15 +529,26 @@ class DeviceListView(ObjectListView):
|
||||
def device(request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
console_ports = ConsolePort.objects.filter(device=device).select_related('cs_port__device')
|
||||
cs_ports = ConsoleServerPort.objects.filter(device=device).select_related('connected_console')
|
||||
power_ports = PowerPort.objects.filter(device=device).select_related('power_outlet__device')
|
||||
power_outlets = PowerOutlet.objects.filter(device=device).select_related('connected_port')
|
||||
console_ports = natsorted(
|
||||
ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
|
||||
)
|
||||
cs_ports = natsorted(
|
||||
ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name')
|
||||
)
|
||||
power_ports = natsorted(
|
||||
PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name')
|
||||
)
|
||||
power_outlets = natsorted(
|
||||
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
||||
)
|
||||
interfaces = Interface.objects.filter(device=device, mgmt_only=False)\
|
||||
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
||||
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\
|
||||
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
||||
device_bays = DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer')
|
||||
device_bays = natsorted(
|
||||
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
||||
key=attrgetter('name')
|
||||
)
|
||||
|
||||
# Gather any secrets which belong to this device
|
||||
secrets = device.secrets.all()
|
||||
|
||||
@@ -123,6 +123,8 @@ class Aggregate(CreatedUpdatedModel):
|
||||
|
||||
# Ensure that the aggregate being added does not cover an existing aggregate
|
||||
covered_aggregates = Aggregate.objects.filter(prefix__net_contained=str(self.prefix))
|
||||
if self.pk:
|
||||
covered_aggregates = covered_aggregates.exclude(pk=self.pk)
|
||||
if covered_aggregates:
|
||||
raise ValidationError("{} is overlaps with an existing aggregate ({})"
|
||||
.format(self.prefix, covered_aggregates[0]))
|
||||
|
||||
@@ -78,3 +78,7 @@ SHORT_DATETIME_FORMAT = 'Y-m-d H:i'
|
||||
# banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
|
||||
BANNER_TOP = ''
|
||||
BANNER_BOTTOM = ''
|
||||
|
||||
# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to
|
||||
# prefer IPv4 instead.
|
||||
PREFER_IPV4 = False
|
||||
|
||||
@@ -12,7 +12,7 @@ except ImportError:
|
||||
"the documentation.")
|
||||
|
||||
|
||||
VERSION = '1.2.0'
|
||||
VERSION = '1.2.3-dev'
|
||||
|
||||
# Import local configuration
|
||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
@@ -40,6 +40,7 @@ DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
||||
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
|
||||
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
|
||||
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
|
||||
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
|
||||
|
||||
# Attempt to import LDAP configuration if it has been defined
|
||||
@@ -137,7 +138,6 @@ TEMPLATES = [
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'utilities.context_processors.settings',
|
||||
'django.core.context_processors.request',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
html {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
$(document).ready(function() {
|
||||
|
||||
// "Select all" checkbox in a table header
|
||||
$('th input:checkbox').click(function (event) {
|
||||
$('th input:checkbox[name=_all]').click(function (event) {
|
||||
$(this).parents('table').find('td input:checkbox').prop('checked', $(this).prop('checked'));
|
||||
});
|
||||
// Uncheck the "select all" checkbox if an item is unchecked
|
||||
$('input:checkbox[name=pk]').click(function (event) {
|
||||
if (!$(this).attr('checked')) {
|
||||
$(this).parents('table').find('input:checkbox[name=_all]').prop('checked', false);
|
||||
}
|
||||
});
|
||||
|
||||
// Slugify
|
||||
function slugify(s, num_chars) {
|
||||
|
||||
@@ -42,7 +42,7 @@ class SecretListView(generics.GenericAPIView):
|
||||
"""
|
||||
List secrets (filterable). If a private key is POSTed, attempt to decrypt each Secret.
|
||||
"""
|
||||
queryset = Secret.objects.select_related('device__primary_ip', 'role')\
|
||||
queryset = Secret.objects.select_related('device__primary_ip4', 'device__primary_ip6', 'role')\
|
||||
.prefetch_related('role__users', 'role__groups')
|
||||
serializer_class = serializers.SecretSerializer
|
||||
filter_class = SecretFilter
|
||||
@@ -87,7 +87,7 @@ class SecretDetailView(generics.GenericAPIView):
|
||||
"""
|
||||
Retrieve a single Secret. If a private key is POSTed, attempt to decrypt the Secret.
|
||||
"""
|
||||
queryset = Secret.objects.select_related('device__primary_ip', 'role')\
|
||||
queryset = Secret.objects.select_related('device__primary_ip4', 'device__primary_ip6', 'role')\
|
||||
.prefetch_related('role__users', 'role__groups')
|
||||
serializer_class = serializers.SecretSerializer
|
||||
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]
|
||||
|
||||
@@ -14,8 +14,28 @@
|
||||
</div>
|
||||
<h1>Providers</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Search</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form action="{% url 'circuits:provider_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/filter_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<div class="col-md-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||
<strong>Search</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<table class="table table-hover panel-body">
|
||||
<tr>
|
||||
<td>Manufacturer</td>
|
||||
<td>{{ devicetype.manufacturer }}</td>
|
||||
<td><a href="{% url 'dcim:devicetype_list' %}?manufacturer={{ devicetype.manufacturer.slug }}">{{ devicetype.manufacturer }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Model Name</td>
|
||||
@@ -54,7 +54,13 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Full Depth</td>
|
||||
<td>{{ devicetype.is_full_depth|yesno|capfirst }}</td>
|
||||
<td>
|
||||
{% if devicetype.is_full_depth %}
|
||||
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -64,21 +70,70 @@
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<tr>
|
||||
<td>Is a Console Server</td>
|
||||
<td>{{ devicetype.is_console_server|yesno|capfirst }}</td>
|
||||
<td class="text-right">
|
||||
{% if devicetype.is_console_server %}
|
||||
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<strong>Console Server</strong><br />
|
||||
<small class="text-muted">This device {% if devicetype.is_console_server %}has{% else %}does not have{% endif %} console server ports</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Is a PDU</td>
|
||||
<td>{{ devicetype.is_pdu|yesno|capfirst }}</td>
|
||||
<td class="text-right">
|
||||
{% if devicetype.is_pdu %}
|
||||
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<strong>PDU</strong><br />
|
||||
<small class="text-muted">This device {% if devicetype.is_pdu %}has{% else %}does not have{% endif %} power outlets</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Is a Network Device</td>
|
||||
<td>{{ devicetype.is_network_device|yesno|capfirst }}</td>
|
||||
<td class="text-right">
|
||||
{% if devicetype.is_network_device %}
|
||||
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<strong>Network Device</strong><br />
|
||||
<small class="text-muted">This device {% if devicetype.is_network_device %}has{% else %}does not have{% endif %} non-management network interfaces</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-right">
|
||||
{% if devicetype.subdevice_role == True %}
|
||||
<label class="label label-primary">Parent</label>
|
||||
{% elif devicetype.subdevice_role == False %}
|
||||
<label class="label label-info">Child</label>
|
||||
{% else %}
|
||||
<label class="label label-default">None</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<strong>Parent/Child</strong><br />
|
||||
{% if devicetype.subdevice_role == True %}
|
||||
<small class="text-muted">This device has device bays for mounting child devices</small>
|
||||
{% elif devicetype.subdevice_role == False %}
|
||||
<small class="text-muted">This device can only be mounted in a parent device</small>
|
||||
{% else %}
|
||||
<small class="text-muted">This device does not have device bays</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% 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=mgmt_interface_table title='Management Interfaces' add_url='dcim:devicetype_add_interface' add_url_extra='?mgmt_only=1' delete_url='dcim:devicetype_delete_interface' %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if devicetype.is_parent_device %}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<a href="{% url add_url pk=devicetype.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add {{ title }}</a>
|
||||
<a href="{% url add_url pk=devicetype.pk %}{{ add_url_extra }}" class="btn btn-primary btn-xs pull-right">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add {{ title }}
|
||||
</a>
|
||||
<strong>{{ title }}</strong>
|
||||
</div>
|
||||
{% render_table table 'table.html' %}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<div class="col-md-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||
<strong>Search</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<div class="col-md-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||
<strong>Search</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span class="glyphicon glyphicon-filter" aria-hidden="true"></span>
|
||||
<strong>Filter</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<div class="col-md-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||
<strong>Search</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<div class="col-md-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||
<strong>Search</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<div class="col-md-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||
<strong>Search by ID</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -15,3 +15,4 @@ py-gfm==0.1.3
|
||||
pycrypto==2.6.1
|
||||
sqlparse==0.1.19
|
||||
xmltodict==0.10.2
|
||||
natsort>=5.0.0
|
||||
|
||||
Reference in New Issue
Block a user