Compare commits

...

190 Commits

Author SHA1 Message Date
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
589cbeb559 Fixed DeviceType test 2016-07-26 11:58:57 -04:00
Jeremy Stretch
8dbeec8b00 Release v1.3.2 2016-07-26 11:54:01 -04:00
Jeremy Stretch
8f4980044a Closes #292: Added part_number field to DeviceType 2016-07-26 11:28:45 -04:00
Jeremy Stretch
cc4470ade7 Include form factor on InterfaceTemplate list 2016-07-26 10:54:11 -04:00
Jeremy Stretch
3b4c8fa49c Fix unclosed form element 2016-07-26 10:17:19 -04:00
Jeremy Stretch
b4d68382ce Fix unclosed form element 2016-07-26 10:16:23 -04:00
Jeremy Stretch
4be5c33905 Enabled bulk deletion of device bays, interfaces, console server ports, and power outlets from devices 2016-07-26 10:14:51 -04:00
Jeremy Stretch
88b022d742 Corrected Unicode display of ExportTemplates 2016-07-26 09:15:46 -04:00
Jeremy Stretch
e5b19a9374 Fixes #385: Corrected Unicode rendering of UserAction 2016-07-26 09:11:35 -04:00
Jeremy Stretch
bd6e68fe6c Fixes #384: Corrected max_length in description fields 2016-07-25 17:12:45 -04:00
Jeremy Stretch
8e2a69af56 Corrected manufacturer column name in DeviceTypeTable 2016-07-25 17:06:10 -04:00
Jeremy Stretch
450c51604c Replaced all object-specific BulkDeleteForms with the stock form provided by utilities.BulkDeleteView 2016-07-25 17:02:53 -04:00
Jeremy Stretch
d47bf4ab6b Rewrote all DeviceType component template deletion views to utilize BulkDeleteView() 2016-07-25 16:32:07 -04:00
Jeremy Stretch
d241cce502 ipam.VLAN: Added description field, extended name to 64 chars 2016-07-25 14:58:49 -04:00
Jeremy Stretch
c466dc5999 Fixes #381: Implements a new RackImportTable 2016-07-25 12:04:42 -04:00
Jeremy Stretch
b62cd32428 Fixes #370: Notify user when secret decryption fails 2016-07-22 14:43:14 -04:00
Jeremy Stretch
b9223dda1a Updated CONTRIBUTING to discourage the use of issues for questions/discussion 2016-07-22 12:16:03 -04:00
Jeremy Stretch
b9c09b2fc2 Merge pull request #360 from jallakim/allowed-hosts
Be more specific in the documentation regarding ALLOWED_HOSTS
2016-07-22 11:48:52 -04:00
Joachim Tingvold
deda796e42 Triple -> single ticks + grammar. 2016-07-22 17:41:00 +02:00
Joachim Tingvold
55ab720695 Be more specific in the documentation regarding ALLOWED_HOSTS. 2016-07-21 21:05:24 +02:00
Jeremy Stretch
275223ec53 Fixes #359: Use standard serializers for related objects 2016-07-21 14:48:02 -04:00
Jeremy Stretch
f44b20bbda Fixed old links in the documentation 2016-07-21 12:27:20 -04:00
Jeremy Stretch
c96d03cc4b Post-release version bump 2016-07-21 12:22:03 -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
d2c3fea5b9 Release v1.3.1 2016-07-21 11:45:59 -04:00
Jeremy Stretch
8ee083f7c1 Fixed Unicode support in forms 2016-07-21 10:47:38 -04:00
Jeremy Stretch
9a9e3c1479 Upgrade to Django 1.9.8 (security fix) 2016-07-20 17:17:39 -04:00
Jeremy Stretch
48b8602c3f Corrected error reporting on duplicate InterfaceConnections 2016-07-20 16:42:04 -04:00
Jeremy Stretch
e1fc78bc44 Created a template tag for displaying utilization graphs 2016-07-20 13:56:17 -04:00
Jeremy Stretch
65fb10059a Merge pull request #346 from bellwood/patch-1
properly support #304
2016-07-20 13:33:16 -04:00
bellwood
2e8211399d Update tables.py 2016-07-20 13:25:03 -04:00
bellwood
6fe40ef223 support for #304 2016-07-20 13:23:49 -04:00
bellwood
3f94295d7e support for #304
support for #304
2016-07-20 13:22:20 -04:00
bellwood
5c59677c57 properly support #304
support for #304
2016-07-20 13:04:11 -04:00
Jeremy Stretch
0bd2aa9289 Updated the CONTRIBUTING guide 2016-07-20 11:50:32 -04:00
Jeremy Stretch
19d7caf1da Corrects a device_type error introduced in c643e3a74f 2016-07-20 10:10:40 -04:00
Jeremy Stretch
b8d7dd170e #303: First stab at implementing a natural ordering for sites, racks, and devices 2016-07-20 10:07:32 -04:00
Jeremy Stretch
c643e3a74f Fixes #327: Disable rack assignment for installed child devices 2016-07-19 13:09:15 -04:00
Jeremy Stretch
2d690ca38a Merge pull request #314 from Zanthras/interfaceconnections
Add an API call for listing all interface connections
2016-07-19 12:35:56 -04:00
Joel
c65b9fcb0b Add an api endpoint for listing all connections 2016-07-19 09:08:14 -07:00
Joel
4f6f032ca2 Update the valid urls, to expose the new api connection listing endpoint. Naming convention updated for both interface connections to match the rest. 2016-07-19 09:08:14 -07:00
Jeremy Stretch
50d20650b4 Merge pull request #333 from digitalocean/secrets-api-filter
Fixes #332 - Add device filter to secrets api.
2016-07-19 11:12:59 -04:00
Jeremy Stretch
783341017f Fixes #331: Add group field to VLAN bulk edit form 2016-07-19 11:11:16 -04:00
Zach Moody
c9dc6d04ef Fixes #332 - Add device filter to secrets api. 2016-07-19 10:08:59 -05:00
Jeremy Stretch
82ad479037 Enforce authentication for all secrets API views 2016-07-18 15:28:36 -04:00
Jeremy Stretch
0d46a65a36 Unicode handling cleanup 2016-07-18 14:48:51 -04:00
Jeremy Stretch
7a50cd2320 Post-release version bump 2016-07-18 13:50:46 -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
4f347d3428 Version bump: v1.3.0 2016-07-18 13:43:39 -04:00
Jeremy Stretch
d6c2fe2385 Fixes #317: Rack elevation display fix for device types greater than 42U in height 2016-07-18 13:03:40 -04:00
Jeremy Stretch
cb4643d810 Added support for group assignment during VLAN import 2016-07-18 11:59:55 -04:00
Jeremy Stretch
d201dad535 Fixes #322: Corrected 'vlan_group' to 'group' 2016-07-18 11:35:50 -04:00
Jeremy Stretch
32d8cf451a Fixes #320: Disallow prefixes with host masks 2016-07-18 10:06:43 -04:00
Jeremy Stretch
46da9866e3 Added group to VLAN view 2016-07-15 16:32:00 -04:00
Jeremy Stretch
534e6ac19e Fixes #308: Update rack assignment for all child devices when moving a parent device 2016-07-15 16:05:21 -04:00
Jeremy Stretch
518af1b95c Corrected RackGroupNestedSerializer() definition 2016-07-15 15:34:28 -04:00
Jeremy Stretch
4f95ce4984 Fixes #311: Correct IPAddress family evaluation on import 2016-07-15 15:14:49 -04:00
Jeremy Stretch
da10b34738 Closes #42: Allow VLAN assignment during prefix import 2016-07-15 14:25:30 -04:00
Jeremy Stretch
a9ab0a012f Merge pull request #309 from digitalocean/vlan-groups
Closes #111: Implement VLAN groups
2016-07-15 13:36:32 -04:00
Jeremy Stretch
45a8ee7325 Closes #111: Implement VLAN groups 2016-07-15 13:26:54 -04:00
Jeremy Stretch
23451fe974 Added a custom 500 handler to include exception details 2016-07-15 11:04:03 -04:00
Jeremy Stretch
5def0e91d7 Fixes #307: Validate device type assignment during import validation 2016-07-15 09:45:45 -04:00
Jeremy Stretch
f301af5ecd Fixes #301: Prevent deletion of DeviceBay when installed device is deleted 2016-07-14 17:41:16 -04:00
Jeremy Stretch
dd62caf2f0 Fixes #227: Introduces support for bulk import of child devices 2016-07-14 17:35:52 -04:00
Jeremy Stretch
4a00971d44 Fixes #43: Introduce toggle to enforce unique IP space per VRF 2016-07-14 16:13:02 -04:00
Jeremy Stretch
bf44e512ff Post-release version bump 2016-07-14 15:22:14 -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
026403ed38 Release v1.2.2 2016-07-14 15:21:22 -04:00
Jeremy Stretch
f6bd1f0c48 Make the HA warning re: SECRET_KEY a note 2016-07-14 14:03:57 -04:00
Jeremy Stretch
66489438b9 Merge pull request #298 from rekkoner/develop
Updated SECRET_KEY instructions for HA installs. Issue 295
2016-07-14 14:01:06 -04:00
Jeremy Stretch
e5a6a4f05e Fixes #174: Added search and site filter to provider list 2016-07-14 13:53:30 -04:00
brandon whitehead
9e4aa9c056 Updated SECRET_KEY instructions for HA installs. Issue 295 2016-07-14 12:33:21 -05:00
Jeremy Stretch
4ce40891f0 Prettified device type view 2016-07-14 12:39:55 -04:00
Jeremy Stretch
46b1ac23af Allow for setting mgmt_only=True in "Add management interfaces" link 2016-07-14 11:39:53 -04:00
Jeremy Stretch
a5f6e64849 Fixes #290: Added mgmt interfaces table to device type view 2016-07-14 11:30:15 -04:00
Jeremy Stretch
b9db1ac7f7 Merge pull request #283 from ercpe/html-overflow
Use overflow-y: scoll on html element
2016-07-13 16:03:53 -04:00
Jeremy Stretch
124c2acad7 Merge pull request #287 from bellwood/ui-add-glyphicons-to-panel-headers
Add 'filter' glyphicon to filter panel header
2016-07-13 16:02:15 -04:00
bellwood
2691590aa1 Add 'search' glyphicon to filter panel header 2016-07-13 15:36:26 -04:00
bellwood
51cc0d5083 Add 'search' glyphicon to filter panel header 2016-07-13 15:36:07 -04:00
bellwood
9c32943d73 Add 'search' glyphicon to filter panel header 2016-07-13 15:35:41 -04:00
bellwood
4483ba55dd Add 'search' glyphicon to filter panel header 2016-07-13 15:34:23 -04:00
bellwood
f20e0edb35 Add 'search' glyphicon to filter panel header 2016-07-13 15:33:52 -04:00
bellwood
aed2180142 Add 'search' glyphicon to filter panel header 2016-07-13 15:32:39 -04:00
Jeremy Stretch
4913d25d18 Fixes #268: Added support for full 32-bit ASN space 2016-07-13 15:30:15 -04:00
bellwood
9e181c20c7 Add 'filter' glyphicon to filter panel header 2016-07-13 15:26:24 -04:00
Jeremy Stretch
404d934736 Removed redundant template context processor 2016-07-13 14:08:46 -04:00
Jeremy Stretch
024c7da15b Fixes #115: Fix deprecated django.core.context_processors reference 2016-07-13 14:05:21 -04:00
Jeremy Stretch
d3a5b82d93 Fixes #282: De-select "all" checkbox if one or more objects are deselected 2016-07-13 13:50:50 -04:00
Jeremy Stretch
1e3a03c463 Merge branch 'develop' of github.com:digitalocean/netbox into develop 2016-07-13 13:08:17 -04:00
Jeremy Stretch
bafbc052e2 Fixes #270: Add rack group filter for devices 2016-07-13 13:07:55 -04:00
Jeremy Stretch
9421ec040c Fixes #271: Add rack group filter for devices 2016-07-13 13:07:02 -04:00
Jeremy Stretch
07fc2e5502 Merge pull request #273 from bellwood/devices-filter-add-rackgroup
allow filtering by rack group
2016-07-13 12:55:11 -04:00
Jeremy Stretch
9098001bcb Post-release version bump 2016-07-13 12:11:10 -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
d9bf199e75 Version bump: v1.2.1 2016-07-13 12:01:34 -04:00
Jeremy Stretch
6f1ed9fc16 Clarified the process of voting on feature requests 2016-07-13 11:47:20 -04:00
Jeremy Stretch
96b496ffa8 Updated documentation to include banner settings 2016-07-13 11:24:34 -04:00
Jeremy Stretch
f1b6f0cfee Fixes #285: Added PREFER_IPV4 configuration setting 2016-07-13 11:16:09 -04:00
Jeremy Stretch
e19ce043d6 Fixes #275: Exclude self when checking for overlapping aggregates 2016-07-13 10:37:25 -04:00
Johann Schmitz
35a2671525 Use overflow-y: scoll on html element to avoid jumping around when the previous/next page adds a vertical scrollbar. 2016-07-13 15:39:59 +02:00
Jeremy Stretch
03542b400d Renamed IPAMConfig to DCIMConfig 2016-07-13 09:25:13 -04:00
Jeremy Stretch
73d24532c9 Merge pull request #281 from lukerussell/Link-to-docs-in-readme
Added a link to docs in readme.md
2016-07-13 09:04:00 -04:00
Jeremy Stretch
b60f964835 Fixes #272: Added a step to copy the gunicorn config 2016-07-12 16:57:00 -04:00
lukerussell
8e7e02a622 Added a link to docs in readme.md
Adding a direct link for easy access. I couldn't find a link anywhere except digging in through the docs/ directory.
2016-07-13 06:04:29 +10:00
Jeremy Stretch
2c23ca33a2 Fixes #274: Correct reference to old field 2016-07-12 15:48:56 -04:00
bellwood
69affb7a6e fixed "rack group" filter label for/dcim/racks/ 2016-07-12 15:16:32 -04:00
bellwood
6a6cf14a38 Update forms.py
added label
2016-07-12 15:12:36 -04:00
Jeremy Stretch
e1da3b8f10 Related to #243: Implemented natsort on all Device and DeviceType objects (except interfaces) 2016-07-12 14:53:59 -04:00
bellwood
da50cd0f03 allow filtering by rack group
adds the ability to filter devices by rack group
2016-07-12 14:42:47 -04:00
Jeremy Stretch
d80ffd2308 Merge pull request #248 from Zanthras/develop
possible fix for #243 generic sorting for device bays
2016-07-12 14:13:25 -04:00
Jeremy Stretch
18846cf40a Fixes #271: Corrected select_related() in secrets API 2016-07-12 12:27:26 -04:00
Jeremy Stretch
e81a2094df Post-release version bump 2016-07-12 11:40:40 -04:00
Joel
173a4cde8b Update the requirements file to include the natsort library requirement. 2016-07-08 23:27:00 -07:00
Joel
d9867423de Use the natsort library to provide a generic sorting option for better sorting of generic names for device bays. 2016-07-08 22:47:08 -07:00
182 changed files with 4537 additions and 1201 deletions

View File

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

View File

@@ -4,6 +4,8 @@ NetBox is an IP address management (IPAM) and data center infrastructure managem
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/) Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox).
The complete documentation for Netbox can be found at [Read the Docs](http://netbox.readthedocs.io/en/latest/).
Questions? Comments? Please join us on IRC in **#netbox** on **irc.freenode.net**!
### Build Status
@@ -23,6 +25,6 @@ Questions? Comments? Please join us on IRC in **#netbox** on **irc.freenode.net*
# Installation
Please see docs/getting-started.md for instructions on installing NetBox.
Please see [the documentation](http://netbox.readthedocs.io/en/latest/) for instructions on installing NetBox.
To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases) and run `upgrade.sh`.

View File

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

View File

@@ -13,11 +13,24 @@ ADMINS = [
---
## BANNER_TOP
## BANNER_BOTTOM
Setting these variables will display content in a banner at the top and/or bottom of the page, respectively. To replicate the content of the top banner in the bottom banner, set:
```
BANNER_TOP = 'Your banner text'
BANNER_BOTTOM = BANNER_TOP
```
---
## DEBUG
Default: False
This setting enables debugging. This should be done only during development or troubleshooting. Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users.
This setting enables debugging. This should be done only during development or troubleshooting. Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users.
---
@@ -34,9 +47,17 @@ In order to send email, NetBox needs an email server configured. The following i
---
# ENFORCE_GLOBAL_UNIQUE
Default: False
Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table (all prefixes and IP addresses not assigned to a VRF), set `ENFORCE_GLOBAL_UNIQUE` to True.
---
## LOGIN_REQUIRED
Default: False,
Default: False
Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users are permitted to access most data in NetBox (excluding secrets) but not make any changes.
@@ -66,6 +87,14 @@ Determine how many objects to display per page within each list of objects.
---
## PREFER_IPV4
Default: False
When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to prefer IPv4 instead.
---
## TIME_ZONE
Default: UTC

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

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

View File

@@ -112,6 +112,9 @@ Generate a random secret key of at least 50 alphanumeric characters. This key mu
You may use the script located at `netbox/generate_secret_key.py` to generate a suitable key.
!!! note
In the case of a highly available installation with multiple web servers, `SECRET_KEY` must be identical among all servers in order to maintain a persistent user session state.
# Run Database Migrations
Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
@@ -160,6 +163,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:

View File

@@ -4,42 +4,40 @@ As with the initial installation, you can upgrade NetBox by either downloading t
## Option A: Download a Release
Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`. For this guide we are using 1.0.4 as the old version and 1.0.7 as the new version.
Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`.
Download and extract the latest version:
Download & extract latest version:
```
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
# tar -xzf vX.Y.Z.tar.gz -C /opt
# cd /opt/
# ln -sf netbox-1.0.7/ netbox
# ln -sf netbox-X.Y.Z/ netbox
```
Copy the 'configuration.py' you created when first installing to the new version:
```
# cp /opt/netbox-1.0.4/configuration.py /opt/netbox/configuration.py
# cp /opt/netbox-X.Y.Z/configuration.py /opt/netbox/configuration.py
```
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
```
# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py
```
## Option B: Clone the Git Repository (latest master release)
For this guide, we'll use `/opt/netbox`.
This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
Check that your git branch is up to date & is set to master:
```
# cd /opt/netbox
# git status
```
If not on branch master, set it and verify status:
```
# git checkout master
# git pull origin master
# git status
```
Pull down the set branch from git status above:
```
# git pull
```
# Run the Upgrade Script
Once the new code is in place, run the upgrade script (which may need to be run as root depending on how your environment is configured).

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', 'commit_rate',
'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',
'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

@@ -1,9 +1,41 @@
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
class ProviderFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='circuits__site',
queryset=Site.objects.all(),
label='Site',
)
site = django_filters.ModelMultipleChoiceFilter(
name='circuits__site',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
class Meta:
model = Provider
fields = ['q', 'name', 'account', 'asn']
def search(self, queryset, value):
return queryset.filter(
Q(name__icontains=value) |
Q(account__icontains=value) |
Q(comments__icontains=value)
)
class CircuitFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
@@ -31,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(),
@@ -48,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,9 +2,10 @@ 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, ConfirmationForm, CSVDataField, Livesearch, SmallTextarea,
SlugField,
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, Livesearch, SmallTextarea, SlugField,
)
from .models import Circuit, CircuitType, Provider
@@ -55,8 +56,14 @@ class ProviderBulkEditForm(forms.Form, BootstrapMixin):
comments = CommentField()
class ProviderBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
def provider_site_choices():
site_choices = Site.objects.all()
return [(s.slug, s.name) for s in site_choices]
class ProviderFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=provider_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
#
@@ -71,10 +78,6 @@ class CircuitTypeForm(forms.ModelForm, BootstrapMixin):
fields = ['name', 'slug']
class CircuitTypeBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=CircuitType.objects.all(), widget=forms.MultipleHiddenInput)
#
# Circuits
#
@@ -98,7 +101,7 @@ class CircuitForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Circuit
fields = [
'cid', 'type', 'provider', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
'port_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
]
help_texts = {
@@ -159,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', 'commit_rate',
'xconnect_id', 'pp_info']
class CircuitImportForm(BulkImportForm, BootstrapMixin):
@@ -176,33 +181,37 @@ 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()
class CircuitBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
def circuit_type_choices():
type_choices = CircuitType.objects.annotate(circuit_count=Count('circuits'))
return [(t.slug, '{} ({})'.format(t.name, t.circuit_count)) for t in type_choices]
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in type_choices]
def circuit_provider_choices():
provider_choices = Provider.objects.annotate(circuit_count=Count('circuits'))
return [(p.slug, '{} ({})'.format(p.name, p.circuit_count)) for p in 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, '{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
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,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-13 19:24
from __future__ import unicode_literals
import dcim.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('circuits', '0002_auto_20160622_1821'),
]
operations = [
migrations.AlterField(
model_name='provider',
name='asn',
field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'),
),
]

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

@@ -1,7 +1,9 @@
from django.core.urlresolvers import reverse
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
@@ -12,7 +14,7 @@ class Provider(CreatedUpdatedModel):
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
asn = models.PositiveIntegerField(blank=True, null=True, verbose_name='ASN')
asn = ASNField(blank=True, null=True, verbose_name='ASN')
account = models.CharField(max_length=30, blank=True, verbose_name='Account number')
portal_url = models.URLField(blank=True, verbose_name='Portal')
noc_contact = models.TextField(blank=True, verbose_name='NOC contact')
@@ -65,6 +67,7 @@ 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')
@@ -79,7 +82,7 @@ class Circuit(CreatedUpdatedModel):
unique_together = ['provider', 'cid']
def __unicode__(self):
return "{0} {1}".format(self.provider, self.cid)
return u'{} {}'.format(self.provider, self.cid)
def get_absolute_url(self):
return reverse('circuits:circuit', args=[self.pk])
@@ -89,6 +92,7 @@ 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),

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,
)
@@ -16,6 +17,8 @@ from .models import Circuit, CircuitType, Provider
class ProviderListView(ObjectListView):
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
filter = filters.ProviderFilter
filter_form = forms.ProviderFilterForm
table = tables.ProviderTable
edit_permissions = ['circuits.change_provider', 'circuits.delete_provider']
template_name = 'circuits/provider_list.html'
@@ -25,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,
})
@@ -74,7 +79,6 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_provider'
cls = Provider
form = forms.ProviderBulkDeleteForm
default_redirect_url = 'circuits:provider_list'
@@ -100,7 +104,6 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuittype'
cls = CircuitType
form = forms.CircuitTypeBulkDeleteForm
default_redirect_url = 'circuits:circuittype_list'
@@ -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]
@@ -169,5 +176,4 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuit'
cls = Circuit
form = forms.CircuitBulkDeleteForm
default_redirect_url = 'circuits:circuit_list'

View File

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

View File

@@ -78,8 +78,8 @@ class DeviceTypeAdmin(admin.ModelAdmin):
InterfaceTemplateAdmin,
DeviceBayTemplateAdmin,
]
list_display = ['model', 'manufacturer', 'slug', 'u_height', 'console_ports', 'console_server_ports', 'power_ports',
'power_outlets', 'interfaces', 'device_bays']
list_display = ['model', 'manufacturer', 'slug', 'part_number', 'u_height', 'console_ports', 'console_server_ports',
'power_ports', 'power_outlets', 'interfaces', 'device_bays']
list_filter = ['manufacturer']
def get_queryset(self, request):
@@ -180,4 +180,4 @@ class DeviceAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super(DeviceAdmin, self).get_queryset(request)
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip', 'rack')
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip4', 'primary_ip6', 'rack')

View File

@@ -6,6 +6,7 @@ from dcim.models import (
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, 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']
@@ -38,7 +40,7 @@ class RackGroupSerializer(serializers.ModelSerializer):
fields = ['id', 'name', 'slug', 'site']
class RackGroupNestedSerializer(SiteSerializer):
class RackGroupNestedSerializer(RackGroupSerializer):
class Meta(SiteSerializer.Meta):
fields = ['id', 'name', 'slug']
@@ -52,10 +54,11 @@ class RackGroupNestedSerializer(SiteSerializer):
class RackSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
group = RackGroupNestedSerializer()
tenant = TenantNestedSerializer()
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', 'u_height', 'comments']
class RackNestedSerializer(RackSerializer):
@@ -69,8 +72,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', 'u_height', 'comments',
'front_units', 'rear_units']
def get_front_units(self, obj):
units = obj.get_rack_units(face=RACK_FACE_FRONT)
@@ -111,8 +114,8 @@ class DeviceTypeSerializer(serializers.ModelSerializer):
class Meta:
model = DeviceType
fields = ['id', 'manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'is_network_device']
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
'is_console_server', 'is_pdu', 'is_network_device']
class DeviceTypeNestedSerializer(DeviceTypeSerializer):
@@ -164,9 +167,9 @@ class DeviceTypeDetailSerializer(DeviceTypeSerializer):
interface_templates = InterfaceTemplateNestedSerializer(many=True, read_only=True)
class Meta(DeviceTypeSerializer.Meta):
fields = ['id', 'manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'is_network_device', 'console_port_templates', 'cs_port_templates', 'power_port_templates',
'power_outlet_templates', 'interface_templates']
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
'is_console_server', 'is_pdu', 'is_network_device', 'console_port_templates', 'cs_port_templates',
'power_port_templates', 'power_outlet_templates', 'interface_templates']
#
@@ -218,6 +221,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 +231,8 @@ 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', 'rack',
'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments']
def get_parent_device(self, obj):
try:

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,20 @@
from netaddr import EUI, mac_unix_expanded
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from .formfields import MACAddressFormField
class ASNField(models.BigIntegerField):
description = "32-bit ASN field"
default_validators = [
MinValueValidator(1),
MaxValueValidator(4294967295),
]
class mac_unix_expanded_uppercase(mac_unix_expanded):
word_fmt = '%.2X'

View File

@@ -6,6 +6,7 @@ from .models import (
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
Platform, PowerOutlet, PowerPort, Rack, RackGroup, 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,27 @@ 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)',
)
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)
)
@@ -102,7 +124,7 @@ class DeviceTypeFilter(django_filters.FilterSet):
class Meta:
model = DeviceType
fields = ['manufacturer_id', 'manufacturer', 'model', 'u_height', 'is_console_server', 'is_pdu',
fields = ['manufacturer_id', 'manufacturer', 'model', 'part_number', 'u_height', 'is_console_server', 'is_pdu',
'is_network_device']
@@ -122,6 +144,11 @@ class DeviceFilter(django_filters.FilterSet):
to_field_name='slug',
label='Site name (slug)',
)
rack_group_id = django_filters.ModelMultipleChoiceFilter(
name='rack__group',
queryset=RackGroup.objects.all(),
label='Rack group (ID)',
)
rack_id = django_filters.ModelMultipleChoiceFilter(
name='rack',
queryset=Rack.objects.all(),
@@ -138,6 +165,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,11 +233,11 @@ class DeviceFilter(django_filters.FilterSet):
'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(modules__serial__icontains=value) |
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, ConfirmationForm, CSVDataField, ExpandableNameField,
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
)
@@ -38,6 +40,15 @@ 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
#
# Sites
#
@@ -48,7 +59,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 +74,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
#
@@ -85,13 +113,9 @@ class RackGroupForm(forms.ModelForm, BootstrapMixin):
fields = ['site', 'name', 'slug']
class RackGroupBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=RackGroup.objects.all(), widget=forms.MultipleHiddenInput)
def rackgroup_site_choices():
site_choices = Site.objects.annotate(rack_count=Count('rack_groups'))
return [(s.slug, '{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
return [(s.slug, u'{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
class RackGroupFilterForm(forms.Form, BootstrapMixin):
@@ -111,7 +135,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', 'u_height', 'comments']
help_texts = {
'site': "The site at which the rack exists",
'name': "Organizational rack name",
@@ -139,10 +163,12 @@ 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.'})
class Meta:
model = Rack
fields = ['site', 'group_name', 'name', 'facility_id', 'u_height']
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'u_height']
def clean(self):
@@ -165,29 +191,33 @@ 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)
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
u_height = forms.IntegerField(required=False, label='Height (U)')
comments = CommentField()
class RackBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
def rack_site_choices():
site_choices = Site.objects.annotate(rack_count=Count('racks'))
return [(s.slug, '{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
return [(s.slug, u'{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
def rack_group_choices():
group_choices = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
return [(g.pk, '{} ({})'.format(g, g.rack_count)) for g in 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]
class RackFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices,
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, label='Rack Group',
widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
#
@@ -202,10 +232,6 @@ class ManufacturerForm(forms.ModelForm, BootstrapMixin):
fields = ['name', 'slug']
class ManufacturerBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Manufacturer.objects.all(), widget=forms.MultipleHiddenInput)
#
# Device types
#
@@ -215,8 +241,8 @@ class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = DeviceType
fields = ['manufacturer', 'model', 'slug', '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):
@@ -225,13 +251,9 @@ class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
u_height = forms.IntegerField(min_value=1, required=False)
class DeviceTypeBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
def devicetype_manufacturer_choices():
manufacturer_choices = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
return [(m.slug, '{} ({})'.format(m.name, m.devicetype_count)) for m in manufacturer_choices]
return [(m.slug, u'{} ({})'.format(m.name, m.devicetype_count)) for m in manufacturer_choices]
class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
@@ -303,10 +325,6 @@ class DeviceRoleForm(forms.ModelForm, BootstrapMixin):
fields = ['name', 'slug', 'color']
class DeviceRoleBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=DeviceRole.objects.all(), widget=forms.MultipleHiddenInput)
#
# Platforms
#
@@ -319,10 +337,6 @@ class PlatformForm(forms.ModelForm, BootstrapMixin):
fields = ['name', 'slug']
class PlatformBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Platform.objects.all(), widget=forms.MultipleHiddenInput)
#
# Devices
#
@@ -348,7 +362,7 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Device
fields = ['name', 'device_role', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
fields = ['name', 'device_role', 'tenant', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
'platform', 'primary_ip4', 'primary_ip6', 'comments']
help_texts = {
'device_role': "The function this device serves",
@@ -373,10 +387,10 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
for family in [4, 6]:
ip_choices = []
interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance)
ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
ip_choices += [(ip.id, u'{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
.select_related('nat_inside__interface')
ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
ip_choices += [(ip.id, u'{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices
else:
@@ -396,8 +410,8 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
self.fields['rack'].choices = []
# Rack position
pk = self.instance.pk if self.instance.pk else None
try:
pk = self.instance.pk if self.instance.pk else None
if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
position_choices = Rack.objects.get(pk=self.data['rack'])\
.get_rack_units(face=self.data.get('face'), exclude=pk)
@@ -425,32 +439,31 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
else:
self.fields['device_type'].choices = []
# Disable rack assignment if this is a child device installed in a parent device
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
self.fields['site'].disabled = True
self.fields['rack'].disabled = True
class DeviceFromCSVForm(forms.ModelForm):
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()
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid platform.'})
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
'invalid_choice': 'Invalid site name.',
})
rack_name = forms.CharField()
face = forms.CharField(required=False)
class Meta:
fields = []
model = Device
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'site', 'rack_name',
'position', 'face']
def clean(self):
manufacturer = self.cleaned_data.get('manufacturer')
model_name = self.cleaned_data.get('model_name')
site = self.cleaned_data.get('site')
rack_name = self.cleaned_data.get('rack_name')
# Validate device type
if manufacturer and model_name:
@@ -459,6 +472,25 @@ class DeviceFromCSVForm(forms.ModelForm):
except DeviceType.DoesNotExist:
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
class DeviceFromCSVForm(BaseDeviceFromCSVForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
'invalid_choice': 'Invalid site name.',
})
rack_name = forms.CharField()
face = forms.CharField(required=False)
class Meta(BaseDeviceFromCSVForm.Meta):
fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'site',
'rack_name', 'position', 'face']
def clean(self):
super(DeviceFromCSVForm, self).clean()
site = self.cleaned_data.get('site')
rack_name = self.cleaned_data.get('rack_name')
# Validate rack
if site and rack_name:
try:
@@ -468,60 +500,104 @@ class DeviceFromCSVForm(forms.ModelForm):
def clean_face(self):
face = self.cleaned_data['face']
if face:
if not face:
return None
try:
return {
'front': 0,
'rear': 1,
}[face.lower()]
except KeyError:
raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
parent = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Parent device not found.'})
device_bay_name = forms.CharField(required=False)
class Meta(BaseDeviceFromCSVForm.Meta):
fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'parent',
'device_bay_name']
def clean(self):
super(ChildDeviceFromCSVForm, self).clean()
parent = self.cleaned_data.get('parent')
device_bay_name = self.cleaned_data.get('device_bay_name')
# Validate device bay
if parent and device_bay_name:
try:
return {
'front': 0,
'rear': 1,
}[face.lower()]
except KeyError:
raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
return face
device_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
if device_bay.installed_device:
self.add_error('device_bay_name',
"Device bay ({} {}) is already occupied".format(parent, device_bay_name))
else:
self.instance.parent_bay = device_bay
except DeviceBay.DoesNotExist:
self.add_error('device_bay_name', "Parent device/bay ({} {}) not found".format(parent, device_bay_name))
class DeviceImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=DeviceFromCSVForm)
class ChildDeviceImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=ChildDeviceFromCSVForm)
class DeviceBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
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')
class DeviceBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
def device_site_choices():
site_choices = Site.objects.annotate(device_count=Count('racks__devices'))
return [(s.slug, '{} ({})'.format(s.name, s.device_count)) for s in site_choices]
return [(s.slug, u'{} ({})'.format(s.name, s.device_count)) for s in site_choices]
def device_rack_group_choices():
group_choices = RackGroup.objects.select_related('site').annotate(device_count=Count('racks__devices'))
return [(g.pk, u'{} ({})'.format(g, g.device_count)) for g in group_choices]
def device_role_choices():
role_choices = DeviceRole.objects.annotate(device_count=Count('devices'))
return [(r.slug, '{} ({})'.format(r.name, r.device_count)) for r in role_choices]
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, '{} ({})'.format(t, t.device_count)) for t in type_choices]
return [(t.pk, u'{} ({})'.format(t, t.device_count)) for t in type_choices]
def device_platform_choices():
platform_choices = Platform.objects.annotate(device_count=Count('devices'))
return [(p.slug, '{} ({})'.format(p.name, p.device_count)) for p in platform_choices]
return [(p.slug, u'{} ({})'.format(p.name, p.device_count)) for p in platform_choices]
class DeviceFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=device_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
rack_group_id = forms.MultipleChoiceField(required=False, choices=device_rack_group_choices, label='Rack Group',
widget=forms.SelectMultiple(attrs={'size': 8}))
role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
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)

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-13 19:24
from __future__ import unicode_literals
import dcim.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dcim', '0008_device_remove_primary_ip'),
]
operations = [
migrations.AlterField(
model_name='site',
name='asn',
field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'),
),
]

View File

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

View File

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

View File

@@ -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

@@ -1,16 +1,20 @@
from collections import OrderedDict
from django.core.exceptions import ValidationError
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.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
from .fields import MACAddressField
from .fields import ASNField, MACAddressField
RACK_FACE_FRONT = 0
RACK_FACE_REAR = 1
@@ -53,20 +57,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
@@ -136,6 +183,12 @@ def order_interfaces(queryset, sql_col, primary_ordering=tuple()):
}).order_by(*ordering)
class SiteManager(NaturalOrderByManager):
def get_queryset(self):
return self.natural_order_by('name')
class Site(CreatedUpdatedModel):
"""
A Site represents a geographic location within a network; typically a building or campus. The optional facility
@@ -143,12 +196,15 @@ 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 = models.PositiveIntegerField(blank=True, null=True, verbose_name='ASN')
asn = ASNField(blank=True, null=True, verbose_name='ASN')
physical_address = models.CharField(max_length=200, blank=True)
shipping_address = models.CharField(max_length=200, blank=True)
comments = models.TextField(blank=True)
objects = SiteManager()
class Meta:
ordering = ['name']
@@ -162,6 +218,7 @@ class Site(CreatedUpdatedModel):
return ','.join([
self.name,
self.slug,
self.tenant.name if self.tenant else '',
self.facility,
str(self.asn),
])
@@ -205,12 +262,18 @@ class RackGroup(models.Model):
]
def __unicode__(self):
return '{} - {}'.format(self.site.name, self.name)
return u'{} - {}'.format(self.site.name, self.name)
def get_absolute_url(self):
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
class RackManager(NaturalOrderByManager):
def get_queryset(self):
return self.natural_order_by('site__name', 'name')
class Rack(CreatedUpdatedModel):
"""
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
@@ -220,9 +283,12 @@ 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)
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT)
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)')
comments = models.TextField(blank=True)
objects = RackManager()
class Meta:
ordering = ['site', 'name']
unique_together = [
@@ -253,6 +319,7 @@ class Rack(CreatedUpdatedModel):
self.group.name if self.group else '',
self.name,
self.facility_id or '',
self.tenant.name if self.tenant else '',
str(self.u_height),
])
@@ -341,6 +408,15 @@ class Rack(CreatedUpdatedModel):
def get_0u_devices(self):
return self.devices.filter(position=0)
def get_utilization(self):
"""
Determine the utilization rate of the rack and return it as a percentage.
"""
if self.u_consumed is None:
self.u_consumed = 0
u_available = self.u_height - self.u_consumed
return int(float(self.u_height - u_available) / self.u_height * 100)
#
# Device Types
@@ -381,6 +457,7 @@ class DeviceType(models.Model):
manufacturer = models.ForeignKey('Manufacturer', related_name='device_types', on_delete=models.PROTECT)
model = models.CharField(max_length=50)
slug = models.SlugField()
part_number = models.CharField(max_length=50, blank=True, help_text="Discrete part number (optional)")
u_height = models.PositiveSmallIntegerField(verbose_name='Height (U)', default=1)
is_full_depth = models.BooleanField(default=True, verbose_name="Is full depth",
help_text="Device consumes both front and rear rack faces")
@@ -403,7 +480,7 @@ class DeviceType(models.Model):
]
def __unicode__(self):
return "{} {}".format(self.manufacturer, self.model)
return u'{} {}'.format(self.manufacturer, self.model)
def get_absolute_url(self):
return reverse('dcim:devicetype', args=[self.pk])
@@ -582,6 +659,12 @@ class Platform(models.Model):
return "{}?platform={}".format(reverse('dcim:device_list'), self.slug)
class DeviceManager(NaturalOrderByManager):
def get_queryset(self):
return self.natural_order_by('name')
class Device(CreatedUpdatedModel):
"""
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
@@ -596,6 +679,7 @@ 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')
@@ -611,6 +695,8 @@ class Device(CreatedUpdatedModel):
blank=True, null=True, verbose_name='Primary IPv6')
comments = models.TextField(blank=True)
objects = DeviceManager()
class Meta:
ordering = ['name']
unique_together = ['rack', 'position', 'face']
@@ -623,6 +709,10 @@ class Device(CreatedUpdatedModel):
def clean(self):
# Validate device type assignment
if not hasattr(self, 'device_type'):
raise ValidationError("Must specify device type.")
# Child devices cannot be assigned to a rack face/unit
if self.device_type.is_child_device and (self.face is not None or self.position):
raise ValidationError("Child device types cannot be assigned a rack face or position.")
@@ -632,10 +722,7 @@ class Device(CreatedUpdatedModel):
raise ValidationError("Must specify rack face with rack position.")
# Validate rack space
try:
rack_face = self.face if not self.device_type.is_full_depth else None
except DeviceType.DoesNotExist:
raise ValidationError("Must specify device type.")
rack_face = self.face if not self.device_type.is_full_depth else None
exclude_list = [self.pk] if self.pk else []
try:
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
@@ -679,10 +766,14 @@ class Device(CreatedUpdatedModel):
self.device_type.device_bay_templates.all()]
)
# Update Rack assignment for any child Devices
Device.objects.filter(parent_bay__device=self).update(rack=self.rack)
def to_csv(self):
return ','.join([
self.name or '',
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 '',
@@ -713,7 +804,9 @@ class Device(CreatedUpdatedModel):
@property
def primary_ip(self):
if self.primary_ip6:
if settings.PREFER_IPV4 and self.primary_ip4:
return self.primary_ip4
elif self.primary_ip6:
return self.primary_ip6
elif self.primary_ip4:
return self.primary_ip4
@@ -915,8 +1008,8 @@ class Interface(models.Model):
return connection.interface_a
except InterfaceConnection.DoesNotExist:
return None
except InterfaceConnection.MultipleObjectsReturned as e:
raise e("Multiple connections found for {0} interface {1}!".format(self.device, self))
except InterfaceConnection.MultipleObjectsReturned:
raise MultipleObjectsReturned("Multiple connections found for {} interface {}!".format(self.device, self))
class InterfaceConnection(models.Model):
@@ -950,14 +1043,15 @@ class DeviceBay(models.Model):
"""
device = models.ForeignKey('Device', related_name='device_bays', on_delete=models.CASCADE)
name = models.CharField(max_length=50, verbose_name='Name')
installed_device = models.OneToOneField('Device', related_name='parent_bay', blank=True, null=True)
installed_device = models.OneToOneField('Device', related_name='parent_bay', on_delete=models.SET_NULL, blank=True,
null=True)
class Meta:
ordering = ['device', 'name']
unique_together = ['device', 'name']
def __unicode__(self):
return '{} - {}'.format(self.device.name, self.name)
return u'{} - {}'.format(self.device.name, self.name)
def clean(self):

View File

@@ -16,27 +16,27 @@ DEVICE_LINK = """
</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 = """
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 %}
"""
@@ -48,14 +48,21 @@ STATUS_ICON = """
{% endif %}
"""
UTILIZATION_GRAPH = """
{% load helpers %}
{% utilization_graph record.get_utilization %}
"""
#
# Sites
#
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')
@@ -65,8 +72,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')
#
@@ -79,11 +86,12 @@ 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')
#
@@ -96,12 +104,29 @@ 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')
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
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')
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'u_height', 'devices', 'u_consumed',
'utilization')
class RackImportTable(BaseTable):
name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
facility_id = tables.Column(verbose_name='Facility ID')
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', 'tenant', 'u_height')
#
@@ -113,11 +138,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')
#
@@ -126,93 +152,77 @@ class ManufacturerTable(BaseTable):
class DeviceTypeTable(BaseTable):
pk = ToggleColumn()
manufacturer = tables.Column(verbose_name='Manufacturer')
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
part_number = tables.Column(verbose_name='Part Number')
class Meta(BaseTable.Meta):
model = DeviceType
fields = ('pk', 'model', 'manufacturer', 'u_height')
fields = ('pk', 'model', 'manufacturer', 'part_number', 'u_height')
#
# Device type components
#
class ConsolePortTemplateTable(tables.Table):
class ConsolePortTemplateTable(BaseTable):
pk = ToggleColumn()
class Meta:
class Meta(BaseTable.Meta):
model = ConsolePortTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
attrs = {
'class': 'table table-hover',
}
class ConsoleServerPortTemplateTable(tables.Table):
class ConsoleServerPortTemplateTable(BaseTable):
pk = ToggleColumn()
class Meta:
class Meta(BaseTable.Meta):
model = ConsoleServerPortTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
attrs = {
'class': 'table table-hover',
}
class PowerPortTemplateTable(tables.Table):
class PowerPortTemplateTable(BaseTable):
pk = ToggleColumn()
class Meta:
class Meta(BaseTable.Meta):
model = PowerPortTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
attrs = {
'class': 'table table-hover',
}
class PowerOutletTemplateTable(tables.Table):
class PowerOutletTemplateTable(BaseTable):
pk = ToggleColumn()
class Meta:
class Meta(BaseTable.Meta):
model = PowerOutletTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
attrs = {
'class': 'table table-hover',
}
class InterfaceTemplateTable(tables.Table):
class InterfaceTemplateTable(BaseTable):
pk = ToggleColumn()
class Meta:
class Meta(BaseTable.Meta):
model = InterfaceTemplate
fields = ('pk', 'name')
fields = ('pk', 'name', 'form_factor')
empty_text = "None"
show_header = False
attrs = {
'class': 'table table-hover panel-body',
}
class DeviceBayTemplateTable(tables.Table):
class DeviceBayTemplateTable(BaseTable):
pk = ToggleColumn()
class Meta:
class Meta(BaseTable.Meta):
model = DeviceBayTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
attrs = {
'class': 'table table-hover panel-body',
}
#
@@ -225,11 +235,12 @@ class DeviceRoleTable(BaseTable):
device_count = tables.Column(verbose_name='Devices')
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', 'slug', 'color', 'actions')
#
@@ -241,11 +252,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')
#
@@ -256,6 +267,7 @@ 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')
@@ -265,11 +277,12 @@ class DeviceTable(BaseTable):
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')
@@ -278,7 +291,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,7 @@ class SiteTest(APITestCase):
'display_name',
'site',
'group',
'tenant',
'u_height',
'comments'
]
@@ -115,6 +117,7 @@ class RackTest(APITestCase):
'display_name',
'site',
'group',
'tenant',
'u_height',
'comments'
]
@@ -126,6 +129,7 @@ class RackTest(APITestCase):
'display_name',
'site',
'group',
'tenant',
'u_height',
'comments',
'front_units',
@@ -204,6 +208,7 @@ class DeviceTypeTest(APITestCase):
'manufacturer',
'model',
'slug',
'part_number',
'u_height',
'is_full_depth',
'is_console_server',
@@ -310,6 +315,7 @@ class DeviceTest(APITestCase):
'display_name',
'device_type',
'device_role',
'tenant',
'platform',
'serial',
'rack',
@@ -387,6 +393,7 @@ class DeviceTest(APITestCase):
'rack_name',
'serial',
'status',
'tenant',
]
response = self.client.get(endpoint)

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

View File

@@ -1,12 +1,13 @@
import re
from natsort import natsorted
from operator import attrgetter
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db.models import Count, ProtectedError
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput
from django.db.models import Count, Sum
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.http import urlencode
@@ -14,8 +15,7 @@ from django.views.generic import View
from ipam.models import Prefix, IPAddress, VLAN
from circuits.models import Circuit
from extras.models import TopologyMap
from utilities.error_handlers import handle_protectederror
from 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,
@@ -61,9 +61,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 +81,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,6 +114,24 @@ 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
#
@@ -133,7 +155,6 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackgroup'
cls = RackGroup
form = forms.RackGroupBulkDeleteForm
default_redirect_url = 'dcim:rackgroup_list'
@@ -142,7 +163,8 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class RackListView(ObjectListView):
queryset = Rack.objects.select_related('site').annotate(device_count=Count('devices', distinct=True))
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'))
filter = filters.RackFilter
filter_form = forms.RackFilterForm
table = tables.RackTable
@@ -154,7 +176,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()
@@ -186,7 +208,7 @@ class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_rack'
form = forms.RackImportForm
table = tables.RackTable
table = tables.RackImportTable
template_name = 'dcim/rack_import.html'
obj_list_url = 'dcim:rack_list'
@@ -201,7 +223,11 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['site', 'group', 'u_height', 'comments']:
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', 'tenant', 'u_height', 'comments']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
@@ -211,7 +237,6 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rack'
cls = Rack
form = forms.RackBulkDeleteForm
default_redirect_url = 'dcim:rack_list'
@@ -237,7 +262,6 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_manufacturer'
cls = Manufacturer
form = forms.ManufacturerBulkDeleteForm
default_redirect_url = 'dcim:manufacturer_list'
@@ -259,18 +283,31 @@ def devicetype(request, pk):
devicetype = get_object_or_404(DeviceType, pk=pk)
# Component tables
consoleport_table = tables.ConsolePortTemplateTable(ConsolePortTemplate.objects.filter(device_type=devicetype))
consoleserverport_table = tables.ConsoleServerPortTemplateTable(ConsoleServerPortTemplate.objects
.filter(device_type=devicetype))
powerport_table = tables.PowerPortTemplateTable(PowerPortTemplate.objects.filter(device_type=devicetype))
poweroutlet_table = tables.PowerOutletTemplateTable(PowerOutletTemplate.objects.filter(device_type=devicetype))
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype))
devicebay_table = tables.DeviceBayTemplateTable(DeviceBayTemplate.objects.filter(device_type=devicetype))
consoleport_table = tables.ConsolePortTemplateTable(
natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
consoleserverport_table = tables.ConsoleServerPortTemplateTable(
natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
powerport_table = tables.PowerPortTemplateTable(
natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
poweroutlet_table = tables.PowerOutletTemplateTable(
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
mgmt_interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype,
mgmt_only=True))
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype,
mgmt_only=False))
devicebay_table = tables.DeviceBayTemplateTable(
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
if request.user.has_perm('dcim.change_devicetype'):
consoleport_table.base_columns['pk'].visible = True
consoleserverport_table.base_columns['pk'].visible = True
powerport_table.base_columns['pk'].visible = True
poweroutlet_table.base_columns['pk'].visible = True
mgmt_interface_table.base_columns['pk'].visible = True
interface_table.base_columns['pk'].visible = True
devicebay_table.base_columns['pk'].visible = True
@@ -280,6 +317,7 @@ def devicetype(request, pk):
'consoleserverport_table': consoleserverport_table,
'powerport_table': powerport_table,
'poweroutlet_table': poweroutlet_table,
'mgmt_interface_table': mgmt_interface_table,
'interface_table': interface_table,
'devicebay_table': devicebay_table,
})
@@ -318,7 +356,6 @@ class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicetype'
cls = DeviceType
form = forms.DeviceTypeBulkDeleteForm
default_redirect_url = 'dcim:devicetype_list'
@@ -337,7 +374,7 @@ class ComponentTemplateCreateView(View):
return render(request, 'dcim/component_template_add.html', {
'devicetype': devicetype,
'component_type': self.model._meta.verbose_name,
'form': self.form(),
'form': self.form(initial=request.GET),
'cancel_url': reverse('dcim:devicetype', kwargs={'pk': devicetype.pk}),
})
@@ -380,68 +417,65 @@ class ConsolePortTemplateAddView(ComponentTemplateCreateView):
form = forms.ConsolePortTemplateForm
class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleporttemplate'
cls = ConsolePortTemplate
parent_cls = DeviceType
class ConsoleServerPortTemplateAddView(ComponentTemplateCreateView):
model = ConsoleServerPortTemplate
form = forms.ConsoleServerPortTemplateForm
class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverporttemplate'
cls = ConsoleServerPortTemplate
parent_cls = DeviceType
class PowerPortTemplateAddView(ComponentTemplateCreateView):
model = PowerPortTemplate
form = forms.PowerPortTemplateForm
class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerporttemplate'
cls = PowerPortTemplate
parent_cls = DeviceType
class PowerOutletTemplateAddView(ComponentTemplateCreateView):
model = PowerOutletTemplate
form = forms.PowerOutletTemplateForm
class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlettemplate'
cls = PowerOutletTemplate
parent_cls = DeviceType
class InterfaceTemplateAddView(ComponentTemplateCreateView):
model = InterfaceTemplate
form = forms.InterfaceTemplateForm
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_interfacetemplate'
cls = InterfaceTemplate
parent_cls = DeviceType
class DeviceBayTemplateAddView(ComponentTemplateCreateView):
model = DeviceBayTemplate
form = forms.DeviceBayTemplateForm
def component_template_delete(request, pk, model):
devicetype = get_object_or_404(DeviceType, pk=pk)
class ComponentTemplateBulkDeleteForm(ConfirmationForm):
pk = ModelMultipleChoiceField(queryset=model.objects.all(), widget=MultipleHiddenInput)
if '_confirm' in request.POST:
form = ComponentTemplateBulkDeleteForm(request.POST)
if form.is_valid():
# Delete component templates
objects_to_delete = model.objects.filter(pk__in=[v.id for v in form.cleaned_data['pk']])
try:
deleted_count = objects_to_delete.count()
objects_to_delete.delete()
except ProtectedError, e:
handle_protectederror(list(objects_to_delete), request, e)
return redirect('dcim:devicetype', {'pk': devicetype.pk})
messages.success(request, "Deleted {} {}".format(deleted_count, model._meta.verbose_name_plural))
return redirect('dcim:devicetype', pk=devicetype.pk)
else:
form = ComponentTemplateBulkDeleteForm(initial={'pk': request.POST.getlist('pk')})
selected_objects = model.objects.filter(pk__in=request.POST.getlist('pk'))
if not selected_objects:
messages.warning(request, "No {} were selected for deletion.".format(model._meta.verbose_name_plural))
return redirect('dcim:devicetype', pk=devicetype.pk)
return render(request, 'dcim/component_template_delete.html', {
'devicetype': devicetype,
'form': form,
'selected_objects': selected_objects,
'cancel_url': reverse('dcim:devicetype', kwargs={'pk': devicetype.pk}),
})
class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebaytemplate'
cls = DeviceBayTemplate
parent_cls = DeviceType
#
@@ -466,7 +500,6 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicerole'
cls = DeviceRole
form = forms.DeviceRoleBulkDeleteForm
default_redirect_url = 'dcim:devicerole_list'
@@ -492,7 +525,6 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_platform'
cls = Platform
form = forms.PlatformBulkDeleteForm
default_redirect_url = 'dcim:platform_list'
@@ -513,15 +545,26 @@ class DeviceListView(ObjectListView):
def device(request, pk):
device = get_object_or_404(Device, pk=pk)
console_ports = ConsolePort.objects.filter(device=device).select_related('cs_port__device')
cs_ports = ConsoleServerPort.objects.filter(device=device).select_related('connected_console')
power_ports = PowerPort.objects.filter(device=device).select_related('power_outlet__device')
power_outlets = PowerOutlet.objects.filter(device=device).select_related('connected_port')
console_ports = natsorted(
ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
)
cs_ports = natsorted(
ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name')
)
power_ports = natsorted(
PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name')
)
power_outlets = natsorted(
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
)
interfaces = Interface.objects.filter(device=device, mgmt_only=False)\
.select_related('connected_as_a', 'connected_as_b', 'circuit')
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\
.select_related('connected_as_a', 'connected_as_b', 'circuit')
device_bays = DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer')
device_bays = natsorted(
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
key=attrgetter('name')
)
# Gather any secrets which belong to this device
secrets = device.secrets.all()
@@ -544,6 +587,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,
@@ -556,6 +602,7 @@ def device(request, pk):
'ip_addresses': ip_addresses,
'secrets': secrets,
'related_devices': related_devices,
'show_graphs': show_graphs,
})
@@ -582,6 +629,23 @@ class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
obj_list_url = 'dcim:device_list'
class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_device'
form = forms.ChildDeviceImportForm
table = tables.DeviceImportTable
template_name = 'dcim/device_import_child.html'
obj_list_url = 'dcim:device_list'
def save_obj(self, obj):
# Inherent rack from parent device
obj.rack = obj.parent_bay.device.rack
obj.save()
# Save the reverse relation
device_bay = obj.parent_bay
device_bay.installed_device = obj
device_bay.save()
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_device'
cls = Device
@@ -592,14 +656,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]
@@ -609,7 +674,6 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_device'
cls = Device
form = forms.DeviceBulkDeleteForm
default_redirect_url = 'dcim:device_list'
@@ -781,6 +845,12 @@ def consoleport_delete(request, pk):
})
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleport'
cls = ConsolePort
parent_cls = Device
class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.change_consoleport'
form = forms.ConsoleConnectionImportForm
@@ -936,6 +1006,12 @@ def consoleserverport_delete(request, pk):
})
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverport'
cls = ConsoleServerPort
parent_cls = Device
#
# Power ports
#
@@ -1081,6 +1157,12 @@ def powerport_delete(request, pk):
})
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerport'
cls = PowerPort
parent_cls = Device
class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.change_powerport'
form = forms.PowerConnectionImportForm
@@ -1234,6 +1316,12 @@ def poweroutlet_delete(request, pk):
})
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlet'
cls = PowerOutlet
parent_cls = Device
#
# Interfaces
#
@@ -1328,7 +1416,7 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.add_interface'
cls = Device
form = forms.InterfaceBulkCreateForm
template_name = 'dcim/interface_bulk_add.html'
template_name = 'dcim/interface_add_multi.html'
default_redirect_url = 'dcim:device_list'
def update_objects(self, pk_list, form):
@@ -1357,6 +1445,12 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
len(selected_devices)))
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_interface'
cls = Interface
parent_cls = Device
#
# Device bays
#
@@ -1494,6 +1588,12 @@ def devicebay_depopulate(request, pk):
})
class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebay'
cls = DeviceBay
parent_cls = Device
#
# Interface connections
#

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

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

View File

@@ -1,13 +1,18 @@
from django.contrib import admin
from .models import (
Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VRF,
Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF,
)
@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']
@@ -57,12 +62,20 @@ class IPAddressAdmin(admin.ModelAdmin):
return qs.select_related('vrf', 'nat_inside')
@admin.register(VLANGroup)
class VLANGroupAdmin(admin.ModelAdmin):
list_display = ['name', 'site', 'slug']
prepopulated_fields = {
'slug': ['name'],
}
@admin.register(VLAN)
class VLANAdmin(admin.ModelAdmin):
list_display = ['site', 'vid', 'name', 'status', 'role']
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

@@ -1,7 +1,8 @@
from rest_framework import serializers
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN
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
#
class VRFSerializer(serializers.ModelSerializer):
tenant = TenantNestedSerializer()
class Meta:
model = VRF
fields = ['id', 'name', 'rd', '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
#
@@ -73,17 +84,37 @@ class AggregateNestedSerializer(AggregateSerializer):
fields = ['id', 'family', 'prefix']
#
# VLAN groups
#
class VLANGroupSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
class Meta:
model = VLANGroup
fields = ['id', 'name', 'slug', 'site']
class VLANGroupNestedSerializer(VLANGroupSerializer):
class Meta(VLANGroupSerializer.Meta):
fields = ['id', 'name', 'slug']
#
# VLANs
#
class VLANSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
group = VLANGroupNestedSerializer()
tenant = TenantNestedSerializer()
role = RoleNestedSerializer()
class Meta:
model = VLAN
fields = ['id', 'site', 'vid', 'name', 'status', 'role', 'display_name']
fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name']
class VLANNestedSerializer(VLANSerializer):
@@ -98,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):
@@ -118,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

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

View File

@@ -1,28 +1,36 @@
from rest_framework import generics
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN
from ipam.filters import AggregateFilter, PrefixFilter, IPAddressFilter, VLANFilter, VRFFilter
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
from ipam import filters
from . import serializers
#
# VRFs
#
class VRFListView(generics.ListAPIView):
"""
List all VRFs
"""
queryset = VRF.objects.all()
queryset = VRF.objects.select_related('tenant')
serializer_class = serializers.VRFSerializer
filter_class = VRFFilter
filter_class = filters.VRFFilter
class VRFDetailView(generics.RetrieveAPIView):
"""
Retrieve a single VRF
"""
queryset = VRF.objects.all()
queryset = VRF.objects.select_related('tenant')
serializer_class = serializers.VRFSerializer
#
# Roles
#
class RoleListView(generics.ListAPIView):
"""
List all roles
@@ -39,6 +47,10 @@ class RoleDetailView(generics.RetrieveAPIView):
serializer_class = serializers.RoleSerializer
#
# RIRs
#
class RIRListView(generics.ListAPIView):
"""
List all RIRs
@@ -55,13 +67,17 @@ class RIRDetailView(generics.RetrieveAPIView):
serializer_class = serializers.RIRSerializer
#
# Aggregates
#
class AggregateListView(generics.ListAPIView):
"""
List aggregates (filterable)
"""
queryset = Aggregate.objects.select_related('rir')
serializer_class = serializers.AggregateSerializer
filter_class = AggregateFilter
filter_class = filters.AggregateFilter
class AggregateDetailView(generics.RetrieveAPIView):
@@ -72,54 +88,87 @@ class AggregateDetailView(generics.RetrieveAPIView):
serializer_class = serializers.AggregateSerializer
#
# Prefixes
#
class PrefixListView(generics.ListAPIView):
"""
List prefixes (filterable)
"""
queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role')
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
serializer_class = serializers.PrefixSerializer
filter_class = PrefixFilter
filter_class = filters.PrefixFilter
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
#
# IP addresses
#
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 = IPAddressFilter
filter_class = filters.IPAddressFilter
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
#
# VLAN groups
#
class VLANGroupListView(generics.ListAPIView):
"""
List all VLAN groups
"""
queryset = VLANGroup.objects.select_related('site')
serializer_class = serializers.VLANGroupSerializer
filter_class = filters.VLANGroupFilter
class VLANGroupDetailView(generics.RetrieveAPIView):
"""
Retrieve a single VLAN group
"""
queryset = VLANGroup.objects.select_related('site')
serializer_class = serializers.VLANGroupSerializer
#
# VLANs
#
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 = VLANFilter
filter_class = filters.VLANFilter
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 dcim.models import Site, Device, Interface
from django.db.models import Q
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, Role
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,8 +263,26 @@ 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)
)
class VLANFilter(django_filters.FilterSet):
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(
name='site',
queryset=Site.objects.all(),
@@ -188,6 +294,39 @@ class VLANFilter(django_filters.FilterSet):
to_field_name='slug',
label='Site (slug)',
)
class Meta:
model = VLANGroup
fields = ['site_id', 'site']
class VLANFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
group_id = django_filters.ModelMultipleChoiceFilter(
name='group',
queryset=VLANGroup.objects.all(),
label='Group (ID)',
)
group = django_filters.ModelMultipleChoiceFilter(
name='group',
queryset=VLANGroup.objects.all(),
to_field_name='slug',
label='Group',
)
name = django_filters.CharFilter(
name='name',
lookup_type='icontains',
@@ -197,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(),
@@ -212,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,12 +4,12 @@ from django import forms
from django.db.models import Count
from dcim.models import Site, Device, Interface
from utilities.forms import (
BootstrapMixin, ConfirmationForm, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField,
)
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 (
Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLAN_STATUS_CHOICES, VRF,
Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
)
@@ -17,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
#
@@ -25,7 +37,7 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = VRF
fields = ['name', 'rd', 'description']
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
labels = {
'rd': "RD",
}
@@ -35,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', 'description']
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
class VRFImportForm(BulkImportForm, BootstrapMixin):
@@ -47,11 +61,18 @@ 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)
class VRFBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
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}))
#
@@ -66,10 +87,6 @@ class RIRForm(forms.ModelForm, BootstrapMixin):
fields = ['name', 'slug']
class RIRBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=RIR.objects.all(), widget=forms.MultipleHiddenInput)
#
# Aggregates
#
@@ -103,16 +120,12 @@ class AggregateBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
date_added = forms.DateField(required=False)
description = forms.CharField(max_length=50, required=False)
class AggregateBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
description = forms.CharField(max_length=100, required=False)
def aggregate_rir_choices():
rir_choices = RIR.objects.annotate(aggregate_count=Count('aggregates'))
return [(r.slug, '{} ({})'.format(r.name, r.aggregate_count)) for r in rir_choices]
return [(r.slug, u'{} ({})'.format(r.name, r.aggregate_count)) for r in rir_choices]
class AggregateFilterForm(forms.Form, BootstrapMixin):
@@ -132,10 +145,6 @@ class RoleForm(forms.ModelForm, BootstrapMixin):
fields = ['name', 'slug']
class RoleBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Role.objects.all(), widget=forms.MultipleHiddenInput)
#
# Prefixes
#
@@ -149,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)",
@@ -190,15 +199,48 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
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)
vlan_vid = forms.IntegerField(required=False)
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in PREFIX_STATUS_CHOICES])
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid role.'})
class Meta:
model = Prefix
fields = ['prefix', 'vrf', 'site', 'status_name', 'role', 'description']
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role',
'description']
def clean(self):
super(PrefixFromCSVForm, self).clean()
site = self.cleaned_data.get('site')
vlan_group_name = self.cleaned_data.get('vlan_group_name')
vlan_vid = self.cleaned_data.get('vlan_vid')
# Validate VLAN
vlan_group = None
if vlan_group_name:
try:
vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name)
except VLANGroup.DoesNotExist:
self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name))
if vlan_vid and vlan_group:
try:
self.instance.vlan = VLAN.objects.get(group=vlan_group, vid=vlan_vid)
except VLAN.DoesNotExist:
self.add_error('vlan_vid', "Invalid VLAN ID ({} - {}).".format(vlan_group, vlan_vid))
elif vlan_vid and site:
try:
self.instance.vlan = VLAN.objects.get(site=site, vid=vlan_vid)
except VLAN.MultipleObjectsReturned:
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
elif vlan_vid:
self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.")
def save(self, *args, **kwargs):
m = super(PrefixFromCSVForm, self).save(commit=False)
@@ -216,49 +258,54 @@ 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=50, required=False)
class PrefixBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
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():
site_choices = Site.objects.annotate(prefix_count=Count('prefixes'))
return [(s.slug, '{} ({})'.format(s.name, s.prefix_count)) for s in site_choices]
return [(s.slug, u'{} ({})'.format(s.name, s.prefix_count)) for s in site_choices]
def prefix_status_choices():
status_counts = {}
for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
status_counts[status['status']] = status['count']
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
def prefix_role_choices():
role_choices = Role.objects.annotate(prefix_count=Count('prefixes'))
return [(r.slug, '{} ({})'.format(r.name, r.prefix_count)) for r in role_choices]
return [(r.slug, u'{} ({})'.format(r.name, r.prefix_count)) for r in 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')
@@ -281,7 +328,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)",
@@ -330,6 +377,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)
@@ -337,7 +386,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):
@@ -368,9 +417,9 @@ class IPAddressFromCSVForm(forms.ModelForm):
name=self.cleaned_data['interface_name'])
# Set as primary for device
if self.cleaned_data['is_primary']:
if self.instance.family == 4:
if self.instance.address.version == 4:
self.instance.primary_ip4_for = self.cleaned_data['device']
elif self.instance.family == 6:
elif self.instance.address.version == 6:
self.instance.primary_ip6_for = self.cleaned_data['device']
return super(IPAddressFromCSVForm, self).save(commit=commit)
@@ -382,14 +431,9 @@ 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')
description = forms.CharField(max_length=50, required=False)
class IPAddressBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
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)
def ipaddress_family_choices():
@@ -397,14 +441,41 @@ 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}))
#
# VLAN groups
#
class VLANGroupForm(forms.ModelForm, BootstrapMixin):
slug = SlugField()
class Meta:
model = VLANGroup
fields = ['site', 'name', 'slug']
def vlangroup_site_choices():
site_choices = Site.objects.annotate(vlangroup_count=Count('vlan_groups'))
return [(s.slug, u'{} ({})'.format(s.name, s.vlangroup_count)) for s in site_choices]
class VLANGroupFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=vlangroup_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
#
@@ -412,29 +483,52 @@ class IPAddressFilterForm(forms.Form, BootstrapMixin):
#
class VLANForm(forms.ModelForm, BootstrapMixin):
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, label='Group', widget=APISelect(
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
))
class Meta:
model = VLAN
fields = ['site', 'vid', 'name', '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)",
'vid': "Configured VLAN ID",
'name': "Configured VLAN name",
'status': "Operational status of this VLAN",
'role': "The primary function of this VLAN",
}
widgets = {
'site': forms.Select(attrs={'filter-for': 'group'}),
}
def __init__(self, *args, **kwargs):
super(VLANForm, self).__init__(*args, **kwargs)
# Limit VLAN group choices
if self.is_bound and self.data.get('site'):
self.fields['group'].queryset = VLANGroup.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
else:
self.fields['group'].choices = []
class VLANFromCSVForm(forms.ModelForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Device not found.'})
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'VLAN group not found.'})
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', 'vid', 'name', 'status_name', 'role']
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
def save(self, *args, **kwargs):
m = super(VLANFromCSVForm, self).save(commit=False)
@@ -452,34 +546,47 @@ class VLANImportForm(BulkImportForm, BootstrapMixin):
class VLANBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
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)
class VLANBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
description = forms.CharField(max_length=100, required=False)
def vlan_site_choices():
site_choices = Site.objects.annotate(vlan_count=Count('vlans'))
return [(s.slug, '{} ({})'.format(s.name, s.vlan_count)) for s in site_choices]
return [(s.slug, u'{} ({})'.format(s.name, s.vlan_count)) for s in site_choices]
def vlan_group_choices():
group_choices = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
return [(g.pk, u'{} ({})'.format(g, g.vlan_count)) for g in group_choices]
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'):
status_counts[status['status']] = status['count']
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
def vlan_role_choices():
role_choices = Role.objects.annotate(vlan_count=Count('vlans'))
return [(r.slug, '{} ({})'.format(r.name, r.vlan_count)) for r in role_choices]
return [(r.slug, u'{} ({})'.format(r.name, r.vlan_count)) for r in role_choices]
class VLANFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=vlan_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group',
widget=forms.SelectMultiple(attrs={'size': 8}))
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,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-14 19:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='vrf',
name='enforce_unique',
field=models.BooleanField(default=True, help_text=b'Prevent duplicate prefixes/IP addresses within this VRF', verbose_name=b'Enforce unique space'),
),
]

View File

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

View File

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

View File

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

View File

@@ -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

@@ -1,11 +1,13 @@
from netaddr import IPNetwork, cidr_merge
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from dcim.models import Interface
from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel
from .fields import IPNetworkField, IPAddressField
@@ -45,6 +47,9 @@ 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)
class Meta:
@@ -62,6 +67,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,
])
@@ -123,6 +130,8 @@ class Aggregate(CreatedUpdatedModel):
# Ensure that the aggregate being added does not cover an existing aggregate
covered_aggregates = Aggregate.objects.filter(prefix__net_contained=str(self.prefix))
if self.pk:
covered_aggregates = covered_aggregates.exclude(pk=self.pk)
if covered_aggregates:
raise ValidationError("{} is overlaps with an existing aggregate ({})"
.format(self.prefix, covered_aggregates[0]))
@@ -224,6 +233,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)
@@ -242,6 +252,15 @@ class Prefix(CreatedUpdatedModel):
def get_absolute_url(self):
return reverse('ipam:prefix', args=[self.pk])
def clean(self):
# Disallow host masks
if self.prefix.version == 4 and self.prefix.prefixlen == 32:
raise ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 addresses "
"instead.")
elif self.prefix.version == 6 and self.prefix.prefixlen == 128:
raise ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 addresses "
"instead.")
def save(self, *args, **kwargs):
if self.prefix:
# Clear host bits from prefix
@@ -277,7 +296,7 @@ class Prefix(CreatedUpdatedModel):
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.
@@ -290,6 +309,7 @@ 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,
@@ -307,6 +327,21 @@ class IPAddress(CreatedUpdatedModel):
def get_absolute_url(self):
return reverse('ipam:ipaddress', args=[self.pk])
def clean(self):
# Enforce unique IP space if applicable
if self.vrf and self.vrf.enforce_unique:
duplicate_ips = IPAddress.objects.filter(vrf=self.vrf, address__net_host=str(self.address.ip))\
.exclude(pk=self.pk)
if duplicate_ips:
raise ValidationError("Duplicate IP address found in VRF {}: {}".format(self.vrf,
duplicate_ips.first()))
elif not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE:
duplicate_ips = IPAddress.objects.filter(vrf=None, address__net_host=str(self.address.ip))\
.exclude(pk=self.pk)
if duplicate_ips:
raise ValidationError("Duplicate IP address found in global table: {}".format(duplicate_ips.first()))
def save(self, *args, **kwargs):
if self.address:
# Infer address family from IPAddress object
@@ -338,23 +373,57 @@ class IPAddress(CreatedUpdatedModel):
return None
class VLANGroup(models.Model):
"""
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
"""
name = models.CharField(max_length=50)
slug = models.SlugField()
site = models.ForeignKey('dcim.Site', related_name='vlan_groups')
class Meta:
ordering = ['site', 'name']
unique_together = [
['site', 'name'],
['site', 'slug'],
]
verbose_name = 'VLAN group'
verbose_name_plural = 'VLAN groups'
def __unicode__(self):
return u'{} - {}'.format(self.site.name, self.name)
def get_absolute_url(self):
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
class VLAN(CreatedUpdatedModel):
"""
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
to a Site, however VLAN IDs need not be unique within a Site. Like Prefixes, each VLAN is assigned an operational
status and optionally a user-defined Role. A VLAN can have zero or more Prefixes assigned to it.
to a Site, however VLAN IDs need not be unique within a Site. A VLAN may optionally be assigned to a VLANGroup,
within which all VLAN IDs and names but be unique.
Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
or more Prefixes assigned to it.
"""
site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT)
group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
MinValueValidator(1),
MaxValueValidator(4094)
])
name = models.CharField(max_length=30)
name = models.CharField(max_length=64)
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', 'vid']
ordering = ['site', 'group', 'vid']
unique_together = [
['group', 'vid'],
['group', 'name'],
]
verbose_name = 'VLAN'
verbose_name_plural = 'VLANs'
@@ -364,18 +433,27 @@ class VLAN(CreatedUpdatedModel):
def get_absolute_url(self):
return reverse('ipam:vlan', args=[self.pk])
def clean(self):
# Validate VLAN group
if self.group and self.group.site != self.site:
raise ValidationError("VLAN group must belong to the assigned site ({}).".format(self.site))
def to_csv(self):
return ','.join([
self.site.name,
self.group.name if self.group else '',
str(self.vid),
self.name,
self.tenant.name if self.tenant else '',
self.get_status_display(),
self.role.name if self.role else '',
self.description,
])
@property
def display_name(self):
return u"{} ({})".format(self.vid, self.name)
return u'{} ({})'.format(self.vid, self.name)
def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status]

View File

@@ -3,27 +3,24 @@ from django_tables2.utils import Accessor
from utilities.tables import BaseTable, ToggleColumn
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VRF
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 = """
{% with record.get_utilization as percentage %}
<div class="progress text-center">
{% if percentage < 15 %}<span style="font-size: 12px;">{{ percentage }}%</span>{% endif %}
<div class="progress-bar progress-bar-{% if percentage >= 90 %}danger{% elif percentage >= 75 %}warning{% else %}success{% endif %}"
role="progressbar" aria-valuenow="{{ percentage }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage }}%">
{% if percentage >= 15 %}{{ percentage }}%{% endif %}
</div>
</div>
{% endwith %}
{% load helpers %}
{% 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 = """
@@ -42,6 +39,16 @@ 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 }}" 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 %}
"""
STATUS_LABEL = """
{% if record.pk %}
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
@@ -50,6 +57,22 @@ STATUS_LABEL = """
{% endif %}
"""
VLANGROUP_ACTIONS = """
{% if perms.ipam.change_vlangroup %}
<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 %}
"""
#
# VRFs
@@ -59,11 +82,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')
#
@@ -75,11 +99,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')
#
@@ -110,11 +134,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')
#
@@ -125,25 +149,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.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', 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
#
@@ -152,8 +182,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.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', 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')
@@ -161,7 +192,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):
@@ -177,6 +211,24 @@ class IPAddressBriefTable(BaseTable):
fields = ('address', 'device', 'interface', 'nat_inside')
#
# VLAN groups
#
class VLANGroupTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
vlan_count = tables.Column(verbose_name='VLANs')
slug = tables.Column(verbose_name='Slug')
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', 'actions')
#
# VLANs
#
@@ -185,10 +237,12 @@ class VLANTable(BaseTable):
pk = ToggleColumn()
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
name = tables.Column(verbose_name='Name')
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', 'name', 'status', 'role')
fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role')

View File

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

View File

@@ -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
@@ -12,7 +12,7 @@ from utilities.views import (
)
from . import filters, forms, tables
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VRF
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
def add_available_prefixes(parent, prefix_list):
@@ -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]
@@ -95,7 +152,6 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vrf'
cls = VRF
form = forms.VRFBulkDeleteForm
default_redirect_url = 'ipam:vrf_list'
@@ -121,7 +177,6 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView):
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_rir'
cls = RIR
form = forms.RIRBulkDeleteForm
default_redirect_url = 'ipam:rir_list'
@@ -147,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,
@@ -217,7 +272,6 @@ class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_aggregate'
cls = Aggregate
form = forms.AggregateBulkDeleteForm
default_redirect_url = 'ipam:aggregate_list'
@@ -243,7 +297,6 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_role'
cls = Role
form = forms.RoleBulkDeleteForm
default_redirect_url = 'ipam:role_list'
@@ -252,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', 'role')
filter = filters.PrefixFilter
filter_form = forms.PrefixFilterForm
table = tables.PrefixTable
@@ -275,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)
@@ -288,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)
@@ -312,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'
@@ -340,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]
@@ -354,7 +416,6 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_prefix'
cls = Prefix
form = forms.PrefixBulkDeleteForm
default_redirect_url = 'ipam:prefix_list'
@@ -363,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
@@ -383,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', 'interface__device')
filter = filters.IPAddressFilter
filter_form = forms.IPAddressFilterForm
table = tables.IPAddressTable
@@ -465,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]
@@ -479,10 +542,35 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_ipaddress'
cls = IPAddress
form = forms.IPAddressBulkDeleteForm
default_redirect_url = 'ipam:ipaddress_list'
#
# VLAN groups
#
class VLANGroupListView(ObjectListView):
queryset = VLANGroup.objects.annotate(vlan_count=Count('vlans'))
filter = filters.VLANGroupFilter
filter_form = forms.VLANGroupFilterForm
table = tables.VLANGroupTable
edit_permissions = ['ipam.change_vlangroup', 'ipam.delete_vlangroup']
template_name = 'ipam/vlangroup_list.html'
class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vlangroup'
model = VLANGroup
form_class = forms.VLANGroupForm
cancel_url = 'ipam:vlangroup_list'
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlangroup'
cls = VLANGroup
default_redirect_url = 'ipam:vlangroup_list'
#
# VLANs
#
@@ -500,10 +588,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,
})
@@ -538,7 +627,11 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['site', 'status', 'role']:
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]
@@ -548,5 +641,4 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlan'
cls = VLAN
form = forms.VLANBulkDeleteForm
default_redirect_url = 'ipam:vlan_list'

View File

@@ -78,3 +78,11 @@ SHORT_DATETIME_FORMAT = 'Y-m-d H:i'
# banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
BANNER_TOP = ''
BANNER_BOTTOM = ''
# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to
# prefer IPv4 instead.
PREFER_IPV4 = False
# Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table
# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
ENFORCE_GLOBAL_UNIQUE = False

View File

@@ -12,7 +12,7 @@ except ImportError:
"the documentation.")
VERSION = '1.2.0'
VERSION = '1.4.2'
# Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
@@ -40,6 +40,8 @@ DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
# Attempt to import LDAP configuration if it has been defined
@@ -106,6 +108,7 @@ INSTALLED_APPS = (
'ipam',
'extras',
'secrets',
'tenancy',
'users',
'utilities',
)
@@ -137,7 +140,6 @@ TEMPLATES = [
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'utilities.context_processors.settings',
'django.core.context_processors.request',
],
},
},

View File

@@ -2,10 +2,12 @@ from django.conf.urls import include, url
from django.contrib import admin
from django.views.defaults import page_not_found
from views import home, trigger_500
from views import home, trigger_500, handle_500
from users.views import login, logout
handler500 = handle_500
urlpatterns = [
# Default page
@@ -20,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
@@ -27,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

@@ -1,23 +1,24 @@
from markdown import markdown
import sys
from django.conf import settings
from django.http import Http404
from django.shortcuts import render
from django.utils.safestring import mark_safe
from circuits.models import Provider, Circuit
from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
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(),
@@ -25,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(),
@@ -41,12 +43,20 @@ 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]
})
def trigger_500(request):
"""Hot-wired method of triggering a server error to test reporting."""
raise Exception("Congratulations, you've triggered an exception! Go tell all your friends what an exceptional "
"person you are.")
def handle_500(request):
"""Custom server error handler"""
type_, error, traceback = sys.exc_info()
return render(request, '500.html', {
'exception': str(type_),
'error': error,
}, status=500)

View File

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

View File

@@ -1,17 +1,23 @@
$(document).ready(function() {
// "Select all" checkbox in a table header
$('th input:checkbox').click(function (event) {
$('th input:checkbox[name=_all]').click(function (event) {
$(this).parents('table').find('td input:checkbox').prop('checked', $(this).prop('checked'));
});
// Uncheck the "select all" checkbox if an item is unchecked
$('input:checkbox[name=pk]').click(function (event) {
if (!$(this).attr('checked')) {
$(this).parents('table').find('input:checkbox[name=_all]').prop('checked', false);
}
});
// Slugify
function slugify(s, num_chars) {
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

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

View File

@@ -28,6 +28,7 @@ class SecretRoleListView(generics.ListAPIView):
"""
queryset = SecretRole.objects.all()
serializer_class = serializers.SecretRoleSerializer
permission_classes = [IsAuthenticated]
class SecretRoleDetailView(generics.RetrieveAPIView):
@@ -36,17 +37,19 @@ class SecretRoleDetailView(generics.RetrieveAPIView):
"""
queryset = SecretRole.objects.all()
serializer_class = serializers.SecretRoleSerializer
permission_classes = [IsAuthenticated]
class SecretListView(generics.GenericAPIView):
"""
List secrets (filterable). If a private key is POSTed, attempt to decrypt each Secret.
"""
queryset = Secret.objects.select_related('device__primary_ip', 'role')\
queryset = Secret.objects.select_related('device__primary_ip4', 'device__primary_ip6', 'role')\
.prefetch_related('role__users', 'role__groups')
serializer_class = serializers.SecretSerializer
filter_class = SecretFilter
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]
permission_classes = [IsAuthenticated]
def get(self, request, private_key=None):
queryset = self.filter_queryset(self.get_queryset())
@@ -87,10 +90,11 @@ class SecretDetailView(generics.GenericAPIView):
"""
Retrieve a single Secret. If a private key is POSTed, attempt to decrypt the Secret.
"""
queryset = Secret.objects.select_related('device__primary_ip', 'role')\
queryset = Secret.objects.select_related('device__primary_ip4', 'device__primary_ip6', 'role')\
.prefetch_related('role__users', 'role__groups')
serializer_class = serializers.SecretSerializer
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]
permission_classes = [IsAuthenticated]
def get(self, request, pk, private_key=None):
secret = get_object_or_404(Secret, pk=pk)

View File

@@ -1,9 +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(),
@@ -15,7 +22,19 @@ class SecretFilter(django_filters.FilterSet):
to_field_name='slug',
label='Role (slug)',
)
device = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(),
to_field_name='name',
label='Device (Name)',
)
class Meta:
model = Secret
fields = ['name', 'role_id', 'role']
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

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

View File

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

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

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

View File

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

View File

@@ -24,170 +24,182 @@
<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 %}
</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/vlans/' %} active{% endif %}">
<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/vlans/' %} active{% endif %}">
{% if perms.ipam.add_vlan %}
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'ipam:vlan_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VLANs</a></li>
<li><a href="{% url 'ipam:vlan_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VLAN</a></li>
<li><a href="{% url 'ipam:vlan_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import VLANs</a></li>
</ul>
{% else %}
<a href="{% url 'ipam:vlan_list' %}">VLANs</a>
{% endif %}
<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="fa fa-search" aria-hidden="true"></i> VLANs</a></li>
{% if perms.ipam.add_vlan %}
<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="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="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>
@@ -195,14 +207,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>
@@ -212,12 +224,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,67 @@
<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>Port Speed</td>
<td>
{% if circuit.port_speed %}
{{ circuit.port_speed_human }}
{% else %}
<span class="text-muted">N/A</span>
{% 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 +134,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,19 @@
{% 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.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>
@@ -76,7 +81,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,10000,2000,937649,PP8371 ports 13/14</pre>
</div>
</div>
{% endblock %}

View File

@@ -7,7 +7,7 @@
<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>
{% endif %}
@@ -19,23 +19,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,7 +6,7 @@
<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>
{% endif %}
@@ -14,8 +14,12 @@
</div>
<h1>Providers</h1>
<div class="row">
<div class="col-md-12">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

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

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

View File

@@ -5,7 +5,7 @@
{% block title %}Device Import{% endblock %}
{% block content %}
<h1>Device Import</h1>
{% include 'dcim/inc/_device_import_header.html' %}
<div class="row">
<div class="col-md-12">
<form action="." method="post" class="form">
@@ -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>
@@ -79,7 +84,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,Ashburn-VA,R101,21,Rear</pre>
</div>
</div>
{% endblock %}

View File

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

View File

@@ -107,7 +107,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,23 +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">
<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 %}
@@ -42,19 +42,35 @@
<table class="table table-hover panel-body">
<tr>
<td>Manufacturer</td>
<td>{{ devicetype.manufacturer }}</td>
<td><a href="{% url 'dcim:devicetype_list' %}?manufacturer={{ devicetype.manufacturer.slug }}">{{ devicetype.manufacturer }}</a></td>
</tr>
<tr>
<td>Model Name</td>
<td>{{ devicetype.model }}</td>
</tr>
<tr>
<td>Part Number</td>
<td>
{% if devicetype.part_number %}
{{ devicetype.part_number }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Height (U)</td>
<td>{{ devicetype.u_height }}</td>
</tr>
<tr>
<td>Full Depth</td>
<td>{{ devicetype.is_full_depth|yesno|capfirst }}</td>
<td>
{% if devicetype.is_full_depth %}
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
{% else %}
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
{% endif %}
</td>
</tr>
</table>
</div>
@@ -64,21 +80,70 @@
</div>
<table class="table table-hover panel-body">
<tr>
<td>Is a Console Server</td>
<td>{{ devicetype.is_console_server|yesno|capfirst }}</td>
<td class="text-right">
{% if devicetype.is_console_server %}
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
{% else %}
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
{% endif %}
</td>
<td>
<strong>Console Server</strong><br />
<small class="text-muted">This device {% if devicetype.is_console_server %}has{% else %}does not have{% endif %} console server ports</small>
</td>
</tr>
<tr>
<td>Is a PDU</td>
<td>{{ devicetype.is_pdu|yesno|capfirst }}</td>
<td class="text-right">
{% if devicetype.is_pdu %}
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
{% else %}
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
{% endif %}
</td>
<td>
<strong>PDU</strong><br />
<small class="text-muted">This device {% if devicetype.is_pdu %}has{% else %}does not have{% endif %} power outlets</small>
</td>
</tr>
<tr>
<td>Is a Network Device</td>
<td>{{ devicetype.is_network_device|yesno|capfirst }}</td>
<td class="text-right">
{% if devicetype.is_network_device %}
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
{% else %}
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
{% endif %}
</td>
<td>
<strong>Network Device</strong><br />
<small class="text-muted">This device {% if devicetype.is_network_device %}has{% else %}does not have{% endif %} non-management network interfaces</small>
</td>
</tr>
<tr>
<td class="text-right">
{% if devicetype.subdevice_role == True %}
<label class="label label-primary">Parent</label>
{% elif devicetype.subdevice_role == False %}
<label class="label label-info">Child</label>
{% else %}
<label class="label label-default">None</label>
{% endif %}
</td>
<td>
<strong>Parent/Child</strong><br />
{% if devicetype.subdevice_role == True %}
<small class="text-muted">This device has device bays for mounting child devices</small>
{% elif devicetype.subdevice_role == False %}
<small class="text-muted">This device can only be mounted in a parent device</small>
{% else %}
<small class="text-muted">This device does not have device bays</small>
{% endif %}
</td>
</tr>
</table>
</div>
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %}
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
{% include 'dcim/inc/devicetype_component_table.html' with table=mgmt_interface_table title='Management Interfaces' add_url='dcim:devicetype_add_interface' add_url_extra='?mgmt_only=1' delete_url='dcim:devicetype_delete_interface' %}
</div>
<div class="col-md-6">
{% if devicetype.is_parent_device %}

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

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

View File

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

View File

@@ -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

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

View File

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

View File

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

View File

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

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