mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-31 06:08:19 +01:00
Compare commits
233 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a97a1c9d2 | ||
|
|
893e327ac6 | ||
|
|
814c50f461 | ||
|
|
1958c0b118 | ||
|
|
a11b33d214 | ||
|
|
7d053f8ba4 | ||
|
|
1e1aba73ef | ||
|
|
b9b009c0b5 | ||
|
|
a6ff6505c6 | ||
|
|
823257ca72 | ||
|
|
0804c1acbd | ||
|
|
28facca291 | ||
|
|
a7ca49c44d | ||
|
|
5639fc9791 | ||
|
|
8b00513175 | ||
|
|
00ffa358b2 | ||
|
|
1ff7e1149c | ||
|
|
2c7bad9fff | ||
|
|
c4f481d705 | ||
|
|
99a3a216c3 | ||
|
|
87f5dd05f5 | ||
|
|
cc87d99017 | ||
|
|
1366730a3f | ||
|
|
473dafc2c8 | ||
|
|
38d5a8fdc0 | ||
|
|
b114b9d396 | ||
|
|
f9cd89a4a4 | ||
|
|
4313a717c4 | ||
|
|
cbace6f831 | ||
|
|
edabc8eee9 | ||
|
|
9b47e57e8e | ||
|
|
2f32488c25 | ||
|
|
62d497dd0b | ||
|
|
e19feb92ea | ||
|
|
fbde6282b2 | ||
|
|
7895ccfae1 | ||
|
|
dfd4a712c9 | ||
|
|
b97339017b | ||
|
|
1b862045e3 | ||
|
|
244c07e5f7 | ||
|
|
eb41bc66a4 | ||
|
|
01c5d9e909 | ||
|
|
5f5e4ce1a1 | ||
|
|
d50acb39dd | ||
|
|
ee4a3bcb02 | ||
|
|
ceeac9bae3 | ||
|
|
49446ffb74 | ||
|
|
5487ab40af | ||
|
|
5a8ba159f2 | ||
|
|
22e5834d8b | ||
|
|
63b71d43da | ||
|
|
7b5c1964b9 | ||
|
|
b7a5afa797 | ||
|
|
66f90f46de | ||
|
|
d5dcb77d99 | ||
|
|
2b93510c45 | ||
|
|
9717c6f825 | ||
|
|
92c227d103 | ||
|
|
1491222642 | ||
|
|
39fceeb455 | ||
|
|
3562b5552b | ||
|
|
6d778f686d | ||
|
|
245a97176a | ||
|
|
ca56871aaa | ||
|
|
bd4086cb50 | ||
|
|
d8c9b1af27 | ||
|
|
19d2850b29 | ||
|
|
f4636537ad | ||
|
|
f4aec1e7d0 | ||
|
|
8b75969d1d | ||
|
|
a5e1088f1f | ||
|
|
6fa54bed73 | ||
|
|
6e8e6809f3 | ||
|
|
a91a79681f | ||
|
|
8d79353d9b | ||
|
|
f3fffc6161 | ||
|
|
a8d20e13a8 | ||
|
|
9a91bdbdb2 | ||
|
|
6f8591f769 | ||
|
|
7f6d79362e | ||
|
|
c032413201 | ||
|
|
e556c78599 | ||
|
|
1b389d662b | ||
|
|
090efde21a | ||
|
|
74c03e3295 | ||
|
|
858be6d216 | ||
|
|
1e160fd9e9 | ||
|
|
7c6d2a6281 | ||
|
|
fdf168934e | ||
|
|
738a20ad34 | ||
|
|
3602d5a84c | ||
|
|
d23ca041cf | ||
|
|
110387e81b | ||
|
|
498f132cad | ||
|
|
dff3165402 | ||
|
|
3f5f75c71f | ||
|
|
2e1887eb0e | ||
|
|
2170eedf08 | ||
|
|
3acc8ca3ab | ||
|
|
fc76c8eb0f | ||
|
|
7d1ee2e94e | ||
|
|
3cea92384c | ||
|
|
6ba3d611af | ||
|
|
f6345b9a5d | ||
|
|
044f7395bb | ||
|
|
8bda6be65a | ||
|
|
e544705256 | ||
|
|
f88099eb3b | ||
|
|
5f40be4bd5 | ||
|
|
1bf04f2e30 | ||
|
|
7edad4eba9 | ||
|
|
dd7249d2ef | ||
|
|
aab84ba6f3 | ||
|
|
520af82f05 | ||
|
|
9292534324 | ||
|
|
61efe6102e | ||
|
|
0022a5a6e2 | ||
|
|
332487efcd | ||
|
|
28331dfd53 | ||
|
|
d112b6027a | ||
|
|
88933a3120 | ||
|
|
e05871b467 | ||
|
|
7e70bfaacc | ||
|
|
f4b85751bb | ||
|
|
ea11e70e3f | ||
|
|
bd8b239e15 | ||
|
|
7a8fc8dfd5 | ||
|
|
4334f1bc65 | ||
|
|
1995091169 | ||
|
|
ef089d3722 | ||
|
|
1d904b1722 | ||
|
|
ab02f26a0e | ||
|
|
f35b4bf768 | ||
|
|
f2382dd255 | ||
|
|
8df169b170 | ||
|
|
88aeaaffb0 | ||
|
|
e9546b810c | ||
|
|
c208d8fc2e | ||
|
|
7294f43fa3 | ||
|
|
0572c66c17 | ||
|
|
6406e213bd | ||
|
|
cfb56f7cfe | ||
|
|
bd65e782bb | ||
|
|
37811d3f7e | ||
|
|
3bb1cbcdb0 | ||
|
|
5fcd673f9f | ||
|
|
b4d7f9ea43 | ||
|
|
0c142f2078 | ||
|
|
7295c3554e | ||
|
|
78725b8483 | ||
|
|
e97ad3f066 | ||
|
|
3a62e9a322 | ||
|
|
4d18d9661b | ||
|
|
b381bdec27 | ||
|
|
6f5c35c278 | ||
|
|
3e6033e9ff | ||
|
|
32f63a18ff | ||
|
|
00aaf500de | ||
|
|
f2471aedb2 | ||
|
|
beff774295 | ||
|
|
679aa0f764 | ||
|
|
8683efe54a | ||
|
|
f78c228c75 | ||
|
|
8dfef83f1a | ||
|
|
d707844c30 | ||
|
|
3ffea43253 | ||
|
|
77c387a559 | ||
|
|
ac1e4b8e8f | ||
|
|
36101f50c3 | ||
|
|
08dad7f7af | ||
|
|
298152bc50 | ||
|
|
993e94c00c | ||
|
|
4c3a7b84c1 | ||
|
|
8961a54a03 | ||
|
|
2184a9402f | ||
|
|
b9f1d14d4e | ||
|
|
fba154386e | ||
|
|
c8366eff8f | ||
|
|
161d9ed512 | ||
|
|
fadc9521f0 | ||
|
|
40cb47868f | ||
|
|
9d055ff4fd | ||
|
|
9d69f14faa | ||
|
|
8e3ea6c878 | ||
|
|
971f3cd63c | ||
|
|
a9d7a7e306 | ||
|
|
8797298a71 | ||
|
|
3bc182e453 | ||
|
|
7a0ab3aa15 | ||
|
|
fdbef8ee71 | ||
|
|
d95b127378 | ||
|
|
008138cd02 | ||
|
|
2a1630e068 | ||
|
|
31611203eb | ||
|
|
1ee6d16d78 | ||
|
|
0e8c3a8efe | ||
|
|
d084d19675 | ||
|
|
77954a3796 | ||
|
|
8152dc4b04 | ||
|
|
109b233e14 | ||
|
|
cc3b26998b | ||
|
|
95dea1faaa | ||
|
|
57fecdbf17 | ||
|
|
3b4bcc881f | ||
|
|
dfa4dfa4a4 | ||
|
|
8d86e97247 | ||
|
|
5da9d6b46b | ||
|
|
100809f11a | ||
|
|
5256077a3c | ||
|
|
375e66047d | ||
|
|
42d1d6e1b0 | ||
|
|
ca51fab4d8 | ||
|
|
73c983516d | ||
|
|
f733d5a4da | ||
|
|
3d2948daf3 | ||
|
|
69a5d3644a | ||
|
|
2f1018c742 | ||
|
|
d5fc37282f | ||
|
|
525ed359cd | ||
|
|
fe00db62d6 | ||
|
|
bcfa760cf9 | ||
|
|
613e8f05c2 | ||
|
|
59f8f0c7ea | ||
|
|
ae0c8deec2 | ||
|
|
b508415983 | ||
|
|
a98d014763 | ||
|
|
51e5e49d3b | ||
|
|
0eced489da | ||
|
|
5138d12942 | ||
|
|
fe30276db2 | ||
|
|
e51e8b5c8a | ||
|
|
170900e80f | ||
|
|
9991985170 |
198
CHANGELOG.md
198
CHANGELOG.md
@@ -1,3 +1,201 @@
|
||||
2.5.13 (2019-05-31)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#2813](https://github.com/digitalocean/netbox/issues/2813) - Add tenant group filters
|
||||
* [#3085](https://github.com/digitalocean/netbox/issues/3085) - Catch all exceptions during export template rendering
|
||||
* [#3138](https://github.com/digitalocean/netbox/issues/3138) - Add 2.5GE and 5GE interface form factors
|
||||
* [#3151](https://github.com/digitalocean/netbox/issues/3151) - Add inventory item count to manufacturers list
|
||||
* [#3156](https://github.com/digitalocean/netbox/issues/3156) - Add site link to rack reservations overview
|
||||
* [#3183](https://github.com/digitalocean/netbox/issues/3183) - Enable bulk deletion of sites
|
||||
* [#3185](https://github.com/digitalocean/netbox/issues/3185) - Improve performance for custom field access within templates
|
||||
* [#3186](https://github.com/digitalocean/netbox/issues/3186) - Add interface name filter for IP addresses
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#3031](https://github.com/digitalocean/netbox/issues/3031) - Fixed form field population of tags with spaces
|
||||
* [#3132](https://github.com/digitalocean/netbox/issues/3132) - Circuit termination missing from available cable termination types
|
||||
* [#3150](https://github.com/digitalocean/netbox/issues/3150) - Fix formatting of cable length during cable trace
|
||||
* [#3184](https://github.com/digitalocean/netbox/issues/3184) - Correctly display color block for white cables
|
||||
* [#3190](https://github.com/digitalocean/netbox/issues/3190) - Fix custom field rendering for Jinja2 export templates
|
||||
* [#3211](https://github.com/digitalocean/netbox/issues/3211) - Fix error handling when attempting to delete a protected object via API
|
||||
* [#3223](https://github.com/digitalocean/netbox/issues/3223) - Fix filtering devices by "has power outlets"
|
||||
* [#3227](https://github.com/digitalocean/netbox/issues/3227) - Fix exception when deleting a circuit with a termination(s)
|
||||
* [#3228](https://github.com/digitalocean/netbox/issues/3228) - Fixed login link retaining query parameters
|
||||
|
||||
---
|
||||
|
||||
2.5.12 (2019-05-01)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#3127](https://github.com/digitalocean/netbox/issues/3127) - Fix natural ordering of device components
|
||||
|
||||
---
|
||||
|
||||
2.5.11 (2019-04-29)
|
||||
|
||||
## Notes
|
||||
|
||||
This release upgrades the Django framework to version 2.2.
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#2986](https://github.com/digitalocean/netbox/issues/2986) - Improve natural ordering of device components
|
||||
* [#3023](https://github.com/digitalocean/netbox/issues/3023) - Add support for filtering cables by connected device
|
||||
* [#3070](https://github.com/digitalocean/netbox/issues/3070) - Add decommissioning status for devices
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#2621](https://github.com/digitalocean/netbox/issues/2621) - Upgrade Django requirement to 2.2 to fix object deletion issue in the changelog middleware
|
||||
* [#3072](https://github.com/digitalocean/netbox/issues/3072) - Preserve multiselect filter values when updating per-page count for list views
|
||||
* [#3112](https://github.com/digitalocean/netbox/issues/3112) - Fix ordering of interface connections list by termination B name/device
|
||||
* [#3116](https://github.com/digitalocean/netbox/issues/3116) - Fix `tagged_items` count in tags API endpoint
|
||||
* [#3118](https://github.com/digitalocean/netbox/issues/3118) - Disable `last_login` update on login when maintenance mode is enabled
|
||||
|
||||
---
|
||||
|
||||
v2.5.10 (2019-04-08)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#3052](https://github.com/digitalocean/netbox/issues/3052) - Add Jinja2 support for export templates
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#2937](https://github.com/digitalocean/netbox/issues/2937) - Redirect to list view after editing an object from list view
|
||||
* [#3036](https://github.com/digitalocean/netbox/issues/3036) - DCIM interfaces API endpoint should not include VM interfaces
|
||||
* [#3039](https://github.com/digitalocean/netbox/issues/3039) - Fix exception when retrieving change object for a component template via API
|
||||
* [#3041](https://github.com/digitalocean/netbox/issues/3041) - Fix form widget for bulk cable label update
|
||||
* [#3044](https://github.com/digitalocean/netbox/issues/3044) - Ignore site/rack fields when connecting a new cable via device search
|
||||
* [#3046](https://github.com/digitalocean/netbox/issues/3046) - Fix exception at reports API endpoint
|
||||
* [#3047](https://github.com/digitalocean/netbox/issues/3047) - Fix exception when writing mac address for an interface via API
|
||||
|
||||
---
|
||||
|
||||
v2.5.9 (2019-04-01)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#2933](https://github.com/digitalocean/netbox/issues/2933) - Add username to outbound webhook requests
|
||||
* [#3011](https://github.com/digitalocean/netbox/issues/3011) - Add SSL support for django-rq (requires django-rq v1.3.1+)
|
||||
* [#3025](https://github.com/digitalocean/netbox/issues/3025) - Add request ID to outbound webhook requests (for correlating all changes part of a single request)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#2207](https://github.com/digitalocean/netbox/issues/2207) - Fixes deterministic ordering of interfaces
|
||||
* [#2577](https://github.com/digitalocean/netbox/issues/2577) - Clarification of wording in API regarding filtering
|
||||
* [#2924](https://github.com/digitalocean/netbox/issues/2924) - Add interface type for QSFP28 50GE
|
||||
* [#2936](https://github.com/digitalocean/netbox/issues/2936) - Fix device role selection showing duplicate first entry
|
||||
* [#2998](https://github.com/digitalocean/netbox/issues/2998) - Limit device query to non-racked devices if no rack selected when creating a cable
|
||||
* [#3001](https://github.com/digitalocean/netbox/issues/3001) - Fix API representation of ObjectChange `action` and add `changed_object_type`
|
||||
* [#3014](https://github.com/digitalocean/netbox/issues/3014) - Fixes VM Role filtering
|
||||
* [#3019](https://github.com/digitalocean/netbox/issues/3019) - Fix tag population when running NetBox within a path
|
||||
* [#3022](https://github.com/digitalocean/netbox/issues/3022) - Add missing cable termination types to DCIM `_choices` endpoint
|
||||
* [#3026](https://github.com/digitalocean/netbox/issues/3026) - Tweak prefix/IP filter forms to filter using VRF ID rather than route distinguisher
|
||||
* [#3027](https://github.com/digitalocean/netbox/issues/3027) - Ignore empty local context data when rendering config contexts
|
||||
* [#3032](https://github.com/digitalocean/netbox/issues/3032) - Save assigned tags when creating a new secret
|
||||
|
||||
---
|
||||
|
||||
v2.5.8 (2019-03-11)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#2435](https://github.com/digitalocean/netbox/issues/2435) - Printer friendly CSS
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#2065](https://github.com/digitalocean/netbox/issues/2065) - Correct documentation for VM interface serializer
|
||||
* [#2705](https://github.com/digitalocean/netbox/issues/2705) - Fix endpoint grouping in API docs
|
||||
* [#2781](https://github.com/digitalocean/netbox/issues/2781) - Fix filtering of sites/devices/VMs by multiple regions
|
||||
* [#2923](https://github.com/digitalocean/netbox/issues/2923) - Provider filter form's site field should be blank by default
|
||||
* [#2938](https://github.com/digitalocean/netbox/issues/2938) - Enforce deterministic ordering of device components returned by API
|
||||
* [#2939](https://github.com/digitalocean/netbox/issues/2939) - Exclude circuit terminations from API interface connections endpoint
|
||||
* [#2940](https://github.com/digitalocean/netbox/issues/2940) - Allow CSV import of prefixes/IPs to VRF without an RD assigned
|
||||
* [#2944](https://github.com/digitalocean/netbox/issues/2944) - Record the deletion of an IP address in the changelog of its parent interface (if any)
|
||||
* [#2952](https://github.com/digitalocean/netbox/issues/2952) - Added the `slug` field to the Tenant filter for use in the API and search function
|
||||
* [#2954](https://github.com/digitalocean/netbox/issues/2954) - Remove trailing slashes to fix root/template paths on Windows
|
||||
* [#2961](https://github.com/digitalocean/netbox/issues/2961) - Prevent exception when exporting inventory items belonging to unnamed devices
|
||||
* [#2962](https://github.com/digitalocean/netbox/issues/2962) - Increase ExportTemplate `mime_type` field length
|
||||
* [#2966](https://github.com/digitalocean/netbox/issues/2966) - Accept `null` cable length_unit via API
|
||||
* [#2972](https://github.com/digitalocean/netbox/issues/2972) - Improve ContentTypeField serializer to elegantly handle invalid data
|
||||
* [#2976](https://github.com/digitalocean/netbox/issues/2976) - Add delete button to tag view
|
||||
* [#2980](https://github.com/digitalocean/netbox/issues/2980) - Improve rendering time for API docs
|
||||
* [#2982](https://github.com/digitalocean/netbox/issues/2982) - Correct CSS class assignment on color picker
|
||||
* [#2984](https://github.com/digitalocean/netbox/issues/2984) - Fix logging of unlabeled cable ID on cable deletion
|
||||
* [#2985](https://github.com/digitalocean/netbox/issues/2985) - Fix pagination page length for rack elevations
|
||||
|
||||
---
|
||||
|
||||
v2.5.7 (2019-02-21)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#2357](https://github.com/digitalocean/netbox/issues/2357) - Enable filtering of devices by rack face
|
||||
* [#2638](https://github.com/digitalocean/netbox/issues/2638) - Add button to copy unlocked secret to clipboard
|
||||
* [#2870](https://github.com/digitalocean/netbox/issues/2870) - Add Markdown rendering for provider NOC/admin contact fields
|
||||
* [#2878](https://github.com/digitalocean/netbox/issues/2878) - Add cable types for OS1/OS2 singlemode fiber
|
||||
* [#2890](https://github.com/digitalocean/netbox/issues/2890) - Add port types for APC fiber
|
||||
* [#2898](https://github.com/digitalocean/netbox/issues/2898) - Enable filtering cables list by connection status
|
||||
* [#2903](https://github.com/digitalocean/netbox/issues/2903) - Clarify purpose of tags field on interface edit form
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#2852](https://github.com/digitalocean/netbox/issues/2852) - Allow filtering devices by null rack position
|
||||
* [#2884](https://github.com/digitalocean/netbox/issues/2884) - Don't display connect button for wireless interfaces
|
||||
* [#2888](https://github.com/digitalocean/netbox/issues/2888) - Correct foreground color of device roles in rack elevations
|
||||
* [#2893](https://github.com/digitalocean/netbox/issues/2893) - Remove duplicate display of VRF RD on IP address view
|
||||
* [#2895](https://github.com/digitalocean/netbox/issues/2895) - Fix filtering of nullable character fields
|
||||
* [#2901](https://github.com/digitalocean/netbox/issues/2901) - Fix ordering regions by site count
|
||||
* [#2910](https://github.com/digitalocean/netbox/issues/2910) - Fix config context list and edit forms to use Select2 elements
|
||||
* [#2912](https://github.com/digitalocean/netbox/issues/2912) - Cable type in filter form should be blank by default
|
||||
* [#2913](https://github.com/digitalocean/netbox/issues/2913) - Fix assigned prefixes link on VRF view
|
||||
* [#2914](https://github.com/digitalocean/netbox/issues/2914) - Fix empty connected circuit link on device interfaces list
|
||||
* [#2915](https://github.com/digitalocean/netbox/issues/2915) - Fix bulk editing of pass-through ports
|
||||
|
||||
---
|
||||
|
||||
v2.5.6 (2019-02-13)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#2758](https://github.com/digitalocean/netbox/issues/2758) - Add cable trace button to pass-through ports
|
||||
* [#2839](https://github.com/digitalocean/netbox/issues/2839) - Add "110 punch" type for pass-through ports
|
||||
* [#2854](https://github.com/digitalocean/netbox/issues/2854) - Enable bulk editing of pass-through ports
|
||||
* [#2866](https://github.com/digitalocean/netbox/issues/2866) - Add cellular interface types (GSM/CDMA/LTE)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#2841](https://github.com/digitalocean/netbox/issues/2841) - Fix filtering by VRF for prefix and IP address lists
|
||||
* [#2844](https://github.com/digitalocean/netbox/issues/2844) - Correct display of far cable end for pass-through ports
|
||||
* [#2845](https://github.com/digitalocean/netbox/issues/2845) - Enable filtering of rack unit list by unit ID
|
||||
* [#2856](https://github.com/digitalocean/netbox/issues/2856) - Fix navigation links between LAG interfaces and their members on device view
|
||||
* [#2857](https://github.com/digitalocean/netbox/issues/2857) - Add `display_name` to DeviceType API serializer; fix DeviceType list for bulk device edit
|
||||
* [#2862](https://github.com/digitalocean/netbox/issues/2862) - Follow return URL when connecting a cable
|
||||
* [#2864](https://github.com/digitalocean/netbox/issues/2864) - Correct display of VRF name when no RD is assigned
|
||||
* [#2877](https://github.com/digitalocean/netbox/issues/2877) - Fixed device role label display on light background color
|
||||
* [#2880](https://github.com/digitalocean/netbox/issues/2880) - Sanitize user password if an exception is raised during login
|
||||
|
||||
---
|
||||
|
||||
v2.5.5 (2019-01-31)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#2805](https://github.com/digitalocean/netbox/issues/2805) - Allow null route distinguisher for VRFs
|
||||
* [#2809](https://github.com/digitalocean/netbox/issues/2809) - Remove VRF child prefixes table; link to main prefixes view
|
||||
* [#2825](https://github.com/digitalocean/netbox/issues/2825) - Include directly connected device for front/rear ports
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#2824](https://github.com/digitalocean/netbox/issues/2824) - Fix template exception when viewing rack elevations list
|
||||
* [#2833](https://github.com/digitalocean/netbox/issues/2833) - Fix form widget for front port template creation
|
||||
* [#2835](https://github.com/digitalocean/netbox/issues/2835) - Fix certain model filters did not support the `q` query param
|
||||
* [#2837](https://github.com/digitalocean/netbox/issues/2837) - Fix select2 nullable filter fields add multiple null_option elements when paging
|
||||
|
||||
---
|
||||
|
||||
v2.5.4 (2019-01-29)
|
||||
|
||||
## Enhancements
|
||||
|
||||
10
README.md
10
README.md
@@ -37,7 +37,7 @@ and run `upgrade.sh`.
|
||||
|
||||
## Alternative Installations
|
||||
|
||||
* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine))
|
||||
* [Docker container](https://github.com/netbox-community/netbox-docker) (via [@cimnine](https://github.com/cimnine))
|
||||
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
|
||||
* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))
|
||||
|
||||
@@ -45,13 +45,13 @@ and run `upgrade.sh`.
|
||||
|
||||
## Supported SDK
|
||||
|
||||
- [pynetbox](https://github.com/digitalocean/pynetbox) Python API client library for Netbox.
|
||||
- [pynetbox](https://github.com/digitalocean/pynetbox) - A Python API client library for Netbox
|
||||
|
||||
## Community SDK
|
||||
|
||||
- [netbox-client-ruby](https://github.com/ninech/netbox-client-ruby) A ruby client library for Netbox v2.
|
||||
- [netbox-client-ruby](https://github.com/ninech/netbox-client-ruby) - A Ruby client library for Netbox
|
||||
- [powerbox](https://github.com/BatmanAMA/powerbox) - A PowerShell library for Netbox
|
||||
|
||||
## Ansible Inventory
|
||||
|
||||
- [netbox-as-ansible-inventory](https://github.com/AAbouZaid/netbox-as-ansible-inventory) Ansible dynamic inventory script for Netbox.
|
||||
|
||||
- [netbox-as-ansible-inventory](https://github.com/AAbouZaid/netbox-as-ansible-inventory) - Ansible dynamic inventory script for Netbox
|
||||
|
||||
@@ -128,4 +128,4 @@ Reports can be run on the CLI by invoking the management command:
|
||||
python3 manage.py runreport <module>
|
||||
```
|
||||
|
||||
One or more report modules may be specified.
|
||||
where ``<module>`` is the name of the python file in the ``reports`` directory without the ``.py`` extension. One or more report modules may be specified.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Tags
|
||||
|
||||
Tags are free-form text labels which can be applied to a variety of objects within NetBox. Tags are created on-demand as they are assigned to objects. Use commas to separate tags when adding multiple tags to an object 9for example: `Inventoried, Monitored`). Use double quotes around a multi-word tag when adding only one tag, e.g. `"Core Switch"`.
|
||||
Tags are free-form text labels which can be applied to a variety of objects within NetBox. Tags are created on-demand as they are assigned to objects. Use commas to separate tags when adding multiple tags to an object (for example: `Inventoried, Monitored`). Use double quotes around a multi-word tag when adding only one tag, e.g. `"Core Switch"`.
|
||||
|
||||
Each tag has a label and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be `dunder-mifflin-inc`. The slug is generated automatically and makes tags easier to work with as URL parameters.
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ psql -c 'create database netbox'
|
||||
psql netbox < netbox.sql
|
||||
```
|
||||
|
||||
Keep in mind that PostgreSQL user accounts and permissions are not included with the dump: You will need to create those manually if you want to fully replicate the original database (see the [installation docs](installation/1-postgresql.md)). When setting up a development instance of NetBox, it's strongly recommended to use different credentials anyway.
|
||||
Keep in mind that PostgreSQL user accounts and permissions are not included with the dump: You will need to create those manually if you want to fully replicate the original database (see the [installation docs](../installation/1-postgresql.md)). When setting up a development instance of NetBox, it's strongly recommended to use different credentials anyway.
|
||||
|
||||
## Export the Database Schema
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ A list of objects retrieved via the API can be filtered by passing one or more q
|
||||
GET /api/ipam/prefixes/?status=1
|
||||
```
|
||||
|
||||
The same filter can be incldued multiple times. These will effect a logical OR and return objects matching any of the given values. For example, the following will return all active and reserved prefixes:
|
||||
Certain filters can be included multiple times within a single request. These will effect a logical OR and return objects matching any of the given values. For example, the following will return all active and reserved prefixes:
|
||||
|
||||
```
|
||||
GET /api/ipam/prefixes/?status=1&status=2
|
||||
|
||||
@@ -283,6 +283,7 @@ REDIS = {
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 0,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -315,3 +316,9 @@ The TCP port to use when connecting to the Redis server.
|
||||
Default: None
|
||||
|
||||
The password to use when authenticating to the Redis server (optional).
|
||||
|
||||
### SSL
|
||||
|
||||
Default: False
|
||||
|
||||
Use secure sockets layer to encrypt the connections to the Redis server.
|
||||
|
||||
@@ -13,6 +13,10 @@ Some devices house child devices which share physical resources, like space and
|
||||
!!! note
|
||||
This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane.
|
||||
|
||||
For that application you should create a single Device for the chassis, and add Interfaces directly to it. Interfaces can be created in bulk using range patterns, e.g. "Gi1/[1-24]".
|
||||
|
||||
Add Inventory Items if you want to record the line cards themselves as separate entities. There is no explicit relationship between each interface and its line card, but it may be implied by the naming (e.g. interfaces "Gi1/x" are on line card 1)
|
||||
|
||||
## Manufacturers
|
||||
|
||||
Each device type must be assigned to a manufacturer. The model number of a device type must be unique to its manufacturer.
|
||||
@@ -93,6 +97,10 @@ Pass-through ports can also be used to model "bump in the wire" devices, such as
|
||||
|
||||
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.
|
||||
|
||||
Child devices are first-class Devices in their own right: that is, fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and interfaces. You cannot create a LAG between interfaces in different child devices.
|
||||
|
||||
Therefore, Device bays are **not** suitable for modeling chassis-based switches and routers. These should instead be modeled as a single Device, with the line cards as Inventory Items.
|
||||
|
||||
## 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.
|
||||
@@ -111,7 +119,7 @@ The assignment of platforms to devices is an optional feature, and may be disreg
|
||||
|
||||
# Inventory Items
|
||||
|
||||
Inventory items represent hardware components installed within a device, such as a power supply or CPU. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Like device types, each item can optionally be assigned a manufacturer.
|
||||
Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Like device types, each item can optionally be assigned a manufacturer.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ An IP address can be designated as the network address translation (NAT) inside
|
||||
|
||||
A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain. Each VRF is essentially a separate routing table. VRFs are commonly used to isolate customers or organizations from one another within a network, or to route overlapping address space (e.g. multiple instances of the 10.0.0.0/8 space).
|
||||
|
||||
Each VRF is assigned a unique name and route distinguisher (RD). The RD is expected to take one of the forms prescribed in [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), however its formatting is not strictly enforced.
|
||||
Each VRF is assigned a unique name and an optional route distinguisher (RD). The RD is expected to take one of the forms prescribed in [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), however its formatting is not strictly enforced.
|
||||
|
||||
Each prefix and IP address may be assigned to one (and only one) VRF. If you have a prefix or IP address which exists in multiple VRFs, you will need to create a separate instance of it in NetBox for each VRF. Any IP prefix or address not assigned to a VRF is said to belong to the "global" table.
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ from django.db.models import Q
|
||||
|
||||
from dcim.models import Site
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NumericInFilter, TagFilter
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
|
||||
from .constants import CIRCUIT_STATUS_CHOICES
|
||||
from .models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
|
||||
|
||||
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
class ProviderFilter(CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -47,14 +47,14 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
class CircuitTypeFilter(django_filters.FilterSet):
|
||||
class CircuitTypeFilter(NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -87,16 +87,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
choices=CIRCUIT_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site',
|
||||
queryset=Site.objects.all(),
|
||||
|
||||
@@ -4,6 +4,7 @@ from taggit.forms import TagField
|
||||
from dcim.models import Site
|
||||
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.forms import TenancyFilterForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField,
|
||||
@@ -107,7 +108,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
widget=APISelect(
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
)
|
||||
@@ -265,8 +266,9 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
||||
]
|
||||
|
||||
|
||||
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
model = Circuit
|
||||
field_order = ['q', 'type', 'provider', 'status', 'site', 'tenant_group', 'tenant', 'commit_rate']
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
@@ -292,16 +294,6 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.db import models
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.constants import CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, STATUS_CLASSES
|
||||
from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES
|
||||
from dcim.fields import ASNField
|
||||
from dcim.models import CableTermination
|
||||
from extras.models import CustomFieldModel, ObjectChange
|
||||
@@ -274,15 +274,24 @@ class CircuitTermination(CableTermination):
|
||||
"""
|
||||
Reference the parent circuit when recording the change.
|
||||
"""
|
||||
try:
|
||||
related_object = self.circuit
|
||||
except Circuit.DoesNotExist:
|
||||
# Parent circuit has been deleted
|
||||
related_object = None
|
||||
ObjectChange(
|
||||
user=user,
|
||||
request_id=request_id,
|
||||
changed_object=self,
|
||||
related_object=self.circuit,
|
||||
related_object=related_object,
|
||||
action=action,
|
||||
object_data=serialize_object(self)
|
||||
).save()
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return self.circuit
|
||||
|
||||
def get_peer_termination(self):
|
||||
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
||||
try:
|
||||
|
||||
@@ -11,7 +11,7 @@ CIRCUITTYPE_ACTIONS = """
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.circuit.change_circuittype %}
|
||||
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -59,7 +59,7 @@ class CircuitTypeTable(BaseTable):
|
||||
name = tables.LinkColumn()
|
||||
circuit_count = tables.Column(verbose_name='Circuits')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name=''
|
||||
template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
|
||||
91
netbox/circuits/tests/test_views.py
Normal file
91
netbox/circuits/tests/test_views.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import urllib.parse
|
||||
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from circuits.models import Circuit, CircuitType, Provider
|
||||
|
||||
|
||||
class ProviderTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
Provider.objects.bulk_create([
|
||||
Provider(name='Provider 1', slug='provider-1', asn=65001),
|
||||
Provider(name='Provider 2', slug='provider-2', asn=65002),
|
||||
Provider(name='Provider 3', slug='provider-3', asn=65003),
|
||||
])
|
||||
|
||||
def test_provider_list(self):
|
||||
|
||||
url = reverse('circuits:provider_list')
|
||||
params = {
|
||||
"q": "test",
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_provider(self):
|
||||
|
||||
provider = Provider.objects.first()
|
||||
response = self.client.get(provider.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class CircuitTypeTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
CircuitType.objects.bulk_create([
|
||||
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
|
||||
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
|
||||
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
|
||||
])
|
||||
|
||||
def test_circuittype_list(self):
|
||||
|
||||
url = reverse('circuits:circuittype_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class CircuitTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
provider = Provider(name='Provider 1', slug='provider-1', asn=65001)
|
||||
provider.save()
|
||||
|
||||
circuittype = CircuitType(name='Circuit Type 1', slug='circuit-type-1')
|
||||
circuittype.save()
|
||||
|
||||
Circuit.objects.bulk_create([
|
||||
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
|
||||
])
|
||||
|
||||
def test_circuit_list(self):
|
||||
|
||||
url = reverse('circuits:circuit_list')
|
||||
params = {
|
||||
"provider": Provider.objects.first().slug,
|
||||
"type": CircuitType.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_provider(self):
|
||||
|
||||
provider = Provider.objects.first()
|
||||
response = self.client.get(provider.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from dcim.views import CableCreateView, CableTraceView
|
||||
from extras.views import ObjectChangeLogView
|
||||
@@ -9,41 +9,41 @@ app_name = 'circuits'
|
||||
urlpatterns = [
|
||||
|
||||
# Providers
|
||||
url(r'^providers/$', views.ProviderListView.as_view(), name='provider_list'),
|
||||
url(r'^providers/add/$', views.ProviderCreateView.as_view(), name='provider_add'),
|
||||
url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'),
|
||||
url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
|
||||
url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/$', views.ProviderView.as_view(), name='provider'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
|
||||
path(r'providers/', views.ProviderListView.as_view(), name='provider_list'),
|
||||
path(r'providers/add/', views.ProviderCreateView.as_view(), name='provider_add'),
|
||||
path(r'providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'),
|
||||
path(r'providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
|
||||
path(r'providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
||||
path(r'providers/<slug:slug>/', views.ProviderView.as_view(), name='provider'),
|
||||
path(r'providers/<slug:slug>/edit/', views.ProviderEditView.as_view(), name='provider_edit'),
|
||||
path(r'providers/<slug:slug>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'),
|
||||
path(r'providers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
|
||||
|
||||
# Circuit types
|
||||
url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'),
|
||||
url(r'^circuit-types/add/$', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
|
||||
url(r'^circuit-types/import/$', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
|
||||
url(r'^circuit-types/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
|
||||
url(r'^circuit-types/(?P<slug>[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
|
||||
url(r'^circuit-types/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
|
||||
path(r'circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
|
||||
path(r'circuit-types/add/', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
|
||||
path(r'circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
|
||||
path(r'circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
|
||||
path(r'circuit-types/<slug:slug>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
|
||||
path(r'circuit-types/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
|
||||
|
||||
# Circuits
|
||||
url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'),
|
||||
url(r'^circuits/add/$', views.CircuitCreateView.as_view(), name='circuit_add'),
|
||||
url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'),
|
||||
url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
|
||||
url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
|
||||
url(r'^circuits/(?P<pk>\d+)/$', views.CircuitView.as_view(), name='circuit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
|
||||
url(r'^circuits/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
|
||||
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
|
||||
path(r'circuits/', views.CircuitListView.as_view(), name='circuit_list'),
|
||||
path(r'circuits/add/', views.CircuitCreateView.as_view(), name='circuit_add'),
|
||||
path(r'circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'),
|
||||
path(r'circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
|
||||
path(r'circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
|
||||
path(r'circuits/<int:pk>/', views.CircuitView.as_view(), name='circuit'),
|
||||
path(r'circuits/<int:pk>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'),
|
||||
path(r'circuits/<int:pk>/delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'),
|
||||
path(r'circuits/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
|
||||
path(r'circuits/<int:pk>/terminations/swap/', views.circuit_terminations_swap, name='circuit_terminations_swap'),
|
||||
|
||||
# Circuit terminations
|
||||
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}),
|
||||
path(r'circuits/<int:circuit>/terminations/add/', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
|
||||
path(r'circuit-terminations/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
|
||||
path(r'circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
|
||||
path(r'circuit-terminations/<int:termination_a_id>/connect/', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
|
||||
path(r'circuit-terminations/<int:pk>/trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
|
||||
|
||||
]
|
||||
|
||||
@@ -100,7 +100,7 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['id', 'url', 'manufacturer', 'model', 'slug']
|
||||
fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name']
|
||||
|
||||
|
||||
class NestedRearPortTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
@@ -180,8 +181,8 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'instance_count',
|
||||
'id', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height', 'is_full_depth',
|
||||
'subdevice_role', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'instance_count',
|
||||
]
|
||||
|
||||
|
||||
@@ -502,12 +503,16 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
#
|
||||
|
||||
class CableSerializer(ValidatedModelSerializer):
|
||||
termination_a_type = ContentTypeField()
|
||||
termination_b_type = ContentTypeField()
|
||||
termination_a_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES)
|
||||
)
|
||||
termination_b_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES)
|
||||
)
|
||||
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)
|
||||
length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import F, Q
|
||||
from django.db.models import F
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_yasg import openapi
|
||||
@@ -35,7 +35,7 @@ from .exceptions import MissingFilterException
|
||||
|
||||
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(Cable, ['length_unit', 'status', 'type']),
|
||||
(Cable, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']),
|
||||
(ConsolePort, ['connection_status']),
|
||||
(Device, ['face', 'status']),
|
||||
(DeviceType, ['subdevice_role']),
|
||||
@@ -159,6 +159,11 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
exclude_pk = None
|
||||
elevation = rack.get_rack_units(face, exclude_pk)
|
||||
|
||||
# Enable filtering rack units by ID
|
||||
q = request.GET.get('q', None)
|
||||
if q:
|
||||
elevation = [u for u in elevation if q in str(u['id'])]
|
||||
|
||||
page = self.paginate_queryset(elevation)
|
||||
if page is not None:
|
||||
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
|
||||
@@ -414,7 +419,9 @@ class PowerOutletViewSet(CableTraceMixin, ModelViewSet):
|
||||
|
||||
|
||||
class InterfaceViewSet(CableTraceMixin, ModelViewSet):
|
||||
queryset = Interface.objects.select_related(
|
||||
queryset = Interface.objects.filter(
|
||||
device__isnull=False
|
||||
).select_related(
|
||||
'device', '_connected_interface', '_connected_circuittermination', 'cable'
|
||||
).prefetch_related(
|
||||
'ip_addresses', 'tags'
|
||||
@@ -491,11 +498,11 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
|
||||
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = Interface.objects.select_related(
|
||||
'device', '_connected_interface', '_connected_circuittermination'
|
||||
'device', '_connected_interface__device'
|
||||
).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)
|
||||
_connected_interface__isnull=False,
|
||||
pk__lt=F('_connected_interface')
|
||||
)
|
||||
serializer_class = serializers.InterfaceConnectionSerializer
|
||||
filterset_class = filters.InterfaceConnectionFilter
|
||||
|
||||
@@ -43,6 +43,12 @@ RACK_STATUS_CHOICES = [
|
||||
[RACK_STATUS_DEPRECATED, 'Deprecated'],
|
||||
]
|
||||
|
||||
# Device rack position
|
||||
DEVICE_POSITION_CHOICES = [
|
||||
# Rack.u_height is limited to 100
|
||||
(i, 'Unit {}'.format(i)) for i in range(1, 101)
|
||||
]
|
||||
|
||||
# Parent/child device roles
|
||||
SUBDEVICE_ROLE_PARENT = True
|
||||
SUBDEVICE_ROLE_CHILD = False
|
||||
@@ -69,6 +75,8 @@ IFACE_FF_100ME_FIXED = 800
|
||||
IFACE_FF_1GE_FIXED = 1000
|
||||
IFACE_FF_1GE_GBIC = 1050
|
||||
IFACE_FF_1GE_SFP = 1100
|
||||
IFACE_FF_2GE_FIXED = 1120
|
||||
IFACE_FF_5GE_FIXED = 1130
|
||||
IFACE_FF_10GE_FIXED = 1150
|
||||
IFACE_FF_10GE_CX4 = 1170
|
||||
IFACE_FF_10GE_SFP_PLUS = 1200
|
||||
@@ -77,6 +85,7 @@ IFACE_FF_10GE_XENPAK = 1310
|
||||
IFACE_FF_10GE_X2 = 1320
|
||||
IFACE_FF_25GE_SFP28 = 1350
|
||||
IFACE_FF_40GE_QSFP_PLUS = 1400
|
||||
IFACE_FF_50GE_QSFP28 = 1420
|
||||
IFACE_FF_100GE_CFP = 1500
|
||||
IFACE_FF_100GE_CFP2 = 1510
|
||||
IFACE_FF_100GE_CFP4 = 1520
|
||||
@@ -91,6 +100,10 @@ IFACE_FF_80211G = 2610
|
||||
IFACE_FF_80211N = 2620
|
||||
IFACE_FF_80211AC = 2630
|
||||
IFACE_FF_80211AD = 2640
|
||||
# Cellular
|
||||
IFACE_FF_GSM = 2810
|
||||
IFACE_FF_CDMA = 2820
|
||||
IFACE_FF_LTE = 2830
|
||||
# SONET
|
||||
IFACE_FF_SONET_OC3 = 6100
|
||||
IFACE_FF_SONET_OC12 = 6200
|
||||
@@ -139,6 +152,8 @@ IFACE_FF_CHOICES = [
|
||||
[
|
||||
[IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
|
||||
[IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
|
||||
[IFACE_FF_2GE_FIXED, '2.5GBASE-T (2.5GE)'],
|
||||
[IFACE_FF_5GE_FIXED, '5GBASE-T (5GE)'],
|
||||
[IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
|
||||
[IFACE_FF_10GE_CX4, '10GBASE-CX4 (10GE)'],
|
||||
]
|
||||
@@ -154,6 +169,7 @@ IFACE_FF_CHOICES = [
|
||||
[IFACE_FF_10GE_X2, 'X2 (10GE)'],
|
||||
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
|
||||
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
|
||||
[IFACE_FF_50GE_QSFP28, 'QSFP28 (50GE)'],
|
||||
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
|
||||
[IFACE_FF_100GE_CFP2, 'CFP2 (100GE)'],
|
||||
[IFACE_FF_200GE_CFP2, 'CFP2 (200GE)'],
|
||||
@@ -174,6 +190,14 @@ IFACE_FF_CHOICES = [
|
||||
[IFACE_FF_80211AD, 'IEEE 802.11ad'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Cellular',
|
||||
[
|
||||
[IFACE_FF_GSM, 'GSM'],
|
||||
[IFACE_FF_CDMA, 'CDMA'],
|
||||
[IFACE_FF_LTE, 'LTE'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'SONET',
|
||||
[
|
||||
@@ -255,18 +279,23 @@ IFACE_MODE_CHOICES = [
|
||||
|
||||
# Pass-through port types
|
||||
PORT_TYPE_8P8C = 1000
|
||||
PORT_TYPE_110_PUNCH = 1100
|
||||
PORT_TYPE_ST = 2000
|
||||
PORT_TYPE_SC = 2100
|
||||
PORT_TYPE_SC_APC = 2110
|
||||
PORT_TYPE_FC = 2200
|
||||
PORT_TYPE_LC = 2300
|
||||
PORT_TYPE_LC_APC = 2310
|
||||
PORT_TYPE_MTRJ = 2400
|
||||
PORT_TYPE_MPO = 2500
|
||||
PORT_TYPE_LSH = 2600
|
||||
PORT_TYPE_LSH_APC = 2610
|
||||
PORT_TYPE_CHOICES = [
|
||||
[
|
||||
'Copper',
|
||||
[
|
||||
[PORT_TYPE_8P8C, '8P8C'],
|
||||
[PORT_TYPE_110_PUNCH, '110 Punch'],
|
||||
],
|
||||
],
|
||||
[
|
||||
@@ -274,10 +303,13 @@ PORT_TYPE_CHOICES = [
|
||||
[
|
||||
[PORT_TYPE_FC, 'FC'],
|
||||
[PORT_TYPE_LC, 'LC'],
|
||||
[PORT_TYPE_LC_APC, 'LC/APC'],
|
||||
[PORT_TYPE_LSH, 'LSH'],
|
||||
[PORT_TYPE_LSH_APC, 'LSH/APC'],
|
||||
[PORT_TYPE_MPO, 'MPO'],
|
||||
[PORT_TYPE_MTRJ, 'MTRJ'],
|
||||
[PORT_TYPE_SC, 'SC'],
|
||||
[PORT_TYPE_SC_APC, 'SC/APC'],
|
||||
[PORT_TYPE_ST, 'ST'],
|
||||
]
|
||||
]
|
||||
@@ -290,6 +322,7 @@ DEVICE_STATUS_PLANNED = 2
|
||||
DEVICE_STATUS_STAGED = 3
|
||||
DEVICE_STATUS_FAILED = 4
|
||||
DEVICE_STATUS_INVENTORY = 5
|
||||
DEVICE_STATUS_DECOMMISSIONING = 6
|
||||
DEVICE_STATUS_CHOICES = [
|
||||
[DEVICE_STATUS_ACTIVE, 'Active'],
|
||||
[DEVICE_STATUS_OFFLINE, 'Offline'],
|
||||
@@ -297,6 +330,7 @@ DEVICE_STATUS_CHOICES = [
|
||||
[DEVICE_STATUS_STAGED, 'Staged'],
|
||||
[DEVICE_STATUS_FAILED, 'Failed'],
|
||||
[DEVICE_STATUS_INVENTORY, 'Inventory'],
|
||||
[DEVICE_STATUS_DECOMMISSIONING, 'Decommissioning'],
|
||||
]
|
||||
|
||||
# Site statuses
|
||||
@@ -317,6 +351,7 @@ STATUS_CLASSES = {
|
||||
3: 'primary',
|
||||
4: 'danger',
|
||||
5: 'default',
|
||||
6: 'warning',
|
||||
}
|
||||
|
||||
# Console/power/interface connection statuses
|
||||
@@ -329,7 +364,7 @@ CONNECTION_STATUS_CHOICES = [
|
||||
|
||||
# Cable endpoint types
|
||||
CABLE_TERMINATION_TYPES = [
|
||||
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport',
|
||||
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination',
|
||||
]
|
||||
|
||||
# Cable types
|
||||
@@ -341,11 +376,14 @@ CABLE_TYPE_CAT6A = 1610
|
||||
CABLE_TYPE_CAT7 = 1700
|
||||
CABLE_TYPE_DAC_ACTIVE = 1800
|
||||
CABLE_TYPE_DAC_PASSIVE = 1810
|
||||
CABLE_TYPE_MMF = 3000
|
||||
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_SMF_OS1 = 3510
|
||||
CABLE_TYPE_SMF_OS2 = 3520
|
||||
CABLE_TYPE_AOC = 3800
|
||||
CABLE_TYPE_POWER = 5000
|
||||
CABLE_TYPE_CHOICES = (
|
||||
@@ -363,11 +401,14 @@ CABLE_TYPE_CHOICES = (
|
||||
),
|
||||
(
|
||||
'Fiber', (
|
||||
(CABLE_TYPE_MMF, 'Multimode 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_SMF_OS1, 'Singlemode Fiber (OS1)'),
|
||||
(CABLE_TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'),
|
||||
(CABLE_TYPE_AOC, 'Active Optical Cabling (AOC)'),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -31,7 +31,7 @@ class MACAddressField(models.Field):
|
||||
try:
|
||||
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
|
||||
except AddrFormatError as e:
|
||||
raise ValidationError(e)
|
||||
raise ValidationError("Invalid MAC address format: {}".format(value))
|
||||
|
||||
def db_type(self, connection):
|
||||
return 'macaddr'
|
||||
|
||||
@@ -6,9 +6,12 @@ from netaddr import EUI
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.constants import COLOR_CHOICES
|
||||
from utilities.filters import NullableCharFieldFilter, NumericInFilter, TagFilter
|
||||
from utilities.filters import (
|
||||
NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
from .constants import *
|
||||
from .models import (
|
||||
@@ -19,11 +22,7 @@ from .models import (
|
||||
)
|
||||
|
||||
|
||||
class RegionFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class RegionFilter(NameSlugSearchFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
label='Parent region (ID)',
|
||||
@@ -39,17 +38,8 @@ class RegionFilter(django_filters.FilterSet):
|
||||
model = Region
|
||||
fields = ['name', 'slug']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(slug__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
class SiteFilter(TenancyFilterSet, CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -62,25 +52,16 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
choices=SITE_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
region_id = django_filters.NumberFilter(
|
||||
method='filter_region',
|
||||
field_name='pk',
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='region__in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = django_filters.CharFilter(
|
||||
method='filter_region',
|
||||
field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='region__in',
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
label='Region (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
@@ -108,22 +89,8 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def filter_region(self, queryset, name, value):
|
||||
try:
|
||||
region = Region.objects.get(**{name: value})
|
||||
except ObjectDoesNotExist:
|
||||
return queryset.none()
|
||||
return queryset.filter(
|
||||
Q(region=region) |
|
||||
Q(region__in=region.get_descendants())
|
||||
)
|
||||
|
||||
|
||||
class RackGroupFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class RackGroupFilter(NameSlugSearchFilterSet):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@@ -139,24 +106,15 @@ class RackGroupFilter(django_filters.FilterSet):
|
||||
model = RackGroup
|
||||
fields = ['site_id', 'name', 'slug']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(slug__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class RackRoleFilter(django_filters.FilterSet):
|
||||
class RackRoleFilter(NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ['name', 'slug', 'color']
|
||||
|
||||
|
||||
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
class RackFilter(TenancyFilterSet, CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -186,16 +144,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
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
|
||||
@@ -232,7 +180,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
class RackReservationFilter(django_filters.FilterSet):
|
||||
class RackReservationFilter(TenancyFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -267,16 +215,6 @@ class RackReservationFilter(django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
label='User (ID)',
|
||||
@@ -303,7 +241,7 @@ class RackReservationFilter(django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ManufacturerFilter(django_filters.FilterSet):
|
||||
class ManufacturerFilter(NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
@@ -393,7 +331,7 @@ class DeviceTypeFilter(CustomFieldFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||
class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
|
||||
devicetype_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DeviceType.objects.all(),
|
||||
field_name='device_type_id',
|
||||
@@ -457,14 +395,14 @@ class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class DeviceRoleFilter(django_filters.FilterSet):
|
||||
class DeviceRoleFilter(NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ['name', 'slug', 'color', 'vm_role']
|
||||
|
||||
|
||||
class PlatformFilter(django_filters.FilterSet):
|
||||
class PlatformFilter(NameSlugSearchFilterSet):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@@ -482,7 +420,7 @@ class PlatformFilter(django_filters.FilterSet):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class DeviceFilter(CustomFieldFilterSet):
|
||||
class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -517,16 +455,6 @@ class DeviceFilter(CustomFieldFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Platform.objects.all(),
|
||||
label='Platform (ID)',
|
||||
@@ -539,14 +467,15 @@ class DeviceFilter(CustomFieldFilterSet):
|
||||
)
|
||||
name = NullableCharFieldFilter()
|
||||
asset_tag = NullableCharFieldFilter()
|
||||
region_id = django_filters.NumberFilter(
|
||||
method='filter_region',
|
||||
field_name='pk',
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = django_filters.CharFilter(
|
||||
method='filter_region',
|
||||
field_name='slug',
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@@ -569,6 +498,10 @@ class DeviceFilter(CustomFieldFilterSet):
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack (ID)',
|
||||
)
|
||||
position = django_filters.ChoiceFilter(
|
||||
choices=DEVICE_POSITION_CHOICES,
|
||||
null_label='Non-racked'
|
||||
)
|
||||
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Cluster.objects.all(),
|
||||
label='VM cluster (ID)',
|
||||
@@ -628,7 +561,7 @@ class DeviceFilter(CustomFieldFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['serial', 'position']
|
||||
fields = ['serial', 'face']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -641,16 +574,6 @@ class DeviceFilter(CustomFieldFilterSet):
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
def filter_region(self, queryset, name, value):
|
||||
try:
|
||||
region = Region.objects.get(**{name: value})
|
||||
except ObjectDoesNotExist:
|
||||
return queryset.none()
|
||||
return queryset.filter(
|
||||
Q(site__region=region) |
|
||||
Q(site__region__in=region.get_descendants())
|
||||
)
|
||||
|
||||
def _mac_address(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
@@ -683,7 +606,7 @@ class DeviceFilter(CustomFieldFilterSet):
|
||||
return queryset.exclude(powerports__isnull=value)
|
||||
|
||||
def _power_outlets(self, queryset, name, value):
|
||||
return queryset.exclude(poweroutlets_isnull=value)
|
||||
return queryset.exclude(poweroutlets__isnull=value)
|
||||
|
||||
def _interfaces(self, queryset, name, value):
|
||||
return queryset.exclude(interfaces__isnull=value)
|
||||
@@ -696,6 +619,10 @@ class DeviceFilter(CustomFieldFilterSet):
|
||||
|
||||
|
||||
class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
device_id = django_filters.ModelChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
@@ -707,6 +634,13 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortFilter(DeviceComponentFilterSet):
|
||||
cabled = django_filters.BooleanFilter(
|
||||
@@ -995,6 +929,14 @@ class CableFilter(django_filters.FilterSet):
|
||||
color = django_filters.MultipleChoiceFilter(
|
||||
choices=COLOR_CHOICES
|
||||
)
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_connected_device',
|
||||
field_name='name'
|
||||
)
|
||||
device_id = django_filters.CharFilter(
|
||||
method='filter_connected_device',
|
||||
field_name='pk'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
@@ -1005,6 +947,16 @@ class CableFilter(django_filters.FilterSet):
|
||||
return queryset
|
||||
return queryset.filter(label__icontains=value)
|
||||
|
||||
def filter_connected_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
try:
|
||||
device = Device.objects.get(**{name: value})
|
||||
except ObjectDoesNotExist:
|
||||
return queryset.none()
|
||||
cable_pks = device.get_cables(pk_list=True)
|
||||
return queryset.filter(pk__in=cable_pks)
|
||||
|
||||
|
||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
|
||||
@@ -13,7 +13,8 @@ from timezone_field import TimeZoneFormField
|
||||
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from ipam.models import IPAddress, VLAN, VLANGroup
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from tenancy.forms import TenancyFilterForm
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
||||
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField,
|
||||
@@ -256,8 +257,9 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
||||
]
|
||||
|
||||
|
||||
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
model = Site
|
||||
field_order = ['q', 'status', 'region', 'tenant_group', 'tenant']
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
@@ -276,16 +278,6 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -596,8 +588,9 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
||||
]
|
||||
|
||||
|
||||
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
model = Rack
|
||||
field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
@@ -619,16 +612,6 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=RACK_STATUS_CHOICES,
|
||||
required=False,
|
||||
@@ -689,40 +672,6 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
|
||||
return unit_choices
|
||||
|
||||
|
||||
class RackReservationFilterForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
group_id = FilterChoiceField(
|
||||
queryset=RackGroup.objects.select_related('site'),
|
||||
label='Rack group',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/rack-groups/",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RackReservation.objects.all(),
|
||||
@@ -751,6 +700,31 @@ class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
|
||||
field_order = ['q', 'site', 'group_id', 'tenant_group', 'tenant']
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
group_id = FilterChoiceField(
|
||||
queryset=RackGroup.objects.select_related('site'),
|
||||
label='Rack group',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/rack-groups/",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
@@ -1066,7 +1040,6 @@ class FrontPortTemplateCreateForm(ComponentForm):
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.',
|
||||
widget=StaticSelect2(),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -1600,7 +1573,8 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
||||
required=False,
|
||||
label='Type',
|
||||
widget=APISelect(
|
||||
api_url="/api/dcim/device-types/"
|
||||
api_url="/api/dcim/device-types/",
|
||||
display_field='display_name'
|
||||
)
|
||||
)
|
||||
device_role = forms.ModelChoiceField(
|
||||
@@ -1643,8 +1617,12 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
||||
]
|
||||
|
||||
|
||||
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class DeviceFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
model = Device
|
||||
field_order = [
|
||||
'q', 'region', 'site', 'rack_group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant',
|
||||
'manufacturer_id', 'device_type_id', 'mac_address', 'has_primary_ip',
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
@@ -1700,17 +1678,6 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/device-roles/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
manufacturer_id = FilterChoiceField(
|
||||
@@ -2360,6 +2327,27 @@ class FrontPortCreateForm(ComponentForm):
|
||||
}
|
||||
|
||||
|
||||
class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=FrontPort.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PORT_TYPE_CHOICES),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'description',
|
||||
]
|
||||
|
||||
|
||||
class FrontPortBulkRenameForm(BulkRenameForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=FrontPort.objects.all(),
|
||||
@@ -2413,6 +2401,27 @@ class RearPortCreateForm(ComponentForm):
|
||||
)
|
||||
|
||||
|
||||
class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RearPort.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PORT_TYPE_CHOICES),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'description',
|
||||
]
|
||||
|
||||
|
||||
class RearPortBulkRenameForm(BulkRenameForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RearPort.objects.all(),
|
||||
@@ -2665,12 +2674,12 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
|
||||
required=False,
|
||||
widget=StaticSelect2(),
|
||||
initial=''
|
||||
)
|
||||
label = forms.CharField(
|
||||
max_length=100,
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
required=False
|
||||
)
|
||||
color = forms.CharField(
|
||||
max_length=6,
|
||||
@@ -2711,15 +2720,24 @@ class CableFilterForm(BootstrapMixin, forms.Form):
|
||||
label='Search'
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=CABLE_TYPE_CHOICES,
|
||||
choices=add_blank_choice(CABLE_TYPE_CHOICES),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
required=False,
|
||||
choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
color = forms.CharField(
|
||||
max_length=6,
|
||||
required=False,
|
||||
widget=ColorSelect()
|
||||
)
|
||||
device = forms.CharField(
|
||||
required=False,
|
||||
label='Device name'
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -3055,9 +3073,31 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
tenant_group = FilterChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenant-groups/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
filter_for={
|
||||
'tenant': 'group'
|
||||
}
|
||||
)
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -14,22 +14,6 @@ CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*:(\d{{1,9}})(\.\d{{1,9}})?$')
|
||||
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):
|
||||
@@ -64,11 +48,15 @@ class InterfaceManager(Manager):
|
||||
|
||||
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.
|
||||
|
||||
The `id` field is included to enforce deterministic ordering of interfaces in similar vein of other device
|
||||
components.
|
||||
"""
|
||||
|
||||
sql_col = '{}.name'.format(self.model._meta.db_table)
|
||||
ordering = [
|
||||
'_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name',
|
||||
'_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name', 'pk'
|
||||
|
||||
]
|
||||
|
||||
fields = {
|
||||
|
||||
38
netbox/dcim/migrations/0069_deprecate_nullablecharfield.py
Normal file
38
netbox/dcim/migrations/0069_deprecate_nullablecharfield.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 2.1.5 on 2019-02-14 14:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0068_rack_new_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='asset_tag',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=64, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='asset_tag',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='asset_tag',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='facility_id',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
]
|
||||
@@ -16,14 +16,14 @@ from taggit.managers import TaggableManager
|
||||
from timezone_field import TimeZoneField
|
||||
|
||||
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange
|
||||
from utilities.fields import ColorField, NullableCharField
|
||||
from utilities.fields import ColorField
|
||||
from utilities.managers import NaturalOrderingManager
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.utils import serialize_object, to_meters
|
||||
from .constants import *
|
||||
from .exceptions import LoopDetected
|
||||
from .fields import ASNField, MACAddressField
|
||||
from .managers import DeviceComponentManager, InterfaceManager
|
||||
from .managers import InterfaceManager
|
||||
|
||||
|
||||
class ComponentTemplateModel(models.Model):
|
||||
@@ -68,6 +68,10 @@ class ComponentModel(models.Model):
|
||||
object_data=serialize_object(self)
|
||||
).save()
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return getattr(self, 'device', None)
|
||||
|
||||
|
||||
class CableTermination(models.Model):
|
||||
cable = models.ForeignKey(
|
||||
@@ -162,6 +166,14 @@ class CableTermination(models.Model):
|
||||
|
||||
return path + next_segment
|
||||
|
||||
def get_cable_peer(self):
|
||||
if self.cable is None:
|
||||
return None
|
||||
if self._cabled_as_a.exists():
|
||||
return self.cable.termination_b
|
||||
if self._cabled_as_b.exists():
|
||||
return self.cable.termination_a
|
||||
|
||||
|
||||
#
|
||||
# Regions
|
||||
@@ -205,8 +217,7 @@ class Region(MPTTModel, ChangeLoggedModel):
|
||||
self.parent.name if self.parent else None,
|
||||
)
|
||||
|
||||
@property
|
||||
def site_count(self):
|
||||
def get_site_count(self):
|
||||
return Site.objects.filter(
|
||||
Q(region=self) |
|
||||
Q(region__in=self.get_descendants())
|
||||
@@ -458,7 +469,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
facility_id = NullableCharField(
|
||||
facility_id = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
null=True,
|
||||
@@ -499,7 +510,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
blank=True,
|
||||
verbose_name='Serial number'
|
||||
)
|
||||
asset_tag = NullableCharField(
|
||||
asset_tag = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
null=True,
|
||||
@@ -968,7 +979,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
})
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
def display_name(self):
|
||||
return '{} {}'.format(self.manufacturer.name, self.model)
|
||||
|
||||
@property
|
||||
@@ -993,7 +1004,7 @@ class ConsolePortTemplate(ComponentTemplateModel):
|
||||
max_length=50
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
objects = NaturalOrderingManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
@@ -1016,7 +1027,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
|
||||
max_length=50
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
objects = NaturalOrderingManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
@@ -1039,7 +1050,7 @@ class PowerPortTemplate(ComponentTemplateModel):
|
||||
max_length=50
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
objects = NaturalOrderingManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
@@ -1062,7 +1073,7 @@ class PowerOutletTemplate(ComponentTemplateModel):
|
||||
max_length=50
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
objects = NaturalOrderingManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
@@ -1128,7 +1139,7 @@ class FrontPortTemplate(ComponentTemplateModel):
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
objects = NaturalOrderingManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
@@ -1177,7 +1188,7 @@ class RearPortTemplate(ComponentTemplateModel):
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
objects = NaturalOrderingManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
@@ -1200,7 +1211,7 @@ class DeviceBayTemplate(ComponentTemplateModel):
|
||||
max_length=50
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
objects = NaturalOrderingManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
@@ -1342,7 +1353,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
name = NullableCharField(
|
||||
name = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
null=True,
|
||||
@@ -1353,7 +1364,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
blank=True,
|
||||
verbose_name='Serial number'
|
||||
)
|
||||
asset_tag = NullableCharField(
|
||||
asset_tag = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
null=True,
|
||||
@@ -1693,6 +1704,21 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False)
|
||||
return Interface.objects.filter(filter)
|
||||
|
||||
def get_cables(self, pk_list=False):
|
||||
"""
|
||||
Return a QuerySet or PK list matching all Cables connected to a component of this Device.
|
||||
"""
|
||||
cable_pks = []
|
||||
for component_model in [
|
||||
ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, FrontPort, RearPort
|
||||
]:
|
||||
cable_pks += component_model.objects.filter(
|
||||
device=self, cable__isnull=False
|
||||
).values_list('cable', flat=True)
|
||||
if pk_list:
|
||||
return cable_pks
|
||||
return Cable.objects.filter(pk__in=cable_pks)
|
||||
|
||||
def get_children(self):
|
||||
"""
|
||||
Return the set of child Devices installed in DeviceBays within this Device.
|
||||
@@ -1731,7 +1757,7 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
csv_headers = ['device', 'name']
|
||||
@@ -1774,7 +1800,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
csv_headers = ['device', 'name']
|
||||
@@ -1823,7 +1849,7 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
csv_headers = ['device', 'name']
|
||||
@@ -1866,7 +1892,7 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
csv_headers = ['device', 'name']
|
||||
@@ -2187,7 +2213,7 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
|
||||
@@ -2253,7 +2279,7 @@ class RearPort(CableTermination, ComponentModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
csv_headers = ['device', 'name', 'type', 'positions', 'description']
|
||||
@@ -2300,7 +2326,7 @@ class DeviceBay(ComponentModel):
|
||||
null=True
|
||||
)
|
||||
|
||||
objects = DeviceComponentManager()
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
csv_headers = ['device', 'name', 'installed_device']
|
||||
@@ -2377,7 +2403,7 @@ class InventoryItem(ComponentModel):
|
||||
verbose_name='Serial number',
|
||||
blank=True
|
||||
)
|
||||
asset_tag = NullableCharField(
|
||||
asset_tag = models.CharField(
|
||||
max_length=50,
|
||||
unique=True,
|
||||
blank=True,
|
||||
@@ -2412,7 +2438,7 @@ class InventoryItem(ComponentModel):
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.name or '{' + self.device.pk + '}',
|
||||
self.device.name or '{{{}}}'.format(self.device.pk),
|
||||
self.name,
|
||||
self.manufacturer.name if self.manufacturer else None,
|
||||
self.part_id,
|
||||
@@ -2546,16 +2572,15 @@ class Cable(ChangeLoggedModel):
|
||||
('termination_b_type', 'termination_b_id'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Create an ID string for use by __str__(). We have to save a copy of pk since it's nullified after .delete()
|
||||
# is called.
|
||||
self.id_string = '#{}'.format(self.pk)
|
||||
|
||||
def __str__(self):
|
||||
return self.label or self.id_string
|
||||
if self.label:
|
||||
return self.label
|
||||
|
||||
# Save a copy of the PK on the instance since it's nullified if .delete() is called
|
||||
if not hasattr(self, 'id_string'):
|
||||
self.id_string = '#{}'.format(self.pk)
|
||||
|
||||
return self.id_string
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:cable', args=[self.pk])
|
||||
@@ -2640,6 +2665,9 @@ class Cable(ChangeLoggedModel):
|
||||
self.length_unit,
|
||||
)
|
||||
|
||||
def get_status_class(self):
|
||||
return 'success' if self.status else 'info'
|
||||
|
||||
def get_path_endpoints(self):
|
||||
"""
|
||||
Traverse both ends of a cable path and return its connected endpoints. Note that one or both endpoints may be
|
||||
|
||||
@@ -44,7 +44,7 @@ REGION_ACTIONS = """
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_region %}
|
||||
<a href="{% url 'dcim:region_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'dcim:region_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -56,7 +56,7 @@ RACKGROUP_ACTIONS = """
|
||||
<i class="fa fa-eye"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_rackgroup %}
|
||||
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning" title="Edit">
|
||||
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning" title="Edit">
|
||||
<i class="glyphicon glyphicon-pencil"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -67,7 +67,7 @@ RACKROLE_ACTIONS = """
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_rackrole %}
|
||||
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -88,7 +88,7 @@ RACKRESERVATION_ACTIONS = """
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_rackreservation %}
|
||||
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -97,7 +97,7 @@ MANUFACTURER_ACTIONS = """
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_manufacturer %}
|
||||
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -106,7 +106,7 @@ DEVICEROLE_ACTIONS = """
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_devicerole %}
|
||||
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -131,12 +131,13 @@ PLATFORM_ACTIONS = """
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_platform %}
|
||||
<a href="{% url 'dcim:platform_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'dcim:platform_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
DEVICE_ROLE = """
|
||||
<label class="label" style="background-color: #{{ record.device_role.color }}">{{ value }}</label>
|
||||
{% load helpers %}
|
||||
<label class="label" style="color: {{ record.device_role.color|fgcolor }}; background-color: #{{ record.device_role.color }}">{{ value }}</label>
|
||||
"""
|
||||
|
||||
STATUS_LABEL = """
|
||||
@@ -167,7 +168,7 @@ VIRTUALCHASSIS_ACTIONS = """
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_virtualchassis %}
|
||||
<a href="{% url 'dcim:virtualchassis_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'dcim:virtualchassis_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -195,7 +196,7 @@ class RegionTable(BaseTable):
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=REGION_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
@@ -238,7 +239,7 @@ class RackGroupTable(BaseTable):
|
||||
slug = tables.Column()
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKGROUP_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
@@ -257,7 +258,7 @@ class RackRoleTable(BaseTable):
|
||||
rack_count = tables.Column(verbose_name='Racks')
|
||||
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@@ -304,16 +305,21 @@ class RackDetailTable(RackTable):
|
||||
|
||||
class RackReservationTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
site = tables.LinkColumn(
|
||||
viewname='dcim:site',
|
||||
accessor=Accessor('rack.site'),
|
||||
args=[Accessor('rack.site.slug')],
|
||||
)
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||
unit_list = tables.Column(orderable=False, verbose_name='Units')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name=''
|
||||
template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackReservation
|
||||
fields = ('pk', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions')
|
||||
fields = ('pk', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions')
|
||||
|
||||
|
||||
#
|
||||
@@ -322,16 +328,26 @@ class RackReservationTable(BaseTable):
|
||||
|
||||
class ManufacturerTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
devicetype_count = tables.Column(verbose_name='Device Types')
|
||||
platform_count = tables.Column(verbose_name='Platforms')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name='')
|
||||
name = tables.LinkColumn()
|
||||
devicetype_count = tables.Column(
|
||||
verbose_name='Device Types'
|
||||
)
|
||||
inventoryitem_count = tables.Column(
|
||||
verbose_name='Inventory Items'
|
||||
)
|
||||
platform_count = tables.Column(
|
||||
verbose_name='Platforms'
|
||||
)
|
||||
slug = tables.Column()
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=MANUFACTURER_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Manufacturer
|
||||
fields = ('pk', 'name', 'devicetype_count', 'platform_count', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'slug', 'actions')
|
||||
|
||||
|
||||
#
|
||||
@@ -462,7 +478,7 @@ class DeviceRoleTable(BaseTable):
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=DEVICEROLE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
@@ -491,7 +507,7 @@ class PlatformTable(BaseTable):
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=PLATFORM_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
@@ -517,7 +533,7 @@ class DeviceTable(BaseTable):
|
||||
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
||||
device_type = tables.LinkColumn(
|
||||
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
||||
text=lambda record: record.device_type.full_name
|
||||
text=lambda record: record.device_type.display_name
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@@ -646,6 +662,9 @@ class CableTable(BaseTable):
|
||||
orderable=False,
|
||||
verbose_name=''
|
||||
)
|
||||
status = tables.TemplateColumn(
|
||||
template_code=STATUS_LABEL
|
||||
)
|
||||
length = tables.TemplateColumn(
|
||||
template_code=CABLE_LENGTH,
|
||||
order_by='_abs_length'
|
||||
@@ -729,18 +748,18 @@ class InterfaceConnectionTable(BaseTable):
|
||||
)
|
||||
device_b = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
accessor=Accessor('connected_endpoint.device'),
|
||||
args=[Accessor('connected_endpoint.device.pk')],
|
||||
accessor=Accessor('_connected_interface.device'),
|
||||
args=[Accessor('_connected_interface.device.pk')],
|
||||
verbose_name='Device B'
|
||||
)
|
||||
interface_b = tables.LinkColumn(
|
||||
viewname='dcim:interface',
|
||||
accessor=Accessor('connected_endpoint.name'),
|
||||
args=[Accessor('connected_endpoint.pk')],
|
||||
accessor=Accessor('_connected_interface'),
|
||||
args=[Accessor('_connected_interface.pk')],
|
||||
verbose_name='Interface B'
|
||||
)
|
||||
description_b = tables.Column(
|
||||
accessor=Accessor('connected_endpoint.description'),
|
||||
accessor=Accessor('_connected_interface.description'),
|
||||
verbose_name='Description'
|
||||
)
|
||||
|
||||
@@ -775,7 +794,7 @@ class VirtualChassisTable(BaseTable):
|
||||
member_count = tables.Column(verbose_name='Members')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=VIRTUALCHASSIS_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
|
||||
@@ -855,7 +855,7 @@ class DeviceTypeTest(APITestCase):
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'manufacturer', 'model', 'slug', 'url']
|
||||
['display_name', 'id', 'manufacturer', 'model', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_devicetype(self):
|
||||
|
||||
458
netbox/dcim/tests/test_views.py
Normal file
458
netbox/dcim/tests/test_views.py
Normal file
@@ -0,0 +1,458 @@
|
||||
import urllib.parse
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.constants import CABLE_TYPE_CAT6, IFACE_FF_1GE_FIXED
|
||||
from dcim.models import (
|
||||
Cable, Device, DeviceRole, DeviceType, Interface, InventoryItem, Manufacturer, Platform, Rack, RackGroup,
|
||||
RackReservation, RackRole, Site, Region, VirtualChassis,
|
||||
)
|
||||
|
||||
|
||||
class RegionTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
# Create three Regions
|
||||
for i in range(1, 4):
|
||||
Region(name='Region {}'.format(i), slug='region-{}'.format(i)).save()
|
||||
|
||||
def test_region_list(self):
|
||||
|
||||
url = reverse('dcim:region_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class SiteTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
region = Region(name='Region 1', slug='region-1')
|
||||
region.save()
|
||||
|
||||
Site.objects.bulk_create([
|
||||
Site(name='Site 1', slug='site-1', region=region),
|
||||
Site(name='Site 2', slug='site-2', region=region),
|
||||
Site(name='Site 3', slug='site-3', region=region),
|
||||
])
|
||||
|
||||
def test_site_list(self):
|
||||
|
||||
url = reverse('dcim:site_list')
|
||||
params = {
|
||||
"region": Region.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_site(self):
|
||||
|
||||
site = Site.objects.first()
|
||||
response = self.client.get(site.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class RackGroupTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
RackGroup.objects.bulk_create([
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=site),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=site),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=site),
|
||||
])
|
||||
|
||||
def test_rackgroup_list(self):
|
||||
|
||||
url = reverse('dcim:rackgroup_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class RackTypeTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
RackRole.objects.bulk_create([
|
||||
RackRole(name='Rack Role 1', slug='rack-role-1'),
|
||||
RackRole(name='Rack Role 2', slug='rack-role-2'),
|
||||
RackRole(name='Rack Role 3', slug='rack-role-3'),
|
||||
])
|
||||
|
||||
def test_rackrole_list(self):
|
||||
|
||||
url = reverse('dcim:rackrole_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class RackReservationTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
User = get_user_model()
|
||||
user = User(username='testuser', email='testuser@example.com')
|
||||
user.save()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
rack = Rack(name='Rack 1', site=site)
|
||||
rack.save()
|
||||
|
||||
RackReservation.objects.bulk_create([
|
||||
RackReservation(rack=rack, user=user, units=[1, 2, 3], description='Reservation 1'),
|
||||
RackReservation(rack=rack, user=user, units=[4, 5, 6], description='Reservation 2'),
|
||||
RackReservation(rack=rack, user=user, units=[7, 8, 9], description='Reservation 3'),
|
||||
])
|
||||
|
||||
def test_rackreservation_list(self):
|
||||
|
||||
url = reverse('dcim:rackreservation_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class RackTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
Rack.objects.bulk_create([
|
||||
Rack(name='Rack 1', site=site),
|
||||
Rack(name='Rack 2', site=site),
|
||||
Rack(name='Rack 3', site=site),
|
||||
])
|
||||
|
||||
def test_rack_list(self):
|
||||
|
||||
url = reverse('dcim:rack_list')
|
||||
params = {
|
||||
"site": Site.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_rack(self):
|
||||
|
||||
rack = Rack.objects.first()
|
||||
response = self.client.get(rack.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class ManufacturerTypeTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
Manufacturer.objects.bulk_create([
|
||||
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
|
||||
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
|
||||
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
|
||||
])
|
||||
|
||||
def test_manufacturer_list(self):
|
||||
|
||||
url = reverse('dcim:manufacturer_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class DeviceTypeTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
|
||||
manufacturer.save()
|
||||
|
||||
DeviceType.objects.bulk_create([
|
||||
DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturer),
|
||||
DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturer),
|
||||
DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturer),
|
||||
])
|
||||
|
||||
def test_devicetype_list(self):
|
||||
|
||||
url = reverse('dcim:devicetype_list')
|
||||
params = {
|
||||
"manufacturer": Manufacturer.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_devicetype(self):
|
||||
|
||||
devicetype = DeviceType.objects.first()
|
||||
response = self.client.get(devicetype.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class DeviceRoleTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
DeviceRole.objects.bulk_create([
|
||||
DeviceRole(name='Device Role 1', slug='device-role-1'),
|
||||
DeviceRole(name='Device Role 2', slug='device-role-2'),
|
||||
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
||||
])
|
||||
|
||||
def test_devicerole_list(self):
|
||||
|
||||
url = reverse('dcim:devicerole_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class PlatformTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
Platform.objects.bulk_create([
|
||||
Platform(name='Platform 1', slug='platform-1'),
|
||||
Platform(name='Platform 2', slug='platform-2'),
|
||||
Platform(name='Platform 3', slug='platform-3'),
|
||||
])
|
||||
|
||||
def test_platform_list(self):
|
||||
|
||||
url = reverse('dcim:platform_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class DeviceTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
|
||||
manufacturer.save()
|
||||
|
||||
devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
|
||||
devicetype.save()
|
||||
|
||||
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
|
||||
devicerole.save()
|
||||
|
||||
Device.objects.bulk_create([
|
||||
Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole),
|
||||
Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole),
|
||||
Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole),
|
||||
])
|
||||
|
||||
def test_device_list(self):
|
||||
|
||||
url = reverse('dcim:device_list')
|
||||
params = {
|
||||
"device_type_id": DeviceType.objects.first().pk,
|
||||
"role": DeviceRole.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_device(self):
|
||||
|
||||
device = Device.objects.first()
|
||||
response = self.client.get(device.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class InventoryItemTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
|
||||
manufacturer.save()
|
||||
|
||||
devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
|
||||
devicetype.save()
|
||||
|
||||
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
|
||||
devicerole.save()
|
||||
|
||||
device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
|
||||
device.save()
|
||||
|
||||
InventoryItem.objects.bulk_create([
|
||||
InventoryItem(device=device, name='Inventory Item 1'),
|
||||
InventoryItem(device=device, name='Inventory Item 2'),
|
||||
InventoryItem(device=device, name='Inventory Item 3'),
|
||||
])
|
||||
|
||||
def test_inventoryitem_list(self):
|
||||
|
||||
url = reverse('dcim:inventoryitem_list')
|
||||
params = {
|
||||
"device_id": Device.objects.first().pk,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_inventoryitem(self):
|
||||
|
||||
inventoryitem = InventoryItem.objects.first()
|
||||
response = self.client.get(inventoryitem.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class CableTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
|
||||
manufacturer.save()
|
||||
|
||||
devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
|
||||
devicetype.save()
|
||||
|
||||
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
|
||||
devicerole.save()
|
||||
|
||||
device1 = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
|
||||
device1.save()
|
||||
device2 = Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole)
|
||||
device2.save()
|
||||
|
||||
iface1 = Interface(device=device1, name='Interface 1', form_factor=IFACE_FF_1GE_FIXED)
|
||||
iface1.save()
|
||||
iface2 = Interface(device=device1, name='Interface 2', form_factor=IFACE_FF_1GE_FIXED)
|
||||
iface2.save()
|
||||
iface3 = Interface(device=device1, name='Interface 3', form_factor=IFACE_FF_1GE_FIXED)
|
||||
iface3.save()
|
||||
iface4 = Interface(device=device2, name='Interface 1', form_factor=IFACE_FF_1GE_FIXED)
|
||||
iface4.save()
|
||||
iface5 = Interface(device=device2, name='Interface 2', form_factor=IFACE_FF_1GE_FIXED)
|
||||
iface5.save()
|
||||
iface6 = Interface(device=device2, name='Interface 3', form_factor=IFACE_FF_1GE_FIXED)
|
||||
iface6.save()
|
||||
|
||||
Cable(termination_a=iface1, termination_b=iface4, type=CABLE_TYPE_CAT6).save()
|
||||
Cable(termination_a=iface2, termination_b=iface5, type=CABLE_TYPE_CAT6).save()
|
||||
Cable(termination_a=iface3, termination_b=iface6, type=CABLE_TYPE_CAT6).save()
|
||||
|
||||
def test_cable_list(self):
|
||||
|
||||
url = reverse('dcim:cable_list')
|
||||
params = {
|
||||
"type": CABLE_TYPE_CAT6,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_cable(self):
|
||||
|
||||
cable = Cable.objects.first()
|
||||
response = self.client.get(cable.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class VirtualMachineTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1')
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||
)
|
||||
device_role = DeviceRole.objects.create(
|
||||
name='Device Role', slug='device-role-1'
|
||||
)
|
||||
|
||||
# Create 9 member Devices
|
||||
device1 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='Device 1', site=site
|
||||
)
|
||||
device2 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='Device 2', site=site
|
||||
)
|
||||
device3 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='Device 3', site=site
|
||||
)
|
||||
device4 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='Device 4', site=site
|
||||
)
|
||||
device5 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='Device 5', site=site
|
||||
)
|
||||
device6 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='Device 6', site=site
|
||||
)
|
||||
|
||||
# Create three VirtualChassis with two members each
|
||||
vc1 = VirtualChassis.objects.create(master=device1, domain='test-domain-1')
|
||||
Device.objects.filter(pk=device2.pk).update(virtual_chassis=vc1, vc_position=2)
|
||||
vc2 = VirtualChassis.objects.create(master=device3, domain='test-domain-2')
|
||||
Device.objects.filter(pk=device4.pk).update(virtual_chassis=vc2, vc_position=2)
|
||||
vc3 = VirtualChassis.objects.create(master=device5, domain='test-domain-3')
|
||||
Device.objects.filter(pk=device6.pk).update(virtual_chassis=vc3, vc_position=2)
|
||||
|
||||
def test_virtualchassis_list(self):
|
||||
|
||||
url = reverse('dcim:virtualchassis_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_virtualchassis(self):
|
||||
|
||||
virtualchassis = VirtualChassis.objects.first()
|
||||
response = self.client.get(virtualchassis.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from extras.views import ObjectChangeLogView, ImageAttachmentEditView
|
||||
from ipam.views import ServiceCreateView
|
||||
@@ -13,268 +13,271 @@ app_name = 'dcim'
|
||||
urlpatterns = [
|
||||
|
||||
# Regions
|
||||
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'),
|
||||
url(r'^regions/add/$', views.RegionCreateView.as_view(), name='region_add'),
|
||||
url(r'^regions/import/$', views.RegionBulkImportView.as_view(), name='region_import'),
|
||||
url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
||||
url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
|
||||
url(r'^regions/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
|
||||
path(r'regions/', views.RegionListView.as_view(), name='region_list'),
|
||||
path(r'regions/add/', views.RegionCreateView.as_view(), name='region_add'),
|
||||
path(r'regions/import/', views.RegionBulkImportView.as_view(), name='region_import'),
|
||||
path(r'regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
||||
path(r'regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'),
|
||||
path(r'regions/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
|
||||
|
||||
# Sites
|
||||
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
|
||||
url(r'^sites/add/$', views.SiteCreateView.as_view(), name='site_add'),
|
||||
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
|
||||
url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
|
||||
url(r'^sites/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
|
||||
path(r'sites/', views.SiteListView.as_view(), name='site_list'),
|
||||
path(r'sites/add/', views.SiteCreateView.as_view(), name='site_add'),
|
||||
path(r'sites/import/', views.SiteBulkImportView.as_view(), name='site_import'),
|
||||
path(r'sites/edit/', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
|
||||
path(r'sites/delete/', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'),
|
||||
path(r'sites/<slug:slug>/', views.SiteView.as_view(), name='site'),
|
||||
path(r'sites/<slug:slug>/edit/', views.SiteEditView.as_view(), name='site_edit'),
|
||||
path(r'sites/<slug:slug>/delete/', views.SiteDeleteView.as_view(), name='site_delete'),
|
||||
path(r'sites/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
|
||||
path(r'sites/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
|
||||
|
||||
# Rack groups
|
||||
url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'),
|
||||
url(r'^rack-groups/add/$', views.RackGroupCreateView.as_view(), name='rackgroup_add'),
|
||||
url(r'^rack-groups/import/$', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
|
||||
url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
|
||||
url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
|
||||
url(r'^rack-groups/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
|
||||
path(r'rack-groups/', views.RackGroupListView.as_view(), name='rackgroup_list'),
|
||||
path(r'rack-groups/add/', views.RackGroupCreateView.as_view(), name='rackgroup_add'),
|
||||
path(r'rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
|
||||
path(r'rack-groups/delete/', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
|
||||
path(r'rack-groups/<int:pk>/edit/', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
|
||||
path(r'rack-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
|
||||
|
||||
# Rack roles
|
||||
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
|
||||
url(r'^rack-roles/add/$', views.RackRoleCreateView.as_view(), name='rackrole_add'),
|
||||
url(r'^rack-roles/import/$', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
|
||||
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
||||
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||
url(r'^rack-roles/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
|
||||
path(r'rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'),
|
||||
path(r'rack-roles/add/', views.RackRoleCreateView.as_view(), name='rackrole_add'),
|
||||
path(r'rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
|
||||
path(r'rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
||||
path(r'rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||
path(r'rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
|
||||
|
||||
# Rack reservations
|
||||
url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'),
|
||||
url(r'^rack-reservations/edit/$', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
|
||||
url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
|
||||
path(r'rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'),
|
||||
path(r'rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
|
||||
path(r'rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
|
||||
path(r'rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||
path(r'rack-reservations/<int:pk>/delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||
path(r'rack-reservations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
|
||||
|
||||
# Racks
|
||||
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
|
||||
url(r'^rack-elevations/$', views.RackElevationListView.as_view(), name='rack_elevation_list'),
|
||||
url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'),
|
||||
url(r'^racks/import/$', views.RackBulkImportView.as_view(), name='rack_import'),
|
||||
url(r'^racks/edit/$', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),
|
||||
url(r'^racks/delete/$', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
|
||||
url(r'^racks/(?P<pk>\d+)/$', views.RackView.as_view(), name='rack'),
|
||||
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
|
||||
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||
url(r'^racks/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
|
||||
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
|
||||
url(r'^racks/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
|
||||
path(r'racks/', views.RackListView.as_view(), name='rack_list'),
|
||||
path(r'rack-elevations/', views.RackElevationListView.as_view(), name='rack_elevation_list'),
|
||||
path(r'racks/add/', views.RackEditView.as_view(), name='rack_add'),
|
||||
path(r'racks/import/', views.RackBulkImportView.as_view(), name='rack_import'),
|
||||
path(r'racks/edit/', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),
|
||||
path(r'racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
|
||||
path(r'racks/<int:pk>/', views.RackView.as_view(), name='rack'),
|
||||
path(r'racks/<int:pk>/edit/', views.RackEditView.as_view(), name='rack_edit'),
|
||||
path(r'racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||
path(r'racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
|
||||
path(r'racks/<int:rack>/reservations/add/', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
|
||||
path(r'racks/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
|
||||
|
||||
# Manufacturers
|
||||
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||
url(r'^manufacturers/add/$', views.ManufacturerCreateView.as_view(), name='manufacturer_add'),
|
||||
url(r'^manufacturers/import/$', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
|
||||
url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
|
||||
url(r'^manufacturers/(?P<slug>[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
|
||||
url(r'^manufacturers/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
|
||||
path(r'manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||
path(r'manufacturers/add/', views.ManufacturerCreateView.as_view(), name='manufacturer_add'),
|
||||
path(r'manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
|
||||
path(r'manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
|
||||
path(r'manufacturers/<slug:slug>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
|
||||
path(r'manufacturers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
|
||||
|
||||
# Device types
|
||||
url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||
url(r'^device-types/add/$', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
|
||||
url(r'^device-types/import/$', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
|
||||
url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
||||
url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
||||
url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
|
||||
url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
|
||||
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
||||
url(r'^device-types/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
|
||||
path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||
path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
|
||||
path(r'device-types/import/', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
|
||||
path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
||||
path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
||||
path(r'device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
|
||||
path(r'device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
|
||||
path(r'device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
||||
path(r'device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
|
||||
|
||||
# Console port templates
|
||||
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'),
|
||||
path(r'device-types/<int:pk>/console-ports/add/', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
|
||||
path(r'device-types/<int:pk>/console-ports/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'),
|
||||
|
||||
# Console server port templates
|
||||
url(r'^device-types/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'),
|
||||
path(r'device-types/<int:pk>/console-server-ports/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'),
|
||||
path(r'device-types/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'),
|
||||
|
||||
# Power port templates
|
||||
url(r'^device-types/(?P<pk>\d+)/power-ports/add/$', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'),
|
||||
path(r'device-types/<int:pk>/power-ports/add/', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'),
|
||||
path(r'device-types/<int:pk>/power-ports/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'),
|
||||
|
||||
# Power outlet templates
|
||||
url(r'^device-types/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'),
|
||||
url(r'^device-types/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'),
|
||||
path(r'device-types/<int:pk>/power-outlets/add/', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'),
|
||||
path(r'device-types/<int:pk>/power-outlets/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'),
|
||||
|
||||
# Interface templates
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'),
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'),
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
|
||||
path(r'device-types/<int:pk>/interfaces/add/', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'),
|
||||
path(r'device-types/<int:pk>/interfaces/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'),
|
||||
path(r'device-types/<int:pk>/interfaces/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
|
||||
|
||||
# Front port templates
|
||||
url(r'^device-types/(?P<pk>\d+)/front-ports/add/$', views.FrontPortTemplateCreateView.as_view(), name='devicetype_add_frontport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/front-ports/delete/$', views.FrontPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_frontport'),
|
||||
path(r'device-types/<int:pk>/front-ports/add/', views.FrontPortTemplateCreateView.as_view(), name='devicetype_add_frontport'),
|
||||
path(r'device-types/<int:pk>/front-ports/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_frontport'),
|
||||
|
||||
# Rear port templates
|
||||
url(r'^device-types/(?P<pk>\d+)/rear-ports/add/$', views.RearPortTemplateCreateView.as_view(), name='devicetype_add_rearport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/rear-ports/delete/$', views.RearPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_rearport'),
|
||||
path(r'device-types/<int:pk>/rear-ports/add/', views.RearPortTemplateCreateView.as_view(), name='devicetype_add_rearport'),
|
||||
path(r'device-types/<int:pk>/rear-ports/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_rearport'),
|
||||
|
||||
# Device bay templates
|
||||
url(r'^device-types/(?P<pk>\d+)/device-bays/add/$', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'),
|
||||
url(r'^device-types/(?P<pk>\d+)/device-bays/delete/$', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
|
||||
path(r'device-types/<int:pk>/device-bays/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'),
|
||||
path(r'device-types/<int:pk>/device-bays/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
|
||||
|
||||
# Device roles
|
||||
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||
url(r'^device-roles/add/$', views.DeviceRoleCreateView.as_view(), name='devicerole_add'),
|
||||
url(r'^device-roles/import/$', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
|
||||
url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
|
||||
url(r'^device-roles/(?P<slug>[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
|
||||
url(r'^device-roles/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
|
||||
path(r'device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||
path(r'device-roles/add/', views.DeviceRoleCreateView.as_view(), name='devicerole_add'),
|
||||
path(r'device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
|
||||
path(r'device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
|
||||
path(r'device-roles/<slug:slug>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
|
||||
path(r'device-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
|
||||
|
||||
# Platforms
|
||||
url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'),
|
||||
url(r'^platforms/add/$', views.PlatformCreateView.as_view(), name='platform_add'),
|
||||
url(r'^platforms/import/$', views.PlatformBulkImportView.as_view(), name='platform_import'),
|
||||
url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
|
||||
url(r'^platforms/(?P<slug>[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'),
|
||||
url(r'^platforms/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
|
||||
path(r'platforms/', views.PlatformListView.as_view(), name='platform_list'),
|
||||
path(r'platforms/add/', views.PlatformCreateView.as_view(), name='platform_add'),
|
||||
path(r'platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
|
||||
path(r'platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
|
||||
path(r'platforms/<slug:slug>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
|
||||
path(r'platforms/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
|
||||
|
||||
# Devices
|
||||
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
|
||||
url(r'^devices/add/$', views.DeviceCreateView.as_view(), name='device_add'),
|
||||
url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'),
|
||||
url(r'^devices/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
|
||||
url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
|
||||
url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/$', views.DeviceView.as_view(), name='device'),
|
||||
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/config-context/$', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
|
||||
url(r'^devices/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
|
||||
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
|
||||
url(r'^devices/(?P<pk>\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'),
|
||||
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||
url(r'^devices/(?P<pk>\d+)/config/$', views.DeviceConfigView.as_view(), name='device_config'),
|
||||
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
|
||||
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='device_service_assign'),
|
||||
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
||||
path(r'devices/', views.DeviceListView.as_view(), name='device_list'),
|
||||
path(r'devices/add/', views.DeviceCreateView.as_view(), name='device_add'),
|
||||
path(r'devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'),
|
||||
path(r'devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
|
||||
path(r'devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
|
||||
path(r'devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
||||
path(r'devices/<int:pk>/', views.DeviceView.as_view(), name='device'),
|
||||
path(r'devices/<int:pk>/edit/', views.DeviceEditView.as_view(), name='device_edit'),
|
||||
path(r'devices/<int:pk>/delete/', views.DeviceDeleteView.as_view(), name='device_delete'),
|
||||
path(r'devices/<int:pk>/config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
|
||||
path(r'devices/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
|
||||
path(r'devices/<int:pk>/inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'),
|
||||
path(r'devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
|
||||
path(r'devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||
path(r'devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
|
||||
path(r'devices/<int:pk>/add-secret/', secret_add, name='device_addsecret'),
|
||||
path(r'devices/<int:device>/services/assign/', ServiceCreateView.as_view(), name='device_service_assign'),
|
||||
path(r'devices/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
||||
|
||||
# Console ports
|
||||
url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||
url(r'^console-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
|
||||
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
|
||||
path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
|
||||
path(r'devices/<int:pk>/console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
||||
path(r'devices/<int:pk>/console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||
path(r'console-ports/<int:termination_a_id>/connect/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
|
||||
path(r'console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
||||
path(r'console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
||||
path(r'console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
|
||||
|
||||
# Console server ports
|
||||
url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
||||
url(r'^console-server-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
|
||||
url(r'^console-server-ports/rename/$', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
|
||||
url(r'^console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
|
||||
path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
|
||||
path(r'devices/<int:pk>/console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
|
||||
path(r'devices/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
||||
path(r'console-server-ports/<int:termination_a_id>/connect/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
|
||||
path(r'console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
|
||||
path(r'console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
||||
path(r'console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
|
||||
path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
|
||||
path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
|
||||
|
||||
# Power ports
|
||||
url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortCreateView.as_view(), name='powerport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||
url(r'^power-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
|
||||
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
|
||||
path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
|
||||
path(r'devices/<int:pk>/power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'),
|
||||
path(r'devices/<int:pk>/power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||
path(r'power-ports/<int:termination_a_id>/connect/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
|
||||
path(r'power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
||||
path(r'power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
||||
path(r'power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
|
||||
|
||||
# Power outlets
|
||||
url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
||||
url(r'^power-outlets/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
|
||||
url(r'^power-outlets/rename/$', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
|
||||
url(r'^power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
|
||||
path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
|
||||
path(r'devices/<int:pk>/power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
|
||||
path(r'devices/<int:pk>/power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
||||
path(r'power-outlets/<int:termination_a_id>/connect/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
|
||||
path(r'power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
|
||||
path(r'power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
||||
path(r'power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
|
||||
path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
|
||||
path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
|
||||
|
||||
# Interfaces
|
||||
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
url(r'^interfaces/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
|
||||
url(r'^interfaces/(?P<pk>\d+)/$', views.InterfaceView.as_view(), name='interface'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/assign-vlans/$', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
|
||||
url(r'^interfaces/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
|
||||
url(r'^interfaces/rename/$', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
|
||||
url(r'^interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
|
||||
path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||
path(r'devices/<int:pk>/interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
|
||||
path(r'devices/<int:pk>/interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||
path(r'devices/<int:pk>/interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
path(r'interfaces/<int:termination_a_id>/connect/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
|
||||
path(r'interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
|
||||
path(r'interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||
path(r'interfaces/<int:pk>/assign-vlans/', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'),
|
||||
path(r'interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
||||
path(r'interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
|
||||
path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
|
||||
path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
|
||||
path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
|
||||
|
||||
# Front ports
|
||||
# url(r'^devices/front-ports/add/$', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
|
||||
url(r'^devices/(?P<pk>\d+)/front-ports/add/$', views.FrontPortCreateView.as_view(), name='frontport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/front-ports/delete/$', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
|
||||
url(r'^front-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
|
||||
url(r'^front-ports/(?P<pk>\d+)/edit/$', views.FrontPortEditView.as_view(), name='frontport_edit'),
|
||||
url(r'^front-ports/(?P<pk>\d+)/delete/$', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
|
||||
url(r'^front-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
|
||||
url(r'^front-ports/rename/$', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
|
||||
url(r'^front-ports/disconnect/$', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
|
||||
# path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
|
||||
path(r'devices/<int:pk>/front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'),
|
||||
path(r'devices/<int:pk>/front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
|
||||
path(r'devices/<int:pk>/front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
|
||||
path(r'front-ports/<int:termination_a_id>/connect/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
|
||||
path(r'front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
|
||||
path(r'front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
|
||||
path(r'front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
|
||||
path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
|
||||
path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
|
||||
|
||||
# Rear ports
|
||||
# url(r'^devices/rear-ports/add/$', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
|
||||
url(r'^devices/(?P<pk>\d+)/rear-ports/add/$', views.RearPortCreateView.as_view(), name='rearport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/rear-ports/delete/$', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
|
||||
url(r'^rear-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
|
||||
url(r'^rear-ports/(?P<pk>\d+)/edit/$', views.RearPortEditView.as_view(), name='rearport_edit'),
|
||||
url(r'^rear-ports/(?P<pk>\d+)/delete/$', views.RearPortDeleteView.as_view(), name='rearport_delete'),
|
||||
url(r'^rear-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
|
||||
url(r'^rear-ports/rename/$', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
|
||||
url(r'^rear-ports/disconnect/$', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
|
||||
# path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
|
||||
path(r'devices/<int:pk>/rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'),
|
||||
path(r'devices/<int:pk>/rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
|
||||
path(r'devices/<int:pk>/rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
|
||||
path(r'rear-ports/<int:termination_a_id>/connect/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
|
||||
path(r'rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
|
||||
path(r'rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
|
||||
path(r'rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
|
||||
path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
|
||||
path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
|
||||
|
||||
# Device bays
|
||||
url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
|
||||
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/populate/$', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
|
||||
url(r'^device-bays/rename/$', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
|
||||
path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
|
||||
path(r'devices/<int:pk>/bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
|
||||
path(r'devices/<int:pk>/bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
||||
path(r'device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
||||
path(r'device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
|
||||
path(r'device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
|
||||
path(r'device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
|
||||
path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
|
||||
|
||||
# Inventory items
|
||||
url(r'^inventory-items/$', views.InventoryItemListView.as_view(), name='inventoryitem_list'),
|
||||
url(r'^inventory-items/import/$', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'),
|
||||
url(r'^inventory-items/edit/$', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'),
|
||||
url(r'^inventory-items/delete/$', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
|
||||
url(r'^inventory-items/(?P<pk>\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
|
||||
url(r'^inventory-items/(?P<pk>\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
|
||||
url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
|
||||
path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'),
|
||||
path(r'inventory-items/import/', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'),
|
||||
path(r'inventory-items/edit/', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'),
|
||||
path(r'inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
|
||||
path(r'inventory-items/<int:pk>/edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
|
||||
path(r'inventory-items/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
|
||||
path(r'devices/<int:device>/inventory-items/add/', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
|
||||
|
||||
# Cables
|
||||
url(r'^cables/$', views.CableListView.as_view(), name='cable_list'),
|
||||
url(r'^cables/import/$', views.CableBulkImportView.as_view(), name='cable_import'),
|
||||
url(r'^cables/edit/$', views.CableBulkEditView.as_view(), name='cable_bulk_edit'),
|
||||
url(r'^cables/delete/$', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'),
|
||||
url(r'^cables/(?P<pk>\d+)/$', views.CableView.as_view(), name='cable'),
|
||||
url(r'^cables/(?P<pk>\d+)/edit/$', views.CableEditView.as_view(), name='cable_edit'),
|
||||
url(r'^cables/(?P<pk>\d+)/delete/$', views.CableDeleteView.as_view(), name='cable_delete'),
|
||||
url(r'^cables/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}),
|
||||
path(r'cables/', views.CableListView.as_view(), name='cable_list'),
|
||||
path(r'cables/import/', views.CableBulkImportView.as_view(), name='cable_import'),
|
||||
path(r'cables/edit/', views.CableBulkEditView.as_view(), name='cable_bulk_edit'),
|
||||
path(r'cables/delete/', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'),
|
||||
path(r'cables/<int:pk>/', views.CableView.as_view(), name='cable'),
|
||||
path(r'cables/<int:pk>/edit/', views.CableEditView.as_view(), name='cable_edit'),
|
||||
path(r'cables/<int:pk>/delete/', views.CableDeleteView.as_view(), name='cable_delete'),
|
||||
path(r'cables/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}),
|
||||
|
||||
# Console/power/interface connections (read-only)
|
||||
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
|
||||
url(r'^power-connections/$', views.PowerConnectionsListView.as_view(), name='power_connections_list'),
|
||||
url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
|
||||
path(r'console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
|
||||
path(r'power-connections/', views.PowerConnectionsListView.as_view(), name='power_connections_list'),
|
||||
path(r'interface-connections/', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
|
||||
|
||||
# Virtual chassis
|
||||
url(r'^virtual-chassis/$', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
|
||||
url(r'^virtual-chassis/add/$', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
|
||||
url(r'^virtual-chassis/(?P<pk>\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
|
||||
url(r'^virtual-chassis/(?P<pk>\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
|
||||
url(r'^virtual-chassis/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
|
||||
url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
|
||||
url(r'^virtual-chassis-members/(?P<pk>\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
|
||||
path(r'virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
|
||||
path(r'virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
|
||||
path(r'virtual-chassis/<int:pk>/edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
|
||||
path(r'virtual-chassis/<int:pk>/delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
|
||||
path(r'virtual-chassis/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
|
||||
path(r'virtual-chassis/<int:pk>/add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
|
||||
path(r'virtual-chassis-members/<int:pk>/delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
|
||||
|
||||
]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||
@@ -135,7 +136,13 @@ class BulkDisconnectView(GetReturnURLMixin, View):
|
||||
#
|
||||
|
||||
class RegionListView(ObjectListView):
|
||||
queryset = Region.objects.all()
|
||||
queryset = Region.objects.add_related_count(
|
||||
Region.objects.all(),
|
||||
Site,
|
||||
'region',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filter = filters.RegionFilter
|
||||
filter_form = forms.RegionFilterForm
|
||||
table = tables.RegionTable
|
||||
@@ -240,6 +247,14 @@ class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
default_return_url = 'dcim:site_list'
|
||||
|
||||
|
||||
class SiteBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_site'
|
||||
queryset = Site.objects.select_related('region', 'tenant')
|
||||
filter = filters.SiteFilter
|
||||
table = tables.SiteTable
|
||||
default_return_url = 'dcim:site_list'
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
#
|
||||
@@ -347,8 +362,9 @@ class RackElevationListView(View):
|
||||
total_count = racks.count()
|
||||
|
||||
# Pagination
|
||||
paginator = EnhancedPaginator(racks, 25)
|
||||
per_page = request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
page_number = request.GET.get('page', 1)
|
||||
paginator = EnhancedPaginator(racks, per_page)
|
||||
try:
|
||||
page = paginator.page(page_number)
|
||||
except PageNotAnInteger:
|
||||
@@ -442,7 +458,7 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class RackReservationListView(ObjectListView):
|
||||
queryset = RackReservation.objects.all()
|
||||
queryset = RackReservation.objects.select_related('rack__site')
|
||||
filter = filters.RackReservationFilter
|
||||
filter_form = forms.RackReservationFilterForm
|
||||
table = tables.RackReservationTable
|
||||
@@ -500,6 +516,7 @@ class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
class ManufacturerListView(ObjectListView):
|
||||
queryset = Manufacturer.objects.annotate(
|
||||
devicetype_count=Count('device_types', distinct=True),
|
||||
inventoryitem_count=Count('inventory_items', distinct=True),
|
||||
platform_count=Count('platforms', distinct=True),
|
||||
)
|
||||
table = tables.ManufacturerTable
|
||||
@@ -1360,6 +1377,14 @@ class FrontPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
model = FrontPort
|
||||
|
||||
|
||||
class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_frontport'
|
||||
queryset = FrontPort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.FrontPortTable
|
||||
form = forms.FrontPortBulkEditForm
|
||||
|
||||
|
||||
class FrontPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
permission_required = 'dcim.change_frontport'
|
||||
queryset = FrontPort.objects.all()
|
||||
@@ -1404,6 +1429,14 @@ class RearPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
model = RearPort
|
||||
|
||||
|
||||
class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_rearport'
|
||||
queryset = RearPort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.RearPortTable
|
||||
form = forms.RearPortBulkEditForm
|
||||
|
||||
|
||||
class RearPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
permission_required = 'dcim.change_rearport'
|
||||
queryset = RearPort.objects.all()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from rest_framework import serializers
|
||||
from taggit.models import Tag
|
||||
@@ -15,7 +16,8 @@ from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantG
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
from utilities.api import (
|
||||
ChoiceField, ContentTypeField, get_serializer_for_model, SerializedPKRelatedField, ValidatedModelSerializer,
|
||||
ChoiceField, ContentTypeField, get_serializer_for_model, SerializerNotFound, SerializedPKRelatedField,
|
||||
ValidatedModelSerializer,
|
||||
)
|
||||
from .nested_serializers import *
|
||||
|
||||
@@ -53,10 +55,17 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
|
||||
#
|
||||
|
||||
class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||
template_language = ChoiceField(
|
||||
choices=TEMPLATE_LANGUAGE_CHOICES,
|
||||
default=TEMPLATE_LANGUAGE_JINJA2
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
fields = ['id', 'content_type', 'name', 'description', 'template_code', 'mime_type', 'file_extension']
|
||||
fields = [
|
||||
'id', 'content_type', 'name', 'description', 'template_language', 'template_code', 'mime_type',
|
||||
'file_extension',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
@@ -88,7 +97,9 @@ class TagSerializer(ValidatedModelSerializer):
|
||||
#
|
||||
|
||||
class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
content_type = ContentTypeField()
|
||||
content_type = ContentTypeField(
|
||||
queryset=ContentType.objects.all()
|
||||
)
|
||||
parent = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@@ -205,14 +216,25 @@ class ReportDetailSerializer(ReportSerializer):
|
||||
#
|
||||
|
||||
class ObjectChangeSerializer(serializers.ModelSerializer):
|
||||
user = NestedUserSerializer(read_only=True)
|
||||
content_type = ContentTypeField(read_only=True)
|
||||
changed_object = serializers.SerializerMethodField(read_only=True)
|
||||
user = NestedUserSerializer(
|
||||
read_only=True
|
||||
)
|
||||
action = ChoiceField(
|
||||
choices=OBJECTCHANGE_ACTION_CHOICES,
|
||||
read_only=True
|
||||
)
|
||||
changed_object_type = ContentTypeField(
|
||||
read_only=True
|
||||
)
|
||||
changed_object = serializers.SerializerMethodField(
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
fields = [
|
||||
'id', 'time', 'user', 'user_name', 'request_id', 'action', 'content_type', 'changed_object', 'object_data',
|
||||
'id', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object',
|
||||
'object_data',
|
||||
]
|
||||
|
||||
def get_changed_object(self, obj):
|
||||
@@ -221,9 +243,14 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
if obj.changed_object is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.changed_object, prefix='Nested')
|
||||
if serializer is None:
|
||||
|
||||
try:
|
||||
serializer = get_serializer_for_model(obj.changed_object, prefix='Nested')
|
||||
except SerializerNotFound:
|
||||
return obj.object_repr
|
||||
context = {'request': self.context['request']}
|
||||
context = {
|
||||
'request': self.context['request']
|
||||
}
|
||||
data = serializer(obj.changed_object, context=context).data
|
||||
|
||||
return data
|
||||
|
||||
@@ -10,7 +10,7 @@ from taggit.models import Tag
|
||||
|
||||
from extras import filters
|
||||
from extras.models import (
|
||||
ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
||||
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
||||
)
|
||||
from extras.reports import get_report, get_reports
|
||||
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
|
||||
@@ -23,8 +23,9 @@ from . import serializers
|
||||
|
||||
class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(CustomField, ['type']),
|
||||
(ExportTemplate, ['template_language']),
|
||||
(Graph, ['type']),
|
||||
(ObjectChange, ['action']),
|
||||
)
|
||||
|
||||
|
||||
@@ -115,7 +116,9 @@ class TopologyMapViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class TagViewSet(ModelViewSet):
|
||||
queryset = Tag.objects.annotate(tagged_items=Count('taggit_taggeditem_items'))
|
||||
queryset = Tag.objects.annotate(
|
||||
tagged_items=Count('taggit_taggeditem_items', distinct=True)
|
||||
)
|
||||
serializer_class = serializers.TagSerializer
|
||||
filterset_class = filters.TagFilter
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ class ExtrasConfig(AppConfig):
|
||||
port=settings.REDIS_PORT,
|
||||
db=settings.REDIS_DATABASE,
|
||||
password=settings.REDIS_PASSWORD or None,
|
||||
ssl=settings.REDIS_SSL,
|
||||
)
|
||||
rs.ping()
|
||||
except redis.exceptions.ConnectionError:
|
||||
|
||||
@@ -56,6 +56,14 @@ EXPORTTEMPLATE_MODELS = [
|
||||
'cluster', 'virtualmachine', # Virtualization
|
||||
]
|
||||
|
||||
# ExportTemplate language choices
|
||||
TEMPLATE_LANGUAGE_DJANGO = 10
|
||||
TEMPLATE_LANGUAGE_JINJA2 = 20
|
||||
TEMPLATE_LANGUAGE_CHOICES = (
|
||||
(TEMPLATE_LANGUAGE_DJANGO, 'Django'),
|
||||
(TEMPLATE_LANGUAGE_JINJA2, 'Jinja2'),
|
||||
)
|
||||
|
||||
# Topology map types
|
||||
TOPOLOGYMAP_TYPE_NETWORK = 1
|
||||
TOPOLOGYMAP_TYPE_CONSOLE = 2
|
||||
|
||||
@@ -82,7 +82,7 @@ class ExportTemplateFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
fields = ['content_type', 'name']
|
||||
fields = ['content_type', 'name', 'template_language']
|
||||
|
||||
|
||||
class TagFilter(django_filters.FilterSet):
|
||||
|
||||
@@ -4,15 +4,14 @@ from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from mptt.forms import TreeNodeMultipleChoiceField
|
||||
from taggit.forms import TagField
|
||||
from taggit.models import Tag
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect, FilterChoiceField,
|
||||
FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
|
||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect,
|
||||
FilterChoiceField, LaxURLField, JSONField, SlugField,
|
||||
)
|
||||
from .constants import (
|
||||
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
||||
@@ -221,10 +220,6 @@ class TagFilterForm(BootstrapMixin, forms.Form):
|
||||
#
|
||||
|
||||
class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
regions = TreeNodeMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False
|
||||
)
|
||||
data = JSONField()
|
||||
|
||||
class Meta:
|
||||
@@ -233,6 +228,26 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups',
|
||||
'tenants', 'data',
|
||||
]
|
||||
widgets = {
|
||||
'regions': APISelectMultiple(
|
||||
api_url="/api/dcim/regions/"
|
||||
),
|
||||
'sites': APISelectMultiple(
|
||||
api_url="/api/dcim/sites/"
|
||||
),
|
||||
'roles': APISelectMultiple(
|
||||
api_url="/api/dcim/device-roles/"
|
||||
),
|
||||
'platforms': APISelectMultiple(
|
||||
api_url="/api/dcim/platforms/"
|
||||
),
|
||||
'tenant_groups': APISelectMultiple(
|
||||
api_url="/api/tenancy/tenant-groups/"
|
||||
),
|
||||
'tenants': APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
@@ -264,29 +279,53 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
region = FilterTreeNodeMultipleChoiceField(
|
||||
region = FilterChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug'
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/regions/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug'
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
role = FilterChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug'
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/device-roles/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
platform = FilterChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug'
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/platforms/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
tenant_group = FilterChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
to_field_name='slug'
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenant-groups/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug'
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -29,11 +29,11 @@ def cache_changed_object(instance, **kwargs):
|
||||
|
||||
def _record_object_deleted(request, instance, **kwargs):
|
||||
|
||||
# Record that the object was deleted.
|
||||
# Record that the object was deleted
|
||||
if hasattr(instance, 'log_change'):
|
||||
instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
|
||||
|
||||
enqueue_webhooks(instance, OBJECTCHANGE_ACTION_DELETE)
|
||||
enqueue_webhooks(instance, request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
|
||||
|
||||
|
||||
class ObjectChangeMiddleware(object):
|
||||
@@ -43,7 +43,7 @@ class ObjectChangeMiddleware(object):
|
||||
1. Create an ObjectChange to reflect the modification to the object in the changelog.
|
||||
2. Enqueue any relevant webhooks.
|
||||
|
||||
The post_save and pre_delete signals are employed to catch object modifications, however changes are recorded a bit
|
||||
The post_save and post_delete signals are employed to catch object modifications, however changes are recorded a bit
|
||||
differently for each. Objects being saved are cached into thread-local storage for action *after* the response has
|
||||
completed. This ensures that serialization of the object is performed only after any related objects (e.g. tags)
|
||||
have been created. Conversely, deletions are acted upon immediately, so that the serialized representation of the
|
||||
@@ -61,10 +61,10 @@ class ObjectChangeMiddleware(object):
|
||||
# the same request.
|
||||
request.id = uuid.uuid4()
|
||||
|
||||
# Signals don't include the request context, so we're currying it into the pre_delete function ahead of time.
|
||||
# Signals don't include the request context, so we're currying it into the post_delete function ahead of time.
|
||||
record_object_deleted = curry(_record_object_deleted, request)
|
||||
|
||||
# Connect our receivers to the post_save and pre_delete signals.
|
||||
# Connect our receivers to the post_save and post_delete signals.
|
||||
post_save.connect(cache_changed_object, dispatch_uid='record_object_saved')
|
||||
post_delete.connect(record_object_deleted, dispatch_uid='record_object_deleted')
|
||||
|
||||
@@ -79,7 +79,7 @@ class ObjectChangeMiddleware(object):
|
||||
obj.log_change(request.user, request.id, action)
|
||||
|
||||
# Enqueue webhooks
|
||||
enqueue_webhooks(obj, action)
|
||||
enqueue_webhooks(obj, request.user, request.id, action)
|
||||
|
||||
# Housekeeping: 1% chance of clearing out expired ObjectChanges
|
||||
if _thread_locals.changed_objects and settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1:
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.1.7 on 2019-03-05 18:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0016_exporttemplate_add_cable'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='exporttemplate',
|
||||
name='mime_type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
]
|
||||
27
netbox/extras/migrations/0018_exporttemplate_add_jinja2.py
Normal file
27
netbox/extras/migrations/0018_exporttemplate_add_jinja2.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 2.1.7 on 2019-04-08 14:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_template_language(apps, schema_editor):
|
||||
"""
|
||||
Set the language for all existing ExportTemplates to Django (Jinja2 is the default for new ExportTemplates).
|
||||
"""
|
||||
ExportTemplate = apps.get_model('extras', 'ExportTemplate')
|
||||
ExportTemplate.objects.update(template_language=10)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0017_exporttemplate_mime_type_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='exporttemplate',
|
||||
name='template_language',
|
||||
field=models.PositiveSmallIntegerField(default=20),
|
||||
),
|
||||
migrations.RunPython(set_template_language),
|
||||
]
|
||||
@@ -1,7 +1,6 @@
|
||||
from collections import OrderedDict
|
||||
from datetime import date
|
||||
|
||||
import graphviz
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -12,6 +11,8 @@ from django.db.models import F, Q
|
||||
from django.http import HttpResponse
|
||||
from django.template import Template, Context
|
||||
from django.urls import reverse
|
||||
import graphviz
|
||||
from jinja2 import Environment
|
||||
|
||||
from dcim.constants import CONNECTION_STATUS_CONNECTED
|
||||
from utilities.utils import deepmerge, foreground_color
|
||||
@@ -101,17 +102,22 @@ class Webhook(models.Model):
|
||||
#
|
||||
|
||||
class CustomFieldModel(models.Model):
|
||||
_cf = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@property
|
||||
def cf(self):
|
||||
"""
|
||||
Name-based CustomFieldValue accessor for use in templates
|
||||
"""
|
||||
if not hasattr(self, 'get_custom_fields'):
|
||||
return dict()
|
||||
return {field.name: value for field, value in self.get_custom_fields().items()}
|
||||
if self._cf is None:
|
||||
# Cache all custom field values for this instance
|
||||
self._cf = {
|
||||
field.name: value for field, value in self.get_custom_fields().items()
|
||||
}
|
||||
return self._cf
|
||||
|
||||
def get_custom_fields(self):
|
||||
"""
|
||||
@@ -124,7 +130,7 @@ class CustomFieldModel(models.Model):
|
||||
|
||||
# If the object exists, populate its custom fields with values
|
||||
if hasattr(self, 'pk'):
|
||||
values = CustomFieldValue.objects.filter(obj_type=content_type, obj_id=self.pk).select_related('field')
|
||||
values = self.custom_field_values.all()
|
||||
values_dict = {cfv.field_id: cfv.value for cfv in values}
|
||||
return OrderedDict([(field, values_dict.get(field.pk)) for field in fields])
|
||||
else:
|
||||
@@ -355,9 +361,13 @@ class ExportTemplate(models.Model):
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
template_language = models.PositiveSmallIntegerField(
|
||||
choices=TEMPLATE_LANGUAGE_CHOICES,
|
||||
default=TEMPLATE_LANGUAGE_JINJA2
|
||||
)
|
||||
template_code = models.TextField()
|
||||
mime_type = models.CharField(
|
||||
max_length=15,
|
||||
max_length=50,
|
||||
blank=True
|
||||
)
|
||||
file_extension = models.CharField(
|
||||
@@ -374,16 +384,36 @@ class ExportTemplate(models.Model):
|
||||
def __str__(self):
|
||||
return '{}: {}'.format(self.content_type, self.name)
|
||||
|
||||
def render(self, queryset):
|
||||
"""
|
||||
Render the contents of the template.
|
||||
"""
|
||||
context = {
|
||||
'queryset': queryset
|
||||
}
|
||||
|
||||
if self.template_language == TEMPLATE_LANGUAGE_DJANGO:
|
||||
template = Template(self.template_code)
|
||||
output = template.render(Context(context))
|
||||
|
||||
elif self.template_language == TEMPLATE_LANGUAGE_JINJA2:
|
||||
template = Environment().from_string(source=self.template_code)
|
||||
output = template.render(**context)
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
# Replace CRLF-style line terminators
|
||||
output = output.replace('\r\n', '\n')
|
||||
|
||||
return output
|
||||
|
||||
def render_to_response(self, queryset):
|
||||
"""
|
||||
Render the template to an HTTP response, delivered as a named file attachment
|
||||
"""
|
||||
template = Template(self.template_code)
|
||||
output = self.render(queryset)
|
||||
mime_type = 'text/plain' if not self.mime_type else self.mime_type
|
||||
output = template.render(Context({'queryset': queryset}))
|
||||
|
||||
# Replace CRLF-style line terminators
|
||||
output = output.replace('\r\n', '\n')
|
||||
|
||||
# Build the response
|
||||
response = HttpResponse(output, content_type=mime_type)
|
||||
@@ -720,7 +750,7 @@ class ConfigContextModel(models.Model):
|
||||
data = deepmerge(data, context.data)
|
||||
|
||||
# If the object has local config context data defined, merge it last
|
||||
if self.local_context_data is not None:
|
||||
if self.local_context_data:
|
||||
data = deepmerge(data, self.local_context_data)
|
||||
|
||||
return data
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.db.models import Q, QuerySet
|
||||
|
||||
|
||||
class CustomFieldQueryset:
|
||||
"""
|
||||
Annotate custom fields on objects within a QuerySet.
|
||||
"""
|
||||
def __init__(self, queryset, custom_fields):
|
||||
self.queryset = queryset
|
||||
self.model = queryset.model
|
||||
self.custom_fields = custom_fields
|
||||
|
||||
def __iter__(self):
|
||||
for obj in self.queryset:
|
||||
values_dict = {cfv.field_id: cfv.value for cfv in obj.custom_field_values.all()}
|
||||
obj.custom_fields = OrderedDict([(field, values_dict.get(field.pk)) for field in self.custom_fields])
|
||||
yield obj
|
||||
|
||||
|
||||
class ConfigContextQuerySet(QuerySet):
|
||||
|
||||
def get_for_object(self, obj):
|
||||
|
||||
@@ -68,7 +68,7 @@ class TagTable(BaseTable):
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=TAG_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
|
||||
105
netbox/extras/tests/test_views.py
Normal file
105
netbox/extras/tests/test_views.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import urllib.parse
|
||||
import uuid
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
from taggit.models import Tag
|
||||
|
||||
from dcim.models import Site
|
||||
from extras.models import ConfigContext, ObjectChange
|
||||
|
||||
|
||||
class TagTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
Tag.objects.bulk_create([
|
||||
Tag(name='Tag 1', slug='tag-1'),
|
||||
Tag(name='Tag 2', slug='tag-2'),
|
||||
Tag(name='Tag 3', slug='tag-3'),
|
||||
])
|
||||
|
||||
def test_tag_list(self):
|
||||
|
||||
url = reverse('extras:tag_list')
|
||||
params = {
|
||||
"q": "tag",
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class ConfigContextTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
# Create three ConfigContexts
|
||||
for i in range(1, 4):
|
||||
configcontext = ConfigContext(
|
||||
name='Config Context {}'.format(i),
|
||||
data='{{"foo": {}}}'.format(i)
|
||||
)
|
||||
configcontext.save()
|
||||
configcontext.sites.add(site)
|
||||
|
||||
def test_configcontext_list(self):
|
||||
|
||||
url = reverse('extras:configcontext_list')
|
||||
params = {
|
||||
"q": "foo",
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_configcontext(self):
|
||||
|
||||
configcontext = ConfigContext.objects.first()
|
||||
response = self.client.get(configcontext.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class ObjectChangeTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
user = User(username='testuser', email='testuser@example.com')
|
||||
user.save()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
# Create three ObjectChanges
|
||||
for i in range(1, 4):
|
||||
site.log_change(
|
||||
user=user,
|
||||
request_id=uuid.uuid4(),
|
||||
action=2
|
||||
)
|
||||
|
||||
def test_objectchange_list(self):
|
||||
|
||||
url = reverse('extras:objectchange_list')
|
||||
params = {
|
||||
"user": User.objects.first(),
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_objectchange(self):
|
||||
|
||||
objectchange = ObjectChange.objects.first()
|
||||
response = self.client.get(objectchange.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from extras import views
|
||||
|
||||
@@ -6,32 +6,32 @@ app_name = 'extras'
|
||||
urlpatterns = [
|
||||
|
||||
# Tags
|
||||
url(r'^tags/$', views.TagListView.as_view(), name='tag_list'),
|
||||
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
url(r'^tags/(?P<slug>[\w-]+)/$', views.TagView.as_view(), name='tag'),
|
||||
url(r'^tags/(?P<slug>[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'),
|
||||
url(r'^tags/(?P<slug>[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||
path(r'tags/', views.TagListView.as_view(), name='tag_list'),
|
||||
path(r'tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
path(r'tags/<slug:slug>/', views.TagView.as_view(), name='tag'),
|
||||
path(r'tags/<slug:slug>/edit/', views.TagEditView.as_view(), name='tag_edit'),
|
||||
path(r'tags/<slug:slug>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||
|
||||
# Config contexts
|
||||
url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
||||
url(r'^config-contexts/add/$', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
|
||||
url(r'^config-contexts/edit/$', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'),
|
||||
url(r'^config-contexts/delete/$', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/$', views.ConfigContextView.as_view(), name='configcontext'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/edit/$', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/delete/$', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
|
||||
path(r'config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
||||
path(r'config-contexts/add/', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
|
||||
path(r'config-contexts/edit/', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'),
|
||||
path(r'config-contexts/delete/', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
|
||||
path(r'config-contexts/<int:pk>/', views.ConfigContextView.as_view(), name='configcontext'),
|
||||
path(r'config-contexts/<int:pk>/edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
|
||||
path(r'config-contexts/<int:pk>/delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
|
||||
|
||||
# Image attachments
|
||||
url(r'^image-attachments/(?P<pk>\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
|
||||
url(r'^image-attachments/(?P<pk>\d+)/delete/$', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
|
||||
path(r'image-attachments/<int:pk>/edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
|
||||
path(r'image-attachments/<int:pk>/delete/', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
|
||||
|
||||
# Reports
|
||||
url(r'^reports/$', views.ReportListView.as_view(), name='report_list'),
|
||||
url(r'^reports/(?P<name>[^/]+\.[^/]+)/$', views.ReportView.as_view(), name='report'),
|
||||
url(r'^reports/(?P<name>[^/]+\.[^/]+)/run/$', views.ReportRunView.as_view(), name='report_run'),
|
||||
path(r'reports/', views.ReportListView.as_view(), name='report_list'),
|
||||
path(r'reports/<str:name>/', views.ReportView.as_view(), name='report'),
|
||||
path(r'reports/<str:name>/run/', views.ReportRunView.as_view(), name='report_run'),
|
||||
|
||||
# Change logging
|
||||
url(r'^changelog/$', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
||||
url(r'^changelog/(?P<pk>\d+)/$', views.ObjectChangeView.as_view(), name='objectchange'),
|
||||
path(r'changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
||||
path(r'changelog/<int:pk>/', views.ObjectChangeView.as_view(), name='objectchange'),
|
||||
|
||||
]
|
||||
|
||||
@@ -30,7 +30,7 @@ from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemT
|
||||
|
||||
class TagListView(ObjectListView):
|
||||
queryset = Tag.objects.annotate(
|
||||
items=Count('taggit_taggeditem_items')
|
||||
items=Count('taggit_taggeditem_items', distinct=True)
|
||||
).order_by(
|
||||
'name'
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ from utilities.api import get_serializer_for_model
|
||||
from .constants import WEBHOOK_MODELS
|
||||
|
||||
|
||||
def enqueue_webhooks(instance, action):
|
||||
def enqueue_webhooks(instance, user, request_id, action):
|
||||
"""
|
||||
Find Webhook(s) assigned to this instance + action and enqueue them
|
||||
to be processed
|
||||
@@ -47,5 +47,7 @@ def enqueue_webhooks(instance, action):
|
||||
serializer.data,
|
||||
instance._meta.model_name,
|
||||
action,
|
||||
str(datetime.datetime.now())
|
||||
str(datetime.datetime.now()),
|
||||
user.username,
|
||||
request_id
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ from extras.constants import WEBHOOK_CT_JSON, WEBHOOK_CT_X_WWW_FORM_ENCODED, OBJ
|
||||
|
||||
|
||||
@job('default')
|
||||
def process_webhook(webhook, data, model_name, event, timestamp):
|
||||
def process_webhook(webhook, data, model_name, event, timestamp, username, request_id):
|
||||
"""
|
||||
Make a POST request to the defined Webhook
|
||||
"""
|
||||
@@ -18,6 +18,8 @@ def process_webhook(webhook, data, model_name, event, timestamp):
|
||||
'event': dict(OBJECTCHANGE_ACTION_CHOICES)[event].lower(),
|
||||
'timestamp': timestamp,
|
||||
'model': model_name,
|
||||
'username': username,
|
||||
'request_id': request_id,
|
||||
'data': data
|
||||
}
|
||||
headers = {
|
||||
|
||||
@@ -128,6 +128,7 @@ class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
#
|
||||
|
||||
class PrefixSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
family = ChoiceField(choices=AF_CHOICES, read_only=True)
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
@@ -189,6 +190,7 @@ class IPAddressInterfaceSerializer(WritableNestedSerializer):
|
||||
|
||||
|
||||
class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
family = ChoiceField(choices=AF_CHOICES, read_only=True)
|
||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=IPADDRESS_STATUS_CHOICES, required=False)
|
||||
|
||||
@@ -6,14 +6,14 @@ from netaddr.core import AddrFormatError
|
||||
|
||||
from dcim.models import Site, Device, Interface
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NumericInFilter, TagFilter
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
|
||||
from virtualization.models import VirtualMachine
|
||||
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
class VRFFilter(TenancyFilterSet, CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -22,16 +22,6 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -48,7 +38,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
fields = ['name', 'rd', 'enforce_unique']
|
||||
|
||||
|
||||
class RIRFilter(django_filters.FilterSet):
|
||||
class RIRFilter(NameSlugSearchFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -59,7 +49,7 @@ class RIRFilter(django_filters.FilterSet):
|
||||
fields = ['name', 'slug', 'is_private']
|
||||
|
||||
|
||||
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
class AggregateFilter(CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -96,14 +86,18 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class RoleFilter(django_filters.FilterSet):
|
||||
class RoleFilter(NameSlugSearchFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -142,16 +136,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='rd',
|
||||
label='VRF (RD)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@@ -250,7 +234,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
return queryset.filter(prefix__net_mask_length=value)
|
||||
|
||||
|
||||
class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -281,16 +265,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='rd',
|
||||
label='VRF (RD)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
field_name='name',
|
||||
@@ -312,6 +286,12 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='name',
|
||||
label='Virtual machine (name)',
|
||||
)
|
||||
interface = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface__name',
|
||||
queryset=Interface.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Interface (ID)',
|
||||
)
|
||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Interface.objects.all(),
|
||||
label='Interface (ID)',
|
||||
@@ -373,7 +353,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
return queryset.none()
|
||||
|
||||
|
||||
class VLANGroupFilter(django_filters.FilterSet):
|
||||
class VLANGroupFilter(NameSlugSearchFilterSet):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@@ -390,7 +370,7 @@ class VLANGroupFilter(django_filters.FilterSet):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
class VLANFilter(TenancyFilterSet, CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -419,16 +399,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Role.objects.all(),
|
||||
label='Role (ID)',
|
||||
|
||||
@@ -6,6 +6,7 @@ from taggit.forms import TagField
|
||||
from dcim.models import Site, Rack, Device, Interface
|
||||
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.forms import TenancyFilterForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField,
|
||||
@@ -97,22 +98,13 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm
|
||||
]
|
||||
|
||||
|
||||
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
model = VRF
|
||||
field_order = ['q', 'tenant_group', 'tenant']
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -349,11 +341,11 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
|
||||
|
||||
class PrefixCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(
|
||||
vrf = FlexibleModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
to_field_name='rd',
|
||||
help_text='Route distinguisher of parent VRF',
|
||||
required=False,
|
||||
help_text='Route distinguisher of parent VRF (or {ID})',
|
||||
error_messages={
|
||||
'invalid_choice': 'VRF not found.',
|
||||
}
|
||||
@@ -497,8 +489,12 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
||||
]
|
||||
|
||||
|
||||
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
model = Prefix
|
||||
field_order = [
|
||||
'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'site', 'role', 'tenant_group', 'tenant',
|
||||
'is_pool', 'expand',
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
@@ -524,24 +520,12 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
label='Mask length',
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
vrf = FilterChoiceField(
|
||||
vrf_id = FilterChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
to_field_name='rd',
|
||||
label='VRF',
|
||||
null_label='-- Global --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/ipam/vrfs/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
@@ -764,11 +748,11 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
|
||||
|
||||
class IPAddressCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(
|
||||
vrf = FlexibleModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
to_field_name='rd',
|
||||
help_text='Route distinguisher of the assigned VRF',
|
||||
required=False,
|
||||
help_text='Route distinguisher of parent VRF (or {ID})',
|
||||
error_messages={
|
||||
'invalid_choice': 'VRF not found.',
|
||||
}
|
||||
@@ -946,8 +930,11 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
model = IPAddress
|
||||
field_order = [
|
||||
'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'tenant_group', 'tenant',
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
@@ -973,24 +960,12 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
label='Mask length',
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
vrf = FilterChoiceField(
|
||||
vrf_id = FilterChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
to_field_name='rd',
|
||||
label='VRF',
|
||||
null_label='-- Global --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/ipam/vrfs/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
@@ -1225,8 +1200,9 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
||||
]
|
||||
|
||||
|
||||
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
model = VLAN
|
||||
field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
@@ -1250,16 +1226,6 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=VLAN_STATUS_CHOICES,
|
||||
required=False,
|
||||
|
||||
18
netbox/ipam/migrations/0024_vrf_allow_null_rd.py
Normal file
18
netbox/ipam/migrations/0024_vrf_allow_null_rd.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.1.5 on 2019-01-31 18:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0023_change_logging'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='vrf',
|
||||
name='rd',
|
||||
field=models.CharField(blank=True, max_length=21, null=True, unique=True),
|
||||
),
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
import netaddr
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
@@ -10,8 +10,9 @@ from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.models import Interface
|
||||
from extras.models import CustomFieldModel
|
||||
from extras.models import CustomFieldModel, ObjectChange
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.utils import serialize_object
|
||||
from .constants import *
|
||||
from .fields import IPNetworkField, IPAddressField
|
||||
from .querysets import PrefixQuerySet
|
||||
@@ -29,6 +30,8 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
|
||||
rd = models.CharField(
|
||||
max_length=21,
|
||||
unique=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Route distinguisher'
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
@@ -79,9 +82,9 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
if self.name and self.rd:
|
||||
if self.rd:
|
||||
return "{} ({})".format(self.name, self.rd)
|
||||
return None
|
||||
return self.name
|
||||
|
||||
|
||||
class RIR(ChangeLoggedModel):
|
||||
@@ -627,6 +630,27 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
self.family = self.address.version
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def log_change(self, user, request_id, action):
|
||||
"""
|
||||
Include the connected Interface (if any).
|
||||
"""
|
||||
|
||||
# It's possible that an IPAddress can be deleted _after_ its parent Interface, in which case trying to resolve
|
||||
# the interface will raise DoesNotExist.
|
||||
try:
|
||||
parent_obj = self.interface
|
||||
except ObjectDoesNotExist:
|
||||
parent_obj = None
|
||||
|
||||
ObjectChange(
|
||||
user=user,
|
||||
request_id=request_id,
|
||||
changed_object=self,
|
||||
related_object=parent_obj,
|
||||
action=action,
|
||||
object_data=serialize_object(self)
|
||||
).save()
|
||||
|
||||
def to_csv(self):
|
||||
|
||||
# Determine if this IP is primary for a Device
|
||||
|
||||
@@ -30,7 +30,7 @@ RIR_ACTIONS = """
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.ipam.change_rir %}
|
||||
<a href="{% url 'ipam:rir_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'ipam:rir_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -52,7 +52,7 @@ ROLE_ACTIONS = """
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.ipam.change_role %}
|
||||
<a href="{% url 'ipam:role_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'ipam:role_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -152,7 +152,7 @@ VLANGROUP_ACTIONS = """
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if perms.ipam.change_vlangroup %}
|
||||
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -203,7 +203,7 @@ class RIRTable(BaseTable):
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
is_private = BooleanColumn(verbose_name='Private')
|
||||
aggregate_count = tables.Column(verbose_name='Aggregates')
|
||||
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RIR
|
||||
@@ -288,7 +288,7 @@ class RoleTable(BaseTable):
|
||||
orderable=False,
|
||||
verbose_name='VLANs'
|
||||
)
|
||||
actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||
actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Role
|
||||
@@ -319,6 +319,7 @@ class PrefixTable(BaseTable):
|
||||
|
||||
class PrefixDetailTable(PrefixTable):
|
||||
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False)
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
|
||||
class Meta(PrefixTable.Meta):
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
@@ -349,6 +350,7 @@ class IPAddressDetailTable(IPAddressTable):
|
||||
nat_inside = tables.LinkColumn(
|
||||
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
|
||||
)
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
|
||||
class Meta(IPAddressTable.Meta):
|
||||
fields = (
|
||||
@@ -392,7 +394,7 @@ class VLANGroupTable(BaseTable):
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
vlan_count = tables.Column(verbose_name='VLANs')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@@ -423,6 +425,7 @@ class VLANTable(BaseTable):
|
||||
|
||||
class VLANDetailTable(VLANTable):
|
||||
prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes')
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
|
||||
class Meta(VLANTable.Meta):
|
||||
fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
|
||||
@@ -437,7 +440,7 @@ class VLANMemberTable(BaseTable):
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=VLAN_MEMBER_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import json
|
||||
|
||||
from django.urls import reverse
|
||||
from netaddr import IPNetwork
|
||||
from rest_framework import status
|
||||
@@ -16,7 +18,7 @@ class VRFTest(APITestCase):
|
||||
|
||||
self.vrf1 = VRF.objects.create(name='Test VRF 1', rd='65000:1')
|
||||
self.vrf2 = VRF.objects.create(name='Test VRF 2', rd='65000:2')
|
||||
self.vrf3 = VRF.objects.create(name='Test VRF 3', rd='65000:3')
|
||||
self.vrf3 = VRF.objects.create(name='Test VRF 3') # No RD
|
||||
|
||||
def test_get_vrf(self):
|
||||
|
||||
@@ -44,19 +46,26 @@ class VRFTest(APITestCase):
|
||||
|
||||
def test_create_vrf(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test VRF 4',
|
||||
'rd': '65000:4',
|
||||
}
|
||||
data_list = [
|
||||
# VRF with RD
|
||||
{
|
||||
'name': 'Test VRF 4',
|
||||
'rd': '65000:4',
|
||||
},
|
||||
# VRF without RD
|
||||
{
|
||||
'name': 'Test VRF 5',
|
||||
}
|
||||
]
|
||||
|
||||
url = reverse('ipam-api:vrf-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(VRF.objects.count(), 4)
|
||||
vrf4 = VRF.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(vrf4.name, data['name'])
|
||||
self.assertEqual(vrf4.rd, data['rd'])
|
||||
for data in data_list:
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
vrf = VRF.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(vrf.name, data['name'])
|
||||
self.assertEqual(vrf.rd, data['rd'] if 'rd' in data else None)
|
||||
|
||||
def test_create_vrf_bulk(self):
|
||||
|
||||
@@ -863,6 +872,8 @@ class VLANTest(APITestCase):
|
||||
self.vlan2 = VLAN.objects.create(vid=2, name='Test VLAN 2')
|
||||
self.vlan3 = VLAN.objects.create(vid=3, name='Test VLAN 3')
|
||||
|
||||
self.prefix1 = Prefix.objects.create(prefix=IPNetwork('192.168.1.0/24'))
|
||||
|
||||
def test_get_vlan(self):
|
||||
|
||||
url = reverse('ipam-api:vlan-detail', kwargs={'pk': self.vlan1.pk})
|
||||
@@ -953,6 +964,20 @@ class VLANTest(APITestCase):
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(VLAN.objects.count(), 2)
|
||||
|
||||
def test_delete_vlan_with_prefix(self):
|
||||
self.prefix1.vlan = self.vlan1
|
||||
self.prefix1.save()
|
||||
|
||||
url = reverse('ipam-api:vlan-detail', kwargs={'pk': self.vlan1.pk})
|
||||
response = self.client.delete(url, **self.header)
|
||||
|
||||
# can't use assertHttpStatus here because we don't have response.data
|
||||
self.assertEqual(response.status_code, 409)
|
||||
|
||||
content = json.loads(response.content.decode('utf-8'))
|
||||
self.assertIn('detail', content)
|
||||
self.assertTrue(content['detail'].startswith('Unable to delete object.'))
|
||||
|
||||
|
||||
class ServiceTest(APITestCase):
|
||||
|
||||
|
||||
282
netbox/ipam/tests/test_views.py
Normal file
282
netbox/ipam/tests/test_views.py
Normal file
@@ -0,0 +1,282 @@
|
||||
from netaddr import IPNetwork
|
||||
import urllib.parse
|
||||
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from ipam.constants import IP_PROTOCOL_TCP
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
class VRFTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
VRF.objects.bulk_create([
|
||||
VRF(name='VRF 1', rd='65000:1'),
|
||||
VRF(name='VRF 2', rd='65000:2'),
|
||||
VRF(name='VRF 3', rd='65000:3'),
|
||||
])
|
||||
|
||||
def test_vrf_list(self):
|
||||
|
||||
url = reverse('ipam:vrf_list')
|
||||
params = {
|
||||
"q": "65000",
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_configcontext(self):
|
||||
|
||||
vrf = VRF.objects.first()
|
||||
response = self.client.get(vrf.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class RIRTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
RIR.objects.bulk_create([
|
||||
RIR(name='RIR 1', slug='rir-1'),
|
||||
RIR(name='RIR 2', slug='rir-2'),
|
||||
RIR(name='RIR 3', slug='rir-3'),
|
||||
])
|
||||
|
||||
def test_rir_list(self):
|
||||
|
||||
url = reverse('ipam:rir_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_rir(self):
|
||||
|
||||
rir = RIR.objects.first()
|
||||
response = self.client.get(rir.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class AggregateTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
rir = RIR(name='RIR 1', slug='rir-1')
|
||||
rir.save()
|
||||
|
||||
Aggregate.objects.bulk_create([
|
||||
Aggregate(family=4, prefix=IPNetwork('10.1.0.0/16'), rir=rir),
|
||||
Aggregate(family=4, prefix=IPNetwork('10.2.0.0/16'), rir=rir),
|
||||
Aggregate(family=4, prefix=IPNetwork('10.3.0.0/16'), rir=rir),
|
||||
])
|
||||
|
||||
def test_aggregate_list(self):
|
||||
|
||||
url = reverse('ipam:aggregate_list')
|
||||
params = {
|
||||
"rir": RIR.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_aggregate(self):
|
||||
|
||||
aggregate = Aggregate.objects.first()
|
||||
response = self.client.get(aggregate.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class RoleTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
Role.objects.bulk_create([
|
||||
Role(name='Role 1', slug='role-1'),
|
||||
Role(name='Role 2', slug='role-2'),
|
||||
Role(name='Role 3', slug='role-3'),
|
||||
])
|
||||
|
||||
def test_role_list(self):
|
||||
|
||||
url = reverse('ipam:role_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class PrefixTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
Prefix.objects.bulk_create([
|
||||
Prefix(family=4, prefix=IPNetwork('10.1.0.0/16'), site=site),
|
||||
Prefix(family=4, prefix=IPNetwork('10.2.0.0/16'), site=site),
|
||||
Prefix(family=4, prefix=IPNetwork('10.3.0.0/16'), site=site),
|
||||
])
|
||||
|
||||
def test_prefix_list(self):
|
||||
|
||||
url = reverse('ipam:prefix_list')
|
||||
params = {
|
||||
"site": Site.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_prefix(self):
|
||||
|
||||
prefix = Prefix.objects.first()
|
||||
response = self.client.get(prefix.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class IPAddressTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
vrf = VRF(name='VRF 1', rd='65000:1')
|
||||
vrf.save()
|
||||
|
||||
IPAddress.objects.bulk_create([
|
||||
IPAddress(family=4, address=IPNetwork('10.1.0.0/16'), vrf=vrf),
|
||||
IPAddress(family=4, address=IPNetwork('10.2.0.0/16'), vrf=vrf),
|
||||
IPAddress(family=4, address=IPNetwork('10.3.0.0/16'), vrf=vrf),
|
||||
])
|
||||
|
||||
def test_ipaddress_list(self):
|
||||
|
||||
url = reverse('ipam:ipaddress_list')
|
||||
params = {
|
||||
"vrf": VRF.objects.first().rd,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_ipaddress(self):
|
||||
|
||||
ipaddress = IPAddress.objects.first()
|
||||
response = self.client.get(ipaddress.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class VLANGroupTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
VLANGroup.objects.bulk_create([
|
||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=site),
|
||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=site),
|
||||
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=site),
|
||||
])
|
||||
|
||||
def test_vlangroup_list(self):
|
||||
|
||||
url = reverse('ipam:vlangroup_list')
|
||||
params = {
|
||||
"site": Site.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class VLANTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
vlangroup = VLANGroup(name='VLAN Group 1', slug='vlan-group-1')
|
||||
vlangroup.save()
|
||||
|
||||
VLAN.objects.bulk_create([
|
||||
VLAN(group=vlangroup, vid=101, name='VLAN101'),
|
||||
VLAN(group=vlangroup, vid=102, name='VLAN102'),
|
||||
VLAN(group=vlangroup, vid=103, name='VLAN103'),
|
||||
])
|
||||
|
||||
def test_vlan_list(self):
|
||||
|
||||
url = reverse('ipam:vlan_list')
|
||||
params = {
|
||||
"group": VLANGroup.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_vlan(self):
|
||||
|
||||
vlan = VLAN.objects.first()
|
||||
response = self.client.get(vlan.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class ServiceTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
|
||||
manufacturer.save()
|
||||
|
||||
devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1')
|
||||
devicetype.save()
|
||||
|
||||
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
|
||||
devicerole.save()
|
||||
|
||||
device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
|
||||
device.save()
|
||||
|
||||
Service.objects.bulk_create([
|
||||
Service(device=device, name='Service 1', protocol=IP_PROTOCOL_TCP, port=101),
|
||||
Service(device=device, name='Service 2', protocol=IP_PROTOCOL_TCP, port=102),
|
||||
Service(device=device, name='Service 3', protocol=IP_PROTOCOL_TCP, port=103),
|
||||
])
|
||||
|
||||
def test_service_list(self):
|
||||
|
||||
url = reverse('ipam:service_list')
|
||||
params = {
|
||||
"device_id": Device.objects.first(),
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_service(self):
|
||||
|
||||
service = Service.objects.first()
|
||||
response = self.client.get(service.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from extras.views import ObjectChangeLogView
|
||||
from . import views
|
||||
@@ -8,97 +8,97 @@ app_name = 'ipam'
|
||||
urlpatterns = [
|
||||
|
||||
# VRFs
|
||||
url(r'^vrfs/$', views.VRFListView.as_view(), name='vrf_list'),
|
||||
url(r'^vrfs/add/$', views.VRFCreateView.as_view(), name='vrf_add'),
|
||||
url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'),
|
||||
url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
|
||||
url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
|
||||
url(r'^vrfs/(?P<pk>\d+)/$', views.VRFView.as_view(), name='vrf'),
|
||||
url(r'^vrfs/(?P<pk>\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'),
|
||||
url(r'^vrfs/(?P<pk>\d+)/delete/$', views.VRFDeleteView.as_view(), name='vrf_delete'),
|
||||
url(r'^vrfs/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}),
|
||||
path(r'vrfs/', views.VRFListView.as_view(), name='vrf_list'),
|
||||
path(r'vrfs/add/', views.VRFCreateView.as_view(), name='vrf_add'),
|
||||
path(r'vrfs/import/', views.VRFBulkImportView.as_view(), name='vrf_import'),
|
||||
path(r'vrfs/edit/', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
|
||||
path(r'vrfs/delete/', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
|
||||
path(r'vrfs/<int:pk>/', views.VRFView.as_view(), name='vrf'),
|
||||
path(r'vrfs/<int:pk>/edit/', views.VRFEditView.as_view(), name='vrf_edit'),
|
||||
path(r'vrfs/<int:pk>/delete/', views.VRFDeleteView.as_view(), name='vrf_delete'),
|
||||
path(r'vrfs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}),
|
||||
|
||||
# RIRs
|
||||
url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'),
|
||||
url(r'^rirs/add/$', views.RIRCreateView.as_view(), name='rir_add'),
|
||||
url(r'^rirs/import/$', views.RIRBulkImportView.as_view(), name='rir_import'),
|
||||
url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
|
||||
url(r'^rirs/(?P<slug>[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'),
|
||||
url(r'^vrfs/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}),
|
||||
path(r'rirs/', views.RIRListView.as_view(), name='rir_list'),
|
||||
path(r'rirs/add/', views.RIRCreateView.as_view(), name='rir_add'),
|
||||
path(r'rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'),
|
||||
path(r'rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
|
||||
path(r'rirs/<slug:slug>/edit/', views.RIREditView.as_view(), name='rir_edit'),
|
||||
path(r'vrfs/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}),
|
||||
|
||||
# Aggregates
|
||||
url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'),
|
||||
url(r'^aggregates/add/$', views.AggregateCreateView.as_view(), name='aggregate_add'),
|
||||
url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
|
||||
url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
|
||||
url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
|
||||
url(r'^aggregates/(?P<pk>\d+)/$', views.AggregateView.as_view(), name='aggregate'),
|
||||
url(r'^aggregates/(?P<pk>\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'),
|
||||
url(r'^aggregates/(?P<pk>\d+)/delete/$', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
|
||||
url(r'^aggregates/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}),
|
||||
path(r'aggregates/', views.AggregateListView.as_view(), name='aggregate_list'),
|
||||
path(r'aggregates/add/', views.AggregateCreateView.as_view(), name='aggregate_add'),
|
||||
path(r'aggregates/import/', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
|
||||
path(r'aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
|
||||
path(r'aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
|
||||
path(r'aggregates/<int:pk>/', views.AggregateView.as_view(), name='aggregate'),
|
||||
path(r'aggregates/<int:pk>/edit/', views.AggregateEditView.as_view(), name='aggregate_edit'),
|
||||
path(r'aggregates/<int:pk>/delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
|
||||
path(r'aggregates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}),
|
||||
|
||||
# Roles
|
||||
url(r'^roles/$', views.RoleListView.as_view(), name='role_list'),
|
||||
url(r'^roles/add/$', views.RoleCreateView.as_view(), name='role_add'),
|
||||
url(r'^roles/import/$', views.RoleBulkImportView.as_view(), name='role_import'),
|
||||
url(r'^roles/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
|
||||
url(r'^roles/(?P<slug>[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'),
|
||||
url(r'^roles/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}),
|
||||
path(r'roles/', views.RoleListView.as_view(), name='role_list'),
|
||||
path(r'roles/add/', views.RoleCreateView.as_view(), name='role_add'),
|
||||
path(r'roles/import/', views.RoleBulkImportView.as_view(), name='role_import'),
|
||||
path(r'roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
|
||||
path(r'roles/<slug:slug>/edit/', views.RoleEditView.as_view(), name='role_edit'),
|
||||
path(r'roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}),
|
||||
|
||||
# Prefixes
|
||||
url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'),
|
||||
url(r'^prefixes/add/$', views.PrefixCreateView.as_view(), name='prefix_add'),
|
||||
url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'),
|
||||
url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
|
||||
url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}),
|
||||
url(r'^prefixes/(?P<pk>\d+)/prefixes/$', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
|
||||
path(r'prefixes/', views.PrefixListView.as_view(), name='prefix_list'),
|
||||
path(r'prefixes/add/', views.PrefixCreateView.as_view(), name='prefix_add'),
|
||||
path(r'prefixes/import/', views.PrefixBulkImportView.as_view(), name='prefix_import'),
|
||||
path(r'prefixes/edit/', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
|
||||
path(r'prefixes/delete/', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
|
||||
path(r'prefixes/<int:pk>/', views.PrefixView.as_view(), name='prefix'),
|
||||
path(r'prefixes/<int:pk>/edit/', views.PrefixEditView.as_view(), name='prefix_edit'),
|
||||
path(r'prefixes/<int:pk>/delete/', views.PrefixDeleteView.as_view(), name='prefix_delete'),
|
||||
path(r'prefixes/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}),
|
||||
path(r'prefixes/<int:pk>/prefixes/', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'),
|
||||
path(r'prefixes/<int:pk>/ip-addresses/', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
|
||||
|
||||
# IP addresses
|
||||
url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'),
|
||||
url(r'^ip-addresses/add/$', views.IPAddressCreateView.as_view(), name='ipaddress_add'),
|
||||
url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'),
|
||||
url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
|
||||
url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
|
||||
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}),
|
||||
url(r'^ip-addresses/assign/$', views.IPAddressAssignView.as_view(), name='ipaddress_assign'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
|
||||
path(r'ip-addresses/', views.IPAddressListView.as_view(), name='ipaddress_list'),
|
||||
path(r'ip-addresses/add/', views.IPAddressCreateView.as_view(), name='ipaddress_add'),
|
||||
path(r'ip-addresses/bulk-add/', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'),
|
||||
path(r'ip-addresses/import/', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
|
||||
path(r'ip-addresses/edit/', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
|
||||
path(r'ip-addresses/delete/', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
|
||||
path(r'ip-addresses/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}),
|
||||
path(r'ip-addresses/assign/', views.IPAddressAssignView.as_view(), name='ipaddress_assign'),
|
||||
path(r'ip-addresses/<int:pk>/', views.IPAddressView.as_view(), name='ipaddress'),
|
||||
path(r'ip-addresses/<int:pk>/edit/', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
|
||||
path(r'ip-addresses/<int:pk>/delete/', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
|
||||
|
||||
# VLAN groups
|
||||
url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'),
|
||||
url(r'^vlan-groups/add/$', views.VLANGroupCreateView.as_view(), name='vlangroup_add'),
|
||||
url(r'^vlan-groups/import/$', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
|
||||
url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
|
||||
url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
|
||||
url(r'^vlan-groups/(?P<pk>\d+)/vlans/$', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'),
|
||||
url(r'^vlan-groups/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),
|
||||
path(r'vlan-groups/', views.VLANGroupListView.as_view(), name='vlangroup_list'),
|
||||
path(r'vlan-groups/add/', views.VLANGroupCreateView.as_view(), name='vlangroup_add'),
|
||||
path(r'vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
|
||||
path(r'vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
|
||||
path(r'vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
|
||||
path(r'vlan-groups/<int:pk>/vlans/', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'),
|
||||
path(r'vlan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),
|
||||
|
||||
# VLANs
|
||||
url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
|
||||
url(r'^vlans/add/$', views.VLANCreateView.as_view(), name='vlan_add'),
|
||||
url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'),
|
||||
url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
|
||||
url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
||||
url(r'^vlans/(?P<pk>\d+)/$', views.VLANView.as_view(), name='vlan'),
|
||||
url(r'^vlans/(?P<pk>\d+)/members/$', views.VLANMembersView.as_view(), name='vlan_members'),
|
||||
url(r'^vlans/(?P<pk>\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'),
|
||||
url(r'^vlans/(?P<pk>\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'),
|
||||
url(r'^vlans/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),
|
||||
path(r'vlans/', views.VLANListView.as_view(), name='vlan_list'),
|
||||
path(r'vlans/add/', views.VLANCreateView.as_view(), name='vlan_add'),
|
||||
path(r'vlans/import/', views.VLANBulkImportView.as_view(), name='vlan_import'),
|
||||
path(r'vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
|
||||
path(r'vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
||||
path(r'vlans/<int:pk>/', views.VLANView.as_view(), name='vlan'),
|
||||
path(r'vlans/<int:pk>/members/', views.VLANMembersView.as_view(), name='vlan_members'),
|
||||
path(r'vlans/<int:pk>/edit/', views.VLANEditView.as_view(), name='vlan_edit'),
|
||||
path(r'vlans/<int:pk>/delete/', views.VLANDeleteView.as_view(), name='vlan_delete'),
|
||||
path(r'vlans/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),
|
||||
|
||||
# Services
|
||||
url(r'^services/$', views.ServiceListView.as_view(), name='service_list'),
|
||||
url(r'^services/edit/$', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
|
||||
url(r'^services/delete/$', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),
|
||||
url(r'^services/(?P<pk>\d+)/$', views.ServiceView.as_view(), name='service'),
|
||||
url(r'^services/(?P<pk>\d+)/edit/$', views.ServiceEditView.as_view(), name='service_edit'),
|
||||
url(r'^services/(?P<pk>\d+)/delete/$', views.ServiceDeleteView.as_view(), name='service_delete'),
|
||||
url(r'^services/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}),
|
||||
path(r'services/', views.ServiceListView.as_view(), name='service_list'),
|
||||
path(r'services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
|
||||
path(r'services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),
|
||||
path(r'services/<int:pk>/', views.ServiceView.as_view(), name='service'),
|
||||
path(r'services/<int:pk>/edit/', views.ServiceEditView.as_view(), name='service_edit'),
|
||||
path(r'services/<int:pk>/delete/', views.ServiceDeleteView.as_view(), name='service_delete'),
|
||||
path(r'services/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}),
|
||||
|
||||
]
|
||||
|
||||
@@ -126,14 +126,11 @@ class VRFView(View):
|
||||
def get(self, request, pk):
|
||||
|
||||
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
|
||||
prefix_table = tables.PrefixTable(
|
||||
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')), orderable=False
|
||||
)
|
||||
prefix_table.exclude = ('vrf',)
|
||||
prefix_count = Prefix.objects.filter(vrf=vrf).count()
|
||||
|
||||
return render(request, 'ipam/vrf.html', {
|
||||
'vrf': vrf,
|
||||
'prefix_table': prefix_table,
|
||||
'prefix_count': prefix_count,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -147,18 +147,18 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
|
||||
# Miscellaneous
|
||||
#
|
||||
|
||||
def get_view_name(view_cls, suffix=None):
|
||||
def get_view_name(view, suffix=None):
|
||||
"""
|
||||
Derive the view name from its associated model, if it has one. Fall back to DRF's built-in `get_view_name`.
|
||||
"""
|
||||
if hasattr(view_cls, 'queryset'):
|
||||
if hasattr(view, 'queryset'):
|
||||
# Determine the model name from the queryset.
|
||||
name = view_cls.queryset.model._meta.verbose_name
|
||||
name = view.queryset.model._meta.verbose_name
|
||||
name = ' '.join([w[0].upper() + w[1:] for w in name.split()]) # Capitalize each word
|
||||
|
||||
else:
|
||||
# Replicate DRF's built-in behavior.
|
||||
name = view_cls.__name__
|
||||
name = view.__class__.__name__
|
||||
name = formatting.remove_trailing_string(name, 'View')
|
||||
name = formatting.remove_trailing_string(name, 'ViewSet')
|
||||
name = formatting.camelcase_to_spaces(name)
|
||||
|
||||
@@ -132,6 +132,7 @@ REDIS = {
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 0,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
|
||||
# The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of
|
||||
|
||||
@@ -22,7 +22,7 @@ except ImportError:
|
||||
)
|
||||
|
||||
|
||||
VERSION = '2.5.4'
|
||||
VERSION = '2.5.13'
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
@@ -131,6 +131,7 @@ REDIS_PORT = REDIS.get('PORT', 6379)
|
||||
REDIS_PASSWORD = REDIS.get('PASSWORD', '')
|
||||
REDIS_DATABASE = REDIS.get('DATABASE', 0)
|
||||
REDIS_DEFAULT_TIMEOUT = REDIS.get('DEFAULT_TIMEOUT', 300)
|
||||
REDIS_SSL = REDIS.get('SSL', False)
|
||||
|
||||
# Email
|
||||
EMAIL_HOST = EMAIL.get('SERVER')
|
||||
@@ -197,7 +198,7 @@ ROOT_URLCONF = 'netbox.urls'
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR + '/templates/'],
|
||||
'DIRS': [BASE_DIR + '/templates'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
@@ -223,7 +224,7 @@ USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STATIC_ROOT = BASE_DIR + '/static/'
|
||||
STATIC_ROOT = BASE_DIR + '/static'
|
||||
STATIC_URL = '/{}static/'.format(BASE_PATH)
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, "project-static"),
|
||||
@@ -291,6 +292,7 @@ RQ_QUEUES = {
|
||||
'DB': REDIS_DATABASE,
|
||||
'PASSWORD': REDIS_PASSWORD,
|
||||
'DEFAULT_TIMEOUT': REDIS_DEFAULT_TIMEOUT,
|
||||
'SSL': REDIS_SSL,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +317,7 @@ SWAGGER_SETTINGS = {
|
||||
'utilities.custom_inspectors.IdInFilterInspector',
|
||||
'drf_yasg.inspectors.CoreAPICompatInspector',
|
||||
],
|
||||
'DEFAULT_MODEL_DEPTH': 1,
|
||||
'DEFAULT_PAGINATOR_INSPECTORS': [
|
||||
'utilities.custom_inspectors.NullablePaginatorInspector',
|
||||
'drf_yasg.inspectors.DjangoRestResponsePagination',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.conf.urls import include
|
||||
from django.urls import path, re_path
|
||||
from django.views.static import serve
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.views import get_schema_view
|
||||
@@ -24,58 +25,58 @@ schema_view = get_schema_view(
|
||||
_patterns = [
|
||||
|
||||
# Base views
|
||||
url(r'^$', HomeView.as_view(), name='home'),
|
||||
url(r'^search/$', SearchView.as_view(), name='search'),
|
||||
path(r'', HomeView.as_view(), name='home'),
|
||||
path(r'search/', SearchView.as_view(), name='search'),
|
||||
|
||||
# Login/logout
|
||||
url(r'^login/$', LoginView.as_view(), name='login'),
|
||||
url(r'^logout/$', LogoutView.as_view(), name='logout'),
|
||||
path(r'login/', LoginView.as_view(), name='login'),
|
||||
path(r'logout/', LogoutView.as_view(), name='logout'),
|
||||
|
||||
# Apps
|
||||
url(r'^circuits/', include('circuits.urls')),
|
||||
url(r'^dcim/', include('dcim.urls')),
|
||||
url(r'^extras/', include('extras.urls')),
|
||||
url(r'^ipam/', include('ipam.urls')),
|
||||
url(r'^secrets/', include('secrets.urls')),
|
||||
url(r'^tenancy/', include('tenancy.urls')),
|
||||
url(r'^user/', include('users.urls')),
|
||||
url(r'^virtualization/', include('virtualization.urls')),
|
||||
path(r'circuits/', include('circuits.urls')),
|
||||
path(r'dcim/', include('dcim.urls')),
|
||||
path(r'extras/', include('extras.urls')),
|
||||
path(r'ipam/', include('ipam.urls')),
|
||||
path(r'secrets/', include('secrets.urls')),
|
||||
path(r'tenancy/', include('tenancy.urls')),
|
||||
path(r'user/', include('users.urls')),
|
||||
path(r'virtualization/', include('virtualization.urls')),
|
||||
|
||||
# API
|
||||
url(r'^api/$', APIRootView.as_view(), name='api-root'),
|
||||
url(r'^api/circuits/', include('circuits.api.urls')),
|
||||
url(r'^api/dcim/', include('dcim.api.urls')),
|
||||
url(r'^api/extras/', include('extras.api.urls')),
|
||||
url(r'^api/ipam/', include('ipam.api.urls')),
|
||||
url(r'^api/secrets/', include('secrets.api.urls')),
|
||||
url(r'^api/tenancy/', include('tenancy.api.urls')),
|
||||
url(r'^api/virtualization/', include('virtualization.api.urls')),
|
||||
url(r'^api/docs/$', schema_view.with_ui('swagger'), name='api_docs'),
|
||||
url(r'^api/redoc/$', schema_view.with_ui('redoc'), name='api_redocs'),
|
||||
url(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'),
|
||||
path(r'api/', APIRootView.as_view(), name='api-root'),
|
||||
path(r'api/circuits/', include('circuits.api.urls')),
|
||||
path(r'api/dcim/', include('dcim.api.urls')),
|
||||
path(r'api/extras/', include('extras.api.urls')),
|
||||
path(r'api/ipam/', include('ipam.api.urls')),
|
||||
path(r'api/secrets/', include('secrets.api.urls')),
|
||||
path(r'api/tenancy/', include('tenancy.api.urls')),
|
||||
path(r'api/virtualization/', include('virtualization.api.urls')),
|
||||
path(r'api/docs/', schema_view.with_ui('swagger'), name='api_docs'),
|
||||
path(r'api/redoc/', schema_view.with_ui('redoc'), name='api_redocs'),
|
||||
re_path(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'),
|
||||
|
||||
# Serving static media in Django to pipe it through LoginRequiredMiddleware
|
||||
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
||||
path(r'media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}),
|
||||
|
||||
# Admin
|
||||
url(r'^admin/', admin_site.urls),
|
||||
path(r'admin/', admin_site.urls),
|
||||
|
||||
]
|
||||
|
||||
if settings.WEBHOOKS_ENABLED:
|
||||
_patterns += [
|
||||
url(r'^admin/webhook-backend-status/', include('django_rq.urls')),
|
||||
path(r'admin/webhook-backend-status/', include('django_rq.urls')),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
import debug_toolbar
|
||||
_patterns += [
|
||||
url(r'^__debug__/', include(debug_toolbar.urls)),
|
||||
path(r'__debug__/', include(debug_toolbar.urls)),
|
||||
]
|
||||
|
||||
# Prepend BASE_PATH
|
||||
urlpatterns = [
|
||||
url(r'^{}'.format(settings.BASE_PATH), include(_patterns))
|
||||
path(r'{}'.format(settings.BASE_PATH), include(_patterns))
|
||||
]
|
||||
|
||||
handler500 = 'utilities.views.server_error'
|
||||
|
||||
@@ -267,6 +267,7 @@ class SearchView(View):
|
||||
class APIRootView(APIView):
|
||||
_ignore_model_permissions = True
|
||||
exclude_from_schema = True
|
||||
swagger_schema = None
|
||||
|
||||
def get_view_name(self):
|
||||
return "API Root"
|
||||
|
||||
7
netbox/project-static/clipboard-2.0.4.min.js
vendored
Executable file
7
netbox/project-static/clipboard-2.0.4.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
@@ -49,6 +49,19 @@ footer p {
|
||||
}
|
||||
}
|
||||
|
||||
/* Printer friendly CSS class and various fixes for printing. */
|
||||
@media print {
|
||||
body {
|
||||
padding-top: 0px;
|
||||
}
|
||||
a[href]:after {
|
||||
content: none !important;
|
||||
}
|
||||
.noprint {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Collapse the nav menu on displays less than 960px wide */
|
||||
@media (max-width: 959px) {
|
||||
.navbar-header {
|
||||
@@ -546,6 +559,7 @@ table.report th a {
|
||||
.color-block {
|
||||
display: block;
|
||||
width: 80px;
|
||||
border: 1px solid grey;
|
||||
}
|
||||
.text-nowrap {
|
||||
white-space: nowrap;
|
||||
@@ -575,4 +589,4 @@ td .progress {
|
||||
}
|
||||
textarea {
|
||||
font-family: Consolas, Lucida Console, monospace;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,10 @@ $(document).ready(function() {
|
||||
// Assign color picker selection classes
|
||||
function colorPickerClassCopy(data, container) {
|
||||
if (data.element) {
|
||||
// Remove any existing color-selection classes
|
||||
$(container).attr('class', function(i, c) {
|
||||
return c.replace(/(^|\s)color-selection-\S+/g, '');
|
||||
});
|
||||
$(container).addClass($(data.element).attr("class"));
|
||||
}
|
||||
return data.text;
|
||||
@@ -151,10 +155,14 @@ $(document).ready(function() {
|
||||
|
||||
filter_for_elements.each(function(index, filter_for_element) {
|
||||
var param_name = $(filter_for_element).attr(attr_name);
|
||||
var is_nullable = $(filter_for_element).attr("nullable");
|
||||
var is_visible = $(filter_for_element).is(":visible");
|
||||
var value = $(filter_for_element).val();
|
||||
|
||||
if (param_name && value) {
|
||||
if (param_name && is_visible && value) {
|
||||
parameters[param_name] = value;
|
||||
} else if (param_name && is_visible && is_nullable) {
|
||||
parameters[param_name] = "null";
|
||||
}
|
||||
});
|
||||
|
||||
@@ -197,8 +205,8 @@ $(document).ready(function() {
|
||||
return obj;
|
||||
});
|
||||
|
||||
// Handle the null option
|
||||
if (element.getAttribute('data-null-option')) {
|
||||
// Handle the null option, but only add it once
|
||||
if (element.getAttribute('data-null-option') && data.previous === null) {
|
||||
var null_option = $(element).children()[0]
|
||||
results.unshift({
|
||||
id: null_option.value,
|
||||
@@ -243,7 +251,7 @@ $(document).ready(function() {
|
||||
|
||||
ajax: {
|
||||
delay: 250,
|
||||
url: "/api/extras/tags/",
|
||||
url: netbox_api_path + "extras/tags/",
|
||||
|
||||
data: function(params) {
|
||||
// Paging. Note that `params.page` indexes at 1
|
||||
@@ -259,6 +267,10 @@ $(document).ready(function() {
|
||||
|
||||
processResults: function (data) {
|
||||
var results = $.map(data.results, function (obj) {
|
||||
// If tag contains space add double quotes
|
||||
if (/\s/.test(obj.name))
|
||||
obj.name = '"' + obj.name + '"'
|
||||
|
||||
return {
|
||||
id: obj.name,
|
||||
text: obj.name
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
$(document).ready(function() {
|
||||
// Instantiate ClipboardJS on all copy buttons
|
||||
new ClipboardJS('button.copy-secret');
|
||||
|
||||
// Unlocking a secret
|
||||
$('button.unlock-secret').click(function(event) {
|
||||
@@ -45,6 +47,7 @@ $(document).ready(function() {
|
||||
console.log("Secret retrieved successfully");
|
||||
$('#secret_' + secret_id).text(response.plaintext);
|
||||
$('button.unlock-secret[secret-id=' + secret_id + ']').hide();
|
||||
$('button.copy-secret[secret-id=' + secret_id + ']').show();
|
||||
$('button.lock-secret[secret-id=' + secret_id + ']').show();
|
||||
} else {
|
||||
console.log("Secret was not decrypted. Prompt user for private key.");
|
||||
@@ -67,6 +70,7 @@ $(document).ready(function() {
|
||||
var secret_div = $('#secret_' + secret_id);
|
||||
secret_div.html('********');
|
||||
$('button.lock-secret[secret-id=' + secret_id + ']').hide();
|
||||
$('button.copy-secret[secret-id=' + secret_id + ']').hide();
|
||||
$('button.unlock-secret[secret-id=' + secret_id + ']').show();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ from django.db.models import Q
|
||||
|
||||
from dcim.models import Device
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from utilities.filters import NumericInFilter, TagFilter
|
||||
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
|
||||
from .models import Secret, SecretRole
|
||||
|
||||
|
||||
class SecretRoleFilter(django_filters.FilterSet):
|
||||
class SecretRoleFilter(NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = SecretRole
|
||||
|
||||
@@ -8,7 +8,7 @@ SECRETROLE_ACTIONS = """
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.secrets.change_secretrole %}
|
||||
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -23,7 +23,7 @@ class SecretRoleTable(BaseTable):
|
||||
secret_count = tables.Column(verbose_name='Secrets')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name=''
|
||||
template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
|
||||
82
netbox/secrets/tests/test_views.py
Normal file
82
netbox/secrets/tests/test_views.py
Normal file
@@ -0,0 +1,82 @@
|
||||
import urllib.parse
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from secrets.models import Secret, SecretRole
|
||||
|
||||
|
||||
class SecretRoleTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
TEST_USERNAME = 'testuser'
|
||||
TEST_PASSWORD = 'testpassword'
|
||||
|
||||
User = get_user_model()
|
||||
User.objects.create(username=TEST_USERNAME, email='testuser@example.com', password=TEST_PASSWORD)
|
||||
|
||||
self.client = Client()
|
||||
self.client.login(username=TEST_USERNAME, password=TEST_PASSWORD)
|
||||
|
||||
SecretRole.objects.bulk_create([
|
||||
SecretRole(name='Secret Role 1', slug='secret-role-1'),
|
||||
SecretRole(name='Secret Role 2', slug='secret-role-2'),
|
||||
SecretRole(name='Secret Role 3', slug='secret-role-3'),
|
||||
])
|
||||
|
||||
def test_secretrole_list(self):
|
||||
|
||||
url = reverse('secrets:secret_list')
|
||||
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class SecretTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
|
||||
manufacturer.save()
|
||||
|
||||
devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1')
|
||||
devicetype.save()
|
||||
|
||||
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
|
||||
devicerole.save()
|
||||
|
||||
device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
|
||||
device.save()
|
||||
|
||||
secretrole = SecretRole(name='Secret Role 1', slug='secret-role-1')
|
||||
secretrole.save()
|
||||
|
||||
Secret.objects.bulk_create([
|
||||
Secret(device=device, role=secretrole, name='Secret 1', ciphertext=b'1234567890'),
|
||||
Secret(device=device, role=secretrole, name='Secret 2', ciphertext=b'1234567890'),
|
||||
Secret(device=device, role=secretrole, name='Secret 3', ciphertext=b'1234567890'),
|
||||
])
|
||||
|
||||
def test_secret_list(self):
|
||||
|
||||
url = reverse('secrets:secret_list')
|
||||
params = {
|
||||
"role": SecretRole.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_configcontext(self):
|
||||
|
||||
secret = Secret.objects.first()
|
||||
response = self.client.get(secret.get_absolute_url(), follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from extras.views import ObjectChangeLogView
|
||||
from . import views
|
||||
@@ -8,21 +8,21 @@ app_name = 'secrets'
|
||||
urlpatterns = [
|
||||
|
||||
# Secret roles
|
||||
url(r'^secret-roles/$', views.SecretRoleListView.as_view(), name='secretrole_list'),
|
||||
url(r'^secret-roles/add/$', views.SecretRoleCreateView.as_view(), name='secretrole_add'),
|
||||
url(r'^secret-roles/import/$', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
|
||||
url(r'^secret-roles/delete/$', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
|
||||
url(r'^secret-roles/(?P<slug>[\w-]+)/edit/$', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
|
||||
url(r'^secret-roles/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}),
|
||||
path(r'secret-roles/', views.SecretRoleListView.as_view(), name='secretrole_list'),
|
||||
path(r'secret-roles/add/', views.SecretRoleCreateView.as_view(), name='secretrole_add'),
|
||||
path(r'secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
|
||||
path(r'secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
|
||||
path(r'secret-roles/<slug:slug>/edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
|
||||
path(r'secret-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}),
|
||||
|
||||
# Secrets
|
||||
url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'),
|
||||
url(r'^secrets/import/$', views.SecretBulkImportView.as_view(), name='secret_import'),
|
||||
url(r'^secrets/edit/$', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'),
|
||||
url(r'^secrets/delete/$', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'),
|
||||
url(r'^secrets/(?P<pk>\d+)/$', views.SecretView.as_view(), name='secret'),
|
||||
url(r'^secrets/(?P<pk>\d+)/edit/$', views.secret_edit, name='secret_edit'),
|
||||
url(r'^secrets/(?P<pk>\d+)/delete/$', views.SecretDeleteView.as_view(), name='secret_delete'),
|
||||
url(r'^secrets/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}),
|
||||
path(r'secrets/', views.SecretListView.as_view(), name='secret_list'),
|
||||
path(r'secrets/import/', views.SecretBulkImportView.as_view(), name='secret_import'),
|
||||
path(r'secrets/edit/', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'),
|
||||
path(r'secrets/delete/', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'),
|
||||
path(r'secrets/<int:pk>/', views.SecretView.as_view(), name='secret'),
|
||||
path(r'secrets/<int:pk>/edit/', views.secret_edit, name='secret_edit'),
|
||||
path(r'secrets/<int:pk>/delete/', views.SecretDeleteView.as_view(), name='secret_delete'),
|
||||
path(r'secrets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}),
|
||||
|
||||
]
|
||||
|
||||
@@ -120,6 +120,8 @@ def secret_add(request, pk):
|
||||
secret.plaintext = str(form.cleaned_data['plaintext'])
|
||||
secret.encrypt(master_key)
|
||||
secret.save()
|
||||
form.save_m2m()
|
||||
|
||||
messages.success(request, "Added new secret: {}.".format(secret))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:device_addsecret', pk=device.pk)
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<div class="col-xs-4 text-center">
|
||||
<p class="text-muted">{% now 'Y-m-d H:i:s T' %}</p>
|
||||
</div>
|
||||
<div class="col-xs-4 text-right">
|
||||
<div class="col-xs-4 text-right noprint">
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/">Docs</a> ·
|
||||
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'api_docs' %}">API</a> ·
|
||||
@@ -69,6 +69,7 @@
|
||||
<script src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
|
||||
<script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static 'select2-4.0.5/js/select2.min.js' %}"></script>
|
||||
<script src="{% static 'clipboard-2.0.4.min.js' %}"></script>
|
||||
<script src="{% static 'js/forms.js' %}?v{{ settings.VERSION }}"></script>
|
||||
<script type="text/javascript">
|
||||
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block title %}{{ circuit }}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="row noprint">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'circuits:circuit_list' %}">Circuits</a></li>
|
||||
@@ -25,7 +25,7 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.circuits.change_circuit %}
|
||||
<a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.circuits.add_circuit %}
|
||||
{% add_button 'circuits:circuit_add' %}
|
||||
{% import_button 'circuits:circuit_import' %}
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.circuits.add_circuittype %}
|
||||
{% add_button 'circuits:circuittype_add' %}
|
||||
{% import_button 'circuits:circuittype_import' %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block title %}{{ provider }}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="row noprint">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
|
||||
@@ -25,7 +25,7 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<div class="pull-right noprint">
|
||||
{% if show_graphs %}
|
||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider-graphs' pk=provider.pk %}" title="Show graphs">
|
||||
<i class="fa fa-signal" aria-hidden="true"></i>
|
||||
@@ -85,11 +85,11 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NOC Contact</td>
|
||||
<td>{{ provider.noc_contact|linebreaksbr|placeholder }}</td>
|
||||
<td class="rendered-markdown">{{ provider.noc_contact|gfm|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Admin Contact</td>
|
||||
<td>{{ provider.admin_contact|linebreaksbr|placeholder }}</td>
|
||||
<td class="rendered-markdown">{{ provider.admin_contact|gfm|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Circuits</td>
|
||||
@@ -172,7 +172,7 @@
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if perms.circuits.add_circuit %}
|
||||
<div class="panel-footer text-right">
|
||||
<div class="panel-footer text-right noprint">
|
||||
<a href="{% url 'circuits:circuit_add' %}?provider={{ provider.pk }}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add circuit
|
||||
</a>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.circuits.add_provider %}
|
||||
{% add_button 'circuits:provider_add' %}
|
||||
{% import_button 'circuits:provider_import' %}
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load helpers %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="row noprint">
|
||||
<div class="col-md-12">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:cable_list' %}">Cables</a></li>
|
||||
@@ -10,7 +10,7 @@
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.dcim.change_cable %}
|
||||
<a href="{% url 'dcim:cable_edit' pk=cable.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span> Edit this cable
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
<form method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.dcim.add_cable %}
|
||||
{% import_button 'dcim:cable_import' %}
|
||||
{% endif %}
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:cable_bulk_edit' bulk_delete_url='dcim:cable_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</h4>
|
||||
<p><span class="label label-{% if cable.status %}success{% else %}info{% endif %}">{{ cable.get_status_display }}</span></p>
|
||||
<p>{{ cable.get_type_display|default:"" }}</p>
|
||||
{% if cable.length %}- {{ cable.length }}{{ cable.get_length_unit_display }}{% endif %}
|
||||
{% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %}
|
||||
<span class="label color-block center-block" style="background-color: #{{ cable.color }}"> </span>
|
||||
{% else %}
|
||||
<h4 class="text-muted">No Cable</h4>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
<div class="pull-right noprint">
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Console Connections{% endblock %}</h1>
|
||||
@@ -11,7 +11,7 @@
|
||||
{% include 'responsive_table.html' %}
|
||||
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block title %}{{ device }}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="row noprint">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a></li>
|
||||
@@ -33,7 +33,7 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.dcim.change_device %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
@@ -163,7 +163,7 @@
|
||||
<tr>
|
||||
<td>Device Type</td>
|
||||
<td>
|
||||
<span><a href="{% url 'dcim:devicetype' pk=device.device_type.pk %}">{{ device.device_type.full_name }}</a> ({{ device.device_type.u_height }}U)</span>
|
||||
<span><a href="{% url 'dcim:devicetype' pk=device.device_type.pk %}">{{ device.device_type.display_name }}</a> ({{ device.device_type.u_height }}U)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -199,7 +199,7 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="panel-footer text-right">
|
||||
<div class="panel-footer text-right noprint">
|
||||
{% if perms.dcim.change_virtualchassis %}
|
||||
<a href="{% url 'dcim:virtualchassis_add_member' pk=device.virtual_chassis.pk %}?site={{ device.site.pk }}&rack={{ device.rack.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Member
|
||||
@@ -317,7 +317,7 @@
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
|
||||
<div class="panel-footer text-right">
|
||||
<div class="panel-footer text-right noprint">
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
|
||||
@@ -352,7 +352,7 @@
|
||||
<form id="secret_form">
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
<div class="panel-footer text-right">
|
||||
<div class="panel-footer text-right noprint">
|
||||
<a href="{% url 'dcim:device_addsecret' pk=device.pk %}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add secret
|
||||
@@ -377,7 +377,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_service %}
|
||||
<div class="panel-footer text-right">
|
||||
<div class="panel-footer text-right noprint">
|
||||
<a href="{% url 'dcim:device_service_assign' device=device.pk %}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
|
||||
</a>
|
||||
@@ -390,7 +390,7 @@
|
||||
</div>
|
||||
{% include 'inc/image_attachments.html' with images=device.images.all %}
|
||||
{% if perms.extras.add_imageattachment %}
|
||||
<div class="panel-footer text-right">
|
||||
<div class="panel-footer text-right noprint">
|
||||
<a href="{% url 'dcim:device_add_image' object_id=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Attach an image
|
||||
@@ -398,7 +398,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel panel-default noprint">
|
||||
<div class="panel-heading">
|
||||
<strong>Related Devices</strong>
|
||||
</div>
|
||||
@@ -416,7 +416,7 @@
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ rd.device_type.full_name }}</td>
|
||||
<td>{{ rd.device_type.display_name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
@@ -459,7 +459,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer">
|
||||
<div class="panel-footer noprint">
|
||||
{% if device_bays and perms.dcim.change_devicebay %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
@@ -493,7 +493,7 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interfaces</strong>
|
||||
<div class="pull-right">
|
||||
<div class="pull-right noprint">
|
||||
<button class="btn btn-default btn-xs toggle-ips" selected="selected">
|
||||
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
|
||||
</button>
|
||||
@@ -521,7 +521,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer">
|
||||
<div class="panel-footer noprint">
|
||||
{% if interfaces and perms.dcim.change_interface %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
@@ -581,7 +581,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer">
|
||||
<div class="panel-footer noprint">
|
||||
{% if consoleserverports and perms.dcim.change_consoleport %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
@@ -636,7 +636,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer">
|
||||
<div class="panel-footer noprint">
|
||||
{% if poweroutlets and perms.dcim.change_powerport %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
@@ -682,7 +682,8 @@
|
||||
<th>Rear Port</th>
|
||||
<th>Position</th>
|
||||
<th>Description</th>
|
||||
<th>Connected Cable</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="2">Connection</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -692,11 +693,14 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer">
|
||||
<div class="panel-footer noprint">
|
||||
{% if front_ports and perms.dcim.change_frontport %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
@@ -735,7 +739,8 @@
|
||||
<th>Type</th>
|
||||
<th>Positions</th>
|
||||
<th>Description</th>
|
||||
<th>Connected Cable</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="2">Connection</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -745,11 +750,14 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer">
|
||||
<div class="panel-footer noprint">
|
||||
{% if rear_ports and perms.dcim.change_rearport %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Model</td>
|
||||
<td>{{ device.device_type.full_name }}</td>
|
||||
<td>{{ device.device_type.display_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Serial Number</td>
|
||||
@@ -53,7 +53,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<div class="panel-footer text-right">
|
||||
<div class="panel-footer text-right noprint">
|
||||
<a href="{% url 'dcim:inventoryitem_add' device=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span> Add Inventory Item
|
||||
</a>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.dcim.add_device %}
|
||||
{% add_button 'dcim:device_add' %}
|
||||
{% import_button 'dcim:device_import' %}
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="col-md-9">
|
||||
{% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.dcim.add_devicerole %}
|
||||
{% add_button 'dcim:devicerole_add' %}
|
||||
{% import_button 'dcim:devicerole_import' %}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="row noprint">
|
||||
<div class="col-md-12">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:devicetype_list' %}">Device Types</a></li>
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.change_devicetype or perms.dcim.delete_devicetype %}
|
||||
<div class="pull-right">
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.dcim.change_devicetype %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.dcim.add_devicetype %}
|
||||
{% add_button 'dcim:devicetype_add' %}
|
||||
{% import_button 'dcim:devicetype_import' %}
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:devicetype_bulk_edit' bulk_delete_url='dcim:devicetype_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
{% endif %}
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right">
|
||||
<td class="text-right noprint">
|
||||
{% if cp.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=cp.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
{% endif %}
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right">
|
||||
<td class="text-right noprint">
|
||||
{% if csp.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=csp.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<a href="{% url 'dcim:device' pk=devicebay.installed_device.pk %}">{{ devicebay.installed_device }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ devicebay.installed_device.device_type.full_name }}</span>
|
||||
<span>{{ devicebay.installed_device.device_type.display_name }}</span>
|
||||
</td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
@@ -23,7 +23,7 @@
|
||||
<span class="text-muted">Vacant</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right">
|
||||
<td class="text-right noprint">
|
||||
{% if perms.dcim.change_devicebay %}
|
||||
{% if devicebay.installed_device %}
|
||||
<a href="{% url 'dcim:devicebay_depopulate' pk=devicebay.pk %}" class="btn btn-danger btn-xs">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<strong>{{ title }}</strong>
|
||||
</div>
|
||||
{% include 'responsive_table.html' %}
|
||||
<div class="panel-footer">
|
||||
<div class="panel-footer noprint">
|
||||
{% if table.rows %}
|
||||
{% if edit_url %}
|
||||
<button type="submit" name="_edit" formaction="{% url edit_url pk=devicetype.pk %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning">
|
||||
|
||||
@@ -23,17 +23,38 @@
|
||||
{# Description #}
|
||||
<td>{{ frontport.description|placeholder }}</td>
|
||||
|
||||
{# Cable #}
|
||||
<td>
|
||||
{% if frontport.cable %}
|
||||
{# Cable/connection #}
|
||||
{% if frontport.cable %}
|
||||
<td>
|
||||
<a href="{{ frontport.cable.get_absolute_url }}">{{ frontport.cable }}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:frontport_trace' pk=frontport.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
{% with far_end=frontport.get_cable_peer %}
|
||||
<td>
|
||||
{% if far_end.parent.provider %}
|
||||
<i class="fa fa-fw fa-globe" title="Circuit"></i>
|
||||
<a href="{{ far_end.parent.get_absolute_url }}">
|
||||
{{ far_end.parent.provider }}
|
||||
{{ far_end.parent }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ far_end.parent.get_absolute_url }}">
|
||||
{{ far_end.parent }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ far_end }}</td>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<td colspan="3">
|
||||
<span class="text-muted">Not connected</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right">
|
||||
<td class="text-right noprint">
|
||||
{% if frontport.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=frontport.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% load helpers %}
|
||||
<tr class="interface{% if not iface.enabled %} danger{% elif iface.cable.status %} success{% elif iface.cable %} info{% elif iface.is_virtual %} warning{% endif %}" id="iface_{{ iface.name }}">
|
||||
<tr class="interface{% if not iface.enabled %} danger{% elif iface.cable.status %} success{% elif iface.cable %} info{% elif iface.is_virtual %} warning{% endif %}" id="interface_{{ iface.name }}">
|
||||
|
||||
{# Checkbox #}
|
||||
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
@@ -96,7 +96,7 @@
|
||||
{{ peer_termination.connected_endpoint.device }}
|
||||
</a><br/>
|
||||
<small>via <i class="fa fa-fw fa-globe" title="Circuit"></i>
|
||||
<a href="{{ iface.connected_endpoint.circuit.get_absolure_url }}">
|
||||
<a href="{{ iface.connected_endpoint.circuit.get_absolute_url }}">
|
||||
{{ iface.connected_endpoint.circuit.provider }}
|
||||
{{ iface.connected_endpoint.circuit }}
|
||||
</a>
|
||||
@@ -134,7 +134,7 @@
|
||||
{% endif %}
|
||||
|
||||
{# Buttons #}
|
||||
<td class="text-right text-nowrap">
|
||||
<td class="text-right text-nowrap noprint">
|
||||
{% if show_graphs %}
|
||||
{% if iface.connected_endpoint %}
|
||||
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs">
|
||||
@@ -150,7 +150,7 @@
|
||||
{% if perms.dcim.change_interface %}
|
||||
{% if iface.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=iface.cable %}
|
||||
{% elif not iface.is_virtual and perms.dcim.add_cable %}
|
||||
{% elif iface.is_connectable and perms.dcim.add_cable %}
|
||||
<a href="{% url 'dcim:interface_connect' termination_a_id=iface.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-success btn-xs" title="Connect">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
@@ -231,7 +231,7 @@
|
||||
</td>
|
||||
|
||||
{# Buttons #}
|
||||
<td class="text-right text-nowrap">
|
||||
<td class="text-right text-nowrap noprint">
|
||||
{% if perms.ipam.change_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<td>{{ item.serial }}</td>
|
||||
<td>{{ item.asset_tag|default:"" }}</td>
|
||||
<td>{{ item.description }}</td>
|
||||
<td class="text-right">
|
||||
<td class="text-right noprint">
|
||||
{% if perms.dcim.change_inventoryitem %}
|
||||
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
{% endif %}
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right">
|
||||
<td class="text-right noprint">
|
||||
{% if po.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=po.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
{% endif %}
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right">
|
||||
<td class="text-right noprint">
|
||||
{% if pp.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=pp.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
{% if u.device %}
|
||||
<li class="occupied h{{ u.device.device_type.u_height }}u"{% ifequal u.device.face face_id %} style="background-color: #{{ u.device.device_role.color }}"{% endifequal %}>
|
||||
{% ifequal u.device.face face_id %}
|
||||
<a href="{% url 'dcim:device' pk=u.device.pk %}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true"
|
||||
data-content="{{ u.device.device_role }}<br />{{ u.device.device_type.full_name }} ({{ u.device.device_type.u_height }}U){% if u.device.asset_tag %}<br />{{ u.device.asset_tag }}{% endif %}{% if u.device.serial %}<br />{{ u.device.serial }}{% endif %}">
|
||||
<a href="{% url 'dcim:device' pk=u.device.pk %}" style="color: {{ u.device.device_role.color|fgcolor }}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true"
|
||||
data-content="{{ u.device.device_role }}<br />{{ u.device.device_type.display_name }} ({{ u.device.device_type.u_height }}U){% if u.device.asset_tag %}<br />{{ u.device.asset_tag }}{% endif %}{% if u.device.serial %}<br />{{ u.device.serial }}{% endif %}">
|
||||
{{ u.device }}
|
||||
{% if u.device.devicebay_count %}
|
||||
({{ u.device.get_children.count }}/{{ u.device.devicebay_count }})
|
||||
|
||||
@@ -22,17 +22,38 @@
|
||||
{# Description #}
|
||||
<td>{{ rearport.description|placeholder }}</td>
|
||||
|
||||
{# Cable #}
|
||||
<td>
|
||||
{% if rearport.cable %}
|
||||
{# Cable/connection #}
|
||||
{% if rearport.cable %}
|
||||
<td>
|
||||
<a href="{{ rearport.cable.get_absolute_url }}">{{ rearport.cable }}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:rearport_trace' pk=rearport.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
{% with far_end=rearport.get_cable_peer %}
|
||||
<td>
|
||||
{% if far_end.parent.provider %}
|
||||
<i class="fa fa-fw fa-globe" title="Circuit"></i>
|
||||
<a href="{{ far_end.parent.get_absolute_url }}">
|
||||
{{ far_end.parent.provider }}
|
||||
{{ far_end.parent }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ far_end.parent.get_absolute_url }}">
|
||||
{{ far_end.parent }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ far_end }}</td>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<td colspan="3">
|
||||
<span class="text-muted">Not connected</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right">
|
||||
<td class="text-right noprint">
|
||||
{% if rearport.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=rearport.cable %}
|
||||
{% elif perms.dcim.add_cable %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load helpers %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="row noprint">
|
||||
<div class="col-md-12">
|
||||
<ol class="breadcrumb">
|
||||
{% if interface.device %}
|
||||
@@ -15,7 +15,7 @@
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.dcim.change_interface %}
|
||||
<a href="{% if interface.device %}{% url 'dcim:interface_edit' pk=interface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=interface.pk %}{% endif %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span> Edit this interface
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user