Compare commits

..

125 Commits

Author SHA1 Message Date
Jeremy Stretch
58e3d5ae09 Merge pull request #474 from digitalocean/develop
Release v1.5.2
2016-08-16 09:33:57 -04:00
Jeremy Stretch
2eb8b4fe71 Release v1.5.2 2016-08-16 09:32:53 -04:00
Jeremy Stretch
989ec721d3 Fixes #472: Hide the connection button for interfaces which have a circuit terminated to them 2016-08-16 09:29:20 -04:00
Jeremy Stretch
d045429b51 Fixes #469: Added missing import buttons to list views 2016-08-15 17:57:17 -04:00
Jeremy Stretch
78c3b25f0a Fixes #467: Include prefixes and IPs which inherit tenancy from their VRF in tenant stats 2016-08-15 16:11:17 -04:00
Jeremy Stretch
13136d0ccb Fixes #468: Added validation to prevent a connected interface from having its form factor set to 'virtual' 2016-08-15 15:52:06 -04:00
Jeremy Stretch
8faa16c831 Fixes #460: For real this time 2016-08-15 15:39:48 -04:00
Jeremy Stretch
4f774f8ba6 Fixes #460: Corrected ordering of IP addresses with differing prefix lengths 2016-08-13 01:02:03 -04:00
Jeremy Stretch
bf1b8ab9b8 Enable custom export templates for Tenants 2016-08-12 13:29:24 -04:00
Jeremy Stretch
b74f338aa1 Fixes #463: Prevent prepopulation of livesearch field with '---------' 2016-08-12 11:24:29 -04:00
Jeremy Stretch
35aa8acd09 Post-release version bump 2016-08-11 13:54:07 -04:00
Jeremy Stretch
6a48b310d2 Merge pull request #459 from digitalocean/develop
Release v1.5.1
2016-08-11 13:53:34 -04:00
Jeremy Stretch
0b4d3446bf Release v1.5.1 2016-08-11 13:47:09 -04:00
Jeremy Stretch
2b8e06faa2 Added Device asset_tag field to API tests 2016-08-11 13:46:44 -04:00
Jeremy Stretch
c86a1123f0 Closes #421: Added asset_tag field to devices 2016-08-11 11:58:21 -04:00
Jeremy Stretch
e2ad1d4be0 Closes #456: Added IP search box to home page 2016-08-11 11:06:56 -04:00
Jeremy Stretch
219f084805 Fixes #457: Added role field to rack edit form 2016-08-11 10:19:50 -04:00
Jeremy Stretch
6f12297dcf Fixes #441: Added openssl-devel to list of dependencies 2016-08-10 22:37:51 -04:00
Jeremy Stretch
098ff961e3 Fixes #454: Correct typecasting on rack export 2016-08-10 22:27:27 -04:00
Jeremy Stretch
74528c6036 Colorized roles in rack and device lists 2016-08-10 22:20:09 -04:00
Jeremy Stretch
3324f397d9 Post-release version bump 2016-08-10 17:48:14 -04:00
Jeremy Stretch
2509405465 Merge pull request #453 from digitalocean/develop
Release v1.5.0
2016-08-10 17:47:48 -04:00
Jeremy Stretch
0f779dd682 Release v1.5.0 2016-08-10 17:41:08 -04:00
Jeremy Stretch
6bf8a7707c Reverting to psycopg2==2.6.1 due to pip installation issues 2016-08-10 17:30:31 -04:00
Jeremy Stretch
8ff752a58b Updated requirements for v1.5 2016-08-10 16:42:42 -04:00
Jeremy Stretch
7938c9211f Removed obsolete setting LOGOUT_URL 2016-08-10 16:08:02 -04:00
Jeremy Stretch
1f4d5b84b3 Added status field to DeviceTestCase tests (Django 1.10 BooleanField issue) 2016-08-10 16:04:44 -04:00
Jeremy Stretch
3a018888c8 Updated docs for new features 2016-08-10 14:09:38 -04:00
Jeremy Stretch
a89fb734a0 Added colored labels for device and rack roles 2016-08-10 13:35:55 -04:00
Jeremy Stretch
b2f03dfb16 Fixed redirect after editing RackGroups and VLANGroups 2016-08-10 13:13:26 -04:00
Jeremy Stretch
cbde6e8321 Miscellaneous query optimizations 2016-08-10 13:07:11 -04:00
Jeremy Stretch
ed03449164 Closes #241: Introduced rack roles 2016-08-10 11:52:27 -04:00
Jeremy Stretch
47a89999b8 Extended DCIM API to include a device modules list 2016-08-10 10:44:37 -04:00
Jeremy Stretch
a7dd2695a2 Closes #157: Added manufacturer field to module model 2016-08-10 10:24:47 -04:00
Jeremy Stretch
8035538b74 Fixes #445: Limit rack height to between 1U and 100U (inclusive) 2016-08-09 17:19:23 -04:00
Jeremy Stretch
474b19d927 We have a logo! 2016-08-09 14:57:14 -04:00
Jeremy Stretch
31ebbb3324 Fixes #444: Corrected prefix model validation 2016-08-09 09:50:50 -04:00
Jeremy Stretch
bddd29c99f Fixes #443: Correctly display and initialize VRF for creation of new IP addresses 2016-08-08 21:44:54 -04:00
Jeremy Stretch
e7116b81a4 #180: Added type and width fields to Rack model 2016-08-08 18:01:15 -04:00
Jeremy Stretch
d463161619 Closes #149: Added upstream_speed field to Circuit 2016-08-08 16:51:19 -04:00
Jeremy Stretch
5116db3344 Fixes #442: Correct child device import instructions 2016-08-08 12:28:38 -04:00
Jeremy Stretch
b131fbd774 Corred typo in HTML 2016-08-08 12:04:20 -04:00
Jeremy Stretch
324a5e10d7 Fixes #433: Correct form validation when editing child devices 2016-08-08 09:45:44 -04:00
Jeremy Stretch
69debfdefa Post-release version bump 2016-08-06 16:32:54 -04:00
Jeremy Stretch
93fccd5985 Merge pull request #438 from digitalocean/develop
Release v1.4.2
2016-08-06 16:31:32 -04:00
Jeremy Stretch
e55acf8c63 Migration for new interface form factors added in #167 2016-08-06 16:27:00 -04:00
Jeremy Stretch
c19e358eef Making PEP8 happy 2016-08-06 16:19:44 -04:00
Jeremy Stretch
efe7b46021 Release v1.4.2 2016-08-06 16:16:35 -04:00
Jeremy Stretch
ededd3f464 Fixes #253: Added ability to search by prefix to IP address filters 2016-08-06 16:02:57 -04:00
Jeremy Stretch
ac2aa7ea89 Fixes #435: Added a "add prefix" button to the VLAN view 2016-08-06 15:44:28 -04:00
Jeremy Stretch
04c9ebd46d Fixes #434: Increased user actions history on home page from 15 to 50; restored admin UI access but disabled bulk deletion function 2016-08-06 15:35:13 -04:00
Jeremy Stretch
c3c3b80cd9 Fixed toggling of secret lock/unlock buttons 2016-08-04 14:42:34 -04:00
Jeremy Stretch
29c4394e64 Fixes #429: Correct redirection of user when adding a secret to a device 2016-08-04 14:37:38 -04:00
Jeremy Stretch
76b9a1c3af #167: Added new interface form factors 2016-08-04 13:38:45 -04:00
Jeremy Stretch
6184eb6664 Fixes #425: Ignore leading and trailing periods when generating a slug 2016-08-04 11:52:55 -04:00
Jeremy Stretch
e413012cbb Fixes #427: Prevent error when duplicate IPs are present in a prefix's IP list 2016-08-04 11:48:30 -04:00
Jeremy Stretch
ea2e734ba8 Post-release version bump 2016-08-03 17:49:28 -04:00
Jeremy Stretch
4ee63f4ff8 Post-release version bump 2016-08-03 17:49:08 -04:00
Jeremy Stretch
946a1b751b Merge pull request #423 from digitalocean/develop
Release v1.4.1
2016-08-03 17:46:13 -04:00
Jeremy Stretch
4ab40c4489 Release v1.4.1 2016-08-03 17:39:57 -04:00
Jeremy Stretch
7944ee6419 Fixes #422: Added ability to encapsulate within double quotes values which contain commas 2016-08-03 17:33:15 -04:00
Jeremy Stretch
72690bfd0a Potential fix for #419: Ditch annotation in favor of discrete queries to gather Tenant stats 2016-08-03 14:24:09 -04:00
Jeremy Stretch
0f0d0c150a Merge pull request #420 from digitalocean/free_ip_ranges
Fixes #289: Annotate free IP ranges within a prefix
2016-08-03 14:09:36 -04:00
Jeremy Stretch
3b9ac3b986 More intelligent handling of first/last IPs 2016-08-03 12:30:29 -04:00
Jeremy Stretch
79b1bbb9e1 Fixed calculation of available IPs between two existing IPs 2016-08-03 12:20:24 -04:00
Jeremy Stretch
533b4082d8 Fixed calculation of last_ip_in_prefix for IPv6 2016-08-03 12:06:17 -04:00
Jeremy Stretch
81d955ab7d Rewrote add_available_ipaddresses() to be much more efficient and IPv6-friendly 2016-08-03 12:00:35 -04:00
Jeremy Stretch
57373c9d6f Initial work on #289 2016-08-02 17:20:12 -04:00
Jeremy Stretch
bc9158a74f Closes #412: Tenant group assignment is no longer mandatory 2016-08-02 16:04:25 -04:00
Jeremy Stretch
9f3647cd53 Addresses #395: Show child prefixes from all VRFs if the parent prefix is in the global table 2016-08-02 15:48:12 -04:00
Jeremy Stretch
d294e916a4 Fixes #406: Corrected ordering of port_speed and commit_rate in CircuitTable 2016-08-02 10:50:25 -04:00
Jeremy Stretch
249faffe42 Fixes #409: Filter IPs and prefixes by tenant slug rather than by its PK 2016-08-02 10:39:42 -04:00
Jeremy Stretch
3327954a34 Fixes #411: Corrected name of secret roles page 2016-08-02 10:27:58 -04:00
Jeremy Stretch
3b76377cac Post-release version bump 2016-08-01 13:44:22 -04:00
Jeremy Stretch
9889e120bd Merge pull request #408 from digitalocean/develop
Release v1.4.0
2016-08-01 13:43:48 -04:00
Jeremy Stretch
00e0fb5798 Pre-release version bump 2016-08-01 13:35:49 -04:00
Jeremy Stretch
1fd189f9b1 Replaced most glyphicons with Font Awesome 2016-08-01 13:29:45 -04:00
Jeremy Stretch
b73f980eb2 Closes #176: Added initial_data fixtures for new installs 2016-08-01 12:29:26 -04:00
Jeremy Stretch
65ea2af4b7 Partial conversion from glyphicons to font awesome 2016-07-29 18:04:38 -04:00
Jeremy Stretch
8a9c6ce37a Standardized display of attributes for primary objects 2016-07-29 15:31:35 -04:00
Jeremy Stretch
1bbe7f95d6 PEP8 cleanup 2016-07-29 14:46:29 -04:00
Jeremy Stretch
d09ede8d1f Corrected omitted variable 2016-07-29 14:29:13 -04:00
Jeremy Stretch
bcb9ab7116 Show graphs button only if there is at least one graph to display 2016-07-29 13:19:58 -04:00
Jeremy Stretch
75c3e62ca8 Changed VRF and VLAN views to use PrefixBriefTable 2016-07-29 13:03:44 -04:00
Jeremy Stretch
38aee33df0 Closes #358: Improved search of all objects 2016-07-29 12:51:23 -04:00
Jeremy Stretch
2daffdf087 Added account field to provider table 2016-07-29 12:33:40 -04:00
Jeremy Stretch
03d71f9764 Standardized breadcrumb hierarchies 2016-07-29 11:20:47 -04:00
Jeremy Stretch
fa906c74c0 Fixed actions column alignment 2016-07-29 10:57:09 -04:00
Jeremy Stretch
d933d034e0 Changed TenantGroup edit link to a button 2016-07-28 16:03:59 -04:00
Jeremy Stretch
397943b222 Allow unassigning VRF and tenants when editing objects in bulk 2016-07-28 15:59:49 -04:00
Jeremy Stretch
6b41794e12 Implemented bulk editing for sites 2016-07-28 15:30:29 -04:00
Jeremy Stretch
b6e5bafd65 Replaced edit links with buttons 2016-07-28 15:04:33 -04:00
Jeremy Stretch
e6c06b39e8 Adds tenant assignment to Prefix and IPAddress objects 2016-07-28 13:50:46 -04:00
Jeremy Stretch
a25534f3de Fixes #397: Only include child IPs which belong to the same VRF as the parent prefix 2016-07-28 11:24:25 -04:00
Jeremy Stretch
aa6c840c45 Fixes #392: Don't include child devices in non-racked devices table 2016-07-28 10:06:25 -04:00
Jeremy Stretch
aee9314bbf Added tenancy page 2016-07-28 09:47:16 -04:00
Jeremy Stretch
3bb10bca1b Linkified VRF column in prefix and IP address tables 2016-07-27 16:52:20 -04:00
Jeremy Stretch
1f9e4dc707 Fixed platform selection during bulk editing of devices 2016-07-27 16:38:21 -04:00
Jeremy Stretch
76efea87ff Closes #394: Added global option to VRF selection widget during bulk editing 2016-07-27 16:29:30 -04:00
Jeremy Stretch
483ad256a8 Miscellaneous API query optimizations 2016-07-27 14:40:19 -04:00
Jeremy Stretch
618566abe8 Added VRF stats to home page 2016-07-27 14:13:07 -04:00
Jeremy Stretch
1413f5d89e Merge pull request #393 from digitalocean/multitenancy
Multitenancy
2016-07-27 14:05:48 -04:00
Jeremy Stretch
4cc84aed5a PEP8 fix 2016-07-27 13:59:18 -04:00
Jeremy Stretch
300e67388b Tenancy-related API cleanup 2016-07-27 13:42:17 -04:00
Jeremy Stretch
2981ead41b Extended IPAM API to support tenancy 2016-07-27 13:37:55 -04:00
Jeremy Stretch
e4960873f3 Added stats to tenant view 2016-07-27 11:56:47 -04:00
Jeremy Stretch
2abee211a2 Implemented tenancy for VRFs and VLANs 2016-07-27 11:29:20 -04:00
Jeremy Stretch
65b008a493 Cleaned up migrations 2016-07-26 18:01:01 -04:00
Jeremy Stretch
2236d2f941 Fixed tenant assignment on bulk edit of racks, devices 2016-07-26 17:49:41 -04:00
Jeremy Stretch
41b2b7dbf6 Fixed Tenant import 2016-07-26 17:47:40 -04:00
Jeremy Stretch
27c21237ff Added description to Tenant model 2016-07-26 17:44:32 -04:00
Jeremy Stretch
faa12abc70 Enabled filtering of sites, racks, and devices by tenant 2016-07-26 17:28:46 -04:00
Jeremy Stretch
7ca4c816c0 Added related_name to tenant fields on Site, Rack, and Device 2016-07-26 17:16:03 -04:00
Jeremy Stretch
6f68628377 Added tenant to circuit bulk editing; enabled filtering of circuits by tenant 2016-07-26 17:10:11 -04:00
Jeremy Stretch
82a98f0e8f Applied tenancy to sites, racks, and devices 2016-07-26 16:46:22 -04:00
Jeremy Stretch
1939db1574 Added tenant to import/export of Circuits 2016-07-26 15:48:48 -04:00
Jeremy Stretch
06af05708a Applied tenancy to circuits 2016-07-26 15:42:26 -04:00
Jeremy Stretch
fa2ccc1c18 Initial multitenancy implementation 2016-07-26 14:58:37 -04:00
Jeremy Stretch
b790d7d50f Post-release version bump 2016-07-26 12:24:32 -04:00
Jeremy Stretch
af5dba2e0d Merge pull request #386 from digitalocean/develop
Release v1.3.2
2016-07-26 12:22:29 -04:00
Jeremy Stretch
8cb38de7d5 Merge pull request #357 from digitalocean/develop
Release v1.3.1
2016-07-21 11:48:40 -04:00
Jeremy Stretch
5ba5e8def9 Merge pull request #324 from digitalocean/develop
Release v1.3.0
2016-07-18 13:49:08 -04:00
Jeremy Stretch
4e64e1ea95 Merge pull request #299 from digitalocean/develop
Release v1.2.2
2016-07-14 15:21:40 -04:00
Jeremy Stretch
300aff71bb Merge pull request #286 from digitalocean/develop
Release v1.2.1
2016-07-13 12:08:48 -04:00
Jeremy Stretch
0c3970233e Merge pull request #269 from digitalocean/develop
Release v1.2.0
2016-07-12 11:37:56 -04:00
159 changed files with 3703 additions and 894 deletions

View File

@@ -1,4 +1,4 @@
# NetBox
![NetBox](docs/netbox_logo.png "NetBox logo")
NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers.

View File

@@ -10,15 +10,21 @@ Sites can be assigned an optional facility ID to identify the actual facility ho
# Racks
Within each site exist one or more racks. Each rack within NetBox represents a physical two- or four-post equipment rack in which equipment is mounted. Rack height is measured in *rack units *(U); most racks are between 42U and 48U, but NetBox allows you to define racks of any height. Each rack has two faces (front and rear) on which devices can be mounted.
Within each site exist one or more racks. Each rack within NetBox represents a physical two- or four-post equipment rack in which equipment is mounted. Rack height is measured in *rack units* (U); most racks are between 42U and 48U, but NetBox allows you to define racks of any height. Each rack has two faces (front and rear) on which devices can be mounted.
Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, M204.313) whereas internally you refer to is simply as "R113." The facility ID can alternatively be used to store a rack's serial number.
The available rack types include 2- and 4-post frames, 4-post cabinet, and wall-mounted frame and cabinet. Rail-to-rail width may be 19 or 23 inches.
### Rack Groups
Racks can be arranged into groups. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site is a campus, each group might be a building. If each site is a building, each rack group might be a floor or room.
Each group is assigned to a parent site for easy navigation. Hierarchical recursion of rack groups is not currently supported.
Each group is assigned to a parent site for easy navigation. Hierarchical recursion of rack groups is not supported.
### Rack Roles
Each rak can optionally be assigned to a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices.
---
@@ -74,7 +80,7 @@ The assignment of platforms to devices is an entirely optional feature, and may
### Modules
A device can be assigned modules which represent internal components. Currently, these are used merely for inventory tracking, although future development might see their functionality expand.
A device can be assigned modules which represent internal components. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Each module can optionally be assigned to a manufacturer.
### Components

View File

@@ -0,0 +1,22 @@
NetBox supports the concept of individual tenants within its parent organization. Typically, these are used to represent individual customers or internal departments.
# Tenants
A tenant represents a discrete organization. Certain resources within NetBox can be assigned to a tenant. This makes it very convenient to track which resources are assigned to which customers, for instance.
The following objects can be assigned to tenants:
* Sites
* Racks
* Devices
* VRFs
* Prefixes
* IP addresses
* VLANs
* Circuits
If a prefix or IP address is not assigned to a tenant, it will appear to inherit the tenant to which its parent VRF is assigned, if any.
### Tenant Groups
Tenants can be grouped by type. For instance, you might create one group called "Customers" and one called "Acquisitions." The assignment of tenants to groups is optional.

View File

@@ -10,9 +10,10 @@ NetBox requires following system dependencies:
* libffi-dev
* graphviz
* libpq-dev
* libssl-dev
```
# sudo apt-get install -y python2.7 python-dev git python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev
# sudo apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
```
You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.
@@ -163,6 +164,18 @@ Are you sure you want to do this?
Type 'yes' to continue, or 'no' to cancel: yes
```
# Load Initial Data (Optional)
NetBox ships with some initial data to help you get started: RIR definitions, common devices roles, etc. You can delete any seed data that you don't want to keep.
!!! note
This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch.
```
# ./manage.py loaddata initial_data
Installed 43 object(s) from 4 fixture(s)
```
# Test the Application
At this point, NetBox should be able to run. We can verify this by starting a development instance:

BIN
docs/netbox_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -17,6 +17,7 @@ pages:
- 'DCIM': 'data-model/dcim.md'
- 'IPAM': 'data-model/ipam.md'
- 'Secrets': 'data-model/secrets.md'
- 'Tenancy': 'data-model/tenancy.md'
- 'Extras': 'data-model/extras.md'
- 'API Integration': 'api-integration.md'

View File

@@ -21,10 +21,11 @@ class CircuitTypeAdmin(admin.ModelAdmin):
@admin.register(Circuit)
class CircuitAdmin(admin.ModelAdmin):
list_display = ['cid', 'provider', 'type', 'site', 'install_date', 'port_speed', 'commit_rate', 'xconnect_id']
list_filter = ['provider']
list_display = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed_human',
'upstream_speed_human', 'commit_rate_human', 'xconnect_id']
list_filter = ['provider', 'type', 'tenant']
exclude = ['interface']
def get_queryset(self, request):
qs = super(CircuitAdmin, self).get_queryset(request)
return qs.select_related('provider', 'type', 'site')
return qs.select_related('provider', 'type', 'tenant', 'site')

View File

@@ -2,6 +2,7 @@ from rest_framework import serializers
from circuits.models import Provider, CircuitType, Circuit
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
from tenancy.api.serializers import TenantNestedSerializer
#
@@ -45,13 +46,14 @@ class CircuitTypeNestedSerializer(CircuitTypeSerializer):
class CircuitSerializer(serializers.ModelSerializer):
provider = ProviderNestedSerializer()
type = CircuitTypeNestedSerializer()
tenant = TenantNestedSerializer()
site = SiteNestedSerializer()
interface = InterfaceNestedSerializer()
class Meta:
model = Circuit
fields = ['id', 'cid', 'provider', 'type', 'site', 'interface', 'install_date', 'port_speed', 'commit_rate',
'xconnect_id', 'comments']
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed',
'upstream_speed', 'commit_rate', 'xconnect_id', 'comments']
class CircuitNestedSerializer(CircuitSerializer):

View File

@@ -42,7 +42,7 @@ class CircuitListView(generics.ListAPIView):
"""
List circuits (filterable)
"""
queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
serializer_class = serializers.CircuitSerializer
filter_class = CircuitFilter
@@ -51,5 +51,5 @@ class CircuitDetailView(generics.RetrieveAPIView):
"""
Retrieve a single circuit
"""
queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
serializer_class = serializers.CircuitSerializer

View File

@@ -3,6 +3,7 @@ import django_filters
from django.db.models import Q
from dcim.models import Site
from tenancy.models import Tenant
from .models import Provider, Circuit, CircuitType
@@ -28,10 +29,10 @@ class ProviderFilter(django_filters.FilterSet):
fields = ['q', 'name', 'account', 'asn']
def search(self, queryset, value):
value = value.strip()
return queryset.filter(
Q(name__icontains=value) |
Q(account__icontains=value)
Q(account__icontains=value) |
Q(comments__icontains=value)
)
@@ -62,6 +63,17 @@ class CircuitFilter(django_filters.FilterSet):
to_field_name='slug',
label='Circuit type (slug)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
@@ -79,5 +91,9 @@ class CircuitFilter(django_filters.FilterSet):
fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'site_id', 'site', 'interface', 'install_date']
def search(self, queryset, value):
value = value.strip()
return queryset.filter(cid__icontains=value)
return queryset.filter(
Q(cid__icontains=value) |
Q(xconnect_id__icontains=value) |
Q(pp_info__icontains=value) |
Q(comments__icontains=value)
)

View File

@@ -0,0 +1,26 @@
[
{
"model": "circuits.circuittype",
"pk": 1,
"fields": {
"name": "Internet",
"slug": "internet"
}
},
{
"model": "circuits.circuittype",
"pk": 2,
"fields": {
"name": "Private WAN",
"slug": "private-wan"
}
},
{
"model": "circuits.circuittype",
"pk": 3,
"fields": {
"name": "Out-of-Band",
"slug": "out-of-band"
}
}
]

View File

@@ -2,6 +2,8 @@ from django import forms
from django.db.models import Count
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
from tenancy.forms import bulkedit_tenant_choices
from tenancy.models import Tenant
from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, Livesearch, SmallTextarea, SlugField,
)
@@ -99,8 +101,8 @@ class CircuitForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Circuit
fields = [
'cid', 'type', 'provider', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
'port_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
'port_speed', 'upstream_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
]
help_texts = {
'cid': "Unique circuit ID",
@@ -160,13 +162,15 @@ class CircuitFromCSVForm(forms.ModelForm):
error_messages={'invalid_choice': 'Provider not found.'})
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Invalid circuit type.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
site = forms.ModelChoiceField(Site.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Site not found.'})
class Meta:
model = Circuit
fields = ['cid', 'provider', 'type', 'site', 'install_date', 'port_speed', 'commit_rate', 'xconnect_id',
'pp_info']
fields = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'upstream_speed',
'commit_rate', 'xconnect_id', 'pp_info']
class CircuitImportForm(BulkImportForm, BootstrapMixin):
@@ -177,6 +181,7 @@ class CircuitBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
comments = CommentField()
@@ -192,6 +197,11 @@ def circuit_provider_choices():
return [(p.slug, u'{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
def circuit_tenant_choices():
tenant_choices = Tenant.objects.annotate(circuit_count=Count('circuits'))
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in tenant_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]
@@ -201,5 +211,7 @@ class CircuitFilterForm(forms.Form, BootstrapMixin):
type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices)
provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=circuit_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-07-26 21:59
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0001_initial'),
('circuits', '0003_provider_32bit_asn_support'),
]
operations = [
migrations.AddField(
model_name='circuit',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.Tenant'),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-08 20:24
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0004_circuit_add_tenant'),
]
operations = [
migrations.AddField(
model_name='circuit',
name='upstream_speed',
field=models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed', null=True, verbose_name=b'Upstream speed (Kbps)'),
),
]

View File

@@ -3,6 +3,7 @@ from django.db import models
from dcim.fields import ASNField
from dcim.models import Site, Interface
from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel
@@ -66,10 +67,13 @@ class Circuit(CreatedUpdatedModel):
cid = models.CharField(max_length=50, verbose_name='Circuit ID')
provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT)
tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT)
site = models.ForeignKey(Site, related_name='circuits', on_delete=models.PROTECT)
interface = models.OneToOneField(Interface, related_name='circuit', blank=True, null=True)
install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)')
upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)',
help_text='Upstream speed, if different from port speed')
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
@@ -90,9 +94,11 @@ class Circuit(CreatedUpdatedModel):
self.cid,
self.provider.name,
self.type.name,
self.tenant.name if self.tenant else '',
self.site.name,
self.install_date.isoformat() if self.install_date else '',
str(self.port_speed),
str(self.upstream_speed),
str(self.commit_rate) if self.commit_rate else '',
self.xconnect_id,
self.pp_info,
@@ -113,12 +119,18 @@ class Circuit(CreatedUpdatedModel):
else:
return '{} Kbps'.format(speed)
@property
def port_speed_human(self):
return self._humanize_speed(self.port_speed)
port_speed_human.admin_order_field = 'port_speed'
def upstream_speed_human(self):
if not self.upstream_speed:
return ''
return self._humanize_speed(self.upstream_speed)
upstream_speed_human.admin_order_field = 'upstream_speed'
@property
def commit_rate_human(self):
if not self.commit_rate:
return ''
return self._humanize_speed(self.commit_rate)
commit_rate_human.admin_order_field = 'commit_rate'

View File

@@ -6,9 +6,9 @@ from utilities.tables import BaseTable, ToggleColumn
from .models import Circuit, CircuitType, Provider
CIRCUITTYPE_EDIT_LINK = """
CIRCUITTYPE_ACTIONS = """
{% if perms.circuit.change_circuittype %}
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}">Edit</a>
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@@ -21,11 +21,12 @@ class ProviderTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn('circuits:provider', args=[Accessor('slug')], verbose_name='Name')
asn = tables.Column(verbose_name='ASN')
account = tables.Column(verbose_name='Account')
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
class Meta(BaseTable.Meta):
model = Provider
fields = ('pk', 'name', 'asn', 'circuit_count')
fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
#
@@ -37,11 +38,12 @@ class CircuitTypeTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name')
circuit_count = tables.Column(verbose_name='Circuits')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=CIRCUITTYPE_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = CircuitType
fields = ('pk', 'name', 'circuit_count', 'slug', 'edit')
fields = ('pk', 'name', 'circuit_count', 'slug', 'actions')
#
@@ -53,10 +55,13 @@ class CircuitTable(BaseTable):
cid = tables.LinkColumn('circuits:circuit', args=[Accessor('pk')], verbose_name='ID')
type = tables.Column(verbose_name='Type')
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
port_speed_human = tables.Column(verbose_name='Port Speed')
commit_rate_human = tables.Column(verbose_name='Commit Rate')
port_speed = tables.Column(accessor=Accessor('port_speed_human'), order_by=Accessor('port_speed'),
verbose_name='Port Speed')
commit_rate = tables.Column(accessor=Accessor('commit_rate_human'), order_by=Accessor('commit_rate'),
verbose_name='Commit Rate')
class Meta(BaseTable.Meta):
model = Circuit
fields = ('pk', 'cid', 'type', 'provider', 'site', 'port_speed_human', 'commit_rate_human')
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'site', 'port_speed', 'commit_rate')

View File

@@ -2,6 +2,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Count
from django.shortcuts import get_object_or_404, render
from extras.models import Graph, GRAPH_TYPE_PROVIDER
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
@@ -27,10 +28,12 @@ def provider(request, slug):
provider = get_object_or_404(Provider, slug=slug)
circuits = Circuit.objects.filter(provider=provider).select_related('site', 'interface__device')
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
return render(request, 'circuits/provider.html', {
'provider': provider,
'circuits': circuits,
'show_graphs': show_graphs,
})
@@ -109,7 +112,7 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class CircuitListView(ObjectListView):
queryset = Circuit.objects.select_related('provider', 'type', 'site')
queryset = Circuit.objects.select_related('provider', 'type', 'tenant', 'site')
filter = filters.CircuitFilter
filter_form = forms.CircuitFilterForm
table = tables.CircuitTable
@@ -159,6 +162,10 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['tenant'] == 0:
fields_to_update['tenant'] = None
elif form.cleaned_data['tenant']:
fields_to_update['tenant'] = form.cleaned_data['tenant']
for field in ['type', 'provider', 'port_speed', 'commit_rate', 'comments']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]

View File

@@ -4,7 +4,7 @@ from django.db.models import Count
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site,
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, Site,
)
@@ -24,9 +24,17 @@ class RackGroupAdmin(admin.ModelAdmin):
}
@admin.register(RackRole)
class RackRoleAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'color']
prepopulated_fields = {
'slug': ['name'],
}
@admin.register(Rack)
class RackAdmin(admin.ModelAdmin):
list_display = ['name', 'facility_id', 'site', 'u_height']
list_display = ['name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height']
#
@@ -175,7 +183,8 @@ class DeviceAdmin(admin.ModelAdmin):
DeviceBayAdmin,
ModuleAdmin,
]
list_display = ['display_name', 'device_type', 'device_role', 'primary_ip', 'rack', 'position', 'serial']
list_display = ['display_name', 'device_type', 'device_role', 'primary_ip', 'rack', 'position', 'asset_tag',
'serial']
list_filter = ['device_role']
def get_queryset(self, request):

View File

@@ -3,9 +3,10 @@ from rest_framework import serializers
from ipam.models import IPAddress
from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
)
from tenancy.api.serializers import TenantNestedSerializer
#
@@ -13,10 +14,11 @@ from dcim.models import (
#
class SiteSerializer(serializers.ModelSerializer):
tenant = TenantNestedSerializer()
class Meta:
model = Site
fields = ['id', 'name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
@@ -44,6 +46,23 @@ class RackGroupNestedSerializer(RackGroupSerializer):
fields = ['id', 'name', 'slug']
#
# Rack roles
#
class RackRoleSerializer(serializers.ModelSerializer):
class Meta:
model = RackRole
fields = ['id', 'name', 'slug', 'color']
class RackRoleNestedSerializer(RackRoleSerializer):
class Meta(RackRoleSerializer.Meta):
fields = ['id', 'name', 'slug']
#
# Racks
#
@@ -52,10 +71,13 @@ class RackGroupNestedSerializer(RackGroupSerializer):
class RackSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
group = RackGroupNestedSerializer()
tenant = TenantNestedSerializer()
role = RackRoleNestedSerializer()
class Meta:
model = Rack
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'u_height', 'comments']
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
'u_height', 'comments']
class RackNestedSerializer(RackSerializer):
@@ -69,8 +91,8 @@ class RackDetailSerializer(RackSerializer):
rear_units = serializers.SerializerMethodField()
class Meta(RackSerializer.Meta):
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'u_height', 'comments', 'front_units',
'rear_units']
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
'u_height', 'comments', 'front_units', 'rear_units']
def get_front_units(self, obj):
units = obj.get_rack_units(face=RACK_FACE_FRONT)
@@ -218,6 +240,7 @@ class DeviceIPAddressNestedSerializer(serializers.ModelSerializer):
class DeviceSerializer(serializers.ModelSerializer):
device_type = DeviceTypeNestedSerializer()
device_role = DeviceRoleNestedSerializer()
tenant = TenantNestedSerializer()
platform = PlatformNestedSerializer()
rack = RackNestedSerializer()
primary_ip = DeviceIPAddressNestedSerializer()
@@ -227,8 +250,9 @@ class DeviceSerializer(serializers.ModelSerializer):
class Meta:
model = Device
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'platform', 'serial', 'rack', 'position',
'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments']
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial',
'asset_tag', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
'primary_ip6', 'comments']
def get_parent_device(self, obj):
try:
@@ -380,6 +404,25 @@ class DeviceBayDetailSerializer(DeviceBaySerializer):
fields = ['id', 'device', 'name', 'installed_device']
#
# Modules
#
class ModuleSerializer(serializers.ModelSerializer):
device = DeviceNestedSerializer()
manufacturer = ManufacturerNestedSerializer()
class Meta:
model = Module
fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered']
class ModuleNestedSerializer(ModuleSerializer):
class Meta(ModuleSerializer.Meta):
fields = ['id', 'device', 'parent', 'name']
#
# Interface connections
#

View File

@@ -18,6 +18,10 @@ urlpatterns = [
url(r'^rack-groups/$', RackGroupListView.as_view(), name='rackgroup_list'),
url(r'^rack-groups/(?P<pk>\d+)/$', RackGroupDetailView.as_view(), name='rackgroup_detail'),
# Rack roles
url(r'^rack-roles/$', RackRoleListView.as_view(), name='rackrole_list'),
url(r'^rack-roles/(?P<pk>\d+)/$', RackRoleDetailView.as_view(), name='rackrole_detail'),
# Racks
url(r'^racks/$', RackListView.as_view(), name='rack_list'),
url(r'^racks/(?P<pk>\d+)/$', RackDetailView.as_view(), name='rack_detail'),
@@ -50,6 +54,7 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/power-outlets/$', PowerOutletListView.as_view(), name='device_poweroutlets'),
url(r'^devices/(?P<pk>\d+)/interfaces/$', InterfaceListView.as_view(), name='device_interfaces'),
url(r'^devices/(?P<pk>\d+)/device-bays/$', DeviceBayListView.as_view(), name='device_devicebays'),
url(r'^devices/(?P<pk>\d+)/modules/$', ModuleListView.as_view(), name='device_modules'),
# Console ports
url(r'^console-ports/(?P<pk>\d+)/$', ConsolePortView.as_view(), name='consoleport'),

View File

@@ -10,7 +10,7 @@ from django.shortcuts import get_object_or_404
from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface,
InterfaceConnection, Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site,
InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
)
from dcim import filters
from .exceptions import MissingFilterException
@@ -27,7 +27,7 @@ class SiteListView(generics.ListAPIView):
"""
List all sites
"""
queryset = Site.objects.all()
queryset = Site.objects.select_related('tenant')
serializer_class = serializers.SiteSerializer
@@ -35,7 +35,7 @@ class SiteDetailView(generics.RetrieveAPIView):
"""
Retrieve a single site
"""
queryset = Site.objects.all()
queryset = Site.objects.select_related('tenant')
serializer_class = serializers.SiteSerializer
@@ -47,7 +47,7 @@ class RackGroupListView(generics.ListAPIView):
"""
List all rack groups
"""
queryset = RackGroup.objects.all()
queryset = RackGroup.objects.select_related('site')
serializer_class = serializers.RackGroupSerializer
filter_class = filters.RackGroupFilter
@@ -56,10 +56,30 @@ class RackGroupDetailView(generics.RetrieveAPIView):
"""
Retrieve a single rack group
"""
queryset = RackGroup.objects.all()
queryset = RackGroup.objects.select_related('site')
serializer_class = serializers.RackGroupSerializer
#
# Rack roles
#
class RackRoleListView(generics.ListAPIView):
"""
List all rack roles
"""
queryset = RackRole.objects.all()
serializer_class = serializers.RackRoleSerializer
class RackRoleDetailView(generics.RetrieveAPIView):
"""
Retrieve a single rack role
"""
queryset = RackRole.objects.all()
serializer_class = serializers.RackRoleSerializer
#
# Racks
#
@@ -68,7 +88,7 @@ class RackListView(generics.ListAPIView):
"""
List racks (filterable)
"""
queryset = Rack.objects.select_related('site')
queryset = Rack.objects.select_related('site', 'group', 'tenant')
serializer_class = serializers.RackSerializer
filter_class = filters.RackFilter
@@ -77,7 +97,7 @@ class RackDetailView(generics.RetrieveAPIView):
"""
Retrieve a single rack
"""
queryset = Rack.objects.select_related('site')
queryset = Rack.objects.select_related('site', 'group', 'tenant')
serializer_class = serializers.RackDetailSerializer
@@ -193,8 +213,9 @@ class DeviceListView(generics.ListAPIView):
"""
List devices (filterable)
"""
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'platform', 'rack__site')\
.prefetch_related('primary_ip4__nat_outside', 'primary_ip6__nat_outside')
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
'rack__site', 'parent_bay').prefetch_related('primary_ip4__nat_outside',
'primary_ip6__nat_outside')
serializer_class = serializers.DeviceSerializer
filter_class = filters.DeviceFilter
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
@@ -204,7 +225,8 @@ class DeviceDetailView(generics.RetrieveAPIView):
"""
Retrieve a single device
"""
queryset = Device.objects.all()
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
'rack__site', 'parent_bay')
serializer_class = serializers.DeviceSerializer
@@ -347,18 +369,23 @@ class DeviceBayListView(generics.ListAPIView):
def get_queryset(self):
device = get_object_or_404(Device, pk=self.kwargs['pk'])
queryset = DeviceBay.objects.filter(device=device).select_related('installed_device')
return DeviceBay.objects.filter(device=device).select_related('installed_device')
# Filter by type (physical or virtual)
iface_type = self.request.query_params.get('type')
if iface_type == 'physical':
queryset = queryset.exclude(form_factor=IFACE_FF_VIRTUAL)
elif iface_type == 'virtual':
queryset = queryset.filter(form_factor=IFACE_FF_VIRTUAL)
elif iface_type is not None:
queryset = queryset.empty()
return queryset
#
# Modules
#
class ModuleListView(generics.ListAPIView):
"""
List device modules (by device)
"""
serializer_class = serializers.ModuleSerializer
def get_queryset(self):
device = get_object_or_404(Device, pk=self.kwargs['pk'])
return Module.objects.filter(device=device).select_related('device', 'manufacturer')
#

View File

@@ -4,8 +4,9 @@ from django.db.models import Q
from .models import (
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site,
Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
)
from tenancy.models import Tenant
class SiteFilter(django_filters.FilterSet):
@@ -13,17 +14,27 @@ class SiteFilter(django_filters.FilterSet):
action='search',
label='Search',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
class Meta:
model = Site
fields = ['q', 'name', 'facility', 'asn']
def search(self, queryset, value):
value = value.strip()
qs_filter = Q(name__icontains=value) | Q(facility__icontains=value) | Q(physical_address__icontains=value) | \
Q(shipping_address__icontains=value)
Q(shipping_address__icontains=value) | Q(comments__icontains=value)
try:
qs_filter |= Q(asn=int(value))
qs_filter |= Q(asn=int(value.strip()))
except ValueError:
pass
return queryset.filter(qs_filter)
@@ -74,16 +85,38 @@ class RackFilter(django_filters.FilterSet):
to_field_name='slug',
label='Group',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
name='role',
queryset=RackRole.objects.all(),
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
name='role',
queryset=RackRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
class Meta:
model = Rack
fields = ['q', 'site_id', 'site', 'u_height']
def search(self, queryset, value):
value = value.strip()
return queryset.filter(
Q(name__icontains=value) |
Q(facility_id__icontains=value)
Q(facility_id__icontains=value) |
Q(comments__icontains=value)
)
@@ -143,6 +176,17 @@ class DeviceFilter(django_filters.FilterSet):
to_field_name='slug',
label='Role (slug)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
device_type_id = django_filters.ModelMultipleChoiceFilter(
name='device_type',
queryset=DeviceType.objects.all(),
@@ -195,16 +239,17 @@ class DeviceFilter(django_filters.FilterSet):
class Meta:
model = Device
fields = ['q', 'name', 'site_id', 'site', 'rack_id', 'role_id', 'role', 'device_type_id', 'manufacturer_id',
'manufacturer', 'model', 'platform_id', 'platform', 'status', 'is_console_server', 'is_pdu',
'is_network_device']
fields = ['q', 'name', 'serial', 'asset_tag', 'site_id', 'site', 'rack_id', 'role_id', 'role', 'device_type_id',
'manufacturer_id', 'manufacturer', 'model', 'platform_id', 'platform', 'status', 'is_console_server',
'is_pdu', 'is_network_device']
def search(self, queryset, value):
value = value.strip()
return queryset.filter(
Q(name__icontains=value) |
Q(serial__icontains=value) |
Q(modules__serial__icontains=value)
Q(serial__icontains=value.strip()) |
Q(modules__serial__icontains=value.strip()) |
Q(asset_tag=value.strip()) |
Q(comments__icontains=value)
).distinct()

View File

@@ -0,0 +1,201 @@
[
{
"model": "dcim.devicerole",
"pk": 1,
"fields": {
"name": "Console Server",
"slug": "console-server",
"color": "teal"
}
},
{
"model": "dcim.devicerole",
"pk": 2,
"fields": {
"name": "Core Switch",
"slug": "core-switch",
"color": "blue"
}
},
{
"model": "dcim.devicerole",
"pk": 3,
"fields": {
"name": "Distribution Switch",
"slug": "distribution-switch",
"color": "blue"
}
},
{
"model": "dcim.devicerole",
"pk": 4,
"fields": {
"name": "Access Switch",
"slug": "access-switch",
"color": "blue"
}
},
{
"model": "dcim.devicerole",
"pk": 5,
"fields": {
"name": "Management Switch",
"slug": "management-switch",
"color": "orange"
}
},
{
"model": "dcim.devicerole",
"pk": 6,
"fields": {
"name": "Firewall",
"slug": "firewall",
"color": "red"
}
},
{
"model": "dcim.devicerole",
"pk": 7,
"fields": {
"name": "Router",
"slug": "router",
"color": "purple"
}
},
{
"model": "dcim.devicerole",
"pk": 8,
"fields": {
"name": "Server",
"slug": "server",
"color": "medium_gray"
}
},
{
"model": "dcim.devicerole",
"pk": 9,
"fields": {
"name": "PDU",
"slug": "pdu",
"color": "dark_gray"
}
},
{
"model": "dcim.manufacturer",
"pk": 1,
"fields": {
"name": "APC",
"slug": "apc"
}
},
{
"model": "dcim.manufacturer",
"pk": 2,
"fields": {
"name": "Cisco",
"slug": "cisco"
}
},
{
"model": "dcim.manufacturer",
"pk": 3,
"fields": {
"name": "Dell",
"slug": "dell"
}
},
{
"model": "dcim.manufacturer",
"pk": 4,
"fields": {
"name": "HP",
"slug": "hp"
}
},
{
"model": "dcim.manufacturer",
"pk": 5,
"fields": {
"name": "Juniper",
"slug": "juniper"
}
},
{
"model": "dcim.manufacturer",
"pk": 6,
"fields": {
"name": "Arista",
"slug": "arista"
}
},
{
"model": "dcim.manufacturer",
"pk": 7,
"fields": {
"name": "Opengear",
"slug": "opengear"
}
},
{
"model": "dcim.manufacturer",
"pk": 8,
"fields": {
"name": "Super Micro",
"slug": "super-micro"
}
},
{
"model": "dcim.platform",
"pk": 1,
"fields": {
"name": "Cisco IOS",
"slug": "cisco-ios",
"rpc_client": "cisco-ios"
}
},
{
"model": "dcim.platform",
"pk": 2,
"fields": {
"name": "Cisco NX-OS",
"slug": "cisco-nx-os",
"rpc_client": ""
}
},
{
"model": "dcim.platform",
"pk": 3,
"fields": {
"name": "Juniper Junos",
"slug": "juniper-junos",
"rpc_client": "juniper-junos"
}
},
{
"model": "dcim.platform",
"pk": 4,
"fields": {
"name": "Arista EOS",
"slug": "arista-eos",
"rpc_client": ""
}
},
{
"model": "dcim.platform",
"pk": 5,
"fields": {
"name": "Linux",
"slug": "linux",
"rpc_client": ""
}
},
{
"model": "dcim.platform",
"pk": 6,
"fields": {
"name": "Opengear",
"slug": "opengear",
"rpc_client": "opengear"
}
}
]

View File

@@ -4,8 +4,10 @@ from django import forms
from django.db.models import Count, Q
from ipam.models import IPAddress
from tenancy.forms import bulkedit_tenant_choices
from tenancy.models import Tenant
from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
APISelect, add_blank_choice, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
)
@@ -13,7 +15,8 @@ from .models import (
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
Interface, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackRole,
Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
)
@@ -38,6 +41,39 @@ def get_device_by_name_or_pk(name):
return device
def bulkedit_platform_choices():
choices = [
(None, '---------'),
(0, 'None'),
]
choices += [(p.pk, p.name) for p in Platform.objects.all()]
return choices
def bulkedit_rackgroup_choices():
"""
Include an option to remove the currently assigned group from a rack.
"""
choices = [
(None, '---------'),
(0, 'None'),
]
choices += [(r.pk, r) for r in RackGroup.objects.all()]
return choices
def bulkedit_rackrole_choices():
"""
Include an option to remove the currently assigned role from a rack.
"""
choices = [
(None, '---------'),
(0, 'None'),
]
choices += [(r.pk, r.name) for r in RackRole.objects.all()]
return choices
#
# Sites
#
@@ -48,7 +84,7 @@ class SiteForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Site
fields = ['name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments']
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments']
widgets = {
'physical_address': SmallTextarea(attrs={'rows': 3}),
'shipping_address': SmallTextarea(attrs={'rows': 3}),
@@ -63,16 +99,33 @@ class SiteForm(forms.ModelForm, BootstrapMixin):
class SiteFromCSVForm(forms.ModelForm):
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
class Meta:
model = Site
fields = ['name', 'slug', 'facility', 'asn']
fields = ['name', 'slug', 'tenant', 'facility', 'asn']
class SiteImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=SiteFromCSVForm)
class SiteBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
def site_tenant_choices():
tenant_choices = Tenant.objects.annotate(site_count=Count('sites'))
return [(t.slug, u'{} ({})'.format(t.name, t.site_count)) for t in tenant_choices]
class SiteFilterForm(forms.Form, BootstrapMixin):
tenant = forms.MultipleChoiceField(required=False, choices=site_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
#
# Rack groups
#
@@ -95,6 +148,18 @@ class RackGroupFilterForm(forms.Form, BootstrapMixin):
widget=forms.SelectMultiple(attrs={'size': 8}))
#
# Rack roles
#
class RackRoleForm(forms.ModelForm, BootstrapMixin):
slug = SlugField()
class Meta:
model = RackRole
fields = ['name', 'slug', 'color']
#
# Racks
#
@@ -107,7 +172,7 @@ class RackForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Rack
fields = ['site', 'group', 'name', 'facility_id', 'u_height', 'comments']
fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'comments']
help_texts = {
'site': "The site at which the rack exists",
'name': "Organizational rack name",
@@ -135,10 +200,15 @@ class RackFromCSVForm(forms.ModelForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Site not found.'})
group_name = forms.CharField(required=False)
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
role = forms.ModelChoiceField(RackRole.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Role not found.'})
type = forms.CharField(required=False)
class Meta:
model = Rack
fields = ['site', 'group_name', 'name', 'facility_id', 'u_height']
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height']
def clean(self):
@@ -152,6 +222,19 @@ class RackFromCSVForm(forms.ModelForm):
except RackGroup.DoesNotExist:
self.add_error('group_name', "Invalid rack group ({})".format(group))
def clean_type(self):
rack_type = self.cleaned_data['type']
if not rack_type:
return None
try:
choices = {v.lower(): k for k, v in RACK_TYPE_CHOICES}
return choices[rack_type.lower()]
except KeyError:
raise forms.ValidationError('Invalid rack type ({}). Valid choices are: {}.'.format(
rack_type,
', '.join({v: k for k, v in RACK_TYPE_CHOICES}),
))
class RackImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=RackFromCSVForm)
@@ -159,8 +242,12 @@ class RackImportForm(BulkImportForm, BootstrapMixin):
class RackBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False)
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site')
group = forms.TypedChoiceField(choices=bulkedit_rackgroup_choices, coerce=int, required=False, label='Group')
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
role = forms.TypedChoiceField(choices=bulkedit_rackrole_choices, coerce=int, required=False, label='Role')
type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
u_height = forms.IntegerField(required=False, label='Height (U)')
comments = CommentField()
@@ -175,11 +262,25 @@ def rack_group_choices():
return [(g.pk, u'{} ({})'.format(g, g.rack_count)) for g in group_choices]
def rack_tenant_choices():
tenant_choices = Tenant.objects.annotate(rack_count=Count('racks'))
return [(t.slug, u'{} ({})'.format(t.name, t.rack_count)) for t in tenant_choices]
def rack_role_choices():
role_choices = RackRole.objects.annotate(rack_count=Count('racks'))
return [(r.slug, u'{} ({})'.format(r.name, r.rack_count)) for r in role_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',
widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
role = forms.MultipleChoiceField(required=False, choices=rack_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
#
@@ -203,8 +304,8 @@ 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',
'is_network_device', 'subdevice_role']
fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
'is_pdu', 'is_network_device', 'subdevice_role']
class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
@@ -324,8 +425,8 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Device
fields = ['name', 'device_role', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
'platform', 'primary_ip4', 'primary_ip6', 'comments']
fields = ['name', 'device_role', 'tenant', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position',
'face', 'status', 'platform', 'primary_ip4', 'primary_ip6', 'comments']
help_texts = {
'device_role': "The function this device serves",
'serial': "Chassis serial number",
@@ -364,7 +465,7 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
self.fields['primary_ip6'].widget.attrs['readonly'] = True
# Limit rack choices
if self.is_bound:
if self.is_bound and self.data.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
@@ -405,11 +506,15 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
self.fields['site'].disabled = True
self.fields['rack'].disabled = True
self.initial['site'] = self.instance.parent_bay.device.rack.site_id
self.initial['rack'] = self.instance.parent_bay.device.rack_id
class BaseDeviceFromCSVForm(forms.ModelForm):
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Invalid device role.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Invalid manufacturer.'})
model_name = forms.CharField()
@@ -441,8 +546,8 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):
face = forms.CharField(required=False)
class Meta(BaseDeviceFromCSVForm.Meta):
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'site', 'rack_name',
'position', 'face']
fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
'site', 'rack_name', 'position', 'face']
def clean(self):
@@ -477,8 +582,8 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
device_bay_name = forms.CharField(required=False)
class Meta(BaseDeviceFromCSVForm.Meta):
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'parent',
'device_bay_name']
fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
'parent', 'device_bay_name']
def clean(self):
@@ -512,8 +617,9 @@ 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')
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, label='Platform')
platform_delete = forms.BooleanField(required=False, label='Set platform to "none"')
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
platform = forms.TypedChoiceField(choices=bulkedit_platform_choices, coerce=int, required=False,
label='Platform')
status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
@@ -533,6 +639,11 @@ def device_role_choices():
return [(r.slug, u'{} ({})'.format(r.name, r.device_count)) for r in role_choices]
def device_tenant_choices():
tenant_choices = Tenant.objects.annotate(device_count=Count('devices'))
return [(t.slug, u'{} ({})'.format(t.name, t.device_count)) for t in tenant_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]
@@ -550,6 +661,8 @@ class DeviceFilterForm(forms.Form, BootstrapMixin):
widget=forms.SelectMultiple(attrs={'size': 8}))
role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=device_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type',
widget=forms.SelectMultiple(attrs={'size': 8}))
platform = forms.MultipleChoiceField(required=False, choices=device_platform_choices)
@@ -1206,4 +1319,4 @@ class ModuleForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Module
fields = ['name', 'part_id', 'serial']
fields = ['name', 'manufacturer', 'part_id', 'serial']

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-07-26 21:59
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0001_initial'),
('dcim', '0011_devicetype_part_number'),
]
operations = [
migrations.AddField(
model_name='device',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='tenancy.Tenant'),
),
migrations.AddField(
model_name='rack',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='tenancy.Tenant'),
),
migrations.AddField(
model_name='site',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sites', to='tenancy.Tenant'),
),
]

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-06 20:24
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0012_site_rack_device_add_tenant'),
]
operations = [
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200),
),
]

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-08 21:11
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0013_add_interface_form_factors'),
]
operations = [
migrations.AddField(
model_name='rack',
name='type',
field=models.PositiveSmallIntegerField(blank=True, choices=[(100, b'2-post frame'), (200, b'4-post frame'), (300, b'4-post cabinet'), (1000, b'Wall-mounted frame'), (1100, b'Wall-mounted cabinet')], null=True, verbose_name=b'Type'),
),
migrations.AddField(
model_name='rack',
name='width',
field=models.PositiveSmallIntegerField(choices=[(19, b'19 inches'), (23, b'23 inches')], default=19, help_text=b'Rail-to-rail width', verbose_name=b'Width'),
),
]

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-09 21:18
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0014_rack_add_type_width'),
]
operations = [
migrations.AlterField(
model_name='rack',
name='u_height',
field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name=b'Height (U)'),
),
]

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-10 13:45
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0015_rack_add_u_height_validator'),
]
operations = [
migrations.AddField(
model_name='module',
name='manufacturer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='modules', to='dcim.Manufacturer'),
),
]

View File

@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-10 14:58
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0016_module_add_manufacturer'),
]
operations = [
migrations.CreateModel(
name='RackRole',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(unique=True)),
('color', models.CharField(choices=[[b'teal', b'Teal'], [b'green', b'Green'], [b'blue', b'Blue'], [b'purple', b'Purple'], [b'yellow', b'Yellow'], [b'orange', b'Orange'], [b'red', b'Red'], [b'light_gray', b'Light Gray'], [b'medium_gray', b'Medium Gray'], [b'dark_gray', b'Dark Gray']], max_length=30)),
],
options={
'ordering': ['name'],
},
),
migrations.AddField(
model_name='rack',
name='role',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='dcim.RackRole'),
),
]

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-08-11 15:42
from __future__ import unicode_literals
from django.db import migrations
import utilities.fields
class Migration(migrations.Migration):
dependencies = [
('dcim', '0017_rack_add_role'),
]
operations = [
migrations.AddField(
model_name='device',
name='asset_tag',
field=utilities.fields.NullableCharField(blank=True, help_text=b'A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name=b'Asset tag'),
),
]

View File

@@ -3,11 +3,12 @@ from collections import OrderedDict
from django.conf import settings
from django.core.exceptions import MultipleObjectsReturned, ValidationError
from django.core.urlresolvers import reverse
from django.core.validators import MinValueValidator
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Count, Q, ObjectDoesNotExist
from extras.rpc import RPC_CLIENTS
from tenancy.models import Tenant
from utilities.fields import NullableCharField
from utilities.managers import NaturalOrderByManager
from utilities.models import CreatedUpdatedModel
@@ -15,6 +16,26 @@ from utilities.models import CreatedUpdatedModel
from .fields import ASNField, MACAddressField
RACK_TYPE_2POST = 100
RACK_TYPE_4POST = 200
RACK_TYPE_CABINET = 300
RACK_TYPE_WALLFRAME = 1000
RACK_TYPE_WALLCABINET = 1100
RACK_TYPE_CHOICES = (
(RACK_TYPE_2POST, '2-post frame'),
(RACK_TYPE_4POST, '4-post frame'),
(RACK_TYPE_CABINET, '4-post cabinet'),
(RACK_TYPE_WALLFRAME, 'Wall-mounted frame'),
(RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'),
)
RACK_WIDTH_19IN = 19
RACK_WIDTH_23IN = 23
RACK_WIDTH_CHOICES = (
(RACK_WIDTH_19IN, '19 inches'),
(RACK_WIDTH_23IN, '23 inches'),
)
RACK_FACE_FRONT = 0
RACK_FACE_REAR = 1
RACK_FACE_CHOICES = [
@@ -40,7 +61,7 @@ COLOR_RED = 'red'
COLOR_GRAY1 = 'light_gray'
COLOR_GRAY2 = 'medium_gray'
COLOR_GRAY3 = 'dark_gray'
DEVICE_ROLE_COLOR_CHOICES = [
ROLE_COLOR_CHOICES = [
[COLOR_TEAL, 'Teal'],
[COLOR_GREEN, 'Green'],
[COLOR_BLUE, 'Blue'],
@@ -56,20 +77,63 @@ DEVICE_ROLE_COLOR_CHOICES = [
IFACE_FF_VIRTUAL = 0
IFACE_FF_100M_COPPER = 800
IFACE_FF_1GE_COPPER = 1000
IFACE_FF_GBIC = 1050
IFACE_FF_SFP = 1100
IFACE_FF_10GE_COPPER = 1150
IFACE_FF_SFP_PLUS = 1200
IFACE_FF_XFP = 1300
IFACE_FF_QSFP_PLUS = 1400
IFACE_FF_CFP = 1500
IFACE_FF_QSFP28 = 1600
IFACE_FF_T1 = 4000
IFACE_FF_E1 = 4010
IFACE_FF_T3 = 4040
IFACE_FF_E3 = 4050
IFACE_FF_STACKWISE = 5000
IFACE_FF_STACKWISE_PLUS = 5050
IFACE_FF_CHOICES = [
[IFACE_FF_VIRTUAL, 'Virtual'],
[IFACE_FF_100M_COPPER, '10/100M (100BASE-TX)'],
[IFACE_FF_1GE_COPPER, '1GE (1000BASE-T)'],
[IFACE_FF_SFP, '1GE (SFP)'],
[IFACE_FF_10GE_COPPER, '10GE (10GBASE-T)'],
[IFACE_FF_SFP_PLUS, '10GE (SFP+)'],
[IFACE_FF_XFP, '10GE (XFP)'],
[IFACE_FF_QSFP_PLUS, '40GE (QSFP+)'],
[
'Virtual interfaces',
[
[IFACE_FF_VIRTUAL, 'Virtual'],
]
],
[
'Ethernet',
[
[IFACE_FF_100M_COPPER, '100BASE-TX (10/100M)'],
[IFACE_FF_1GE_COPPER, '1000BASE-T (1GE)'],
[IFACE_FF_10GE_COPPER, '10GBASE-T (10GE)'],
]
],
[
'Modular',
[
[IFACE_FF_GBIC, 'GBIC (1GE)'],
[IFACE_FF_SFP, 'SFP (1GE)'],
[IFACE_FF_XFP, 'XFP (10GE)'],
[IFACE_FF_SFP_PLUS, 'SFP+ (10GE)'],
[IFACE_FF_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_FF_CFP, 'CFP (100GE)'],
[IFACE_FF_QSFP28, 'QSFP28 (100GE)'],
]
],
[
'Serial',
[
[IFACE_FF_T1, 'T1 (1.544 Mbps)'],
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
[IFACE_FF_T3, 'T3 (45 Mbps)'],
[IFACE_FF_E3, 'E3 (34 Mbps)'],
]
],
[
'Stacking',
[
[IFACE_FF_STACKWISE, 'Cisco StackWise'],
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
]
],
]
STATUS_ACTIVE = True
@@ -139,6 +203,10 @@ def order_interfaces(queryset, sql_col, primary_ordering=tuple()):
}).order_by(*ordering)
#
# Sites
#
class SiteManager(NaturalOrderByManager):
def get_queryset(self):
@@ -152,6 +220,7 @@ class Site(CreatedUpdatedModel):
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='sites', on_delete=models.PROTECT)
facility = models.CharField(max_length=50, blank=True)
asn = ASNField(blank=True, null=True, verbose_name='ASN')
physical_address = models.CharField(max_length=200, blank=True)
@@ -173,6 +242,7 @@ class Site(CreatedUpdatedModel):
return ','.join([
self.name,
self.slug,
self.tenant.name if self.tenant else '',
self.facility,
str(self.asn),
])
@@ -198,6 +268,10 @@ class Site(CreatedUpdatedModel):
return self.circuits.count()
#
# Racks
#
class RackGroup(models.Model):
"""
Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
@@ -222,6 +296,24 @@ class RackGroup(models.Model):
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
class RackRole(models.Model):
"""
Racks can be organized by functional role, similar to Devices.
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES)
class Meta:
ordering = ['name']
def __unicode__(self):
return self.name
def get_absolute_url(self):
return "{}?role={}".format(reverse('dcim:rack_list'), self.slug)
class RackManager(NaturalOrderByManager):
def get_queryset(self):
@@ -237,7 +329,13 @@ class Rack(CreatedUpdatedModel):
facility_id = NullableCharField(max_length=30, blank=True, null=True, verbose_name='Facility ID')
site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT)
group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL)
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)')
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT)
role = models.ForeignKey('RackRole', related_name='racks', blank=True, null=True, on_delete=models.PROTECT)
type = models.PositiveSmallIntegerField(choices=RACK_TYPE_CHOICES, blank=True, null=True, verbose_name='Type')
width = models.PositiveSmallIntegerField(choices=RACK_WIDTH_CHOICES, default=RACK_WIDTH_19IN, verbose_name='Width',
help_text='Rail-to-rail width')
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)',
validators=[MinValueValidator(1), MaxValueValidator(100)])
comments = models.TextField(blank=True)
objects = RackManager()
@@ -272,6 +370,10 @@ class Rack(CreatedUpdatedModel):
self.group.name if self.group else '',
self.name,
self.facility_id or '',
self.tenant.name if self.tenant else '',
self.role.name if self.role else '',
self.get_type_display() if self.type else '',
str(self.width),
str(self.u_height),
])
@@ -579,7 +681,7 @@ class DeviceRole(models.Model):
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
color = models.CharField(max_length=30, choices=DEVICE_ROLE_COLOR_CHOICES)
color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES)
class Meta:
ordering = ['name']
@@ -631,9 +733,12 @@ class Device(CreatedUpdatedModel):
"""
device_type = models.ForeignKey('DeviceType', related_name='instances', on_delete=models.PROTECT)
device_role = models.ForeignKey('DeviceRole', related_name='devices', on_delete=models.PROTECT)
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='devices', on_delete=models.PROTECT)
platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
name = NullableCharField(max_length=50, blank=True, null=True, unique=True)
serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
asset_tag = NullableCharField(max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
help_text='A unique tag used to identify this device')
rack = models.ForeignKey('Rack', related_name='devices', on_delete=models.PROTECT)
position = models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(1)],
verbose_name='Position (U)',
@@ -724,10 +829,12 @@ class Device(CreatedUpdatedModel):
return ','.join([
self.name or '',
self.device_role.name,
self.tenant.name if self.tenant else '',
self.device_type.manufacturer.name,
self.device_type.model,
self.platform.name if self.platform else '',
self.serial,
self.asset_tag if self.asset_tag else '',
self.rack.site.name,
self.rack.name,
str(self.position) if self.position else '',
@@ -925,6 +1032,13 @@ class Interface(models.Model):
def __unicode__(self):
return self.name
def clean(self):
if self.form_factor == IFACE_FF_VIRTUAL and self.is_connected:
raise ValidationError({'form_factor': "Virtual interfaces cannot be connected to another interface or "
"circuit. Disconnect the interface or choose a physical form "
"factor."})
@property
def is_physical(self):
return self.form_factor != IFACE_FF_VIRTUAL
@@ -1023,6 +1137,8 @@ class Module(models.Model):
device = models.ForeignKey('Device', related_name='modules', on_delete=models.CASCADE)
parent = models.ForeignKey('self', related_name='submodules', blank=True, null=True, on_delete=models.CASCADE)
name = models.CharField(max_length=50, verbose_name='Name')
manufacturer = models.ForeignKey('Manufacturer', related_name='modules', blank=True, null=True,
on_delete=models.PROTECT)
part_id = models.CharField(max_length=50, verbose_name='Part ID', blank=True)
serial = models.CharField(max_length=50, verbose_name='Serial number', blank=True)
discovered = models.BooleanField(default=False, verbose_name='Discovered')

View File

@@ -10,36 +10,58 @@ from .models import (
)
COLOR_LABEL = """
<label class="label {{ record.color }}">{{ record }}</label>
"""
DEVICE_LINK = """
<a href="{% url 'dcim:device' pk=record.pk %}">
{{ record.name|default:'<span class="label label-info">Unnamed device</span>' }}
</a>
"""
RACKGROUP_EDIT_LINK = """
RACKGROUP_ACTIONS = """
{% if perms.dcim.change_rackgroup %}
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}">Edit</a>
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
DEVICEROLE_EDIT_LINK = """
RACKROLE_ACTIONS = """
{% if perms.dcim.change_rackrole %}
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
RACK_ROLE = """
{% if record.role %}
<label class="label {{ record.role.color }}">{{ value }}</label>
{% else %}
&mdash;
{% endif %}
"""
DEVICEROLE_ACTIONS = """
{% if perms.dcim.change_devicerole %}
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}">Edit</a>
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
MANUFACTURER_EDIT_LINK = """
MANUFACTURER_ACTIONS = """
{% if perms.dcim.change_manufacturer %}
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}">Edit</a>
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
PLATFORM_EDIT_LINK = """
PLATFORM_ACTIONS = """
{% if perms.dcim.change_platform %}
<a href="{% url 'dcim:platform_edit' slug=record.slug %}">Edit</a>
<a href="{% url 'dcim:platform_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
DEVICE_ROLE = """
<label class="label {{ record.device_role.color }}">{{ value }}</label>
"""
STATUS_ICON = """
{% if record.status %}
<span class="glyphicon glyphicon-ok-sign text-success" title="Active" aria-hidden="true"></span>
@@ -59,8 +81,10 @@ UTILIZATION_GRAPH = """
#
class SiteTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
facility = tables.Column(verbose_name='Facility')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
asn = tables.Column(verbose_name='ASN')
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
@@ -70,8 +94,8 @@ class SiteTable(BaseTable):
class Meta(BaseTable.Meta):
model = Site
fields = ('name', 'facility', 'asn', 'rack_count', 'device_count', 'prefix_count', 'vlan_count',
'circuit_count')
fields = ('pk', 'name', 'facility', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
'vlan_count', 'circuit_count')
#
@@ -84,11 +108,30 @@ class RackGroupTable(BaseTable):
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
rack_count = tables.Column(verbose_name='Racks')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=RACKGROUP_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=RACKGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = RackGroup
fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'edit')
fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'actions')
#
# Rack roles
#
class RackRoleTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
rack_count = tables.Column(verbose_name='Racks')
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = RackGroup
fields = ('pk', 'name', 'rack_count', 'color', 'slug', 'actions')
#
@@ -101,14 +144,17 @@ class RackTable(BaseTable):
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)')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
role = tables.TemplateColumn(RACK_ROLE, verbose_name='Role')
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
u_consumed = tables.Column(accessor=Accessor('u_consumed'), verbose_name='Used (U)')
u_consumed = tables.TemplateColumn("{{ record.u_consumed|default:'0' }}U", verbose_name='Used')
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')
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'u_consumed',
'utilization')
class RackImportTable(BaseTable):
@@ -116,11 +162,12 @@ class RackImportTable(BaseTable):
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')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
u_height = tables.Column(verbose_name='Height (U)')
class Meta(BaseTable.Meta):
model = Rack
fields = ('site', 'group', 'name', 'facility_id', 'u_height')
fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
#
@@ -132,11 +179,12 @@ class ManufacturerTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name')
devicetype_count = tables.Column(verbose_name='Device Types')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=MANUFACTURER_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = Manufacturer
fields = ('pk', 'name', 'devicetype_count', 'slug', 'edit')
fields = ('pk', 'name', 'devicetype_count', 'slug', 'actions')
#
@@ -226,13 +274,14 @@ class DeviceRoleTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices')
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color')
slug = tables.Column(verbose_name='Slug')
color = tables.Column(verbose_name='Color')
edit = tables.TemplateColumn(template_code=DEVICEROLE_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = DeviceRole
fields = ('pk', 'name', 'device_count', 'slug', 'color')
fields = ('pk', 'name', 'device_count', 'color', 'slug', 'actions')
#
@@ -244,11 +293,11 @@ class PlatformTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=PLATFORM_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
class Meta(BaseTable.Meta):
model = Platform
fields = ('pk', 'name', 'device_count', 'slug', 'edit')
fields = ('pk', 'name', 'device_count', 'slug', 'actions')
#
@@ -259,20 +308,22 @@ class DeviceTable(BaseTable):
pk = ToggleColumn()
status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
device_role = tables.Column(verbose_name='Role')
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
device_type = tables.Column(verbose_name='Type')
primary_ip = tables.TemplateColumn(orderable=False, verbose_name='IP Address',
template_code="{{ record.primary_ip.address.ip }}")
class Meta(BaseTable.Meta):
model = Device
fields = ('pk', 'name', 'status', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
class DeviceImportTable(BaseTable):
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
position = tables.Column(verbose_name='Position')
@@ -281,7 +332,7 @@ class DeviceImportTable(BaseTable):
class Meta(BaseTable.Meta):
model = Device
fields = ('name', 'site', 'rack', 'position', 'device_role', 'device_type')
fields = ('name', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
empty_text = False

View File

@@ -15,6 +15,7 @@ class SiteTest(APITestCase):
'id',
'name',
'slug',
'tenant',
'facility',
'asn',
'physical_address',
@@ -40,6 +41,10 @@ class SiteTest(APITestCase):
'display_name',
'site',
'group',
'tenant',
'role',
'type',
'width',
'u_height',
'comments'
]
@@ -115,6 +120,10 @@ class RackTest(APITestCase):
'display_name',
'site',
'group',
'tenant',
'role',
'type',
'width',
'u_height',
'comments'
]
@@ -126,6 +135,10 @@ class RackTest(APITestCase):
'display_name',
'site',
'group',
'tenant',
'role',
'type',
'width',
'u_height',
'comments',
'front_units',
@@ -311,8 +324,10 @@ class DeviceTest(APITestCase):
'display_name',
'device_type',
'device_role',
'tenant',
'platform',
'serial',
'asset_tag',
'rack',
'position',
'face',
@@ -356,6 +371,7 @@ class DeviceTest(APITestCase):
def test_get_list_flat(self, endpoint='/api/dcim/devices/?format=json_flat'):
flat_fields = [
'asset_tag',
'comments',
'device_role_id',
'device_role_name',
@@ -388,6 +404,7 @@ class DeviceTest(APITestCase):
'rack_name',
'serial',
'status',
'tenant',
]
response = self.client.get(endpoint)

View File

@@ -13,59 +13,67 @@ class DeviceTestCase(TestCase):
def test_racked_device(self):
test = DeviceForm(data={
'device_role': get_id(DeviceRole, 'leaf-switch'),
'name': 'test',
'site': get_id(Site, 'test1'),
'face': RACK_FACE_FRONT,
'platform': get_id(Platform, 'juniper-junos'),
'device_type': get_id(DeviceType, 'qfx5100-48s'),
'position': 41,
'rack': '1',
'device_role': get_id(DeviceRole, 'leaf-switch'),
'tenant': None,
'manufacturer': get_id(Manufacturer, 'juniper'),
'device_type': get_id(DeviceType, 'qfx5100-48s'),
'site': get_id(Site, 'test1'),
'rack': '1',
'face': RACK_FACE_FRONT,
'position': 41,
'platform': get_id(Platform, 'juniper-junos'),
'status': STATUS_ACTIVE,
})
self.assertTrue(test.is_valid(), test.fields['position'].choices)
self.assertTrue(test.save())
def test_racked_device_occupied(self):
test = DeviceForm(data={
'device_role': get_id(DeviceRole, 'leaf-switch'),
'name': 'test',
'site': get_id(Site, 'test1'),
'face': RACK_FACE_FRONT,
'platform': get_id(Platform, 'juniper-junos'),
'device_type': get_id(DeviceType, 'qfx5100-48s'),
'position': 1,
'rack': '1',
'device_role': get_id(DeviceRole, 'leaf-switch'),
'tenant': None,
'manufacturer': get_id(Manufacturer, 'juniper'),
'device_type': get_id(DeviceType, 'qfx5100-48s'),
'site': get_id(Site, 'test1'),
'rack': '1',
'face': RACK_FACE_FRONT,
'position': 1,
'platform': get_id(Platform, 'juniper-junos'),
'status': STATUS_ACTIVE,
})
self.assertFalse(test.is_valid())
def test_non_racked_device(self):
test = DeviceForm(data={
'device_role': get_id(DeviceRole, 'pdu'),
'name': 'test',
'site': get_id(Site, 'test1'),
'face': None,
'platform': None,
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'position': None,
'rack': '1',
'device_role': get_id(DeviceRole, 'pdu'),
'tenant': None,
'manufacturer': get_id(Manufacturer, 'servertech'),
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'site': get_id(Site, 'test1'),
'rack': '1',
'face': None,
'position': None,
'platform': None,
'status': STATUS_ACTIVE,
})
self.assertTrue(test.is_valid())
self.assertTrue(test.save())
def test_non_racked_device_with_face(self):
test = DeviceForm(data={
'device_role': get_id(DeviceRole, 'pdu'),
'name': 'test',
'site': get_id(Site, 'test1'),
'face': RACK_FACE_REAR,
'platform': None,
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'position': None,
'rack': '1',
'device_role': get_id(DeviceRole, 'pdu'),
'tenant': None,
'manufacturer': get_id(Manufacturer, 'servertech'),
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'site': get_id(Site, 'test1'),
'rack': '1',
'face': RACK_FACE_REAR,
'position': None,
'platform': None,
'status': STATUS_ACTIVE,
})
self.assertTrue(test.is_valid())
self.assertTrue(test.save())

View File

@@ -15,6 +15,7 @@ urlpatterns = [
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
url(r'^sites/(?P<slug>[\w-]+)/$', views.site, name='site'),
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
@@ -25,6 +26,12 @@ urlpatterns = [
url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
# Rack roles
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
url(r'^rack-roles/add/$', views.RackRoleEditView.as_view(), name='rackrole_add'),
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
# Racks
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'),

View File

@@ -8,6 +8,7 @@ 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.functions import Coalesce
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.http import urlencode
@@ -15,7 +16,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 extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.forms import ConfirmationForm
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
@@ -26,7 +27,7 @@ from .models import (
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
Site,
RackRole, Site,
)
@@ -61,9 +62,11 @@ def expand_pattern(string):
#
class SiteListView(ObjectListView):
queryset = Site.objects.all()
queryset = Site.objects.select_related('tenant')
filter = filters.SiteFilter
filter_form = forms.SiteFilterForm
table = tables.SiteTable
edit_permissions = ['dcim.change_rack', 'dcim.delete_rack']
template_name = 'dcim/site_list.html'
@@ -79,12 +82,14 @@ def site(request, slug):
}
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
topology_maps = TopologyMap.objects.filter(site=site)
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists()
return render(request, 'dcim/site.html', {
'site': site,
'stats': stats,
'rack_groups': rack_groups,
'topology_maps': topology_maps,
'show_graphs': show_graphs,
})
@@ -110,12 +115,30 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
obj_list_url = 'dcim:site_list'
class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_site'
cls = Site
form = forms.SiteBulkEditForm
template_name = 'dcim/site_bulk_edit.html'
default_redirect_url = 'dcim:site_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['tenant'] == 0:
fields_to_update['tenant'] = None
elif form.cleaned_data['tenant']:
fields_to_update['tenant'] = form.cleaned_data['tenant']
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
#
# Rack groups
#
class RackGroupListView(ObjectListView):
queryset = RackGroup.objects.annotate(rack_count=Count('racks'))
queryset = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
filter = filters.RackGroupFilter
filter_form = forms.RackGroupFilterForm
table = tables.RackGroupTable
@@ -127,6 +150,7 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rackgroup'
model = RackGroup
form_class = forms.RackGroupForm
success_url = 'dcim:rackgroup_list'
cancel_url = 'dcim:rackgroup_list'
@@ -136,12 +160,39 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
default_redirect_url = 'dcim:rackgroup_list'
#
# Rack roles
#
class RackRoleListView(ObjectListView):
queryset = RackRole.objects.annotate(rack_count=Count('racks'))
table = tables.RackRoleTable
edit_permissions = ['dcim.change_rackrole', 'dcim.delete_rackrole']
template_name = 'dcim/rackrole_list.html'
class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rackrole'
model = RackRole
form_class = forms.RackRoleForm
success_url = 'dcim:rackrole_list'
cancel_url = 'dcim:rackrole_list'
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackrole'
cls = RackRole
default_redirect_url = 'dcim:rackrole_list'
#
# Racks
#
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', 'group', 'tenant', 'role').prefetch_related('devices__device_type')\
.annotate(device_count=Count('devices', distinct=True),
u_consumed=Coalesce(Sum('devices__device_type__u_height'), 0))
filter = filters.RackFilter
filter_form = forms.RackFilterForm
table = tables.RackTable
@@ -153,7 +204,7 @@ def rack(request, pk):
rack = get_object_or_404(Rack, pk=pk)
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True)\
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\
.select_related('device_type__manufacturer')
next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
@@ -200,7 +251,12 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['site', 'group', 'u_height', 'comments']:
for field in ['group', 'tenant', 'role']:
if form.cleaned_data[field] == 0:
fields_to_update[field] = None
elif form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
for field in ['site', 'type', 'width', 'u_height', 'comments']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
@@ -506,8 +562,8 @@ class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class DeviceListView(ObjectListView):
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'rack__site', 'primary_ip4',
'primary_ip6')
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'rack__site',
'primary_ip4', 'primary_ip6')
filter = filters.DeviceFilter
filter_form = forms.DeviceFilterForm
table = tables.DeviceTable
@@ -560,6 +616,9 @@ def device(request, pk):
related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\
.select_related('rack', 'device_type__manufacturer')[:10]
# Show graph button on interfaces only if at least one graph has been created.
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists()
return render(request, 'dcim/device.html', {
'device': device,
'console_ports': console_ports,
@@ -572,6 +631,7 @@ def device(request, pk):
'ip_addresses': ip_addresses,
'secrets': secrets,
'related_devices': related_devices,
'show_graphs': show_graphs,
})
@@ -625,14 +685,15 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['platform']:
fields_to_update['platform'] = form.cleaned_data['platform']
elif form.cleaned_data['platform_delete']:
fields_to_update['platform'] = None
for field in ['tenant', 'platform']:
if form.cleaned_data[field] == 0:
fields_to_update[field] = None
elif form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
if form.cleaned_data['status']:
status = form.cleaned_data['status']
fields_to_update['status'] = True if status == 'True' else False
for field in ['device_type', 'device_role', 'serial']:
for field in ['tenant', 'device_type', 'device_role', 'serial']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
@@ -648,7 +709,8 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
def device_inventory(request, pk):
device = get_object_or_404(Device, pk=pk)
modules = Module.objects.filter(device=device, parent=None).prefetch_related('submodules')
modules = Module.objects.filter(device=device, parent=None).select_related('manufacturer')\
.prefetch_related('submodules')
return render(request, 'dcim/device_inventory.html', {
'device': device,

View File

@@ -19,3 +19,9 @@ class TopologyMapAdmin(admin.ModelAdmin):
prepopulated_fields = {
'slug': ['name'],
}
@admin.register(UserAction)
class UserActionAdmin(admin.ModelAdmin):
actions = None
list_display = ['user', 'action', 'content_type', 'object_id', 'message']

View File

@@ -18,9 +18,10 @@ GRAPH_TYPE_CHOICES = (
)
EXPORTTEMPLATE_MODELS = [
'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection',
'aggregate', 'prefix', 'ipaddress', 'vlan',
'provider', 'circuit'
'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
]
ACTION_CREATE = 1

View File

@@ -7,7 +7,12 @@ from .models import (
@admin.register(VRF)
class VRFAdmin(admin.ModelAdmin):
list_display = ['name', 'rd']
list_display = ['name', 'rd', 'tenant', 'enforce_unique']
list_filter = ['tenant']
def get_queryset(self, request):
qs = super(VRFAdmin, self).get_queryset(request)
return qs.select_related('tenant')
@admin.register(Role)
@@ -35,7 +40,7 @@ class AggregateAdmin(admin.ModelAdmin):
@admin.register(Prefix)
class PrefixAdmin(admin.ModelAdmin):
list_display = ['prefix', 'vrf', 'site', 'status', 'role', 'vlan']
list_display = ['prefix', 'vrf', 'tenant', 'site', 'status', 'role', 'vlan']
list_filter = ['family', 'site', 'status', 'role']
search_fields = ['prefix']
@@ -46,7 +51,7 @@ class PrefixAdmin(admin.ModelAdmin):
@admin.register(IPAddress)
class IPAddressAdmin(admin.ModelAdmin):
list_display = ['address', 'vrf', 'nat_inside']
list_display = ['address', 'vrf', 'tenant', 'nat_inside']
list_filter = ['family']
fields = ['address', 'vrf', 'device', 'interface', 'nat_inside']
readonly_fields = ['interface', 'device', 'nat_inside']
@@ -67,10 +72,10 @@ class VLANGroupAdmin(admin.ModelAdmin):
@admin.register(VLAN)
class VLANAdmin(admin.ModelAdmin):
list_display = ['site', 'vid', 'name', 'status', 'role']
list_filter = ['site', 'status', 'role']
list_display = ['site', 'vid', 'name', 'tenant', 'status', 'role']
list_filter = ['site', 'tenant', 'status', 'role']
search_fields = ['vid', 'name']
def get_queryset(self, request):
qs = super(VLANAdmin, self).get_queryset(request)
return qs.select_related('site', 'role')
return qs.select_related('site', 'tenant', 'role')

View File

@@ -2,6 +2,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 tenancy.api.serializers import TenantNestedSerializer
#
@@ -9,10 +10,11 @@ from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLAN
#
class VRFSerializer(serializers.ModelSerializer):
tenant = TenantNestedSerializer()
class Meta:
model = VRF
fields = ['id', 'name', 'rd', 'enforce_unique', 'description']
fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description']
class VRFNestedSerializer(VRFSerializer):
@@ -21,6 +23,15 @@ class VRFNestedSerializer(VRFSerializer):
fields = ['id', 'name', 'rd']
class VRFTenantSerializer(VRFSerializer):
"""
Include tenant serializer. Useful for determining tenant inheritance for Prefixes and IPAddresses.
"""
class Meta(VRFSerializer.Meta):
fields = ['id', 'name', 'rd', 'tenant']
#
# Roles
#
@@ -98,11 +109,12 @@ class VLANGroupNestedSerializer(VLANGroupSerializer):
class VLANSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
group = VLANGroupNestedSerializer()
tenant = TenantNestedSerializer()
role = RoleNestedSerializer()
class Meta:
model = VLAN
fields = ['id', 'site', 'group', 'vid', 'name', 'status', 'role', 'description', 'display_name']
fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name']
class VLANNestedSerializer(VLANSerializer):
@@ -117,13 +129,14 @@ class VLANNestedSerializer(VLANSerializer):
class PrefixSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
vrf = VRFNestedSerializer()
vrf = VRFTenantSerializer()
tenant = TenantNestedSerializer()
vlan = VLANNestedSerializer()
role = RoleNestedSerializer()
class Meta:
model = Prefix
fields = ['id', 'family', 'prefix', 'site', 'vrf', 'vlan', 'status', 'role', 'description']
fields = ['id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description']
class PrefixNestedSerializer(PrefixSerializer):
@@ -137,12 +150,13 @@ class PrefixNestedSerializer(PrefixSerializer):
#
class IPAddressSerializer(serializers.ModelSerializer):
vrf = VRFNestedSerializer()
vrf = VRFTenantSerializer()
tenant = TenantNestedSerializer()
interface = InterfaceNestedSerializer()
class Meta:
model = IPAddress
fields = ['id', 'family', 'address', 'vrf', 'interface', 'description', 'nat_inside', 'nat_outside']
fields = ['id', 'family', 'address', 'vrf', 'tenant', 'interface', 'description', 'nat_inside', 'nat_outside']
class IPAddressNestedSerializer(IPAddressSerializer):

View File

@@ -14,7 +14,7 @@ class VRFListView(generics.ListAPIView):
"""
List all VRFs
"""
queryset = VRF.objects.all()
queryset = VRF.objects.select_related('tenant')
serializer_class = serializers.VRFSerializer
filter_class = filters.VRFFilter
@@ -23,7 +23,7 @@ class VRFDetailView(generics.RetrieveAPIView):
"""
Retrieve a single VRF
"""
queryset = VRF.objects.all()
queryset = VRF.objects.select_related('tenant')
serializer_class = serializers.VRFSerializer
@@ -96,7 +96,7 @@ class PrefixListView(generics.ListAPIView):
"""
List prefixes (filterable)
"""
queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role')
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
serializer_class = serializers.PrefixSerializer
filter_class = filters.PrefixFilter
@@ -105,7 +105,7 @@ class PrefixDetailView(generics.RetrieveAPIView):
"""
Retrieve a single prefix
"""
queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role')
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
serializer_class = serializers.PrefixSerializer
@@ -117,7 +117,7 @@ class IPAddressListView(generics.ListAPIView):
"""
List IP addresses (filterable)
"""
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
.prefetch_related('nat_outside')
serializer_class = serializers.IPAddressSerializer
filter_class = filters.IPAddressFilter
@@ -127,7 +127,7 @@ class IPAddressDetailView(generics.RetrieveAPIView):
"""
Retrieve a single IP address
"""
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
.prefetch_related('nat_outside')
serializer_class = serializers.IPAddressSerializer
@@ -140,7 +140,7 @@ class VLANGroupListView(generics.ListAPIView):
"""
List all VLAN groups
"""
queryset = VLANGroup.objects.all()
queryset = VLANGroup.objects.select_related('site')
serializer_class = serializers.VLANGroupSerializer
filter_class = filters.VLANGroupFilter
@@ -149,7 +149,7 @@ class VLANGroupDetailView(generics.RetrieveAPIView):
"""
Retrieve a single VLAN group
"""
queryset = VLANGroup.objects.all()
queryset = VLANGroup.objects.select_related('site')
serializer_class = serializers.VLANGroupSerializer
@@ -161,7 +161,7 @@ class VLANListView(generics.ListAPIView):
"""
List VLANs (filterable)
"""
queryset = VLAN.objects.select_related('site', 'role')
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
serializer_class = serializers.VLANSerializer
filter_class = filters.VLANFilter
@@ -170,5 +170,5 @@ class VLANDetailView(generics.RetrieveAPIView):
"""
Retrieve a single VLAN
"""
queryset = VLAN.objects.select_related('site', 'role')
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
serializer_class = serializers.VLANSerializer

View File

@@ -2,17 +2,42 @@ import django_filters
from netaddr import IPNetwork
from netaddr.core import AddrFormatError
from django.db.models import Q
from dcim.models import Site, Device, Interface
from tenancy.models import Tenant
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
class VRFFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
)
name = django_filters.CharFilter(
name='name',
lookup_type='icontains',
label='Name',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
def search(self, queryset, value):
return queryset.filter(
Q(name__icontains=value) |
Q(rd__icontains=value) |
Q(description__icontains=value)
)
class Meta:
model = VRF
@@ -20,6 +45,10 @@ class VRFFilter(django_filters.FilterSet):
class AggregateFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
)
rir_id = django_filters.ModelMultipleChoiceFilter(
name='rir',
queryset=RIR.objects.all(),
@@ -36,6 +65,15 @@ class AggregateFilter(django_filters.FilterSet):
model = Aggregate
fields = ['family', 'rir_id', 'rir', 'date_added']
def search(self, queryset, value):
qs_filter = Q(description__icontains=value)
try:
prefix = str(IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
except AddrFormatError:
pass
return queryset.filter(qs_filter)
class PrefixFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
@@ -55,6 +93,14 @@ class PrefixFilter(django_filters.FilterSet):
action='_vrf',
label='VRF',
)
tenant_id = django_filters.MethodFilter(
action='_tenant_id',
label='Tenant (ID)',
)
tenant = django_filters.MethodFilter(
action='_tenant',
label='Tenant',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
@@ -92,12 +138,13 @@ class PrefixFilter(django_filters.FilterSet):
fields = ['family', 'site_id', 'site', 'vrf', 'vrf_id', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role']
def search(self, queryset, value):
value = value.strip()
qs_filter = Q(description__icontains=value)
try:
query = str(IPNetwork(value).cidr)
return queryset.filter(prefix__net_contains_or_equals=query)
prefix = str(IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
except AddrFormatError:
return queryset.none()
pass
return queryset.filter(qs_filter)
def search_by_parent(self, queryset, value):
value = value.strip()
@@ -120,12 +167,34 @@ class PrefixFilter(django_filters.FilterSet):
return queryset.filter(vrf__isnull=True)
return queryset.filter(vrf__pk=value)
def _tenant(self, queryset, value):
if str(value) == '':
return queryset
return queryset.filter(
Q(tenant__slug=value) |
Q(tenant__isnull=True, vrf__tenant__slug=value)
)
def _tenant_id(self, queryset, value):
try:
value = int(value)
except ValueError:
return queryset.none()
return queryset.filter(
Q(tenant__pk=value) |
Q(tenant__isnull=True, vrf__tenant__pk=value)
)
class IPAddressFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
)
parent = django_filters.MethodFilter(
action='search_by_parent',
label='Parent prefix',
)
vrf = django_filters.MethodFilter(
action='_vrf',
label='VRF',
@@ -135,6 +204,14 @@ class IPAddressFilter(django_filters.FilterSet):
action='_vrf',
label='VRF',
)
tenant_id = django_filters.MethodFilter(
action='_tenant_id',
label='Tenant (ID)',
)
tenant = django_filters.MethodFilter(
action='_tenant',
label='Tenant',
)
device_id = django_filters.ModelMultipleChoiceFilter(
name='interface__device',
queryset=Device.objects.all(),
@@ -157,10 +234,21 @@ class IPAddressFilter(django_filters.FilterSet):
fields = ['q', 'family', 'vrf_id', 'vrf', 'device_id', 'device', 'interface_id']
def search(self, queryset, value):
value = value.strip()
qs_filter = Q(description__icontains=value)
try:
query = str(IPNetwork(value))
return queryset.filter(address__net_host=query)
ipaddress = str(IPNetwork(value.strip()))
qs_filter |= Q(address__net_host=ipaddress)
except AddrFormatError:
pass
return queryset.filter(qs_filter)
def search_by_parent(self, queryset, value):
value = value.strip()
if not value:
return queryset
try:
query = str(IPNetwork(value).cidr)
return queryset.filter(address__net_contained_or_equal=query)
except AddrFormatError:
return queryset.none()
@@ -175,6 +263,24 @@ class IPAddressFilter(django_filters.FilterSet):
return queryset.filter(vrf__isnull=True)
return queryset.filter(vrf__pk=value)
def _tenant(self, queryset, value):
if str(value) == '':
return queryset
return queryset.filter(
Q(tenant__slug=value) |
Q(tenant__isnull=True, vrf__tenant__slug=value)
)
def _tenant_id(self, queryset, value):
try:
value = int(value)
except ValueError:
return queryset.none()
return queryset.filter(
Q(tenant__pk=value) |
Q(tenant__isnull=True, vrf__tenant__pk=value)
)
class VLANGroupFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
@@ -195,6 +301,10 @@ class VLANGroupFilter(django_filters.FilterSet):
class VLANFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
@@ -226,6 +336,17 @@ class VLANFilter(django_filters.FilterSet):
name='vid',
label='VLAN number (1-4095)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
name='role',
queryset=Role.objects.all(),
@@ -241,3 +362,11 @@ class VLANFilter(django_filters.FilterSet):
class Meta:
model = VLAN
fields = ['site_id', 'site', 'vid', 'name', 'status', 'role_id', 'role']
def search(self, queryset, value):
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
try:
qs_filter |= Q(vid=int(value))
except ValueError:
pass
return queryset.filter(qs_filter)

View File

@@ -0,0 +1,125 @@
[
{
"model": "ipam.aggregate",
"pk": 1,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:20.938Z",
"family": 4,
"prefix": "10.0.0.0/8",
"rir": 6,
"date_added": null,
"description": "Private IPv4 space"
}
},
{
"model": "ipam.aggregate",
"pk": 2,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:32.679Z",
"family": 4,
"prefix": "172.16.0.0/12",
"rir": 6,
"date_added": null,
"description": "Private IPv4 space"
}
},
{
"model": "ipam.aggregate",
"pk": 3,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z",
"family": 4,
"prefix": "192.168.0.0/16",
"rir": 6,
"date_added": null,
"description": "Private IPv4 space"
}
},
{
"model": "ipam.rir",
"pk": 1,
"fields": {
"name": "ARIN",
"slug": "arin"
}
},
{
"model": "ipam.rir",
"pk": 2,
"fields": {
"name": "RIPE",
"slug": "ripe"
}
},
{
"model": "ipam.rir",
"pk": 3,
"fields": {
"name": "APNIC",
"slug": "apnic"
}
},
{
"model": "ipam.rir",
"pk": 4,
"fields": {
"name": "LACNIC",
"slug": "lacnic"
}
},
{
"model": "ipam.rir",
"pk": 5,
"fields": {
"name": "AFRINIC",
"slug": "afrinic"
}
},
{
"model": "ipam.rir",
"pk": 6,
"fields": {
"name": "RFC 1918",
"slug": "rfc-1918"
}
},
{
"model": "ipam.role",
"pk": 1,
"fields": {
"name": "Production",
"slug": "production",
"weight": 1000
}
},
{
"model": "ipam.role",
"pk": 2,
"fields": {
"name": "Development",
"slug": "development",
"weight": 1000
}
},
{
"model": "ipam.role",
"pk": 3,
"fields": {
"name": "Management",
"slug": "management",
"weight": 1000
}
},
{
"model": "ipam.role",
"pk": 4,
"fields": {
"name": "Backup",
"slug": "backup",
"weight": 1000
}
}
]

View File

@@ -4,6 +4,8 @@ from django import forms
from django.db.models import Count
from dcim.models import Site, Device, Interface
from tenancy.forms import bulkedit_tenant_choices
from tenancy.models import Tenant
from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField
from .models import (
@@ -15,6 +17,18 @@ FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES
FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES
def bulkedit_vrf_choices():
"""
Include an option to assign the object to the global table.
"""
choices = [
(None, '---------'),
(0, 'Global'),
]
choices += [(v.pk, v.name) for v in VRF.objects.all()]
return choices
#
# VRFs
#
@@ -23,7 +37,7 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = VRF
fields = ['name', 'rd', 'enforce_unique', 'description']
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
labels = {
'rd': "RD",
}
@@ -33,10 +47,12 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
class VRFFromCSVForm(forms.ModelForm):
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
class Meta:
model = VRF
fields = ['name', 'rd', 'enforce_unique', 'description']
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
class VRFImportForm(BulkImportForm, BootstrapMixin):
@@ -45,9 +61,20 @@ class VRFImportForm(BulkImportForm, BootstrapMixin):
class VRFBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
description = forms.CharField(max_length=100, required=False)
def vrf_tenant_choices():
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vrfs'))
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
class VRFFilterForm(forms.Form, BootstrapMixin):
tenant = forms.MultipleChoiceField(required=False, choices=vrf_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
#
# RIRs
#
@@ -131,7 +158,7 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Prefix
fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'description']
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'description']
help_texts = {
'prefix': "IPv4 or IPv6 network",
'vrf': "VRF (if applicable)",
@@ -155,23 +182,21 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
self.fields['vlan'].choices = []
def clean_prefix(self):
data = self.cleaned_data['prefix']
try:
prefix = IPNetwork(data)
except:
raise
prefix = self.cleaned_data['prefix']
if prefix.version == 4 and prefix.prefixlen == 32:
raise forms.ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 "
"addresses instead.")
elif prefix.version == 6 and prefix.prefixlen == 128:
raise forms.ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 "
"addresses instead.")
return data
return prefix
class PrefixFromCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
error_messages={'invalid_choice': 'VRF not found.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant 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)
@@ -182,7 +207,8 @@ class PrefixFromCSVForm(forms.ModelForm):
class Meta:
model = Prefix
fields = ['prefix', 'vrf', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', 'description']
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role',
'description']
def clean(self):
@@ -228,18 +254,21 @@ class PrefixImportForm(BulkImportForm, BootstrapMixin):
class PrefixBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
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')
vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
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)
def prefix_vrf_choices():
vrf_choices = [('', 'All'), (0, 'Global')]
vrf_choices += [(v.pk, v.name) for v in VRF.objects.all()]
return vrf_choices
vrf_choices = VRF.objects.annotate(prefix_count=Count('prefixes'))
return [(v.pk, u'{} ({})'.format(v.name, v.prefix_count)) for v in vrf_choices]
def tenant_choices():
tenant_choices = Tenant.objects.all()
return [(t.slug, t.name) for t in tenant_choices]
def prefix_site_choices():
@@ -260,13 +289,19 @@ def prefix_role_choices():
class PrefixFilterForm(forms.Form, BootstrapMixin):
parent = forms.CharField(required=False, label='Search Within')
vrf = forms.ChoiceField(required=False, choices=prefix_vrf_choices, label='VRF')
status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices)
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
'placeholder': 'Network',
}))
vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF',
widget=forms.SelectMultiple(attrs={'size': 6}))
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
widget=forms.SelectMultiple(attrs={'size': 6}))
status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices,
widget=forms.SelectMultiple(attrs={'size': 6}))
site = forms.MultipleChoiceField(required=False, choices=prefix_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
widget=forms.SelectMultiple(attrs={'size': 6}))
role = forms.MultipleChoiceField(required=False, choices=prefix_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
widget=forms.SelectMultiple(attrs={'size': 6}))
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
@@ -289,7 +324,7 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = IPAddress
fields = ['address', 'vrf', 'nat_device', 'nat_inside', 'description']
fields = ['address', 'vrf', 'tenant', 'nat_device', 'nat_inside', 'description']
help_texts = {
'address': "IPv4 or IPv6 address and mask",
'vrf': "VRF (if applicable)",
@@ -338,6 +373,8 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
class IPAddressFromCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
error_messages={'invalid_choice': 'VRF not found.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Device not found.'})
interface_name = forms.CharField(required=False)
@@ -345,7 +382,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
class Meta:
model = IPAddress
fields = ['address', 'vrf', 'device', 'interface_name', 'is_primary', 'description']
fields = ['address', 'vrf', 'tenant', 'device', 'interface_name', 'is_primary', 'description']
def clean(self):
@@ -390,9 +427,8 @@ class IPAddressImportForm(BulkImportForm, BootstrapMixin):
class IPAddressBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
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')
vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
description = forms.CharField(max_length=100, required=False)
@@ -401,14 +437,19 @@ def ipaddress_family_choices():
def ipaddress_vrf_choices():
vrf_choices = [('', 'All'), (0, 'Global')]
vrf_choices += [(v.pk, v.name) for v in VRF.objects.all()]
return vrf_choices
vrf_choices = VRF.objects.annotate(ipaddress_count=Count('ip_addresses'))
return [(v.pk, u'{} ({})'.format(v.name, v.ipaddress_count)) for v in vrf_choices]
class IPAddressFilterForm(forms.Form, BootstrapMixin):
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
'placeholder': 'Prefix',
}))
family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family')
vrf = forms.ChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF')
vrf = forms.MultipleChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF',
widget=forms.SelectMultiple(attrs={'size': 6}))
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
widget=forms.SelectMultiple(attrs={'size': 6}))
#
@@ -444,7 +485,7 @@ class VLANForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = VLAN
fields = ['site', 'group', 'vid', 'name', 'description', 'status', 'role']
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
help_texts = {
'site': "The site at which this VLAN exists",
'group': "VLAN group (optional)",
@@ -475,13 +516,15 @@ class VLANFromCSVForm(forms.ModelForm):
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.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant 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', 'group', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
def save(self, *args, **kwargs):
m = super(VLANFromCSVForm, self).save(commit=False)
@@ -500,6 +543,7 @@ 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)
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
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)
@@ -515,6 +559,11 @@ def vlan_group_choices():
return [(g.pk, u'{} ({})'.format(g, g.vlan_count)) for g in group_choices]
def vlan_tenant_choices():
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vlans'))
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
def vlan_status_choices():
status_counts = {}
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
@@ -532,6 +581,8 @@ class VLANFilterForm(forms.Form, BootstrapMixin):
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}))
tenant = forms.MultipleChoiceField(required=False, choices=vlan_tenant_choices,
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

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-07-27 14:39
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0001_initial'),
('ipam', '0005_auto_20160725_1842'),
]
operations = [
migrations.AddField(
model_name='vlan',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='tenancy.Tenant'),
),
migrations.AddField(
model_name='vrf',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vrfs', to='tenancy.Tenant'),
),
]

View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-07-28 15:32
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0001_initial'),
('ipam', '0006_vrf_vlan_add_tenant'),
]
operations = [
migrations.AddField(
model_name='ipaddress',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='tenancy.Tenant'),
),
migrations.AddField(
model_name='prefix',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='tenancy.Tenant'),
),
]

View File

@@ -5,8 +5,10 @@ from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models.expressions import RawSQL
from dcim.models import Interface
from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel
from .fields import IPNetworkField, IPAddressField
@@ -46,6 +48,7 @@ class VRF(CreatedUpdatedModel):
"""
name = models.CharField(max_length=50)
rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher')
tenant = models.ForeignKey(Tenant, related_name='vrfs', blank=True, null=True, on_delete=models.PROTECT)
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)
@@ -65,6 +68,8 @@ class VRF(CreatedUpdatedModel):
return ','.join([
self.name,
self.rd,
self.tenant.name if self.tenant else '',
'True' if self.enforce_unique else '',
self.description,
])
@@ -229,6 +234,7 @@ class Prefix(CreatedUpdatedModel):
site = models.ForeignKey('dcim.Site', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True)
vrf = models.ForeignKey('VRF', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
verbose_name='VRF')
tenant = models.ForeignKey(Tenant, related_name='prefixes', blank=True, null=True, on_delete=models.PROTECT)
vlan = models.ForeignKey('VLAN', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
verbose_name='VLAN')
status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1)
@@ -249,12 +255,13 @@ class Prefix(CreatedUpdatedModel):
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.")
if self.prefix:
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:
@@ -289,9 +296,23 @@ class Prefix(CreatedUpdatedModel):
return STATUS_CHOICE_CLASSES[self.status]
class IPAddressManager(models.Manager):
def get_queryset(self):
"""
By default, PostgreSQL will order INETs with shorter (larger) prefix lengths ahead of those with longer
(smaller) masks. This makes no sense when ordering IPs, which should be ordered solely by family and host
address. We can use HOST() to extract just the host portion of the address (ignoring its mask), but we must
then re-cast this value to INET() so that records will be ordered properly. We are essentially re-casting each
IP address as a /32 or /128.
"""
qs = super(IPAddressManager, self).get_queryset()
return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host')
class IPAddress(CreatedUpdatedModel):
"""
An IPAddress represents an individual IPV4 or IPv6 address and its mask. The mask length should match what is
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like
Prefixes, IPAddresses can optionally be assigned to a VRF. An IPAddress can optionally be assigned to an Interface.
Interfaces can have zero or more IPAddresses assigned to them.
@@ -304,12 +325,15 @@ class IPAddress(CreatedUpdatedModel):
address = IPAddressField()
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
verbose_name='VRF')
tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
null=True)
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
null=True, verbose_name='NAT IP (inside)')
description = models.CharField(max_length=100, blank=True)
objects = IPAddressManager()
class Meta:
ordering = ['family', 'address']
verbose_name = 'IP address'
@@ -407,9 +431,10 @@ class VLAN(CreatedUpdatedModel):
MaxValueValidator(4094)
])
name = models.CharField(max_length=64)
description = models.CharField(max_length=100, blank=True)
tenant = models.ForeignKey(Tenant, related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
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)
description = models.CharField(max_length=100, blank=True)
class Meta:
ordering = ['site', 'group', 'vid']
@@ -438,6 +463,7 @@ class VLAN(CreatedUpdatedModel):
self.group.name if self.group else '',
str(self.vid),
self.name,
self.tenant.name if self.tenant else '',
self.get_status_display(),
self.role.name if self.role else '',
self.description,

View File

@@ -6,8 +6,10 @@ from utilities.tables import BaseTable, ToggleColumn
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
RIR_EDIT_LINK = """
{% if perms.ipam.change_rir %}<a href="{% url 'ipam:rir_edit' slug=record.slug %}">Edit</a>{% endif %}
RIR_ACTIONS = """
{% if perms.ipam.change_rir %}
<a href="{% url 'ipam:rir_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
UTILIZATION_GRAPH = """
@@ -15,8 +17,10 @@ UTILIZATION_GRAPH = """
{% utilization_graph record.get_utilization %}
"""
ROLE_EDIT_LINK = """
{% if perms.ipam.change_role %}<a href="{% url 'ipam:role_edit' slug=record.slug %}">Edit</a>{% endif %}
ROLE_ACTIONS = """
{% if perms.ipam.change_role %}
<a href="{% url 'ipam:role_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
PREFIX_LINK = """
@@ -35,6 +39,26 @@ PREFIX_LINK_BRIEF = """
</span>
"""
IPADDRESS_LINK = """
{% if record.pk %}
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
{% elif perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Lots of{% endif %} free IP{{ record.0|pluralize }}</a>
{% else %}
{{ record.0 }}
{% endif %}
"""
VRF_LINK = """
{% if record.vrf %}
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
{% elif prefix.vrf %}
{{ prefix.vrf }}
{% else %}
Global
{% endif %}
"""
STATUS_LABEL = """
{% if record.pk %}
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
@@ -43,9 +67,19 @@ STATUS_LABEL = """
{% endif %}
"""
VLANGROUP_EDIT_LINK = """
VLANGROUP_ACTIONS = """
{% if perms.ipam.change_vlangroup %}
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}">Edit</a>
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
TENANT_LINK = """
{% if record.tenant %}
<a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}">{{ record.tenant }}</a>
{% elif record.vrf.tenant %}
<a href="{% url 'tenancy:tenant' slug=record.vrf.tenant.slug %}">{{ record.vrf.tenant }}</a>*
{% else %}
&mdash;
{% endif %}
"""
@@ -58,11 +92,12 @@ class VRFTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name')
rd = tables.Column(verbose_name='RD')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
description = tables.Column(orderable=False, verbose_name='Description')
class Meta(BaseTable.Meta):
model = VRF
fields = ('pk', 'name', 'rd', 'description')
fields = ('pk', 'name', 'rd', 'tenant', 'description')
#
@@ -74,11 +109,11 @@ class RIRTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name')
aggregate_count = tables.Column(verbose_name='Aggregates')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=RIR_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
class Meta(BaseTable.Meta):
model = RIR
fields = ('pk', 'name', 'aggregate_count', 'slug', 'edit')
fields = ('pk', 'name', 'aggregate_count', 'slug', 'actions')
#
@@ -109,11 +144,11 @@ class RoleTable(BaseTable):
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=ROLE_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
class Meta(BaseTable.Meta):
model = Role
fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'edit')
fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'actions')
#
@@ -124,25 +159,31 @@ class PrefixTable(BaseTable):
pk = ToggleColumn()
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix')
vrf = tables.Column(orderable=False, default='Global', verbose_name='VRF')
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
role = tables.Column(verbose_name='Role')
description = tables.Column(orderable=False, verbose_name='Description')
class Meta(BaseTable.Meta):
model = Prefix
fields = ('pk', 'prefix', 'status', 'vrf', 'site', 'role', 'description')
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description')
row_attrs = {
'class': lambda record: 'success' if not record.pk else '',
}
class PrefixBriefTable(BaseTable):
prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF, verbose_name='Prefix')
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
role = tables.Column(verbose_name='Role')
class Meta(BaseTable.Meta):
model = Prefix
fields = ('prefix', 'status', 'site', 'role')
fields = ('prefix', 'vrf', 'status', 'site', 'role')
orderable = False
#
@@ -151,8 +192,9 @@ class PrefixBriefTable(BaseTable):
class IPAddressTable(BaseTable):
pk = ToggleColumn()
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
vrf = tables.Column(orderable=False, default='Global', verbose_name='VRF')
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
verbose_name='Device')
interface = tables.Column(orderable=False, verbose_name='Interface')
@@ -160,7 +202,10 @@ class IPAddressTable(BaseTable):
class Meta(BaseTable.Meta):
model = IPAddress
fields = ('pk', 'address', 'vrf', 'device', 'interface', 'description')
fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description')
row_attrs = {
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
}
class IPAddressBriefTable(BaseTable):
@@ -186,11 +231,12 @@ class VLANGroupTable(BaseTable):
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='')
actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = VLANGroup
fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'edit')
fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'actions')
#
@@ -203,9 +249,10 @@ class VLANTable(BaseTable):
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')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
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', 'group', 'name', 'tenant', 'status', 'role')

View File

@@ -1,8 +1,8 @@
from netaddr import IPSet
import netaddr
from django_tables2 import RequestConfig
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Count
from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, render
from dcim.models import Device
@@ -21,7 +21,7 @@ def add_available_prefixes(parent, prefix_list):
"""
# Find all unallocated space
available_prefixes = IPSet(parent) ^ IPSet([p.prefix for p in prefix_list])
available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
# Concatenate and sort complete list of children
@@ -31,13 +31,65 @@ def add_available_prefixes(parent, prefix_list):
return prefix_list
def add_available_ipaddresses(prefix, ipaddress_list):
"""
Annotate ranges of available IP addresses within a given prefix.
"""
output = []
prev_ip = None
# Ignore the "network address" for IPv4 prefixes larger than /31
if prefix.version == 4 and prefix.prefixlen < 31:
first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1)
else:
first_ip_in_prefix = netaddr.IPAddress(prefix.first)
# Ignore the broadcast address for IPv4 prefixes larger than /31
if prefix.version == 4 and prefix.prefixlen < 31:
last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1)
else:
last_ip_in_prefix = netaddr.IPAddress(prefix.last)
if not ipaddress_list:
return [(
int(last_ip_in_prefix - first_ip_in_prefix + 1),
'{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
)]
# Account for any available IPs before the first real IP
if ipaddress_list[0].address.ip > first_ip_in_prefix:
skipped_count = int(ipaddress_list[0].address.ip - first_ip_in_prefix)
first_skipped = '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
output.append((skipped_count, first_skipped))
# Iterate through existing IPs and annotate free ranges
for ip in ipaddress_list:
if prev_ip:
diff = int(ip.address.ip - prev_ip.address.ip)
if diff > 1:
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
output.append((diff - 1, first_skipped))
output.append(ip)
prev_ip = ip
# Include any remaining available IPs
if prev_ip.address.ip < last_ip_in_prefix:
skipped_count = int(last_ip_in_prefix - prev_ip.address.ip)
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
output.append((skipped_count, first_skipped))
return output
#
# VRFs
#
class VRFListView(ObjectListView):
queryset = VRF.objects.all()
queryset = VRF.objects.select_related('tenant')
filter = filters.VRFFilter
filter_form = forms.VRFFilterForm
table = tables.VRFTable
edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf']
template_name = 'ipam/vrf_list.html'
@@ -47,10 +99,11 @@ def vrf(request, pk):
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
prefixes = Prefix.objects.filter(vrf=vrf)
prefix_table = tables.PrefixBriefTable(prefixes)
return render(request, 'ipam/vrf.html', {
'vrf': vrf,
'prefixes': prefixes,
'prefix_table': prefix_table,
})
@@ -85,6 +138,10 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['tenant'] == 0:
fields_to_update['tenant'] = None
elif form.cleaned_data['tenant']:
fields_to_update['tenant'] = form.cleaned_data['tenant']
for field in ['description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
@@ -145,7 +202,7 @@ class AggregateListView(ObjectListView):
if a.prefix.version == 4:
ipv4_total += a.prefix.size
elif a.prefix.version == 6:
ipv6_total += a.prefix.size / 2**64
ipv6_total += a.prefix.size / 2 ** 64
return {
'ipv4_total': ipv4_total,
@@ -248,7 +305,7 @@ class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class PrefixListView(ObjectListView):
queryset = Prefix.objects.select_related('site', 'role')
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'role')
filter = filters.PrefixFilter
filter_form = forms.PrefixFilterForm
table = tables.PrefixTable
@@ -271,10 +328,12 @@ def prefix(request, pk):
aggregate = None
# Count child IP addresses
ipaddress_count = IPAddress.objects.filter(address__net_contained_or_equal=str(prefix.prefix)).count()
ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
.count()
# Parent prefixes table
parent_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contains=str(prefix.prefix))\
parent_prefixes = Prefix.objects.filter(Q(vrf=prefix.vrf) | Q(vrf__isnull=True))\
.filter(prefix__net_contains=str(prefix.prefix))\
.select_related('site', 'role').annotate_depth()
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
@@ -284,7 +343,13 @@ def prefix(request, pk):
duplicate_prefix_table = tables.PrefixBriefTable(duplicate_prefixes)
# Child prefixes table
child_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix))\
if prefix.vrf:
# If the prefix is in a VRF, show child prefixes only within that VRF.
child_prefixes = Prefix.objects.filter(vrf=prefix.vrf)
else:
# If the prefix is in the global table, show child prefixes from all VRFs.
child_prefixes = Prefix.objects.all()
child_prefixes = child_prefixes.filter(prefix__net_contained=str(prefix.prefix))\
.select_related('site', 'role').annotate_depth(limit=0)
if child_prefixes:
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
@@ -308,7 +373,7 @@ class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_prefix'
model = Prefix
form_class = forms.PrefixForm
fields_initial = ['site', 'vrf', 'prefix']
fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan']
cancel_url = 'ipam:prefix_list'
@@ -336,10 +401,11 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['vrf']:
fields_to_update['vrf'] = form.cleaned_data['vrf']
elif form.cleaned_data['vrf_global']:
fields_to_update['vrf'] = None
for field in ['vrf', 'tenant']:
if form.cleaned_data[field] == 0:
fields_to_update[field] = None
elif form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
for field in ['site', 'status', 'role', 'description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
@@ -358,8 +424,9 @@ def prefix_ipaddresses(request, pk):
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
# Find all IPAddresses belonging to this Prefix
ipaddresses = IPAddress.objects.filter(address__net_contained_or_equal=str(prefix.prefix))\
ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses)
ip_table = tables.IPAddressTable(ipaddresses)
ip_table.model = IPAddress
@@ -378,7 +445,7 @@ def prefix_ipaddresses(request, pk):
#
class IPAddressListView(ObjectListView):
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
filter = filters.IPAddressFilter
filter_form = forms.IPAddressFilterForm
table = tables.IPAddressTable
@@ -460,10 +527,11 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['vrf']:
fields_to_update['vrf'] = form.cleaned_data['vrf']
elif form.cleaned_data['vrf_global']:
fields_to_update['vrf'] = None
for field in ['vrf', 'tenant']:
if form.cleaned_data[field] == 0:
fields_to_update[field] = None
elif form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
for field in ['description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
@@ -482,7 +550,7 @@ class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class VLANGroupListView(ObjectListView):
queryset = VLANGroup.objects.annotate(vlan_count=Count('vlans'))
queryset = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
filter = filters.VLANGroupFilter
filter_form = forms.VLANGroupFilterForm
table = tables.VLANGroupTable
@@ -494,6 +562,7 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vlangroup'
model = VLANGroup
form_class = forms.VLANGroupForm
success_url = 'ipam:vlangroup_list'
cancel_url = 'ipam:vlangroup_list'
@@ -508,7 +577,7 @@ class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class VLANListView(ObjectListView):
queryset = VLAN.objects.select_related('site', 'role')
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
filter = filters.VLANFilter
filter_form = forms.VLANFilterForm
table = tables.VLANTable
@@ -520,10 +589,11 @@ def vlan(request, pk):
vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk)
prefixes = Prefix.objects.filter(vlan=vlan)
prefix_table = tables.PrefixBriefTable(prefixes)
return render(request, 'ipam/vlan.html', {
'vlan': vlan,
'prefixes': prefixes,
'prefix_table': prefix_table,
})
@@ -558,6 +628,10 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['tenant'] == 0:
fields_to_update['tenant'] = None
elif form.cleaned_data['tenant']:
fields_to_update['tenant'] = form.cleaned_data['tenant']
for field in ['site', 'group', 'status', 'role', 'description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]

View File

@@ -12,7 +12,7 @@ except ImportError:
"the documentation.")
VERSION = '1.3.2'
VERSION = '1.5.2'
# Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
@@ -108,6 +108,7 @@ INSTALLED_APPS = (
'ipam',
'extras',
'secrets',
'tenancy',
'users',
'utilities',
)
@@ -171,7 +172,6 @@ MESSAGE_TAGS = {
# Authentication URLs
LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_URL = '/logout/'
# Secrets
SECRETS_MIN_PUBKEY_SIZE = 2048

View File

@@ -22,6 +22,7 @@ urlpatterns = [
url(r'^dcim/', include('dcim.urls', namespace='dcim')),
url(r'^ipam/', include('ipam.urls', namespace='ipam')),
url(r'^secrets/', include('secrets.urls', namespace='secrets')),
url(r'^tenancy/', include('tenancy.urls', namespace='tenancy')),
url(r'^profile/', include('users.urls', namespace='users')),
# API
@@ -29,6 +30,7 @@ urlpatterns = [
url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')),
url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')),
url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')),
url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),
url(r'^api/docs/', include('rest_framework_swagger.urls')),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),

View File

@@ -5,16 +5,20 @@ from django.shortcuts import render
from circuits.models import Provider, Circuit
from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
from extras.models import UserAction
from ipam.models import Aggregate, Prefix, IPAddress, VLAN
from ipam.models import Aggregate, Prefix, IPAddress, VLAN, VRF
from secrets.models import Secret
from tenancy.models import Tenant
def home(request):
stats = {
# DCIM
# Organization
'site_count': Site.objects.count(),
'tenant_count': Tenant.objects.count(),
# DCIM
'rack_count': Rack.objects.count(),
'device_count': Device.objects.count(),
'interface_connections_count': InterfaceConnection.objects.count(),
@@ -22,6 +26,7 @@ def home(request):
'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(),
# IPAM
'vrf_count': VRF.objects.count(),
'aggregate_count': Aggregate.objects.count(),
'prefix_count': Prefix.objects.count(),
'ipaddress_count': IPAddress.objects.count(),
@@ -38,7 +43,7 @@ def home(request):
return render(request, 'home.html', {
'stats': stats,
'recent_activity': UserAction.objects.select_related('user')[:15]
'recent_activity': UserAction.objects.select_related('user')[:50]
})

View File

@@ -21,6 +21,9 @@ body {
margin: 0 auto -61px; /* the bottom margin is the negative value of the footer's height */
padding-bottom: 30px;
}
.navbar-brand {
padding: 12px 15px 8px;
}
.footer, .push {
height: 60px; /* .push must be the same height as .footer */
}
@@ -291,27 +294,39 @@ ul.rack_near_face li.empty:hover a {
display: block;
}
/* Rack elevation colors (from http://flatuicolors.com) */
.teal { background-color: #1abc9c; border-bottom: 1px solid #16a085; }
.teal:hover { background-color: #16a085; }
.green { background-color: #2ecc71; border-bottom: 1px solid #27ae60; }
.green:hover { background-color: #27ae60; }
.blue { background-color: #3498db; border-bottom: 1px solid #2980b9; }
.blue:hover { background-color: #2980b9; }
.purple { background-color: #9b59b6; border-bottom: 1px solid #8e44ad; }
.purple:hover { background-color: #8e44ad; }
.yellow { background-color: #f1c40f; border-bottom: 1px solid #f39c12; }
.yellow:hover { background-color: #f39c12; }
.orange { background-color: #e67e22; border-bottom: 1px solid #d35400; }
.orange:hover { background-color: #d35400; }
.red { background-color: #e74c3c; border-bottom: 1px solid #c0392b; }
.red:hover { background-color: #c0392b; }
.light_gray { background-color: #dce2e3; border-bottom: 1px solid #bdc3c7; }
.light_gray:hover { background-color: #bdc3c7; }
.medium_gray { background-color: #95a5a6; border-bottom: 1px solid #7f8c8d; }
.medium_gray:hover { background-color: #7f8c8d; }
.dark_gray { background-color: #34495e; border-bottom: 1px solid #2c3e50; }
.dark_gray:hover { background-color: #2c3e50; }
/* Colors (from http://flatuicolors.com) */
.teal { background-color: #1abc9c; }
.green { background-color: #2ecc71; }
.blue { background-color: #3498db; }
.purple { background-color: #9b59b6; }
.yellow { background-color: #f1c40f; }
.orange { background-color: #e67e22; }
.red { background-color: #e74c3c; }
.light_gray { background-color: #dce2e3; }
.medium_gray { background-color: #95a5a6; }
.dark_gray { background-color: #34495e; }
/* Rack elevation coloring */
ul.rack .teal { border-bottom: 1px solid #16a085; }
ul.rack .teal:hover { background-color: #16a085; }
ul.rack .green { border-bottom: 1px solid #27ae60; }
ul.rack .green:hover { background-color: #27ae60; }
ul.rack .blue { border-bottom: 1px solid #2980b9; }
ul.rack .blue:hover { background-color: #2980b9; }
ul.rack .purple { border-bottom: 1px solid #8e44ad; }
ul.rack .purple:hover { background-color: #8e44ad; }
ul.rack .yellow { border-bottom: 1px solid #f39c12; }
ul.rack .yellow:hover { background-color: #f39c12; }
ul.rack .orange { border-bottom: 1px solid #d35400; }
ul.rack .orange:hover { background-color: #d35400; }
ul.rack .red { border-bottom: 1px solid #c0392b; }
ul.rack .red:hover { background-color: #c0392b; }
ul.rack .light_gray { border-bottom: 1px solid #bdc3c7; }
ul.rack .light_gray:hover { background-color: #bdc3c7; }
ul.rack .medium_gray { border-bottom: 1px solid #7f8c8d; }
ul.rack .medium_gray:hover { background-color: #7f8c8d; }
ul.rack .dark_gray { border-bottom: 1px solid #2c3e50; }
ul.rack .dark_gray:hover { background-color: #2c3e50; }
/* Misc */
.banner-bottom {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -13,11 +13,11 @@ $(document).ready(function() {
// Slugify
function slugify(s, num_chars) {
s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars
s = s.replace(/^\s+|\s+$/g, ''); // Trim leading/trailing spaces
s = s.replace(/[\-\.\s]+/g, '-'); // Convert spaces and decimals to hyphens
s = s.toLowerCase(); // Convert to lowercase
return s.substring(0, num_chars); // Trim to first num_chars chars
s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars
s = s.replace(/^[\s\.]+|[\s\.]+$/g, ''); // Trim leading/trailing spaces
s = s.replace(/[\-\.\s]+/g, '-'); // Convert spaces and decimals to hyphens
s = s.toLowerCase(); // Convert to lowercase
return s.substring(0, num_chars); // Trim to first num_chars chars
}
var slug_field = $('#id_slug');
slug_field.change(function() {

View File

@@ -8,9 +8,15 @@ $(document).ready(function() {
}
// Update livesearch text when real field changes
search_field.val(real_field.children('option:selected').text());
real_field.change(function() {
if (real_field.val()) {
search_field.val(real_field.children('option:selected').text());
}
real_field.change(function() {
if (real_field.val()) {
search_field.val(real_field.children('option:selected').text());
} else {
search_field.val('');
}
});
search_field.autocomplete({

View File

@@ -83,8 +83,8 @@ $(document).ready(function() {
},
success: function (response, status) {
$('#secret_' + secret_id).html(response.plaintext);
$('button.unlock-secret').hide();
$('button.lock-secret').show();
$('button.unlock-secret[secret-id=' + secret_id + ']').hide();
$('button.lock-secret[secret-id=' + secret_id + ']').show();
},
error: function (xhr, ajaxOptions, thrownError) {
if (xhr.status == 403) {

View File

@@ -1,10 +1,16 @@
import django_filters
from django.db.models import Q
from .models import Secret, SecretRole
from dcim.models import Device
class SecretFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
)
role_id = django_filters.ModelMultipleChoiceFilter(
name='role',
queryset=SecretRole.objects.all(),
@@ -26,3 +32,9 @@ class SecretFilter(django_filters.FilterSet):
class Meta:
model = Secret
fields = ['name', 'role_id', 'role', 'device']
def search(self, queryset, value):
return queryset.filter(
Q(name__icontains=value) |
Q(device__name__icontains=value)
)

View File

@@ -0,0 +1,42 @@
[
{
"model": "secrets.secretrole",
"pk": 1,
"fields": {
"name": "Login Credentials",
"slug": "login-credentials",
"users": [],
"groups": []
}
},
{
"model": "secrets.secretrole",
"pk": 2,
"fields": {
"name": "RADIUS Key",
"slug": "radius-key",
"users": [],
"groups": []
}
},
{
"model": "secrets.secretrole",
"pk": 3,
"fields": {
"name": "SNMPv2 Community",
"slug": "snmpv2-community",
"users": [],
"groups": []
}
},
{
"model": "secrets.secretrole",
"pk": 4,
"fields": {
"name": "SNMPv3 Credentials",
"slug": "snmpv3-credentials",
"users": [],
"groups": []
}
}
]

View File

@@ -6,9 +6,9 @@ from utilities.tables import BaseTable, ToggleColumn
from .models import SecretRole, Secret
SECRETROLE_EDIT_LINK = """
SECRETROLE_ACTIONS = """
{% if perms.secrets.change_secretrole %}
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}">Edit</a>
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@@ -22,11 +22,12 @@ class SecretRoleTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name')
secret_count = tables.Column(verbose_name='Secrets')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=SECRETROLE_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = SecretRole
fields = ('pk', 'name', 'secret_count', 'slug', 'edit')
fields = ('pk', 'name', 'secret_count', 'slug', 'actions')
#

View File

@@ -92,7 +92,7 @@ def secret_add(request, pk):
messages.success(request, "Added new secret: {0}".format(secret))
if '_addanother' in request.POST:
return redirect('secrets:secret_add')
return redirect('dcim:device_addsecret', pk=device.pk)
else:
return redirect('secrets:secret', pk=secret.pk)

View File

@@ -13,7 +13,7 @@
<div class="panel panel-danger" style="margin-top: 200px">
<div class="panel-heading">
<strong>
<i class="glyphicon glyphicon-warning-sign"></i>
<i class="fa fa-warning"></i>
Server Error
</strong>
</div>

View File

@@ -8,6 +8,7 @@
<link rel="stylesheet" href="{% static 'font-awesome-4.6.3/css/font-awesome.min.css' %}">
<link rel="stylesheet" href="{% static 'jquery-ui-1.11.4/jquery-ui.css' %}">
<link rel="stylesheet" href="{% static 'css/base.css' %}">
<link rel="icon" type="image/png" href="{% static 'img/netbox.ico' %}" />
</head>
<body>
<nav class="navbar navbar-default navbar-fixed-top">
@@ -19,178 +20,194 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">NetBox</a>
<a class="navbar-brand" href="/">
<img src="{% static 'img/netbox_logo.png' %}" />
</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
<ul class="nav navbar-nav">
<li class="dropdown{% if request.path|startswith:'/dcim/sites/' %} active{% endif %}">
{% if perms.dcim.add_site %}
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Sites <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:site_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Sites</a></li>
<li><a href="{% url 'dcim:site_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Site</a></li>
<li><a href="{% url 'dcim:site_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Sites</a></li>
</ul>
{% else %}
<a href="{% url 'dcim:site_list' %}">Sites</a>
{% endif %}
<li class="dropdown{% if request.path|startswith:'/dcim/sites/' or 'tenancy' in request.path %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:site_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Sites</a></li>
{% if perms.dcim.add_site %}
<li><a href="{% url 'dcim:site_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Site</a></li>
<li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</a></li>
{% if perms.tenancy.add_tenant %}
<li><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant</a></li>
<li><a href="{% url 'tenancy:tenant_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Tenants</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'tenancy:tenantgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenant Groups</a></li>
{% if perms.tenancy.add_tenantgroup %}
<li><a href="{% url 'tenancy:tenantgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant Group</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|startswith:'/dcim/rack' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:rack_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Racks</a></li>
<li><a href="{% url 'dcim:rack_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Racks</a></li>
{% if perms.dcim.add_rack %}
<li><a href="{% url 'dcim:rack_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Rack</a></li>
<li><a href="{% url 'dcim:rack_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Racks</a></li>
<li><a href="{% url 'dcim:rack_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack</a></li>
<li><a href="{% url 'dcim:rack_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Racks</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dcim:rackgroup_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Rack Groups</a></li>
<li><a href="{% url 'dcim:rackgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Groups</a></li>
{% if perms.dcim.add_rackgroup %}
<li><a href="{% url 'dcim:rackgroup_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Rack Group</a></li>
<li><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Group</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dcim:rackrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Roles</a></li>
{% if perms.dcim.add_rackrole %}
<li><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Role</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|startswith:'/dcim/device' or request.path|startswith:'/dcim/manufacturers/' or request.path|startswith:'/dcim/platforms/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:device_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Devices</a></li>
<li><a href="{% url 'dcim:device_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Devices</a></li>
{% if perms.dcim.add_device %}
<li><a href="{% url 'dcim:device_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Device</a></li>
<li><a href="{% url 'dcim:device_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Devices</a></li>
<li><a href="{% url 'dcim:device_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device</a></li>
<li><a href="{% url 'dcim:device_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Devices</a></li>
{% endif %}
{% if perms.ipam.add_device or perms.ipam.add_devicetype %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'dcim:devicetype_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Device Types</a></li>
<li><a href="{% url 'dcim:devicetype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Types</a></li>
{% if perms.dcim.add_devicetype %}
<li><a href="{% url 'dcim:devicetype_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Device Type</a></li>
<li><a href="{% url 'dcim:devicetype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Type</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dcim:devicerole_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Device Roles</a></li>
<li><a href="{% url 'dcim:devicerole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Roles</a></li>
{% if perms.dcim.add_devicerole %}
<li><a href="{% url 'dcim:devicerole_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Device Role</a></li>
<li><a href="{% url 'dcim:devicerole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Role</a></li>
{% endif %}
{% if perms.dcim.add_devicerole or perms.dcim.add_manufacturer %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'dcim:manufacturer_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Manufacturers</a></li>
<li><a href="{% url 'dcim:manufacturer_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Manufacturers</a></li>
{% if perms.dcim.add_manufacturer %}
<li><a href="{% url 'dcim:manufacturer_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Manufacturer</a></li>
<li><a href="{% url 'dcim:manufacturer_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Manufacturer</a></li>
{% endif %}
{% if perms.dcim.add_manufacturer or perms.dcim.add_platform %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'dcim:platform_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Platforms</a></li>
<li><a href="{% url 'dcim:platform_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Platforms</a></li>
{% if perms.dcim.add_platform %}
<li><a href="{% url 'dcim:platform_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Platform</a></li>
<li><a href="{% url 'dcim:platform_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Platform</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|startswith:'/dcim/console-connections/' or request.path|startswith:'/dcim/power-connections/' or request.path|startswith:'/dcim/interface-connections/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Connections <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:console_connections_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Console Connections</a></li>
<li><a href="{% url 'dcim:console_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Console Connections</a></li>
{% if perms.dcim.change_consoleport %}
<li><a href="{% url 'dcim:console_connections_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Console Connections</a></li>
<li><a href="{% url 'dcim:console_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Console Connections</a></li>
{% endif %}
{% if perms.ipam.change_consoleport or perms.ipam.change_powerport %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'dcim:power_connections_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Power Connections</a></li>
<li><a href="{% url 'dcim:power_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Power Connections</a></li>
{% if perms.dcim.change_powerport %}
<li><a href="{% url 'dcim:power_connections_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Power Connections</a></li>
<li><a href="{% url 'dcim:power_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Power Connections</a></li>
{% endif %}
{% if perms.ipam.change_powerport or perms.ipam.add_interfaceconnection %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'dcim:interface_connections_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Interface Connections</a></li>
<li><a href="{% url 'dcim:interface_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Interface Connections</a></li>
{% if perms.dcim.add_interfaceconnection %}
<li><a href="{% url 'dcim:interface_connections_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Interface Connections</a></li>
<li><a href="{% url 'dcim:interface_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Interface Connections</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|startswith:'/ipam/' and not request.path|startswith:'/ipam/vlan' %} 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>
<li><a href="{% url 'ipam:ipaddress_list' %}"><i class="fa fa-search" aria-hidden="true"></i> IP Addresses</a></li>
{% if perms.ipam.add_ipaddress %}
<li><a href="{% url 'ipam:ipaddress_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add an IP</a></li>
<li><a href="{% url 'ipam:ipaddress_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import IPs</a></li>
<li><a href="{% url 'ipam:ipaddress_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an IP</a></li>
<li><a href="{% url 'ipam:ipaddress_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import IPs</a></li>
{% endif %}
{% if perms.ipam.add_ipaddress or perms.ipam.add_prefix %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'ipam:prefix_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Prefixes</a></li>
<li><a href="{% url 'ipam:prefix_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefixes</a></li>
{% if perms.ipam.add_prefix %}
<li><a href="{% url 'ipam:prefix_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Prefix</a></li>
<li><a href="{% url 'ipam:prefix_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Prefixes</a></li>
<li><a href="{% url 'ipam:prefix_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Prefix</a></li>
<li><a href="{% url 'ipam:prefix_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Prefixes</a></li>
{% endif %}
{% if perms.ipam.add_prefix or perms.ipam.add_aggregate %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'ipam:aggregate_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Aggregates</a></li>
<li><a href="{% url 'ipam:aggregate_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Aggregates</a></li>
{% if perms.ipam.add_aggregate %}
<li><a href="{% url 'ipam:aggregate_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add an Aggregate</a></li>
<li><a href="{% url 'ipam:aggregate_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Aggregates</a></li>
<li><a href="{% url 'ipam:aggregate_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an Aggregate</a></li>
<li><a href="{% url 'ipam:aggregate_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Aggregates</a></li>
{% endif %}
{% if perms.ipam.add_aggregate or perms.ipam.add_vrf %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'ipam:vrf_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VRFs</a></li>
<li><a href="{% url 'ipam:vrf_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VRFs</a></li>
{% if perms.ipam.add_vrf %}
<li><a href="{% url 'ipam:vrf_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VRF</a></li>
<li><a href="{% url 'ipam:vrf_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import VRFs</a></li>
<li><a href="{% url 'ipam:vrf_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VRF</a></li>
<li><a href="{% url 'ipam:vrf_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import VRFs</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'ipam:rir_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> RIRs</a></li>
<li><a href="{% url 'ipam:rir_list' %}"><i class="fa fa-search" aria-hidden="true"></i> RIRs</a></li>
{% if perms.ipam.add_rir %}
<li><a href="{% url 'ipam:rir_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a RIR</a></li>
<li><a href="{% url 'ipam:rir_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a RIR</a></li>
{% endif %}
{% if perms.ipam.add_rir or perms.ipam.add_role %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'ipam:role_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Prefix/VLAN Roles</a></li>
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefix/VLAN Roles</a></li>
{% if perms.ipam.add_role %}
<li><a href="{% url 'ipam:role_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Role</a></li>
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
{% 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>
<li><a href="{% url 'ipam:vlan_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLANs</a></li>
{% if perms.ipam.add_vlan %}
<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>
<li><a href="{% url 'ipam:vlan_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN</a></li>
<li><a href="{% url 'ipam:vlan_import' %}"><i class="fa fa-download" 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>
<li><a href="{% url 'ipam:vlangroup_list' %}"><i class="fa fa-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>
<li><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN Group</a></li>
{% endif %}
</ul>
</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>
<ul class="dropdown-menu">
<li><a href="{% url 'circuits:provider_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Providers</a></li>
<li><a href="{% url 'circuits:provider_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Providers</a></li>
{% if perms.circuits.add_provider %}
<li><a href="{% url 'circuits:provider_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Provider</a></li>
<li><a href="{% url 'circuits:provider_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Providers</a></li>
<li><a href="{% url 'circuits:provider_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Provider</a></li>
<li><a href="{% url 'circuits:provider_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Providers</a></li>
{% endif %}
{% if perms.circuits.add_circuit or perms.circuits.add_provider %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'circuits:circuit_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Circuits</a></li>
<li><a href="{% url 'circuits:circuit_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuits</a></li>
{% if perms.circuits.add_circuit %}
<li><a href="{% url 'circuits:circuit_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Circuit</a></li>
<li><a href="{% url 'circuits:circuit_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Circuits</a></li>
<li><a href="{% url 'circuits:circuit_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit</a></li>
<li><a href="{% url 'circuits:circuit_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Circuits</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'circuits:circuittype_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Circuit Types</a></li>
<li><a href="{% url 'circuits:circuittype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuit Types</a></li>
{% if perms.circuits.add_circuittype %}
<li><a href="{% url 'circuits:circuittype_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Circuit Type</a></li>
<li><a href="{% url 'circuits:circuittype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit Type</a></li>
{% endif %}
</ul>
</li>
@@ -198,14 +215,14 @@
<li class="dropdown{% if request.path|startswith:'/secrets/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'secrets:secret_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Secrets</a></li>
<li><a href="{% url 'secrets:secret_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secrets</a></li>
{% if perms.secrets.add_secret %}
<li><a href="{% url 'secrets:secret_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Secrets</a></li>
<li><a href="{% url 'secrets:secret_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Secrets</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'secrets:secretrole_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Secret Roles</a></li>
<li><a href="{% url 'secrets:secretrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secret Roles</a></li>
{% if perms.secrets.add_secretrole %}
<li><a href="{% url 'secrets:secretrole_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Secret Role</a></li>
<li><a href="{% url 'secrets:secretrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Secret Role</a></li>
{% endif %}
</ul>
</li>
@@ -215,12 +232,12 @@
<ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %}
{% if request.user.is_staff %}
<li><a href="{% url 'admin:index' %}"><i class="glyphicon glyphicon-cog" aria-hidden="true"></i> Admin</a></li>
<li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs" aria-hidden="true"></i> Admin</a></li>
{% endif %}
<li><a href="{% url 'users:profile' %}"><i class="glyphicon glyphicon-user" aria-hidden="true"></i> Profile</a></li>
<li><a href="{% url 'logout' %}"><i class="glyphicon glyphicon-log-out" aria-hidden="true"></i> Log out</a></li>
<li><a href="{% url 'users:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> Profile</a></li>
<li><a href="{% url 'logout' %}"><i class="fa fa-sign-out" aria-hidden="true"></i> Log out</a></li>
{% else %}
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="glyphicon glyphicon-log-in" aria-hidden="true"></i> Log in</a></li>
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in" aria-hidden="true"></i> Log in</a></li>
{% endif %}
</ul>
</div>

View File

@@ -1,24 +1,24 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}{{ circuit.provider }} Circuit {{ circuit.cid }}{% endblock %}
{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-9">
<ol class="breadcrumb">
<li><a href="{% url 'dcim:site' slug=circuit.site.slug %}">{{ circuit.site }}</a></li>
<li><a href="{% url 'circuits:circuit_list' %}?site={{ circuit.site.slug }}">Circuits</a></li>
<li>{{ circuit }}</li>
<li><a href="{% url 'circuits:circuit_list' %}">Circuits</a></li>
<li><a href="{% url 'circuits:circuit_list' %}?provider={{ circuit.provider.slug }}">{{ circuit.provider }}</a></li>
<li>{{ circuit.cid }}</li>
</ol>
</div>
<div class="col-md-3">
<form action="{% url 'circuits:circuit_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Circuit ID" />
<input type="text" name="q" class="form-control" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
@@ -28,18 +28,18 @@
<div class="pull-right">
{% if perms.circuits.change_circuit %}
<a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this circuit
</a>
{% endif %}
{% if perms.circuits.delete_circuit %}
<a href="{% url 'circuits:circuit_delete' pk=circuit.pk %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this circuit
</a>
{% endif %}
</div>
<h1>{{ circuit.provider }} Circuit {{ circuit.cid }}</h1>
<h1>{{ circuit.provider }} - {{ circuit.cid }}</h1>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
@@ -57,6 +57,68 @@
<td>Circuit ID</td>
<td>{{ circuit.cid }}</td>
</tr>
<tr>
<td>Type</td>
<td><a href="{{ circuit.type.get_absolute_url }}">{{ circuit.type }}</a></td>
</tr>
<tr>
<td>Tenant</td>
<td>
{% if circuit.tenant %}
<a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Install Date</td>
<td>
{% if circuit.install_date %}
{{ circuit.install_date }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Speed</td>
<td>
{% if circuit.upstream_speed %}
<i class="fa fa-arrow-down" title="Downstream"></i> {{ circuit.port_speed_human }} &nbsp;
<i class="fa fa-arrow-up" title="Upstream"></i> {{ circuit.upstream_speed_human }}
{% else %}
{{ circuit.port_speed_human }}
{% endif %}
</td>
</tr>
<tr>
<td>Commit Rate</td>
<td>
{% if circuit.commit_speed %}
{{ circuit.commit_speed_human }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Created</td>
<td>{{ circuit.created }}</td>
</tr>
<tr>
<td>Last Updated</td>
<td>{{ circuit.last_updated }}</td>
</tr>
</table>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Termination</strong>
</div>
<table class="table table-hover panel-body">
<tr>
<td>Site</td>
<td>
@@ -73,44 +135,34 @@
{% endif %}
</td>
</tr>
<tr>
<td>Install Date</td>
<td>{{ circuit.install_date }}</td>
</tr>
<tr>
<td>Port Speed</td>
<td>{{ circuit.port_speed_human }}</td>
</tr>
<tr>
<td>Commit Rate</td>
<td>{% if circuit.commit_rate %}{{ circuit.commit_rate_human }}{% else %}<span class="text-muted">N/A</span>{% endif %}</td>
</tr>
<tr>
<td>Cross-Connect</td>
<td>{{ circuit.xconnect_id }}</td>
<td>
{% if circuit.xconnect_id %}
{{ circuit.xconnect_id }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Patch Panel/Port</td>
<td>{{ circuit.pp_info }}</td>
</tr>
<tr>
<td>Created</td>
<td>{{ circuit.created }}</td>
</tr>
<tr>
<td>Last Updated</td>
<td>{{ circuit.last_updated }}</td>
<td>
{% if circuit.pp_info %}
{{ circuit.pp_info }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if circuit.comments %}
{% if circuit.comments %}
{{ circuit.comments|gfm }}
{% else %}
<span class="text-muted">None</span>

View File

@@ -9,13 +9,20 @@
{% render_field form.provider %}
{% render_field form.cid %}
{% render_field form.type %}
{% render_field form.tenant %}
{% render_field form.install_date %}
{% render_field form.port_speed %}
{% render_field form.commit_rate %}
{% render_field form.xconnect_id %}
{% render_field form.pp_info %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Bandwidth</strong></div>
<div class="panel-body">
{% render_field form.port_speed %}
{% render_field form.upstream_speed %}
{% render_field form.commit_rate %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Termination</strong></div>
<div class="panel-body">

View File

@@ -43,6 +43,11 @@
<td>Circuit type</td>
<td>Transit</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Strickland Propane</td>
</tr>
<tr>
<td>Site</td>
<td>Site name</td>
@@ -55,8 +60,13 @@
</tr>
<tr>
<td>Port Speed</td>
<td>Physical speed in Kbps/td>
<td>10000</td>
<td>Physical speed in Kbps</td>
<td>100000</td>
</tr>
<tr>
<td>Upstream Speed</td>
<td>Upstream speed in Kbps (optional)</td>
<td>20000</td>
</tr>
<tr>
<td>Commit rate</td>
@@ -76,7 +86,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>IC-603122,TeliaSonera,Transit,ASH-4,2016-02-23,10000,2000,937649,PP8371 ports 13/14</pre>
<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,ASH-4,2016-02-23,100000,,2000,937649,PP8371 ports 13/14</pre>
</div>
</div>
{% endblock %}

View File

@@ -7,9 +7,13 @@
<div class="pull-right">
{% if perms.circuits.add_circuit %}
<a href="{% url 'circuits:circuit_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a circuit
</a>
<a href="{% url 'circuits:circuit_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import circuits
</a>
{% endif %}
{% include 'inc/export_button.html' with obj_type='circuits' %}
</div>
@@ -19,23 +23,7 @@
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_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:circuit_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/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>

View File

@@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.circuits.add_circuittype %}
<a href="{% url 'circuits:circuittype_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a circuit type
</a>
{% endif %}

View File

@@ -6,27 +6,41 @@
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="col-md-9">
<ol class="breadcrumb">
<li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
<li>{{ provider }}</li>
</ol>
</div>
<div class="col-md-3">
<form action="{% url 'circuits:provider_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
<div class="pull-right">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider_graphs' pk=provider.pk %}" title="Show graphs">
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
Graphs
</button>
{% if show_graphs %}
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider_graphs' pk=provider.pk %}" title="Show graphs">
<i class="fa fa-signal" aria-hidden="true"></i>
Graphs
</button>
{% endif %}
{% if perms.circuits.change_provider %}
<a href="{% url 'circuits:provider_edit' slug=provider.slug %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this provider
</a>
{% endif %}
{% if perms.circuits.delete_provider %}
<a href="{% url 'circuits:provider_delete' slug=provider.slug %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this provider
</a>
{% endif %}
@@ -41,25 +55,53 @@
<table class="table table-hover panel-body">
<tr>
<td>ASN</td>
<td>{{ provider.asn }}</td>
<td>
{% if provider.asn %}
{{ provider.asn }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Account</td>
<td>{{ provider.account }}</td>
<td>
{% if provider.account %}
{{ provider.account }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Customer Portal</td>
<td>
<a href="{{ provider.portal_url }}">{{ provider.portal_url }}</a>
{% if provider.portal_url %}
<a href="{{ provider.portal_url }}">{{ provider.portal_url }}</a>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>NOC Contact</td>
<td>{{ provider.noc_contact|linebreaksbr }}</td>
<td>
{% if provider.noc_contact %}
{{ provider.noc_contact|linebreaksbr }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Admin Contact</td>
<td>{{ provider.admin_contact|linebreaksbr }}</td>
<td>
{% if provider.admin_contact %}
{{ provider.admin_contact|linebreaksbr }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Created</td>
@@ -76,7 +118,7 @@
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if provider.comments %}
{% if provider.comments %}
{{ provider.comments|gfm }}
{% else %}
<span class="text-muted">None</span>

View File

@@ -6,9 +6,13 @@
<div class="pull-right">
{% if perms.circuits.add_provider %}
<a href="{% url 'circuits:provider_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a provider
</a>
<a href="{% url 'circuits:provider_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import providers
</a>
{% endif %}
{% include 'inc/export_button.html' with obj_type='providers' %}
</div>
@@ -18,23 +22,7 @@
{% 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/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>

View File

@@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.change_consoleport %}
<a href="{% url 'dcim:console_connections_import' %}" class="btn btn-info">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
<span class="fa fa-download" aria-hidden="true"></span>
Import connections
</a>
{% endif %}

View File

@@ -14,6 +14,16 @@
<strong>Device</strong>
</div>
<table class="table table-hover panel-body">
<tr>
<td>Tenant</td>
<td>
{% if device.tenant %}
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Site</td>
<td>
@@ -50,12 +60,22 @@
</td>
</tr>
<tr>
<td>Serial</td>
<td>Serial Number</td>
<td>
{% if device.serial %}
<span>{{ device.serial }}</span>
{% else %}
<span class="text-muted">Not defined</span>
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Asset Tag</td>
<td>
{% if device.asset_tag %}
<span>{{ device.asset_tag }}</span>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
@@ -77,7 +97,7 @@
<tr>
<td>Role</td>
<td>
<a href="{% url 'dcim:device_list' %}?role={{ device.device_role.slug }}">{{ device.device_role }}</a>
<a href="{{ device.device_role.get_absolute_url }}">{{ device.device_role }}</a>
</td>
</tr>
<tr>
@@ -86,7 +106,7 @@
{% if device.platform %}
<span>{{ device.platform }}</span>
{% else %}
<span class="text-warning">Not assigned</span>
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
@@ -111,7 +131,7 @@
<span>(NAT: {{ device.primary_ip4.nat_outside.address.ip }})</span>
{% endif %}
{% else %}
<span class="text-muted">Not defined</span>
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
@@ -126,7 +146,7 @@
<span>(NAT: {{ device.primary_ip6.nat_outside.address.ip }})</span>
{% endif %}
{% else %}
<span class="text-muted">Not defined</span>
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
@@ -257,7 +277,7 @@
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if device.comments %}
{% if device.comments %}
{{ device.comments|gfm }}
{% else %}
<span class="text-muted">None</span>

View File

@@ -9,6 +9,7 @@
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
<td>{{ device.device_type }}</td>
<td>{{ device.device_role }}</td>
<td>{{ device.tenant }}</td>
<td>{{ device.serial }}</td>
</tr>
{% endfor %}

View File

@@ -7,6 +7,7 @@
<div class="panel-body">
{% render_field form.name %}
{% render_field form.device_role %}
{% render_field form.tenant %}
</div>
</div>
<div class="panel panel-default">
@@ -15,6 +16,7 @@
{% render_field form.manufacturer %}
{% render_field form.device_type %}
{% render_field form.serial %}
{% render_field form.asset_tag %}
</div>
</div>
<div class="panel panel-default">

View File

@@ -36,6 +36,11 @@
<td>Functional role of device</td>
<td>ToR Switch</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Pied Piper</td>
</tr>
<tr>
<td>Device manufacturer</td>
<td>Hardware manufacturer</td>
@@ -52,10 +57,15 @@
<td>Juniper Junos</td>
</tr>
<tr>
<td>Serial</td>
<td>Serial number (optional)</td>
<td>Serial number</td>
<td>Physical serial number (optional)</td>
<td>CAB00577291</td>
</tr>
<tr>
<td>Asset tag</td>
<td>Unique alphanumeric tag (optional)</td>
<td>ABC123456</td>
</tr>
<tr>
<td>Site</td>
<td>Site name</td>
@@ -79,7 +89,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>rack101_sw1,ToR Switch,Juniper,EX4300-48T,Juniper Junos,CAB00577291,Ashburn-VA,R101,21,Rear</pre>
<pre>rack101_sw1,ToR Switch,Pied Piper,Juniper,EX4300-48T,Juniper Junos,CAB00577291,ABC123456,Ashburn-VA,R101,21,Rear</pre>
</div>
</div>
{% endblock %}

View File

@@ -36,6 +36,11 @@
<td>Functional role of device</td>
<td>Blade Server</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Pied Piper</td>
</tr>
<tr>
<td>Device manufacturer</td>
<td>Hardware manufacturer</td>
@@ -52,10 +57,15 @@
<td>Linux</td>
</tr>
<tr>
<td>Serial</td>
<td>Serial number (optional)</td>
<td>Serial number</td>
<td>Physical serial number (optional)</td>
<td>CAB00577291</td>
</tr>
<tr>
<td>Asset tag</td>
<td>Unique alphanumeric tag (optional)</td>
<td>ABC123456</td>
</tr>
<tr>
<td>Parent device</td>
<td>Parent device</td>
@@ -69,7 +79,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>Blade12,Blade Server,Dell,BS2000T,Linux,CAB00577291,Server101,Slot4</pre>
<pre>Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,ABC123456,Server101,Slot4</pre>
</div>
</div>
{% endblock %}

View File

@@ -17,7 +17,23 @@
</tr>
<tr>
<td>Serial Number</td>
<td>{{ device.serial }}</td>
<td>
{% if device.serial %}
<span>{{ device.serial }}</span>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Asset Tag</td>
<td>
{% if device.asset_tag %}
<span>{{ device.asset_tag }}</span>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
</table>
</div>
@@ -32,6 +48,7 @@
<tr>
<th>Module</th>
<th></th>
<th>Manufacturer</th>
<th>Part Number</th>
<th>Serial Number</th>
<th></th>
@@ -42,6 +59,7 @@
<tr>
<td>{{ m.name }}</td>
<td>{% if not m.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
<td>{{ m.manufacturer|default:'' }}</td>
<td>{{ m.part_id }}</td>
<td>{{ m.serial }}</td>
<td class="text-right">
@@ -57,6 +75,7 @@
<tr>
<td style="padding-left: 20px">{{ m2.name }}</td>
<td>{% if not m2.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
<td>{{ m2.manufacturer|default:'' }}</td>
<td>{{ m2.part_id }}</td>
<td>{{ m2.serial }}</td>
<td class="text-right">
@@ -72,6 +91,7 @@
<tr>
<td style="padding-left: 40px">{{ m3.name }}</td>
<td>{% if not m3.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
<td>{{ m3.manufacturer|default:'' }}</td>
<td>{{ m3.part_id }}</td>
<td>{{ m3.serial }}</td>
<td class="text-right">
@@ -87,6 +107,7 @@
<tr>
<td style="padding-left: 60px">{{ m4.name }}</td>
<td>{% if not m4.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
<td>{{ m4.manufacturer|default:'' }}</td>
<td>{{ m4.part_id }}</td>
<td>{{ m4.serial }}</td>
<td class="text-right">
@@ -107,7 +128,7 @@
</div>
{% if perms.dcim.add_module %}
<a href="{% url 'dcim:module_add' pk=device.pk %}" class="btn btn-success">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a Module
</a>
{% endif %}

View File

@@ -7,11 +7,11 @@
<div class="pull-right">
{% if perms.dcim.add_device %}
<a href="{% url 'dcim:device_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a device
</a>
<a href="{% url 'dcim:device_import' %}" class="btn btn-info">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
<span class="fa fa-download" aria-hidden="true"></span>
Import devices
</a>
{% endif %}
@@ -23,24 +23,7 @@
{% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}
</div>
<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">
<form action="{% url 'dcim:device_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Name or serial" {% 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/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>

View File

@@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.add_devicerole %}
<a href="{% url 'dcim:devicerole_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a device role
</a>
{% endif %}

View File

@@ -19,13 +19,13 @@
<div class="pull-right">
{% if perms.dcim.change_devicetype %}
<a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this device type
</a>
{% endif %}
{% if perms.dcim.delete_devicetype %}
<a href="{% url 'dcim:devicetype_delete' pk=devicetype.pk %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this device type
</a>
{% endif %}

View File

@@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.add_devicetype %}
<a href="{% url 'dcim:devicetype_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a device type
</a>
{% endif %}

View File

@@ -16,10 +16,10 @@
<div class="col-md-3">
<form action="{% url 'dcim:device_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Device name or serial" />
<input type="text" name="q" class="form-control" placeholder="Search devices" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>

View File

@@ -34,10 +34,12 @@
</td>
{% endif %}
<td class="text-right">
{% if iface.circuit or iface.connection %}
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs">
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
</button>
{% if show_graphs %}
{% if iface.circuit or iface.connection %}
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs">
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
</button>
{% endif %}
{% endif %}
{% if perms.dcim.change_interface %}
{% if iface.is_physical %}
@@ -54,6 +56,10 @@
<a href="{% url 'dcim:interfaceconnection_delete' pk=iface.connection.pk %}?device={{ device.pk }}" class="btn btn-danger btn-xs" title="Delete connection">
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
</a>
{% elif iface.circuit and perms.circuits.change_circuit %}
<a href="{% url 'circuits:circuit_edit' pk=iface.circuit.pk %}" class="btn btn-danger btn-xs" title="Edit circuit">
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
</a>
{% else %}
<a href="{% url 'dcim:interfaceconnection_add' pk=device.pk %}?interface={{ iface.pk }}" class="btn btn-success btn-xs" title="Connect">
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>

View File

@@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.add_interfaceconnection %}
<a href="{% url 'dcim:interface_connections_import' %}" class="btn btn-info">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
<span class="fa fa-download" aria-hidden="true"></span>
Import connections
</a>
{% endif %}

View File

@@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.add_manufacturer %}
<a href="{% url 'dcim:manufacturer_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a manufacturer
</a>
{% endif %}

View File

@@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.add_platform %}
<a href="{% url 'dcim:platform_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a platform
</a>
{% endif %}

View File

@@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.change_powerport %}
<a href="{% url 'dcim:power_connections_import' %}" class="btn btn-info">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
<span class="fa fa-download" aria-hidden="true"></span>
Import connections
</a>
{% endif %}

View File

@@ -8,18 +8,18 @@
<div class="row">
<div class="col-md-9">
<ol class="breadcrumb">
<li><a href="{% url 'dcim:site' slug=rack.site.slug %}">{{ rack.site }}</a></li>
<li><a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}">Racks</a></li>
<li><a href="{% url 'dcim:rack_list' %}">Racks</a></li>
<li><a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}">{{ rack.site }}</a></li>
<li>{{ rack }}</li>
</ol>
</div>
<div class="col-md-3">
<form action="{% url 'dcim:rack_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Rack name or ID" />
<input type="text" name="q" class="form-control" placeholder="Search racks" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
@@ -29,25 +29,25 @@
<div class="pull-right">
{% if prev_rack %}
<a href="{% url 'dcim:rack' pk=prev_rack.pk %}" class="btn btn-primary">
<span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
<span class="fa fa-chevron-left" aria-hidden="true"></span>
Previous Rack
</a>
{% endif %}
{% if next_rack %}
<a href="{% url 'dcim:rack' pk=next_rack.pk %}" class="btn btn-primary">
<span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
<span class="fa fa-chevron-right" aria-hidden="true"></span>
Next Rack
</a>
{% endif %}
{% if perms.dcim.change_rack %}
<a href="{% url 'dcim:rack_edit' pk=rack.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this rack
</a>
{% endif %}
{% if perms.dcim.delete_rack %}
<a href="{% url 'dcim:rack_delete' pk=rack.pk %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this rack
</a>
{% endif %}
@@ -81,11 +81,45 @@
<td>
{% if rack.facility_id %}
<span>{{ rack.facility_id }}</span>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Tenant</td>
<td>
{% if rack.tenant %}
<a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Role</td>
<td>
{% if rack.role %}
<a href="{{ rack.role.get_absolute_url }}">{{ rack.role }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Type</td>
<td>
{% if rack.type %}
{{ rack.get_type_display }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Width</td>
<td>{{ rack.get_width_display }}</td>
</tr>
<tr>
<td>Height</td>
<td>{{ rack.u_height }}U</td>
@@ -146,7 +180,7 @@
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if rack.comments %}
{% if rack.comments %}
{{ rack.comments|gfm }}
{% else %}
<span class="text-muted">None</span>

View File

@@ -4,12 +4,24 @@
{% block title %}Rack Bulk Edit{% endblock %}
{% block select_objects_table %}
<tr>
<th>Name</th>
<th>Site</th>
<th>Group</th>
<th>Tenant</th>
<th>Type</th>
<th>Width</th>
<th>Height</th>
</tr>
{% for rack in selected_objects %}
<tr>
<td><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack }}</a></td>
<td>{{ rack.facility_id }}</td>
<td>{{ rack.site }}</td>
<td>{{ rack.u_height }}</td>
<td>{{ rack.group }}</td>
<td>{{ rack.tenant }}</td>
<td>{{ rack.get_type_display }}</td>
<td>{{ rack.get_width_display }}</td>
<td>{{ rack.u_height }}U</td>
</tr>
{% endfor %}
{% endblock %}

View File

@@ -9,6 +9,10 @@
{% render_field form.group %}
{% render_field form.name %}
{% render_field form.facility_id %}
{% render_field form.tenant %}
{% render_field form.role %}
{% render_field form.type %}
{% render_field form.width %}
{% render_field form.u_height %}
</div>
</div>

View File

@@ -48,6 +48,21 @@
<td>Rack ID assigned by the facility (optional)</td>
<td>J12.100</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Pied Piper</td>
</tr>
<tr>
<td>Type</td>
<td>Rack type (optional)</td>
<td>4-post cabinet</td>
</tr>
<tr>
<td>Width</td>
<td>Rail-to-rail width (19 or 23 inches)</td>
<td>19</td>
</tr>
<tr>
<td>Height</td>
<td>Height in rack units</td>
@@ -56,7 +71,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>DC-4,Cage 1400,R101,J12.100,42</pre>
<pre>DC-4,Cage 1400,R101,J12.100,Pied Piper,4-post cabinet,19,42</pre>
</div>
</div>
{% endblock %}

View File

@@ -7,11 +7,11 @@
<div class="pull-right">
{% if perms.dcim.add_rack %}
<a href="{% url 'dcim:rack_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a rack
</a>
<a href="{% url 'dcim:rack_import' %}" class="btn btn-info">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
<span class="fa fa-download" aria-hidden="true"></span>
Import racks
</a>
{% endif %}
@@ -23,24 +23,7 @@
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rack_bulk_edit' bulk_delete_url='dcim:rack_bulk_delete' %}
</div>
<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">
<form action="{% url 'dcim:rack_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/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>

View File

@@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.add_rackgroup %}
<a href="{% url 'dcim:rackgroup_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a rack group
</a>
{% endif %}

View File

@@ -0,0 +1,21 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Rack Role{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_rackrole %}
<a href="{% url 'dcim:rackrole_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a rack role
</a>
{% endif %}
</div>
<h1>Rack Roles</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackrole_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@@ -16,10 +16,10 @@
<div class="col-md-3">
<form action="{% url 'dcim:site_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Search" />
<input type="text" name="q" class="form-control" placeholder="Search sites" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
@@ -27,38 +27,62 @@
</div>
</div>
<div class="pull-right">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site_graphs' pk=site.pk %}" title="Show graphs">
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
Graphs
</button>
{% if show_graphs %}
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site_graphs' pk=site.pk %}" title="Show graphs">
<i class="fa fa-signal" aria-hidden="true"></i>
Graphs
</button>
{% endif %}
{% if perms.dcim.change_site %}
<a href="{% url 'dcim:site_edit' slug=site.slug %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this site
</a>
{% endif %}
{% if perms.dcim.delete_site %}
<a href="{% url 'dcim:site_delete' slug=site.slug %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this site
</a>
{% endif %}
</div>
<h1>{{ site.name }}</h1>
<div class="row">
<div class="col-md-6">
<div class="col-md-7">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Site</strong>
</div>
<table class="table table-hover panel-body">
<tr>
<td>Tenant</td>
<td>
{% if site.tenant %}
<a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Facility</td>
<td>{{ site.facility }}</td>
<td>
{% if site.facility %}
{{ site.facility }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>AS Number</td>
<td>{{ site.asn }}</td>
<td>
{% if site.asn %}
{{ site.asn }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Physical Address</td>
@@ -69,8 +93,10 @@
<i class="glyphicon glyphicon-map-marker"></i> Map it
</a>
</div>
<span>{{ site.physical_address|linebreaksbr }}</span>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
<span>{{ site.physical_address|linebreaksbr }}</span>
</td>
</tr>
<tr>
@@ -79,7 +105,7 @@
{% if site.shipping_address %}
<span>{{ site.shipping_address|linebreaksbr }}</span>
{% else %}
<span class="text-muted">See physical address</span>
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
@@ -98,7 +124,7 @@
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if site.comments %}
{% if site.comments %}
{{ site.comments|gfm }}
{% else %}
<span class="text-muted">None</span>
@@ -106,43 +132,33 @@
</div>
</div>
</div>
<div class="col-md-6">
<div class="col-md-5">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Stats</strong>
</div>
<table class="table table-hover panel-body">
<tr>
<td>Racks</td>
<td>
<a href="{% url 'dcim:rack_list' %}?site={{ site.slug }}">{{ stats.rack_count }}</a>
</td>
</tr>
<tr>
<td>Devices</td>
<td>
<a href="{% url 'dcim:device_list' %}?site={{ site.slug }}">{{ stats.device_count }}</a>
</td>
</tr>
<tr>
<td>Prefixes</td>
<td>
<a href="{% url 'ipam:prefix_list' %}?site={{ site.slug }}">{{ stats.prefix_count }}</a>
</td>
</tr>
<tr>
<td>VLANs</td>
<td>
<a href="{% url 'ipam:vlan_list' %}?site={{ site.slug }}">{{ stats.vlan_count }}</a>
</td>
</tr>
<tr>
<td>Circuits</td>
<td>
<a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}">{{ stats.circuit_count }}</a>
</td>
</tr>
</table>
<div class="row panel-body">
<div class="col-md-4 text-center">
<h2><a href="{% url 'dcim:rack_list' %}?site={{ site.slug }}" class="btn {% if stats.rack_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.rack_count }}</a></h2>
<p>Racks</p>
</div>
<div class="col-md-4 text-center">
<h2><a href="{% url 'dcim:device_list' %}?site={{ site.slug }}" class="btn {% if stats.device_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.device_count }}</a></h2>
<p>Devices</p>
</div>
<div class="col-md-4 text-center">
<h2><a href="{% url 'ipam:prefix_list' %}?site={{ site.slug }}" class="btn {% if stats.prefix_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.prefix_count }}</a></h2>
<p>Prefixes</p>
</div>
<div class="col-md-4 text-center">
<h2><a href="{% url 'ipam:vlan_list' %}?site={{ site.slug }}" class="btn {% if stats.vlan_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.vlan_count }}</a></h2>
<p>VLANs</p>
</div>
<div class="col-md-4 text-center">
<h2><a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2>
<p>Circuits</p>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">

View File

@@ -0,0 +1,13 @@
{% extends 'utilities/bulk_edit_form.html' %}
{% load form_helpers %}
{% block title %}Site Bulk Edit{% endblock %}
{% block select_objects_table %}
{% for site in selected_objects %}
<tr>
<td><a href="{% url 'dcim:site' slug=site.slug %}">{{ site.slug }}</a></td>
<td>{{ site.tenant }}</td>
</tr>
{% endfor %}
{% endblock %}

View File

@@ -7,6 +7,7 @@
<div class="panel-body">
{% render_field form.name %}
{% render_field form.slug %}
{% render_field form.tenant %}
{% render_field form.facility %}
{% render_field form.asn %}
{% render_field form.physical_address %}

View File

@@ -38,6 +38,11 @@
<td>URL-friendly name</td>
<td>ash4-south</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Pied Piper</td>
</tr>
<tr>
<td>Facility</td>
<td>Name of the hosting facility (optional)</td>
@@ -51,7 +56,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>ASH-4 South,ash4-south,Equinix DC6,65000</pre>
<pre>ASH-4 South,ash4-south,Pied Piper,Equinix DC6,65000</pre>
</div>
</div>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More