Compare commits

...

251 Commits

Author SHA1 Message Date
Jeremy Stretch
8cf8710130 Merge pull request #2725 from digitalocean/develop
Release v2.5.2
2018-12-21 11:46:31 -05:00
Jeremy Stretch
3705e37678 Release v2.5.2 2018-12-21 11:44:30 -05:00
Jeremy Stretch
ebe5193348 Fixes #2724: Limit rear port choices to current device when editing a front port 2018-12-21 11:09:44 -05:00
Jeremy Stretch
a3097d254e Fixes #2721: Detect loops when tracing front/rear ports 2018-12-21 10:54:20 -05:00
Jeremy Stretch
38276d9539 Fixes #2723: Correct permission evaluation when bulk deleting tags 2018-12-21 09:11:07 -05:00
Jeremy Stretch
91a2168952 Fixes #2717: Fix bulk deletion of tags 2018-12-21 09:08:00 -05:00
Jeremy Stretch
4a10b4ece0 Fixes #2704: Fix form select widget population on parent with null value 2018-12-20 15:49:35 -05:00
Jeremy Stretch
853b1fad15 Fixes #2712: Preserve list filtering after editing objects in bulk 2018-12-20 15:33:53 -05:00
Jeremy Stretch
7acbeb55bc Minor tweaks 2018-12-20 09:54:59 -05:00
Jeremy Stretch
8498e0088b Merge pull request #2667 from Jemikwa/develop
#2656 Updating LDAP documentation
2018-12-20 09:53:21 -05:00
Jeremy Stretch
aae10f7d71 Tweaked 200GE and 400GE interface type labels 2018-12-20 09:18:18 -05:00
Jeremy Stretch
6b19a2b101 Fixes #2709: Update example report for compatibility with v2.5 2018-12-19 16:34:35 -05:00
Jeremy Stretch
b44a76e6bd Closes #2537: Added AUTH_LDAP_MIRROR_GROUPS setting to LDAP docs 2018-12-19 16:24:41 -05:00
Jeremy Stretch
7f71fc1d42 Closes #2561: Add 200G and 400G interface types 2018-12-19 16:13:04 -05:00
Jeremy Stretch
ba9fe408bc #2675: Added InventoryItem search form field for 'discovered' 2018-12-19 14:15:22 -05:00
Jeremy Stretch
40cb576e11 Fixes #2673: Fix exception on LLDP neighbors view for device with a circuit connected 2018-12-19 14:04:22 -05:00
Jeremy Stretch
2f1db2fdf3 Fixes #2691: Cable trace should follow circuits 2018-12-19 12:48:20 -05:00
Jeremy Stretch
f4a22e5af3 Introduced fgcolor template filter to render ideal foreground color for any background color 2018-12-19 12:17:40 -05:00
Jeremy Stretch
aca57ec281 Fixes #2698: Remove pagination restriction on bulk component creation for devices/VMs 2018-12-19 10:59:12 -05:00
Jeremy Stretch
68cb8b6895 Closes #2701: Enable filtering of prefixes by exact prefix value 2018-12-19 10:02:18 -05:00
Jeremy Stretch
82e8c0152e Fixes #2707: Correct permission evaluation for circuit termination cabling 2018-12-19 09:36:45 -05:00
Jeremy Stretch
d4a9318826 Post-release version bump 2018-12-13 15:24:13 -05:00
Jeremy Stretch
27a893a9a1 Merge pull request #2688 from digitalocean/develop
Release v2.5.1
2018-12-13 15:20:09 -05:00
Jeremy Stretch
9f1fcca5ea Release v2.5.1 2018-12-13 15:03:08 -05:00
Jeremy Stretch
bb564363d5 Fix regression from #2683 2018-12-13 14:59:54 -05:00
Jeremy Stretch
dd2a6a41da Fixes #2687: Correct naming of before/after filters for changelog entries 2018-12-13 14:43:05 -05:00
Jeremy Stretch
a6c8c615eb Closes #2674: Enable filtering changelog by object type under web UI 2018-12-13 14:37:03 -05:00
Jeremy Stretch
0d3b1bfca4 Fixes #2683: Fix exception when connecting a cable to a RearPort with no corresponding FrontPort 2018-12-12 16:40:34 -05:00
Jeremy Stretch
edd763b1aa Fixes #2684: Fix custom field filtering 2018-12-12 16:06:50 -05:00
Jeremy Stretch
2418fed65b Fixes #2663: Prevent duplicate interfaces from appearing under VLAN members view 2018-12-12 13:18:42 -05:00
Jeremy Stretch
785cdcefd6 Closes #2671: Add documentation of API brief format 2018-12-12 10:27:18 -05:00
Jeremy Stretch
3480832bf5 Added changelog for #2662 (fixed under #2680) 2018-12-12 09:59:54 -05:00
Jeremy Stretch
ee038bd77b Closes #2655: Add 128GFC Fibrechannel interface type 2018-12-12 09:48:17 -05:00
Jeremy Stretch
6460c95e00 Fixes #2678: Fix error when viewing webhook in admin UI without write permission 2018-12-12 09:30:31 -05:00
Jeremy Stretch
b0a6781623 Fixes #2680: Disallow POST requests to /dcim/interface-connections/ API endpoint 2018-12-12 09:20:07 -05:00
Jeremy Stretch
8364e56e86 Fixes #2676: Fix exception when passing dictionary value to a ChoiceField 2018-12-11 17:00:20 -05:00
Jeremy Stretch
b8a4316297 Changelog for #2666 2018-12-11 13:47:24 -05:00
Jeremy Stretch
24d1707693 Merge pull request #2670 from DanSheps/2666-fix-length-display
Fixes #2666: Uses correct function for displaying choices label
2018-12-11 13:45:16 -05:00
dansheps
b4f79f1667 Fixes #2666: Uses correct function for displaying choices label
* Changes record.length_type to record.get_length_type_display
2018-12-11 12:40:07 -06:00
Jemikwa
064dd9bef2 Updating LDAP documentation
Adding information on service restarts and logging LDAP queries for troubleshooting.
2018-12-11 11:45:45 -06:00
Jeremy Stretch
b697c30941 #2627: Removed reference to provider from Circuit.__str__() 2018-12-11 11:15:45 -05:00
Jeremy Stretch
93c95fdfa8 Post-release version bump 2018-12-10 10:29:51 -05:00
Jeremy Stretch
8863a3126d Merge pull request #2660 from digitalocean/develop
Release v2.5.0
2018-12-10 10:27:24 -05:00
Jeremy Stretch
acbe5f6418 Release v2.5.0 2018-12-10 10:22:32 -05:00
Jeremy Stretch
4e6652d811 Change pip command to pip3 2018-12-10 09:57:37 -05:00
Jeremy Stretch
7d4fa69595 Fixes #2657: Fix typo 2018-12-10 09:54:30 -05:00
Jeremy Stretch
baeb7937fc Updated requirements for v2.5 release 2018-12-07 15:52:25 -05:00
Jeremy Stretch
2bd9f8a11f Updated installation docs for v2.5 release 2018-12-07 15:29:18 -05:00
Jeremy Stretch
44a2919a29 Django 2.1 requires Python 3.5+ 2018-12-07 14:44:36 -05:00
Jeremy Stretch
77fbc42f75 Relax Python version requirement to 3.4 2018-12-07 14:29:17 -05:00
Jeremy Stretch
65edffea63 Merge v2.5 work 2018-12-07 10:51:28 -05:00
Jeremy Stretch
9689ba2c4f Fix representation of connected_endpoint_type for non-connected components 2018-12-06 16:39:03 -05:00
Jeremy Stretch
703be259fd Normalize connection_status for non-connected device components during migration 2018-12-06 16:32:42 -05:00
Jeremy Stretch
45a1dfbd8a Closes #2649: Add connected_endpoint_type to connectable device component API representations 2018-12-06 16:14:03 -05:00
Jeremy Stretch
360303f86c Closes #2474: Add cabled and connection_status filters for device components 2018-12-06 12:39:12 -05:00
Jeremy Stretch
64d37cd450 Closes #2648: Include the connection_status field in nested represenations of connectable device components 2018-12-06 12:14:54 -05:00
Jeremy Stretch
71dee2758b Simplified filter_device() for Interfaces 2018-12-06 11:33:24 -05:00
Jeremy Stretch
870edbb44a Fixes #2626: Remove extraneous permissions generated from proxy models 2018-12-05 16:53:58 -05:00
Jeremy Stretch
2a07e8f3f0 Move queryset_to_csv() utility into ObjectListView to allow overriding by individual views 2018-12-05 16:35:59 -05:00
Jeremy Stretch
ab4cb46d94 Additional API change notes 2018-12-05 11:13:29 -05:00
Jeremy Stretch
f0874f4be0 Add missing choices for new cable and rack fields 2018-12-04 15:15:40 -05:00
Jeremy Stretch
dffa2d3556 Closes #2632: Change representation of null values from 0 to 'null' 2018-12-04 15:09:07 -05:00
Jeremy Stretch
a3ade01224 Fixes #2639: Fix preservation of length/dimensions unit for racks and cables 2018-12-03 11:13:37 -05:00
Jeremy Stretch
d1cd366dc9 Fixes #2616: Convert Rack outer_unit and Cable length_unit to integer-based choice fields 2018-11-30 12:26:28 -05:00
Jeremy Stretch
a1a9396287 Closes #2594: upgrade.sh no longer invokes sudo 2018-11-30 11:12:10 -05:00
Jeremy Stretch
08b4b24296 Fixes #2622: Enable filtering cables by multiple types/colors 2018-11-28 14:22:55 -05:00
Jeremy Stretch
5acd429c55 Fixes #2624: Delete associated content type and permissions when removing InterfaceConnection model 2018-11-28 13:45:02 -05:00
Jeremy Stretch
4da755e75f Formatting cleanup 2018-11-27 11:57:29 -05:00
Jeremy Stretch
bd7aee7c1f Closes #2614: Simplify calls of super() for Python 3 2018-11-27 10:52:24 -05:00
Jeremy Stretch
7d262296e1 Added a description and repo URL for each dependency 2018-11-27 09:51:48 -05:00
Jeremy Stretch
3f1b42d466 Release v2.5-beta2 2018-11-26 16:27:57 -05:00
Jeremy Stretch
7d8ae5e763 Fixes #2609: Fixed exception when ChoiceField integer value is passed as a string 2018-11-26 14:05:57 -05:00
Jeremy Stretch
2bae50f501 Enforce consistent ordering of value/label keys for ChoiceField 2018-11-26 14:02:38 -05:00
Jeremy Stretch
a46f68c6e4 Fixes #2608: Fixed null outer_unit error on rack import 2018-11-26 13:41:35 -05:00
Jeremy Stretch
d59be2912e Closes #2601: Added a description field to pass-through ports 2018-11-20 21:28:19 -05:00
Jeremy Stretch
240d22696f Strip annotations from queryset when retrieving object count for API pagination 2018-11-20 21:02:06 -05:00
Jeremy Stretch
2b1516ea79 Changelog for #2602 2018-11-20 13:21:38 -05:00
Jeremy Stretch
89622f1ddf Fixes #2602: Return HTTP 204 when no new IPs/prefixes are available for provisioning 2018-11-20 13:03:59 -05:00
Jeremy Stretch
874acab90f Merge v2.4.8 2018-11-20 12:28:24 -05:00
Jeremy Stretch
c29ae9b785 Added missing import buttons to cables list 2018-11-20 09:41:38 -05:00
Jeremy Stretch
5ce955a719 Fixed toggling of connection status for cable path endpoints 2018-11-20 09:23:30 -05:00
Jeremy Stretch
8c3a294384 Standardized behavior and display of connection_status 2018-11-19 15:26:06 -05:00
Jeremy Stretch
55cc327e05 Updated migrations 2018-11-19 13:44:18 -05:00
Jeremy Stretch
a324638f1f Improved logic for recording cable path connection status 2018-11-19 12:37:53 -05:00
Jeremy Stretch
7dde370ee1 Fixes #2593: Fix toggling of connected cable's status 2018-11-15 14:23:23 -05:00
John Anderson
dfe6ba5603 #2583 changelog 2018-11-15 00:55:47 -05:00
John Anderson
fd9b2f2fda cleanup component filters and UI filter forms for device and device type - #2583 2018-11-15 00:42:01 -05:00
John Anderson
3c0181ef35 added logic to append &type=physical to interface API query for cable creation - #2585 2018-11-14 23:35:15 -05:00
Jeremy Stretch
641254b23a Closes #2053: Introduced the LOGIN_TIMEOUT configuration setting 2018-11-14 14:18:32 -05:00
Jeremy Stretch
63bd48003e Clean up cable termination types 2018-11-14 12:17:18 -05:00
Jeremy Stretch
f8e6cfbeba Closes #2426: Introduced SESSION_FILE_PATH configuration setting for authentication without write access to database 2018-11-13 10:31:44 -05:00
John Anderson
fc41359df6 removed blank line 2018-11-12 23:34:14 -05:00
John Anderson
5649024d93 #2586 changelog 2018-11-12 23:27:14 -05:00
John Anderson
65bc8f0254 resolved #2586 - tests for the Cable model clean method 2018-11-12 23:26:08 -05:00
John Anderson
7887a70b70 #2585 changelog 2018-11-12 23:20:17 -05:00
John Anderson
8a6913fe19 fixed #2585 - connections with virtual interfaces 2018-11-12 23:18:23 -05:00
John Anderson
7cd0e0b244 #2584 changelog 2018-11-12 22:19:30 -05:00
John Anderson
9543b5e716 fixed #2584 - validation for connecting front port to corresponding rear port 2018-11-12 22:15:06 -05:00
Jeremy Stretch
bc8dbfde7c Merge branch 'develop' into develop-2.5 2018-11-12 16:03:52 -05:00
Jeremy Stretch
59589fdd29 Fixes #2580: Remove erroneous CSS class from "add components" button 2018-11-12 12:04:04 -05:00
Jeremy Stretch
75f0d8ee90 Closes #2578: Reorganized nested serializers 2018-11-12 11:36:44 -05:00
Jeremy Stretch
04ae6ec7af Fixes #2554: Fix cable trace display when following a rear port with no cable attached 2018-11-09 15:22:34 -05:00
Jeremy Stretch
3bbf4a3352 Fixes #2579: Add missing cable disconnect buttons for front/rear ports 2018-11-09 15:17:42 -05:00
Jeremy Stretch
0316072863 Fixes #2574: Remove duplicate interface links from topology maps 2018-11-09 09:54:30 -05:00
Jeremy Stretch
be5bf6b711 Closes #2570: Add bulk disconnect view for front/rear pass-through ports 2018-11-08 15:12:24 -05:00
Jeremy Stretch
788847edaa Fixes #2573: Fix bulk console/power/interface disconnections 2018-11-08 15:04:34 -05:00
Jeremy Stretch
30f8fb4c11 Fixes #2572: Add button to disconnect cable from circuit termination 2018-11-08 12:27:15 -05:00
Jeremy Stretch
3e92aa9fe7 Fixes #2571: Enforce deletion of attached cable when deleting a termination point 2018-11-08 12:15:56 -05:00
Jeremy Stretch
bb5432de7d PEP8 fix 2018-11-08 11:13:03 -05:00
Jeremy Stretch
21fd889810 Fix regression from #2566 2018-11-08 11:11:52 -05:00
Jeremy Stretch
a228f1e1c2 Closes #2569: Added LSH fiber type; removed SC duplex/simplex designations 2018-11-08 10:49:26 -05:00
Jeremy Stretch
4b5181d640 Fixes #2566: Prevent both ends of a cable from connecting to the same termination point 2018-11-08 10:40:17 -05:00
Jeremy Stretch
0dee55885b Fixes #2567: Introduced proxy models to represent console/power/interface connections 2018-11-08 09:51:47 -05:00
Jeremy Stretch
1e36a884fa Changelog for #2563 2018-11-08 09:11:28 -05:00
Jeremy Stretch
d4e266d48c Fixes #2563: Enable export templates for cables 2018-11-07 15:49:45 -05:00
mmahacek
c063961e4a Add Cable count to home page (#2556)
* Add stats.cable_count

* Add Cable count to home page
2018-11-07 10:36:04 -05:00
Jeremy Stretch
f61cea695b Release v2.5-beta1 2018-11-06 15:59:46 -05:00
Jeremy Stretch
e806b58ac9 Release v2.5-beta1 2018-11-06 15:58:55 -05:00
Jeremy Stretch
338ce737c8 Removed deprecated expand_numeric_pattern() 2018-11-06 15:39:38 -05:00
Jeremy Stretch
f41f141130 Removed unused class NullableModelMultipleChoiceField 2018-11-06 15:32:59 -05:00
Jeremy Stretch
9f0169286b Standardize usage of NaturalOrderingManager 2018-11-06 14:05:23 -05:00
Jeremy Stretch
60452a7b0c Implemented DeviceComponentManager; removed natsort as a dependency 2018-11-06 12:43:30 -05:00
Jeremy Stretch
6140dd955a Set unit_length to empty string when not specified during cable import 2018-11-06 12:12:06 -05:00
Jeremy Stretch
5e06c5a9ea Added cable connection views for front/rear ports 2018-11-06 12:04:46 -05:00
Jeremy Stretch
ad4e1f01aa Improved templates for cable creation 2018-11-06 11:52:41 -05:00
Jeremy Stretch
bb114ea66b Re-implemented connected device API view & test 2018-11-06 11:44:32 -05:00
Jeremy Stretch
ce00226bc6 Merge release v2.4.7 into develop-2.5 2018-11-06 11:19:38 -05:00
Jeremy Stretch
01628cdd31 Updated base requirements 2018-11-05 15:09:11 -05:00
Jeremy Stretch
e57b3bc4ab Closes #2057: Added description columns to interface connections list 2018-11-05 14:41:51 -05:00
Jeremy Stretch
82076d3ca6 Changelog for #1931 2018-11-05 14:33:50 -05:00
Jeremy Stretch
46e594f1f1 Closes #1931: Added a count of assigned IP addresses to the interface API serializer 2018-11-05 14:32:22 -05:00
Jeremy Stretch
e8caa46484 Expanded v2.5 release notes 2018-11-05 14:01:34 -05:00
Jeremy Stretch
1847e82d04 Closes #867: Add a 'description' field to CircuitTermination 2018-11-05 13:53:22 -05:00
Jeremy Stretch
045856c9ac Introduced a placeholder template filter 2018-11-05 13:00:46 -05:00
Jeremy Stretch
53b6a07a41 Changelog for #2165 2018-11-05 12:07:02 -05:00
Jeremy Stretch
976388f29c Removed 'interface_ordering' field from DeviceType 2018-11-05 12:02:55 -05:00
Jeremy Stretch
e5f12109c5 Closes #2165: Re-implemented natural ordering for interfaces 2018-11-05 11:51:38 -05:00
Juho Juopperi
319869c22e Update django is_safe_url calls to new API (#2546) 2018-11-05 08:52:00 -05:00
Jeremy Stretch
66ef5c726c Removed deprecated CableQuerySet 2018-11-02 15:33:31 -04:00
Jeremy Stretch
f8a961b568 Cleaned up import statements 2018-11-02 15:20:08 -04:00
Jeremy Stretch
521a989d23 Removed Python2 hack 2018-11-02 14:59:48 -04:00
Jeremy Stretch
0a71c63583 Upgraded to Django 2.1 2018-11-02 14:42:15 -04:00
Jeremy Stretch
b05171b126 Upgraded django-tables2 to v2.0.2 2018-11-02 14:27:17 -04:00
Jeremy Stretch
9804db4551 Updated dependencies 2018-11-02 14:14:15 -04:00
Jeremy Stretch
516faac73f Fixed deprecation warnings from DRF 3.9 2018-11-02 13:48:44 -04:00
Jeremy Stretch
29d546a3f1 Upgraded django-filter to 2.0.0 2018-11-02 13:46:28 -04:00
Jeremy Stretch
bb5c0989f3 Fixed compatability with Django REST Framework v3.9.0 2018-11-02 13:16:44 -04:00
Jeremy Stretch
3ea2978df4 Standardized naming of reverse relationships from component templates to DeviceType 2018-11-02 11:55:52 -04:00
Jeremy Stretch
cf9c8f35a0 Normalize ordering of cables by PK 2018-11-02 11:27:26 -04:00
Jeremy Stretch
cefec1be60 Show connected circuit termination on interface view 2018-11-02 11:15:09 -04:00
Jeremy Stretch
16d8308fa8 Deprecated CableTermination.get_connected_cable() 2018-11-02 11:04:21 -04:00
Jeremy Stretch
af95ba48aa Replaced is_console_server, is_pdu, and is_network_device filters on Device and DeviceType 2018-11-02 10:45:31 -04:00
Jeremy Stretch
43446cc642 Added more detail to v2.5.0 release notes 2018-11-02 10:02:44 -04:00
Jeremy Stretch
c60c5502f6 Closes #450: Add 'outer_width' and 'outer_depth' fields to Rack 2018-11-02 09:51:17 -04:00
Jeremy Stretch
4ffe1866c5 Closes #1444: Add field to Rack model 2018-11-02 09:17:51 -04:00
Jeremy Stretch
d4ec309d68 Changelog for #2104 2018-11-02 08:59:51 -04:00
Jeremy Stretch
fa6c4db13b Closes #2104: Add 'status' field to Rack 2018-11-01 16:03:42 -04:00
Jeremy Stretch
91ce6c2420 Cable migration for ExportTemplate 2018-11-01 14:54:53 -04:00
Jeremy Stretch
62da0778ee Suppress print() output from migrations during testing 2018-11-01 14:54:36 -04:00
Jeremy Stretch
7ffb5f16dd Fix color display for cables 2018-11-01 14:32:43 -04:00
Jeremy Stretch
623de7d210 Fixed cable list display for circuit terminations 2018-11-01 14:14:31 -04:00
Jeremy Stretch
1119209fa0 Added CSV export for cables 2018-11-01 13:39:39 -04:00
Jeremy Stretch
c3db7191d2 Added bulk edit/delete views for cables 2018-11-01 13:19:24 -04:00
Jeremy Stretch
2af3685dae Added cables documentation 2018-11-01 12:22:25 -04:00
Jeremy Stretch
841ddd5d83 Add circuit termination as valid termination type for front/rear ports 2018-11-01 11:57:38 -04:00
Jeremy Stretch
a5f0deaea3 Put length and length_unit fields on same line in form 2018-11-01 11:54:34 -04:00
Jeremy Stretch
105f9365bf Fixed Cable model validation 2018-11-01 11:54:15 -04:00
Jeremy Stretch
903dfb6f07 Added validation to Cable model 2018-11-01 11:44:02 -04:00
Jeremy Stretch
571a3e9677 Added cable column to console/power/interface connection tables 2018-11-01 10:51:38 -04:00
Jeremy Stretch
79c110df39 Create id_string on Cable to preserve access to pk after object deletion 2018-11-01 10:25:30 -04:00
Jeremy Stretch
ad03a0925b Fixed cable type filtering 2018-11-01 10:18:37 -04:00
Jeremy Stretch
5e7fccb64e Add blank choice to ColorSelect widget 2018-11-01 10:14:47 -04:00
Jeremy Stretch
3bc6fc5e24 Added changelog view for Cable 2018-11-01 10:04:43 -04:00
Jeremy Stretch
849db8fe44 Finished CableCSVForm 2018-11-01 09:59:53 -04:00
Jeremy Stretch
55c632ace7 Introduce CableBulkImportView 2018-10-31 17:05:25 -04:00
Jeremy Stretch
cd243a90d0 connected_endpoint must be read-only on termination serializers 2018-10-31 16:12:36 -04:00
Jeremy Stretch
4e9ace39ce Optimized queryset for InterfaceConnectionsListView 2018-10-31 16:01:46 -04:00
Jeremy Stretch
18628166dc Removed old console/power connection views 2018-10-31 15:54:29 -04:00
Jeremy Stretch
f7e23d912c Expanded cable connection form 2018-10-31 15:31:44 -04:00
Jeremy Stretch
121a6afb45 Fixed typo in API URL template 2018-10-31 15:23:00 -04:00
Jeremy Stretch
c2656c4ce1 Added tests for Cable model 2018-10-31 15:17:51 -04:00
Jeremy Stretch
d22c23290f Moved cable caching logic from Cable.save() to signals 2018-10-31 15:01:01 -04:00
Jeremy Stretch
8992c57039 Remove reverse relationship to CableTermination.cable 2018-10-31 15:00:09 -04:00
Jeremy Stretch
939a52dcbd Changed variable names for clarity 2018-10-31 14:01:20 -04:00
Jeremy Stretch
6f788fcd23 Tweaked display of connected circuits 2018-10-31 12:53:17 -04:00
Jeremy Stretch
f2f9ff5cbd Cleaned up migrations 2018-10-31 11:52:43 -04:00
Jeremy Stretch
470aabe1d7 Added dedicated cable trace view; removed modal 2018-10-30 16:30:03 -04:00
Jeremy Stretch
669fe1f192 Add missing interfaces table 2018-10-30 15:16:15 -04:00
Jeremy Stretch
62ca511619 Updated documentation 2018-10-30 15:12:14 -04:00
Jeremy Stretch
10492de50d Fix NestedCableSerializer 2018-10-30 14:58:04 -04:00
Jeremy Stretch
e1e9a451da PEP8 fix 2018-10-30 14:57:49 -04:00
Jeremy Stretch
60f5371b31 Fix front ports queryset 2018-10-30 14:54:29 -04:00
Jeremy Stretch
aad4f58633 Add 'cable' field to CircuitTermination serializer 2018-10-30 14:53:41 -04:00
Jeremy Stretch
2fa1c81070 Added initial tests for cable connections 2018-10-30 14:28:46 -04:00
Jeremy Stretch
3420b5fbdf CircuitTermination should inherit from CableTermination 2018-10-30 14:27:45 -04:00
Jeremy Stretch
4df74780b8 Extended Cables to connect CircuitTerminations 2018-10-30 12:16:22 -04:00
Jeremy Stretch
cbfb25f003 Initial work on a cable tracing modal 2018-10-29 16:45:20 -04:00
Jeremy Stretch
e3dc12338b Introduced a 'trace' API endpoint for cable terminations 2018-10-29 15:43:41 -04:00
Jeremy Stretch
e75ef5fd2d Replace references to Interface.connection with connected_endpoint 2018-10-29 14:15:04 -04:00
Jeremy Stretch
8ebf7a7ad0 Standard naming of reverse relationship to Device from component models 2018-10-29 13:42:58 -04:00
Jeremy Stretch
d7766b9828 Replace 'is_connected' boolean with Cable attachment 2018-10-29 13:36:41 -04:00
Jeremy Stretch
427226de8c Optimized API views 2018-10-29 13:26:09 -04:00
Jeremy Stretch
cb1bdccf51 'cable' should be a read-only field 2018-10-29 13:14:41 -04:00
Jeremy Stretch
1ab4344537 Fix front/rear port connection styling 2018-10-29 13:09:23 -04:00
Jeremy Stretch
17a71d3e26 Optimized front/rear port connection displays 2018-10-29 12:56:17 -04:00
Jeremy Stretch
05431aa83b Added 'cable' field to device component serializers 2018-10-29 12:33:43 -04:00
Jeremy Stretch
e21b23cd98 Introduced 'cable' field on CableTermination to cache connected Cable 2018-10-29 12:24:16 -04:00
Jeremy Stretch
dc67028bd9 Corrected cable display 2018-10-29 09:41:03 -04:00
Jeremy Stretch
8f8df2b8e8 Added cable types 2018-10-26 15:45:19 -04:00
Jeremy Stretch
8573c8b8cb Remove is_console_server, is_pdu, and is_network_device from DeviceType 2018-10-26 14:48:22 -04:00
Jeremy Stretch
33cdecbc52 Remove errant print() statement 2018-10-26 14:06:27 -04:00
Jeremy Stretch
0c7510c1a8 PEP8 cleanup 2018-10-26 13:27:54 -04:00
Jeremy Stretch
94946c5854 Swapped the order of cable and connection fields 2018-10-26 13:21:43 -04:00
Jeremy Stretch
83dec3b587 Fixed color display 2018-10-26 13:14:12 -04:00
Jeremy Stretch
3518d023dc Implemented Cable API endpoint 2018-10-26 12:25:11 -04:00
Jeremy Stretch
4ad7882762 Clever icon thing 2018-10-26 11:32:04 -04:00
Jeremy Stretch
957de0cd98 Added cable connections to component lists 2018-10-26 11:29:03 -04:00
Jeremy Stretch
6bea8cc546 Added length and length_unit fields to Cable 2018-10-26 10:28:25 -04:00
Jeremy Stretch
2d90fc608e Introduced CableView and CableEditView 2018-10-25 15:51:12 -04:00
Jeremy Stretch
f134a6ec63 Introduced CableTermination abstract model to ptovide Cable access from termination points 2018-10-25 15:21:16 -04:00
Jeremy Stretch
266429101b Removed 'is_patch_panel' from DeviceType 2018-10-25 12:44:28 -04:00
Jeremy Stretch
b19e2037af Dropped 'Panel' from FrontPanelPort/RearPanelPort names 2018-10-25 12:11:13 -04:00
Jeremy Stretch
e79a156e1f Improved cable tracing 2018-10-25 10:56:47 -04:00
Jeremy Stretch
35f80f5085 First stab at cable path tracing and automatic endpoint connections 2018-10-24 16:17:01 -04:00
Jeremy Stretch
47c523a40b Fixed interface connection rendering 2018-10-24 15:07:11 -04:00
Jeremy Stretch
cca44d44f7 Added cable filter form 2018-10-24 14:59:46 -04:00
Jeremy Stretch
54c917beff Renamed Cable endpoints to terminations 2018-10-24 14:38:44 -04:00
Jeremy Stretch
9985f2cb82 Added CableListView 2018-10-24 14:26:08 -04:00
Jeremy Stretch
f30367e094 Deprecated the InterfaceConnection model 2018-10-24 13:59:44 -04:00
Jeremy Stretch
2ec8dc8319 Renamed PowerPort.power_outlet to connected_endpoint 2018-10-24 11:00:13 -04:00
Jeremy Stretch
1595a5ecd7 Renamed ConsolePort.cs_port to connected_endpoint 2018-10-24 10:37:54 -04:00
Jeremy Stretch
ea0de629df Fixed cable creation form 2018-10-23 13:40:10 -04:00
Jeremy Stretch
471bddea09 WIP: Initial work on the cable connection form 2018-10-22 16:58:24 -04:00
Jeremy Stretch
a36b120c8b Merge branch 'develop-2.5' into 20-physical-cabling 2018-10-19 13:34:21 -04:00
Jeremy Stretch
4cfe9fe182 Merge branch 'develop' into develop-2.5 2018-10-19 13:34:03 -04:00
Jeremy Stretch
59af8cc924 Remove ConnectableModel 2018-10-19 11:56:12 -04:00
Jeremy Stretch
d908dffab7 Fixed content type assignment within migration 2018-10-19 10:38:15 -04:00
Jeremy Stretch
ea5121ffe1 Preliminary work on Cables 2018-10-18 15:43:55 -04:00
Jeremy Stretch
22ed4f1b53 Merge release v2.4.6 2018-10-10 09:36:51 -04:00
Jeremy Stretch
3eddeeadc5 Initial work on #20 - patch panels 2018-10-03 14:04:16 -04:00
Jeremy Stretch
364bbdeab8 Merged release v2.4.5 2018-10-03 11:23:21 -04:00
Jeremy Stretch
2ac60bdf48 Merge branch 'develop' into develop-2.5 (v2.4.4 release) 2018-08-22 12:10:44 -04:00
Jeremy Stretch
19b737a534 Added v2.5 notes (so far) to changelog 2018-08-16 16:33:13 -04:00
Jeremy Stretch
29380b1120 Merge branch 'develop' into develop-2.5 2018-08-16 16:31:54 -04:00
Jeremy Stretch
c4be440cd1 Closes #2367: Remove deprecated RPCClient functionality 2018-08-16 12:21:24 -04:00
Jeremy Stretch
7145f86a6e #2359: Extended exempt attributes to 'migrate' command 2018-08-15 15:02:58 -04:00
Jeremy Stretch
7ec4155845 Closes #2359: Implement custom makemigrations command to ignore extraneous field attributes 2018-08-15 14:18:52 -04:00
Jeremy Stretch
6dd62dc891 Refactored log_change() methods for device components 2018-08-14 14:06:16 -04:00
Jeremy Stretch
5f66710fcd Closes #2292: Remove the deprecated UserAction model 2018-08-14 12:13:43 -04:00
Jeremy Stretch
980d62d579 Removed Python 2 string compatibility 2018-08-14 11:58:42 -04:00
Jeremy Stretch
ac546a9711 Closes #2000: Remove support for Python 2 2018-08-14 11:48:12 -04:00
405 changed files with 8949 additions and 6553 deletions

View File

@@ -5,7 +5,6 @@ addons:
postgresql: "9.4"
language: python
python:
- "2.7"
- "3.5"
install:
- pip install -r requirements.txt

View File

@@ -1,3 +1,125 @@
v2.5.2 (2018-12-21)
## Enhancements
* [#2561](https://github.com/digitalocean/netbox/issues/2561) - Add 200G and 400G interface types
* [#2701](https://github.com/digitalocean/netbox/issues/2701) - Enable filtering of prefixes by exact prefix value
## Bug Fixes
* [#2673](https://github.com/digitalocean/netbox/issues/2673) - Fix exception on LLDP neighbors view for device with a circuit connected
* [#2691](https://github.com/digitalocean/netbox/issues/2691) - Cable trace should follow circuits
* [#2698](https://github.com/digitalocean/netbox/issues/2698) - Remove pagination restriction on bulk component creation for devices/VMs
* [#2704](https://github.com/digitalocean/netbox/issues/2704) - Fix form select widget population on parent with null value
* [#2707](https://github.com/digitalocean/netbox/issues/2707) - Correct permission evaluation for circuit termination cabling
* [#2712](https://github.com/digitalocean/netbox/issues/2712) - Preserve list filtering after editing objects in bulk
* [#2717](https://github.com/digitalocean/netbox/issues/2717) - Fix bulk deletion of tags
* [#2721](https://github.com/digitalocean/netbox/issues/2721) - Detect loops when tracing front/rear ports
* [#2723](https://github.com/digitalocean/netbox/issues/2723) - Correct permission evaluation when bulk deleting tags
* [#2724](https://github.com/digitalocean/netbox/issues/2724) - Limit rear port choices to current device when editing a front port
---
v2.5.1 (2018-12-13)
## Enhancements
* [#2655](https://github.com/digitalocean/netbox/issues/2655) - Add 128GFC Fibrechannel interface type
* [#2674](https://github.com/digitalocean/netbox/issues/2674) - Enable filtering changelog by object type under web UI
## Bug Fixes
* [#2662](https://github.com/digitalocean/netbox/issues/2662) - Fix ImproperlyConfigured exception when rendering API docs
* [#2663](https://github.com/digitalocean/netbox/issues/2663) - Prevent duplicate interfaces from appearing under VLAN members view
* [#2666](https://github.com/digitalocean/netbox/issues/2666) - Correct display of length unit in cables list
* [#2676](https://github.com/digitalocean/netbox/issues/2676) - Fix exception when passing dictionary value to a ChoiceField
* [#2678](https://github.com/digitalocean/netbox/issues/2678) - Fix error when viewing webhook in admin UI without write permission
* [#2680](https://github.com/digitalocean/netbox/issues/2680) - Disallow POST requests to `/dcim/interface-connections/` API endpoint
* [#2683](https://github.com/digitalocean/netbox/issues/2683) - Fix exception when connecting a cable to a RearPort with no corresponding FrontPort
* [#2684](https://github.com/digitalocean/netbox/issues/2684) - Fix custom field filtering
* [#2687](https://github.com/digitalocean/netbox/issues/2687) - Correct naming of before/after filters for changelog entries
---
v2.5.0 (2018-12-10)
## Notes
### Python 3 Required
As promised, Python 2 support has been completed removed. Python 3.5 or higher is now required to run NetBox. Please see [our Python 3 migration guide](https://netbox.readthedocs.io/en/stable/installation/migrating-to-python3/) for assistance with upgrading.
### Removed Deprecated User Activity Log
The UserAction model, which was deprecated by the new change logging feature in NetBox v2.4, has been removed. If you need to archive legacy user activity, do so prior to upgrading to NetBox v2.5, as the database migration will remove all data associated with this model.
### View Permissions in Django 2.1
Django 2.1 introduces view permissions for object types (not to be confused with object-level permissions). Implementation of [#323](https://github.com/digitalocean/netbox/issues/323) is planned for NetBox v2.6. Users are encourage to begin assigning view permissions as desired in preparation for their eventual enforcement.
### upgrade.sh No Longer Invokes sudo
The `upgrade.sh` script has been tweaked so that it no longer invokes `sudo` internally. This was done to ensure compatibility when running NetBox inside a Python virtual environment. If you need elevated permissions when upgrading NetBox, call the upgrade script with `sudo upgrade.sh`.
## New Features
### Patch Panels and Cables ([#20](https://github.com/digitalocean/netbox/issues/20))
NetBox now supports modeling physical cables for console, power, and interface connections. The new pass-through port component type has also been introduced to model patch panels and similar devices.
## Enhancements
* [#450](https://github.com/digitalocean/netbox/issues/450) - Added `outer_width` and `outer_depth` fields to rack model
* [#867](https://github.com/digitalocean/netbox/issues/867) - Added `description` field to circuit terminations
* [#1444](https://github.com/digitalocean/netbox/issues/1444) - Added an `asset_tag` field for racks
* [#1931](https://github.com/digitalocean/netbox/issues/1931) - Added a count of assigned IP addresses to the interface API serializer
* [#2000](https://github.com/digitalocean/netbox/issues/2000) - Dropped support for Python 2
* [#2053](https://github.com/digitalocean/netbox/issues/2053) - Introduced the `LOGIN_TIMEOUT` configuration setting
* [#2057](https://github.com/digitalocean/netbox/issues/2057) - Added description columns to interface connections list
* [#2104](https://github.com/digitalocean/netbox/issues/2104) - Added a `status` field for racks
* [#2165](https://github.com/digitalocean/netbox/issues/2165) - Improved natural ordering of Interfaces
* [#2292](https://github.com/digitalocean/netbox/issues/2292) - Removed the deprecated UserAction model
* [#2367](https://github.com/digitalocean/netbox/issues/2367) - Removed deprecated RPCClient functionality
* [#2426](https://github.com/digitalocean/netbox/issues/2426) - Introduced `SESSION_FILE_PATH` configuration setting for authentication without write access to database
* [#2594](https://github.com/digitalocean/netbox/issues/2594) - `upgrade.sh` no longer invokes sudo
## Changes From v2.5-beta2
* [#2474](https://github.com/digitalocean/netbox/issues/2474) - Add `cabled` and `connection_status` filters for device components
* [#2616](https://github.com/digitalocean/netbox/issues/2616) - Convert Rack `outer_unit` and Cable `length_unit` to integer-based choice fields
* [#2622](https://github.com/digitalocean/netbox/issues/2622) - Enable filtering cables by multiple types/colors
* [#2624](https://github.com/digitalocean/netbox/issues/2624) - Delete associated content type and permissions when removing InterfaceConnection model
* [#2626](https://github.com/digitalocean/netbox/issues/2626) - Remove extraneous permissions generated from proxy models
* [#2632](https://github.com/digitalocean/netbox/issues/2632) - Change representation of null values from `0` to `null`
* [#2639](https://github.com/digitalocean/netbox/issues/2639) - Fix preservation of length/dimensions unit for racks and cables
* [#2648](https://github.com/digitalocean/netbox/issues/2648) - Include the `connection_status` field in nested represenations of connectable device components
* [#2649](https://github.com/digitalocean/netbox/issues/2649) - Add `connected_endpoint_type` to connectable device component API representations
## API Changes
* The `/extras/recent-activity/` endpoint (replaced by change logging in v2.4) has been removed
* The `rpc_client` field has been removed from dcim.Platform (see #2367)
* Introduced a new API endpoint for cables at `/dcim/cables/`
* New endpoints for front and rear pass-through ports (and their templates) in parallel with existing device components
* The fields `interface_connection` on Interface and `interface` on CircuitTermination have been replaced with `connected_endpoint` and `connection_status`
* A new `cable` field has been added to console, power, and interface components and to circuit terminations
* New fields for dcim.Rack: `status`, `asset_tag`, `outer_width`, `outer_depth`, `outer_unit`
* The following boolean filters on dcim.Device and dcim.DeviceType have been renamed:
* `is_console_server`: `console_server_ports`
* `is_pdu`: `power_outlets`
* `is_network_device`: `interfaces`
* The following new boolean filters have been introduced for dcim.Device and dcim.DeviceType:
* `console_ports`
* `power_ports`
* `pass_through_ports`
* The field `interface_ordering` has been removed from the DeviceType serializer
* Added a `description` field to the CircuitTermination serializer
* Added `ipaddress_count` to InterfaceSerializer to show the count of assigned IP addresses for each interface
* The `available-prefixes` and `available-ips` IPAM endpoints now return an HTTP 204 response instead of HTTP 400 when no new objects can be created
* Filtering on null values now uses the string `null` instead of zero
---
v2.4.9 (2018-12-07)
## Enhancements

View File

@@ -16,8 +16,6 @@ or join us in the #netbox Slack channel on [NetworkToCode](https://networktocode
### Build Status
NetBox is built against both Python 2.7 and 3.5. Python 3.5 or higher is strongly recommended.
| | status |
|-------------|------------|
| **master** | [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=master)](https://travis-ci.org/digitalocean/netbox) |

View File

@@ -1,25 +1,72 @@
# django-filter-1.1.0 breaks with Django-2.1
Django>=1.11,<2.1
# The Python web framework on which NetBox is built
# https://github.com/django/django
Django
# Django middleware which permits cross-domain API requests
# https://github.com/OttoYiu/django-cors-headers
django-cors-headers
# Runtime UI tool for debugging Django
# https://github.com/jazzband/django-debug-toolbar
django-debug-toolbar
# django-filter-2.0.0 drops Python 2 support (blocked by #2000)
django-filter==1.1.0
# Library for writing reusable URL query filters
# https://github.com/carltongibson/django-filter
django-filter
# Modified Preorder Tree Traversal (recursive nesting of objects)
# https://github.com/django-mptt/django-mptt
django-mptt
# Abstraction models for rendering and paginating HTML tables
# https://github.com/jieter/django-tables2
django-tables2
# User-defined tags for objects
# https://github.com/alex/django-taggit
django-taggit
# A Django REST Framework serializer which represents tags
# https://github.com/glemmaPaul/django-taggit-serializer
django-taggit-serializer
# A Django field for representing time zones
# https://github.com/mfogel/django-timezone-field/
django-timezone-field
# https://github.com/encode/django-rest-framework/issues/6053
djangorestframework==3.8.1
# A REST API framework for Django projects
# https://github.com/encode/django-rest-framework
djangorestframework
# Swagger/OpenAPI schema generation for REST APIs
# https://github.com/axnsan12/drf-yasg
drf-yasg[validation]
# Python interface to the graphviz graph rendering utility
# https://github.com/xflr6/graphviz
graphviz
Markdown
natsort
ncclient
# Simple markup language for rendering HTML
# https://github.com/Python-Markdown/markdown
# py-gfm requires Markdown<3.0
Markdown<3.0
# Library for manipulating IP prefixes and addresses
# https://github.com/drkjam/netaddr
netaddr
paramiko
# Fork of PIL (Python Imaging Library) for image processing
# https://github.com/python-pillow/Pillow
Pillow
# PostgreSQL database adapter for Python
# https://github.com/psycopg/psycopg2
psycopg2-binary
# GitHub-flavored Markdown extensions
# https://github.com/zopieux/py-gfm
py-gfm
# Extensive cryptographic library (fork of pycrypto)
# https://github.com/Legrandin/pycryptodome
pycryptodome
xmltodict

View File

@@ -44,7 +44,7 @@ class DeviceConnectionsReport(Report):
# Check that every console port for every active device has a connection defined.
for console_port in ConsolePort.objects.select_related('device').filter(device__status=DEVICE_STATUS_ACTIVE):
if console_port.cs_port is None:
if console_port.connected_endpoint is None:
self.log_failure(
console_port.device,
"No console connection defined for {}".format(console_port.name)
@@ -63,7 +63,7 @@ class DeviceConnectionsReport(Report):
for device in Device.objects.filter(status=DEVICE_STATUS_ACTIVE):
connected_ports = 0
for power_port in PowerPort.objects.filter(device=device):
if power_port.power_outlet is not None:
if power_port.connected_endpoint is not None:
connected_ports += 1
if power_port.connection_status == CONNECTION_STATUS_PLANNED:
self.log_warning(

View File

@@ -9,7 +9,7 @@ This will launch a customized version of [the built-in Django shell](https://doc
```
$ ./manage.py nbshell
### NetBox interactive shell (jstretch-laptop)
### Python 2.7.6 | Django 1.11.3 | NetBox 2.1.0-dev
### Python 3.5.2 | Django 2.0.8 | NetBox 2.4.3
### lsmodels() will show available models. Use help(<model>) for more info.
```

View File

@@ -104,7 +104,7 @@ The base serializer is used to represent the default view of a model. This inclu
}
```
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. When performing write api actions (`POST`, `PUT`, and `PATCH`), any `ForeignKey` relationships do not use the nested serializer, instead you will pass just the integer ID of the related model.
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. When performing write api actions (`POST`, `PUT`, and `PATCH`), any `ForeignKey` relationships do not use the nested serializer, instead you will pass just the integer ID of the related model.
When a base serializer includes one or more nested serializers, the hierarchical structure precludes it from being used for write operations. Thus, a flat representation of an object may be provided using a writable serializer. This serializer includes only raw database values and is not typically used for retrieval, except as part of the response to the creation or updating of an object.
@@ -122,6 +122,52 @@ When a base serializer includes one or more nested serializers, the hierarchical
}
```
## Brief Format
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of the objects themselves without any related data, such as when populating a drop-down list in a form.
For example, the default (complete) format of an IP address looks like this:
```
GET /api/ipam/prefixes/13980/
{
"id": 13980,
"family": 4,
"prefix": "192.0.2.0/24",
"site": null,
"vrf": null,
"tenant": null,
"vlan": null,
"status": {
"value": 1,
"label": "Active"
},
"role": null,
"is_pool": false,
"description": "",
"tags": [],
"custom_fields": {},
"created": "2018-12-11",
"last_updated": "2018-12-11T16:27:55.073174-05:00"
}
```
The brief format is much more terse, but includes a link to the object's full representation:
```
GET /api/ipam/prefixes/13980/?brief=1
{
"id": 13980,
"url": "https://netbox/api/ipam/prefixes/13980/",
"family": 4,
"prefix": "192.0.2.0/24"
}
```
The brief format is supported for both lists and individual objects.
## Static Choice Fields
Some model fields, such as the `status` field in the above example, utilize static integers corresponding to static choices. The available choices can be retrieved from the read-only `_choices` endpoint within each app. A specific `model:field` tuple may optionally be specified in the URL.

View File

@@ -133,6 +133,14 @@ Setting this to True will permit only authenticated users to access any part of
---
## LOGIN_TIMEOUT
Default: 1209600 seconds (14 days)
The liftetime (in seconds) of the authentication cookie issued to a NetBox user upon login.
---
## MAINTENANCE_MODE
Default: False
@@ -223,6 +231,14 @@ The file path to the location where custom reports will be kept. By default, thi
---
## SESSION_FILE_PATH
Default: None
Session data is used to track authenticated users when they access NetBox. By default, NetBox stores session data in the PostgreSQL database. However, this inhibits authentication to a standby instance of NetBox without write access to the database. Alternatively, a local file path may be specified here and NetBox will store session data as files instead of using the database. Note that the user as which NetBox runs must have read and write permissions to this path.
---
## TIME_ZONE
Default: UTC

View File

@@ -25,7 +25,7 @@ Circuit types are fully customizable.
A circuit may have one or two terminations, annotated as the "A" and "Z" sides of the circuit. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites.
Each circuit termination is tied to a site, and optionally to a specific device and interface within that site. Each termination can be assigned a separate downstream and upstream speed independent from one another. Fields are also available to track cross-connect and patch panel details.
Each circuit termination is tied to a site, and may optionally be connected via a cable to a specific device interface or pass-through port. Each termination can be assigned a separate downstream and upstream speed independent from one another. Fields are also available to track cross-connect and patch panel details.
!!! note
A circuit represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit.

View File

@@ -4,12 +4,6 @@ A device type represents a particular make and model of hardware that exists in
Device types are instantiated as devices installed within racks. For example, you might define a device type to represent a Juniper EX4300-48T network switch with 48 Ethernet interfaces. You can then create multiple devices of this type named "switch1," "switch2," and so on. Each device will inherit the components (such as interfaces) of its device type at the time of creation. (However, changes made to a device type will **not** apply to instances of that device type retroactively.)
The device type model includes three flags which inform what type of components may be added to it:
* `is_console_server`: This device type has console server ports
* `is_pdu`: This device type has power outlets
* `is_network_device`: This device type has network interfaces
Some devices house child devices which share physical resources, like space and power, but which functional independently from one another. A common example of this is blade server chassis. Each device type is designated as one of the following:
* A parent device (which has device bays)
@@ -32,6 +26,8 @@ Each device type is assigned a number of component templates which define the ph
* Power ports
* Power outlets
* Network interfaces
* Front ports
* Rear ports
* Device bays (which house child devices)
Whenever a new device is created, its components are automatically created per the templates assigned to its device type. For example, a Juniper EX4300-48T device type might have the following component templates defined:
@@ -56,32 +52,28 @@ When assigning a multi-U device to a rack, it is considered to be mounted in the
A device is said to be full depth if its installation on one rack face prevents the installation of any other device on the opposite face within the same rack unit(s). This could be either because the device is physically too deep to allow a device behind it, or because the installation of an opposing device would impede airflow.
## Device Roles
## Device Components
Devices can be organized by functional roles. These roles are fully customizable. For example, you might create roles for core switches, distribution switches, and access switches.
---
# Device Components
There are six types of device components which comprise all of the interconnection logic with NetBox:
There are eight types of device components which comprise all of the interconnection logic with NetBox:
* Console ports
* Console server ports
* Power ports
* Power outlets
* Network interfaces
* Front ports
* Rear ports
* Device bays
## Console
### Console
Console ports connect only to console server ports. Console connections can be marked as either *planned* or *connected*.
## Power
### Power
Power ports connect only to power outlets. Power connections can be marked as either *planned* or *connected*.
## Interfaces
### Interfaces
Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. Each type of connection can be classified as either *planned* or *connected*.
@@ -91,10 +83,20 @@ Each interface can also be enabled or disabled, and optionally designated as man
VLANs can be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.)
## Device Bays
### Pass-through Ports
Pass-through ports are used to model physical terminations which comprise part of a longer path, such as a cable terminated to a patch panel. Each front port maps to a position on a rear port. A 24-port UTP patch panel, for instance, would have 24 front ports and 24 rear ports. Although this relationship is typically one-to-one, a rear port may have multiple front ports mapped to it. This can be useful for modeling instances where multiple paths share a common cable (for example, six different fiber connections sharing a 12-strand MPO cable).
Pass-through ports can also be used to model "bump in the wire" devices, such as a media convertor or passive tap.
### Device Bays
Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
## Device Roles
Devices can be organized by functional roles. These roles are fully customizable. For example, you might create roles for core switches, distribution switches, and access switches.
---
# Platforms
@@ -118,3 +120,25 @@ Inventory items represent hardware components installed within a device, such as
A virtual chassis represents a set of devices which share a single control plane: a stack of switches which are managed as a single device, for example. Each device in the virtual chassis is assigned a position and (optionally) a priority. Exactly one device is designated the virtual chassis master: This device will typically be assigned a name, secrets, services, and other attributes related to its management.
It's important to recognize the distinction between a virtual chassis and a chassis-based device. For instance, a virtual chassis is not used to model a chassis switch with removable line cards such as the Juniper EX9208, as its line cards are _not_ physically separate devices capable of operating independently.
---
# Cables
A cable represents a physical connection between two termination points, such as between a console port and a patch panel port, or between two network interfaces. Cables can be traced through pass-through ports to form a complete path between two endpoints. In the example below, three individual cables comprise a path between the two connected endpoints.
```
|<------------------------------------------ Cable Path ------------------------------------------->|
Device A Patch Panel A Patch Panel B Device B
+-----------+ +-------------+ +-------------+ +-----------+
| Interface | --- Cable --- | Front Port | | Front Port | --- Cable --- | Interface |
+-----------+ +-------------+ +-------------+ +-----------+
+-------------+ +-------------+
| Rear Port | --- Cable --- | Rear Port |
+-------------+ +-------------+
```
All connections between device components in NetBox are represented using cables. However, defining the actual cable plant is optional: Components can be be directly connected using cables with no type or other attributes assigned.
Cables are also used to associated ports and interfaces with circuit terminations. To do this, first create the circuit termination, then navigate the desired component and connect a cable between the two.

View File

@@ -1,7 +1,7 @@
NetBox requires a PostgreSQL database to store data. This can be hosted locally or on a remote server. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/current/static/datatype-net-types.html).)
!!! note
The installation instructions provided here have been tested to work on Ubuntu 16.04 and CentOS 7.4. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
The installation instructions provided here have been tested to work on Ubuntu 18.04 and CentOS 7.5. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
!!! warning
NetBox v2.2 and later requires PostgreSQL 9.4 or higher.
@@ -19,7 +19,7 @@ If a recent enough version of PostgreSQL is not available through your distribut
**CentOS**
CentOS 7.4 does not ship with a recent enough version of PostgreSQL, so it will need to be installed from an external repository. The instructions below show the installation of PostgreSQL 9.6.
CentOS 7.5 does not ship with a recent enough version of PostgreSQL, so it will need to be installed from an external repository. The instructions below show the installation of PostgreSQL 9.6.
```no-highlight
# yum install https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm

View File

@@ -5,16 +5,16 @@ This section of the documentation discusses installing and configuring the NetBo
**Ubuntu**
```no-highlight
# apt-get install -y python3 python3-dev python3-setuptools build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
# easy_install3 pip
# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
```
**CentOS**
```no-highlight
# yum install -y epel-release
# yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
# easy_install-3.4 pip
# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
# easy_install-3.6 pip
# ln -s /usr/bin/python36 /usr/bin/python3
```
You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.

View File

@@ -1,7 +1,7 @@
We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence.
!!! info
For the sake of brevity, only Ubuntu 16.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.
For the sake of brevity, only Ubuntu 18.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.
# Web Server Installation

View File

@@ -19,7 +19,7 @@ sudo yum install -y openldap-devel
## Install django-auth-ldap
```no-highlight
sudo pip install django-auth-ldap
pip3 install django-auth-ldap
```
# Configuration
@@ -95,6 +95,9 @@ AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()
# Define a group required to login.
AUTH_LDAP_REQUIRE_GROUP = "CN=NETBOX_USERS,DC=example,DC=com"
# Mirror LDAP group assignments.
AUTH_LDAP_MIRROR_GROUPS = True
# Define special user types using groups. Exercise great caution when assigning superuser status.
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
"is_active": "cn=active,ou=groups,dc=example,dc=com",
@@ -113,3 +116,21 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
* `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
# Troubleshooting LDAP
`supervisorctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/supervisor/`.
For troubleshooting LDAP user/group queries, add the following lines to the start of `ldap_config.py` after `import ldap`.
```python
import logging, logging.handlers
logfile = "/opt/netbox/logs/django-ldap-debug.log"
my_logger = logging.getLogger('django_auth_ldap')
my_logger.setLevel(logging.DEBUG)
handler = logging.handlers.RotatingFileHandler(
logfile, maxBytes=1024 * 500, backupCount=5)
my_logger.addHandler(handler)
```
Ensure the file and path specified in logfile exist and are writable and executable by the application service account. Restart the netbox service and attempt to log into the site to trigger log entries to this file.

View File

@@ -11,4 +11,4 @@ The following sections detail how to set up a new instance of NetBox:
If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md).
NetBox v2.5 and later requires Python 3. Please see the instruction for [migrating to Python 3](migrating-to-python3.md) if you are still using Python 2.
NetBox v2.5 and later requires Python 3.5 or higher. Please see the instructions for [migrating to Python 3](migrating-to-python3.md) if you are still using Python 2.

View File

@@ -64,13 +64,6 @@ Once the new code is in place, run the upgrade script (which may need to be run
# ./upgrade.sh
```
!!! warning
The upgrade script will prefer Python3 and pip3 if both executables are available. To force it to use Python2 and pip, use the `-2` argument as below. Note that Python 2 will no longer be supported in NetBox v2.5.
```no-highlight
# ./upgrade.sh -2
```
This script:
* Installs or upgrades any new required Python packages

View File

@@ -0,0 +1,52 @@
from rest_framework import serializers
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from utilities.api import WritableNestedSerializer
__all__ = [
'NestedCircuitSerializer',
'NestedCircuitTerminationSerializer',
'NestedCircuitTypeSerializer',
'NestedProviderSerializer',
]
#
# Providers
#
class NestedProviderSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
class Meta:
model = Provider
fields = ['id', 'url', 'name', 'slug']
#
# Circuits
#
class NestedCircuitTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
class Meta:
model = CircuitType
fields = ['id', 'url', 'name', 'slug']
class NestedCircuitSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
class Meta:
model = Circuit
fields = ['id', 'url', 'cid']
class NestedCircuitTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = NestedCircuitSerializer()
class Meta:
model = CircuitTermination
fields = ['id', 'url', 'circuit', 'term_side']

View File

@@ -1,14 +1,13 @@
from __future__ import unicode_literals
from rest_framework import serializers
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
from circuits.constants import CIRCUIT_STATUS_CHOICES
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.api.serializers import NestedInterfaceSerializer, NestedSiteSerializer
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
from dcim.api.serializers import ConnectedEndpointSerializer
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer
from utilities.api import ChoiceField, ValidatedModelSerializer
from .nested_serializers import *
#
@@ -26,16 +25,8 @@ class ProviderSerializer(TaggitSerializer, CustomFieldModelSerializer):
]
class NestedProviderSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
class Meta:
model = Provider
fields = ['id', 'url', 'name', 'slug']
#
# Circuit types
# Circuits
#
class CircuitTypeSerializer(ValidatedModelSerializer):
@@ -45,18 +36,6 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug']
class NestedCircuitTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
class Meta:
model = CircuitType
fields = ['id', 'url', 'name', 'slug']
#
# Circuits
#
class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
provider = NestedProviderSerializer()
status = ChoiceField(choices=CIRCUIT_STATUS_CHOICES, required=False)
@@ -72,25 +51,14 @@ class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
]
class NestedCircuitSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
class Meta:
model = Circuit
fields = ['id', 'url', 'cid']
#
# Circuit Terminations
#
class CircuitTerminationSerializer(ValidatedModelSerializer):
class CircuitTerminationSerializer(ConnectedEndpointSerializer):
circuit = NestedCircuitSerializer()
site = NestedSiteSerializer()
interface = NestedInterfaceSerializer(required=False, allow_null=True)
cable = NestedCableSerializer(read_only=True)
class Meta:
model = CircuitTermination
fields = [
'id', 'circuit', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
'id', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
]

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
from rest_framework import routers
from . import views
@@ -17,7 +15,7 @@ router = routers.DefaultRouter()
router.APIRootView = CircuitsRootView
# Field choices
router.register(r'_choices', views.CircuitsFieldChoicesViewSet, base_name='field-choice')
router.register(r'_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice')
# Providers
router.register(r'providers', views.ProviderViewSet)

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.shortcuts import get_object_or_404
from rest_framework.decorators import action
from rest_framework.response import Response
@@ -31,7 +29,7 @@ class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
class ProviderViewSet(CustomFieldModelViewSet):
queryset = Provider.objects.prefetch_related('tags')
serializer_class = serializers.ProviderSerializer
filter_class = filters.ProviderFilter
filterset_class = filters.ProviderFilter
@action(detail=True)
def graphs(self, request, pk=None):
@@ -51,7 +49,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
class CircuitTypeViewSet(ModelViewSet):
queryset = CircuitType.objects.all()
serializer_class = serializers.CircuitTypeSerializer
filter_class = filters.CircuitTypeFilter
filterset_class = filters.CircuitTypeFilter
#
@@ -61,7 +59,7 @@ class CircuitTypeViewSet(ModelViewSet):
class CircuitViewSet(CustomFieldModelViewSet):
queryset = Circuit.objects.select_related('type', 'tenant', 'provider').prefetch_related('tags')
serializer_class = serializers.CircuitSerializer
filter_class = filters.CircuitFilter
filterset_class = filters.CircuitFilter
#
@@ -69,6 +67,8 @@ class CircuitViewSet(CustomFieldModelViewSet):
#
class CircuitTerminationViewSet(ModelViewSet):
queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device')
queryset = CircuitTermination.objects.select_related(
'circuit', 'site', 'connected_endpoint__device', 'cable'
)
serializer_class = serializers.CircuitTerminationSerializer
filter_class = filters.CircuitTerminationFilter
filterset_class = filters.CircuitTerminationFilter

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.apps import AppConfig

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
# Circuit statuses
CIRCUIT_STATUS_DEPROVISIONING = 0

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
import django_filters
from django.db.models import Q
@@ -12,18 +10,21 @@ from .models import Provider, Circuit, CircuitTermination, CircuitType
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter(
method='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='circuits__terminations__site',
field_name='circuits__terminations__site',
queryset=Site.objects.all(),
label='Site',
)
site = django_filters.ModelMultipleChoiceFilter(
name='circuits__terminations__site__slug',
field_name='circuits__terminations__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
@@ -54,7 +55,10 @@ class CircuitTypeFilter(django_filters.FilterSet):
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -64,7 +68,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Provider (ID)',
)
provider = django_filters.ModelMultipleChoiceFilter(
name='provider__slug',
field_name='provider__slug',
queryset=Provider.objects.all(),
to_field_name='slug',
label='Provider (slug)',
@@ -74,7 +78,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Circuit type (ID)',
)
type = django_filters.ModelMultipleChoiceFilter(
name='type__slug',
field_name='type__slug',
queryset=CircuitType.objects.all(),
to_field_name='slug',
label='Circuit type (slug)',
@@ -88,18 +92,18 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant__slug',
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='terminations__site',
field_name='terminations__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='terminations__site__slug',
field_name='terminations__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
@@ -117,6 +121,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
Q(cid__icontains=value) |
Q(terminations__xconnect_id__icontains=value) |
Q(terminations__pp_info__icontains=value) |
Q(terminations__description__icontains=value) |
Q(description__icontains=value) |
Q(comments__icontains=value)
).distinct()
@@ -136,7 +141,7 @@ class CircuitTerminationFilter(django_filters.FilterSet):
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site__slug',
field_name='site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
@@ -152,5 +157,6 @@ class CircuitTerminationFilter(django_filters.FilterSet):
return queryset.filter(
Q(circuit__cid__icontains=value) |
Q(xconnect_id__icontains=value) |
Q(pp_info__icontains=value)
Q(pp_info__icontains=value) |
Q(description__icontains=value)
).distinct()

View File

@@ -1,16 +1,14 @@
from __future__ import unicode_literals
from django import forms
from django.db.models import Count
from taggit.forms import TagField
from dcim.models import Site, Device, Interface, Rack
from dcim.models import Site
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
AnnotatedMultipleChoiceField, APISelect, add_blank_choice, BootstrapMixin, ChainedFieldsMixin,
ChainedModelChoiceField, CommentField, CSVChoiceField, FilterChoiceField, SmallTextarea, SlugField,
AnnotatedMultipleChoiceField, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, FilterChoiceField,
SmallTextarea, SlugField,
)
from .constants import CIRCUIT_STATUS_CHOICES
from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -23,14 +21,22 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
class ProviderForm(BootstrapMixin, CustomFieldForm):
slug = SlugField()
comments = CommentField()
tags = TagField(required=False)
tags = TagField(
required=False
)
class Meta:
model = Provider
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags']
fields = [
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
]
widgets = {
'noc_contact': SmallTextarea(attrs={'rows': 5}),
'admin_contact': SmallTextarea(attrs={'rows': 5}),
'noc_contact': SmallTextarea(
attrs={'rows': 5}
),
'admin_contact': SmallTextarea(
attrs={'rows': 5}
),
}
help_texts = {
'name': "Full name of the provider",
@@ -56,23 +62,57 @@ class ProviderCSVForm(forms.ModelForm):
class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
asn = forms.IntegerField(required=False, label='ASN')
account = forms.CharField(max_length=30, required=False, label='Account number')
portal_url = forms.URLField(required=False, label='Portal')
noc_contact = forms.CharField(required=False, widget=SmallTextarea, label='NOC contact')
admin_contact = forms.CharField(required=False, widget=SmallTextarea, label='Admin contact')
comments = CommentField(widget=SmallTextarea)
pk = forms.ModelMultipleChoiceField(
queryset=Provider.objects.all(),
widget=forms.MultipleHiddenInput
)
asn = forms.IntegerField(
required=False,
label='ASN'
)
account = forms.CharField(
max_length=30,
required=False,
label='Account number'
)
portal_url = forms.URLField(
required=False,
label='Portal'
)
noc_contact = forms.CharField(
required=False,
widget=SmallTextarea,
label='NOC contact'
)
admin_contact = forms.CharField(
required=False,
widget=SmallTextarea,
label='Admin contact'
)
comments = CommentField(
widget=SmallTextarea()
)
class Meta:
nullable_fields = ['asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
nullable_fields = [
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
]
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Provider
q = forms.CharField(required=False, label='Search')
site = FilterChoiceField(queryset=Site.objects.all(), to_field_name='slug')
asn = forms.IntegerField(required=False, label='ASN')
q = forms.CharField(
required=False,
label='Search'
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug'
)
asn = forms.IntegerField(
required=False,
label='ASN'
)
#
@@ -84,7 +124,9 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = CircuitType
fields = ['name', 'slug']
fields = [
'name', 'slug',
]
class CircuitTypeCSVForm(forms.ModelForm):
@@ -104,7 +146,9 @@ class CircuitTypeCSVForm(forms.ModelForm):
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
comments = CommentField()
tags = TagField(required=False)
tags = TagField(
required=False
)
class Meta:
model = Circuit
@@ -159,28 +203,61 @@ class CircuitCSVForm(forms.ModelForm):
class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
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)
status = forms.ChoiceField(choices=add_blank_choice(CIRCUIT_STATUS_CHOICES), required=False, initial='')
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
description = forms.CharField(max_length=100, required=False)
comments = CommentField(widget=SmallTextarea)
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
)
status = forms.ChoiceField(
choices=add_blank_choice(CIRCUIT_STATUS_CHOICES),
required=False,
initial=''
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False
)
commit_rate = forms.IntegerField(
required=False,
label='Commit rate (Kbps)'
)
description = forms.CharField(
max_length=100,
required=False
)
comments = CommentField(
widget=SmallTextarea
)
class Meta:
nullable_fields = ['tenant', 'commit_rate', 'description', 'comments']
nullable_fields = [
'tenant', 'commit_rate', 'description', 'comments',
]
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Circuit
q = forms.CharField(required=False, label='Search')
q = forms.CharField(
required=False,
label='Search'
)
type = FilterChoiceField(
queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
queryset=CircuitType.objects.annotate(
filter_count=Count('circuits')
),
to_field_name='slug'
)
provider = FilterChoiceField(
queryset=Provider.objects.annotate(filter_count=Count('circuits')),
queryset=Provider.objects.annotate(
filter_count=Count('circuits')
),
to_field_name='slug'
)
status = AnnotatedMultipleChoiceField(
@@ -190,74 +267,35 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
required=False
)
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
queryset=Tenant.objects.annotate(
filter_count=Count('circuits')
),
to_field_name='slug',
null_label='-- None --'
)
site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
queryset=Site.objects.annotate(
filter_count=Count('circuit_terminations')
),
to_field_name='slug'
)
commit_rate = forms.IntegerField(required=False, min_value=0, label='Commit rate (Kbps)')
commit_rate = forms.IntegerField(
required=False,
min_value=0,
label='Commit rate (Kbps)'
)
#
# Circuit terminations
#
class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
widget=forms.Select(
attrs={'filter-for': 'rack'}
)
)
rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains=(
('site', 'site'),
),
required=False,
label='Rack',
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}',
attrs={'filter-for': 'device', 'nullable': 'true'}
)
)
device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains=(
('site', 'site'),
('rack', 'rack'),
),
required=False,
label='Device',
widget=APISelect(
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
display_field='display_name',
attrs={'filter-for': 'interface'}
)
)
interface = ChainedModelChoiceField(
queryset=Interface.objects.connectable().select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
),
chains=(
('device', 'device'),
),
required=False,
label='Interface',
widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{device}}&type=physical',
disabled_indicator='is_connected'
)
)
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = CircuitTermination
fields = [
'term_side', 'site', 'rack', 'device', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id',
'pp_info',
'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description',
]
help_texts = {
'port_speed': "Physical circuit speed",
@@ -267,25 +305,3 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
widgets = {
'term_side': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
if instance and instance.interface is not None:
initial = kwargs.get('initial', {}).copy()
initial['rack'] = instance.interface.device.rack
initial['device'] = instance.interface.device
kwargs['initial'] = initial
super(CircuitTerminationForm, self).__init__(*args, **kwargs)
# Mark connected interfaces as disabled
self.fields['interface'].choices = []
for iface in self.fields['interface'].queryset:
self.fields['interface'].choices.append(
(iface.id, {
'label': iface.name,
'disabled': iface.is_connected and iface.pk != self.initial.get('interface'),
})
)

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-22 18:21
from __future__ import unicode_literals
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.14 on 2018-07-31 02:25
from __future__ import unicode_literals
import dcim.fields
from django.db import migrations, models
import django.db.models.deletion

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-22 18:21
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion

View File

@@ -1,7 +1,5 @@
# -*- 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

View File

@@ -1,7 +1,5 @@
# -*- 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

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-08 20:24
from __future__ import unicode_literals
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-12-13 16:30
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-17 20:08
from __future__ import unicode_literals
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-04-19 17:17
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-05-24 15:34
from __future__ import unicode_literals
import dcim.fields
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.9 on 2018-02-06 18:48
from __future__ import unicode_literals
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-05-22 19:04
from __future__ import unicode_literals
from django.db import migrations
import taggit.managers

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-06-13 17:14
from __future__ import unicode_literals
from django.db import migrations, models

View File

@@ -0,0 +1,89 @@
import sys
from django.db import migrations, models
import django.db.models.deletion
from dcim.constants import CONNECTION_STATUS_CONNECTED
def circuit_terminations_to_cables(apps, schema_editor):
"""
Copy all existing CircuitTermination Interface associations as Cables
"""
ContentType = apps.get_model('contenttypes', 'ContentType')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
Interface = apps.get_model('dcim', 'Interface')
Cable = apps.get_model('dcim', 'Cable')
# Load content types
circuittermination_type = ContentType.objects.get_for_model(CircuitTermination)
interface_type = ContentType.objects.get_for_model(Interface)
# Create a new Cable instance from each console connection
if 'test' not in sys.argv:
print("\n Adding circuit terminations... ", end='', flush=True)
for circuittermination in CircuitTermination.objects.filter(interface__isnull=False):
# Create the new Cable
cable = Cable.objects.create(
termination_a_type=circuittermination_type,
termination_a_id=circuittermination.id,
termination_b_type=interface_type,
termination_b_id=circuittermination.interface_id,
status=CONNECTION_STATUS_CONNECTED
)
# Cache the Cable on its two termination points
CircuitTermination.objects.filter(pk=circuittermination.pk).update(
cable=cable,
connected_endpoint=circuittermination.interface,
connection_status=CONNECTION_STATUS_CONNECTED
)
# Cache the connected Cable on the Interface
Interface.objects.filter(pk=circuittermination.interface_id).update(
cable=cable,
_connected_circuittermination=circuittermination,
connection_status=CONNECTION_STATUS_CONNECTED
)
cable_count = Cable.objects.filter(termination_a_type=circuittermination_type).count()
if 'test' not in sys.argv:
print("{} cables created".format(cable_count))
class Migration(migrations.Migration):
atomic = False
dependencies = [
('circuits', '0012_change_logging'),
('dcim', '0066_cables'),
]
operations = [
# Add new CircuitTermination fields
migrations.AddField(
model_name='circuittermination',
name='connected_endpoint',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Interface'),
),
migrations.AddField(
model_name='circuittermination',
name='connection_status',
field=models.NullBooleanField(),
),
migrations.AddField(
model_name='circuittermination',
name='cable',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Cable'),
),
# Copy CircuitTermination connections to Interfaces as Cables
migrations.RunPython(circuit_terminations_to_cables),
# Remove interface field from CircuitTermination
migrations.RemoveField(
model_name='circuittermination',
name='interface',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.1.3 on 2018-11-05 18:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0013_cables'),
]
operations = [
migrations.AddField(
model_name='circuittermination',
name='description',
field=models.CharField(blank=True, max_length=100),
),
]

View File

@@ -1,20 +1,17 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible
from taggit.managers import TaggableManager
from dcim.constants import STATUS_CLASSES
from dcim.constants import CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, STATUS_CLASSES
from dcim.fields import ASNField
from dcim.models import CableTermination
from extras.models import CustomFieldModel, ObjectChange
from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object
from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
@python_2_unicode_compatible
class Provider(ChangeLoggedModel, CustomFieldModel):
"""
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
@@ -84,7 +81,6 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
)
@python_2_unicode_compatible
class CircuitType(ChangeLoggedModel):
"""
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
@@ -116,12 +112,11 @@ class CircuitType(ChangeLoggedModel):
)
@python_2_unicode_compatible
class Circuit(ChangeLoggedModel, CustomFieldModel):
"""
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device
interface, but this is not required. Circuit port speed and commit rate are measured in Kbps.
circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
in Kbps.
"""
cid = models.CharField(
max_length=50,
@@ -181,7 +176,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
unique_together = ['provider', 'cid']
def __str__(self):
return '{} {}'.format(self.provider, self.cid)
return self.cid
def get_absolute_url(self):
return reverse('circuits:circuit', args=[self.pk])
@@ -217,8 +212,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
return self._get_termination('Z')
@python_2_unicode_compatible
class CircuitTermination(models.Model):
class CircuitTermination(CableTermination):
circuit = models.ForeignKey(
to='circuits.Circuit',
on_delete=models.CASCADE,
@@ -234,13 +228,17 @@ class CircuitTermination(models.Model):
on_delete=models.PROTECT,
related_name='circuit_terminations'
)
interface = models.OneToOneField(
connected_endpoint = models.OneToOneField(
to='dcim.Interface',
on_delete=models.PROTECT,
related_name='circuit_termination',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True
)
connection_status = models.NullBooleanField(
choices=CONNECTION_STATUS_CHOICES,
blank=True
)
port_speed = models.PositiveIntegerField(
verbose_name='Port speed (Kbps)'
)
@@ -260,13 +258,17 @@ class CircuitTermination(models.Model):
blank=True,
verbose_name='Patch panel/port(s)'
)
description = models.CharField(
max_length=100,
blank=True
)
class Meta:
ordering = ['circuit', 'term_side']
unique_together = ['circuit', 'term_side']
def __str__(self):
return '{} (Side {})'.format(self.circuit, self.get_term_side_display())
return 'Side {}'.format(self.get_term_side_display())
def log_change(self, user, request_id, action):
"""

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django.utils import timezone

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
import django_tables2 as tables
from django.utils.safestring import mark_safe
from django_tables2.utils import Accessor
@@ -25,12 +23,6 @@ STATUS_LABEL = """
class CircuitTerminationColumn(tables.Column):
def render(self, value):
if value.interface:
return mark_safe('<a href="{}" title="{}">{}</a>'.format(
value.interface.device.get_absolute_url(),
value.site,
value.interface.device
))
return mark_safe('<a href="{}">{}</a>'.format(
value.site.get_absolute_url(),
value.site

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.urls import reverse
from rest_framework import status
@@ -15,7 +13,7 @@ class ProviderTest(APITestCase):
def setUp(self):
super(ProviderTest, self).setUp()
super().setUp()
self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
@@ -137,7 +135,7 @@ class CircuitTypeTest(APITestCase):
def setUp(self):
super(CircuitTypeTest, self).setUp()
super().setUp()
self.circuittype1 = CircuitType.objects.create(name='Test Circuit Type 1', slug='test-circuit-type-1')
self.circuittype2 = CircuitType.objects.create(name='Test Circuit Type 2', slug='test-circuit-type-2')
@@ -212,7 +210,7 @@ class CircuitTest(APITestCase):
def setUp(self):
super(CircuitTest, self).setUp()
super().setUp()
self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
@@ -328,46 +326,26 @@ class CircuitTerminationTest(APITestCase):
def setUp(self):
super(CircuitTerminationTest, self).setUp()
super().setUp()
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_network_device=True
)
devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
)
device1 = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='Test Device 1', site=self.site1
)
device2 = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='Test Device 2', site=self.site2
)
self.interface1 = Interface.objects.create(device=device1, name='Test Interface 1')
self.interface2 = Interface.objects.create(device=device2, name='Test Interface 2')
self.interface3 = Interface.objects.create(device=device1, name='Test Interface 3')
self.interface4 = Interface.objects.create(device=device2, name='Test Interface 4')
self.interface5 = Interface.objects.create(device=device1, name='Test Interface 5')
self.interface6 = Interface.objects.create(device=device2, name='Test Interface 6')
provider = Provider.objects.create(name='Test Provider', slug='test-provider')
circuittype = CircuitType.objects.create(name='Test Circuit Type', slug='test-circuit-type')
self.circuit1 = Circuit.objects.create(cid='TEST0001', provider=provider, type=circuittype)
self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype)
self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
self.circuittermination1 = CircuitTermination.objects.create(
circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, interface=self.interface1, port_speed=1000000
circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
)
self.circuittermination2 = CircuitTermination.objects.create(
circuit=self.circuit1, term_side=TERM_SIDE_Z, site=self.site2, interface=self.interface2, port_speed=1000000
circuit=self.circuit1, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000
)
self.circuittermination3 = CircuitTermination.objects.create(
circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, interface=self.interface3, port_speed=1000000
circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
)
self.circuittermination4 = CircuitTermination.objects.create(
circuit=self.circuit2, term_side=TERM_SIDE_Z, site=self.site2, interface=self.interface4, port_speed=1000000
circuit=self.circuit2, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000
)
def test_get_circuittermination(self):
@@ -390,7 +368,6 @@ class CircuitTerminationTest(APITestCase):
'circuit': self.circuit3.pk,
'term_side': TERM_SIDE_A,
'site': self.site1.pk,
'interface': self.interface5.pk,
'port_speed': 1000000,
}
@@ -403,20 +380,18 @@ class CircuitTerminationTest(APITestCase):
self.assertEqual(circuittermination4.circuit_id, data['circuit'])
self.assertEqual(circuittermination4.term_side, data['term_side'])
self.assertEqual(circuittermination4.site_id, data['site'])
self.assertEqual(circuittermination4.interface_id, data['interface'])
self.assertEqual(circuittermination4.port_speed, data['port_speed'])
def test_update_circuittermination(self):
circuittermination5 = CircuitTermination.objects.create(
circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, interface=self.interface5, port_speed=1000000
circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
)
data = {
'circuit': self.circuit3.pk,
'term_side': TERM_SIDE_Z,
'site': self.site2.pk,
'interface': self.interface6.pk,
'port_speed': 1000000,
}
@@ -428,7 +403,6 @@ class CircuitTerminationTest(APITestCase):
circuittermination1 = CircuitTermination.objects.get(pk=response.data['id'])
self.assertEqual(circuittermination1.term_side, data['term_side'])
self.assertEqual(circuittermination1.site_id, data['site'])
self.assertEqual(circuittermination1.interface_id, data['interface'])
self.assertEqual(circuittermination1.port_speed, data['port_speed'])
def test_delete_circuittermination(self):

View File

@@ -1,10 +1,9 @@
from __future__ import unicode_literals
from django.conf.urls import url
from dcim.views import CableCreateView, CableTraceView
from extras.views import ObjectChangeLogView
from . import views
from .models import Circuit, CircuitType, Provider
from .models import Circuit, CircuitTermination, CircuitType, Provider
app_name = 'circuits'
urlpatterns = [
@@ -44,5 +43,7 @@ urlpatterns = [
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
url(r'^circuit-terminations/(?P<pk>\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
url(r'^circuit-terminations/(?P<termination_a_id>\d+)/connect/$', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
url(r'^circuit-terminations/(?P<pk>\d+)/trace/$', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
]

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin
@@ -134,7 +132,7 @@ class CircuitListView(ObjectListView):
queryset = Circuit.objects.select_related(
'provider', 'type', 'tenant'
).prefetch_related(
'terminations__site', 'terminations__interface__device'
'terminations__site'
)
filter = filters.CircuitFilter
filter_form = forms.CircuitFilterForm
@@ -148,12 +146,12 @@ class CircuitView(View):
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
termination_a = CircuitTermination.objects.select_related(
'site__region', 'interface__device'
'site__region', 'connected_endpoint__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_A
).first()
termination_z = CircuitTermination.objects.select_related(
'site__region', 'interface__device'
'site__region', 'connected_endpoint__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_Z
).first()

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
from rest_framework.exceptions import APIException

View File

@@ -0,0 +1,249 @@
from rest_framework import serializers
from dcim.constants import CONNECTION_STATUS_CHOICES
from dcim.models import (
Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceType, DeviceRole, FrontPort, FrontPortTemplate,
Interface, Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, RearPort, RearPortTemplate,
Region, Site, VirtualChassis,
)
from utilities.api import ChoiceField, WritableNestedSerializer
__all__ = [
'NestedCableSerializer',
'NestedConsolePortSerializer',
'NestedConsoleServerPortSerializer',
'NestedDeviceBaySerializer',
'NestedDeviceRoleSerializer',
'NestedDeviceSerializer',
'NestedDeviceTypeSerializer',
'NestedFrontPortSerializer',
'NestedFrontPortTemplateSerializer',
'NestedInterfaceSerializer',
'NestedManufacturerSerializer',
'NestedPlatformSerializer',
'NestedPowerOutletSerializer',
'NestedPowerPortSerializer',
'NestedRackGroupSerializer',
'NestedRackRoleSerializer',
'NestedRackSerializer',
'NestedRearPortSerializer',
'NestedRearPortTemplateSerializer',
'NestedRegionSerializer',
'NestedSiteSerializer',
'NestedVirtualChassisSerializer',
]
#
# Regions/sites
#
class NestedRegionSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
class Meta:
model = Region
fields = ['id', 'url', 'name', 'slug']
class NestedSiteSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
class Meta:
model = Site
fields = ['id', 'url', 'name', 'slug']
#
# Racks
#
class NestedRackGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
class Meta:
model = RackGroup
fields = ['id', 'url', 'name', 'slug']
class NestedRackRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
class Meta:
model = RackRole
fields = ['id', 'url', 'name', 'slug']
class NestedRackSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
class Meta:
model = Rack
fields = ['id', 'url', 'name', 'display_name']
#
# Device types
#
class NestedManufacturerSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
class Meta:
model = Manufacturer
fields = ['id', 'url', 'name', 'slug']
class NestedDeviceTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True)
class Meta:
model = DeviceType
fields = ['id', 'url', 'manufacturer', 'model', 'slug']
class NestedRearPortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
class Meta:
model = RearPortTemplate
fields = ['id', 'url', 'name']
class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
class Meta:
model = FrontPortTemplate
fields = ['id', 'url', 'name']
#
# Devices
#
class NestedDeviceRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
class Meta:
model = DeviceRole
fields = ['id', 'url', 'name', 'slug']
class NestedPlatformSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
class Meta:
model = Platform
fields = ['id', 'url', 'name', 'slug']
class NestedDeviceSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
class Meta:
model = Device
fields = ['id', 'url', 'name', 'display_name']
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta:
model = ConsoleServerPort
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
class NestedConsolePortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = NestedDeviceSerializer(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta:
model = ConsolePort
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
class NestedPowerOutletSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta:
model = PowerOutlet
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
class NestedPowerPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = NestedDeviceSerializer(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta:
model = PowerPort
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
class NestedInterfaceSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta:
model = Interface
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
class NestedRearPortSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
class Meta:
model = RearPort
fields = ['id', 'url', 'device', 'name', 'cable']
class NestedFrontPortSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
class Meta:
model = FrontPort
fields = ['id', 'url', 'device', 'name', 'cable']
class NestedDeviceBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
device = NestedDeviceSerializer(read_only=True)
class Meta:
model = DeviceBay
fields = ['id', 'url', 'device', 'name']
#
# Cables
#
class NestedCableSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
class Meta:
model = Cable
fields = ['id', 'url', 'label']
#
# Virtual chassis
#
class NestedVirtualChassisSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer()
class Meta:
model = VirtualChassis
fields = ['id', 'url', 'master']

View File

@@ -1,43 +1,58 @@
from __future__ import unicode_literals
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
from circuits.models import Circuit, CircuitTermination
from dcim.constants import (
CONNECTION_STATUS_CHOICES, DEVICE_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_MODE_CHOICES, IFACE_ORDERING_CHOICES,
RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, SITE_STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
)
from dcim.constants import *
from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, VirtualChassis,
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
)
from extras.api.customfields import CustomFieldModelSerializer
from ipam.models import IPAddress, VLAN
from tenancy.api.serializers import NestedTenantSerializer
from users.api.serializers import NestedUserSerializer
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
from ipam.models import VLAN
from tenancy.api.nested_serializers import NestedTenantSerializer
from users.api.nested_serializers import NestedUserSerializer
from utilities.api import (
ChoiceField, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer,
WritableNestedSerializer,
ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer,
WritableNestedSerializer, get_serializer_for_model,
)
from virtualization.models import Cluster
from virtualization.api.nested_serializers import NestedClusterSerializer
from .nested_serializers import *
class ConnectedEndpointSerializer(ValidatedModelSerializer):
connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
connected_endpoint = serializers.SerializerMethodField(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
def get_connected_endpoint_type(self, obj):
if hasattr(obj, 'connected_endpoint') and obj.connected_endpoint is not None:
return '{}.{}'.format(
obj.connected_endpoint._meta.app_label,
obj.connected_endpoint._meta.model_name
)
return None
def get_connected_endpoint(self, obj):
"""
Return the appropriate serializer for the type of connected object.
"""
if getattr(obj, 'connected_endpoint', None) is None:
return None
serializer = get_serializer_for_model(obj.connected_endpoint, prefix='Nested')
context = {'request': self.context['request']}
data = serializer(obj.connected_endpoint, context=context).data
return data
#
# Regions
# Regions/sites
#
class NestedRegionSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
class Meta:
model = Region
fields = ['id', 'url', 'name', 'slug']
class RegionSerializer(serializers.ModelSerializer):
parent = NestedRegionSerializer(required=False, allow_null=True)
@@ -46,10 +61,6 @@ class RegionSerializer(serializers.ModelSerializer):
fields = ['id', 'name', 'slug', 'parent']
#
# Sites
#
class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
status = ChoiceField(choices=SITE_STATUS_CHOICES, required=False)
region = NestedRegionSerializer(required=False, allow_null=True)
@@ -72,16 +83,8 @@ class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
]
class NestedSiteSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
class Meta:
model = Site
fields = ['id', 'url', 'name', 'slug']
#
# Rack groups
# Racks
#
class RackGroupSerializer(ValidatedModelSerializer):
@@ -92,18 +95,6 @@ class RackGroupSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug', 'site']
class NestedRackGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
class Meta:
model = RackGroup
fields = ['id', 'url', 'name', 'slug']
#
# Rack roles
#
class RackRoleSerializer(ValidatedModelSerializer):
class Meta:
@@ -111,32 +102,23 @@ class RackRoleSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug', 'color']
class NestedRackRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
class Meta:
model = RackRole
fields = ['id', 'url', 'name', 'slug']
#
# Racks
#
class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
site = NestedSiteSerializer()
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=RACK_STATUS_CHOICES, required=False)
role = NestedRackRoleSerializer(required=False, allow_null=True)
type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True)
width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
outer_unit = ChoiceField(choices=RACK_DIMENSION_UNIT_CHOICES, required=False)
tags = TagListSerializerField(required=False)
class Meta:
model = Rack
fields = [
'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width',
'u_height', 'desc_units', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'status', 'role', 'serial',
'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
# Omit the UniqueTogetherValidator that would be automatically added to validate (group, facility_id). This
# prevents facility_id from being interpreted as a required field.
@@ -153,31 +135,11 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
validator(data)
# Enforce model validation
super(RackSerializer, self).validate(data)
super().validate(data)
return data
class NestedRackSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
class Meta:
model = Rack
fields = ['id', 'url', 'name', 'display_name']
#
# Rack units
#
class NestedDeviceSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
class Meta:
model = Device
fields = ['id', 'url', 'name', 'display_name']
class RackUnitSerializer(serializers.Serializer):
"""
A rack unit is an abstraction formed by the set (rack, position, face); it does not exist as a row in the database.
@@ -188,10 +150,6 @@ class RackUnitSerializer(serializers.Serializer):
device = NestedDeviceSerializer(read_only=True)
#
# Rack reservations
#
class RackReservationSerializer(ValidatedModelSerializer):
rack = NestedRackSerializer()
user = NestedUserSerializer()
@@ -203,7 +161,7 @@ class RackReservationSerializer(ValidatedModelSerializer):
#
# Manufacturers
# Device types
#
class ManufacturerSerializer(ValidatedModelSerializer):
@@ -213,21 +171,8 @@ class ManufacturerSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug']
class NestedManufacturerSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
class Meta:
model = Manufacturer
fields = ['id', 'url', 'name', 'slug']
#
# Device types
#
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
manufacturer = NestedManufacturerSerializer()
interface_ordering = ChoiceField(choices=IFACE_ORDERING_CHOICES, required=False)
subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True)
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
tags = TagListSerializerField(required=False)
@@ -235,25 +180,11 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
class Meta:
model = DeviceType
fields = [
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'instance_count',
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'instance_count',
]
class NestedDeviceTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True)
class Meta:
model = DeviceType
fields = ['id', 'url', 'manufacturer', 'model', 'slug']
#
# Console port templates
#
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
@@ -262,10 +193,6 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
fields = ['id', 'device_type', 'name']
#
# Console server port templates
#
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
@@ -274,10 +201,6 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
fields = ['id', 'device_type', 'name']
#
# Power port templates
#
class PowerPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
@@ -286,10 +209,6 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
fields = ['id', 'device_type', 'name']
#
# Power outlet templates
#
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
@@ -298,10 +217,6 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
fields = ['id', 'device_type', 'name']
#
# Interface templates
#
class InterfaceTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
@@ -311,9 +226,24 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
#
# Device bay templates
#
class RearPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=PORT_TYPE_CHOICES)
class Meta:
model = RearPortTemplate
fields = ['id', 'device_type', 'name', 'type', 'positions']
class FrontPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=PORT_TYPE_CHOICES)
rear_port = NestedRearPortTemplateSerializer()
class Meta:
model = FrontPortTemplate
fields = ['id', 'device_type', 'name', 'type', 'rear_port', 'rear_port_position']
class DeviceBayTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
@@ -324,7 +254,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
#
# Device roles
# Devices
#
class DeviceRoleSerializer(ValidatedModelSerializer):
@@ -334,64 +264,12 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug', 'color', 'vm_role']
class NestedDeviceRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
class Meta:
model = DeviceRole
fields = ['id', 'url', 'name', 'slug']
#
# Platforms
#
class PlatformSerializer(ValidatedModelSerializer):
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
class Meta:
model = Platform
fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'rpc_client']
class NestedPlatformSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
class Meta:
model = Platform
fields = ['id', 'url', 'name', 'slug']
#
# Devices
#
# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
class DeviceIPAddressSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
class Meta:
model = IPAddress
fields = ['id', 'url', 'family', 'address']
# Cannot import virtualization.api.NestedClusterSerializer due to circular dependency
class NestedClusterSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
class Meta:
model = Cluster
fields = ['id', 'url', 'name']
# Cannot import NestedVirtualChassisSerializer due to circular dependency
class DeviceVirtualChassisSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer()
class Meta:
model = VirtualChassis
fields = ['id', 'url', 'master']
fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args']
class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
@@ -403,12 +281,12 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
rack = NestedRackSerializer(required=False, allow_null=True)
face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True)
status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False)
primary_ip = DeviceIPAddressSerializer(read_only=True)
primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True)
primary_ip6 = DeviceIPAddressSerializer(required=False, allow_null=True)
primary_ip = NestedIPAddressSerializer(read_only=True)
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
parent_device = serializers.SerializerMethodField()
cluster = NestedClusterSerializer(required=False, allow_null=True)
virtual_chassis = DeviceVirtualChassisSerializer(required=False, allow_null=True)
virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True)
tags = TagListSerializerField(required=False)
class Meta:
@@ -416,8 +294,8 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
fields = [
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields', 'created',
'last_updated', 'local_context_data',
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'tags',
'custom_fields', 'created', 'last_updated',
]
validators = []
@@ -430,7 +308,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
validator(data)
# Enforce model validation
super(DeviceSerializer, self).validate(data)
super().validate(data)
return data
@@ -452,203 +330,90 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
fields = [
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields',
'config_context', 'created', 'last_updated', 'local_context_data',
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'tags',
'custom_fields', 'config_context', 'created', 'last_updated',
]
def get_config_context(self, obj):
return obj.get_config_context()
#
# Console server ports
#
class ConsoleServerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False)
class Meta:
model = ConsoleServerPort
fields = ['id', 'device', 'name', 'connected_console', 'tags']
read_only_fields = ['connected_console']
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer(read_only=True)
is_connected = serializers.SerializerMethodField(read_only=True)
class Meta:
model = ConsoleServerPort
fields = ['id', 'url', 'device', 'name', 'is_connected']
def get_is_connected(self, obj):
return hasattr(obj, 'connected_console') and obj.connected_console is not None
#
# Console ports
#
class ConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
device = NestedDeviceSerializer()
cs_port = NestedConsoleServerPortSerializer(required=False, allow_null=True)
tags = TagListSerializerField(required=False)
class Meta:
model = ConsolePort
fields = ['id', 'device', 'name', 'cs_port', 'connection_status', 'tags']
class NestedConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = NestedDeviceSerializer(read_only=True)
is_connected = serializers.SerializerMethodField(read_only=True)
class Meta:
model = ConsolePort
fields = ['id', 'url', 'device', 'name', 'is_connected']
def get_is_connected(self, obj):
return obj.cs_port is not None
#
# Power outlets
#
class PowerOutletSerializer(TaggitSerializer, ValidatedModelSerializer):
device = NestedDeviceSerializer()
tags = TagListSerializerField(required=False)
class Meta:
model = PowerOutlet
fields = ['id', 'device', 'name', 'connected_port', 'tags']
read_only_fields = ['connected_port']
class NestedPowerOutletSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer(read_only=True)
is_connected = serializers.SerializerMethodField(read_only=True)
class Meta:
model = PowerOutlet
fields = ['id', 'url', 'device', 'name', 'is_connected']
def get_is_connected(self, obj):
return hasattr(obj, 'connected_port') and obj.connected_port is not None
#
# Power ports
#
class PowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
device = NestedDeviceSerializer()
power_outlet = NestedPowerOutletSerializer(required=False, allow_null=True)
tags = TagListSerializerField(required=False)
class Meta:
model = PowerPort
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status', 'tags']
class NestedPowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = NestedDeviceSerializer(read_only=True)
is_connected = serializers.SerializerMethodField(read_only=True)
class Meta:
model = PowerPort
fields = ['id', 'url', 'device', 'name', 'is_connected']
def get_is_connected(self, obj):
return obj.power_outlet is not None
#
# Interfaces
#
class IsConnectedMixin(object):
"""
Provide a method for setting is_connected on Interface serializers.
"""
def get_is_connected(self, obj):
"""
Return True if the interface has a connected interface or circuit.
"""
if obj.connection:
return True
if hasattr(obj, 'circuit_termination') and obj.circuit_termination is not None:
return True
return False
class NestedInterfaceSerializer(IsConnectedMixin, WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
is_connected = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Interface
fields = ['id', 'url', 'device', 'name', 'is_connected']
class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
class Meta:
model = Circuit
fields = ['id', 'url', 'cid']
class InterfaceCircuitTerminationSerializer(WritableNestedSerializer):
circuit = InterfaceNestedCircuitSerializer(read_only=True)
class Meta:
model = CircuitTermination
fields = [
'id', 'circuit', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
'tags',
]
# Cannot import ipam.api.NestedVLANSerializer due to circular dependency
class InterfaceVLANSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False)
class Meta:
model = VLAN
fields = ['id', 'url', 'vid', 'name', 'display_name']
model = ConsolePort
fields = [
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
'tags',
]
class InterfaceSerializer(TaggitSerializer, IsConnectedMixin, ValidatedModelSerializer):
class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False)
class Meta:
model = PowerOutlet
fields = [
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
'tags',
]
class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False)
class Meta:
model = PowerPort
fields = [
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
'tags',
]
class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
lag = NestedInterfaceSerializer(required=False, allow_null=True)
is_connected = serializers.SerializerMethodField(read_only=True)
interface_connection = serializers.SerializerMethodField(read_only=True)
circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True)
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
tagged_vlans = SerializedPKRelatedField(
queryset=VLAN.objects.all(),
serializer=InterfaceVLANSerializer,
serializer=NestedVLANSerializer,
required=False,
many=True
)
cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False)
class Meta:
model = Interface
fields = [
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
'is_connected', 'interface_connection', 'circuit_termination', 'mode', 'untagged_vlan', 'tagged_vlans',
'tags',
'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan',
'tagged_vlans', 'tags', 'count_ipaddresses',
]
# TODO: This validation should be handled by Interface.clean()
def validate(self, data):
# All associated VLANs be global or assigned to the parent device's site.
@@ -666,21 +431,42 @@ class InterfaceSerializer(TaggitSerializer, IsConnectedMixin, ValidatedModelSeri
"be global.".format(vlan)
})
return super(InterfaceSerializer, self).validate(data)
def get_interface_connection(self, obj):
if obj.connection:
context = {
'request': self.context['request'],
'interface': obj.connected_interface,
}
return ContextualInterfaceConnectionSerializer(obj.connection, context=context).data
return None
return super().validate(data)
#
# Device bays
#
class RearPortSerializer(ValidatedModelSerializer):
device = NestedDeviceSerializer()
type = ChoiceField(choices=PORT_TYPE_CHOICES)
cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False)
class Meta:
model = RearPort
fields = ['id', 'device', 'name', 'type', 'positions', 'description', 'cable', 'tags']
class FrontPortRearPortSerializer(WritableNestedSerializer):
"""
NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device)
"""
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
class Meta:
model = RearPort
fields = ['id', 'url', 'name']
class FrontPortSerializer(ValidatedModelSerializer):
device = NestedDeviceSerializer()
type = ChoiceField(choices=PORT_TYPE_CHOICES)
rear_port = FrontPortRearPortSerializer()
cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False)
class Meta:
model = FrontPort
fields = ['id', 'device', 'name', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'tags']
class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
device = NestedDeviceSerializer()
@@ -692,15 +478,6 @@ class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
fields = ['id', 'device', 'name', 'installed_device', 'tags']
class NestedDeviceBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
device = NestedDeviceSerializer(read_only=True)
class Meta:
model = DeviceBay
fields = ['id', 'url', 'device', 'name']
#
# Inventory items
#
@@ -720,41 +497,76 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
]
#
# Cables
#
class CableSerializer(ValidatedModelSerializer):
termination_a_type = ContentTypeField()
termination_b_type = ContentTypeField()
termination_a = serializers.SerializerMethodField(read_only=True)
termination_b = serializers.SerializerMethodField(read_only=True)
status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False)
class Meta:
model = Cable
fields = [
'id', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type', 'termination_b_id',
'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit',
]
def _get_termination(self, obj, side):
"""
Serialize a nested representation of a termination.
"""
if side.lower() not in ['a', 'b']:
raise ValueError("Termination side must be either A or B.")
termination = getattr(obj, 'termination_{}'.format(side.lower()))
if termination is None:
return None
serializer = get_serializer_for_model(termination, prefix='Nested')
context = {'request': self.context['request']}
data = serializer(termination, context=context).data
return data
def get_termination_a(self, obj):
return self._get_termination(obj, 'a')
def get_termination_b(self, obj):
return self._get_termination(obj, 'b')
class TracedCableSerializer(serializers.ModelSerializer):
"""
Used only while tracing a cable path.
"""
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
class Meta:
model = Cable
fields = [
'id', 'url', 'type', 'status', 'label', 'color', 'length', 'length_unit',
]
#
# Interface connections
#
class InterfaceConnectionSerializer(ValidatedModelSerializer):
interface_a = NestedInterfaceSerializer()
interface_b = NestedInterfaceSerializer()
interface_a = serializers.SerializerMethodField()
interface_b = NestedInterfaceSerializer(source='connected_endpoint')
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
class Meta:
model = InterfaceConnection
fields = ['id', 'interface_a', 'interface_b', 'connection_status']
model = Interface
fields = ['interface_a', 'interface_b', 'connection_status']
class NestedInterfaceConnectionSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail')
class Meta:
model = InterfaceConnection
fields = ['id', 'url', 'connection_status']
class ContextualInterfaceConnectionSerializer(serializers.ModelSerializer):
"""
A read-only representation of an InterfaceConnection from the perspective of either of its two connected Interfaces.
"""
interface = serializers.SerializerMethodField(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta:
model = InterfaceConnection
fields = ['id', 'interface', 'connection_status']
def get_interface(self, obj):
return NestedInterfaceSerializer(self.context['interface'], context=self.context).data
def get_interface_a(self, obj):
context = {'request': self.context['request']}
return NestedInterfaceSerializer(instance=obj, context=context).data
#
@@ -768,11 +580,3 @@ class VirtualChassisSerializer(TaggitSerializer, ValidatedModelSerializer):
class Meta:
model = VirtualChassis
fields = ['id', 'master', 'domain', 'tags']
class NestedVirtualChassisSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
class Meta:
model = VirtualChassis
fields = ['id', 'url']

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
from rest_framework import routers
from . import views
@@ -17,7 +15,7 @@ router = routers.DefaultRouter()
router.APIRootView = DCIMRootView
# Field choices
router.register(r'_choices', views.DCIMFieldChoicesViewSet, base_name='field-choice')
router.register(r'_choices', views.DCIMFieldChoicesViewSet, basename='field-choice')
# Sites
router.register(r'regions', views.RegionViewSet)
@@ -39,6 +37,8 @@ router.register(r'console-server-port-templates', views.ConsoleServerPortTemplat
router.register(r'power-port-templates', views.PowerPortTemplateViewSet)
router.register(r'power-outlet-templates', views.PowerOutletTemplateViewSet)
router.register(r'interface-templates', views.InterfaceTemplateViewSet)
router.register(r'front-port-templates', views.FrontPortTemplateViewSet)
router.register(r'rear-port-templates', views.RearPortTemplateViewSet)
router.register(r'device-bay-templates', views.DeviceBayTemplateViewSet)
# Devices
@@ -52,19 +52,24 @@ router.register(r'console-server-ports', views.ConsoleServerPortViewSet)
router.register(r'power-ports', views.PowerPortViewSet)
router.register(r'power-outlets', views.PowerOutletViewSet)
router.register(r'interfaces', views.InterfaceViewSet)
router.register(r'front-ports', views.FrontPortViewSet)
router.register(r'rear-ports', views.RearPortViewSet)
router.register(r'device-bays', views.DeviceBayViewSet)
router.register(r'inventory-items', views.InventoryItemViewSet)
# Connections
router.register(r'console-connections', views.ConsoleConnectionViewSet, base_name='consoleconnections')
router.register(r'power-connections', views.PowerConnectionViewSet, base_name='powerconnections')
router.register(r'interface-connections', views.InterfaceConnectionViewSet)
router.register(r'console-connections', views.ConsoleConnectionViewSet, basename='consoleconnections')
router.register(r'power-connections', views.PowerConnectionViewSet, basename='powerconnections')
router.register(r'interface-connections', views.InterfaceConnectionViewSet, basename='interfaceconnections')
# Cables
router.register(r'cables', views.CableViewSet)
# Virtual chassis
router.register(r'virtual-chassis', views.VirtualChassisViewSet)
# Miscellaneous
router.register(r'connected-device', views.ConnectedDeviceViewSet, base_name='connected-device')
router.register(r'connected-device', views.ConnectedDeviceViewSet, basename='connected-device')
app_name = 'dcim-api'
urlpatterns = router.urls

View File

@@ -1,8 +1,7 @@
from __future__ import unicode_literals
from collections import OrderedDict
from django.conf import settings
from django.db.models import F, Q
from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404
from drf_yasg import openapi
@@ -15,15 +14,17 @@ from rest_framework.viewsets import GenericViewSet, ViewSet
from dcim import filters
from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, VirtualChassis,
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
)
from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.api import IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable
from utilities.api import (
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable,
)
from . import serializers
from .exceptions import MissingFilterException
@@ -34,17 +35,51 @@ from .exceptions import MissingFilterException
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(Cable, ['length_unit']),
(Device, ['face', 'status']),
(ConsolePort, ['connection_status']),
(Interface, ['form_factor', 'mode']),
(InterfaceConnection, ['connection_status']),
(Interface, ['connection_status', 'form_factor', 'mode']),
(InterfaceTemplate, ['form_factor']),
(PowerPort, ['connection_status']),
(Rack, ['type', 'width']),
(Rack, ['outer_unit', 'status', 'type', 'width']),
(Site, ['status']),
)
# Mixins
class CableTraceMixin(object):
@action(detail=True, url_path='trace')
def trace(self, request, pk):
"""
Trace a complete cable path and return each segment as a three-tuple of (termination, cable, termination).
"""
obj = get_object_or_404(self.queryset.model, pk=pk)
# Initialize the path array
path = []
for near_end, cable, far_end in obj.trace(follow_circuits=True):
# Serialize each object
serializer_a = get_serializer_for_model(near_end, prefix='Nested')
x = serializer_a(near_end, context={'request': request}).data
if cable is not None:
y = serializers.TracedCableSerializer(cable, context={'request': request}).data
else:
y = None
if far_end is not None:
serializer_b = get_serializer_for_model(far_end, prefix='Nested')
z = serializer_b(far_end, context={'request': request}).data
else:
z = None
path.append((x, y, z))
return Response(path)
#
# Regions
#
@@ -52,7 +87,7 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
class RegionViewSet(ModelViewSet):
queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer
filter_class = filters.RegionFilter
filterset_class = filters.RegionFilter
#
@@ -62,7 +97,7 @@ class RegionViewSet(ModelViewSet):
class SiteViewSet(CustomFieldModelViewSet):
queryset = Site.objects.select_related('region', 'tenant').prefetch_related('tags')
serializer_class = serializers.SiteSerializer
filter_class = filters.SiteFilter
filterset_class = filters.SiteFilter
@action(detail=True)
def graphs(self, request, pk=None):
@@ -82,7 +117,7 @@ class SiteViewSet(CustomFieldModelViewSet):
class RackGroupViewSet(ModelViewSet):
queryset = RackGroup.objects.select_related('site')
serializer_class = serializers.RackGroupSerializer
filter_class = filters.RackGroupFilter
filterset_class = filters.RackGroupFilter
#
@@ -92,7 +127,7 @@ class RackGroupViewSet(ModelViewSet):
class RackRoleViewSet(ModelViewSet):
queryset = RackRole.objects.all()
serializer_class = serializers.RackRoleSerializer
filter_class = filters.RackRoleFilter
filterset_class = filters.RackRoleFilter
#
@@ -102,7 +137,7 @@ class RackRoleViewSet(ModelViewSet):
class RackViewSet(CustomFieldModelViewSet):
queryset = Rack.objects.select_related('site', 'group__site', 'tenant').prefetch_related('tags')
serializer_class = serializers.RackSerializer
filter_class = filters.RackFilter
filterset_class = filters.RackFilter
@action(detail=True)
def units(self, request, pk=None):
@@ -132,7 +167,7 @@ class RackViewSet(CustomFieldModelViewSet):
class RackReservationViewSet(ModelViewSet):
queryset = RackReservation.objects.select_related('rack', 'user', 'tenant')
serializer_class = serializers.RackReservationSerializer
filter_class = filters.RackReservationFilter
filterset_class = filters.RackReservationFilter
# Assign user from request
def perform_create(self, serializer):
@@ -146,7 +181,7 @@ class RackReservationViewSet(ModelViewSet):
class ManufacturerViewSet(ModelViewSet):
queryset = Manufacturer.objects.all()
serializer_class = serializers.ManufacturerSerializer
filter_class = filters.ManufacturerFilter
filterset_class = filters.ManufacturerFilter
#
@@ -156,7 +191,7 @@ class ManufacturerViewSet(ModelViewSet):
class DeviceTypeViewSet(CustomFieldModelViewSet):
queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('tags')
serializer_class = serializers.DeviceTypeSerializer
filter_class = filters.DeviceTypeFilter
filterset_class = filters.DeviceTypeFilter
#
@@ -166,37 +201,49 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
class ConsolePortTemplateViewSet(ModelViewSet):
queryset = ConsolePortTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.ConsolePortTemplateSerializer
filter_class = filters.ConsolePortTemplateFilter
filterset_class = filters.ConsolePortTemplateFilter
class ConsoleServerPortTemplateViewSet(ModelViewSet):
queryset = ConsoleServerPortTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.ConsoleServerPortTemplateSerializer
filter_class = filters.ConsoleServerPortTemplateFilter
filterset_class = filters.ConsoleServerPortTemplateFilter
class PowerPortTemplateViewSet(ModelViewSet):
queryset = PowerPortTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.PowerPortTemplateSerializer
filter_class = filters.PowerPortTemplateFilter
filterset_class = filters.PowerPortTemplateFilter
class PowerOutletTemplateViewSet(ModelViewSet):
queryset = PowerOutletTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.PowerOutletTemplateSerializer
filter_class = filters.PowerOutletTemplateFilter
filterset_class = filters.PowerOutletTemplateFilter
class InterfaceTemplateViewSet(ModelViewSet):
queryset = InterfaceTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.InterfaceTemplateSerializer
filter_class = filters.InterfaceTemplateFilter
filterset_class = filters.InterfaceTemplateFilter
class FrontPortTemplateViewSet(ModelViewSet):
queryset = FrontPortTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.FrontPortTemplateSerializer
filterset_class = filters.FrontPortTemplateFilter
class RearPortTemplateViewSet(ModelViewSet):
queryset = RearPortTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.RearPortTemplateSerializer
filterset_class = filters.RearPortTemplateFilter
class DeviceBayTemplateViewSet(ModelViewSet):
queryset = DeviceBayTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.DeviceBayTemplateSerializer
filter_class = filters.DeviceBayTemplateFilter
filterset_class = filters.DeviceBayTemplateFilter
#
@@ -206,7 +253,7 @@ class DeviceBayTemplateViewSet(ModelViewSet):
class DeviceRoleViewSet(ModelViewSet):
queryset = DeviceRole.objects.all()
serializer_class = serializers.DeviceRoleSerializer
filter_class = filters.DeviceRoleFilter
filterset_class = filters.DeviceRoleFilter
#
@@ -216,7 +263,7 @@ class DeviceRoleViewSet(ModelViewSet):
class PlatformViewSet(ModelViewSet):
queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer
filter_class = filters.PlatformFilter
filterset_class = filters.PlatformFilter
#
@@ -230,7 +277,7 @@ class DeviceViewSet(CustomFieldModelViewSet):
).prefetch_related(
'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
)
filter_class = filters.DeviceFilter
filterset_class = filters.DeviceFilter
def get_serializer_class(self):
"""
@@ -321,34 +368,54 @@ class DeviceViewSet(CustomFieldModelViewSet):
# Device components
#
class ConsolePortViewSet(ModelViewSet):
queryset = ConsolePort.objects.select_related('device', 'cs_port__device').prefetch_related('tags')
class ConsolePortViewSet(CableTraceMixin, ModelViewSet):
queryset = ConsolePort.objects.select_related(
'device', 'connected_endpoint__device', 'cable'
).prefetch_related(
'tags'
)
serializer_class = serializers.ConsolePortSerializer
filter_class = filters.ConsolePortFilter
filterset_class = filters.ConsolePortFilter
class ConsoleServerPortViewSet(ModelViewSet):
queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device').prefetch_related('tags')
class ConsoleServerPortViewSet(CableTraceMixin, ModelViewSet):
queryset = ConsoleServerPort.objects.select_related(
'device', 'connected_endpoint__device', 'cable'
).prefetch_related(
'tags'
)
serializer_class = serializers.ConsoleServerPortSerializer
filter_class = filters.ConsoleServerPortFilter
filterset_class = filters.ConsoleServerPortFilter
class PowerPortViewSet(ModelViewSet):
queryset = PowerPort.objects.select_related('device', 'power_outlet__device').prefetch_related('tags')
class PowerPortViewSet(CableTraceMixin, ModelViewSet):
queryset = PowerPort.objects.select_related(
'device', 'connected_endpoint__device', 'cable'
).prefetch_related(
'tags'
)
serializer_class = serializers.PowerPortSerializer
filter_class = filters.PowerPortFilter
filterset_class = filters.PowerPortFilter
class PowerOutletViewSet(ModelViewSet):
queryset = PowerOutlet.objects.select_related('device', 'connected_port__device').prefetch_related('tags')
class PowerOutletViewSet(CableTraceMixin, ModelViewSet):
queryset = PowerOutlet.objects.select_related(
'device', 'connected_endpoint__device', 'cable'
).prefetch_related(
'tags'
)
serializer_class = serializers.PowerOutletSerializer
filter_class = filters.PowerOutletFilter
filterset_class = filters.PowerOutletFilter
class InterfaceViewSet(ModelViewSet):
queryset = Interface.objects.select_related('device').prefetch_related('tags')
class InterfaceViewSet(CableTraceMixin, ModelViewSet):
queryset = Interface.objects.select_related(
'device', '_connected_interface', '_connected_circuittermination', 'cable'
).prefetch_related(
'ip_addresses', 'tags'
)
serializer_class = serializers.InterfaceSerializer
filter_class = filters.InterfaceFilter
filterset_class = filters.InterfaceFilter
@action(detail=True)
def graphs(self, request, pk=None):
@@ -361,16 +428,36 @@ class InterfaceViewSet(ModelViewSet):
return Response(serializer.data)
class FrontPortViewSet(ModelViewSet):
queryset = FrontPort.objects.select_related(
'device__device_type__manufacturer', 'rear_port', 'cable'
).prefetch_related(
'tags'
)
serializer_class = serializers.FrontPortSerializer
filterset_class = filters.FrontPortFilter
class RearPortViewSet(ModelViewSet):
queryset = RearPort.objects.select_related(
'device__device_type__manufacturer', 'cable'
).prefetch_related(
'tags'
)
serializer_class = serializers.RearPortSerializer
filterset_class = filters.RearPortFilter
class DeviceBayViewSet(ModelViewSet):
queryset = DeviceBay.objects.select_related('installed_device').prefetch_related('tags')
serializer_class = serializers.DeviceBaySerializer
filter_class = filters.DeviceBayFilter
filterset_class = filters.DeviceBayFilter
class InventoryItemViewSet(ModelViewSet):
queryset = InventoryItem.objects.select_related('device', 'manufacturer').prefetch_related('tags')
serializer_class = serializers.InventoryItemSerializer
filter_class = filters.InventoryItemFilter
filterset_class = filters.InventoryItemFilter
#
@@ -378,21 +465,47 @@ class InventoryItemViewSet(ModelViewSet):
#
class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = ConsolePort.objects.select_related('device', 'cs_port__device').filter(cs_port__isnull=False)
queryset = ConsolePort.objects.select_related(
'device', 'connected_endpoint__device'
).filter(
connected_endpoint__isnull=False
)
serializer_class = serializers.ConsolePortSerializer
filter_class = filters.ConsoleConnectionFilter
filterset_class = filters.ConsoleConnectionFilter
class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = PowerPort.objects.select_related('device', 'power_outlet__device').filter(power_outlet__isnull=False)
queryset = PowerPort.objects.select_related(
'device', 'connected_endpoint__device'
).filter(
connected_endpoint__isnull=False
)
serializer_class = serializers.PowerPortSerializer
filter_class = filters.PowerConnectionFilter
filterset_class = filters.PowerConnectionFilter
class InterfaceConnectionViewSet(ModelViewSet):
queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = Interface.objects.select_related(
'device', '_connected_interface', '_connected_circuittermination'
).filter(
# Avoid duplicate connections by only selecting the lower PK in a connected pair
Q(_connected_interface__isnull=False, pk__lt=F('_connected_interface')) |
Q(_connected_circuittermination__isnull=False)
)
serializer_class = serializers.InterfaceConnectionSerializer
filter_class = filters.InterfaceConnectionFilter
filterset_class = filters.InterfaceConnectionFilter
#
# Cables
#
class CableViewSet(ModelViewSet):
queryset = Cable.objects.prefetch_related(
'termination_a', 'termination_b'
)
serializer_class = serializers.CableSerializer
filterset_class = filters.CableFilter
#
@@ -418,32 +531,39 @@ class ConnectedDeviceViewSet(ViewSet):
* `peer_interface`: The name of the peer interface
"""
permission_classes = [IsAuthenticatedOrLoginNotRequired]
_device_param = Parameter('peer_device', 'query',
description='The name of the peer device', required=True, type=openapi.TYPE_STRING)
_interface_param = Parameter('peer_interface', 'query',
description='The name of the peer interface', required=True, type=openapi.TYPE_STRING)
_device_param = Parameter(
name='peer_device',
in_='query',
description='The name of the peer device',
required=True,
type=openapi.TYPE_STRING
)
_interface_param = Parameter(
name='peer_interface',
in_='query',
description='The name of the peer interface',
required=True,
type=openapi.TYPE_STRING
)
def get_view_name(self):
return "Connected Device Locator"
@swagger_auto_schema(
manual_parameters=[_device_param, _interface_param], responses={'200': serializers.DeviceSerializer})
manual_parameters=[_device_param, _interface_param],
responses={'200': serializers.DeviceSerializer}
)
def list(self, request):
peer_device_name = request.query_params.get(self._device_param.name)
if not peer_device_name:
# TODO: remove this after 2.4 as the switch to using underscores is a breaking change
peer_device_name = request.query_params.get('peer-device')
peer_interface_name = request.query_params.get(self._interface_param.name)
if not peer_interface_name:
# TODO: remove this after 2.4 as the switch to using underscores is a breaking change
peer_interface_name = request.query_params.get('peer-interface')
if not peer_device_name or not peer_interface_name:
raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.')
# Determine local interface from peer interface's connection
peer_interface = get_object_or_404(Interface, device__name=peer_device_name, name=peer_interface_name)
local_interface = peer_interface.connected_interface
local_interface = peer_interface._connected_interface
if local_interface is None:
return Response()

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.apps import AppConfig

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
# Rack types
RACK_TYPE_2POST = 100
@@ -31,6 +29,20 @@ RACK_FACE_CHOICES = [
[RACK_FACE_REAR, 'Rear'],
]
# Rack statuses
RACK_STATUS_RESERVED = 0
RACK_STATUS_AVAILABLE = 1
RACK_STATUS_PLANNED = 2
RACK_STATUS_ACTIVE = 3
RACK_STATUS_DEPRECATED = 4
RACK_STATUS_CHOICES = [
[RACK_STATUS_ACTIVE, 'Active'],
[RACK_STATUS_PLANNED, 'Planned'],
[RACK_STATUS_RESERVED, 'Reserved'],
[RACK_STATUS_AVAILABLE, 'Available'],
[RACK_STATUS_DEPRECATED, 'Deprecated'],
]
# Parent/child device roles
SUBDEVICE_ROLE_PARENT = True
SUBDEVICE_ROLE_CHILD = False
@@ -70,6 +82,9 @@ IFACE_FF_100GE_CFP2 = 1510
IFACE_FF_100GE_CFP4 = 1520
IFACE_FF_100GE_CPAK = 1550
IFACE_FF_100GE_QSFP28 = 1600
IFACE_FF_200GE_CFP2 = 1650
IFACE_FF_200GE_QSFP56 = 1700
IFACE_FF_400GE_QSFP_DD = 1750
# Wireless
IFACE_FF_80211A = 2600
IFACE_FF_80211G = 2610
@@ -91,6 +106,7 @@ IFACE_FF_4GFC_SFP = 3040
IFACE_FF_8GFC_SFP_PLUS = 3080
IFACE_FF_16GFC_SFP_PLUS = 3160
IFACE_FF_32GFC_SFP28 = 3320
IFACE_FF_128GFC_QSFP28 = 3400
# Serial
IFACE_FF_T1 = 4000
IFACE_FF_E1 = 4010
@@ -140,9 +156,12 @@ IFACE_FF_CHOICES = [
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
[IFACE_FF_100GE_CFP2, 'CFP2 (100GE)'],
[IFACE_FF_200GE_CFP2, 'CFP2 (200GE)'],
[IFACE_FF_100GE_CFP4, 'CFP4 (100GE)'],
[IFACE_FF_100GE_CPAK, 'Cisco CPAK (100GE)'],
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
[IFACE_FF_200GE_QSFP56, 'QSFP56 (200GE)'],
[IFACE_FF_400GE_QSFP_DD, 'QSFP-DD (400GE)'],
]
],
[
@@ -176,6 +195,7 @@ IFACE_FF_CHOICES = [
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
[IFACE_FF_32GFC_SFP28, 'SFP28 (32GFC)'],
[IFACE_FF_128GFC_QSFP28, 'QSFP28 (128GFC)'],
]
],
[
@@ -233,6 +253,36 @@ IFACE_MODE_CHOICES = [
[IFACE_MODE_TAGGED_ALL, 'Tagged All'],
]
# Pass-through port types
PORT_TYPE_8P8C = 1000
PORT_TYPE_ST = 2000
PORT_TYPE_SC = 2100
PORT_TYPE_FC = 2200
PORT_TYPE_LC = 2300
PORT_TYPE_MTRJ = 2400
PORT_TYPE_MPO = 2500
PORT_TYPE_LSH = 2600
PORT_TYPE_CHOICES = [
[
'Copper',
[
[PORT_TYPE_8P8C, '8P8C'],
],
],
[
'Fiber Optic',
[
[PORT_TYPE_FC, 'FC'],
[PORT_TYPE_LC, 'LC'],
[PORT_TYPE_LSH, 'LSH'],
[PORT_TYPE_MPO, 'MPO'],
[PORT_TYPE_MTRJ, 'MTRJ'],
[PORT_TYPE_SC, 'SC'],
[PORT_TYPE_ST, 'ST'],
]
]
]
# Device statuses
DEVICE_STATUS_OFFLINE = 0
DEVICE_STATUS_ACTIVE = 1
@@ -259,7 +309,7 @@ SITE_STATUS_CHOICES = [
[SITE_STATUS_RETIRED, 'Retired'],
]
# Bootstrap CSS classes for device statuses
# Bootstrap CSS classes for device/rack statuses
STATUS_CLASSES = {
0: 'warning',
1: 'success',
@@ -277,12 +327,81 @@ CONNECTION_STATUS_CHOICES = [
[CONNECTION_STATUS_CONNECTED, 'Connected'],
]
# Platform -> RPC client mappings
RPC_CLIENT_JUNIPER_JUNOS = 'juniper-junos'
RPC_CLIENT_CISCO_IOS = 'cisco-ios'
RPC_CLIENT_OPENGEAR = 'opengear'
RPC_CLIENT_CHOICES = [
[RPC_CLIENT_JUNIPER_JUNOS, 'Juniper Junos (NETCONF)'],
[RPC_CLIENT_CISCO_IOS, 'Cisco IOS (SSH)'],
[RPC_CLIENT_OPENGEAR, 'Opengear (SSH)'],
# Cable endpoint types
CABLE_TERMINATION_TYPES = [
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport',
]
# Cable types
CABLE_TYPE_CAT3 = 1300
CABLE_TYPE_CAT5 = 1500
CABLE_TYPE_CAT5E = 1510
CABLE_TYPE_CAT6 = 1600
CABLE_TYPE_CAT6A = 1610
CABLE_TYPE_CAT7 = 1700
CABLE_TYPE_MMF_OM1 = 3010
CABLE_TYPE_MMF_OM2 = 3020
CABLE_TYPE_MMF_OM3 = 3030
CABLE_TYPE_MMF_OM4 = 3040
CABLE_TYPE_SMF = 3500
CABLE_TYPE_POWER = 5000
CABLE_TYPE_CHOICES = (
(
'Copper', (
(CABLE_TYPE_CAT3, 'CAT3'),
(CABLE_TYPE_CAT5, 'CAT5'),
(CABLE_TYPE_CAT5E, 'CAT5e'),
(CABLE_TYPE_CAT6, 'CAT6'),
(CABLE_TYPE_CAT6A, 'CAT6a'),
(CABLE_TYPE_CAT7, 'CAT7'),
),
),
(
'Fiber', (
(CABLE_TYPE_MMF_OM1, 'Multimode Fiber (OM1)'),
(CABLE_TYPE_MMF_OM2, 'Multimode Fiber (OM2)'),
(CABLE_TYPE_MMF_OM3, 'Multimode Fiber (OM3)'),
(CABLE_TYPE_MMF_OM4, 'Multimode Fiber (OM4)'),
(CABLE_TYPE_SMF, 'Singlemode Fiber'),
),
),
(CABLE_TYPE_POWER, 'Power'),
)
CABLE_TERMINATION_TYPE_CHOICES = {
# (API endpoint, human-friendly name)
'consoleport': ('console-ports', 'Console port'),
'consoleserverport': ('console-server-ports', 'Console server port'),
'powerport': ('power-ports', 'Power port'),
'poweroutlet': ('power-outlets', 'Power outlet'),
'interface': ('interfaces', 'Interface'),
'frontport': ('front-ports', 'Front panel port'),
'rearport': ('rear-ports', 'Rear panel port'),
}
COMPATIBLE_TERMINATION_TYPES = {
'consoleport': ['consoleserverport', 'frontport', 'rearport'],
'consoleserverport': ['consoleport', 'frontport', 'rearport'],
'powerport': ['poweroutlet'],
'poweroutlet': ['powerport'],
'interface': ['interface', 'circuittermination', 'frontport', 'rearport'],
'frontport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
'circuittermination': ['interface', 'frontport', 'rearport'],
}
LENGTH_UNIT_METER = 1200
LENGTH_UNIT_CENTIMETER = 1100
LENGTH_UNIT_MILLIMETER = 1000
LENGTH_UNIT_FOOT = 2100
LENGTH_UNIT_INCH = 2000
CABLE_LENGTH_UNIT_CHOICES = (
(LENGTH_UNIT_METER, 'Meters'),
(LENGTH_UNIT_CENTIMETER, 'Centimeters'),
(LENGTH_UNIT_FOOT, 'Feet'),
(LENGTH_UNIT_INCH, 'Inches'),
)
RACK_DIMENSION_UNIT_CHOICES = (
(LENGTH_UNIT_MILLIMETER, 'Millimeters'),
(LENGTH_UNIT_INCH, 'Inches'),
)

View File

@@ -0,0 +1,5 @@
class LoopDetected(Exception):
"""
A loop has been detected while tracing a cable path.
"""
pass

View File

@@ -1,10 +1,7 @@
from __future__ import unicode_literals
from netaddr import AddrFormatError, EUI, mac_unix_expanded
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from netaddr import AddrFormatError, EUI, mac_unix_expanded
class ASNField(models.BigIntegerField):

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
import django_filters
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
@@ -9,17 +7,15 @@ from netaddr.core import AddrFormatError
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.constants import COLOR_CHOICES
from utilities.filters import NullableCharFieldFilter, NumericInFilter, TagFilter
from virtualization.models import Cluster
from .constants import (
DEVICE_STATUS_CHOICES, IFACE_FF_LAG, NONCONNECTABLE_IFACE_TYPES, SITE_STATUS_CHOICES, VIRTUAL_IFACE_TYPES,
WIRELESS_IFACE_TYPES, IFACE_FF_CHOICES,
)
from .constants import *
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, VirtualChassis,
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
)
@@ -33,7 +29,7 @@ class RegionFilter(django_filters.FilterSet):
label='Parent region (ID)',
)
parent = django_filters.ModelMultipleChoiceFilter(
name='parent__slug',
field_name='parent__slug',
queryset=Region.objects.all(),
to_field_name='slug',
label='Parent region (slug)',
@@ -54,7 +50,10 @@ class RegionFilter(django_filters.FilterSet):
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -68,7 +67,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Region (ID)',
)
region = django_filters.ModelMultipleChoiceFilter(
name='region__slug',
field_name='region__slug',
queryset=Region.objects.all(),
to_field_name='slug',
label='Region (slug)',
@@ -78,7 +77,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant__slug',
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
@@ -120,7 +119,7 @@ class RackGroupFilter(django_filters.FilterSet):
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site__slug',
field_name='site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
@@ -148,7 +147,10 @@ class RackRoleFilter(django_filters.FilterSet):
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -159,7 +161,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site__slug',
field_name='site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
@@ -169,7 +171,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Group (ID)',
)
group = django_filters.ModelMultipleChoiceFilter(
name='group__slug',
field_name='group__slug',
queryset=RackGroup.objects.all(),
to_field_name='slug',
label='Group',
@@ -179,26 +181,34 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant__slug',
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
status = django_filters.MultipleChoiceFilter(
choices=RACK_STATUS_CHOICES,
null_value=None
)
role_id = django_filters.ModelMultipleChoiceFilter(
queryset=RackRole.objects.all(),
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
name='role__slug',
field_name='role__slug',
queryset=RackRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
asset_tag = NullableCharFieldFilter()
tag = TagFilter()
class Meta:
model = Rack
fields = ['name', 'serial', 'type', 'width', 'u_height', 'desc_units']
fields = [
'name', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
'outer_unit',
]
def search(self, queryset, name, value):
if not value.strip():
@@ -207,12 +217,16 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
Q(name__icontains=value) |
Q(facility_id__icontains=value) |
Q(serial__icontains=value.strip()) |
Q(asset_tag__icontains=value.strip()) |
Q(comments__icontains=value)
)
class RackReservationFilter(django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -222,23 +236,23 @@ class RackReservationFilter(django_filters.FilterSet):
label='Rack (ID)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='rack__site',
field_name='rack__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='rack__site__slug',
field_name='rack__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
group_id = django_filters.ModelMultipleChoiceFilter(
name='rack__group',
field_name='rack__group',
queryset=RackGroup.objects.all(),
label='Group (ID)',
)
group = django_filters.ModelMultipleChoiceFilter(
name='rack__group__slug',
field_name='rack__group__slug',
queryset=RackGroup.objects.all(),
to_field_name='slug',
label='Group',
@@ -248,7 +262,7 @@ class RackReservationFilter(django_filters.FilterSet):
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant__slug',
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
@@ -258,7 +272,7 @@ class RackReservationFilter(django_filters.FilterSet):
label='User (ID)',
)
user = django_filters.ModelMultipleChoiceFilter(
name='user',
field_name='user',
queryset=User.objects.all(),
to_field_name='username',
label='User (name)',
@@ -286,8 +300,11 @@ class ManufacturerFilter(django_filters.FilterSet):
fields = ['name', 'slug']
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
class DeviceTypeFilter(CustomFieldFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -297,18 +314,41 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
name='manufacturer__slug',
field_name='manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
console_ports = django_filters.BooleanFilter(
method='_console_ports',
label='Has console ports',
)
console_server_ports = django_filters.BooleanFilter(
method='_console_server_ports',
label='Has console server ports',
)
power_ports = django_filters.BooleanFilter(
method='_power_ports',
label='Has power ports',
)
power_outlets = django_filters.BooleanFilter(
method='_power_outlets',
label='Has power outlets',
)
interfaces = django_filters.BooleanFilter(
method='_interfaces',
label='Has interfaces',
)
pass_through_ports = django_filters.BooleanFilter(
method='_pass_through_ports',
label='Has pass-through ports',
)
tag = TagFilter()
class Meta:
model = DeviceType
fields = [
'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'is_network_device', 'subdevice_role',
'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
]
def search(self, queryset, name, value):
@@ -321,11 +361,32 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
Q(comments__icontains=value)
)
def _console_ports(self, queryset, name, value):
return queryset.exclude(consoleport_templates__isnull=value)
def _console_server_ports(self, queryset, name, value):
return queryset.exclude(consoleserverport_templates__isnull=value)
def _power_ports(self, queryset, name, value):
return queryset.exclude(powerport_templates__isnull=value)
def _power_outlets(self, queryset, name, value):
return queryset.exclude(poweroutlet_templates__isnull=value)
def _interfaces(self, queryset, name, value):
return queryset.exclude(interface_templates__isnull=value)
def _pass_through_ports(self, queryset, name, value):
return queryset.exclude(
frontport_templates__isnull=value,
rearport_templates__isnull=value
)
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
devicetype_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
name='device_type_id',
field_name='device_type_id',
label='Device type (ID)',
)
@@ -365,6 +426,20 @@ class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
fields = ['name', 'form_factor', 'mgmt_only']
class FrontPortTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
model = FrontPortTemplate
fields = ['name', 'type']
class RearPortTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
model = RearPortTemplate
fields = ['name', 'type']
class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
@@ -381,12 +456,12 @@ class DeviceRoleFilter(django_filters.FilterSet):
class PlatformFilter(django_filters.FilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
name='manufacturer',
field_name='manufacturer',
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
name='manufacturer__slug',
field_name='manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
@@ -397,19 +472,22 @@ class PlatformFilter(django_filters.FilterSet):
fields = ['name', 'slug']
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
class DeviceFilter(CustomFieldFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter(
method='search',
label='Search',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
name='device_type__manufacturer',
field_name='device_type__manufacturer',
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
name='device_type__manufacturer__slug',
field_name='device_type__manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
@@ -419,12 +497,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Device type (ID)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
name='device_role_id',
field_name='device_role_id',
queryset=DeviceRole.objects.all(),
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
name='device_role__slug',
field_name='device_role__slug',
queryset=DeviceRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
@@ -434,7 +512,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant__slug',
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
@@ -444,7 +522,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Platform (ID)',
)
platform = django_filters.ModelMultipleChoiceFilter(
name='platform__slug',
field_name='platform__slug',
queryset=Platform.objects.all(),
to_field_name='slug',
label='Platform (slug)',
@@ -453,12 +531,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
asset_tag = NullableCharFieldFilter()
region_id = django_filters.NumberFilter(
method='filter_region',
name='pk',
field_name='pk',
label='Region (ID)',
)
region = django_filters.CharFilter(
method='filter_region',
name='slug',
field_name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
@@ -466,18 +544,18 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site__slug',
field_name='site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site name (slug)',
)
rack_group_id = django_filters.ModelMultipleChoiceFilter(
name='rack__group',
field_name='rack__group',
queryset=RackGroup.objects.all(),
label='Rack group (ID)',
)
rack_id = django_filters.ModelMultipleChoiceFilter(
name='rack',
field_name='rack',
queryset=Rack.objects.all(),
label='Rack (ID)',
)
@@ -486,7 +564,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='VM cluster (ID)',
)
model = django_filters.ModelMultipleChoiceFilter(
name='device_type__slug',
field_name='device_type__slug',
queryset=DeviceType.objects.all(),
to_field_name='slug',
label='Device model (slug)',
@@ -496,21 +574,9 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
null_value=None
)
is_full_depth = django_filters.BooleanFilter(
name='device_type__is_full_depth',
field_name='device_type__is_full_depth',
label='Is full depth',
)
is_console_server = django_filters.BooleanFilter(
name='device_type__is_console_server',
label='Is a console server',
)
is_pdu = django_filters.BooleanFilter(
name='device_type__is_pdu',
label='Is a PDU',
)
is_network_device = django_filters.BooleanFilter(
name='device_type__is_network_device',
label='Is a network device',
)
mac_address = django_filters.CharFilter(
method='_mac_address',
label='MAC address',
@@ -520,10 +586,34 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Has a primary IP',
)
virtual_chassis_id = django_filters.ModelMultipleChoiceFilter(
name='virtual_chassis',
field_name='virtual_chassis',
queryset=VirtualChassis.objects.all(),
label='Virtual chassis (ID)',
)
console_ports = django_filters.BooleanFilter(
method='_console_ports',
label='Has console ports',
)
console_server_ports = django_filters.BooleanFilter(
method='_console_server_ports',
label='Has console server ports',
)
power_ports = django_filters.BooleanFilter(
method='_power_ports',
label='Has power ports',
)
power_outlets = django_filters.BooleanFilter(
method='_power_outlets',
label='Has power outlets',
)
interfaces = django_filters.BooleanFilter(
method='_interfaces',
label='Has interfaces',
)
pass_through_ports = django_filters.BooleanFilter(
method='_pass_through_ports',
label='Has pass-through ports',
)
tag = TagFilter()
class Meta:
@@ -573,6 +663,27 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
Q(primary_ip6__isnull=False)
)
def _console_ports(self, queryset, name, value):
return queryset.exclude(consoleports__isnull=value)
def _console_server_ports(self, queryset, name, value):
return queryset.exclude(consoleserverports__isnull=value)
def _power_ports(self, queryset, name, value):
return queryset.exclude(powerports__isnull=value)
def _power_outlets(self, queryset, name, value):
return queryset.exclude(poweroutlets_isnull=value)
def _interfaces(self, queryset, name, value):
return queryset.exclude(interfaces__isnull=value)
def _pass_through_ports(self, queryset, name, value):
return queryset.exclude(
frontports__isnull=value,
rearports__isnull=value
)
class DeviceComponentFilterSet(django_filters.FilterSet):
device_id = django_filters.ModelChoiceFilter(
@@ -588,54 +699,78 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
class ConsolePortFilter(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta:
model = ConsolePort
fields = ['name']
fields = ['name', 'connection_status']
class ConsoleServerPortFilter(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta:
model = ConsoleServerPort
fields = ['name']
fields = ['name', 'connection_status']
class PowerPortFilter(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta:
model = PowerPort
fields = ['name']
fields = ['name', 'connection_status']
class PowerOutletFilter(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta:
model = PowerOutlet
fields = ['name']
fields = ['name', 'connection_status']
class InterfaceFilter(django_filters.FilterSet):
"""
Not using DeviceComponentFilterSet for Interfaces because we need to glean the ordering logic from the parent
Device's DeviceType.
Not using DeviceComponentFilterSet for Interfaces because we need to check for VirtualChassis membership.
"""
device = django_filters.CharFilter(
method='filter_device',
name='name',
field_name='name',
label='Device',
)
device_id = django_filters.NumberFilter(
method='filter_device',
name='pk',
field_name='pk',
label='Device (ID)',
)
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
type = django_filters.CharFilter(
method='filter_type',
label='Interface type',
)
lag_id = django_filters.ModelMultipleChoiceFilter(
name='lag',
field_name='lag',
queryset=Interface.objects.all(),
label='LAG interface (ID)',
)
@@ -659,14 +794,13 @@ class InterfaceFilter(django_filters.FilterSet):
class Meta:
model = Interface
fields = ['name', 'enabled', 'mtu', 'mgmt_only']
fields = ['name', 'connection_status', 'form_factor', 'enabled', 'mtu', 'mgmt_only']
def filter_device(self, queryset, name, value):
try:
device = Device.objects.select_related('device_type').get(**{name: value})
vc_interface_ids = [i['id'] for i in device.vc_interfaces.values('id')]
ordering = device.device_type.interface_ordering
return queryset.filter(pk__in=vc_interface_ids).order_naturally(ordering)
device = Device.objects.get(**{name: value})
vc_interface_ids = device.vc_interfaces.values_list('id', flat=True)
return queryset.filter(pk__in=vc_interface_ids)
except Device.DoesNotExist:
return queryset.none()
@@ -708,6 +842,30 @@ class InterfaceFilter(django_filters.FilterSet):
return queryset.none()
class FrontPortFilter(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta:
model = FrontPort
fields = ['name', 'type']
class RearPortFilter(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta:
model = RearPort
fields = ['name', 'type']
class DeviceBayFilter(DeviceComponentFilterSet):
class Meta:
@@ -738,7 +896,7 @@ class InventoryItemFilter(DeviceComponentFilterSet):
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
name='manufacturer__slug',
field_name='manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
@@ -768,23 +926,23 @@ class VirtualChassisFilter(django_filters.FilterSet):
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='master__site',
field_name='master__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='master__site__slug',
field_name='master__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site name (slug)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='master__tenant',
field_name='master__tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='master__tenant__slug',
field_name='master__tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
@@ -805,6 +963,28 @@ class VirtualChassisFilter(django_filters.FilterSet):
return queryset.filter(qs_filter)
class CableFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
type = django_filters.MultipleChoiceFilter(
choices=CABLE_TYPE_CHOICES
)
color = django_filters.MultipleChoiceFilter(
choices=COLOR_CHOICES
)
class Meta:
model = Cable
fields = ['type', 'status', 'color', 'length', 'length_unit']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(label__icontains=value)
class ConsoleConnectionFilter(django_filters.FilterSet):
site = django_filters.CharFilter(
method='filter_site',
@@ -822,14 +1002,14 @@ class ConsoleConnectionFilter(django_filters.FilterSet):
def filter_site(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(cs_port__device__site__slug=value)
return queryset.filter(connected_endpoint__device__site__slug=value)
def filter_device(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(device__name__icontains=value) |
Q(cs_port__device__name__icontains=value)
Q(connected_endpoint__device__name__icontains=value)
)
@@ -850,14 +1030,14 @@ class PowerConnectionFilter(django_filters.FilterSet):
def filter_site(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(power_outlet__device__site__slug=value)
return queryset.filter(connected_endpoint__device__site__slug=value)
def filter_device(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(device__name__icontains=value) |
Q(power_outlet__device__name__icontains=value)
Q(connected_endpoint__device__name__icontains=value)
)
@@ -872,21 +1052,21 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
)
class Meta:
model = InterfaceConnection
model = Interface
fields = ['connection_status']
def filter_site(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(interface_a__device__site__slug=value) |
Q(interface_b__device__site__slug=value)
Q(device__site__slug=value) |
Q(_connected_interface__device__site__slug=value)
)
def filter_device(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(interface_a__device__name__icontains=value) |
Q(interface_b__device__name__icontains=value)
Q(device__name__icontains=value) |
Q(_connected_interface__device__name__icontains=value)
)

View File

@@ -76,10 +76,7 @@
"model": "MX960",
"slug": "mx960",
"u_height": 16,
"is_full_depth": true,
"is_console_server": false,
"is_pdu": false,
"is_network_device": true
"is_full_depth": true
}
},
{
@@ -92,10 +89,7 @@
"model": "EX9214",
"slug": "ex9214",
"u_height": 16,
"is_full_depth": true,
"is_console_server": false,
"is_pdu": false,
"is_network_device": true
"is_full_depth": true
}
},
{
@@ -108,10 +102,7 @@
"model": "QFX5100-24Q",
"slug": "qfx5100-24q",
"u_height": 1,
"is_full_depth": true,
"is_console_server": false,
"is_pdu": false,
"is_network_device": true
"is_full_depth": true
}
},
{
@@ -124,10 +115,7 @@
"model": "QFX5100-48S",
"slug": "qfx5100-48s",
"u_height": 1,
"is_full_depth": true,
"is_console_server": false,
"is_pdu": false,
"is_network_device": true
"is_full_depth": true
}
},
{
@@ -140,10 +128,7 @@
"model": "CM4148",
"slug": "cm4148",
"u_height": 1,
"is_full_depth": true,
"is_console_server": true,
"is_pdu": false,
"is_network_device": false
"is_full_depth": true
}
},
{
@@ -156,10 +141,7 @@
"model": "CWG-24VYM415C9",
"slug": "cwg-24vym415c9",
"u_height": 0,
"is_full_depth": false,
"is_console_server": false,
"is_pdu": true,
"is_network_device": false
"is_full_depth": false
}
},
{
@@ -1903,8 +1885,7 @@
"pk": 1,
"fields": {
"name": "Juniper Junos",
"slug": "juniper-junos",
"rpc_client": "juniper-junos"
"slug": "juniper-junos"
}
},
{
@@ -1912,8 +1893,7 @@
"pk": 2,
"fields": {
"name": "Opengear",
"slug": "opengear",
"rpc_client": "opengear"
"slug": "opengear"
}
},
{
@@ -2153,7 +2133,7 @@
"fields": {
"device": 1,
"name": "Console (RE0)",
"cs_port": 27,
"connected_endpoint": 27,
"connection_status": true
}
},
@@ -2163,7 +2143,7 @@
"fields": {
"device": 1,
"name": "Console (RE1)",
"cs_port": 38,
"connected_endpoint": 38,
"connection_status": true
}
},
@@ -2173,7 +2153,7 @@
"fields": {
"device": 2,
"name": "Console (RE0)",
"cs_port": 5,
"connected_endpoint": 5,
"connection_status": true
}
},
@@ -2183,7 +2163,7 @@
"fields": {
"device": 2,
"name": "Console (RE1)",
"cs_port": 16,
"connected_endpoint": 16,
"connection_status": true
}
},
@@ -2193,7 +2173,7 @@
"fields": {
"device": 3,
"name": "Console",
"cs_port": 49,
"connected_endpoint": 49,
"connection_status": true
}
},
@@ -2203,7 +2183,7 @@
"fields": {
"device": 4,
"name": "Console",
"cs_port": 48,
"connected_endpoint": 48,
"connection_status": true
}
},
@@ -2213,7 +2193,7 @@
"fields": {
"device": 5,
"name": "Console",
"cs_port": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2223,7 +2203,7 @@
"fields": {
"device": 6,
"name": "Console",
"cs_port": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2233,7 +2213,7 @@
"fields": {
"device": 7,
"name": "Console (RE0)",
"cs_port": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2243,7 +2223,7 @@
"fields": {
"device": 7,
"name": "Console (RE1)",
"cs_port": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2253,7 +2233,7 @@
"fields": {
"device": 8,
"name": "Console (RE0)",
"cs_port": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2263,7 +2243,7 @@
"fields": {
"device": 8,
"name": "Console (RE1)",
"cs_port": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2273,7 +2253,7 @@
"fields": {
"device": 9,
"name": "Console",
"cs_port": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2283,7 +2263,7 @@
"fields": {
"device": 11,
"name": "Serial",
"cs_port": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2293,7 +2273,7 @@
"fields": {
"device": 12,
"name": "Serial",
"cs_port": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2687,7 +2667,7 @@
"fields": {
"device": 1,
"name": "PEM0",
"power_outlet": 25,
"connected_endpoint": 25,
"connection_status": true
}
},
@@ -2697,7 +2677,7 @@
"fields": {
"device": 1,
"name": "PEM1",
"power_outlet": 49,
"connected_endpoint": 49,
"connection_status": true
}
},
@@ -2707,7 +2687,7 @@
"fields": {
"device": 1,
"name": "PEM2",
"power_outlet": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2717,7 +2697,7 @@
"fields": {
"device": 1,
"name": "PEM3",
"power_outlet": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2727,7 +2707,7 @@
"fields": {
"device": 2,
"name": "PEM0",
"power_outlet": 26,
"connected_endpoint": 26,
"connection_status": true
}
},
@@ -2737,7 +2717,7 @@
"fields": {
"device": 2,
"name": "PEM1",
"power_outlet": 50,
"connected_endpoint": 50,
"connection_status": true
}
},
@@ -2747,7 +2727,7 @@
"fields": {
"device": 2,
"name": "PEM2",
"power_outlet": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2757,7 +2737,7 @@
"fields": {
"device": 2,
"name": "PEM3",
"power_outlet": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2767,7 +2747,7 @@
"fields": {
"device": 4,
"name": "PSU0",
"power_outlet": 28,
"connected_endpoint": 28,
"connection_status": true
}
},
@@ -2777,7 +2757,7 @@
"fields": {
"device": 4,
"name": "PSU1",
"power_outlet": 52,
"connected_endpoint": 52,
"connection_status": true
}
},
@@ -2787,7 +2767,7 @@
"fields": {
"device": 5,
"name": "PSU0",
"power_outlet": 56,
"connected_endpoint": 56,
"connection_status": true
}
},
@@ -2797,7 +2777,7 @@
"fields": {
"device": 5,
"name": "PSU1",
"power_outlet": 32,
"connected_endpoint": 32,
"connection_status": true
}
},
@@ -2807,7 +2787,7 @@
"fields": {
"device": 3,
"name": "PSU0",
"power_outlet": 27,
"connected_endpoint": 27,
"connection_status": true
}
},
@@ -2817,7 +2797,7 @@
"fields": {
"device": 3,
"name": "PSU1",
"power_outlet": 51,
"connected_endpoint": 51,
"connection_status": true
}
},
@@ -2827,7 +2807,7 @@
"fields": {
"device": 7,
"name": "PEM0",
"power_outlet": 53,
"connected_endpoint": 53,
"connection_status": true
}
},
@@ -2837,7 +2817,7 @@
"fields": {
"device": 7,
"name": "PEM1",
"power_outlet": 29,
"connected_endpoint": 29,
"connection_status": true
}
},
@@ -2847,7 +2827,7 @@
"fields": {
"device": 7,
"name": "PEM2",
"power_outlet": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2857,7 +2837,7 @@
"fields": {
"device": 7,
"name": "PEM3",
"power_outlet": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2867,7 +2847,7 @@
"fields": {
"device": 8,
"name": "PEM0",
"power_outlet": 54,
"connected_endpoint": 54,
"connection_status": true
}
},
@@ -2877,7 +2857,7 @@
"fields": {
"device": 8,
"name": "PEM1",
"power_outlet": 30,
"connected_endpoint": 30,
"connection_status": true
}
},
@@ -2887,7 +2867,7 @@
"fields": {
"device": 8,
"name": "PEM2",
"power_outlet": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2897,7 +2877,7 @@
"fields": {
"device": 8,
"name": "PEM3",
"power_outlet": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -2907,7 +2887,7 @@
"fields": {
"device": 6,
"name": "PSU0",
"power_outlet": 55,
"connected_endpoint": 55,
"connection_status": true
}
},
@@ -2917,7 +2897,7 @@
"fields": {
"device": 6,
"name": "PSU1",
"power_outlet": 31,
"connected_endpoint": 31,
"connection_status": true
}
},
@@ -2927,7 +2907,7 @@
"fields": {
"device": 9,
"name": "PSU",
"power_outlet": null,
"connected_endpoint": null,
"connection_status": true
}
},
@@ -5748,158 +5728,5 @@
"mgmt_only": true,
"description": ""
}
},
{
"model": "dcim.interfaceconnection",
"pk": 3,
"fields": {
"interface_a": 99,
"interface_b": 15,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 4,
"fields": {
"interface_a": 100,
"interface_b": 153,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 5,
"fields": {
"interface_a": 46,
"interface_b": 14,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 6,
"fields": {
"interface_a": 47,
"interface_b": 152,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 7,
"fields": {
"interface_a": 91,
"interface_b": 144,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 8,
"fields": {
"interface_a": 92,
"interface_b": 145,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 16,
"fields": {
"interface_a": 189,
"interface_b": 37,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 17,
"fields": {
"interface_a": 192,
"interface_b": 175,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 18,
"fields": {
"interface_a": 195,
"interface_b": 41,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 19,
"fields": {
"interface_a": 198,
"interface_b": 179,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 20,
"fields": {
"interface_a": 191,
"interface_b": 197,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 21,
"fields": {
"interface_a": 194,
"interface_b": 200,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 22,
"fields": {
"interface_a": 9,
"interface_b": 218,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 23,
"fields": {
"interface_a": 8,
"interface_b": 206,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 24,
"fields": {
"interface_a": 7,
"interface_b": 212,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 25,
"fields": {
"interface_a": 217,
"interface_b": 205,
"connection_status": true
}
},
{
"model": "dcim.interfaceconnection",
"pk": 26,
"fields": {
"interface_a": 216,
"interface_b": 211,
"connection_status": true
}
}
]

View File

@@ -149,8 +149,7 @@
"pk": 1,
"fields": {
"name": "Cisco IOS",
"slug": "cisco-ios",
"rpc_client": "cisco-ios"
"slug": "cisco-ios"
}
},
{
@@ -158,8 +157,7 @@
"pk": 2,
"fields": {
"name": "Cisco NX-OS",
"slug": "cisco-nx-os",
"rpc_client": ""
"slug": "cisco-nx-os"
}
},
{
@@ -167,8 +165,7 @@
"pk": 3,
"fields": {
"name": "Juniper Junos",
"slug": "juniper-junos",
"rpc_client": "juniper-junos"
"slug": "juniper-junos"
}
},
{
@@ -176,8 +173,7 @@
"pk": 4,
"fields": {
"name": "Arista EOS",
"slug": "arista-eos",
"rpc_client": ""
"slug": "arista-eos"
}
},
{
@@ -185,8 +181,7 @@
"pk": 5,
"fields": {
"name": "Linux",
"slug": "linux",
"rpc_client": ""
"slug": "linux"
}
},
{
@@ -194,8 +189,7 @@
"pk": 6,
"fields": {
"name": "Opengear",
"slug": "opengear",
"rpc_client": "opengear"
"slug": "opengear"
}
}
]

File diff suppressed because it is too large Load Diff

85
netbox/dcim/managers.py Normal file
View File

@@ -0,0 +1,85 @@
from django.db.models import Manager, QuerySet
from django.db.models.expressions import RawSQL
from .constants import NONCONNECTABLE_IFACE_TYPES
# Regular expressions for parsing Interface names
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9\.:]+)')"
SLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(\d{{1,9}})/') AS integer), NULL)"
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9\.:]+)?\d{{1,9}}/(\d{{1,9}})') AS integer), NULL)"
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}/){{2}}(\d{{1,9}})') AS integer), NULL)"
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}/){{3}}(\d{{1,9}})') AS integer), NULL)"
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9\.:]+)?(\d{{1,9}})([^/]|$)') AS integer)"
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*:(\d{{1,9}})(\.\d{{1,9}})?$') AS integer), 0)"
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*\.(\d{{1,9}})$') AS integer), 0)"
class DeviceComponentManager(Manager):
def get_queryset(self):
queryset = super().get_queryset()
table_name = self.model._meta.db_table
sql = r"CONCAT(REGEXP_REPLACE({}.name, '\d+$', ''), LPAD(SUBSTRING({}.name FROM '\d+$'), 8, '0'))"
# Pad any trailing digits to effect natural sorting
return queryset.extra(
select={
'name_padded': sql.format(table_name, table_name),
}
).order_by('name_padded')
class InterfaceQuerySet(QuerySet):
def connectable(self):
"""
Return only physical interfaces which are capable of being connected to other interfaces (i.e. not virtual or
wireless).
"""
return self.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES)
class InterfaceManager(Manager):
def get_queryset(self):
"""
Naturally order interfaces by their type and numeric position. To order interfaces naturally, the `name` field
is split into eight distinct components: leading text (type), slot, subslot, position, subposition, ID, channel,
and virtual circuit:
{type}{slot or ID}/{subslot}/{position}/{subposition}:{channel}.{vc}
Components absent from the interface name are coalesced to zero or null. For example, an interface named
GigabitEthernet1/2/3 would be parsed as follows:
type = 'GigabitEthernet'
slot = 1
subslot = 2
position = 3
subposition = None
id = None
channel = 0
vc = 0
The original `name` field is considered in its entirety to serve as a fallback in the event interfaces do not
match any of the prescribed fields.
"""
sql_col = '{}.name'.format(self.model._meta.db_table)
ordering = [
'_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name',
]
fields = {
'_type': RawSQL(TYPE_RE.format(sql_col), []),
'_id': RawSQL(ID_RE.format(sql_col), []),
'_slot': RawSQL(SLOT_RE.format(sql_col), []),
'_subslot': RawSQL(SUBSLOT_RE.format(sql_col), []),
'_position': RawSQL(POSITION_RE.format(sql_col), []),
'_subposition': RawSQL(SUBPOSITION_RE.format(sql_col), []),
'_channel': RawSQL(CHANNEL_RE.format(sql_col), []),
'_vc': RawSQL(VC_RE.format(sql_col), []),
}
return InterfaceQuerySet(self.model, using=self._db).annotate(**fields).order_by(*ordering)

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-22 18:21
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-22 18:21
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.14 on 2018-07-31 02:06
from __future__ import unicode_literals
import dcim.fields
import django.core.validators
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-28 17:21
from __future__ import unicode_literals
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-01 20:49
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-06 17:22
from __future__ import unicode_literals
import dcim.fields
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-11 18:40
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-11 18:40
from __future__ import unicode_literals
from django.db import migrations

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-11 19:01
from __future__ import unicode_literals
from django.db import migrations

View File

@@ -1,7 +1,5 @@
# -*- 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

View File

@@ -1,7 +1,5 @@
# -*- 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

View File

@@ -1,7 +1,5 @@
# -*- 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

View File

@@ -1,7 +1,5 @@
# -*- 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

View File

@@ -1,7 +1,5 @@
# -*- 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

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-08 21:11
from __future__ import unicode_literals
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-09 21:18
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-10 13:45
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-10 14:58
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-08-11 15:42
from __future__ import unicode_literals
from django.db import migrations
import utilities.fields

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-13 15:20
from __future__ import unicode_literals
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-10-28 15:01
from __future__ import unicode_literals
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-10-31 18:47
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-12-06 16:35
from __future__ import unicode_literals
from django.db import migrations
import utilities.fields

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-12-16 16:08
from __future__ import unicode_literals
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.14 on 2018-07-31 02:13
from __future__ import unicode_literals
import dcim.fields
from django.conf import settings
import django.contrib.postgres.fields

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-29 16:23
from __future__ import unicode_literals
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-06 16:56
from __future__ import unicode_literals
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-02-16 18:43
from __future__ import unicode_literals
from django.conf import settings
import django.contrib.postgres.fields
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-02-16 21:21
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-02-16 21:23
from __future__ import unicode_literals
from django.db import migrations

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-02-16 21:25
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-02-27 19:55
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-02-28 17:14
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-03-02 15:09
from __future__ import unicode_literals
from django.db import migrations
import utilities.fields

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.6 on 2017-03-17 18:39
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.6 on 2017-03-21 14:55
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-05-08 15:57
from __future__ import unicode_literals
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-09 16:00
from __future__ import unicode_literals
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-05-24 15:34
from __future__ import unicode_literals
import dcim.fields
import django.core.validators
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-06-16 21:38
from __future__ import unicode_literals
from django.db import migrations, models

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-06-23 17:05
from __future__ import unicode_literals
from django.db import migrations, models

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