Compare commits

..

1 Commits

Author SHA1 Message Date
Jeremy Stretch
0c3970233e Merge pull request #269 from digitalocean/develop
Release v1.2.0
2016-07-12 11:37:56 -04:00
96 changed files with 513 additions and 1738 deletions

View File

@@ -1,52 +1,37 @@
## Getting Help
# Contributing to NetBox
If you encounter any issues installing or using NetBox, try one of the following resources to get assistance. Please
**do not** open an issue on GitHub except to report bugs or request features.
Thank you for your interest in contributing to NetBox! This document contains some quick pointers on reporting bugs and
requesting new features.
### Freenode IRC
## Reporting Issues
Join the #netbox channel on [Freenode IRC](https://freenode.net/). You can connect to Freenode at irc.freenode.net using
an IRC client, or you can use their [webchat client](https://webchat.freenode.net/).
* First, ensure that you've installed the latest stable version of NetBox. If you're running an older version, it's
possible that the bug has already been fixed.
### Reddit
* Check the [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has already been
reported. If you think you may be experiencing a reported issue, please add a quick comment to it with a "+1" and a
quick description of how it's affecting your installation.
We have established [/r/netbox](https://www.reddit.com/r/netbox) on Reddit for NetBox issues and general discussion.
Reddit registration is free and does not require providing an email address (although it is encouraged).
* If you're unsure whether the behavior you're seeing is expected, you can join #netbox on irc.freenode.net and ask
before going through the trouble of submitting an issue report.
## Reporting Bugs
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of
NetBox. If you're running an older version, it's possible that the bug has already been fixed.
* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has
already been reported. If you think you may be experiencing a reported issue that hasn't already been resolved, please
click "add a reaction" in the top right corner of the issue and add a thumbs up (+1). You might also want to add a
comment describing how it's affecting your installation. This will allow us to prioritize bugs based on how many users
are affected.
* If you haven't found an existing issue that describes your suspected bug, please inquire about it on IRC or Reddit.
**Do not** file an issue until you have received confirmation that it is in fact a bug. Invalid issues are very
distracting and slow the pace at which NetBox is developed.
* When submitting an issue, please be as descriptive as possible. Be sure to include:
* When submitting an issue, please be as descriptive as possible. Be sure to describe:
* The environment in which NetBox is running
* The exact steps that can be taken to reproduce the issue (if applicable)
* Any error messages returned
* Screenshots (if applicable)
* Keep in mind that we prioritize bugs based on their severity and how much work is required to resolve them. It may
take some time for someone to address your issue.
take some time for someone to address your issue. If it's been longer than a week with no updates, please ping us on
IRC.
## Feature Requests
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're
requesting is already listed. (Be sure to search closed issues as well, since some feature requests are rejected.) If
the feature you'd like to see has already been requested, 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.
* 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.
* While suggestions for new features are welcome, it's important to limit the scope of NetBox's feature set to avoid
* 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:
* Ticket management
@@ -54,18 +39,14 @@ feature creep. For example, the following features would be firmly out of scope
* Acting as a DNS server
* Acting as an authentication server
* Before filing a new feature request, propose it on IRC or Reddit first. Feedback you receive there will help validate
and shape the proposed feature before filing a formal issue.
* If you're not sure whether the feature you want is a good fit for NetBox, please ask in #netbox on irc.freenode.net.
Even if it's not quite right for NetBox, we may be able to point you to a tool better suited for the job.
* Good feature requests are very narrowly defined. Be sure to enumerate specific functionality and data schema. The more
effort you put into writing a feature request, the better its chances are of being implemented. Overly broad feature
requests will be closed.
* When submitting a feature request, be sure to include the following:
* When submitting a feature request on GitHub, be sure to include the following:
* A detailed description of the proposed functionality
* A brief description of the functionality
* A use case for the feature; who would use it and what value it would add to NetBox
* A rough description of any changes necessary to the database schema
* A rough description of any changes necessary to the database schema (if applicable)
* Any third-party libraries or other resources which would be involved
## Submitting Pull Requests
@@ -74,8 +55,9 @@ requests will be closed.
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 the `develop` branch, rather than `master`. In NetBox,
the `develop` branch is used for ongoing development, while `master` is used for tagging new stable releases.
* 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
stable releases.
* All code submissions should meet the following criteria (CI will enforce these checks):

View File

@@ -4,8 +4,6 @@ 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
@@ -25,6 +23,6 @@ Questions? Comments? Please join us on IRC in **#netbox** on **irc.freenode.net*
# Installation
Please see [the documentation](http://netbox.readthedocs.io/en/latest/) for instructions on installing NetBox.
Please see docs/getting-started.md for instructions on installing NetBox.
To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases) and run `upgrade.sh`.

View File

@@ -2,7 +2,7 @@ NetBox's local configuration is held in `netbox/netbox/configuration.py`. An exa
## ALLOWED_HOSTS
This is a list of valid fully-qualified domain names (FQDNs) that is used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different (e.g. when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server). NetBox will not permit access to the server via any other hostnames (or IPs). The value of this option is also used to set `CSRF_TRUSTED_ORIGINS`, which restricts `HTTP POST` to the same set of hosts (more about this [here](https://docs.djangoproject.com/en/1.9/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS)). Keep in mind that NetBox, by default, has `USE_X_FORWARDED_HOST = True` (in `netbox/netbox/settings.py`) which means that if you're using a reverse proxy, it's the FQDN used to reach that reverse proxy which needs to be in this list (more about this [here](https://docs.djangoproject.com/en/1.9/ref/settings/#allowed-hosts)).
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:

View File

@@ -13,24 +13,11 @@ 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.
---
@@ -47,17 +34,9 @@ In order to send email, NetBox needs an email server configured. The following i
---
# ENFORCE_GLOBAL_UNIQUE
Default: False
Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table (all prefixes and IP addresses not assigned to a VRF), set `ENFORCE_GLOBAL_UNIQUE` to True.
---
## LOGIN_REQUIRED
Default: False
Default: False,
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.
@@ -87,14 +66,6 @@ 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

View File

@@ -50,4 +50,4 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
# Getting Started
See the [installation guide](installation/postgresql.md) for help getting NetBox up and running quickly.
See the [getting started](getting-started.md) guide for help with getting NetBox up and running quickly.

View File

@@ -112,9 +112,6 @@ 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):

View File

@@ -4,40 +4,42 @@ 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`.
Download and extract the latest 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`. For this guide we are using 1.0.4 as the old version and 1.0.7 as the new version.
Download & extract latest version:
```
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
# tar -xzf vX.Y.Z.tar.gz -C /opt
# cd /opt/
# ln -sf netbox-X.Y.Z/ netbox
# ln -sf netbox-1.0.7/ netbox
```
Copy the 'configuration.py' you created when first installing to the new version:
```
# 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
# cp /opt/netbox-1.0.4/configuration.py /opt/netbox/configuration.py
```
## Option B: Clone the Git Repository (latest master release)
This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
For this guide, we'll use `/opt/netbox`.
Check that your git branch is up to date & is set to master:
```
# cd /opt/netbox
# git checkout master
# git pull origin master
# git status
```
If not on branch master, set it and verify status:
```
# git checkout master
# git status
```
Pull down the set branch from git status above:
```
# git pull
```
# 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).

View File

@@ -1,40 +1,9 @@
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',

View File

@@ -3,7 +3,8 @@ from django.db.models import Count
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, Livesearch, SmallTextarea, SlugField,
APISelect, BootstrapMixin, BulkImportForm, CommentField, ConfirmationForm, CSVDataField, Livesearch, SmallTextarea,
SlugField,
)
from .models import Circuit, CircuitType, Provider
@@ -54,14 +55,8 @@ class ProviderBulkEditForm(forms.Form, BootstrapMixin):
comments = CommentField()
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}))
class ProviderBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
#
@@ -76,6 +71,10 @@ class CircuitTypeForm(forms.ModelForm, BootstrapMixin):
fields = ['name', 'slug']
class CircuitTypeBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=CircuitType.objects.all(), widget=forms.MultipleHiddenInput)
#
# Circuits
#
@@ -182,19 +181,23 @@ class CircuitBulkEditForm(forms.Form, BootstrapMixin):
comments = CommentField()
class CircuitBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
def circuit_type_choices():
type_choices = CircuitType.objects.annotate(circuit_count=Count('circuits'))
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in type_choices]
return [(t.slug, '{} ({})'.format(t.name, t.circuit_count)) for t in type_choices]
def circuit_provider_choices():
provider_choices = Provider.objects.annotate(circuit_count=Count('circuits'))
return [(p.slug, u'{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
return [(p.slug, '{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
def circuit_site_choices():
site_choices = Site.objects.annotate(circuit_count=Count('circuits'))
return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
return [(s.slug, '{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
class CircuitFilterForm(forms.Form, BootstrapMixin):

View File

@@ -1,21 +0,0 @@
# -*- 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'),
),
]

View File

@@ -1,7 +1,6 @@
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
@@ -13,7 +12,7 @@ class Provider(CreatedUpdatedModel):
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
asn = ASNField(blank=True, null=True, verbose_name='ASN')
asn = models.PositiveIntegerField(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')
@@ -80,7 +79,7 @@ class Circuit(CreatedUpdatedModel):
unique_together = ['provider', 'cid']
def __unicode__(self):
return u'{} {}'.format(self.provider, self.cid)
return "{0} {1}".format(self.provider, self.cid)
def get_absolute_url(self):
return reverse('circuits:circuit', args=[self.pk])

View File

@@ -16,8 +16,6 @@ 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'
@@ -76,6 +74,7 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_provider'
cls = Provider
form = forms.ProviderBulkDeleteForm
default_redirect_url = 'circuits:provider_list'
@@ -101,6 +100,7 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuittype'
cls = CircuitType
form = forms.CircuitTypeBulkDeleteForm
default_redirect_url = 'circuits:circuittype_list'
@@ -169,4 +169,5 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuit'
cls = Circuit
form = forms.CircuitBulkDeleteForm
default_redirect_url = 'circuits:circuit_list'

View File

@@ -1 +1 @@
default_app_config = 'dcim.apps.DCIMConfig'
default_app_config = 'dcim.apps.IPAMConfig'

View File

@@ -78,8 +78,8 @@ class DeviceTypeAdmin(admin.ModelAdmin):
InterfaceTemplateAdmin,
DeviceBayTemplateAdmin,
]
list_display = ['model', 'manufacturer', 'slug', 'part_number', 'u_height', 'console_ports', 'console_server_ports',
'power_ports', 'power_outlets', 'interfaces', 'device_bays']
list_display = ['model', 'manufacturer', 'slug', 'u_height', 'console_ports', 'console_server_ports', 'power_ports',
'power_outlets', 'interfaces', 'device_bays']
list_filter = ['manufacturer']
def get_queryset(self, request):
@@ -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_ip4', 'primary_ip6', 'rack')
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip', 'rack')

View File

@@ -38,7 +38,7 @@ class RackGroupSerializer(serializers.ModelSerializer):
fields = ['id', 'name', 'slug', 'site']
class RackGroupNestedSerializer(RackGroupSerializer):
class RackGroupNestedSerializer(SiteSerializer):
class Meta(SiteSerializer.Meta):
fields = ['id', 'name', 'slug']
@@ -111,8 +111,8 @@ class DeviceTypeSerializer(serializers.ModelSerializer):
class Meta:
model = DeviceType
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
'is_console_server', 'is_pdu', 'is_network_device']
fields = ['id', 'manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'is_network_device']
class DeviceTypeNestedSerializer(DeviceTypeSerializer):
@@ -164,9 +164,9 @@ class DeviceTypeDetailSerializer(DeviceTypeSerializer):
interface_templates = InterfaceTemplateNestedSerializer(many=True, read_only=True)
class Meta(DeviceTypeSerializer.Meta):
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
'is_console_server', 'is_pdu', 'is_network_device', 'console_port_templates', 'cs_port_templates',
'power_port_templates', 'power_outlet_templates', 'interface_templates']
fields = ['id', 'manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'is_network_device', 'console_port_templates', 'cs_port_templates', 'power_port_templates',
'power_outlet_templates', 'interface_templates']
#

View File

@@ -61,8 +61,7 @@ urlpatterns = [
url(r'^interfaces/(?P<pk>\d+)/$', InterfaceDetailView.as_view(), name='interface_detail'),
url(r'^interfaces/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_INTERFACE},
name='interface_graphs'),
url(r'^interface-connections/$', InterfaceConnectionListView.as_view(), name='interfaceconnection_list'),
url(r'^interface-connections/(?P<pk>\d+)/$', InterfaceConnectionView.as_view(), name='interfaceconnection_detail'),
url(r'^interface-connections/(?P<pk>\d+)/$', InterfaceConnectionView.as_view(), name='interfaceconnection'),
# Miscellaneous
url(r'^related-connections/$', RelatedConnectionsView.as_view(), name='related_connections'),

View File

@@ -326,14 +326,6 @@ class InterfaceConnectionView(generics.RetrieveUpdateDestroyAPIView):
queryset = InterfaceConnection.objects.all()
class InterfaceConnectionListView(generics.ListAPIView):
"""
Retrieve a list of all interface connections
"""
serializer_class = serializers.InterfaceConnectionSerializer
queryset = InterfaceConnection.objects.all()
#
# Device bays
#
@@ -419,36 +411,53 @@ class RelatedConnectionsView(APIView):
return Response()
else:
raise MissingFilterException(detail='Must specify search parameters "peer-device" and "peer-interface".')
raise MissingFilterException(detail='Must specify search parameters (peer-device and peer-interface).')
# Initialize response skeleton
response = {
'device': serializers.DeviceSerializer(device).data,
'console-ports': [],
'power-ports': [],
'interfaces': [],
}
response = dict()
response['device'] = serializers.DeviceSerializer(device).data
response['console-ports'] = []
response['power-ports'] = []
response['interfaces'] = []
# Console connections
# Build console connections
console_ports = ConsolePort.objects.filter(device=device).select_related('cs_port__device')
for cp in console_ports:
data = serializers.ConsolePortSerializer(instance=cp).data
del(data['device'])
response['console-ports'].append(data)
cp_info = dict()
cp_info['name'] = cp.name
if cp.cs_port:
cp_info['console-server'] = cp.cs_port.device.name
cp_info['port'] = cp.cs_port.name
else:
cp_info['console-server'] = None
cp_info['port'] = None
response['console-ports'].append(cp_info)
# Power connections
# Build power connections
power_ports = PowerPort.objects.filter(device=device).select_related('power_outlet__device')
for pp in power_ports:
data = serializers.PowerPortSerializer(instance=pp).data
del(data['device'])
response['power-ports'].append(data)
pp_info = dict()
pp_info['name'] = pp.name
if pp.power_outlet:
pp_info['pdu'] = pp.power_outlet.device.name
pp_info['outlet'] = pp.power_outlet.name
else:
pp_info['pdu'] = None
pp_info['outlet'] = None
response['power-ports'].append(pp_info)
# Interface connections
interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b',
'circuit')
# Built interface connections
interfaces = Interface.objects.filter(device=device)
for iface in interfaces:
data = serializers.InterfaceDetailSerializer(instance=iface).data
del(data['device'])
response['interfaces'].append(data)
iface_info = dict()
iface_info['name'] = iface.name
peer_interface = iface.get_connected_interface()
if peer_interface:
iface_info['device'] = peer_interface.device.name
iface_info['interface'] = peer_interface.name
else:
iface_info['device'] = None
iface_info['interface'] = None
response['interfaces'].append(iface_info)
return Response(response)

View File

@@ -1,6 +1,6 @@
from django.apps import AppConfig
class DCIMConfig(AppConfig):
class IPAMConfig(AppConfig):
name = "dcim"
verbose_name = "DCIM"

View File

@@ -1,20 +1,11 @@
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'

View File

@@ -102,7 +102,7 @@ class DeviceTypeFilter(django_filters.FilterSet):
class Meta:
model = DeviceType
fields = ['manufacturer_id', 'manufacturer', 'model', 'part_number', 'u_height', 'is_console_server', 'is_pdu',
fields = ['manufacturer_id', 'manufacturer', 'model', 'u_height', 'is_console_server', 'is_pdu',
'is_network_device']
@@ -122,11 +122,6 @@ 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(),

View File

@@ -5,7 +5,7 @@ from django.db.models import Count, Q
from ipam.models import IPAddress
from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
APISelect, BootstrapMixin, BulkImportForm, CommentField, ConfirmationForm, CSVDataField, ExpandableNameField,
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
)
@@ -85,9 +85,13 @@ class RackGroupForm(forms.ModelForm, BootstrapMixin):
fields = ['site', 'name', 'slug']
class RackGroupBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=RackGroup.objects.all(), widget=forms.MultipleHiddenInput)
def rackgroup_site_choices():
site_choices = Site.objects.annotate(rack_count=Count('rack_groups'))
return [(s.slug, u'{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
return [(s.slug, '{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
class RackGroupFilterForm(forms.Form, BootstrapMixin):
@@ -165,20 +169,24 @@ class RackBulkEditForm(forms.Form, BootstrapMixin):
comments = CommentField()
class RackBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
def rack_site_choices():
site_choices = Site.objects.annotate(rack_count=Count('racks'))
return [(s.slug, u'{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
return [(s.slug, '{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
def rack_group_choices():
group_choices = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
return [(g.pk, u'{} ({})'.format(g, g.rack_count)) for g in group_choices]
return [(g.pk, '{} ({})'.format(g, g.rack_count)) for g in 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, label='Rack Group',
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
@@ -194,6 +202,10 @@ class ManufacturerForm(forms.ModelForm, BootstrapMixin):
fields = ['name', 'slug']
class ManufacturerBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Manufacturer.objects.all(), widget=forms.MultipleHiddenInput)
#
# Device types
#
@@ -203,7 +215,7 @@ class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = DeviceType
fields = ['manufacturer', 'model', 'slug', 'part_number', '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', 'subdevice_role']
@@ -213,9 +225,13 @@ class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
u_height = forms.IntegerField(min_value=1, required=False)
class DeviceTypeBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
def devicetype_manufacturer_choices():
manufacturer_choices = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
return [(m.slug, u'{} ({})'.format(m.name, m.devicetype_count)) for m in manufacturer_choices]
return [(m.slug, '{} ({})'.format(m.name, m.devicetype_count)) for m in manufacturer_choices]
class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
@@ -287,6 +303,10 @@ class DeviceRoleForm(forms.ModelForm, BootstrapMixin):
fields = ['name', 'slug', 'color']
class DeviceRoleBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=DeviceRole.objects.all(), widget=forms.MultipleHiddenInput)
#
# Platforms
#
@@ -299,6 +319,10 @@ class PlatformForm(forms.ModelForm, BootstrapMixin):
fields = ['name', 'slug']
class PlatformBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Platform.objects.all(), widget=forms.MultipleHiddenInput)
#
# Devices
#
@@ -349,10 +373,10 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
for family in [4, 6]:
ip_choices = []
interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance)
ip_choices += [(ip.id, u'{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
.select_related('nat_inside__interface')
ip_choices += [(ip.id, u'{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices
else:
@@ -372,8 +396,8 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
self.fields['rack'].choices = []
# Rack position
pk = self.instance.pk if self.instance.pk else None
try:
pk = self.instance.pk if self.instance.pk else None
if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
position_choices = Rack.objects.get(pk=self.data['rack'])\
.get_rack_units(face=self.data.get('face'), exclude=pk)
@@ -401,13 +425,8 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
else:
self.fields['device_type'].choices = []
# Disable rack assignment if this is a child device installed in a parent device
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
self.fields['site'].disabled = True
self.fields['rack'].disabled = True
class BaseDeviceFromCSVForm(forms.ModelForm):
class DeviceFromCSVForm(forms.ModelForm):
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Invalid device role.'})
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
@@ -415,15 +434,23 @@ class BaseDeviceFromCSVForm(forms.ModelForm):
model_name = forms.CharField()
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid platform.'})
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
'invalid_choice': 'Invalid site name.',
})
rack_name = forms.CharField()
face = forms.CharField(required=False)
class Meta:
fields = []
model = Device
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'site', 'rack_name',
'position', 'face']
def clean(self):
manufacturer = self.cleaned_data.get('manufacturer')
model_name = self.cleaned_data.get('model_name')
site = self.cleaned_data.get('site')
rack_name = self.cleaned_data.get('rack_name')
# Validate device type
if manufacturer and model_name:
@@ -432,25 +459,6 @@ class BaseDeviceFromCSVForm(forms.ModelForm):
except DeviceType.DoesNotExist:
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
class DeviceFromCSVForm(BaseDeviceFromCSVForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
'invalid_choice': 'Invalid site name.',
})
rack_name = forms.CharField()
face = forms.CharField(required=False)
class Meta(BaseDeviceFromCSVForm.Meta):
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'site', 'rack_name',
'position', 'face']
def clean(self):
super(DeviceFromCSVForm, self).clean()
site = self.cleaned_data.get('site')
rack_name = self.cleaned_data.get('rack_name')
# Validate rack
if site and rack_name:
try:
@@ -460,54 +468,21 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):
def clean_face(self):
face = self.cleaned_data['face']
if not face:
return None
try:
return {
'front': 0,
'rear': 1,
}[face.lower()]
except KeyError:
raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
parent = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Parent device not found.'})
device_bay_name = forms.CharField(required=False)
class Meta(BaseDeviceFromCSVForm.Meta):
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'parent',
'device_bay_name']
def clean(self):
super(ChildDeviceFromCSVForm, self).clean()
parent = self.cleaned_data.get('parent')
device_bay_name = self.cleaned_data.get('device_bay_name')
# Validate device bay
if parent and device_bay_name:
if face:
try:
device_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
if device_bay.installed_device:
self.add_error('device_bay_name',
"Device bay ({} {}) is already occupied".format(parent, device_bay_name))
else:
self.instance.parent_bay = device_bay
except DeviceBay.DoesNotExist:
self.add_error('device_bay_name', "Parent device/bay ({} {}) not found".format(parent, device_bay_name))
return {
'front': 0,
'rear': 1,
}[face.lower()]
except KeyError:
raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
return face
class DeviceImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=DeviceFromCSVForm)
class ChildDeviceImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=ChildDeviceFromCSVForm)
class DeviceBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
@@ -518,36 +493,33 @@ class DeviceBulkEditForm(forms.Form, BootstrapMixin):
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
class DeviceBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
def device_site_choices():
site_choices = Site.objects.annotate(device_count=Count('racks__devices'))
return [(s.slug, u'{} ({})'.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, u'{} ({})'.format(g, g.device_count)) for g in group_choices]
return [(s.slug, '{} ({})'.format(s.name, s.device_count)) for s in site_choices]
def device_role_choices():
role_choices = DeviceRole.objects.annotate(device_count=Count('devices'))
return [(r.slug, u'{} ({})'.format(r.name, r.device_count)) for r in role_choices]
return [(r.slug, '{} ({})'.format(r.name, r.device_count)) for r in role_choices]
def device_type_choices():
type_choices = DeviceType.objects.select_related('manufacturer').annotate(device_count=Count('instances'))
return [(t.pk, u'{} ({})'.format(t, t.device_count)) for t in type_choices]
return [(t.pk, '{} ({})'.format(t, t.device_count)) for t in type_choices]
def device_platform_choices():
platform_choices = Platform.objects.annotate(device_count=Count('devices'))
return [(p.slug, u'{} ({})'.format(p.name, p.device_count)) for p in platform_choices]
return [(p.slug, '{} ({})'.format(p.name, p.device_count)) for p in 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',

View File

@@ -1,21 +0,0 @@
# -*- 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'),
),
]

View File

@@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-14 21:38
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0009_site_32bit_asn_support'),
]
operations = [
migrations.AlterField(
model_name='devicebay',
name='installed_device',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent_bay', to='dcim.Device'),
),
]

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-07-26 15:05
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0010_devicebay_installed_device_set_null'),
]
operations = [
migrations.AddField(
model_name='devicetype',
name='part_number',
field=models.CharField(blank=True, help_text=b'Discrete part number (optional)', max_length=50),
),
]

View File

@@ -1,7 +1,6 @@
from collections import OrderedDict
from django.conf import settings
from django.core.exceptions import MultipleObjectsReturned, ValidationError
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.core.validators import MinValueValidator
from django.db import models
@@ -9,11 +8,9 @@ from django.db.models import Count, Q, ObjectDoesNotExist
from extras.rpc import RPC_CLIENTS
from utilities.fields import NullableCharField
from utilities.managers import NaturalOrderByManager
from utilities.models import CreatedUpdatedModel
from .fields import ASNField, MACAddressField
from .fields import MACAddressField
RACK_FACE_FRONT = 0
RACK_FACE_REAR = 1
@@ -139,12 +136,6 @@ def order_interfaces(queryset, sql_col, primary_ordering=tuple()):
}).order_by(*ordering)
class SiteManager(NaturalOrderByManager):
def get_queryset(self):
return self.natural_order_by('name')
class Site(CreatedUpdatedModel):
"""
A Site represents a geographic location within a network; typically a building or campus. The optional facility
@@ -153,13 +144,11 @@ 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 = ASNField(blank=True, null=True, verbose_name='ASN')
asn = models.PositiveIntegerField(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)
objects = SiteManager()
class Meta:
ordering = ['name']
@@ -216,18 +205,12 @@ class RackGroup(models.Model):
]
def __unicode__(self):
return u'{} - {}'.format(self.site.name, self.name)
return '{} - {}'.format(self.site.name, self.name)
def get_absolute_url(self):
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
class RackManager(NaturalOrderByManager):
def get_queryset(self):
return self.natural_order_by('site__name', 'name')
class Rack(CreatedUpdatedModel):
"""
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
@@ -240,8 +223,6 @@ class Rack(CreatedUpdatedModel):
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)')
comments = models.TextField(blank=True)
objects = RackManager()
class Meta:
ordering = ['site', 'name']
unique_together = [
@@ -360,15 +341,6 @@ class Rack(CreatedUpdatedModel):
def get_0u_devices(self):
return self.devices.filter(position=0)
def get_utilization(self):
"""
Determine the utilization rate of the rack and return it as a percentage.
"""
if self.u_consumed is None:
self.u_consumed = 0
u_available = self.u_height - self.u_consumed
return int(float(self.u_height - u_available) / self.u_height * 100)
#
# Device Types
@@ -409,7 +381,6 @@ class DeviceType(models.Model):
manufacturer = models.ForeignKey('Manufacturer', related_name='device_types', on_delete=models.PROTECT)
model = models.CharField(max_length=50)
slug = models.SlugField()
part_number = models.CharField(max_length=50, blank=True, help_text="Discrete part number (optional)")
u_height = models.PositiveSmallIntegerField(verbose_name='Height (U)', default=1)
is_full_depth = models.BooleanField(default=True, verbose_name="Is full depth",
help_text="Device consumes both front and rear rack faces")
@@ -432,7 +403,7 @@ class DeviceType(models.Model):
]
def __unicode__(self):
return u'{} {}'.format(self.manufacturer, self.model)
return "{} {}".format(self.manufacturer, self.model)
def get_absolute_url(self):
return reverse('dcim:devicetype', args=[self.pk])
@@ -611,12 +582,6 @@ class Platform(models.Model):
return "{}?platform={}".format(reverse('dcim:device_list'), self.slug)
class DeviceManager(NaturalOrderByManager):
def get_queryset(self):
return self.natural_order_by('name')
class Device(CreatedUpdatedModel):
"""
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
@@ -646,8 +611,6 @@ class Device(CreatedUpdatedModel):
blank=True, null=True, verbose_name='Primary IPv6')
comments = models.TextField(blank=True)
objects = DeviceManager()
class Meta:
ordering = ['name']
unique_together = ['rack', 'position', 'face']
@@ -660,10 +623,6 @@ class Device(CreatedUpdatedModel):
def clean(self):
# Validate device type assignment
if not hasattr(self, 'device_type'):
raise ValidationError("Must specify device type.")
# 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.")
@@ -673,7 +632,10 @@ class Device(CreatedUpdatedModel):
raise ValidationError("Must specify rack face with rack position.")
# Validate rack space
rack_face = self.face if not self.device_type.is_full_depth else None
try:
rack_face = self.face if not self.device_type.is_full_depth else None
except DeviceType.DoesNotExist:
raise ValidationError("Must specify device type.")
exclude_list = [self.pk] if self.pk else []
try:
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
@@ -717,9 +679,6 @@ class Device(CreatedUpdatedModel):
self.device_type.device_bay_templates.all()]
)
# Update Rack assignment for any child Devices
Device.objects.filter(parent_bay__device=self).update(rack=self.rack)
def to_csv(self):
return ','.join([
self.name or '',
@@ -754,9 +713,7 @@ class Device(CreatedUpdatedModel):
@property
def primary_ip(self):
if settings.PREFER_IPV4 and self.primary_ip4:
return self.primary_ip4
elif self.primary_ip6:
if self.primary_ip6:
return self.primary_ip6
elif self.primary_ip4:
return self.primary_ip4
@@ -958,8 +915,8 @@ class Interface(models.Model):
return connection.interface_a
except InterfaceConnection.DoesNotExist:
return None
except InterfaceConnection.MultipleObjectsReturned:
raise MultipleObjectsReturned("Multiple connections found for {} interface {}!".format(self.device, self))
except InterfaceConnection.MultipleObjectsReturned as e:
raise e("Multiple connections found for {0} interface {1}!".format(self.device, self))
class InterfaceConnection(models.Model):
@@ -993,15 +950,14 @@ class DeviceBay(models.Model):
"""
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', on_delete=models.SET_NULL, blank=True,
null=True)
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 u'{} - {}'.format(self.device.name, self.name)
return '{} - {}'.format(self.device.name, self.name)
def clean(self):

View File

@@ -48,11 +48,6 @@ STATUS_ICON = """
{% endif %}
"""
UTILIZATION_GRAPH = """
{% load helpers %}
{% utilization_graph record.get_utilization %}
"""
#
# Sites
@@ -103,24 +98,10 @@ class RackTable(BaseTable):
facility_id = tables.Column(verbose_name='Facility ID')
u_height = tables.Column(verbose_name='Height (U)')
devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
u_consumed = tables.Column(accessor=Accessor('u_consumed'), verbose_name='Used (U)')
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
class Meta(BaseTable.Meta):
model = Rack
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'u_height', 'devices', 'u_consumed', 'utilization')
class RackImportTable(BaseTable):
name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
facility_id = tables.Column(verbose_name='Facility ID')
u_height = tables.Column(verbose_name='Height (U)')
class Meta(BaseTable.Meta):
model = Rack
fields = ('site', 'group', 'name', 'facility_id', 'u_height')
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'u_height', 'devices')
#
@@ -145,77 +126,93 @@ class ManufacturerTable(BaseTable):
class DeviceTypeTable(BaseTable):
pk = ToggleColumn()
manufacturer = tables.Column(verbose_name='Manufacturer')
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
part_number = tables.Column(verbose_name='Part Number')
class Meta(BaseTable.Meta):
model = DeviceType
fields = ('pk', 'model', 'manufacturer', 'part_number', 'u_height')
fields = ('pk', 'model', 'manufacturer', 'u_height')
#
# Device type components
#
class ConsolePortTemplateTable(BaseTable):
class ConsolePortTemplateTable(tables.Table):
pk = ToggleColumn()
class Meta(BaseTable.Meta):
class Meta:
model = ConsolePortTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
attrs = {
'class': 'table table-hover',
}
class ConsoleServerPortTemplateTable(BaseTable):
class ConsoleServerPortTemplateTable(tables.Table):
pk = ToggleColumn()
class Meta(BaseTable.Meta):
class Meta:
model = ConsoleServerPortTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
attrs = {
'class': 'table table-hover',
}
class PowerPortTemplateTable(BaseTable):
class PowerPortTemplateTable(tables.Table):
pk = ToggleColumn()
class Meta(BaseTable.Meta):
class Meta:
model = PowerPortTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
attrs = {
'class': 'table table-hover',
}
class PowerOutletTemplateTable(BaseTable):
class PowerOutletTemplateTable(tables.Table):
pk = ToggleColumn()
class Meta(BaseTable.Meta):
class Meta:
model = PowerOutletTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
attrs = {
'class': 'table table-hover',
}
class InterfaceTemplateTable(BaseTable):
class InterfaceTemplateTable(tables.Table):
pk = ToggleColumn()
class Meta(BaseTable.Meta):
class Meta:
model = InterfaceTemplate
fields = ('pk', 'name', 'form_factor')
fields = ('pk', 'name')
empty_text = "None"
show_header = False
attrs = {
'class': 'table table-hover panel-body',
}
class DeviceBayTemplateTable(BaseTable):
class DeviceBayTemplateTable(tables.Table):
pk = ToggleColumn()
class Meta(BaseTable.Meta):
class Meta:
model = DeviceBayTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
attrs = {
'class': 'table table-hover panel-body',
}
#

View File

@@ -204,7 +204,6 @@ class DeviceTypeTest(APITestCase):
'manufacturer',
'model',
'slug',
'part_number',
'u_height',
'is_full_depth',
'is_console_server',

View File

@@ -50,29 +50,31 @@ urlpatterns = [
url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
# Console port templates
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateAddView.as_view(), name='devicetype_add_consoleport'),
url(r'^device-types/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'),
# Console server port templates
url(r'^device-types/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateAddView.as_view(), name='devicetype_add_consoleserverport'),
url(r'^device-types/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'),
# Power port templates
url(r'^device-types/(?P<pk>\d+)/power-ports/add/$', views.PowerPortTemplateAddView.as_view(), name='devicetype_add_powerport'),
url(r'^device-types/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'),
# Power outlet templates
url(r'^device-types/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletTemplateAddView.as_view(), name='devicetype_add_poweroutlet'),
url(r'^device-types/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'),
# Interface templates
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(), name='devicetype_add_interface'),
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
# Device bay templates
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.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
# Component templates
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateAddView.as_view(),
name='devicetype_add_consoleport'),
url(r'^device-types/(?P<pk>\d+)/console-ports/delete/$', views.component_template_delete,
{'model': ConsolePortTemplate}, name='devicetype_delete_consoleport'),
url(r'^device-types/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateAddView.as_view(),
name='devicetype_add_consoleserverport'),
url(r'^device-types/(?P<pk>\d+)/console-server-ports/delete/$', views.component_template_delete,
{'model': ConsoleServerPortTemplate}, name='devicetype_delete_consoleserverport'),
url(r'^device-types/(?P<pk>\d+)/power-ports/add/$', views.PowerPortTemplateAddView.as_view(),
name='devicetype_add_powerport'),
url(r'^device-types/(?P<pk>\d+)/power-ports/delete/$', views.component_template_delete,
{'model': PowerPortTemplate}, name='devicetype_delete_powerport'),
url(r'^device-types/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletTemplateAddView.as_view(),
name='devicetype_add_poweroutlet'),
url(r'^device-types/(?P<pk>\d+)/power-outlets/delete/$', views.component_template_delete,
{'model': PowerOutletTemplate}, name='devicetype_delete_poweroutlet'),
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(),
name='devicetype_add_interface'),
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.component_template_delete,
{'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
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
@@ -90,7 +92,6 @@ urlpatterns = [
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
url(r'^devices/add/$', views.DeviceEditView.as_view(), name='device_add'),
url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'),
url(r'^devices/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
url(r'^devices/(?P<pk>\d+)/$', views.device, name='device'),
@@ -103,7 +104,6 @@ urlpatterns = [
# Console ports
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.consoleport_add, name='consoleport_add'),
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
url(r'^console-ports/(?P<pk>\d+)/connect/$', views.consoleport_connect, name='consoleport_connect'),
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.consoleport_edit, name='consoleport_edit'),
@@ -111,7 +111,6 @@ urlpatterns = [
# Console server ports
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.consoleserverport_add, name='consoleserverport_add'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.consoleserverport_edit, name='consoleserverport_edit'),
@@ -119,7 +118,6 @@ urlpatterns = [
# Power ports
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.powerport_add, name='powerport_add'),
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
url(r'^power-ports/(?P<pk>\d+)/connect/$', views.powerport_connect, name='powerport_connect'),
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.powerport_edit, name='powerport_edit'),
@@ -127,7 +125,6 @@ urlpatterns = [
# Power outlets
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.poweroutlet_add, name='poweroutlet_add'),
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'),
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.poweroutlet_edit, name='poweroutlet_edit'),
@@ -135,7 +132,6 @@ urlpatterns = [
# Device bays
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.devicebay_add, name='devicebay_add'),
url(r'^devices/(?P<pk>\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
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'),
@@ -150,9 +146,8 @@ urlpatterns = [
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
# Interfaces
url(r'^devices/interfaces/add/$', views.InterfaceBulkAddView.as_view(), name='interface_add_multi'),
url(r'^devices/interfaces/add/$', views.InterfaceBulkAddView.as_view(), name='interface_bulk_add'),
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.interface_add, name='interface_add'),
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.interface_edit, name='interface_edit'),

View File

@@ -1,13 +1,12 @@
import re
from natsort import natsorted
from operator import attrgetter
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db.models import Count, Sum
from django.db.models import Count, ProtectedError
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.http import urlencode
@@ -16,6 +15,7 @@ from django.views.generic import View
from ipam.models import Prefix, IPAddress, VLAN
from circuits.models import Circuit
from extras.models import TopologyMap
from utilities.error_handlers import handle_protectederror
from utilities.forms import ConfirmationForm
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
@@ -133,6 +133,7 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackgroup'
cls = RackGroup
form = forms.RackGroupBulkDeleteForm
default_redirect_url = 'dcim:rackgroup_list'
@@ -141,7 +142,7 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class RackListView(ObjectListView):
queryset = Rack.objects.select_related('site').prefetch_related('devices__device_type').annotate(device_count=Count('devices', distinct=True), u_consumed=Sum('devices__device_type__u_height'))
queryset = Rack.objects.select_related('site').annotate(device_count=Count('devices', distinct=True))
filter = filters.RackFilter
filter_form = forms.RackFilterForm
table = tables.RackTable
@@ -185,7 +186,7 @@ class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_rack'
form = forms.RackImportForm
table = tables.RackImportTable
table = tables.RackTable
template_name = 'dcim/rack_import.html'
obj_list_url = 'dcim:rack_list'
@@ -210,6 +211,7 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rack'
cls = Rack
form = forms.RackBulkDeleteForm
default_redirect_url = 'dcim:rack_list'
@@ -235,6 +237,7 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_manufacturer'
cls = Manufacturer
form = forms.ManufacturerBulkDeleteForm
default_redirect_url = 'dcim:manufacturer_list'
@@ -256,31 +259,18 @@ def devicetype(request, pk):
devicetype = get_object_or_404(DeviceType, pk=pk)
# Component tables
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'))
)
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))
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
@@ -290,7 +280,6 @@ 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,
})
@@ -329,6 +318,7 @@ class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicetype'
cls = DeviceType
form = forms.DeviceTypeBulkDeleteForm
default_redirect_url = 'dcim:devicetype_list'
@@ -347,7 +337,7 @@ class ComponentTemplateCreateView(View):
return render(request, 'dcim/component_template_add.html', {
'devicetype': devicetype,
'component_type': self.model._meta.verbose_name,
'form': self.form(initial=request.GET),
'form': self.form(),
'cancel_url': reverse('dcim:devicetype', kwargs={'pk': devicetype.pk}),
})
@@ -390,65 +380,68 @@ class ConsolePortTemplateAddView(ComponentTemplateCreateView):
form = forms.ConsolePortTemplateForm
class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleporttemplate'
cls = ConsolePortTemplate
parent_cls = DeviceType
class ConsoleServerPortTemplateAddView(ComponentTemplateCreateView):
model = ConsoleServerPortTemplate
form = forms.ConsoleServerPortTemplateForm
class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverporttemplate'
cls = ConsoleServerPortTemplate
parent_cls = DeviceType
class PowerPortTemplateAddView(ComponentTemplateCreateView):
model = PowerPortTemplate
form = forms.PowerPortTemplateForm
class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerporttemplate'
cls = PowerPortTemplate
parent_cls = DeviceType
class PowerOutletTemplateAddView(ComponentTemplateCreateView):
model = PowerOutletTemplate
form = forms.PowerOutletTemplateForm
class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlettemplate'
cls = PowerOutletTemplate
parent_cls = DeviceType
class InterfaceTemplateAddView(ComponentTemplateCreateView):
model = InterfaceTemplate
form = forms.InterfaceTemplateForm
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_interfacetemplate'
cls = InterfaceTemplate
parent_cls = DeviceType
class DeviceBayTemplateAddView(ComponentTemplateCreateView):
model = DeviceBayTemplate
form = forms.DeviceBayTemplateForm
class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebaytemplate'
cls = DeviceBayTemplate
parent_cls = DeviceType
def component_template_delete(request, pk, model):
devicetype = get_object_or_404(DeviceType, pk=pk)
class ComponentTemplateBulkDeleteForm(ConfirmationForm):
pk = ModelMultipleChoiceField(queryset=model.objects.all(), widget=MultipleHiddenInput)
if '_confirm' in request.POST:
form = ComponentTemplateBulkDeleteForm(request.POST)
if form.is_valid():
# Delete component templates
objects_to_delete = model.objects.filter(pk__in=[v.id for v in form.cleaned_data['pk']])
try:
deleted_count = objects_to_delete.count()
objects_to_delete.delete()
except ProtectedError, e:
handle_protectederror(list(objects_to_delete), request, e)
return redirect('dcim:devicetype', {'pk': devicetype.pk})
messages.success(request, "Deleted {} {}".format(deleted_count, model._meta.verbose_name_plural))
return redirect('dcim:devicetype', pk=devicetype.pk)
else:
form = ComponentTemplateBulkDeleteForm(initial={'pk': request.POST.getlist('pk')})
selected_objects = model.objects.filter(pk__in=request.POST.getlist('pk'))
if not selected_objects:
messages.warning(request, "No {} were selected for deletion.".format(model._meta.verbose_name_plural))
return redirect('dcim:devicetype', pk=devicetype.pk)
return render(request, 'dcim/component_template_delete.html', {
'devicetype': devicetype,
'form': form,
'selected_objects': selected_objects,
'cancel_url': reverse('dcim:devicetype', kwargs={'pk': devicetype.pk}),
})
#
@@ -473,6 +466,7 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicerole'
cls = DeviceRole
form = forms.DeviceRoleBulkDeleteForm
default_redirect_url = 'dcim:devicerole_list'
@@ -498,6 +492,7 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_platform'
cls = Platform
form = forms.PlatformBulkDeleteForm
default_redirect_url = 'dcim:platform_list'
@@ -518,26 +513,15 @@ class DeviceListView(ObjectListView):
def device(request, pk):
device = get_object_or_404(Device, pk=pk)
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')
)
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')
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 = natsorted(
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
key=attrgetter('name')
)
device_bays = DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer')
# Gather any secrets which belong to this device
secrets = device.secrets.all()
@@ -598,23 +582,6 @@ class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
obj_list_url = 'dcim:device_list'
class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_device'
form = forms.ChildDeviceImportForm
table = tables.DeviceImportTable
template_name = 'dcim/device_import_child.html'
obj_list_url = 'dcim:device_list'
def save_obj(self, obj):
# Inherent rack from parent device
obj.rack = obj.parent_bay.device.rack
obj.save()
# Save the reverse relation
device_bay = obj.parent_bay
device_bay.installed_device = obj
device_bay.save()
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_device'
cls = Device
@@ -642,6 +609,7 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_device'
cls = Device
form = forms.DeviceBulkDeleteForm
default_redirect_url = 'dcim:device_list'
@@ -813,12 +781,6 @@ def consoleport_delete(request, pk):
})
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleport'
cls = ConsolePort
parent_cls = Device
class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.change_consoleport'
form = forms.ConsoleConnectionImportForm
@@ -974,12 +936,6 @@ def consoleserverport_delete(request, pk):
})
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverport'
cls = ConsoleServerPort
parent_cls = Device
#
# Power ports
#
@@ -1125,12 +1081,6 @@ def powerport_delete(request, pk):
})
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerport'
cls = PowerPort
parent_cls = Device
class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.change_powerport'
form = forms.PowerConnectionImportForm
@@ -1284,12 +1234,6 @@ def poweroutlet_delete(request, pk):
})
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlet'
cls = PowerOutlet
parent_cls = Device
#
# Interfaces
#
@@ -1384,7 +1328,7 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.add_interface'
cls = Device
form = forms.InterfaceBulkCreateForm
template_name = 'dcim/interface_add_multi.html'
template_name = 'dcim/interface_bulk_add.html'
default_redirect_url = 'dcim:device_list'
def update_objects(self, pk_list, form):
@@ -1413,12 +1357,6 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
len(selected_devices)))
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_interface'
cls = Interface
parent_cls = Device
#
# Device bays
#
@@ -1556,12 +1494,6 @@ def devicebay_depopulate(request, pk):
})
class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebay'
cls = DeviceBay
parent_cls = Device
#
# Interface connections
#

View File

@@ -77,7 +77,7 @@ class ExportTemplate(models.Model):
]
def __unicode__(self):
return u'{}: {}'.format(self.content_type, self.name)
return "{}: {}".format(self.content_type, self.name)
def to_response(self, context_dict, filename):
"""
@@ -176,8 +176,8 @@ class UserAction(models.Model):
def __unicode__(self):
if self.message:
return u'{} {}'.format(self.user, self.message)
return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
return ' '.join([self.user, self.message])
return ' '.join([self.user, self.get_action_display(), self.content_type])
def icon(self):
if self.action in [ACTION_CREATE, ACTION_IMPORT]:

View File

@@ -1,7 +1,7 @@
from django.contrib import admin
from .models import (
Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF,
Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VRF,
)
@@ -57,14 +57,6 @@ class IPAddressAdmin(admin.ModelAdmin):
return qs.select_related('vrf', 'nat_inside')
@admin.register(VLANGroup)
class VLANGroupAdmin(admin.ModelAdmin):
list_display = ['name', 'site', 'slug']
prepopulated_fields = {
'slug': ['name'],
}
@admin.register(VLAN)
class VLANAdmin(admin.ModelAdmin):
list_display = ['site', 'vid', 'name', 'status', 'role']

View File

@@ -1,7 +1,7 @@
from rest_framework import serializers
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN
#
@@ -12,7 +12,7 @@ class VRFSerializer(serializers.ModelSerializer):
class Meta:
model = VRF
fields = ['id', 'name', 'rd', 'enforce_unique', 'description']
fields = ['id', 'name', 'rd', 'description']
class VRFNestedSerializer(VRFSerializer):
@@ -73,36 +73,17 @@ class AggregateNestedSerializer(AggregateSerializer):
fields = ['id', 'family', 'prefix']
#
# VLAN groups
#
class VLANGroupSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
class Meta:
model = VLANGroup
fields = ['id', 'name', 'slug', 'site']
class VLANGroupNestedSerializer(VLANGroupSerializer):
class Meta(VLANGroupSerializer.Meta):
fields = ['id', 'name', 'slug']
#
# VLANs
#
class VLANSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
group = VLANGroupNestedSerializer()
role = RoleNestedSerializer()
class Meta:
model = VLAN
fields = ['id', 'site', 'group', 'vid', 'name', 'status', 'role', 'description', 'display_name']
fields = ['id', 'site', 'vid', 'name', 'status', 'role', 'display_name']
class VLANNestedSerializer(VLANSerializer):

View File

@@ -29,10 +29,6 @@ urlpatterns = [
url(r'^ip-addresses/$', IPAddressListView.as_view(), name='ipaddress_list'),
url(r'^ip-addresses/(?P<pk>\d+)/$', IPAddressDetailView.as_view(), name='ipaddress_detail'),
# VLAN groups
url(r'^vlan-groups/$', VLANGroupListView.as_view(), name='vlangroup_list'),
url(r'^vlan-groups/(?P<pk>\d+)/$', VLANGroupDetailView.as_view(), name='vlangroup_detail'),
# VLANs
url(r'^vlans/$', VLANListView.as_view(), name='vlan_list'),
url(r'^vlans/(?P<pk>\d+)/$', VLANDetailView.as_view(), name='vlan_detail'),

View File

@@ -1,22 +1,18 @@
from rest_framework import generics
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
from ipam import filters
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN
from ipam.filters import AggregateFilter, PrefixFilter, IPAddressFilter, VLANFilter, VRFFilter
from . import serializers
#
# VRFs
#
class VRFListView(generics.ListAPIView):
"""
List all VRFs
"""
queryset = VRF.objects.all()
serializer_class = serializers.VRFSerializer
filter_class = filters.VRFFilter
filter_class = VRFFilter
class VRFDetailView(generics.RetrieveAPIView):
@@ -27,10 +23,6 @@ class VRFDetailView(generics.RetrieveAPIView):
serializer_class = serializers.VRFSerializer
#
# Roles
#
class RoleListView(generics.ListAPIView):
"""
List all roles
@@ -47,10 +39,6 @@ class RoleDetailView(generics.RetrieveAPIView):
serializer_class = serializers.RoleSerializer
#
# RIRs
#
class RIRListView(generics.ListAPIView):
"""
List all RIRs
@@ -67,17 +55,13 @@ class RIRDetailView(generics.RetrieveAPIView):
serializer_class = serializers.RIRSerializer
#
# Aggregates
#
class AggregateListView(generics.ListAPIView):
"""
List aggregates (filterable)
"""
queryset = Aggregate.objects.select_related('rir')
serializer_class = serializers.AggregateSerializer
filter_class = filters.AggregateFilter
filter_class = AggregateFilter
class AggregateDetailView(generics.RetrieveAPIView):
@@ -88,17 +72,13 @@ class AggregateDetailView(generics.RetrieveAPIView):
serializer_class = serializers.AggregateSerializer
#
# Prefixes
#
class PrefixListView(generics.ListAPIView):
"""
List prefixes (filterable)
"""
queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role')
serializer_class = serializers.PrefixSerializer
filter_class = filters.PrefixFilter
filter_class = PrefixFilter
class PrefixDetailView(generics.RetrieveAPIView):
@@ -109,10 +89,6 @@ class PrefixDetailView(generics.RetrieveAPIView):
serializer_class = serializers.PrefixSerializer
#
# IP addresses
#
class IPAddressListView(generics.ListAPIView):
"""
List IP addresses (filterable)
@@ -120,7 +96,7 @@ class IPAddressListView(generics.ListAPIView):
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\
.prefetch_related('nat_outside')
serializer_class = serializers.IPAddressSerializer
filter_class = filters.IPAddressFilter
filter_class = IPAddressFilter
class IPAddressDetailView(generics.RetrieveAPIView):
@@ -132,38 +108,13 @@ class IPAddressDetailView(generics.RetrieveAPIView):
serializer_class = serializers.IPAddressSerializer
#
# VLAN groups
#
class VLANGroupListView(generics.ListAPIView):
"""
List all VLAN groups
"""
queryset = VLANGroup.objects.all()
serializer_class = serializers.VLANGroupSerializer
filter_class = filters.VLANGroupFilter
class VLANGroupDetailView(generics.RetrieveAPIView):
"""
Retrieve a single VLAN group
"""
queryset = VLANGroup.objects.all()
serializer_class = serializers.VLANGroupSerializer
#
# VLANs
#
class VLANListView(generics.ListAPIView):
"""
List VLANs (filterable)
"""
queryset = VLAN.objects.select_related('site', 'role')
serializer_class = serializers.VLANSerializer
filter_class = filters.VLANFilter
filter_class = VLANFilter
class VLANDetailView(generics.RetrieveAPIView):

View File

@@ -4,7 +4,7 @@ from netaddr.core import AddrFormatError
from dcim.models import Site, Device, Interface
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, Role
class VRFFilter(django_filters.FilterSet):
@@ -176,24 +176,6 @@ class IPAddressFilter(django_filters.FilterSet):
return queryset.filter(vrf__pk=value)
class VLANGroupFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
class Meta:
model = VLANGroup
fields = ['site_id', 'site']
class VLANFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
@@ -206,17 +188,6 @@ class VLANFilter(django_filters.FilterSet):
to_field_name='slug',
label='Site (slug)',
)
group_id = django_filters.ModelMultipleChoiceFilter(
name='group',
queryset=VLANGroup.objects.all(),
label='Group (ID)',
)
group = django_filters.ModelMultipleChoiceFilter(
name='group',
queryset=VLANGroup.objects.all(),
to_field_name='slug',
label='Group',
)
name = django_filters.CharFilter(
name='name',
lookup_type='icontains',

View File

@@ -4,10 +4,12 @@ from django import forms
from django.db.models import Count
from dcim.models import Site, Device, Interface
from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField
from utilities.forms import (
BootstrapMixin, ConfirmationForm, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField,
)
from .models import (
Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLAN_STATUS_CHOICES, VRF,
)
@@ -23,7 +25,7 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = VRF
fields = ['name', 'rd', 'enforce_unique', 'description']
fields = ['name', 'rd', 'description']
labels = {
'rd': "RD",
}
@@ -36,7 +38,7 @@ class VRFFromCSVForm(forms.ModelForm):
class Meta:
model = VRF
fields = ['name', 'rd', 'enforce_unique', 'description']
fields = ['name', 'rd', 'description']
class VRFImportForm(BulkImportForm, BootstrapMixin):
@@ -48,6 +50,10 @@ class VRFBulkEditForm(forms.Form, BootstrapMixin):
description = forms.CharField(max_length=100, required=False)
class VRFBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
#
# RIRs
#
@@ -60,6 +66,10 @@ class RIRForm(forms.ModelForm, BootstrapMixin):
fields = ['name', 'slug']
class RIRBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=RIR.objects.all(), widget=forms.MultipleHiddenInput)
#
# Aggregates
#
@@ -93,12 +103,16 @@ class AggregateBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
date_added = forms.DateField(required=False)
description = forms.CharField(max_length=100, required=False)
description = forms.CharField(max_length=50, required=False)
class AggregateBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
def aggregate_rir_choices():
rir_choices = RIR.objects.annotate(aggregate_count=Count('aggregates'))
return [(r.slug, u'{} ({})'.format(r.name, r.aggregate_count)) for r in rir_choices]
return [(r.slug, '{} ({})'.format(r.name, r.aggregate_count)) for r in rir_choices]
class AggregateFilterForm(forms.Form, BootstrapMixin):
@@ -118,6 +132,10 @@ class RoleForm(forms.ModelForm, BootstrapMixin):
fields = ['name', 'slug']
class RoleBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Role.objects.all(), widget=forms.MultipleHiddenInput)
#
# Prefixes
#
@@ -174,43 +192,13 @@ class PrefixFromCSVForm(forms.ModelForm):
error_messages={'invalid_choice': 'VRF not found.'})
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Site not found.'})
vlan_group_name = forms.CharField(required=False)
vlan_vid = forms.IntegerField(required=False)
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in PREFIX_STATUS_CHOICES])
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid role.'})
class Meta:
model = Prefix
fields = ['prefix', 'vrf', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', 'description']
def clean(self):
super(PrefixFromCSVForm, self).clean()
site = self.cleaned_data.get('site')
vlan_group_name = self.cleaned_data.get('vlan_group_name')
vlan_vid = self.cleaned_data.get('vlan_vid')
# Validate VLAN
vlan_group = None
if vlan_group_name:
try:
vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name)
except VLANGroup.DoesNotExist:
self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name))
if vlan_vid and vlan_group:
try:
self.instance.vlan = VLAN.objects.get(group=vlan_group, vid=vlan_vid)
except VLAN.DoesNotExist:
self.add_error('vlan_vid', "Invalid VLAN ID ({} - {}).".format(vlan_group, vlan_vid))
elif vlan_vid and site:
try:
self.instance.vlan = VLAN.objects.get(site=site, vid=vlan_vid)
except VLAN.MultipleObjectsReturned:
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
elif vlan_vid:
self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.")
fields = ['prefix', 'vrf', 'site', 'status_name', 'role', 'description']
def save(self, *args, **kwargs):
m = super(PrefixFromCSVForm, self).save(commit=False)
@@ -233,7 +221,11 @@ class PrefixBulkEditForm(forms.Form, BootstrapMixin):
vrf_global = forms.BooleanField(required=False, label='Set VRF to global')
status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False)
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
description = forms.CharField(max_length=100, required=False)
description = forms.CharField(max_length=50, required=False)
class PrefixBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
def prefix_vrf_choices():
@@ -244,19 +236,19 @@ def prefix_vrf_choices():
def prefix_site_choices():
site_choices = Site.objects.annotate(prefix_count=Count('prefixes'))
return [(s.slug, u'{} ({})'.format(s.name, s.prefix_count)) for s in site_choices]
return [(s.slug, '{} ({})'.format(s.name, s.prefix_count)) for s in site_choices]
def prefix_status_choices():
status_counts = {}
for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
status_counts[status['status']] = status['count']
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
def prefix_role_choices():
role_choices = Role.objects.annotate(prefix_count=Count('prefixes'))
return [(r.slug, u'{} ({})'.format(r.name, r.prefix_count)) for r in role_choices]
return [(r.slug, '{} ({})'.format(r.name, r.prefix_count)) for r in role_choices]
class PrefixFilterForm(forms.Form, BootstrapMixin):
@@ -376,9 +368,9 @@ class IPAddressFromCSVForm(forms.ModelForm):
name=self.cleaned_data['interface_name'])
# Set as primary for device
if self.cleaned_data['is_primary']:
if self.instance.address.version == 4:
if self.instance.family == 4:
self.instance.primary_ip4_for = self.cleaned_data['device']
elif self.instance.address.version == 6:
elif self.instance.family == 6:
self.instance.primary_ip6_for = self.cleaned_data['device']
return super(IPAddressFromCSVForm, self).save(commit=commit)
@@ -393,7 +385,11 @@ class IPAddressBulkEditForm(forms.Form, BootstrapMixin):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF',
help_text="Select the VRF to assign, or check below to remove VRF assignment")
vrf_global = forms.BooleanField(required=False, label='Set VRF to global')
description = forms.CharField(max_length=100, required=False)
description = forms.CharField(max_length=50, required=False)
class IPAddressBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
def ipaddress_family_choices():
@@ -411,77 +407,34 @@ class IPAddressFilterForm(forms.Form, BootstrapMixin):
vrf = forms.ChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF')
#
# VLAN groups
#
class VLANGroupForm(forms.ModelForm, BootstrapMixin):
slug = SlugField()
class Meta:
model = VLANGroup
fields = ['site', 'name', 'slug']
def vlangroup_site_choices():
site_choices = Site.objects.annotate(vlangroup_count=Count('vlan_groups'))
return [(s.slug, u'{} ({})'.format(s.name, s.vlangroup_count)) for s in site_choices]
class VLANGroupFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=vlangroup_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
#
# VLANs
#
class VLANForm(forms.ModelForm, BootstrapMixin):
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, label='Group', widget=APISelect(
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
))
class Meta:
model = VLAN
fields = ['site', 'group', 'vid', 'name', 'description', 'status', 'role']
fields = ['site', 'vid', 'name', 'status', 'role']
help_texts = {
'site': "The site at which this VLAN exists",
'group': "VLAN group (optional)",
'vid': "Configured VLAN ID",
'name': "Configured VLAN name",
'status': "Operational status of this VLAN",
'role': "The primary function of this VLAN",
}
widgets = {
'site': forms.Select(attrs={'filter-for': 'group'}),
}
def __init__(self, *args, **kwargs):
super(VLANForm, self).__init__(*args, **kwargs)
# Limit VLAN group choices
if self.is_bound and self.data.get('site'):
self.fields['group'].queryset = VLANGroup.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
else:
self.fields['group'].choices = []
class VLANFromCSVForm(forms.ModelForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Device not found.'})
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'VLAN group not found.'})
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid role.'})
class Meta:
model = VLAN
fields = ['site', 'group', 'vid', 'name', 'status_name', 'role', 'description']
fields = ['site', 'vid', 'name', 'status_name', 'role']
def save(self, *args, **kwargs):
m = super(VLANFromCSVForm, self).save(commit=False)
@@ -499,39 +452,34 @@ class VLANImportForm(BulkImportForm, BootstrapMixin):
class VLANBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
description = forms.CharField(max_length=100, required=False)
class VLANBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
def vlan_site_choices():
site_choices = Site.objects.annotate(vlan_count=Count('vlans'))
return [(s.slug, u'{} ({})'.format(s.name, s.vlan_count)) for s in site_choices]
def vlan_group_choices():
group_choices = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
return [(g.pk, u'{} ({})'.format(g, g.vlan_count)) for g in group_choices]
return [(s.slug, '{} ({})'.format(s.name, s.vlan_count)) for s in site_choices]
def vlan_status_choices():
status_counts = {}
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
status_counts[status['status']] = status['count']
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
def vlan_role_choices():
role_choices = Role.objects.annotate(vlan_count=Count('vlans'))
return [(r.slug, u'{} ({})'.format(r.name, r.vlan_count)) for r in role_choices]
return [(r.slug, '{} ({})'.format(r.name, r.vlan_count)) for r in role_choices]
class VLANFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=vlan_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group',
widget=forms.SelectMultiple(attrs={'size': 8}))
status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices)
role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-14 19:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='vrf',
name='enforce_unique',
field=models.BooleanField(default=True, help_text=b'Prevent duplicate prefixes/IP addresses within this VRF', verbose_name=b'Enforce unique space'),
),
]

View File

@@ -1,38 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-15 16:22
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0010_devicebay_installed_device_set_null'),
('ipam', '0002_vrf_add_enforce_unique'),
]
operations = [
migrations.CreateModel(
name='VLANGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('slug', models.SlugField()),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vlan_groups', to='dcim.Site')),
],
options={
'ordering': ['site', 'name'],
},
),
migrations.AddField(
model_name='vlan',
name='group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='ipam.VLANGroup'),
),
migrations.AlterUniqueTogether(
name='vlangroup',
unique_together=set([('site', 'name'), ('site', 'slug')]),
),
]

View File

@@ -1,27 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-15 17:14
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ipam', '0003_ipam_add_vlangroups'),
]
operations = [
migrations.AlterModelOptions(
name='vlan',
options={'ordering': ['site', 'group', 'vid'], 'verbose_name': 'VLAN', 'verbose_name_plural': 'VLANs'},
),
migrations.AlterModelOptions(
name='vlangroup',
options={'ordering': ['site', 'name'], 'verbose_name': 'VLAN group', 'verbose_name_plural': 'VLAN groups'},
),
migrations.AlterUniqueTogether(
name='vlan',
unique_together=set([('group', 'name'), ('group', 'vid')]),
),
]

View File

@@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-07-25 18:42
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0004_ipam_vlangroup_uniqueness'),
]
operations = [
migrations.AddField(
model_name='vlan',
name='description',
field=models.CharField(blank=True, max_length=100),
),
migrations.AlterField(
model_name='vlan',
name='name',
field=models.CharField(max_length=64),
),
]

View File

@@ -1,6 +1,5 @@
from netaddr import IPNetwork, cidr_merge
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.core.validators import MaxValueValidator, MinValueValidator
@@ -46,8 +45,6 @@ class VRF(CreatedUpdatedModel):
"""
name = models.CharField(max_length=50)
rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher')
enforce_unique = models.BooleanField(default=True, verbose_name='Enforce unique space',
help_text="Prevent duplicate prefixes/IP addresses within this VRF")
description = models.CharField(max_length=100, blank=True)
class Meta:
@@ -126,8 +123,6 @@ 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]))
@@ -247,15 +242,6 @@ class Prefix(CreatedUpdatedModel):
def get_absolute_url(self):
return reverse('ipam:prefix', args=[self.pk])
def clean(self):
# Disallow host masks
if self.prefix.version == 4 and self.prefix.prefixlen == 32:
raise ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 addresses "
"instead.")
elif self.prefix.version == 6 and self.prefix.prefixlen == 128:
raise ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 addresses "
"instead.")
def save(self, *args, **kwargs):
if self.prefix:
# Clear host bits from prefix
@@ -321,21 +307,6 @@ class IPAddress(CreatedUpdatedModel):
def get_absolute_url(self):
return reverse('ipam:ipaddress', args=[self.pk])
def clean(self):
# Enforce unique IP space if applicable
if self.vrf and self.vrf.enforce_unique:
duplicate_ips = IPAddress.objects.filter(vrf=self.vrf, address__net_host=str(self.address.ip))\
.exclude(pk=self.pk)
if duplicate_ips:
raise ValidationError("Duplicate IP address found in VRF {}: {}".format(self.vrf,
duplicate_ips.first()))
elif not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE:
duplicate_ips = IPAddress.objects.filter(vrf=None, address__net_host=str(self.address.ip))\
.exclude(pk=self.pk)
if duplicate_ips:
raise ValidationError("Duplicate IP address found in global table: {}".format(duplicate_ips.first()))
def save(self, *args, **kwargs):
if self.address:
# Infer address family from IPAddress object
@@ -367,56 +338,23 @@ class IPAddress(CreatedUpdatedModel):
return None
class VLANGroup(models.Model):
"""
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
"""
name = models.CharField(max_length=50)
slug = models.SlugField()
site = models.ForeignKey('dcim.Site', related_name='vlan_groups')
class Meta:
ordering = ['site', 'name']
unique_together = [
['site', 'name'],
['site', 'slug'],
]
verbose_name = 'VLAN group'
verbose_name_plural = 'VLAN groups'
def __unicode__(self):
return u'{} - {}'.format(self.site.name, self.name)
def get_absolute_url(self):
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
class VLAN(CreatedUpdatedModel):
"""
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
to a Site, however VLAN IDs need not be unique within a Site. A VLAN may optionally be assigned to a VLANGroup,
within which all VLAN IDs and names but be unique.
Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
or more Prefixes assigned to it.
to a Site, however VLAN IDs need not be unique within a Site. Like Prefixes, each VLAN is assigned an operational
status and optionally a user-defined Role. A VLAN can have zero or more Prefixes assigned to it.
"""
site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT)
group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
MinValueValidator(1),
MaxValueValidator(4094)
])
name = models.CharField(max_length=64)
description = models.CharField(max_length=100, blank=True)
name = models.CharField(max_length=30)
status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1)
role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True)
class Meta:
ordering = ['site', 'group', 'vid']
unique_together = [
['group', 'vid'],
['group', 'name'],
]
ordering = ['site', 'vid']
verbose_name = 'VLAN'
verbose_name_plural = 'VLANs'
@@ -426,26 +364,18 @@ class VLAN(CreatedUpdatedModel):
def get_absolute_url(self):
return reverse('ipam:vlan', args=[self.pk])
def clean(self):
# Validate VLAN group
if self.group and self.group.site != self.site:
raise ValidationError("VLAN group must belong to the assigned site ({}).".format(self.site))
def to_csv(self):
return ','.join([
self.site.name,
self.group.name if self.group else '',
str(self.vid),
self.name,
self.get_status_display(),
self.role.name if self.role else '',
self.description,
])
@property
def display_name(self):
return u'{} ({})'.format(self.vid, self.name)
return u"{} ({})".format(self.vid, self.name)
def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status]

View File

@@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
from utilities.tables import BaseTable, ToggleColumn
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VRF
RIR_EDIT_LINK = """
@@ -11,8 +11,15 @@ RIR_EDIT_LINK = """
"""
UTILIZATION_GRAPH = """
{% load helpers %}
{% utilization_graph record.get_utilization %}
{% with record.get_utilization as percentage %}
<div class="progress text-center">
{% if percentage < 15 %}<span style="font-size: 12px;">{{ percentage }}%</span>{% endif %}
<div class="progress-bar progress-bar-{% if percentage >= 90 %}danger{% elif percentage >= 75 %}warning{% else %}success{% endif %}"
role="progressbar" aria-valuenow="{{ percentage }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage }}%">
{% if percentage >= 15 %}{{ percentage }}%{% endif %}
</div>
</div>
{% endwith %}
"""
ROLE_EDIT_LINK = """
@@ -43,12 +50,6 @@ STATUS_LABEL = """
{% endif %}
"""
VLANGROUP_EDIT_LINK = """
{% if perms.ipam.change_vlangroup %}
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}">Edit</a>
{% endif %}
"""
#
# VRFs
@@ -176,23 +177,6 @@ class IPAddressBriefTable(BaseTable):
fields = ('address', 'device', 'interface', 'nat_inside')
#
# VLAN groups
#
class VLANGroupTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
vlan_count = tables.Column(verbose_name='VLANs')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=VLANGROUP_EDIT_LINK, verbose_name='')
class Meta(BaseTable.Meta):
model = VLANGroup
fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'edit')
#
# VLANs
#
@@ -201,11 +185,10 @@ class VLANTable(BaseTable):
pk = ToggleColumn()
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
name = tables.Column(verbose_name='Name')
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
role = tables.Column(verbose_name='Role')
class Meta(BaseTable.Meta):
model = VLAN
fields = ('pk', 'vid', 'site', 'group', 'name', 'status', 'role')
fields = ('pk', 'vid', 'site', 'name', 'status', 'role')

View File

@@ -58,12 +58,6 @@ urlpatterns = [
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
# VLAN groups
url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'),
url(r'^vlan-groups/add/$', views.VLANGroupEditView.as_view(), name='vlangroup_add'),
url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
# VLANs
url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
url(r'^vlans/add/$', views.VLANEditView.as_view(), name='vlan_add'),

View File

@@ -12,7 +12,7 @@ from utilities.views import (
)
from . import filters, forms, tables
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VRF
def add_available_prefixes(parent, prefix_list):
@@ -95,6 +95,7 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vrf'
cls = VRF
form = forms.VRFBulkDeleteForm
default_redirect_url = 'ipam:vrf_list'
@@ -120,6 +121,7 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView):
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_rir'
cls = RIR
form = forms.RIRBulkDeleteForm
default_redirect_url = 'ipam:rir_list'
@@ -215,6 +217,7 @@ class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_aggregate'
cls = Aggregate
form = forms.AggregateBulkDeleteForm
default_redirect_url = 'ipam:aggregate_list'
@@ -240,6 +243,7 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_role'
cls = Role
form = forms.RoleBulkDeleteForm
default_redirect_url = 'ipam:role_list'
@@ -350,6 +354,7 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_prefix'
cls = Prefix
form = forms.PrefixBulkDeleteForm
default_redirect_url = 'ipam:prefix_list'
@@ -474,35 +479,10 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_ipaddress'
cls = IPAddress
form = forms.IPAddressBulkDeleteForm
default_redirect_url = 'ipam:ipaddress_list'
#
# VLAN groups
#
class VLANGroupListView(ObjectListView):
queryset = VLANGroup.objects.annotate(vlan_count=Count('vlans'))
filter = filters.VLANGroupFilter
filter_form = forms.VLANGroupFilterForm
table = tables.VLANGroupTable
edit_permissions = ['ipam.change_vlangroup', 'ipam.delete_vlangroup']
template_name = 'ipam/vlangroup_list.html'
class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vlangroup'
model = VLANGroup
form_class = forms.VLANGroupForm
cancel_url = 'ipam:vlangroup_list'
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlangroup'
cls = VLANGroup
default_redirect_url = 'ipam:vlangroup_list'
#
# VLANs
#
@@ -558,7 +538,7 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['site', 'group', 'status', 'role', 'description']:
for field in ['site', 'status', 'role']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
@@ -568,4 +548,5 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlan'
cls = VLAN
form = forms.VLANBulkDeleteForm
default_redirect_url = 'ipam:vlan_list'

View File

@@ -78,11 +78,3 @@ 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
# Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table
# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
ENFORCE_GLOBAL_UNIQUE = False

View File

@@ -12,7 +12,7 @@ except ImportError:
"the documentation.")
VERSION = '1.3.2'
VERSION = '1.2.0'
# Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
@@ -40,8 +40,6 @@ 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)
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
# Attempt to import LDAP configuration if it has been defined
@@ -139,6 +137,7 @@ TEMPLATES = [
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'utilities.context_processors.settings',
'django.core.context_processors.request',
],
},
},

View File

@@ -2,12 +2,10 @@ from django.conf.urls import include, url
from django.contrib import admin
from django.views.defaults import page_not_found
from views import home, trigger_500, handle_500
from views import home, trigger_500
from users.views import login, logout
handler500 = handle_500
urlpatterns = [
# Default page

View File

@@ -1,6 +1,9 @@
import sys
from markdown import markdown
from django.conf import settings
from django.http import Http404
from django.shortcuts import render
from django.utils.safestring import mark_safe
from circuits.models import Provider, Circuit
from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
@@ -44,14 +47,6 @@ def home(request):
def trigger_500(request):
"""Hot-wired method of triggering a server error to test reporting."""
raise Exception("Congratulations, you've triggered an exception! Go tell all your friends what an exceptional "
"person you are.")
def handle_500(request):
"""Custom server error handler"""
type_, error, traceback = sys.exc_info()
return render(request, '500.html', {
'exception': str(type_),
'error': error,
}, status=500)

View File

@@ -2,9 +2,6 @@
* {
margin: 0;
}
html {
overflow-y: scroll;
}
html, body {
height: 100%;
}
@@ -225,22 +222,6 @@ ul.rack li.h41u { height: 820px; }
ul.rack li.h41u a, ul.rack li.h41u span { padding: 400px 0; }
ul.rack li.h42u { height: 840px; }
ul.rack li.h42u a, ul.rack li.h42u span { padding: 410px 0; }
ul.rack li.h43u { height: 860px; }
ul.rack li.h43u a, ul.rack li.h43u span { padding: 420px 0; }
ul.rack li.h44u { height: 880px; }
ul.rack li.h44u a, ul.rack li.h44u span { padding: 430px 0; }
ul.rack li.h45u { height: 900px; }
ul.rack li.h45u a, ul.rack li.h45u span { padding: 440px 0; }
ul.rack li.h46u { height: 920px; }
ul.rack li.h46u a, ul.rack li.h46u span { padding: 450px 0; }
ul.rack li.h47u { height: 940px; }
ul.rack li.h47u a, ul.rack li.h47u span { padding: 460px 0; }
ul.rack li.h48u { height: 960px; }
ul.rack li.h48u a, ul.rack li.h48u span { padding: 470px 0; }
ul.rack li.h49u { height: 980px; }
ul.rack li.h49u a, ul.rack li.h49u span { padding: 480px 0; }
ul.rack li.h50u { height: 1000px; }
ul.rack li.h50u a, ul.rack li.h50u span { padding: 490px 0; }
ul.rack li.occupied a {
color: #ffffff;
display: block;

View File

@@ -1,15 +1,9 @@
$(document).ready(function() {
// "Select all" checkbox in a table header
$('th input:checkbox[name=_all]').click(function (event) {
$('th input:checkbox').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) {

View File

@@ -10,16 +10,15 @@ $(document).ready(function() {
$('#privkey_modal').modal('show');
} else {
unlock_secret(secret_id, private_key);
$(this).hide();
$(this).siblings('button.lock-secret').show();
}
});
// Locking a secret
$('button.lock-secret').click(function (event) {
var secret_id = $(this).attr('secret-id');
var secret_div = $('#secret_' + secret_id);
// Delete the plaintext
secret_div.html('********');
$('#secret_' + secret_id).html('********');
$(this).hide();
$(this).siblings('button.unlock-secret').show();
});
@@ -82,16 +81,13 @@ $(document).ready(function() {
xhr.setRequestHeader("X-CSRFToken", csrf_token);
},
success: function (response, status) {
$('#secret_' + secret_id).html(response.plaintext);
$('button.unlock-secret').hide();
$('button.lock-secret').show();
var secret_plaintext = response.plaintext;
$('#secret_' + secret_id).html(secret_plaintext);
return true;
},
error: function (xhr, ajaxOptions, thrownError) {
if (xhr.status == 403) {
alert("Permission denied");
} else {
var json = jQuery.parseJSON(xhr.responseText);
alert("Decryption failed: " + json['error']);
alert("Decryption failed: " + xhr.statusText);
}
}
});

View File

@@ -28,7 +28,6 @@ class SecretRoleListView(generics.ListAPIView):
"""
queryset = SecretRole.objects.all()
serializer_class = serializers.SecretRoleSerializer
permission_classes = [IsAuthenticated]
class SecretRoleDetailView(generics.RetrieveAPIView):
@@ -37,19 +36,17 @@ class SecretRoleDetailView(generics.RetrieveAPIView):
"""
queryset = SecretRole.objects.all()
serializer_class = serializers.SecretRoleSerializer
permission_classes = [IsAuthenticated]
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_ip4', 'device__primary_ip6', 'role')\
queryset = Secret.objects.select_related('device__primary_ip', 'role')\
.prefetch_related('role__users', 'role__groups')
serializer_class = serializers.SecretSerializer
filter_class = SecretFilter
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]
permission_classes = [IsAuthenticated]
def get(self, request, private_key=None):
queryset = self.filter_queryset(self.get_queryset())
@@ -90,11 +87,10 @@ 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_ip4', 'device__primary_ip6', 'role')\
queryset = Secret.objects.select_related('device__primary_ip', 'role')\
.prefetch_related('role__users', 'role__groups')
serializer_class = serializers.SecretSerializer
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]
permission_classes = [IsAuthenticated]
def get(self, request, pk, private_key=None):
secret = get_object_or_404(Secret, pk=pk)

View File

@@ -1,7 +1,6 @@
import django_filters
from .models import Secret, SecretRole
from dcim.models import Device
class SecretFilter(django_filters.FilterSet):
@@ -16,13 +15,7 @@ class SecretFilter(django_filters.FilterSet):
to_field_name='slug',
label='Role (slug)',
)
device = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(),
to_field_name='name',
label='Device (Name)',
)
class Meta:
model = Secret
fields = ['name', 'role_id', 'role', 'device']
fields = ['name', 'role_id', 'role']

View File

@@ -5,7 +5,7 @@ from django import forms
from django.db.models import Count
from dcim.models import Device
from utilities.forms import BootstrapMixin, BulkImportForm, CSVDataField, SlugField
from utilities.forms import BootstrapMixin, BulkImportForm, ConfirmationForm, CSVDataField, SlugField
from .models import Secret, SecretRole, UserKey
@@ -42,6 +42,10 @@ class SecretRoleForm(forms.ModelForm, BootstrapMixin):
fields = ['name', 'slug']
class SecretRoleBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=SecretRole.objects.all(), widget=forms.MultipleHiddenInput)
#
# Secrets
#
@@ -93,9 +97,13 @@ class SecretBulkEditForm(forms.Form, BootstrapMixin):
name = forms.CharField(max_length=100, required=False)
class SecretBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
def secret_role_choices():
role_choices = SecretRole.objects.annotate(secret_count=Count('secrets'))
return [(r.slug, u'{} ({})'.format(r.name, r.secret_count)) for r in role_choices]
return [(r.slug, '{} ({})'.format(r.name, r.secret_count)) for r in role_choices]
class SecretFilterForm(forms.Form, BootstrapMixin):

View File

@@ -219,8 +219,8 @@ class Secret(CreatedUpdatedModel):
def __unicode__(self):
if self.role and self.device:
return u'{} for {}'.format(self.role, self.device)
return u'Secret'
return "{} for {}".format(self.role, self.device)
return "Secret"
def get_absolute_url(self):
return reverse('secrets:secret', args=[self.pk])

View File

@@ -37,6 +37,7 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'secrets.delete_secretrole'
cls = SecretRole
form = forms.SecretRoleBulkDeleteForm
default_redirect_url = 'secrets:secretrole_list'
@@ -218,4 +219,5 @@ class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'secrets.delete_secret'
cls = Secret
form = forms.SecretBulkDeleteForm
default_redirect_url = 'secrets:secret_list'

View File

@@ -12,19 +12,13 @@
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-danger" style="margin-top: 200px">
<div class="panel-heading">
<strong>
<i class="glyphicon glyphicon-warning-sign"></i>
Server Error
</strong>
<strong>Server Error</strong>
</div>
<div class="panel-body">
<p>There was a problem with your request. This error has been logged and administrative staff have
been notified. Please return to the home page and try again.</p>
<p>If you are responsible for this installation, please consider
<a href="https://github.com/digitalocean/netbox/issues">filing a bug report</a>. Additional
information is provided below:</p>
<pre><strong>{{ exception }}</strong><br />
{{ error }}</pre>
<a href="https://github.com/digitalocean/netbox/issues">filing a bug report</a>.</p>
<div class="text-right">
<a href="/" class="btn btn-primary">Home Page</a>
</div>

View File

@@ -110,7 +110,7 @@
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|startswith:'/ipam/' and not request.path|startswith:'/ipam/vlan' %} active{% endif %}">
<li class="dropdown{% if request.path|startswith:'/ipam/' and not request.path|startswith:'/ipam/vlans/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IP Space <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'ipam:ipaddress_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> IP Addresses</a></li>
@@ -156,20 +156,17 @@
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|startswith:'/ipam/vlan' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'ipam:vlan_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VLANs</a></li>
{% if perms.ipam.add_vlan %}
<li class="dropdown{% if request.path|startswith:'/ipam/vlans/' %} active{% endif %}">
{% if perms.ipam.add_vlan %}
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'ipam:vlan_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VLANs</a></li>
<li><a href="{% url 'ipam:vlan_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VLAN</a></li>
<li><a href="{% url 'ipam:vlan_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import VLANs</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'ipam:vlangroup_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VLAN Groups</a></li>
{% if perms.ipam.add_vlangroup %}
<li><a href="{% url 'ipam:vlangroup_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VLAN Group</a></li>
{% endif %}
</ul>
</ul>
{% else %}
<a href="{% url 'ipam:vlan_list' %}">VLANs</a>
{% endif %}
</li>
<li class="dropdown{% if request.path|startswith:'/circuits/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>

View File

@@ -14,28 +14,8 @@
</div>
<h1>Providers</h1>
<div class="row">
<div class="col-md-9">
<div class="col-md-12">
{% 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 %}

View File

@@ -289,180 +289,100 @@
</div>
<div class="col-md-6">
{% if device_bays or device.device_type.is_parent_device %}
{% if perms.dcim.delete_devicebay %}
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
{% csrf_token %}
{% endif %}
<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' with selectable=True %}
{% include 'dcim/inc/_devicebay.html' %}
{% empty %}
<tr>
<td colspan="4">No device bays defined</td>
</tr>
{% endfor %}
</table>
{% if perms.dcim.add_devicebay or perms.dcim.delete_devicebay %}
<div class="panel-footer">
<div class="row">
<div class="col-md-6">
{% if device_bays and perms.dcim.delete_devicebay %}
<button type="submit" class="btn btn-xs btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
</button>
{% endif %}
</div>
<div class="col-md-6 text-right">
{% if perms.dcim.add_devicebay %}
<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 bay
</a>
{% endif %}
</div>
</div>
</div>
{% 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>
{% if perms.dcim.delete_devicebay %}
</form>
{% endif %}
{% endif %}
{% if interfaces or device.device_type.is_network_device %}
{% if perms.dcim.delete_interface %}
<form method="post" action="{% url 'dcim:interface_bulk_delete' pk=device.pk %}">
{% csrf_token %}
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Interfaces</strong>
</div>
<table class="table table-hover panel-body">
{% for iface in interfaces %}
{% include 'dcim/inc/_interface.html' with selectable=True %}
{% include 'dcim/inc/_interface.html' %}
{% empty %}
<tr>
<td colspan="4">No interfaces defined</td>
</tr>
{% endfor %}
</table>
{% if perms.dcim.add_interface or perms.dcim.delete_interface %}
<div class="panel-footer">
<div class="row">
<div class="col-md-6">
{% if interfaces and perms.dcim.delete_interface %}
<button type="submit" class="btn btn-xs btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
</button>
{% endif %}
</div>
<div class="col-md-6 text-right">
{% if perms.dcim.add_interface %}
<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>
{% endif %}
</div>
</div>
{% 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>
{% if perms.dcim.delete_interface %}
</form>
{% endif %}
{% endif %}
{% if cs_ports or device.device_type.is_console_server %}
{% if perms.dcim.delete_consoleserverport %}
<form method="post" action="{% url 'dcim:consoleserverport_bulk_delete' pk=device.pk %}">
{% csrf_token %}
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Console Server Ports</strong>
</div>
<table class="table table-hover panel-body">
{% for csp in cs_ports %}
{% include 'dcim/inc/_consoleserverport.html' with selectable=True %}
{% include 'dcim/inc/_consoleserverport.html' %}
{% empty %}
<tr>
<td colspan="4">No console server ports defined</td>
</tr>
{% endfor %}
</table>
{% if perms.dcim.add_consoleserverport or perms.dcim.delete_consoleserverport %}
<div class="panel-footer">
<div class="row">
<div class="col-md-6">
{% if cs_ports and perms.dcim.delete_consoleserverport %}
<button type="submit" class="btn btn-xs btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
</button>
{% endif %}
</div>
<div class="col-md-6 text-right">
{% if perms.dcim.add_consoleserverport %}
<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>
{% endif %}
</div>
</div>
{% 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>
{% if perms.dcim.delete_consoleserverport %}
</form>
{% endif %}
{% endif %}
{% if power_outlets or device.device_type.is_pdu %}
{% if perms.dcim.delete_poweroutlet %}
<form method="post" action="{% url 'dcim:poweroutlet_bulk_delete' pk=device.pk %}">
{% csrf_token %}
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Power Outlets</strong>
</div>
<table class="table table-hover panel-body">
{% for po in power_outlets %}
{% include 'dcim/inc/_poweroutlet.html' with selectable=True %}
{% include 'dcim/inc/_poweroutlet.html' %}
{% empty %}
<tr>
<td colspan="4">No power outlets defined</td>
</tr>
{% endfor %}
</table>
{% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
<div class="panel-footer">
<div class="row">
<div class="col-md-6">
{% if power_outlets and perms.dcim.delete_poweroutlet %}
<button type="submit" class="btn btn-xs btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
</button>
{% endif %}
</div>
<div class="col-md-6 text-right">
{% if perms.dcim.add_poweroutlet %}
<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>
{% endif %}
</div>
</div>
{% 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>
{% if perms.dcim.delete_poweroutlet %}
</form>
{% endif %}
{% endif %}
</div>
</div>

View File

@@ -22,32 +22,8 @@
<div class="panel-body">
{% render_field form.site %}
{% render_field form.rack %}
{% if obj.device_type.is_child_device and obj.parent_bay %}
<div class="form-group">
<label class="col-md-3 control-label">Parent device</label>
<div class="col-md-9">
<p class="form-control-static">
<a href="{% url 'dcim:device' pk=obj.parent_bay.device.pk %}">{{ obj.parent_bay.device }}</a>
</p>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label">Parent bay</label>
<div class="col-md-9">
<p class="form-control-static">
{{ obj.parent_bay.name }}
{% if perms.dcim.change_devicebay %}
<a href="{% url 'dcim:devicebay_depopulate' pk=obj.parent_bay.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Remove device"></i> Remove
</a>
{% endif %}
</p>
</div>
</div>
{% elif not obj.device_type.is_child_device %}
{% render_field form.face %}
{% render_field form.position %}
{% endif %}
{% render_field form.face %}
{% render_field form.position %}
</div>
</div>
<div class="panel panel-default">

View File

@@ -5,7 +5,7 @@
{% block title %}Device Import{% endblock %}
{% block content %}
{% include 'dcim/inc/_device_import_header.html' %}
<h1>Device Import</h1>
<div class="row">
<div class="col-md-12">
<form action="." method="post" class="form">

View File

@@ -1,75 +0,0 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Device Import{% endblock %}
{% block content %}
{% include 'dcim/inc/_device_import_header.html' with active_tab='child_import' %}
<div class="row">
<div class="col-md-12">
<form action="." method="post" class="form">
{% csrf_token %}
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Name</td>
<td>Device name (optional)</td>
<td>Blade12</td>
</tr>
<tr>
<td>Device role</td>
<td>Functional role of device</td>
<td>Blade Server</td>
</tr>
<tr>
<td>Device manufacturer</td>
<td>Hardware manufacturer</td>
<td>Dell</td>
</tr>
<tr>
<td>Device model</td>
<td>Hardware model</td>
<td>BS2000T</td>
</tr>
<tr>
<td>Platform</td>
<td>Software running on device (optional)</td>
<td>Linux</td>
</tr>
<tr>
<td>Serial</td>
<td>Serial number (optional)</td>
<td>CAB00577291</td>
</tr>
<tr>
<td>Parent device</td>
<td>Parent device</td>
<td>Server101</td>
</tr>
<tr>
<td>Device bay</td>
<td>Device bay name</td>
<td>Slot 4</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>Blade12,Blade Server,Dell,BS2000T,Linux,CAB00577291,Server101,Slot4</pre>
</div>
</div>
{% endblock %}

View File

@@ -25,7 +25,6 @@
<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">

View File

@@ -42,35 +42,19 @@
<table class="table table-hover panel-body">
<tr>
<td>Manufacturer</td>
<td><a href="{% url 'dcim:devicetype_list' %}?manufacturer={{ devicetype.manufacturer.slug }}">{{ devicetype.manufacturer }}</a></td>
<td>{{ devicetype.manufacturer }}</td>
</tr>
<tr>
<td>Model Name</td>
<td>{{ devicetype.model }}</td>
</tr>
<tr>
<td>Part Number</td>
<td>
{% if devicetype.part_number %}
{{ devicetype.part_number }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Height (U)</td>
<td>{{ devicetype.u_height }}</td>
</tr>
<tr>
<td>Full Depth</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>
<td>{{ devicetype.is_full_depth|yesno|capfirst }}</td>
</tr>
</table>
</div>
@@ -80,70 +64,21 @@
</div>
<table class="table table-hover panel-body">
<tr>
<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>
<td>Is a Console Server</td>
<td>{{ devicetype.is_console_server|yesno|capfirst }}</td>
</tr>
<tr>
<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>
<td>Is a PDU</td>
<td>{{ devicetype.is_pdu|yesno|capfirst }}</td>
</tr>
<tr>
<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>
<td>Is a Network Device</td>
<td>{{ devicetype.is_network_device|yesno|capfirst }}</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 %}

View File

@@ -1,9 +1,4 @@
<tr{% if cp.cs_port and not cp.connection_status %} class="info"{% endif %}>
{% if selectable and perms.dcim.delete_consoleport %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ cp.pk }}" />
</td>
{% endif %}
<td>
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
</td>

View File

@@ -1,9 +1,4 @@
<tr{% if csp.connected_console and not csp.connected_console.connection_status %} class="info"{% endif %}>
{% if selectable and perms.dcim.delete_consoleserverport %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ csp.pk }}" />
</td>
{% endif %}
<td>
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp.name }}
</td>

View File

@@ -1,5 +0,0 @@
<h1>Device Import</h1>
<ul class="nav nav-tabs" style="margin-bottom: 20px">
<li role="presentation"{% if not active_tab %} class="active"{% endif %}><a href="{% url 'dcim:device_import' %}">Racked Devices</a></li>
<li role="presentation"{% if active_tab == 'child_import' %} class="active"{% endif %}><a href="{% url 'dcim:device_import_child' %}">Child Devices</a></li>
</ul>

View File

@@ -1,9 +1,4 @@
<tr>
{% if selectable and perms.dcim.delete_devicebay %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
</td>
{% endif %}
<td>
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }}
</td>

View File

@@ -1,9 +1,4 @@
<tr{% if iface.connection and not iface.connection.connection_status %} class="info"{% endif %}>
{% if selectable and perms.dcim.delete_interface %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
</td>
{% endif %}
<td>
<i class="fa fa-fw fa-{{ icon|default:"exchange" }}"></i> <span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
{% if iface.description %}

View File

@@ -1,9 +1,4 @@
<tr{% if po.connected_port and not po.connected_port.connection_status %} class="info"{% endif %}>
{% if selectable and perms.dcim.delete_poweroutlet %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ po.pk }}" />
</td>
{% endif %}
<td>
<i class="fa fa-fw fa-bolt"></i> {{ po.name }}
</td>

View File

@@ -1,9 +1,4 @@
<tr{% if pp.power_outlet and not pp.connection_status %} class="info"{% endif %}>
{% if selectable and perms.dcim.delete_powerport %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ pp.pk }}" />
</td>
{% endif %}
<td>
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
</td>

View File

@@ -7,7 +7,7 @@
<input type="hidden" name="pk_all" value="{% for row in table.rows %}{{ row.record.pk|default:'' }}{% if not forloop.last %},{% endif %}{% endfor %}" />
{% render_table table table_template|default:'table.html' %}
{% if perms.dcim.add_interface %}
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_add_multi' %}" class="btn btn-primary btn-sm">
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_add' %}" class="btn btn-primary btn-sm">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Add Interfaces
</button>

View File

@@ -4,10 +4,7 @@
{% csrf_token %}
<div class="panel panel-default">
<div class="panel-heading">
<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>
<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>
<strong>{{ title }}</strong>
</div>
{% render_table table 'table.html' %}

View File

@@ -25,7 +25,6 @@
<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">

View File

@@ -21,7 +21,6 @@
<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">

View File

@@ -2,7 +2,6 @@
<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">

View File

@@ -26,7 +26,6 @@
<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">

View File

@@ -43,16 +43,6 @@
<td>Name of assigned site (optional)</td>
<td>HQ</td>
</tr>
<tr>
<td>VLAN Group</td>
<td>Name of group for VLAN selection (optional)</td>
<td>Customers</td>
</tr>
<tr>
<td>VLAN ID</td>
<td>Numeric VLAN ID (optional)</td>
<td>801</td>
</tr>
<tr>
<td>Status</td>
<td>Current status</td>
@@ -71,7 +61,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>192.168.42.0/24,65000:123,HQ,Customers,801,Active,Customer,7th floor WiFi</pre>
<pre>192.168.42.0/24,65000:123,HQ,Active,Customer,7th floor WiFi</pre>
</div>
</div>
{% endblock %}

View File

@@ -26,7 +26,6 @@
<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">

View File

@@ -51,16 +51,6 @@
<td>Site</td>
<td><a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a></td>
</tr>
<tr>
<td>Group</td>
<td>
{% if vlan.group %}
<a href="{{ vlan.group.get_absolute_url }}">{{ vlan.group.name }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>VLAN ID</td>
<td>{{ vlan.vid }}</td>
@@ -69,16 +59,6 @@
<td>Name</td>
<td>{{ vlan.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>
{% if vlan.description %}
{{ vlan.description }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Status</td>
<td>

View File

@@ -11,7 +11,6 @@
<td>{{ vlan.site }}</td>
<td>{{ vlan.status }}</td>
<td>{{ vlan.role }}</td>
<td>{{ vlan.description }}</td>
</tr>
{% endfor %}
{% endblock %}

View File

@@ -33,11 +33,6 @@
<td>Name of assigned site</td>
<td>LAS2</td>
</tr>
<tr>
<td>Group</td>
<td>Name of VLAN group (optional)</td>
<td>Backend Network</td>
</tr>
<tr>
<td>ID</td>
<td>Configured VLAN ID</td>
@@ -58,15 +53,10 @@
<td>Functional role (optional)</td>
<td>Security</td>
</tr>
<tr>
<td>Description</td>
<td>Short description (optional)</td>
<td>Security team only</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>LAS2,Backend Network,1400,Cameras,Active,Security,Security team only</pre>
<pre>LAS2,1400,Cameras,Active,Security</pre>
</div>
</div>
{% endblock %}

View File

@@ -26,7 +26,6 @@
<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">

View File

@@ -1,24 +0,0 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}VLAN Groups{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.ipam.add_vlangroup %}
<a href="{% url 'ipam:vlangroup_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Add a VLAN group
</a>
{% endif %}
</div>
<h1>VLAN Groups</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:vlangroup_bulk_delete' %}
</div>
<div class="col-md-3">
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -30,16 +30,6 @@
<td>Route Distinguisher</td>
<td>{{ vrf.rd }}</td>
</tr>
<tr>
<td>Enforce Uniqueness</td>
<td>
{% if vrf.enforce_unique %}
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
{% else %}
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
{% endif %}
</td>
</tr>
<tr>
<td>Description</td>
<td>

View File

@@ -38,11 +38,6 @@
<td>Route distinguisher</td>
<td>65000:123456</td>
</tr>
<tr>
<td>Enforce uniqueness</td>
<td>Prevent duplicate prefixes/IP addresses</td>
<td>True</td>
</tr>
<tr>
<td>Description</td>
<td>Short description (optional)</td>
@@ -51,7 +46,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>Customer_ABC,65000:123456,True,Native VRF for customer ABC</pre>
<pre>Customer_ABC,65000:123456,Native VRF for customer ABC</pre>
</div>
</div>
{% endblock %}

View File

@@ -17,10 +17,7 @@
<textarea class="form-control" id="user_privkey" style="height: 300px;"></textarea>
</div>
<div class="form-group text-right">
<button id="submit_privkey" class="btn btn-primary unlock-secret" data-dismiss="modal">
<i class="fa fa-save" aria-hidden="True"></i>
Save RSA Key
</button>
<button id="submit_privkey" class="btn btn-primary unlock-secret" data-dismiss="modal">Submit RSA Key</button>
</div>
</div>
</div>

View File

@@ -5,15 +5,11 @@
{% block message %}
<p>
Are you sure you want to delete these {{ obj_type_plural|default:"objects" }}{% if parent_obj %} from <a href="{{ parent_obj.get_absolute_url }}">{{ parent_obj }}</a>{% endif %}?
Are you sure you want to delete these {{ obj_type_plural|default:"objects" }}?
</p>
<ul>
{% for obj in selected_objects %}
{% if obj.get_absolute_url %}
<li><a href="{{ obj.get_absolute_url }}">{{ obj }}</a></li>
{% else %}
<li>{{ obj }}</li>
{% endif %}
<li><a href="{{ obj.get_absolute_url }}">{{ obj }}</a></li>
{% endfor %}
</ul>
{% endblock %}

View File

@@ -1,7 +0,0 @@
<div class="progress text-center">
{% if utilization < 30 %}<span style="font-size: 12px;">{{ utilization }}%</span>{% endif %}
<div class="progress-bar progress-bar-{% if utilization >= danger_threshold %}danger{% elif utilization >= warning_threshold %}warning{% else %}success{% endif %}"
role="progressbar" aria-valuenow="{{ utilization }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ utilization }}%">
{% if utilization >= 30 %}{{ utilization }}%{% endif %}
</div>
</div>

View File

@@ -60,7 +60,7 @@ class SelectWithDisabled(forms.Select):
option_label = option_label['label']
disabled_html = ' disabled="disabled"' if option_disabled else ''
return format_html(u'<option value="{}"{}{}>{}</option>',
return format_html('<option value="{}"{}{}>{}</option>',
option_value,
selected_html,
disabled_html,

View File

@@ -1,30 +0,0 @@
from django.db.models import Manager
class NaturalOrderByManager(Manager):
def natural_order_by(self, *fields):
"""
Attempt to order records naturally by segmenting a field into three parts:
1. Leading integer (if any)
2. Middle portion
3. Trailing integer (if any)
:param fields: The fields on which to order the queryset. The last field in the list will be ordered naturally.
"""
db_table = self.model._meta.db_table
primary_field = fields[-1]
id1 = '_{}_{}1'.format(db_table, primary_field)
id2 = '_{}_{}2'.format(db_table, primary_field)
id3 = '_{}_{}3'.format(db_table, primary_field)
queryset = super(NaturalOrderByManager, self).get_queryset().extra(select={
id1: "CAST(SUBSTRING({}.{} FROM '^(\d+)') AS integer)".format(db_table, primary_field),
id2: "SUBSTRING({}.{} FROM '^\d*(.*?)\d*$')".format(db_table, primary_field),
id3: "CAST(SUBSTRING({}.{} FROM '(\d+)$') AS integer)".format(db_table, primary_field),
})
ordering = fields[0:-1] + (id1, id2, id3)
return queryset.order_by(*ordering)

View File

@@ -95,15 +95,3 @@ def querystring_toggle(request, multi=True, page_key='page', **kwargs):
return '?' + querystring
else:
return ''
@register.inclusion_tag('utilities/templatetags/utilization_graph.html')
def utilization_graph(utilization, warning_threshold=75, danger_threshold=90):
"""
Display a horizontal bar graph indicating a percentage of utilization.
"""
return {
'utilization': utilization,
'warning_threshold': warning_threshold,
'danger_threshold': danger_threshold,
}

View File

@@ -3,11 +3,9 @@ from django_tables2 import RequestConfig
from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse
from django.db import transaction, IntegrityError
from django.db.models import ProtectedError
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.template import TemplateSyntaxError
@@ -136,12 +134,12 @@ class ObjectEditView(View):
obj_created = not obj.pk
obj.save()
msg = u'Created ' if obj_created else u'Modified '
msg = 'Created ' if obj_created else 'Modified '
msg += self.model._meta.verbose_name
if hasattr(obj, 'get_absolute_url'):
msg = u'{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), obj)
msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), obj)
else:
msg = u'{} {}'.format(msg, obj)
msg = '{} {}'.format(msg, obj)
messages.success(request, msg)
if obj_created:
UserAction.objects.log_create(request.user, obj, msg)
@@ -194,7 +192,7 @@ class ObjectDeleteView(View):
if form.is_valid():
try:
obj.delete()
msg = u'Deleted {} {}'.format(self.model._meta.verbose_name, obj)
msg = 'Deleted {} {}'.format(self.model._meta.verbose_name, obj)
messages.success(request, msg)
UserAction.objects.log_delete(request.user, obj, msg)
return redirect(self.redirect_url)
@@ -236,7 +234,7 @@ class BulkImportView(View):
obj_table = self.table(new_objs)
if new_objs:
msg = u'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural)
msg = 'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural)
messages.success(request, msg)
UserAction.objects.log_import(request.user, ContentType.objects.get_for_model(new_objs[0]), msg)
@@ -283,7 +281,7 @@ class BulkEditView(View):
if form.is_valid():
updated_count = self.update_objects(pk_list, form)
if updated_count:
msg = u'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural)
msg = 'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural)
messages.success(self.request, msg)
UserAction.objects.log_bulk_edit(request.user, ContentType.objects.get_for_model(self.cls), msg)
return redirect(redirect_url)
@@ -311,7 +309,6 @@ class BulkEditView(View):
class BulkDeleteView(View):
cls = None
parent_cls = None
form = None
template_name = 'utilities/confirm_bulk_delete.html'
default_redirect_url = None
@@ -320,35 +317,24 @@ class BulkDeleteView(View):
def dispatch(self, *args, **kwargs):
return super(BulkDeleteView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
return redirect(self.default_redirect_url)
def post(self, request, *args, **kwargs):
# Attempt to derive parent object if a parent class has been given
if self.parent_cls:
parent_obj = get_object_or_404(self.parent_cls, **kwargs)
else:
parent_obj = None
# Determine URL to redirect users upon deletion of objects
posted_redirect_url = request.POST.get('redirect_url')
if posted_redirect_url and is_safe_url(url=posted_redirect_url, host=request.get_host()):
redirect_url = posted_redirect_url
elif parent_obj:
redirect_url = parent_obj.get_absolute_url()
elif self.default_redirect_url:
redirect_url = reverse(self.default_redirect_url)
else:
raise ImproperlyConfigured('No redirect URL has been provided.')
redirect_url = reverse(self.default_redirect_url)
# Are we deleting *all* objects in the queryset or just a selected subset?
if request.POST.get('_all'):
pk_list = [x for x in request.POST.get('pk_all').split(',') if x]
else:
pk_list = request.POST.getlist('pk')
form_cls = self.get_form()
if '_confirm' in request.POST:
form = form_cls(request.POST)
form = self.form(request.POST)
if form.is_valid():
# Delete objects
@@ -359,13 +345,13 @@ class BulkDeleteView(View):
handle_protectederror(list(queryset), request, e)
return redirect(redirect_url)
msg = u'Deleted {} {}'.format(deleted_count, self.cls._meta.verbose_name_plural)
msg = 'Deleted {} {}'.format(deleted_count, self.cls._meta.verbose_name_plural)
messages.success(request, msg)
UserAction.objects.log_bulk_delete(request.user, ContentType.objects.get_for_model(self.cls), msg)
return redirect(redirect_url)
else:
form = form_cls(initial={'pk': pk_list})
form = self.form(initial={'pk': pk_list})
selected_objects = self.cls.objects.filter(pk__in=pk_list)
if not selected_objects:
@@ -374,18 +360,7 @@ class BulkDeleteView(View):
return render(request, self.template_name, {
'form': form,
'parent_obj': parent_obj,
'obj_type_plural': self.cls._meta.verbose_name_plural,
'selected_objects': selected_objects,
'cancel_url': redirect_url,
})
def get_form(self):
"""Provide a standard bulk delete form if none has been specified for the view"""
class BulkDeleteForm(ConfirmationForm):
pk = ModelMultipleChoiceField(queryset=self.cls.objects.all(), widget=MultipleHiddenInput)
if self.form:
return self.form
return BulkDeleteForm

View File

@@ -1,5 +1,5 @@
cryptography==1.4
Django==1.9.8
Django==1.9.7
django-debug-toolbar==1.4
django-filter==0.13.0
django-rest-swagger==0.3.7
@@ -15,4 +15,3 @@ py-gfm==0.1.3
pycrypto==2.6.1
sqlparse==0.1.19
xmltodict==0.10.2
natsort>=5.0.0