Compare commits

...

350 Commits

Author SHA1 Message Date
Jeremy Stretch
10f8a94399 Merge pull request #9039 from netbox-community/develop
Release v3.1.11
2022-04-05 14:45:41 -04:00
jeremystretch
631de20a8d Release v3.1.11 2022-04-05 14:35:27 -04:00
jeremystretch
0f5fe746e0 Add warning for legacy ASN field on site 2022-04-05 10:22:42 -04:00
jeremystretch
a7fc8621a8 Closes #9036: Add bulk edit capability for site contact fields 2022-04-05 10:18:20 -04:00
jeremystretch
e575279738 Changelog for #8365 2022-04-04 15:58:54 -04:00
Jeremy Stretch
796f7258cc Merge pull request #9018 from stephanblanke/8365-filering-for-child-devices-by-parent-device
Closes #8365: Filtering for child devices by parent device
2022-04-04 15:18:20 -04:00
Stephan Blanke
8126087b3e Merge branch 'netbox-community:develop' into 8365-filering-for-child-devices-by-parent-device 2022-04-02 18:10:12 +02:00
Stephan Blanke
780459d2bf Closes #8365: Filtering for child devices by parent device 2022-04-02 18:08:48 +02:00
jeremystretch
99a01207bc Closes #9012: Linkify circuits count in providers list 2022-04-01 09:06:44 -04:00
Jeremy Stretch
6d6457ad18 Merge pull request #9010 from kkthxbye-code/fix-9009
Annotate rack search queryset with device count
2022-04-01 08:43:22 -04:00
jeremystretch
35f3a42e7f Remove 2022 survey announcement 2022-04-01 08:31:53 -04:00
kkthxbye
a84ae88214 Annotate rack search queryset with device count 2022-04-01 09:34:16 +02:00
jeremystretch
58e4d08bb0 Closes #8790: Include site and prefixes columns in VLAN group VLANs table 2022-03-30 15:51:12 -04:00
jeremystretch
91e8f57afb Change log & cleanup for #8163, #8866 2022-03-30 15:39:28 -04:00
Jeremy Stretch
e3d0628a06 Merge pull request #8870 from minitriga/issue_8866
APISelect JavaScript only perform fetch if Django substitutes have been replaced.
2022-03-30 15:37:14 -04:00
Jeremy Stretch
9fca9ca7ec Merge pull request #8983 from stephanblanke/8163-bridge-members-panel-in-interface-view
Closes #8163: Add bridge members panel to interface view
2022-03-30 15:27:37 -04:00
jeremystretch
2d09a40663 Closes #8601: Include group when displaying tenant assigned to cluster 2022-03-30 15:04:13 -04:00
jeremystretch
1eaf55c555 Closes #8336: Add note about referening object in custom link template 2022-03-30 14:14:49 -04:00
jeremystretch
db535e6453 Closes #8436: Update token permissions documentation 2022-03-30 14:05:27 -04:00
jeremystretch
dadec9d3cb Add instruction for checking out an older release 2022-03-30 13:03:08 -04:00
jeremystretch
ff780177d0 Clean up exception templates 2022-03-29 16:01:10 -04:00
Stephan Blanke
b7e2ea1ca5 Closes #8163: Add bridge members panel to interface view 2022-03-28 20:37:00 +02:00
jeremystretch
894665b067 Changelog for #8785, #8830 2022-03-28 10:35:49 -04:00
jeremystretch
48b7294ff1 #8785: Tweak regex validator to avoid creating no-op migration file 2022-03-28 10:35:00 -04:00
Jeremy Stretch
cde8ff282d Merge pull request #8962 from apellini/patch-2
#8830 Adding ClusterXL
2022-03-28 10:06:50 -04:00
Jeremy Stretch
0b44a595e2 Merge pull request #8945 from fmlshai/develop
Fix #8785 - allow wildcard dns records
2022-03-28 10:04:33 -04:00
Jeremy Stretch
37781bd208 Fix parentheses 2022-03-28 09:37:33 -04:00
fmlshai
e0344e9251 Update validators.py
Updated DNSValidator regex
2022-03-28 15:20:19 +02:00
jeremystretch
a1808a54a4 Fixes #8974: Use monospace font for text areas in config revision form 2022-03-28 09:13:15 -04:00
neope
1cef513f6c Adding ClusterXL as FHRPGroupProtocolChoices
Adding in choices group standard, checkpoint and cisco and ungroupped other.
2022-03-25 19:41:35 +01:00
jeremystretch
57759aa4a3 PRVB 2022-03-25 10:29:44 -04:00
Jeremy Stretch
d50148fab7 Merge pull request #8968 from netbox-community/develop
Release v3.1.10
2022-03-25 10:28:38 -04:00
jeremystretch
271c2ea3e3 Correct changelog 2022-03-25 10:16:40 -04:00
jeremystretch
20a6f6ac79 Release v3.1.10 2022-03-25 10:14:37 -04:00
jeremystretch
8924d5fa05 Correct change log 2022-03-25 10:04:48 -04:00
jeremystretch
26637d934b Change log for #8232, #8926 2022-03-25 10:02:21 -04:00
jeremystretch
dde4495e20 #8232: Cleanup & test fix 2022-03-25 09:59:58 -04:00
tranthang2404
1278429518 Closes #8232: Add color show full 100% utilization (#8816)
* Closes #8232: Add color show full 100% utilization

* change rounding

* change rounding

* fix hard code html

* format
2022-03-25 09:52:13 -04:00
Jeremy Stretch
421f5a03aa Merge pull request #8963 from minitriga/issue_8926
Closes #8926: Implement type and roll to device bay table
2022-03-25 09:12:45 -04:00
Alex Gittings
a433d5d59d Closes #8926: Implement type and roll to device bay table 2022-03-25 09:25:55 +00:00
neope
934493bf5f #8830 Adding ClusterXL
Adding ClusterCL Choice to FHRP Group
2022-03-25 08:35:57 +01:00
jeremystretch
a5820e27a6 Fixes #8905: Disable ordering by assigned tags to prevent erroneous results 2022-03-24 11:56:18 -04:00
jeremystretch
d312fe7c2b Fixes #8696: Fix help link under FHRP group assigment creation view 2022-03-24 11:14:24 -04:00
Jeremy Stretch
124fc73386 Merge pull request #8953 from 991jo/fix-8952
Fixed #8952: rack rear faces link not clickable
2022-03-24 10:53:42 -04:00
jeremystretch
c78e7c14d3 Fixes #8947: Retain filter parameters when handling an export template exception 2022-03-24 10:47:39 -04:00
jeremystretch
30a6dc2f64 Fixes #8951: Allow changing device type & platform to different manufacturer simultaneously 2022-03-24 10:34:09 -04:00
Johannes Erwerle
6ceb78fd4c Fixed #8952: rack rear faces link not clickable 2022-03-24 09:34:23 +01:00
jeremystretch
e09ab79a1a Changelog for #8924 2022-03-23 17:01:57 -04:00
Jeremy Stretch
b6587c00ce Merge pull request #8925 from kkthxbye-code/fast_script_list
Fix #8924 - Speed up rendering of the script list
2022-03-23 16:43:15 -04:00
fmlshai
f45e64c756 Fix #8785 - allow wildcard dns records
Added * to the DNSValidator regex to allow wildcard domains like *.example.com
2022-03-23 14:38:26 +01:00
kkthxbye
ae46cd33b6 - Move do_not_call_in_templates to BaseScript
- Fix the name classproperty
2022-03-23 12:18:14 +01:00
jeremystretch
41efad4056 Fixes #8919: Fix filtering of VLAN groups by site under prefix edit form 2022-03-22 11:39:26 -04:00
jeremystretch
5f89226cd7 Update testing instructions 2022-03-22 10:59:43 -04:00
jeremystretch
197dfca5b2 Fixes #8935: Correct ordering of next/previous racks to use naturalized names 2022-03-22 09:50:38 -04:00
jeremystretch
e6980626d8 Fixes #8932: Fix error when setting null value for interface rf_role via REST API 2022-03-22 09:37:57 -04:00
kkthxbye
22980cea7b Speed up rendering of the script list 2022-03-21 10:46:51 +01:00
jeremystretch
f64987d0c4 Changelog for #8813 2022-03-18 13:25:47 -04:00
PieterL75
0da04232f3 Fixes #8813 Retain search value after submitting (#8907)
* Fixes #8813 Retain search value after submitting

* remove autofocus from searchbar

Co-authored-by: Pieter Lambrecht <pieter.lambrecht@sentia.com>
2022-03-18 13:23:39 -04:00
jeremystretch
9a0bb14e76 Install tblib to fix tracebacks during parallel test runs 2022-03-18 11:47:43 -04:00
jeremystretch
900825a2af Changelog for #8457, #8575, #8645 2022-03-18 11:46:49 -04:00
Jeremy Stretch
52de50aa64 Merge pull request #8873 from minitriga/issue_8457
Closes: #8457 - Nonracked  Devices Location/Site
2022-03-18 11:32:24 -04:00
Jeremy Stretch
1541060091 Merge pull request #8742 from minitriga/issue_8645
Allow filtering on Core models for Contacts
2022-03-18 11:24:09 -04:00
Alex Gittings
50bc0caccf Fix issues with ordering and add field_groups 2022-03-18 14:58:51 +00:00
minitriga
8f5b14ec84 Update netbox/dcim/forms/filtersets.py
Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
2022-03-18 14:39:37 +00:00
minitriga
da37db1ea9 Update netbox/circuits/filtersets.py
Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
2022-03-18 14:39:22 +00:00
Alex Gittings
5abde866f1 Closes: #8457 - implement nonracked devices on locations and sites 2022-03-18 14:34:42 +00:00
Jeremy Stretch
32eed72d2b Merge pull request #8874 from minitriga/issue_8575
Closes: #8575 - Show Racks on Cable Table and Cable Page
2022-03-18 10:14:40 -04:00
jeremystretch
585b5a221d Changelog for #8553 2022-03-17 17:06:21 -04:00
Jeremy Stretch
db52fe475a Merge pull request #8573 from 991jo/asn_search_fix
Fixes 8553: Fix contacts and ASNs missing in the search dropdown and …
2022-03-17 16:43:30 -04:00
Jeremy Stretch
c5db99f383 Merge pull request #8887 from sc68cal/sc68cal-patch-1
Update GitHub link for Netaddr
2022-03-16 20:18:47 -04:00
Sean M. Collins
fd6d3205d0 Update GitHub link for Netaddr
The project was renamed/moved to a new location in GitHub and we should update the link
in case the redirect stops functioning
2022-03-16 11:45:14 -04:00
Alex Gittings
9548cf32ff add new line 2022-03-15 00:05:10 +00:00
Alex Gittings
bdbfff911b add new line 2022-03-15 00:04:22 +00:00
Alex Gittings
a143eca57d Closes: #8575 Implement rack_a and rack_b for cable table 2022-03-15 00:02:16 +00:00
Alex Gittings
3edff89a4d Fixes: #8866 - Does not perform API Select Search if a django peramiter has not been replaced 2022-03-14 17:57:33 +00:00
jeremystretch
1add5accf2 Fixes #8844: Correct VLAN ID max value 2022-03-14 09:57:51 -04:00
jeremystretch
faba6c9bdc Fixes #8850: Show airflow field on device REST API serializer when config context data is included 2022-03-14 09:54:11 -04:00
jeremystretch
4eb7cd06b4 Adjust font size for serial number under device status view 2022-03-14 09:46:40 -04:00
Alex Gittings
342f1d31be fix pycodestyle issues 2022-03-09 17:55:45 +00:00
Alex Gittings
b779bbfc9d add contacts to site table 2022-03-09 17:49:02 +00:00
Alex Gittings
ef6576bdd6 merge develop into issue 2022-03-09 17:47:58 +00:00
Alex Gittings
27dab262de add columns for each model table that has contacts 2022-03-09 17:35:25 +00:00
Alex Gittings
412c1df15a acidentally removed NestedContactAssignmentSerializer in previous commit 2022-03-09 16:48:29 +00:00
Alex Gittings
73af3ba095 remove contacts from api endpoints 2022-03-09 16:45:19 +00:00
Alex Gittings
21b7564976 Merge branch 'issue_8645' of https://github.com/minitriga/netbox into issue_8645 2022-03-09 16:36:16 +00:00
Alex Gittings
bf22b820bf Fixes #8645; Allow filtering on core models in the UI 2022-03-09 16:35:47 +00:00
thatmattlove
8cd24b1a67 Fixes #8820: correct navbar color in dark mode 2022-03-08 14:28:52 -07:00
jeremystretch
1fdc7a9163 Merge branch 'master' into develop 2022-03-07 10:49:06 -05:00
Jeremy Stretch
6807db4967 Merge pull request #8818 from netbox-community/fix-tzdata
Add tzdata dependency
2022-03-07 10:48:12 -05:00
jeremystretch
b0ea416d6d Add tzdata dependency 2022-03-07 10:38:05 -05:00
jeremystretch
c515218760 PRVB 2022-03-07 10:07:07 -05:00
Jeremy Stretch
8053ea0a22 Merge pull request #8814 from netbox-community/develop
Release v3.1.9
2022-03-07 09:59:16 -05:00
jeremystretch
a5603c9953 Release v3.1.9 2022-03-07 09:47:31 -05:00
Jeremy Stretch
bffe63a233 Merge pull request #8793 from seros1521/fix_8715
Fixes #8715: eliminates duplicates when used in many-to-many field constraints
2022-03-07 09:27:14 -05:00
jeremystretch
2cfbfe473e Fixes #8807: Correct REST API URL for FHRP group assignments 2022-03-07 09:02:47 -05:00
jeremystretch
3c78c100b5 Fixes #8808: Fix members count under FHRP group list 2022-03-07 09:00:00 -05:00
jeremystretch
2451b0a5b1 Clean up search results layout 2022-03-07 08:50:58 -05:00
Jeremy Stretch
85e9438ff7 Merge pull request #8734 from emersonfelipesp/add_pluginfooter_block
Closes #8733: Add {% block pluginfooter %} to 'base/layout.html' template
2022-03-04 16:07:54 -05:00
jeremystretch
81610ba86e Fixes #8724: Fix exception during device import with invalid device type 2022-03-04 13:45:59 -05:00
jeremystretch
6423b386d2 Closes #8758: Allow empty string substitution when renaming objects in bulk 2022-03-04 13:30:32 -05:00
jeremystretch
5c48d116eb Closes #8664: Show assigned ASNs/sites under list views 2022-03-04 13:20:17 -05:00
seros1521
90257e9dee Fixes #8715: eliminates duplicates when used in many-to-many field constraints
When using permissions that use tags, a user may receive multiple permissions
of the same type if multiple tags are assigned to the device. This causes the
RestrictedQuerySet class to generate a query similar to this:

>>> dcim.models.Device.objects.filter(Q(tags__name='tag1')|Q(tags__name='tag2'))
<ConfigContextModelQuerySet [<Device: device1>, <Device: device1>]>

This query returns the same object twice if both tags are assigned to it. This
is due to the use of the django-taggit library. The library's documentation
describes this behavior as expected and suggests using an explicit distinct()
call in queries to avoid duplicates.

However, the use of DISTINCT in queries has a global side effect -
deduplication of responses, which may or may not be acceptable behavior
(depending on further use). Since it is not known how RestrictedQuerySet will
be used in the rest of the code, it was decided to dedupe using a subquery.
2022-03-04 14:37:05 +07:00
Jeremy Stretch
3436905744 Merge pull request #8771 from jasonyates/8770-documentation
Updating mkdocs to automatically adjust theme
2022-03-02 08:38:04 -05:00
Jason Yates
e3258bcf5a Updating mkdocs to automatically adjust theme
Automatically adjusts documentation theme between default/slate based on users preference for dark mode.
2022-03-02 08:45:22 +00:00
jeremystretch
2b6e0405a5 Closes #8736: Add PC and UPC fiber end faces for LC/SC/LSH port types 2022-03-01 11:43:00 -05:00
jeremystretch
7f752d9102 Closes #8762: Link to rack elevations list from site view 2022-03-01 11:32:17 -05:00
jeremystretch
df430394b0 Closes #8766: Add SCTP to service protocols list 2022-03-01 11:07:19 -05:00
jeremystretch
1ab51ca04e Announce 2022 community survey 2022-03-01 09:29:58 -05:00
jeremystretch
cb0386779c Announce 2022 community survey 2022-03-01 09:17:24 -05:00
Emerson Pereira
28de330b50 Replace 'pluginfooter' block with 'footer' and 'footer_links' blocks
- 'footer' blocks represents the <footer> html tag
- 'footer_links' are the anchor tags inside nav
2022-02-26 01:13:11 -03:00
jeremystretch
06cb7f35f1 Update changelog 2022-02-25 13:26:02 -05:00
thatmattlove
796c5d785e Fix navbar-toggler-icon visibility in dark mode 2022-02-25 11:23:14 -07:00
thatmattlove
c88db77814 Fixes #8633: Recheck sidenav state on window resize
* Recheck sidenav state on window resize
* Remove `data-sidenav-pinned` attribute when hiding sidenav
* Remove `data-sidenav-hidden` attribute when showing sidenav
2022-02-25 11:23:14 -07:00
Jeremy Stretch
6fe0f4cd7d Merge pull request #8741 from djothi/develop
Closes #8594: Add description filter for all models with a description field
2022-02-25 13:18:40 -05:00
Jeremy Stretch
3bf90c3c38 Merge pull request #8735 from minitriga/issue_8629
Add description to tag table search function
2022-02-25 13:13:14 -05:00
Matt Love
992f3535b7 Merge pull request #8722 from stephanblanke/develop
Generalize scopeSelector js to allow easy reuse of existing layout configurations
2022-02-25 10:55:18 -07:00
Djothi Carpentier
06eacb5a5c Add description filter to WirelessLAN & WirelessLink 2022-02-25 18:15:33 +01:00
Djothi Carpentier
c0152ce52f Add description filter to VMInterface 2022-02-25 18:15:33 +01:00
Djothi Carpentier
5a60224d77 Add description filter for Token & ObjectPermission 2022-02-25 18:15:33 +01:00
Djothi Carpentier
c137fa2022 Add description filter for Tenant & ContactRole 2022-02-25 18:15:33 +01:00
Djothi Carpentier
879d01a750 Add description filter for VRF, RouteTarget, Aggregate, ASN, Role, Prefix, IPRange, VLAN & Service 2022-02-25 18:15:33 +01:00
Djothi Carpentier
6db878743c Add description filter for CustomField, ExportTemplate & Tag 2022-02-25 18:15:33 +01:00
Djothi Carpentier
08b90090f5 Add description filter for Site, RackRole, RackReservation & DeviceRole 2022-02-25 18:15:33 +01:00
Djothi Carpentier
42466d5fc4 Add description filter for ProviderNetwork, CircuitType, Circuit & CircuitTermination 2022-02-25 18:15:22 +01:00
Alex Gittings
36d6dd1ca9 Fixes #8645; Allow filtering on core models in the UI and API for contact assignments 2022-02-24 17:08:38 +00:00
Alex Gittings
4863591bc8 Fixes #8629; Add description to tag table search function 2022-02-24 10:02:21 +00:00
Emerson Pereira
c489501441 Add {% block pluginfooter %} to 'base/layout.html' template
Makes it easy to insert footer information into Netbox footer.
2022-02-24 00:38:11 -03:00
Stephan Blanke
1a7438acfd Fixed code comments 2022-02-22 23:32:34 +01:00
Stephan Blanke
b1de85a44f Fixes #8710: Show/hide form elements based on scope selection 2022-02-22 23:27:11 +01:00
jeremystretch
4913d7ee39 Fixes #8713: Restore missing "add" button on services list view 2022-02-22 09:05:31 -05:00
jeremystretch
2503a3e3ca Fixes #8717: Fix redirection after bulk edit/delete of prefixes from aggregate view 2022-02-22 09:02:31 -05:00
Johannes Erwerle
538984c6d2 Fixes 8553: Fix Provider network, ASN, and contact options missings from global search selector 2022-02-21 12:26:12 +01:00
jeremystretch
90ee689d5a Closes #8678: Validate minimum required Python version 2022-02-17 10:31:28 -05:00
jeremystretch
b343035060 Fixes #8674: Fix rendering of tabbed content in documentation 2022-02-16 16:21:32 -05:00
Daniel Sheppard
6bbf168cec Fixes #8546 - Fix import to restrict bridge, parent, lag to device interfaces 2022-02-15 09:27:55 -06:00
jeremystretch
b5e4fdc3d8 PRVB 2022-02-15 09:32:52 -05:00
Jeremy Stretch
90f91eeea4 Merge pull request #8640 from netbox-community/develop
Release v3.1.8
2022-02-15 09:31:00 -05:00
jeremystretch
ae0ae5fd4e Remove outdated installation video 2022-02-15 09:20:19 -05:00
jeremystretch
5b7486cff8 Release v3.1.8 2022-02-15 09:01:58 -05:00
jeremystretch
14240318f1 Fixes #8609: Display validation error when attempting to assign VLANs to interface with no mode during bulk edit 2022-02-15 08:39:45 -05:00
jeremystretch
18eb9ffae6 Changelog for #8391 2022-02-14 10:34:30 -05:00
Jeremy Stretch
c0a62793c4 Merge pull request #8441 from seulsale/8391-install-date-null
Fixes #8391: Install date should appear empty when exported
2022-02-14 10:32:56 -05:00
jeremystretch
dd848d754f Changelog & cleanup for #8556 2022-02-14 10:06:56 -05:00
Jeremy Stretch
f058850598 Merge pull request #8581 from mathieu-mp/8556-add-full-name-to-change-log-tables
Closes #8556: Add 'Full Name' column to Change Log table
2022-02-14 09:49:36 -05:00
Jeremy Stretch
0c7220016b Merge pull request #8621 from JonathonReinhart/nbshell-tab-complete
Enable tab completion in nbshell
2022-02-14 09:27:35 -05:00
jeremystretch
8c19124717 Fixes #8622: Correct help text of status field on VM import form 2022-02-14 08:54:36 -05:00
Mathieu PAYROL
46f4359e1f Closes #8556: Add 'Full Name' column to Change Log table 2022-02-14 09:07:57 +01:00
Sergio Saucedo
f80452c7d9 Update import order 2022-02-14 00:47:48 -06:00
Sergio Saucedo
611f1b57dd Implement custom DateTimeColumn improving null values handling 2022-02-14 00:44:50 -06:00
Jonathon Reinhart
d1b1a45725 Enable tab completion in nbshell 2022-02-13 03:00:57 -05:00
jeremystretch
6e38f7e532 Changelog for #8577 2022-02-11 16:00:01 -05:00
Jeremy Stretch
2c1e681984 Merge pull request #8584 from 991jo/assigned_contacts_fix
Fixes #8577: Contact assignment amounts not shown during contact glob…
2022-02-11 15:49:02 -05:00
jeremystretch
f11ad99983 Fixes #8611: Fix bulk editing for certain custom link, webhook, and journal entry fields 2022-02-11 15:34:41 -05:00
jeremystretch
e1ef911d40 #8564: Fix deepmerge logic to allow nullifying dicts 2022-02-11 15:22:50 -05:00
jeremystretch
a4ca585ef2 Remove references to the old mailing list 2022-02-10 14:56:21 -05:00
jeremystretch
076461a1b6 Change notes for #7150, #8398 2022-02-10 14:22:40 -05:00
Jeremy Stretch
0c7407ebb6 Merge pull request #8592 from 991jo/fix_rack_svg_url
Fixes #7150: Devices on the elevations opposite side should be clickable
2022-02-10 14:21:01 -05:00
Jeremy Stretch
f13a3fa549 Merge pull request #8589 from ITJamie/patch-1
small documentation upgrade regarding group syncs
2022-02-10 14:11:12 -05:00
Markku Leiniö
c0a65eb593 Fixes #8398: Add ConfigParam.size to enlarge specific config fields (#8565)
* Fixes #8398: Add ConfigParam.size to enlarge specific config fields

* Revert "Fixes #8398: Add ConfigParam.size to enlarge specific config fields"

This reverts commit 05e8fff458.

* Use forms.Textarea for the banner config fields
2022-02-10 12:15:02 -05:00
jeremystretch
450a7730d3 Fixes #8578: Object change log tables should honor user's configured preferences 2022-02-10 12:07:09 -05:00
jeremystretch
41ee4b642f Fixes #8604: Fix tag filter on config context list filter form 2022-02-10 11:56:41 -05:00
thatmattlove
3ee3c52e14 Improve CI performance 2022-02-09 10:26:09 -07:00
Johannes Erwerle
e76a5bfd85 Fixes #7150: Devices on the elevations opposite side should be clickable 2022-02-09 15:07:36 +01:00
Jamie (Bear) Murphy
59c89a3b9d small documentation upgrade regarding group syncs
small documentation upgrade regarding group syncs
2022-02-09 13:05:51 +00:00
Sergio Saucedo
8fc605037a Implement custom DateColumn improving null values handling 2022-02-08 01:26:26 -06:00
Johannes Erwerle
311ddf82c5 Fixes #8577: Contact assignment amounts not shown during contact global search 2022-02-08 08:03:48 +01:00
thatmattlove
9d65486c64 Fixes #8564: reset the table config to an empty object when reset is clicked 2022-02-07 16:03:09 -07:00
thatmattlove
ccce7751a0 Fixes #8564: only use columns form field in user table config form submit 2022-02-07 14:36:28 -07:00
thatmattlove
7252f0b490 Add optional selector to getSelectedOptions for more specific field selection 2022-02-07 14:34:35 -07:00
thatmattlove
094d2e586a Fix code formatting 2022-02-07 14:14:43 -07:00
thatmattlove
6c1507c88c Implement replaceAll utility function
add #8331 release notes
2022-02-07 14:04:58 -07:00
mathieu-mp
60f48326e1 #8331 Maximize browser compatibility 2022-02-07 14:04:49 -07:00
jeremystretch
5b985a924b Changelog for #8548 & misc cleanup 2022-02-07 10:37:11 -05:00
Jeremy Stretch
ee74989f74 Merge pull request #8566 from tijshuisman/develop
Fixes #8548: Virtual Chassis position zero not shown in device page
2022-02-07 10:32:44 -05:00
Tijs Huisman
e2fc7e8cd7 Fixes #8548: Virtual Chassis position zero not shown in device page 2022-02-05 15:10:03 +01:00
jeremystretch
aff55881df Changelog for #8561 2022-02-04 16:22:30 -05:00
Jeremy Stretch
4d066a075d Merge pull request #8563 from jasonyates/8561-rear-console
Fixes #8561 - Unable to connect a cable from rear ports of a patch panel to a device console port
2022-02-04 16:17:07 -05:00
Jason Yates
201077b6f6 Fixes #8561 - Unable to connect a cable from rear ports of a patch panel to a device console port 2022-02-04 20:44:43 +00:00
jeremystretch
795134c084 PRVB 2022-02-03 11:34:36 -05:00
Jeremy Stretch
4f689223b4 Merge pull request #8540 from netbox-community/develop
Release v3.1.7
2022-02-03 11:31:48 -05:00
jeremystretch
70ce7293ac Release v3.1.7 2022-02-03 10:51:41 -05:00
jeremystretch
94a0a3b568 Closes #8502: Omit [all] from social-auth-core in base requirements 2022-02-03 10:39:39 -05:00
jeremystretch
69305f0509 Fixes #8315: Fix display of NAT link for primary IPv4 address under device view 2022-02-03 10:30:26 -05:00
jeremystretch
24f48b11e6 Closes #8530: Indicate CSV or YAML as format for "all data" export 2022-02-03 10:22:38 -05:00
jeremystretch
ff3b48fa59 Fixes #8527: Fix display of changelog retention period 2022-02-03 09:48:21 -05:00
jeremystretch
db3f478598 Closes #8517: Render boolean custom fields as icons in object tables 2022-02-02 16:24:51 -05:00
jeremystretch
e20ac803f3 Fixes #8498: Fix display of selected content type filters in object list views 2022-02-02 16:08:12 -05:00
Daniel Sheppard
ea283365e7 Fixes #8425 - Fix exception when viewing change list/records with removed plugins 2022-02-02 11:18:41 -06:00
jeremystretch
8211830bd8 Fixes #8514: Correct several links to config parameters 2022-02-02 09:27:29 -05:00
jeremystretch
2a8e0f9404 Update table accessors to use dunders in path 2022-02-02 09:18:50 -05:00
jeremystretch
c15cfc26f1 Fixes #8512: Correct file permissions to allow execution of housekeeping script 2022-02-01 16:58:09 -05:00
jeremystretch
4f4e6938eb Closes #7504: Include IP range data under IPAM role views 2022-02-01 16:47:29 -05:00
jeremystretch
8545a547b9 Closes #8494: Include locations count under tenant view 2022-02-01 16:31:34 -05:00
jeremystretch
3bb7184f28 Fixes #8499: Content types REST API endpoint should not require model permission 2022-02-01 15:14:13 -05:00
Jeremy Stretch
dd71942a5e Merge pull request #8489 from 991jo/fix-unittest-docs
Fixes #8477: The commands for running the tests in the development se…
2022-01-28 14:19:57 -05:00
jeremystretch
19fdd5e151 Fixes #8465: Accept empty string values for Interface rf_channel in REST API 2022-01-28 14:03:36 -05:00
jeremystretch
f537dc632e Fixes #8456: Fix redundant display of VRF RD in prefix view 2022-01-28 13:19:23 -05:00
jeremystretch
2221006970 Closes #8462: Linkify manufacturer column in device type table 2022-01-28 13:09:57 -05:00
Johannes Erwerle
5d29c5958b Fixes #8477: The commands for running the tests in the development section are not working 2022-01-28 17:54:37 +01:00
Jeremy Stretch
64dd46c7e4 Merge pull request #8482 from 991jo/feature-asn-ui-improvement
Fixes #8476: Bring the ASN Web UI up to the standard set by other obj…
2022-01-28 09:37:54 -05:00
Johannes Erwerle
8df382d976 Fixes #8476: Bring the ASN Web UI up to the standard set by other objects 2022-01-28 11:58:29 +01:00
Sergio Saucedo
31c58409e1 Set install_date default value as empty string 2022-01-24 02:36:27 -06:00
jeremystretch
69eb6b11d0 Closes #8368: Enable controlling the order of custom script form fields with field_order 2022-01-18 16:01:40 -05:00
jeremystretch
1f2d4fd2b3 Closes #8381: Add contacts to global search function 2022-01-18 15:40:19 -05:00
jeremystretch
21468fff25 Closes #8367: Add ASNs to global search function 2022-01-18 15:36:21 -05:00
jeremystretch
4711b4d529 Correct FeatureQuery invocations 2022-01-18 15:17:05 -05:00
Daniel Sheppard
29d4859e02 Fixes #8375 - Change ASN display column from ASDOT to ASPLAIN. Add ASDOT display column. 2022-01-18 11:23:52 -06:00
jeremystretch
4b81d86311 Closes #8376: Correct example condition defitinions; call out value vs label ealuation for choice fields 2022-01-18 11:31:39 -05:00
jeremystretch
38963e7960 Fixes #8377: Fix calculation of absolute cable lengths when specified in fractional units 2022-01-18 11:09:12 -05:00
jeremystretch
1584d51433 PRVB 2022-01-17 10:16:37 -05:00
Jeremy Stretch
98571c62a6 Merge pull request #8372 from netbox-community/develop
Release v3.1.6
2022-01-17 10:15:24 -05:00
jeremystretch
69f525bfd3 Release v3.1.6 2022-01-17 09:49:16 -05:00
jeremystretch
2b31154834 Fixes #8358: Fix inconsistent styling of custom fields on filter & bulk edit forms 2022-01-14 14:23:58 -05:00
jeremystretch
b0948ea018 Changelog for #8342, #8357 2022-01-14 11:51:02 -05:00
Jeremy Stretch
a50e4e3380 Merge pull request #8352 from jasonyates/8342-created-updated
Fixes #8342
2022-01-14 11:48:52 -05:00
Jeremy Stretch
5564664b13 Merge pull request #8360 from jasonyates/8357-location-filter
Fixes #8357 - Filter view for Locations is missing tags field
2022-01-14 11:48:36 -05:00
Jason Yates
1ae5a2c808 Fixes #8357 - Filter view for Locations is missing tags field
Adding tag field to Locations filter view
2022-01-14 06:19:25 -08:00
Jason Yates
0181a25d70 Fixes #8342
created & last_updated fields are missing from some REST API calls. Added missing fields to the following API calls

/api/dcim/virtual-chassis/
/api/dcim/cables/
/api/dcim/power-panels/
/api/dcim/rack-reservations/
/api/circuits/circuit-terminations/
/api/extras/webhooks/
/api/extras/custom-fields/
/api/extras/custom-links/
/api/extras/export-templates/
/api/extras/tags/
2022-01-13 19:13:28 -08:00
jeremystretch
60ba4a9830 Changelog for #8337 2022-01-13 15:24:15 -05:00
Jeremy Stretch
3802a78c9d Merge pull request #8341 from jasonyates/8337-created-updated
Add created & last updated as available fields to all tables
2022-01-13 15:23:12 -05:00
jeremystretch
0ca6d73614 #8293: Tweak table column output & add changelog 2022-01-13 15:10:06 -05:00
Jeremy Stretch
aa77f8f0d2 Merge pull request #8329 from jasonyates/8293-asdot
Adding asdot notation to ASN views
2022-01-13 15:02:21 -05:00
Jason Yates
381796e708 Add created & last updated as available fields to all tables
Adds two fields to all relevant tables to allow the addition of Created & Last Updated columns.

All tables with a Configure Table option were updated.

Some sections reformatted to comply with E501 line length as a result of changes
2022-01-13 09:22:32 +00:00
Jason Yates
62fc7717c8 Suggested changes
* Updating asdot computation to use an fstring
* Cleaning code. Custom property now returns either the ASN with ASDOT notation or just the ASN. asn_with_asdot can now be referenced in ASNTable & objet template.
2022-01-13 04:58:51 +00:00
jeremystretch
e19451bb4f Plug WG8333 in the plugins development docs 2022-01-12 14:40:33 -05:00
Jason Yates
85f588e8c9 Updating page title to include asdot notation 2022-01-12 16:44:22 +00:00
Jason Yates
ea644868a6 Adding asdot notation to ASN views
Adds custom property to asn model to compute asdot notation if required.
Updates asn view to show asdot notation if one exists in the format xxxxx (yyy.yyy)
Adds a custom column renderer to asn table to display asdot notation if one exists
2022-01-12 14:06:22 +00:00
jeremystretch
d08accaaf1 Changelog for #8279 2022-01-11 16:27:30 -05:00
Jeremy Stretch
f49272cacb Merge pull request #8321 from jasonyates/8279-vc-rack-view
Fixes #8279 - No virtual chassis name in rack view
2022-01-11 16:25:50 -05:00
Jason Yates
be8fef0228 Fixes #8279
A device that is part of a VC that has no name should display [virtual-chassis name]:[virtual-chassis position] as opposed to [device_type] in the rack rendering.
2022-01-11 21:03:18 +00:00
jeremystretch
b584f09223 Fixes #8319: Custom URL fields should honor ALLOWED_URL_SCHEMES config parameter 2022-01-11 15:32:04 -05:00
jeremystretch
d2968c95df Fixes #8314: Prevent custom fields with default values from appearing as applied filters erroneously 2022-01-11 15:02:10 -05:00
jeremystretch
7421e5f7d7 Fixes #8317: Fix CSV import of multi-select custom field values 2022-01-11 14:52:47 -05:00
jeremystretch
0b2a43cfcc Document formal release cycle 2022-01-11 12:54:07 -05:00
jeremystretch
50309d3ab3 Reference netbox-demo-data repo in development guide 2022-01-10 15:34:27 -05:00
jeremystretch
dd0b16bff5 Fixes #8305: Fix assignment of custom field data to FHRP groups via UI 2022-01-10 15:26:01 -05:00
jeremystretch
d5443adc74 Tweak sidebar colors & remove hover delay 2022-01-10 15:13:12 -05:00
jeremystretch
9152ba72f1 Fixes #8306: Redirect user to previous page after login 2022-01-10 14:44:25 -05:00
jeremystretch
076ca46ab4 Closes #8302: Linkify role column in device & VM tables 2022-01-10 09:48:14 -05:00
jeremystretch
02519b270e Fixes #8301: Fix delete button for various object children views 2022-01-10 09:30:50 -05:00
jeremystretch
5aa7dedccb Changelog for #8246, #8285 2022-01-10 08:38:08 -05:00
Jeremy Stretch
6383dfa854 Merge pull request #8292 from jasonyates/8246-commit-rate
Fixes #8246 - Circuits list view to display formatted commit rate
2022-01-10 08:36:47 -05:00
Jeremy Stretch
5a4fb0323b Merge pull request #8286 from jasonyates/8285-cluster-count-tenant
Fixes #8285 tenant cluster count
2022-01-10 08:34:02 -05:00
jeremystretch
e84a282aa6 Revert REST API changes from #8284 2022-01-10 08:24:45 -05:00
Jason Yates
f732493473 Fixing code style E302 2022-01-08 22:24:25 +00:00
Jason Yates
f66a265fcf Fixes #8246 - Circuits list view to display formatted commit rate
Adds a custom column class to format the commit rate in the circuits table view using humanize_speed template helper. Export still exports the raw number.
2022-01-08 21:55:07 +00:00
Daniel Sheppard
f1472d218e Update changelog for #8262 and #8265 2022-01-08 00:21:38 -06:00
Daniel Sheppard
d65c05aacd Merge pull request #8269 from bluikko/cisco-stackwise-n
Merge PR from bluikko for #8265
2022-01-08 00:20:43 -06:00
Daniel Sheppard
2b28ffa2f4 Merge pull request #8284 from jasonyates/8262-tenant-cable-stat
Fixes #8262 - Add Cable stat for Tenant
2022-01-08 00:15:35 -06:00
Daniel Sheppard
10ec31df3e Fix #8287 - Correct label in export template form 2022-01-08 00:13:58 -06:00
Jason Yates
184b1055dc Fixes #8285 - Cluster count missing from tenant api output 2022-01-07 20:17:43 +00:00
Jason Yates
eaec25e6c2 Fixes #8262 - Add Cable stat for Tenant 2022-01-07 20:02:45 +00:00
bluikko
b63e29610e Add Cisco StackWise-n choices 2022-01-07 11:56:54 +07:00
jeremystretch
b0db5a8b0a PRVB 2022-01-06 09:58:50 -05:00
Jeremy Stretch
d3e2241ff7 Merge pull request #8257 from netbox-community/develop
Release v3.1.5
2022-01-06 09:52:54 -05:00
jeremystretch
e90b9f6c19 Release v3.1.5 2022-01-06 09:24:28 -05:00
jeremystretch
4c1199e009 Fixes #8255: Fix bulk editing of authentication parameters for wireless LANs and links 2022-01-06 08:54:05 -05:00
jeremystretch
65471068b6 Closes #8252: Linkify type and group columns in clusters table 2022-01-05 21:36:20 -05:00
jeremystretch
c6467a824b #8228: Always add a blank choice 2022-01-05 17:10:59 -05:00
jeremystretch
b1d1f3c6b2 Fixes #8228: Optional ChoiceVar fields should not force a selection 2022-01-05 15:46:04 -05:00
jeremystretch
574c2e2770 Closes #8244: Add length & length unit fields to cable filter form 2022-01-05 15:32:34 -05:00
jeremystretch
aec2d233c9 Changelog for #8231 2022-01-05 15:18:49 -05:00
Jeremy Stretch
39418f2bbe Merge pull request #8247 from netbox-community/8231-htmx-confirmation-dialogs
Closes #8231: Use HTMX for object deletion confirmations
2022-01-05 15:14:51 -05:00
jeremystretch
ccda73494f Center modal dialog vertically 2022-01-05 14:57:56 -05:00
jeremystretch
443b4ccc57 Initial work on #8231 2022-01-05 14:06:56 -05:00
jeremystretch
511aedd5db Omit table configuration form from rack elevations view 2022-01-05 11:39:58 -05:00
jeremystretch
2524290099 Introduce modals template block 2022-01-05 09:21:48 -05:00
jeremystretch
01e8017265 Clean up template blocks 2022-01-05 09:09:39 -05:00
jeremystretch
8338fc405f Simplify theme color palette 2022-01-04 20:51:10 -05:00
jeremystretch
0a22b3990f #7450: Clean up footer and navbar styles 2022-01-04 20:42:44 -05:00
jeremystretch
662cafe416 Form widgets & style cleanup 2022-01-04 15:01:16 -05:00
jeremystretch
ea961ba8f2 Fixes #8224: Fix KeyError exception when creating FHRP group with IP address and protocol "other" 2022-01-04 13:49:07 -05:00
jeremystretch
8c8774cd2f Fixes #8226: Honor return URL after populating a device bay 2022-01-04 13:24:15 -05:00
jeremystretch
2fe02ddb1f Add tests for IPAM object children views 2022-01-04 09:32:41 -05:00
jeremystretch
e11e8a5d64 Fixes #8213: Fix ValueError exception under prefix IP addresses view 2022-01-04 09:15:25 -05:00
jeremystretch
79bebf7c9b PRVB 2022-01-03 11:18:46 -05:00
Jeremy Stretch
8d3b660ce0 Merge pull request #8212 from netbox-community/develop
Release v3.1.4
2022-01-03 11:16:27 -05:00
jeremystretch
9de53fe070 Release v3.1.4 2022-01-03 11:00:23 -05:00
jeremystretch
ecb9fc65b7 Closes #8197: Allow filtering sites by group when connecting a cable 2022-01-03 10:41:43 -05:00
Jeremy Stretch
7b25d0379f Merge pull request #8202 from netbja/patch-1
Small syntax error
2022-01-03 10:39:56 -05:00
jeremystretch
05d4176d34 Fixes #8201: Custom integer fields should allow negative integers as minimum/maximum values 2022-01-03 10:07:19 -05:00
jeremystretch
7b0dff88ae Closes #8210: Establish netbox/local/ as a path for local resources 2022-01-03 09:45:30 -05:00
jeremystretch
1c7604e0fe Fixes #8200: Correct typo in navigation menu 2022-01-03 09:20:26 -05:00
jeremystretch
e18dc43aae Fixes #8196: Fix IndexError exception when viewing large IPv6 prefixes in UI 2022-01-03 09:17:15 -05:00
netbja
caaad684a4 Small syntax error
No double quotes after password.
2021-12-31 11:25:12 +01:00
jeremystretch
cdd51aee75 Closes #8194: Enable bulk user assignment to groups under admin UI 2021-12-30 13:19:18 -05:00
jeremystretch
51851f6c99 Refactor users.admin 2021-12-30 13:08:09 -05:00
jeremystretch
ab98aa489c Related objects should be prefetched for Prefix/IPRange child object views 2021-12-30 12:43:37 -05:00
jeremystretch
5829985ca8 Remove power utilization as default column from racks table 2021-12-30 12:02:20 -05:00
jeremystretch
2fa8e27f05 Fixes #8192: Add "add prefix" button to aggregate child prefixes view 2021-12-30 12:00:37 -05:00
jeremystretch
68f92dfd5d Fix redirection URL for prefix IP ranges view 2021-12-30 11:47:21 -05:00
jeremystretch
67aeb380e7 Fix DNS name label in IP address bulk edit form 2021-12-30 11:46:09 -05:00
jeremystretch
f7d91b7139 Extend "Adding models" documentation 2021-12-30 10:12:28 -05:00
jeremystretch
b6e157f393 Add features summary to README 2021-12-30 10:08:31 -05:00
jeremystretch
2319fce092 Add tab to cable connect view 2021-12-30 09:51:30 -05:00
jeremystretch
a5f1707662 Fixes #8191: Fix return URL when adding IP addresses to VM interfaces 2021-12-30 09:46:02 -05:00
jeremystretch
6cda55da06 Fixes #8187: Fix rendering of tags column in object tables 2021-12-30 09:41:35 -05:00
jeremystretch
c3f2fee633 PRVB 2021-12-29 12:40:04 -05:00
Jeremy Stretch
1f575a2a47 Merge pull request #8185 from netbox-community/develop
Release v3.1.3
2021-12-29 12:31:07 -05:00
jeremystretch
13c4d13157 Release NetBox v3.1.3 2021-12-29 12:10:46 -05:00
jeremystretch
43fadab3bb Closes #8034: Enable specifying custom field validators during CSV import 2021-12-29 11:57:27 -05:00
jeremystretch
82a0240d2e Closes #8182: Introduce checkmark template tag 2021-12-29 10:26:42 -05:00
jeremystretch
f2aa35d3d2 Closes #7600: Include count of available IPs on prefix view 2021-12-29 09:59:25 -05:00
jeremystretch
9c9fcaf42f Fixes #7290: Defer loading API-backed form fields 2021-12-29 09:30:43 -05:00
jeremystretch
146a51ceba Clean up API tokens view 2021-12-29 09:10:56 -05:00
jeremystretch
b0350e9e96 Remove navbar background color 2021-12-29 08:56:59 -05:00
jeremystretch
35e346c4b9 Fix circuit termination button style 2021-12-28 16:13:58 -05:00
jeremystretch
1987647cc3 Closes #8175: Display parent object when attaching an image 2021-12-28 13:06:27 -05:00
jeremystretch
542534aeba Add direct link to preferences in user menu 2021-12-23 14:41:39 -05:00
jeremystretch
908a2824ba Reduce saturation of 'info' theme color 2021-12-23 14:34:09 -05:00
Jeremy Stretch
cab9733b60 Merge pull request #8159 from netbox-community/6782-custom-link-columns
Closes #6782: Custom link columns
2021-12-22 21:13:13 -05:00
jeremystretch
99e0dcec76 Changelog & docs for #6782 2021-12-22 20:57:59 -05:00
jeremystretch
9dafb36c88 Introduce CustomLinkColumn 2021-12-22 20:56:11 -05:00
jeremystretch
3d7d19b608 Move rendering logic under CustomLink class 2021-12-22 20:25:57 -05:00
jeremystretch
d650d10cb2 #7449: Apply distinctive styling to top navbar 2021-12-22 15:32:35 -05:00
jeremystretch
7fe45018e9 #7449: Remove red color from logout link 2021-12-22 15:22:06 -05:00
jeremystretch
4c4cab87fb #7449: Don't color valid form fields 2021-12-22 15:18:24 -05:00
jeremystretch
94c7f64baf Relocate confirmation_form.html 2021-12-22 15:08:04 -05:00
jeremystretch
f369b5f588 Reorganize & clean up templatetag templates 2021-12-22 15:05:24 -05:00
jeremystretch
37065b7c50 Remove obsolete template 2021-12-22 14:47:42 -05:00
jeremystretch
0a7372460f Changelog for #7887 2021-12-22 12:48:24 -05:00
Jeremy Stretch
063abc8ef7 Merge pull request #8153 from davama/develop
Add missing HTTP_X_FORWARDED_FOR
2021-12-22 12:46:22 -05:00
jeremystretch
fb4511d099 Fixes #8140: Restore missing fields on wireless LAN & link REST API serializers 2021-12-22 10:55:06 -05:00
jeremystretch
275560698f Fixes #8139: Fix rendering of table configuration form under VM interfaces view 2021-12-21 14:10:12 -05:00
jeremystretch
d4b6fe14c3 Fixes #8138: Fix alignment of tags panel within IP address view 2021-12-21 14:04:15 -05:00
jeremystretch
f1350a1022 FIxes #7972: Standardize name of RemoteUserBackend logger 2021-12-21 13:57:12 -05:00
jeremystretch
344fb638fd Fixes #8127: Fix disassociation of interface under IP address edit view 2021-12-21 13:17:54 -05:00
thatmattlove
373cc74a33 Fixes #8134: reinitialize event listeners when HTMX swaps elements 2021-12-21 11:11:33 -07:00
jeremystretch
8e95ac42c2 Closes #8100: Add "other" choice for FHRP group protocol 2021-12-21 13:05:38 -05:00
jeremystretch
ceb941df81 Closes #8135: Append version when fetching static assets 2021-12-21 13:00:52 -05:00
jeremystretch
d275538116 Changelog & cleanup for #7246, #8097 2021-12-21 11:53:31 -05:00
Jeremy Stretch
fa38cdbc0d Merge pull request #8121 from kkthxbye-code/fix-8097
Fix #8097: Re-fix markdown table rendering
2021-12-21 11:50:24 -05:00
Jeremy Stretch
7569544b7b Merge pull request #8063 from rizlas/develop
Get_Environment from napalm should not need any decoding
2021-12-21 11:43:23 -05:00
Jeremy Stretch
853a52f3ca Merge branch 'develop' into fix-8097 2021-12-21 11:37:58 -05:00
rizlas
39a0b15df4 Update netbox/dcim/api/views.py
Test without decode_dict function

Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
2021-12-21 17:15:54 +01:00
jeremystretch
a0db10838b Fixes #8131: Restore annotation of available IPs under prefix IPs view 2021-12-21 11:09:30 -05:00
jeremystretch
f2f10dff92 Fix RearPortTemplateTable buttons 2021-12-21 10:57:46 -05:00
jeremystretch
7ba45b2887 Clean up imports 2021-12-21 10:48:10 -05:00
jeremystretch
c91eb8f406 Remove extraneous output from service edit template 2021-12-21 10:30:30 -05:00
jeremystretch
57a78b3cad Clean up device/devicetype tab views 2021-12-21 10:28:28 -05:00
jeremystretch
b755c7dab3 Add changelog for #7962 (via #8114) 2021-12-21 09:03:36 -05:00
Jeremy Stretch
9ffd791ae4 Merge pull request #8130 from netbox-community/8114-htmx-jobs
Closes #8114: Use HTMX to update report/script results
2021-12-21 09:01:15 -05:00
jeremystretch
8af12b22bb Clean up report & script templates 2021-12-21 08:43:01 -05:00
jeremystretch
17ba0a97d5 Remove jobs Javascript 2021-12-20 20:59:14 -05:00
jeremystretch
4ae2b4e0b9 Convert reports to use HTMX 2021-12-20 20:52:29 -05:00
jeremystretch
872691a138 Convert scripts to use HTMX 2021-12-20 20:45:32 -05:00
kkthxbye-code
3a54ecb522 Fix #8097: Re-fix markdown table rendering 2021-12-20 23:31:24 +01:00
jeremystretch
42b590af77 PRVB 2021-12-20 16:06:42 -05:00
rizlas
2ec64a2ea2 Get_Environment from napalm should not need any decoding 2021-12-14 10:17:00 +01:00
Dave
038d7e0fa6 Add missing HTTP_X_FORWARDED_FOR
See discussion [here](https://github.com/netbox-community/netbox/discussions/7876) for background.

From the [doc](https://netbox.readthedocs.io/en/stable/customization/custom-scripts/) i should be able to access `META.HTTP_X_FORWARDED_FOR` but i was not able to since they were not being sent downstream
2021-11-19 15:20:00 -05:00
298 changed files with 4482 additions and 3193 deletions

View File

@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.1.2
placeholder: v3.1.11
validations:
required: true
- type: dropdown

View File

@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.1.2
placeholder: v3.1.11
validations:
required: true
- type: dropdown

View File

@@ -38,14 +38,26 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install Yarn Package Manager
run: npm install -g yarn
- name: Setup Node.js with Yarn Caching
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: yarn
cache-dependency-path: netbox/project-static/yarn.lock
- name: Install Frontend Dependencies
run: yarn --cwd netbox/project-static
- name: Install dependencies & set up configuration
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pycodestyle coverage
pip install pycodestyle coverage tblib
ln -s configuration.testing.py netbox/netbox/configuration.py
yarn --cwd netbox/project-static
- name: Build documentation
run: mkdocs build
@@ -63,7 +75,7 @@ jobs:
run: scripts/verify-bundles.sh
- name: Run tests
run: coverage run --source="netbox/" netbox/manage.py test netbox/
run: coverage run --source="netbox/" netbox/manage.py test netbox/ --parallel
- name: Show coverage report
run: coverage report --skip-covered --omit *migrations*

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@ yarn-error.log*
!/netbox/project-static/docs/.info
/netbox/netbox/configuration.py
/netbox/netbox/ldap_config.py
/netbox/local/*
/netbox/reports/*
!/netbox/reports/__init__.py
/netbox/scripts/*

View File

@@ -16,13 +16,6 @@ categories for discussions:
feature request
* **Q&A** - Request help with installing or using NetBox
### Mailing List
We also have a Google Groups [mailing list](https://groups.google.com/g/netbox-discuss)
for general discussion, however we're encouraging people to use GitHub
discussions where possible, as it's much easier for newcomers to review past
discussions.
### Slack
For real-time chat, you can join the **#netbox** Slack channel on [NetDev Community](https://netdev.chat/).

View File

@@ -5,11 +5,46 @@
![Master branch build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master)
NetBox is an infrastructure resource modeling (IRM) tool designed to empower
network automation. Initially conceived by the network engineering team at
network automation, used by thousands of organizations around the world.
Initially conceived by the network engineering team at
[DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically
to address the needs of network and infrastructure engineers. It is intended to
function as a domain-specific source of truth for network operations.
Myriad infrastructure components can be modeled in NetBox, including:
* Hierarchical regions, site groups, sites, and locations
* Racks, devices, and device components
* Cables and wireless connections
* Power distribution
* Data circuits and providers
* Virtual machines and clusters
* IP prefixes, ranges, and addresses
* VRFs and route targets
* FHRP groups (VRRP, HSRP, etc.)
* AS numbers
* VLANs and scoped VLAN groups
* Organizational tenants and contacts
In addition to its extensive built-in models and functionality, NetBox can be
customized and extended through the use of:
* Custom fields
* Custom links
* Configuration contexts
* Custom model validation rules
* Reports
* Custom scripts
* Export templates
* Conditional webhooks
* Plugins
* Single sign-on (SSO) authentication
* NAPALM integration
* Detailed change logging
NetBox also features a complete REST API as well as a GraphQL API for easily
integrating with other tools and systems.
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/)
Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a
complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox).
@@ -33,7 +68,6 @@ The complete documentation for NetBox can be found at [Read the Docs](https://ne
* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - Discussion forum hosted by GitHub; ideal for Q&A and other structured discussions
* [Slack](https://netdev.chat/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out
* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being replaced by GitHub discussions
### Installation

View File

@@ -83,7 +83,7 @@ markdown-include
mkdocs-material
# Library for manipulating IP prefixes and addresses
# https://github.com/drkjam/netaddr
# https://github.com/netaddr/netaddr
netaddr
# Fork of PIL (Python Imaging Library) for image processing
@@ -98,13 +98,9 @@ psycopg2-binary
# https://github.com/yaml/pyyaml
PyYAML
# In-memory key/value store used for caching and queuing
# https://github.com/andymccurdy/redis-py
redis
# Social authentication framework
# https://github.com/python-social-auth/social-core
social-auth-core[all]
social-auth-core
# Django app for social-auth-core
# https://github.com/python-social-auth/social-app-django

0
contrib/netbox-housekeeping.sh Normal file → Executable file
View File

View File

@@ -1,5 +1,22 @@
{!models/extras/webhook.md!}
## Conditional Webhooks
A webhook may include a set of conditional logic expressed in JSON used to control whether a webhook triggers for a specific object. For example, you may wish to trigger a webhook for devices only when the `status` field of an object is "active":
```json
{
"and": [
{
"attr": "status.value",
"value": "active"
}
]
}
```
For more detail, see the reference documentation for NetBox's [conditional logic](../reference/conditions.md).
## Webhook Processing
When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks.

View File

@@ -3,7 +3,7 @@
NetBox includes a `housekeeping` management command that should be run nightly. This command handles:
* Clearing expired authentication sessions from the database
* Deleting changelog records older than the configured [retention time](../configuration/optional-settings.md#changelog_retention)
* Deleting changelog records older than the configured [retention time](../configuration/dynamic-settings.md#changelog_retention)
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.

View File

@@ -35,7 +35,7 @@ The list of groups to assign a new user account when created using remote authen
Default: `{}` (Empty dictionary)
A mapping of permissions to assign a new user account when created using remote authentication. Each key in the dictionary should be set to a dictionary of the attributes to be applied to the permission, or `None` to allow all objects. (Requires `REMOTE_AUTH_ENABLED`.)
A mapping of permissions to assign a new user account when created using remote authentication. Each key in the dictionary should be set to a dictionary of the attributes to be applied to the permission, or `None` to allow all objects. (Requires `REMOTE_AUTH_ENABLED` as True and `REMOTE_AUTH_GROUP_SYNC_ENABLED` as False.)
---
@@ -43,7 +43,7 @@ A mapping of permissions to assign a new user account when created using remote
Default: `False`
NetBox can be configured to support remote user authentication by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.)
NetBox can be configured to support remote user authentication by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.) (`REMOTE_AUTH_DEFAULT_GROUPS` will not function if `REMOTE_AUTH_ENABLED` is enabled)
---

View File

@@ -21,6 +21,7 @@
---
{!models/ipam/fhrpgroup.md!}
{!models/ipam/fhrpgroupassignment.md!}
---

View File

@@ -77,6 +77,10 @@ This is the human-friendly names of your script. If omitted, the class name will
A human-friendly description of what your script does.
### `field_order`
By default, script variables will be ordered in the form as they are defined in the script. `field_order` may be defined as an iterable of field names to determine the order in which variables are rendered. Any fields not included in this iterable be listed last.
### `commit_default`
The checkbox to commit database changes when executing a script is checked by default. Set `commit_default` to False under the script's Meta class to leave this option unchecked by default.

View File

@@ -50,7 +50,7 @@ The `fail()` method may optionally specify a field with which to associate the s
## Assigning Custom Validators
Custom validators are associated with specific NetBox models under the [CUSTOM_VALIDATORS](../configuration/optional-settings.md#custom_validators) configuration parameter. There are three manners by which custom validation rules can be defined:
Custom validators are associated with specific NetBox models under the [CUSTOM_VALIDATORS](../configuration/dynamic-settings.md#custom_validators) configuration parameter. There are three manners by which custom validation rules can be defined:
1. Plain JSON mapping (no custom logic)
2. Dotted path to a custom validator class

View File

@@ -37,23 +37,32 @@ Most models will need view classes created in `views.py` to serve the following
Add the relevant URL path for each view created in the previous step to `urls.py`.
## 6. Create the FilterSet
## 6. Add relevant forms
Depending on the type of model being added, you may need to define several types of form classes. These include:
* A base model form (for creating/editing individual objects)
* A bulk edit form
* A bulk import form (for CSV-based import)
* A filterset form (for filtering the object list view)
## 7. Create the FilterSet
Each model should have a corresponding FilterSet class defined. This is used to filter UI and API queries. Subclass the appropriate class from `netbox.filtersets` that matches the model's parent class.
## 7. Create the table class
## 8. Create the table class
Create a table class for the model in `tables.py` by subclassing `utilities.tables.BaseTable`. Under the table's `Meta` class, be sure to list both the fields and default columns.
## 8. Create the object template
## 9. Create the object template
Create the HTML template for the object view. (The other views each typically employ a generic template.) This template should extend `generic/object.html`.
## 9. Add the model to the navigation menu
## 10. Add the model to the navigation menu
Add the relevant navigation menu items in `netbox/netbox/navigation_menu.py`.
## 10. REST API components
## 11. REST API components
Create the following for each model:
@@ -62,13 +71,13 @@ Create the following for each model:
* API view in `api/views.py`
* Endpoint route in `api/urls.py`
## 11. GraphQL API components
## 12. GraphQL API components
Create a Graphene object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`.
Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention.
## 12. Add tests
## 13. Add tests
Add tests for the following:
@@ -76,7 +85,7 @@ Add tests for the following:
* API views
* Filter sets
## 13. Documentation
## 14. Documentation
Create a new documentation page for the model in `docs/models/<app_label>/<model_name>.md`. Include this file under the "features" documentation where appropriate.

View File

@@ -114,24 +114,32 @@ This ensures that your development environment is now complete and operational.
!!! info "IDE Integration"
Some IDEs, such as PyCharm, will integrate with Django's development server and allow you to run it directly within the IDE. This is strongly encouraged as it makes for a much more convenient development environment.
## Populating Demo Data
Once you have your development environment up and running, it might be helpful to populate some "dummy" data to make interacting with the UI and APIs more convenient. Check out the [netbox-demo-data](https://github.com/netbox-community/netbox-demo-data) repo on GitHub, which houses a collection of sample data that can be easily imported to any new NetBox deployment. (This sample data is used to populate the public demo instance at <https://demo.netbox.dev>.)
The demo data is provided in JSON format and loaded into an empty database using Django's `loaddata` management command. Consult the demo data repo's `README` file for complete instructions on populating the data.
## Running Tests
Prior to committing any substantial changes to the code base, be sure to run NetBox's test suite to catch any potential errors. Tests are run using the `test` management command. Remember to ensure the Python virtual environment is active before running this command.
Prior to committing any substantial changes to the code base, be sure to run NetBox's test suite to catch any potential errors. Tests are run using the `test` management command. Remember to ensure the Python virtual environment is active before running this command. Also keep in mind that these commands are executed in the `/netbox/` directory, not the root directory of the repository.
When running tests, it's advised to use the special testing configuration file that ships with NetBox. This ensures that tests are run with the same configuration parameters consistently. To override your local configuration when running tests, set the `NETBOX_CONFIGURATION` environment variable to `netbox.configuration_testing`.
```no-highlight
$ python netbox/manage.py test
$ NETBOX_CONFIGURATION=netbox.configuration_testing python manage.py test
```
In cases where you haven't made any changes to the database (which is most of the time), you can append the `--keepdb` argument to this command to reuse the test database between runs. This cuts down on the time it takes to run the test suite since the database doesn't have to be rebuilt each time. (Note that this argument will cause errors if you've modified any model fields since the previous test run.)
```no-highlight
$ python netbox/manage.py test --keepdb
$ python manage.py test --keepdb
```
You can also limit the command to running only a specific subset of tests. For example, to run only IPAM and DCIM view tests:
```no-highlight
$ python netbox/manage.py test dcim.tests.test_views ipam.tests.test_views
$ python manage.py test dcim.tests.test_views ipam.tests.test_views
```
## Submitting Pull Requests

View File

@@ -7,9 +7,8 @@ NetBox is maintained as a [GitHub project](https://github.com/netbox-community/n
There are several official forums for communication among the developers and community members:
* [GitHub issues](https://github.com/netbox-community/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in a GitHub issue.
* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
* [GitHub discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
* [#netbox on NetDev Community Slack](https://netdev.chat/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being phased out in favor of GitHub discussions.
## Governance

View File

@@ -67,4 +67,4 @@ Authorization: Token $TOKEN
## Disabling the GraphQL API
If not needed, the GraphQL API can be disabled by setting the [`GRAPHQL_ENABLED`](../configuration/optional-settings.md#graphql_enabled) configuration parameter to False and restarting NetBox.
If not needed, the GraphQL API can be disabled by setting the [`GRAPHQL_ENABLED`](../configuration/dynamic-settings.md#graphql_enabled) configuration parameter to False and restarting NetBox.

View File

@@ -50,7 +50,7 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
| Application | Django/Python |
| Database | PostgreSQL 10+ |
| Task queuing | Redis/django-rq |
| Live device access | NAPALM |
| Live device access | NAPALM (optional) |
## Supported Python Versions
@@ -58,4 +58,6 @@ NetBox supports Python 3.7, 3.8, and 3.9 environments currently. (Support for Py
## Getting Started
See the [installation guide](installation/index.md) for help getting NetBox up and running quickly.
Minor NetBox releases (e.g. v3.1) are published three times a year; in April, August, and December. These typically introduce major new features and may contain breaking API changes. Patch releases are published roughly every one to two weeks to resolve bugs and fulfill minor feature requests. These are backward-compatible with previous releases unless otherwise noted. The NetBox maintainers strongly recommend running the latest stable release whenever possible.
Please see the [official installation guide](installation/index.md) for detailed instructions on obtaining and installing NetBox.

View File

@@ -152,7 +152,7 @@ LOGGING = {
'netbox_auth_log': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'filename': '/opt/netbox/logs/django-ldap-debug.log',
'filename': '/opt/netbox/local/logs/django-ldap-debug.log',
'maxBytes': 1024 * 500,
'backupCount': 5,
},

View File

@@ -11,10 +11,6 @@ The following sections detail how to set up a new instance of NetBox:
5. [HTTP server](5-http-server.md)
6. [LDAP authentication](6-ldap.md) (optional)
The video below demonstrates the installation of NetBox v3.0 on Ubuntu 20.04 for your reference.
<iframe width="560" height="315" src="https://www.youtube.com/embed/7Fpd2-q9_28" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
## Requirements
| Dependency | Minimum Version |

View File

@@ -6,7 +6,7 @@ Prior to upgrading your NetBox instance, be sure to carefully review all [releas
## Update Dependencies to Required Versions
NetBox v3.0 and later requires the following:
NetBox v3.0 and later require the following:
| Dependency | Minimum Version |
|------------|-----------------|
@@ -67,6 +67,11 @@ sudo git checkout master
sudo git pull origin master
```
!!! info "Checking out an older release"
If you need to upgrade to an older version rather than the current stable release, you can check out any valid [git tag](https://github.com/netbox-community/netbox/tags), each of which represents a release. For example, to checkout the code for NetBox v2.11.11, do:
sudo git checkout v2.11.11
## Run the Upgrade Script
Once the new code is in place, verify that any optional Python packages required by your deployment (e.g. `napalm` or `django-auth-ldap`) are listed in `local_requirements.txt`. Then, run the upgrade script:

View File

@@ -2,7 +2,7 @@
Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a Network Monitoring System (NMS).
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the Netbox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `obj`, and custom fields through `obj.cf`.
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the NetBox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `obj`, and custom fields through `obj.cf`.
For example, you might define a link like this:
@@ -32,6 +32,10 @@ The following context data is available within the template when rendering a cus
| `user` | The current user (if authenticated) |
| `perms` | The [permissions](https://docs.djangoproject.com/en/stable/topics/auth/default/#permissions) assigned to the user |
While most of the context variables listed above will have consistent attributes, the object will be an instance of the specific object being viewed when the link is rendered. Different models have different fields and properties, so you may need to some research to determine the attributes available for use within your template for a specific object type.
Checking the REST API representation of an object is generally a convenient way to determine what attributes are available. You can also reference the NetBox source code directly for a comprehensive list.
## Conditional Rendering
Only links which render with non-empty text are included on the page. You can employ conditional Jinja2 logic to control the conditions under which a link gets rendered.
@@ -55,3 +59,7 @@ The link will only appear when viewing a device with a manufacturer name of "Cis
## Link Groups
Group names can be specified to organize links into groups. Links with the same group name will render as a dropdown menu beneath a single button bearing the name of the group.
## Table Columns
Custom links can also be included in object tables by selecting the desired links from the table configuration form. When displayed, each link will render as a hyperlink for its corresponding object. When exported (e.g. as CSV data), each link render only its URL.

View File

@@ -81,16 +81,3 @@ If no body template is specified, the request body will be populated with a JSON
}
}
```
## Conditional Webhooks
A webhook may include a set of conditional logic expressed in JSON used to control whether a webhook triggers for a specific object. For example, you may wish to trigger a webhook for devices only when the `status` field of an object is "active":
```json
{
"attr": "status",
"value": "active"
}
```
For more detail, see the reference documentation for NetBox's [conditional logic](../reference/conditions.md).

View File

@@ -8,9 +8,3 @@ A first-hop redundancy protocol (FHRP) enables multiple physical interfaces to p
* Gateway Load Balancing Protocol (GLBP)
NetBox models these redundancy groups by protocol and group ID. Each group may optionally be assigned an authentication type and key. (Note that the authentication key is stored as a plaintext value in NetBox.) Each group may be assigned or more virtual IPv4 and/or IPv6 addresses.
## FHRP Group Assignments
Member device and VM interfaces can be assigned to FHRP groups, along with a numeric priority value. For instance, three interfaces, each belonging to a different router, may each be assigned to the same FHRP group to serve a common virtual IP address. Each of these assignments would typically receive a different priority.
Interfaces are assigned to FHRP groups under the interface detail view.

View File

@@ -0,0 +1,5 @@
# FHRP Group Assignments
Member device and VM interfaces can be assigned to FHRP groups, along with a numeric priority value. For instance, three interfaces, each belonging to a different router, may each be assigned to the same FHRP group to serve a common virtual IP address. Each of these assignments would typically receive a different priority.
Interfaces are assigned to FHRP groups under the interface detail view.

View File

@@ -3,7 +3,7 @@
A token is a unique identifier mapped to a NetBox user account. Each user may have one or more tokens which he or she can use for authentication when making REST API requests. To create a token, navigate to the API tokens page under your user profile.
!!! note
The creation and modification of API tokens can be restricted per user by an administrator. If you don't see an option to create an API token, ask an administrator to grant you access.
All users can create and manage REST API tokens under the user control panel in the UI. The ability to view, add, change, or delete tokens via the REST API itself is controlled by the relevant model permissions, assigned to users and/or groups in the admin UI. These permissions should be used with great care to avoid accidentally permitting a user to create tokens for other user accounts.
Each token contains a 160-bit key represented as 40 hexadecimal characters. When creating a token, you'll typically leave the key field blank so that a random key will be automatically generated. However, NetBox allows you to specify a key in case you need to restore a previously deleted token to operation.

View File

@@ -1,5 +1,8 @@
# Plugin Development
!!! info "Help Improve the NetBox Plugins Framework!"
We're looking for volunteers to help improve NetBox's plugins framework. If you have experience developing plugins, we'd love to hear from you! You can find more information about this initiative [here](https://github.com/netbox-community/netbox/discussions/8338).
This documentation covers the development of custom plugins for NetBox. Plugins are essentially self-contained [Django apps](https://docs.djangoproject.com/en/stable/) which integrate with NetBox to provide custom functionality. Since the development of Django apps is already very well-documented, we'll only be covering the aspects that are specific to NetBox.
Plugins can do a lot, including:

View File

@@ -81,13 +81,16 @@ The following condition will evaluate as true:
```json
{
"attr": "status",
"attr": "status.value",
"value": ["planned", "staging"],
"op": "in",
"negate": true
}
```
!!! note "Evaluating static choice fields"
Pay close attention when evaluating static choice fields, such as the `status` field above. These fields typically render as a dictionary specifying both the field's raw value (`value`) and its human-friendly label (`label`). be sure to specify on which of these you want to match.
## Condition Sets
Multiple conditions can be combined into nested sets using AND or OR logic. This is done by declaring a JSON object with a single key (`and` or `or`) containing a list of condition objects and/or child condition sets.
@@ -102,7 +105,7 @@ Multiple conditions can be combined into nested sets using AND or OR logic. This
{
"and": [
{
"attr": "status",
"attr": "status.value",
"value": "active"
},
{

View File

@@ -1,6 +1,14 @@
# Release Notes
Listed below are the major features introduced in each NetBox release. For more detail on a specific release train, see its individual release notes page.
NetBox releases are numbered as major, minor, and patch releases. For example, version 3.1.0 is a minor release, and v3.1.5 is a patch release. Briefly, these can be described as follows:
* **Major** - Introduces or removes an entire API or other core functionality
* **Minor** - Implements major new features but may include breaking changes for API consumers or other integrations
* **Patch** - A maintenance release which fixes bugs and may introduce backward-compatible enhancements
Minor releases are published in April, August, and December of each calendar year. Patch releases are published as needed to address bugs and fulfill minor feature requests, typically around every one to two weeks.
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
#### [Version 3.1](./version-3.1.md) (December 2021)

View File

@@ -367,7 +367,7 @@ More information about IP ranges is available [in the documentation](../models/i
#### Custom Model Validation ([#5963](https://github.com/netbox-community/netbox/issues/5963))
This release introduces the [`CUSTOM_VALIDATORS`](../configuration/optional-settings.md#custom_validators) configuration parameter, which allows administrators to map NetBox models to custom validator classes to enforce custom validation logic. For example, the following configuration requires every site to have a name of at least ten characters and a description:
This release introduces the [`CUSTOM_VALIDATORS`](../configuration/dynamic-settings.md#custom_validators) configuration parameter, which allows administrators to map NetBox models to custom validator classes to enforce custom validation logic. For example, the following configuration requires every site to have a name of at least ten characters and a description:
```python
from extras.validators import CustomValidator

View File

@@ -1,5 +1,223 @@
# NetBox v3.1
## v3.1.11 (2022-04-05)
### Enhancements
* [#8163](https://github.com/netbox-community/netbox/issues/8163) - Show bridge interface members under interface view
* [#8365](https://github.com/netbox-community/netbox/issues/8365) - Enable filtering child devices by parent device ID
* [#8785](https://github.com/netbox-community/netbox/issues/8785) - Permit wildcard values in IP address DNS names
* [#8790](https://github.com/netbox-community/netbox/issues/8790) - Include site and prefixes columns in VLAN group VLANs table
* [#8830](https://github.com/netbox-community/netbox/issues/8830) - Add Checkpoint ClusterXL protocol for FHRP groups
* [#8974](https://github.com/netbox-community/netbox/issues/8974) - Use monospace font for text areas in config revision form
* [#9012](https://github.com/netbox-community/netbox/issues/9012) - Linkify circuits count in providers list
* [#9036](https://github.com/netbox-community/netbox/issues/9036) - Add bulk edit capability for site contact fields
### Bug Fixes
* [#8866](https://github.com/netbox-community/netbox/issues/8866) - Prevent exception when searching for a rack position with no rack specified under device edit view
* [#9009](https://github.com/netbox-community/netbox/issues/9009) - Fix device count for racks in global search results
---
## v3.1.10 (2022-03-25)
### Enhancements
* [#8232](https://github.com/netbox-community/netbox/issues/8232) - Use a different color for 100% utilization bars
* [#8457](https://github.com/netbox-community/netbox/issues/8457) - Enable adding non-racked devices from site & location views
* [#8553](https://github.com/netbox-community/netbox/issues/8553) - Add missing object types to global search form
* [#8575](https://github.com/netbox-community/netbox/issues/8575) - Add rack columns to cables list
* [#8645](https://github.com/netbox-community/netbox/issues/8645) - Enable filtering objects by assigned contacts & contact roles
* [#8926](https://github.com/netbox-community/netbox/issues/8926) - Add device type, role columns to device bay table
### Bug Fixes
* [#8696](https://github.com/netbox-community/netbox/issues/8696) - Fix help link under FHRP group assigment creation view
* [#8813](https://github.com/netbox-community/netbox/issues/8813) - Retain global search bar query after submitting
* [#8820](https://github.com/netbox-community/netbox/issues/8820) - Fix navbar background color in dark mode
* [#8850](https://github.com/netbox-community/netbox/issues/8850) - Show airflow field on device REST API serializer when config context data is included
* [#8905](https://github.com/netbox-community/netbox/issues/8905) - Disable ordering by assigned tags to prevent erroneous results
* [#8919](https://github.com/netbox-community/netbox/issues/8919) - Fix filtering of VLAN groups by site under prefix edit form
* [#8924](https://github.com/netbox-community/netbox/issues/8924) - Improve load time of custom script list
* [#8932](https://github.com/netbox-community/netbox/issues/8932) - Fix error when setting null value for interface `rf_role` via REST API
* [#8935](https://github.com/netbox-community/netbox/issues/8935) - Correct ordering of next/previous racks to use naturalized names
* [#8947](https://github.com/netbox-community/netbox/issues/8947) - Retain filter parameters when handling an export template exception
* [#8951](https://github.com/netbox-community/netbox/issues/8951) - Allow changing device type & platform to different manufacturer simultaneously
* [#8952](https://github.com/netbox-community/netbox/issues/8952) - Device images in rear rack elevations should be hyperlinked
---
## v3.1.9 (2022-03-07)
### Enhancements
* [#8594](https://github.com/netbox-community/netbox/issues/8594) - Enable filtering by exact description match for all applicable models
* [#8629](https://github.com/netbox-community/netbox/issues/8629) - Add description to tag table search function
* [#8664](https://github.com/netbox-community/netbox/issues/8664) - Show assigned ASNs/sites under list views
* [#8736](https://github.com/netbox-community/netbox/issues/8736) - Add PC and UPC fiber end faces for LC/SC/LSH port types
* [#8758](https://github.com/netbox-community/netbox/issues/8758) - Allow empty string substitution when renaming objects in bulk
* [#8762](https://github.com/netbox-community/netbox/issues/8762) - Link to rack elevations list from site view
* [#8766](https://github.com/netbox-community/netbox/issues/8766) - Add SCTP to service protocols list
### Bug Fixes
* [#8546](https://github.com/netbox-community/netbox/issues/8546) - Fix bulk import to restrict bridge, parent, and LAG to device interfaces
* [#8633](https://github.com/netbox-community/netbox/issues/8633) - Prevent navigation sidebar pin from disappearing at certain breakpoints
* [#8674](https://github.com/netbox-community/netbox/issues/8674) - Fix rendering of tabbed content in documentation
* [#8710](https://github.com/netbox-community/netbox/issues/8710) - Fix dynamic scope selection form fields when creating a VLAN group
* [#8713](https://github.com/netbox-community/netbox/issues/8713) - Restore missing "add" button on services list view
* [#8715](https://github.com/netbox-community/netbox/issues/8715) - Avoid returning multiple objects when restricting querysets using multiple tags in permissions
* [#8717](https://github.com/netbox-community/netbox/issues/8717) - Fix redirection after bulk edit/delete of prefixes from aggregate view
* [#8724](https://github.com/netbox-community/netbox/issues/8724) - Fix exception during device import with invalid device type
* [#8807](https://github.com/netbox-community/netbox/issues/8807) - Correct REST API URL for FHRP group assignments
* [#8808](https://github.com/netbox-community/netbox/issues/8808) - Fix members count under FHRP group list
---
## v3.1.8 (2022-02-15)
### Enhancements
* [#7150](https://github.com/netbox-community/netbox/issues/7150) - Linkify devices on the far side of a rack elevation
* [#8398](https://github.com/netbox-community/netbox/issues/8398) - Embiggen configuration form fields for banner message content
* [#8556](https://github.com/netbox-community/netbox/issues/8556) - Add full username column to changelog table
* [#8620](https://github.com/netbox-community/netbox/issues/8620) - Enable tab completion for `nbshell`
### Bug Fixes
* [#8331](https://github.com/netbox-community/netbox/issues/8331) - Implement `replaceAll` string utility function to improve browser compatibility
* [#8391](https://github.com/netbox-community/netbox/issues/8391) - Null date columns should return empty strings during CSV export
* [#8548](https://github.com/netbox-community/netbox/issues/8548) - Fix display of VC members when position is zero
* [#8561](https://github.com/netbox-community/netbox/issues/8561) - Include option to connect a rear port to a console port
* [#8564](https://github.com/netbox-community/netbox/issues/8564) - Fix errant table configuration key `available_columns`
* [#8577](https://github.com/netbox-community/netbox/issues/8577) - Show contact assignment counts in global search results
* [#8578](https://github.com/netbox-community/netbox/issues/8578) - Object change log tables should honor user's configured preferences
* [#8604](https://github.com/netbox-community/netbox/issues/8604) - Fix tag filter on config context list filter form
* [#8609](https://github.com/netbox-community/netbox/issues/8609) - Display validation error when attempting to assign VLANs to interface with no mode during bulk edit
* [#8611](https://github.com/netbox-community/netbox/issues/8611) - Fix bulk editing for certain custom link, webhook, and journal entry fields
---
## v3.1.7 (2022-02-03)
### Enhancements
* [#7504](https://github.com/netbox-community/netbox/issues/7504) - Include IP range data under IPAM role views
* [#8275](https://github.com/netbox-community/netbox/issues/8275) - Introduce alternative ASDOT-formatted column for ASNs
* [#8367](https://github.com/netbox-community/netbox/issues/8367) - Add ASNs to global search function
* [#8368](https://github.com/netbox-community/netbox/issues/8368) - Enable controlling the order of custom script form fields with `field_order`
* [#8381](https://github.com/netbox-community/netbox/issues/8381) - Add contacts to global search function
* [#8462](https://github.com/netbox-community/netbox/issues/8462) - Linkify manufacturer column in device type table
* [#8476](https://github.com/netbox-community/netbox/issues/8476) - Bring the ASN Web UI up to the standard set by other objects
* [#8494](https://github.com/netbox-community/netbox/issues/8494) - Include locations count under tenant view
* [#8517](https://github.com/netbox-community/netbox/issues/8517) - Render boolean custom fields as icons in object tables
* [#8530](https://github.com/netbox-community/netbox/issues/8530) - Indicate CSV or YAML as format for "all data" export
### Bug Fixes
* [#8315](https://github.com/netbox-community/netbox/issues/8315) - Fix display of NAT link for primary IPv4 address under device view
* [#8377](https://github.com/netbox-community/netbox/issues/8377) - Fix calculation of absolute cable lengths when specified in fractional units
* [#8425](https://github.com/netbox-community/netbox/issues/8425) - Fix exception when viewing change list/records with removed plugins
* [#8456](https://github.com/netbox-community/netbox/issues/8456) - Fix redundant display of VRF RD in prefix view
* [#8465](https://github.com/netbox-community/netbox/issues/8465) - Accept empty string values for Interface `rf_channel` in REST API
* [#8498](https://github.com/netbox-community/netbox/issues/8498) - Fix display of selected content type filters in object list views
* [#8499](https://github.com/netbox-community/netbox/issues/8499) - Content types REST API endpoint should not require model permission
* [#8512](https://github.com/netbox-community/netbox/issues/8512) - Correct file permissions to allow execution of housekeeping script
* [#8527](https://github.com/netbox-community/netbox/issues/8527) - Fix display of changelog retention period
---
## v3.1.6 (2022-01-17)
### Enhancements
* [#8246](https://github.com/netbox-community/netbox/issues/8246) - Show human-friendly values for commit rates in circuits table
* [#8262](https://github.com/netbox-community/netbox/issues/8262) - Add cable count to tenant stats
* [#8265](https://github.com/netbox-community/netbox/issues/8265) - Add Stackwise-n interface types
* [#8293](https://github.com/netbox-community/netbox/issues/8293) - Show 4-byte ASNs in ASDOT notation
* [#8302](https://github.com/netbox-community/netbox/issues/8302) - Linkify role column in device & VM tables
* [#8337](https://github.com/netbox-community/netbox/issues/8337) - Enable sorting object tables by created & updated times
### Bug Fixes
* [#8279](https://github.com/netbox-community/netbox/issues/8279) - Fix display of virtual chassis members in rack elevations
* [#8285](https://github.com/netbox-community/netbox/issues/8285) - Fix `cluster_count` under tenant REST API serializer
* [#8287](https://github.com/netbox-community/netbox/issues/8287) - Correct label in export template form
* [#8301](https://github.com/netbox-community/netbox/issues/8301) - Fix delete button for various object children views
* [#8305](https://github.com/netbox-community/netbox/issues/8305) - Fix assignment of custom field data to FHRP groups via UI
* [#8306](https://github.com/netbox-community/netbox/issues/8306) - Redirect user to previous page after login
* [#8314](https://github.com/netbox-community/netbox/issues/8314) - Prevent custom fields with default values from appearing as applied filters erroneously
* [#8317](https://github.com/netbox-community/netbox/issues/8317) - Fix CSV import of multi-select custom field values
* [#8319](https://github.com/netbox-community/netbox/issues/8319) - Custom URL fields should honor `ALLOWED_URL_SCHEMES` config parameter
* [#8342](https://github.com/netbox-community/netbox/issues/8342) - Restore `created` & `last_updated` fields missing from several REST API serializers
* [#8357](https://github.com/netbox-community/netbox/issues/8357) - Add missing tags field to location filter form
* [#8358](https://github.com/netbox-community/netbox/issues/8358) - Fix inconsistent styling of custom fields on filter & bulk edit forms
---
## v3.1.5 (2022-01-06)
### Enhancements
* [#8231](https://github.com/netbox-community/netbox/issues/8231) - Use in-page dialogs for confirming object deletion
* [#8244](https://github.com/netbox-community/netbox/issues/8244) - Add length & length unit fields to cable filter form
* [#8252](https://github.com/netbox-community/netbox/issues/8252) - Linkify type and group columns in clusters table
### Bug Fixes
* [#8213](https://github.com/netbox-community/netbox/issues/8213) - Fix ValueError exception under prefix IP addresses view
* [#8224](https://github.com/netbox-community/netbox/issues/8224) - Fix KeyError exception when creating FHRP group with IP address and protocol "other"
* [#8226](https://github.com/netbox-community/netbox/issues/8226) - Honor return URL after populating a device bay
* [#8228](https://github.com/netbox-community/netbox/issues/8228) - Optional ChoiceVar fields should not force a selection
* [#8255](https://github.com/netbox-community/netbox/issues/8255) - Fix bulk editing of authentication parameters for wireless LANs and links
---
## v3.1.4 (2022-01-03)
### Enhancements
* [#8192](https://github.com/netbox-community/netbox/issues/8192) - Add "add prefix" button to aggregate child prefixes view
* [#8194](https://github.com/netbox-community/netbox/issues/8194) - Enable bulk user assignment to groups under admin UI
* [#8197](https://github.com/netbox-community/netbox/issues/8197) - Allow filtering sites by group when connecting a cable
* [#8210](https://github.com/netbox-community/netbox/issues/8210) - Establish `netbox/local/` as a path for local resources
### Bug Fixes
* [#8187](https://github.com/netbox-community/netbox/issues/8187) - Fix rendering of tags column in object tables
* [#8191](https://github.com/netbox-community/netbox/issues/8191) - Fix return URL when adding IP addresses to VM interfaces
* [#8196](https://github.com/netbox-community/netbox/issues/8196) - Fix IndexError exception when viewing large IPv6 prefixes in UI
* [#8201](https://github.com/netbox-community/netbox/issues/8201) - Custom integer fields should allow negative integers as minimum/maximum values
---
## v3.1.3 (2021-12-29)
### Enhancements
* [#6782](https://github.com/netbox-community/netbox/issues/6782) - Enable the inclusion of custom links in tables
* [#7600](https://github.com/netbox-community/netbox/issues/7600) - Include count of available IPs on prefix view
* [#8034](https://github.com/netbox-community/netbox/issues/8034) - Enable specifying custom field validators during CSV import
* [#8100](https://github.com/netbox-community/netbox/issues/8100) - Add "other" choice for FHRP group protocol
* [#8175](https://github.com/netbox-community/netbox/issues/8175) - Display parent object when attaching an image
### Bug Fixes
* [#7246](https://github.com/netbox-community/netbox/issues/7246) - Don't attempt to URL-decode NAPALM response payloads
* [#7290](https://github.com/netbox-community/netbox/issues/7290) - Defer loading API-backed form fields
* [#7887](https://github.com/netbox-community/netbox/issues/7887) - Forward `HTTP_X_FORWARDED_FOR` to custom scripts
* [#7962](https://github.com/netbox-community/netbox/issues/7962) - Fix user menu under report/script result view
* [#7972](https://github.com/netbox-community/netbox/issues/7972) - Standardize name of `RemoteUserBackend` logger
* [#8097](https://github.com/netbox-community/netbox/issues/8097) - Fix styling of Markdown tables
* [#8127](https://github.com/netbox-community/netbox/issues/8127) - Fix disassociation of interface under IP address edit view
* [#8131](https://github.com/netbox-community/netbox/issues/8131) - Restore annotation of available IPs under prefix IPs view
* [#8134](https://github.com/netbox-community/netbox/issues/8134) - Fix bulk editing of objects within dynamic tables
* [#8139](https://github.com/netbox-community/netbox/issues/8139) - Fix rendering of table configuration form under VM interfaces view
* [#8140](https://github.com/netbox-community/netbox/issues/8140) - Restore missing fields on wireless LAN & link REST API serializers
---
## v3.1.2 (2021-12-20)
### Enhancements

View File

@@ -42,7 +42,7 @@ $ curl -X POST \
https://netbox/api/users/tokens/provision/ \
--data '{
"username": "hankhill",
"password: "I<3C3H8",
"password": "I<3C3H8",
}'
```

View File

@@ -8,11 +8,13 @@ theme:
icon:
repo: fontawesome/brands/github
palette:
- scheme: default
- media: "(prefers-color-scheme: light)"
scheme: default
toggle:
icon: material/lightbulb-outline
name: Switch to Dark Mode
- scheme: slate
- media: "(prefers-color-scheme: dark)"
scheme: slate
toggle:
icon: material/lightbulb
name: Switch to Light Mode
@@ -34,7 +36,8 @@ markdown_extensions:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
- pymdownx.superfences
- pymdownx.tabbed
- pymdownx.tabbed:
alternate_style: true
nav:
- Introduction: 'index.md'
- Installation:

View File

@@ -100,5 +100,5 @@ class CircuitTerminationSerializer(ValidatedModelSerializer, LinkTerminationSeri
fields = [
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type',
'_occupied',
'_occupied', 'created', 'last_updated',
]

View File

@@ -5,7 +5,7 @@ from dcim.filtersets import CableTerminationFilterSet
from dcim.models import Region, Site, SiteGroup
from extras.filters import TagFilter
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
from tenancy.filtersets import TenancyFilterSet
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
from utilities.filters import TreeNodeMultipleChoiceFilter
from .choices import *
from .models import *
@@ -19,7 +19,7 @@ __all__ = (
)
class ProviderFilterSet(PrimaryModelFilterSet):
class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -98,7 +98,7 @@ class ProviderNetworkFilterSet(PrimaryModelFilterSet):
class Meta:
model = ProviderNetwork
fields = ['id', 'name']
fields = ['id', 'name', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -115,10 +115,10 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
class Meta:
model = CircuitType
fields = ['id', 'name', 'slug']
fields = ['id', 'name', 'slug', 'description']
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -193,7 +193,7 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = Circuit
fields = ['id', 'cid', 'install_date', 'commit_rate']
fields = ['id', 'cid', 'description', 'install_date', 'commit_rate']
def search(self, queryset, name, value):
if not value.strip():
@@ -234,7 +234,7 @@ class CircuitTerminationFilterSet(ChangeLoggedModelFilterSet, CableTerminationFi
class Meta:
model = CircuitTermination
fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id']
fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description']
def search(self, queryset, name, value):
if not value.strip():

View File

@@ -5,7 +5,7 @@ from circuits.choices import CircuitStatusChoices
from circuits.models import *
from dcim.models import Region, Site, SiteGroup
from extras.forms import CustomFieldModelFilterForm
from tenancy.forms import TenancyFilterForm
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from utilities.forms import DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField
__all__ = (
@@ -16,24 +16,23 @@ __all__ = (
)
class ProviderFilterForm(CustomFieldModelFilterForm):
class ProviderFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = Provider
field_groups = [
['q', 'tag'],
['region_id', 'site_group_id', 'site_id'],
['asn'],
['contact', 'contact_role']
]
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -42,8 +41,7 @@ class ProviderFilterForm(CustomFieldModelFilterForm):
'region_id': '$region_id',
'site_group_id': '$site_group_id',
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
asn = forms.IntegerField(
required=False,
@@ -61,8 +59,7 @@ class ProviderNetworkFilterForm(CustomFieldModelFilterForm):
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider'),
fetch_trigger='open'
label=_('Provider')
)
tag = TagFilterField(model)
@@ -72,7 +69,7 @@ class CircuitTypeFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model)
class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Circuit
field_groups = [
['q', 'tag'],
@@ -80,18 +77,17 @@ class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
['type_id', 'status', 'commit_rate'],
['region_id', 'site_group_id', 'site_id'],
['tenant_group_id', 'tenant_id'],
['contact', 'contact_role']
]
type_id = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(),
required=False,
label=_('Type'),
fetch_trigger='open'
label=_('Type')
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider'),
fetch_trigger='open'
label=_('Provider')
)
provider_network_id = DynamicModelMultipleChoiceField(
queryset=ProviderNetwork.objects.all(),
@@ -99,8 +95,7 @@ class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'provider_id': '$provider_id'
},
label=_('Provider network'),
fetch_trigger='open'
label=_('Provider network')
)
status = forms.MultipleChoiceField(
choices=CircuitStatusChoices,
@@ -110,14 +105,12 @@ class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -126,8 +119,7 @@ class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
'region_id': '$region_id',
'site_group_id': '$site_group_id',
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
commit_rate = forms.IntegerField(
required=False,

View File

@@ -2,7 +2,9 @@ import django_tables2 as tables
from django_tables2.utils import Accessor
from tenancy.tables import TenantColumn
from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, MarkdownColumn, TagColumn, ToggleColumn
from utilities.tables import (
BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn,
)
from .models import *
@@ -22,21 +24,47 @@ CIRCUITTERMINATION_LINK = """
{% endif %}
"""
#
# Table columns
#
class CommitRateColumn(tables.TemplateColumn):
"""
Humanize the commit rate in the column view
"""
template_code = """
{% load helpers %}
{{ record.commit_rate|humanize_speed }}
"""
def __init__(self, *args, **kwargs):
super().__init__(template_code=self.template_code, *args, **kwargs)
def value(self, value):
return str(value) if value else None
#
# Providers
#
class ProviderTable(BaseTable):
pk = ToggleColumn()
name = tables.Column(
linkify=True
)
circuit_count = tables.Column(
circuit_count = LinkedCountColumn(
accessor=Accessor('count_circuits'),
viewname='circuits:circuit_list',
url_params={'provider_id': 'pk'},
verbose_name='Circuits'
)
comments = MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn(
url_name='circuits:provider_list'
)
@@ -45,7 +73,7 @@ class ProviderTable(BaseTable):
model = Provider
fields = (
'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count',
'comments', 'tags',
'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
@@ -69,7 +97,7 @@ class ProviderNetworkTable(BaseTable):
class Meta(BaseTable.Meta):
model = ProviderNetwork
fields = ('pk', 'id', 'name', 'provider', 'description', 'comments', 'tags')
fields = ('pk', 'id', 'name', 'provider', 'description', 'comments', 'tags', 'created', 'last_updated',)
default_columns = ('pk', 'name', 'provider', 'description')
@@ -92,7 +120,7 @@ class CircuitTypeTable(BaseTable):
class Meta(BaseTable.Meta):
model = CircuitType
fields = ('pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions')
fields = ('pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions', 'created', 'last_updated',)
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
@@ -119,7 +147,11 @@ class CircuitTable(BaseTable):
template_code=CIRCUITTERMINATION_LINK,
verbose_name='Side Z'
)
commit_rate = CommitRateColumn()
comments = MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn(
url_name='circuits:circuit_list'
)
@@ -128,7 +160,7 @@ class CircuitTable(BaseTable):
model = Circuit
fields = (
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
'commit_rate', 'description', 'comments', 'tags',
'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',

View File

@@ -108,8 +108,8 @@ class CircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
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 1', slug='circuit-type-1', description='foobar1'),
CircuitType(name='Circuit Type 2', slug='circuit-type-2', description='foobar2'),
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
))
@@ -121,6 +121,10 @@ class CircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'slug': ['circuit-type-1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Circuit.objects.all()
@@ -187,8 +191,8 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
ProviderNetwork.objects.bulk_create(provider_networks)
circuits = (
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE),
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE),
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'),
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'),
Circuit(provider=providers[0], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED),
Circuit(provider=providers[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
Circuit(provider=providers[1], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
@@ -241,6 +245,10 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
@@ -319,8 +327,8 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
Circuit.objects.bulk_create(circuits)
circuit_terminations = ((
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC'),
CircuitTermination(circuit=circuits[0], site=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF'),
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC', description='foobar1'),
CircuitTermination(circuit=circuits[0], site=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF', description='foobar2'),
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A', port_speed=2000, upstream_speed=2000, xconnect_id='GHI'),
CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
@@ -349,6 +357,10 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'xconnect_id': ['ABC', 'DEF']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_circuit_id(self):
circuits = Circuit.objects.all()[:2]
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
@@ -386,8 +398,8 @@ class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
Provider.objects.bulk_create(providers)
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
ProviderNetwork(name='Provider Network 1', provider=providers[0], description='foobar1'),
ProviderNetwork(name='Provider Network 2', provider=providers[1], description='foobar2'),
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
)
ProviderNetwork.objects.bulk_create(provider_networks)
@@ -396,6 +408,10 @@ class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'name': ['Provider Network 1', 'Provider Network 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_provider(self):
providers = Provider.objects.all()[:2]
params = {'provider_id': [providers[0].pk, providers[1].pk]}

View File

@@ -219,7 +219,7 @@ class RackReservationSerializer(PrimaryModelSerializer):
class Meta:
model = RackReservation
fields = [
'id', 'url', 'display', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags',
'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description', 'tags',
'custom_fields',
]
@@ -497,9 +497,9 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
class Meta(DeviceSerializer.Meta):
fields = [
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data',
'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments',
'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
@@ -619,9 +619,9 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
parent = NestedInterfaceSerializer(required=False, allow_null=True)
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
lag = NestedInterfaceSerializer(required=False, allow_null=True)
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_null=True)
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False)
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True)
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
tagged_vlans = SerializedPKRelatedField(
queryset=VLAN.objects.all(),
@@ -762,7 +762,7 @@ class CableSerializer(PrimaryModelSerializer):
fields = [
'id', 'url', 'display', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type',
'termination_b_id', 'termination_b', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit',
'tags', 'custom_fields',
'tags', 'custom_fields', 'created', 'last_updated',
]
def _get_termination(self, obj, side):
@@ -856,7 +856,10 @@ class VirtualChassisSerializer(PrimaryModelSerializer):
class Meta:
model = VirtualChassis
fields = ['id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count']
fields = [
'id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count',
'created', 'last_updated',
]
#
@@ -875,7 +878,10 @@ class PowerPanelSerializer(PrimaryModelSerializer):
class Meta:
model = PowerPanel
fields = ['id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count']
fields = [
'id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count',
'created', 'last_updated',
]
class PowerFeedSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):

View File

@@ -15,14 +15,14 @@ from circuits.models import Circuit
from dcim import filtersets
from dcim.models import *
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
from ipam.models import Prefix, VLAN, ASN
from ipam.models import Prefix, VLAN
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.exceptions import ServiceUnavailable
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.views import ModelViewSet
from netbox.config import get_config
from utilities.api import get_serializer_for_model
from utilities.utils import count_related, decode_dict
from utilities.utils import count_related
from virtualization.models import VirtualMachine
from . import serializers
from .exceptions import MissingFilterException
@@ -501,7 +501,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
response[method] = {'error': 'Only get_* NAPALM methods are supported'}
continue
try:
response[method] = decode_dict(getattr(d, method)())
response[method] = getattr(d, method)()
except NotImplementedError:
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
except Exception as e:

View File

@@ -816,6 +816,10 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_STACKWISE_PLUS = 'cisco-stackwise-plus'
TYPE_FLEXSTACK = 'cisco-flexstack'
TYPE_FLEXSTACK_PLUS = 'cisco-flexstack-plus'
TYPE_STACKWISE80 = 'cisco-stackwise-80'
TYPE_STACKWISE160 = 'cisco-stackwise-160'
TYPE_STACKWISE320 = 'cisco-stackwise-320'
TYPE_STACKWISE480 = 'cisco-stackwise-480'
TYPE_JUNIPER_VCP = 'juniper-vcp'
TYPE_SUMMITSTACK = 'extreme-summitstack'
TYPE_SUMMITSTACK128 = 'extreme-summitstack-128'
@@ -950,6 +954,10 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'),
(TYPE_FLEXSTACK, 'Cisco FlexStack'),
(TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'),
(TYPE_STACKWISE80, 'Cisco StackWise-80'),
(TYPE_STACKWISE160, 'Cisco StackWise-160'),
(TYPE_STACKWISE320, 'Cisco StackWise-320'),
(TYPE_STACKWISE480, 'Cisco StackWise-480'),
(TYPE_JUNIPER_VCP, 'Juniper VCP'),
(TYPE_SUMMITSTACK, 'Extreme SummitStack'),
(TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'),
@@ -1005,13 +1013,19 @@ class PortTypeChoices(ChoiceSet):
TYPE_MRJ21 = 'mrj21'
TYPE_ST = 'st'
TYPE_SC = 'sc'
TYPE_SC_PC = 'sc-pc'
TYPE_SC_UPC = 'sc-upc'
TYPE_SC_APC = 'sc-apc'
TYPE_FC = 'fc'
TYPE_LC = 'lc'
TYPE_LC_PC = 'lc-pc'
TYPE_LC_UPC = 'lc-upc'
TYPE_LC_APC = 'lc-apc'
TYPE_MTRJ = 'mtrj'
TYPE_MPO = 'mpo'
TYPE_LSH = 'lsh'
TYPE_LSH_PC = 'lsh-pc'
TYPE_LSH_UPC = 'lsh-upc'
TYPE_LSH_APC = 'lsh-apc'
TYPE_SPLICE = 'splice'
TYPE_CS = 'cs'
@@ -1051,12 +1065,18 @@ class PortTypeChoices(ChoiceSet):
(
(TYPE_FC, 'FC'),
(TYPE_LC, 'LC'),
(TYPE_LC_PC, 'LC/PC'),
(TYPE_LC_UPC, 'LC/UPC'),
(TYPE_LC_APC, 'LC/APC'),
(TYPE_LSH, 'LSH'),
(TYPE_LSH_PC, 'LSH/PC'),
(TYPE_LSH_UPC, 'LSH/UPC'),
(TYPE_LSH_APC, 'LSH/APC'),
(TYPE_MPO, 'MPO'),
(TYPE_MTRJ, 'MTRJ'),
(TYPE_SC, 'SC'),
(TYPE_SC_PC, 'SC/PC'),
(TYPE_SC_UPC, 'SC/UPC'),
(TYPE_SC_APC, 'SC/APC'),
(TYPE_ST, 'ST'),
(TYPE_CS, 'CS'),

View File

@@ -7,8 +7,8 @@ from ipam.models import ASN
from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
)
from tenancy.filtersets import TenancyFilterSet
from tenancy.models import Tenant
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from tenancy.models import *
from utilities.choices import ColorChoices
from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
@@ -62,7 +62,7 @@ __all__ = (
)
class RegionFilterSet(OrganizationalModelFilterSet):
class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(),
label='Parent region (ID)',
@@ -80,7 +80,7 @@ class RegionFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description']
class SiteGroupFilterSet(OrganizationalModelFilterSet):
class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
label='Parent site group (ID)',
@@ -98,7 +98,7 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description']
class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -142,7 +142,7 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
model = Site
fields = [
'id', 'name', 'slug', 'facility', 'asn', 'latitude', 'longitude', 'contact_name', 'contact_phone',
'contact_email',
'contact_email', 'description'
]
def search(self, queryset, name, value):
@@ -167,7 +167,7 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
return queryset.filter(qs_filter)
class LocationFilterSet(TenancyFilterSet, OrganizationalModelFilterSet):
class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region',
@@ -237,10 +237,10 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
class Meta:
model = RackRole
fields = ['id', 'name', 'slug', 'color']
fields = ['id', 'name', 'slug', 'color', 'description']
class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -385,7 +385,7 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = RackReservation
fields = ['id', 'created']
fields = ['id', 'created', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -398,7 +398,7 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
)
class ManufacturerFilterSet(OrganizationalModelFilterSet):
class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
tag = TagFilter()
class Meta:
@@ -586,7 +586,7 @@ class DeviceRoleFilterSet(OrganizationalModelFilterSet):
class Meta:
model = DeviceRole
fields = ['id', 'name', 'slug', 'color', 'vm_role']
fields = ['id', 'name', 'slug', 'color', 'vm_role', 'description']
class PlatformFilterSet(OrganizationalModelFilterSet):
@@ -608,7 +608,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet):
class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -639,6 +639,11 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContex
to_field_name='slug',
label='Role (slug)',
)
parent_device_id = django_filters.ModelMultipleChoiceFilter(
field_name='parent_bay__device',
queryset=Device.objects.all(),
label='Parent Device (ID)',
)
platform_id = django_filters.ModelMultipleChoiceFilter(
queryset=Platform.objects.all(),
label='Platform (ID)',
@@ -1289,7 +1294,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
return queryset
class PowerPanelFilterSet(PrimaryModelFilterSet):
class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',

View File

@@ -122,6 +122,18 @@ class SiteBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
label=_('ASNs'),
required=False
)
contact_name = forms.CharField(
max_length=50,
required=False
)
contact_phone = forms.CharField(
max_length=20,
required=False
)
contact_email = forms.EmailField(
required=False,
label='Contact E-mail'
)
description = forms.CharField(
max_length=100,
required=False
@@ -134,7 +146,8 @@ class SiteBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
class Meta:
nullable_fields = [
'region', 'group', 'tenant', 'asn', 'asns', 'description', 'time_zone',
'region', 'group', 'tenant', 'asn', 'asns', 'contact_name', 'contact_phone', 'contact_email', 'description',
'time_zone',
]
@@ -1043,8 +1056,14 @@ class InterfaceBulkEditForm(
def clean(self):
super().clean()
if not self.cleaned_data['mode']:
if self.cleaned_data['untagged_vlan']:
raise forms.ValidationError({'untagged_vlan': "Interface mode must be specified to assign VLANs"})
elif self.cleaned_data['tagged_vlans']:
raise forms.ValidationError({'tagged_vlans': "Interface mode must be specified to assign VLANs"})
# Untagged interfaces cannot be assigned tagged VLANs
if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']:
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']:
raise forms.ValidationError({
'mode': "An access interface cannot have tagged VLANs assigned."
})

View File

@@ -605,6 +605,19 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
'rf_channel_width', 'tx_power',
)
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
if data:
# Limit interface choices for parent, bridge and lag to device only
params = {}
if data.get('device'):
params[f"device__{self.fields['device'].to_field_name}"] = data.get('device')
if params:
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
def clean_enabled(self):
# Make sure enabled is True when it's not included in the uploaded data
if 'enabled' not in self.data:

View File

@@ -27,7 +27,7 @@ class ConnectCableToDeviceForm(TenancyForm, CustomFieldModelForm):
label='Region',
required=False
)
termination_b_site_group = DynamicModelChoiceField(
termination_b_sitegroup = DynamicModelChoiceField(
queryset=SiteGroup.objects.all(),
label='Site group',
required=False
@@ -38,7 +38,7 @@ class ConnectCableToDeviceForm(TenancyForm, CustomFieldModelForm):
required=False,
query_params={
'region_id': '$termination_b_region',
'group_id': '$termination_b_site_group',
'group_id': '$termination_b_sitegroup',
}
)
termination_b_location = DynamicModelChoiceField(
@@ -78,9 +78,9 @@ class ConnectCableToDeviceForm(TenancyForm, CustomFieldModelForm):
class Meta:
model = Cable
fields = [
'termination_b_region', 'termination_b_site', 'termination_b_rack', 'termination_b_device',
'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit',
'tags',
'termination_b_region', 'termination_b_sitegroup', 'termination_b_site', 'termination_b_rack',
'termination_b_device', 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color',
'length', 'length_unit', 'tags',
]
widgets = {
'status': StaticSelect,
@@ -182,7 +182,7 @@ class ConnectCableToCircuitTerminationForm(TenancyForm, CustomFieldModelForm):
label='Region',
required=False
)
termination_b_site_group = DynamicModelChoiceField(
termination_b_sitegroup = DynamicModelChoiceField(
queryset=SiteGroup.objects.all(),
label='Site group',
required=False
@@ -193,7 +193,7 @@ class ConnectCableToCircuitTerminationForm(TenancyForm, CustomFieldModelForm):
required=False,
query_params={
'region_id': '$termination_b_region',
'group_id': '$termination_b_site_group',
'group_id': '$termination_b_sitegroup',
}
)
termination_b_circuit = DynamicModelChoiceField(
@@ -219,9 +219,9 @@ class ConnectCableToCircuitTerminationForm(TenancyForm, CustomFieldModelForm):
class Meta(ConnectCableToDeviceForm.Meta):
fields = [
'termination_b_provider', 'termination_b_region', 'termination_b_site', 'termination_b_circuit',
'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit',
'tags',
'termination_b_provider', 'termination_b_region', 'termination_b_sitegroup', 'termination_b_site',
'termination_b_circuit', 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color',
'length', 'length_unit', 'tags',
]
def clean_termination_b_id(self):
@@ -235,7 +235,7 @@ class ConnectCableToPowerFeedForm(TenancyForm, CustomFieldModelForm):
label='Region',
required=False
)
termination_b_site_group = DynamicModelChoiceField(
termination_b_sitegroup = DynamicModelChoiceField(
queryset=SiteGroup.objects.all(),
label='Site group',
required=False
@@ -246,7 +246,7 @@ class ConnectCableToPowerFeedForm(TenancyForm, CustomFieldModelForm):
required=False,
query_params={
'region_id': '$termination_b_region',
'group_id': '$termination_b_site_group',
'group_id': '$termination_b_sitegroup',
}
)
termination_b_location = DynamicModelChoiceField(
@@ -281,8 +281,9 @@ class ConnectCableToPowerFeedForm(TenancyForm, CustomFieldModelForm):
class Meta(ConnectCableToDeviceForm.Meta):
fields = [
'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'tenant_group',
'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
'termination_b_region', 'termination_b_sitegroup', 'termination_b_site', 'termination_b_location',
'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label',
'color', 'length', 'length_unit', 'tags',
]
def clean_termination_b_id(self):

View File

@@ -5,9 +5,10 @@ from django.utils.translation import gettext as _
from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from tenancy.models import *
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
from ipam.models import ASN
from tenancy.forms import TenancyFilterForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import (
APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect,
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
@@ -57,14 +58,12 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -73,8 +72,7 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm):
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
@@ -82,14 +80,12 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm):
query_params={
'site_id': '$site_id',
},
label=_('Location'),
fetch_trigger='open'
label=_('Location')
)
virtual_chassis_id = DynamicModelMultipleChoiceField(
queryset=VirtualChassis.objects.all(),
required=False,
label=_('Virtual Chassis'),
fetch_trigger='open'
label=_('Virtual Chassis')
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
@@ -99,40 +95,48 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm):
'location_id': '$location_id',
'virtual_chassis_id': '$virtual_chassis_id'
},
label=_('Device'),
fetch_trigger='open'
label=_('Device')
)
class RegionFilterForm(CustomFieldModelFilterForm):
class RegionFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = Region
field_groups = [
['q', 'tag'],
['parent_id'],
['contact', 'contact_role'],
]
parent_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Parent region'),
fetch_trigger='open'
label=_('Parent region')
)
tag = TagFilterField(model)
class SiteGroupFilterForm(CustomFieldModelFilterForm):
class SiteGroupFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = SiteGroup
field_groups = [
['q', 'tag'],
['parent_id'],
['contact', 'contact_role'],
]
parent_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Parent group'),
fetch_trigger='open'
label=_('Parent group')
)
tag = TagFilterField(model)
class SiteFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Site
field_groups = [
['q', 'tag'],
['status', 'region_id', 'group_id'],
['tenant_group_id', 'tenant_id'],
['asn_id']
['asn_id'],
['contact', 'contact_role'],
]
status = forms.MultipleChoiceField(
choices=SiteStatusChoices,
@@ -142,42 +146,38 @@ class SiteFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
asn_id = DynamicModelMultipleChoiceField(
queryset=ASN.objects.all(),
required=False,
label=_('ASNs'),
fetch_trigger='open'
label=_('ASNs')
)
tag = TagFilterField(model)
class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Location
field_groups = [
['q'],
['q', 'tag'],
['region_id', 'site_group_id', 'site_id', 'parent_id'],
['tenant_group_id', 'tenant_id'],
['contact', 'contact_role'],
]
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -186,8 +186,7 @@ class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
parent_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
@@ -196,8 +195,7 @@ class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
'region_id': '$region_id',
'site_id': '$site_id',
},
label=_('Parent'),
fetch_trigger='open'
label=_('Parent')
)
tag = TagFilterField(model)
@@ -207,7 +205,7 @@ class RackRoleFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model)
class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Rack
field_groups = [
['q', 'tag'],
@@ -215,12 +213,12 @@ class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
['status', 'role_id'],
['type', 'width', 'serial', 'asset_tag'],
['tenant_group_id', 'tenant_id'],
['contact', 'contact_role']
]
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -228,8 +226,7 @@ class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
@@ -238,8 +235,7 @@ class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'site_id': '$site_id'
},
label=_('Location'),
fetch_trigger='open'
label=_('Location')
)
status = forms.MultipleChoiceField(
choices=RackStatusChoices,
@@ -260,8 +256,7 @@ class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
queryset=RackRole.objects.all(),
required=False,
null_option='None',
label=_('Role'),
fetch_trigger='open'
label=_('Role')
)
serial = forms.CharField(
required=False
@@ -280,8 +275,7 @@ class RackElevationFilterForm(RackFilterForm):
query_params={
'site_id': '$site_id',
'location_id': '$location_id',
},
fetch_trigger='open'
}
)
@@ -296,8 +290,7 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -305,15 +298,13 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.prefetch_related('site'),
required=False,
label=_('Location'),
null_option='None',
fetch_trigger='open'
null_option='None'
)
user_id = DynamicModelMultipleChoiceField(
queryset=User.objects.all(),
@@ -321,14 +312,17 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
label=_('User'),
widget=APISelectMultiple(
api_url='/api/users/users/',
),
fetch_trigger='open'
)
)
tag = TagFilterField(model)
class ManufacturerFilterForm(CustomFieldModelFilterForm):
class ManufacturerFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = Manufacturer
field_groups = [
['q', 'tag'],
['contact', 'contact_role'],
]
tag = TagFilterField(model)
@@ -342,8 +336,7 @@ class DeviceTypeFilterForm(CustomFieldModelFilterForm):
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label=_('Manufacturer'),
fetch_trigger='open'
label=_('Manufacturer')
)
subdevice_role = forms.MultipleChoiceField(
choices=add_blank_choice(SubdeviceRoleChoices),
@@ -410,13 +403,12 @@ class PlatformFilterForm(CustomFieldModelFilterForm):
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label=_('Manufacturer'),
fetch_trigger='open'
label=_('Manufacturer')
)
tag = TagFilterField(model)
class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm):
class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Device
field_groups = [
['q', 'tag'],
@@ -428,18 +420,17 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi
'has_primary_ip', 'virtual_chassis_member', 'console_ports', 'console_server_ports', 'power_ports',
'power_outlets', 'interfaces', 'pass_through_ports', 'local_context_data',
],
['contact', 'contact_role'],
]
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -448,8 +439,7 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
@@ -458,8 +448,7 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi
query_params={
'site_id': '$site_id'
},
label=_('Location'),
fetch_trigger='open'
label=_('Location')
)
rack_id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
@@ -469,20 +458,17 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi
'site_id': '$site_id',
'location_id': '$location_id',
},
label=_('Rack'),
fetch_trigger='open'
label=_('Rack')
)
role_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
label=_('Role'),
fetch_trigger='open'
label=_('Role')
)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label=_('Manufacturer'),
fetch_trigger='open'
label=_('Manufacturer')
)
device_type_id = DynamicModelMultipleChoiceField(
queryset=DeviceType.objects.all(),
@@ -490,15 +476,13 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi
query_params={
'manufacturer_id': '$manufacturer_id'
},
label=_('Model'),
fetch_trigger='open'
label=_('Model')
)
platform_id = DynamicModelMultipleChoiceField(
queryset=Platform.objects.all(),
required=False,
null_option='None',
label=_('Platform'),
fetch_trigger='open'
label=_('Platform')
)
status = forms.MultipleChoiceField(
choices=DeviceStatusChoices,
@@ -589,14 +573,12 @@ class VirtualChassisFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -605,8 +587,7 @@ class VirtualChassisFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
tag = TagFilterField(model)
@@ -616,14 +597,13 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
field_groups = [
['q', 'tag'],
['site_id', 'rack_id', 'device_id'],
['type', 'status', 'color'],
['type', 'status', 'color', 'length', 'length_unit'],
['tenant_group_id', 'tenant_id'],
]
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -631,8 +611,7 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
rack_id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
@@ -641,8 +620,17 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
null_option='None',
query_params={
'site_id': '$site_id'
}
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
query_params={
'site_id': '$site_id',
'tenant_id': '$tenant_id',
'rack_id': '$rack_id',
},
fetch_trigger='open'
label=_('Device')
)
type = forms.MultipleChoiceField(
choices=add_blank_choice(CableTypeChoices),
@@ -657,37 +645,32 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
color = ColorField(
required=False
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
query_params={
'site_id': '$site_id',
'tenant_id': '$tenant_id',
'rack_id': '$rack_id',
},
label=_('Device'),
fetch_trigger='open'
length = forms.IntegerField(
required=False
)
length_unit = forms.ChoiceField(
choices=add_blank_choice(CableLengthUnitChoices),
required=False
)
tag = TagFilterField(model)
class PowerPanelFilterForm(CustomFieldModelFilterForm):
class PowerPanelFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = PowerPanel
field_groups = (
('q', 'tag'),
('region_id', 'site_group_id', 'site_id', 'location_id')
('region_id', 'site_group_id', 'site_id', 'location_id'),
('contact', 'contact_role')
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -696,8 +679,7 @@ class PowerPanelFilterForm(CustomFieldModelFilterForm):
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
@@ -706,8 +688,7 @@ class PowerPanelFilterForm(CustomFieldModelFilterForm):
query_params={
'site_id': '$site_id'
},
label=_('Location'),
fetch_trigger='open'
label=_('Location')
)
tag = TagFilterField(model)
@@ -723,14 +704,12 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -738,8 +717,7 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
power_panel_id = DynamicModelMultipleChoiceField(
queryset=PowerPanel.objects.all(),
@@ -748,8 +726,7 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm):
query_params={
'site_id': '$site_id'
},
label=_('Power panel'),
fetch_trigger='open'
label=_('Power panel')
)
rack_id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
@@ -758,8 +735,7 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm):
query_params={
'site_id': '$site_id'
},
label=_('Rack'),
fetch_trigger='open'
label=_('Rack')
)
status = forms.MultipleChoiceField(
choices=PowerFeedStatusChoices,
@@ -990,8 +966,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label=_('Manufacturer'),
fetch_trigger='open'
label=_('Manufacturer')
)
serial = forms.CharField(
required=False
@@ -1016,8 +991,7 @@ class ConsoleConnectionFilterForm(FilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -1025,8 +999,7 @@ class ConsoleConnectionFilterForm(FilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
@@ -1034,8 +1007,7 @@ class ConsoleConnectionFilterForm(FilterForm):
query_params={
'site_id': '$site_id'
},
label=_('Device'),
fetch_trigger='open'
label=_('Device')
)
@@ -1043,8 +1015,7 @@ class PowerConnectionFilterForm(FilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -1052,8 +1023,7 @@ class PowerConnectionFilterForm(FilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
@@ -1061,8 +1031,7 @@ class PowerConnectionFilterForm(FilterForm):
query_params={
'site_id': '$site_id'
},
label=_('Device'),
fetch_trigger='open'
label=_('Device')
)
@@ -1070,8 +1039,7 @@ class InterfaceConnectionFilterForm(FilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -1079,8 +1047,7 @@ class InterfaceConnectionFilterForm(FilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
@@ -1088,6 +1055,5 @@ class InterfaceConnectionFilterForm(FilterForm):
query_params={
'site_id': '$site_id'
},
label=_('Device'),
fetch_trigger='open'
label=_('Device')
)

View File

@@ -301,16 +301,14 @@ class RackReservationForm(TenancyForm, CustomFieldModelForm):
required=False,
initial_params={
'sites': '$site'
},
fetch_trigger='open'
}
)
site_group = DynamicModelChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
initial_params={
'sites': '$site'
},
fetch_trigger='open'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
@@ -318,24 +316,21 @@ class RackReservationForm(TenancyForm, CustomFieldModelForm):
query_params={
'region_id': '$region',
'group_id': '$site_group',
},
fetch_trigger='open'
}
)
location = DynamicModelChoiceField(
queryset=Location.objects.all(),
required=False,
query_params={
'site_id': '$site'
},
fetch_trigger='open'
}
)
rack = DynamicModelChoiceField(
queryset=Rack.objects.all(),
query_params={
'site_id': '$site',
'location_id': '$location',
},
fetch_trigger='open'
}
)
units = NumericArrayField(
base_field=forms.IntegerField(),
@@ -349,8 +344,7 @@ class RackReservationForm(TenancyForm, CustomFieldModelForm):
)
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False,
fetch_trigger='open'
required=False
)
class Meta:
@@ -611,11 +605,6 @@ class DeviceForm(TenancyForm, CustomFieldModelForm):
# can be flipped from one face to another.
self.fields['position'].widget.add_query_param('exclude', self.instance.pk)
# Limit platform by manufacturer
self.fields['platform'].queryset = Platform.objects.filter(
Q(manufacturer__isnull=True) | Q(manufacturer=self.instance.device_type.manufacturer)
)
# Disable rack assignment if this is a child device installed in a parent device
if self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
self.fields['site'].disabled = True

View File

@@ -0,0 +1,31 @@
from django.db import migrations
from utilities.utils import to_meters
def recalculate_abs_length(apps, schema_editor):
"""
Recalculate absolute lengths for all cables with a length and length unit defined. Fixes
incorrectly calculated values as reported under bug #8377.
"""
Cable = apps.get_model('dcim', 'Cable')
cables = Cable.objects.filter(length__isnull=False).exclude(length_unit='')
for cable in cables:
cable._abs_length = to_meters(cable.length, cable.length_unit)
Cable.objects.bulk_update(cables, ['_abs_length'], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('dcim', '0143_remove_primary_for_related_name'),
]
operations = [
migrations.RunPython(
code=recalculate_abs_length,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -5,42 +5,3 @@ from .devices import *
from .power import *
from .racks import *
from .sites import *
__all__ = (
'BaseInterface',
'Cable',
'CablePath',
'LinkTermination',
'ConsolePort',
'ConsolePortTemplate',
'ConsoleServerPort',
'ConsoleServerPortTemplate',
'Device',
'DeviceBay',
'DeviceBayTemplate',
'DeviceRole',
'DeviceType',
'FrontPort',
'FrontPortTemplate',
'Interface',
'InterfaceTemplate',
'InventoryItem',
'Location',
'Manufacturer',
'Platform',
'PowerFeed',
'PowerOutlet',
'PowerOutletTemplate',
'PowerPanel',
'PowerPort',
'PowerPortTemplate',
'Rack',
'RackReservation',
'RackRole',
'RearPort',
'RearPortTemplate',
'Region',
'Site',
'SiteGroup',
'VirtualChassis',
)

View File

@@ -762,6 +762,10 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
def is_lag(self):
return self.type == InterfaceTypeChoices.TYPE_LAG
@property
def is_bridge(self):
return self.type == InterfaceTypeChoices.TYPE_BRIDGE
@property
def link(self):
return self.cable or self.wireless_link

View File

@@ -670,10 +670,11 @@ class Device(PrimaryModel, ConfigContextModel):
})
# Prevent 0U devices from being assigned to a specific position
if self.position and self.device_type.u_height == 0:
raise ValidationError({
'position': f"A U0 device type ({self.device_type}) cannot be assigned to a rack position."
})
if hasattr(self, 'device_type'):
if self.position and self.device_type.u_height == 0:
raise ValidationError({
'position': f"A U0 device type ({self.device_type}) cannot be assigned to a rack position."
})
if self.rack:
@@ -738,8 +739,8 @@ class Device(PrimaryModel, ConfigContextModel):
if hasattr(self, 'device_type') and self.platform:
if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer:
raise ValidationError({
'platform': "The assigned platform is limited to {} device types, but this device's type belongs "
"to {}.".format(self.platform.manufacturer, self.device_type.manufacturer)
'platform': f"The assigned platform is limited to {self.platform.manufacturer} device types, but "
f"this device's type belongs to {self.device_type.manufacturer}."
})
# A Device can only be assigned to a Cluster in the same Site (or no Site)

View File

@@ -412,7 +412,7 @@ class Rack(PrimaryModel):
available_units.remove(u)
occupied_unit_count = self.u_height - len(available_units)
percentage = int(float(occupied_unit_count) / self.u_height * 100)
percentage = float(occupied_unit_count) / self.u_height * 100
return percentage

View File

@@ -19,7 +19,12 @@ __all__ = (
def get_device_name(device):
return device.name or str(device.device_type)
if device.virtual_chassis:
return f'{device.virtual_chassis.name}:{device.vc_position}'
elif device.name:
return device.name
else:
return str(device.device_type)
class RackElevationSVG:
@@ -121,10 +126,16 @@ class RackElevationSVG:
link.add(drawing.text(str(name), insert=text, fill='white', class_='device-image-label'))
def _draw_device_rear(self, drawing, device, start, end, text):
rect = drawing.rect(start, end, class_="slot blocked")
rect.set_desc(self._get_device_description(device))
drawing.add(rect)
drawing.add(drawing.text(get_device_name(device), insert=text))
link = drawing.add(
drawing.a(
href='{}{}'.format(self.base_url, reverse('dcim:device', kwargs={'pk': device.pk})),
target='_top',
fill='black'
)
)
link.set_desc(self._get_device_description(device))
link.add(drawing.rect(start, end, class_="slot blocked"))
link.add(drawing.text(get_device_name(device), insert=text))
# Embed rear device type image if one exists
if self.include_images and device.device_type.rear_image:
@@ -135,10 +146,10 @@ class RackElevationSVG:
class_='device-image'
)
image.fit(scale='slice')
drawing.add(image)
drawing.add(drawing.text(get_device_name(device), insert=text, stroke='black',
stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label'))
drawing.add(drawing.text(get_device_name(device), insert=text, fill='white', class_='device-image-label'))
link.add(image)
link.add(drawing.text(get_device_name(device), insert=text, stroke='black',
stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label'))
link.add(drawing.text(get_device_name(device), insert=text, fill='white', class_='device-image-label'))
@staticmethod
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):

View File

@@ -23,6 +23,12 @@ class CableTable(BaseTable):
orderable=False,
verbose_name='Side A'
)
rack_a = tables.Column(
accessor=Accessor('termination_a__device__rack'),
orderable=False,
linkify=True,
verbose_name='Rack A'
)
termination_a = tables.Column(
accessor=Accessor('termination_a'),
orderable=False,
@@ -35,6 +41,12 @@ class CableTable(BaseTable):
orderable=False,
verbose_name='Side B'
)
rack_b = tables.Column(
accessor=Accessor('termination_b__device__rack'),
orderable=False,
linkify=True,
verbose_name='Rack B'
)
termination_b = tables.Column(
accessor=Accessor('termination_b'),
orderable=False,
@@ -45,7 +57,7 @@ class CableTable(BaseTable):
tenant = TenantColumn()
length = TemplateColumn(
template_code=CABLE_LENGTH,
order_by='_abs_length'
order_by=('_abs_length', 'length_unit')
)
color = ColorColumn()
tags = TagColumn(
@@ -55,8 +67,8 @@ class CableTable(BaseTable):
class Meta(BaseTable.Meta):
model = Cable
fields = (
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
'status', 'type', 'tenant', 'color', 'length', 'tags',
'pk', 'id', 'label', 'termination_a_parent', 'rack_a', 'termination_a', 'termination_b_parent', 'rack_b', 'termination_b',
'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',

View File

@@ -97,7 +97,7 @@ class DeviceRoleTable(BaseTable):
model = DeviceRole
fields = (
'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags',
'actions',
'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions')
@@ -130,7 +130,7 @@ class PlatformTable(BaseTable):
model = Platform
fields = (
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
'description', 'tags', 'actions',
'description', 'tags', 'actions', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions',
@@ -194,6 +194,9 @@ class DeviceTable(BaseTable):
vc_priority = tables.Column(
verbose_name='VC Priority'
)
contacts = tables.ManyToManyColumn(
linkify_item=True
)
comments = MarkdownColumn()
tags = TagColumn(
url_name='dcim:device_list'
@@ -204,7 +207,8 @@ class DeviceTable(BaseTable):
fields = (
'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags',
'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
@@ -260,7 +264,7 @@ class CableTerminationTable(BaseTable):
linkify=True
)
cable_color = ColorColumn(
accessor='cable.color',
accessor='cable__color',
orderable=False,
verbose_name='Cable Color'
)
@@ -275,7 +279,7 @@ class CableTerminationTable(BaseTable):
class PathEndpointTable(CableTerminationTable):
connection = TemplateColumn(
accessor='_path.last_node',
accessor='_path__last_node',
template_code=LINKTERMINATION,
verbose_name='Connection',
orderable=False
@@ -297,7 +301,7 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
model = ConsolePort
fields = (
'pk', 'id', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'link_peer', 'connection', 'tags',
'link_peer', 'connection', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
@@ -341,7 +345,7 @@ class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable):
model = ConsoleServerPort
fields = (
'pk', 'id', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable',
'cable_color', 'link_peer', 'connection', 'tags',
'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
@@ -386,7 +390,7 @@ class PowerPortTable(DeviceComponentTable, PathEndpointTable):
model = PowerPort
fields = (
'pk', 'id', 'name', 'device', 'label', 'type', 'description', 'mark_connected', 'maximum_draw',
'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'tags',
'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
@@ -437,7 +441,7 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
model = PowerOutlet
fields = (
'pk', 'id', 'name', 'device', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected',
'cable', 'cable_color', 'link_peer', 'connection', 'tags',
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description')
@@ -515,7 +519,7 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
'pk', 'id', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn',
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description',
'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection',
'tags', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans',
'tags', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
@@ -586,7 +590,7 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
model = FrontPort
fields = (
'pk', 'id', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
@@ -637,7 +641,7 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):
model = RearPort
fields = (
'pk', 'id', 'name', 'device', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable',
'cable_color', 'link_peer', 'tags',
'cable_color', 'link_peer', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description')
@@ -676,6 +680,15 @@ class DeviceBayTable(DeviceComponentTable):
'args': [Accessor('device_id')],
}
)
device_role = ColoredLabelColumn(
accessor=Accessor('installed_device__device_role'),
verbose_name='Role'
)
device_type = tables.Column(
accessor=Accessor('installed_device__device_type'),
linkify=True,
verbose_name='Type'
)
status = tables.TemplateColumn(
template_code=DEVICEBAY_STATUS,
order_by=Accessor('installed_device__status')
@@ -689,7 +702,11 @@ class DeviceBayTable(DeviceComponentTable):
class Meta(DeviceComponentTable.Meta):
model = DeviceBay
fields = ('pk', 'id', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags')
fields = (
'pk', 'id', 'name', 'device', 'label', 'status', 'device_role', 'device_type', 'installed_device', 'description', 'tags',
'created', 'last_updated',
)
default_columns = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description')
@@ -736,7 +753,7 @@ class InventoryItemTable(DeviceComponentTable):
model = InventoryItem
fields = (
'pk', 'id', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
'discovered', 'tags',
'discovered', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
@@ -788,5 +805,5 @@ class VirtualChassisTable(BaseTable):
class Meta(BaseTable.Meta):
model = VirtualChassis
fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags')
fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags', 'created', 'last_updated',)
default_columns = ('pk', 'name', 'domain', 'master', 'member_count')

View File

@@ -41,6 +41,9 @@ class ManufacturerTable(BaseTable):
verbose_name='Platforms'
)
slug = tables.Column()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn(
url_name='dcim:manufacturer_list'
)
@@ -50,7 +53,7 @@ class ManufacturerTable(BaseTable):
model = Manufacturer
fields = (
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
'actions',
'contacts', 'actions', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',
@@ -67,6 +70,9 @@ class DeviceTypeTable(BaseTable):
linkify=True,
verbose_name='Device Type'
)
manufacturer = tables.Column(
linkify=True
)
is_full_depth = BooleanColumn(
verbose_name='Full Depth'
)
@@ -84,7 +90,7 @@ class DeviceTypeTable(BaseTable):
model = DeviceType
fields = (
'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
'airflow', 'comments', 'instance_count', 'tags',
'airflow', 'comments', 'instance_count', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
@@ -111,8 +117,7 @@ class ComponentTemplateTable(BaseTable):
class ConsolePortTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn(
model=ConsolePortTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_consoleports'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
@@ -124,8 +129,7 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
class ConsoleServerPortTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn(
model=ConsoleServerPortTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_consoleserverports'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
@@ -137,8 +141,7 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
class PowerPortTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn(
model=PowerPortTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_powerports'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
@@ -150,8 +153,7 @@ class PowerPortTemplateTable(ComponentTemplateTable):
class PowerOutletTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn(
model=PowerOutletTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_poweroutlets'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
@@ -166,8 +168,7 @@ class InterfaceTemplateTable(ComponentTemplateTable):
)
actions = ButtonsColumn(
model=InterfaceTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_interfaces'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
@@ -183,8 +184,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
color = ColorColumn()
actions = ButtonsColumn(
model=FrontPortTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_frontports'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
@@ -197,8 +197,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
color = ColorColumn()
actions = ButtonsColumn(
model=RearPortTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_rearports'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
@@ -210,8 +209,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
class DeviceBayTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn(
model=DeviceBayTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_devicebays'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):

View File

@@ -27,13 +27,16 @@ class PowerPanelTable(BaseTable):
url_params={'power_panel_id': 'pk'},
verbose_name='Feeds'
)
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn(
url_name='dcim:powerpanel_list'
)
class Meta(BaseTable.Meta):
model = PowerPanel
fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'tags')
fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated',)
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
@@ -72,7 +75,7 @@ class PowerFeedTable(CableTerminationTable):
fields = (
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'available_power',
'comments', 'tags',
'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',

View File

@@ -31,7 +31,10 @@ class RackRoleTable(BaseTable):
class Meta(BaseTable.Meta):
model = RackRole
fields = ('pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions')
fields = (
'pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions',
'created', 'last_updated',
)
default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions')
@@ -72,6 +75,9 @@ class RackTable(BaseTable):
orderable=False,
verbose_name='Power'
)
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn(
url_name='dcim:rack_list'
)
@@ -87,12 +93,13 @@ class RackTable(BaseTable):
class Meta(BaseTable.Meta):
model = Rack
fields = (
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'tags',
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag',
'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization',
'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
'get_utilization', 'get_power_utilization',
'get_utilization',
)
@@ -127,7 +134,7 @@ class RackReservationTable(BaseTable):
model = RackReservation
fields = (
'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags',
'actions',
'actions', 'created', 'last_updated',
)
default_columns = (
'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description', 'actions',

View File

@@ -29,6 +29,9 @@ class RegionTable(BaseTable):
url_params={'region_id': 'pk'},
verbose_name='Sites'
)
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn(
url_name='dcim:region_list'
)
@@ -36,7 +39,7 @@ class RegionTable(BaseTable):
class Meta(BaseTable.Meta):
model = Region
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'contacts', 'tags', 'actions', 'created', 'last_updated')
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
@@ -54,6 +57,9 @@ class SiteGroupTable(BaseTable):
url_params={'group_id': 'pk'},
verbose_name='Sites'
)
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn(
url_name='dcim:sitegroup_list'
)
@@ -61,7 +67,7 @@ class SiteGroupTable(BaseTable):
class Meta(BaseTable.Meta):
model = SiteGroup
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'contacts', 'tags', 'actions', 'created', 'last_updated')
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
@@ -82,12 +88,19 @@ class SiteTable(BaseTable):
linkify=True
)
asn_count = LinkedCountColumn(
accessor=tables.A('asns.count'),
accessor=tables.A('asns__count'),
viewname='ipam:asn_list',
url_params={'site_id': 'pk'},
verbose_name='ASN Count'
)
asns = tables.ManyToManyColumn(
linkify_item=True,
verbose_name='ASNs'
)
tenant = TenantColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
comments = MarkdownColumn()
tags = TagColumn(
url_name='dcim:site_list'
@@ -96,9 +109,9 @@ class SiteTable(BaseTable):
class Meta(BaseTable.Meta):
model = Site
fields = (
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone',
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
'contact_phone', 'contact_email', 'comments', 'tags',
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count',
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
'contact_phone', 'contact_email', 'contacts', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')
@@ -126,6 +139,9 @@ class LocationTable(BaseTable):
url_params={'location_id': 'pk'},
verbose_name='Devices'
)
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn(
url_name='dcim:location_list'
)
@@ -137,7 +153,7 @@ class LocationTable(BaseTable):
class Meta(BaseTable.Meta):
model = Location
fields = (
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags',
'actions',
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'contacts',
'tags', 'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions')

View File

@@ -9,7 +9,8 @@ LINKTERMINATION = """
"""
CABLE_LENGTH = """
{% if record.length %}{{ record.length }} {{ record.get_length_unit_display }}{% endif %}
{% load helpers %}
{% if record.length %}{{ record.length|simplify_decimal }} {{ record.length_unit }}{% endif %}
"""
CABLE_TERMINATION_PARENT = """
@@ -297,6 +298,8 @@ REARPORT_BUTTONS = """
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='interface' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Interface</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='console-server-port' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Console Server Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='console-port' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Console Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Front Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Rear Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='circuit-termination' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Circuit Termination</a></li>

View File

@@ -9,6 +9,7 @@ from dcim.models import *
from ipam.models import ASN, RIR, VLAN
from utilities.testing import APITestCase, APIViewTestCases
from virtualization.models import Cluster, ClusterType
from wireless.choices import WirelessChannelChoices
from wireless.models import WirelessLAN
@@ -1239,10 +1240,8 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
'name': 'Interface 4',
'type': '1000base-t',
'mode': InterfaceModeChoices.MODE_TAGGED,
'tx_power': 10,
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
'untagged_vlan': vlans[2].pk,
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
},
{
'device': device.pk,
@@ -1250,10 +1249,8 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
'type': '1000base-t',
'mode': InterfaceModeChoices.MODE_TAGGED,
'bridge': interfaces[0].pk,
'tx_power': 10,
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
'untagged_vlan': vlans[2].pk,
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
},
{
'device': device.pk,
@@ -1261,10 +1258,24 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
'type': 'virtual',
'mode': InterfaceModeChoices.MODE_TAGGED,
'parent': interfaces[1].pk,
'tx_power': 10,
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
'untagged_vlan': vlans[2].pk,
},
{
'device': device.pk,
'name': 'Interface 7',
'type': InterfaceTypeChoices.TYPE_80211A,
'tx_power': 10,
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
'rf_channel': WirelessChannelChoices.CHANNEL_5G_32,
},
{
'device': device.pk,
'name': 'Interface 8',
'type': InterfaceTypeChoices.TYPE_80211A,
'tx_power': 10,
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
'rf_channel': "",
},
]

View File

@@ -151,8 +151,8 @@ class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
ASN.objects.bulk_create(asns)
sites = (
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0], tenant=tenants[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com'),
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com'),
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0], tenant=tenants[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com', description='foobar1'),
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com', description='foobar2'),
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2], tenant=tenants[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30, contact_name='Contact 3', contact_phone='123-555-0003', contact_email='contact3@example.com'),
)
Site.objects.bulk_create(sites)
@@ -201,6 +201,10 @@ class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'contact_email': ['contact1@example.com', 'contact2@example.com']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_status(self):
params = {'status': [SiteStatusChoices.STATUS_ACTIVE, SiteStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -329,8 +333,8 @@ class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
rack_roles = (
RackRole(name='Rack Role 1', slug='rack-role-1', color='ff0000'),
RackRole(name='Rack Role 2', slug='rack-role-2', color='00ff00'),
RackRole(name='Rack Role 1', slug='rack-role-1', color='ff0000', description='foobar1'),
RackRole(name='Rack Role 2', slug='rack-role-2', color='00ff00', description='foobar2'),
RackRole(name='Rack Role 3', slug='rack-role-3', color='0000ff'),
)
RackRole.objects.bulk_create(rack_roles)
@@ -347,6 +351,10 @@ class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'color': ['ff0000', '00ff00']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Rack.objects.all()
@@ -570,8 +578,8 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants)
reservations = (
RackReservation(rack=racks[0], units=[1, 2, 3], user=users[0], tenant=tenants[0]),
RackReservation(rack=racks[1], units=[4, 5, 6], user=users[1], tenant=tenants[1]),
RackReservation(rack=racks[0], units=[1, 2, 3], user=users[0], tenant=tenants[0], description='foobar1'),
RackReservation(rack=racks[1], units=[4, 5, 6], user=users[1], tenant=tenants[1], description='foobar2'),
RackReservation(rack=racks[2], units=[7, 8, 9], user=users[2], tenant=tenants[2]),
)
RackReservation.objects.bulk_create(reservations)
@@ -604,6 +612,10 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant_group(self):
tenant_groups = TenantGroup.objects.all()[:2]
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
@@ -1088,8 +1100,8 @@ class DeviceRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
device_roles = (
DeviceRole(name='Device Role 1', slug='device-role-1', color='ff0000', vm_role=True),
DeviceRole(name='Device Role 2', slug='device-role-2', color='00ff00', vm_role=True),
DeviceRole(name='Device Role 1', slug='device-role-1', color='ff0000', vm_role=True, description='foobar1'),
DeviceRole(name='Device Role 2', slug='device-role-2', color='00ff00', vm_role=True, description='foobar2'),
DeviceRole(name='Device Role 3', slug='device-role-3', color='0000ff', vm_role=False),
)
DeviceRole.objects.bulk_create(device_roles)
@@ -1112,6 +1124,10 @@ class DeviceRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'vm_role': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Platform.objects.all()

View File

@@ -27,13 +27,7 @@ from virtualization.models import VirtualMachine
from . import filtersets, forms, tables
from .choices import DeviceFaceChoices
from .constants import NONCONNECTABLE_IFACE_TYPES
from .models import (
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
InventoryItem, Manufacturer, PathEndpoint, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel,
PowerPort, PowerPortTemplate, Rack, Location, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
SiteGroup, VirtualChassis,
)
from .models import *
class DeviceComponentsView(generic.ObjectChildrenView):
@@ -51,10 +45,21 @@ class DeviceComponentsView(generic.ObjectChildrenView):
class DeviceTypeComponentsView(DeviceComponentsView):
queryset = DeviceType.objects.all()
template_name = 'dcim/devicetype/component_templates.html'
viewname = None # Used for return_url resolution
def get_children(self, request, parent):
return self.child_model.objects.restrict(request.user, 'view').filter(device_type=parent)
def get_extra_context(self, request, instance):
if self.viewname:
return_url = reverse(self.viewname, kwargs={'pk': instance.pk})
else:
return_url = instance.get_absolute_url()
return {
'active_tab': f"{self.child_model._meta.verbose_name_plural.replace(' ', '-')}",
'return_url': return_url,
}
class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
"""
@@ -323,6 +328,11 @@ class SiteView(generic.ObjectView):
'device_count',
cumulative=True
).restrict(request.user, 'view').filter(site=instance)
nonracked_devices = Device.objects.filter(
site=instance,
position__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer')
asns = ASN.objects.restrict(request.user, 'view').filter(sites=instance)
asn_count = asns.count()
@@ -333,6 +343,7 @@ class SiteView(generic.ObjectView):
'stats': stats,
'locations': locations,
'asns': asns,
'nonracked_devices': nonracked_devices,
}
@@ -410,11 +421,17 @@ class LocationView(generic.ObjectView):
).filter(pk__in=location_ids).exclude(pk=instance.pk)
child_locations_table = tables.LocationTable(child_locations)
paginate_table(child_locations_table, request)
nonracked_devices = Device.objects.filter(
location=instance,
position__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer')
return {
'rack_count': rack_count,
'device_count': device_count,
'child_locations_table': child_locations_table,
'nonracked_devices': nonracked_devices,
}
@@ -592,8 +609,8 @@ class RackView(generic.ObjectView):
peer_racks = peer_racks.filter(location=instance.location)
else:
peer_racks = peer_racks.filter(location__isnull=True)
next_rack = peer_racks.filter(name__gt=instance.name).order_by('name').first()
prev_rack = peer_racks.filter(name__lt=instance.name).order_by('-name').first()
next_rack = peer_racks.filter(_name__gt=instance._name).first()
prev_rack = peer_racks.filter(_name__lt=instance._name).reverse().first()
reservations = RackReservation.objects.restrict(request.user, 'view').filter(rack=instance)
power_feeds = PowerFeed.objects.restrict(request.user, 'view').filter(rack=instance).prefetch_related(
@@ -798,48 +815,56 @@ class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
child_model = ConsolePortTemplate
table = tables.ConsolePortTemplateTable
filterset = filtersets.ConsolePortTemplateFilterSet
viewname = 'dcim:devicetype_consoleports'
class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView):
child_model = ConsoleServerPortTemplate
table = tables.ConsoleServerPortTemplateTable
filterset = filtersets.ConsoleServerPortTemplateFilterSet
viewname = 'dcim:devicetype_consoleserverports'
class DeviceTypePowerPortsView(DeviceTypeComponentsView):
child_model = PowerPortTemplate
table = tables.PowerPortTemplateTable
filterset = filtersets.PowerPortTemplateFilterSet
viewname = 'dcim:devicetype_powerports'
class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
child_model = PowerOutletTemplate
table = tables.PowerOutletTemplateTable
filterset = filtersets.PowerOutletTemplateFilterSet
viewname = 'dcim:devicetype_poweroutlets'
class DeviceTypeInterfacesView(DeviceTypeComponentsView):
child_model = InterfaceTemplate
table = tables.InterfaceTemplateTable
filterset = filtersets.InterfaceTemplateFilterSet
viewname = 'dcim:devicetype_interfaces'
class DeviceTypeFrontPortsView(DeviceTypeComponentsView):
child_model = FrontPortTemplate
table = tables.FrontPortTemplateTable
filterset = filtersets.FrontPortTemplateFilterSet
viewname = 'dcim:devicetype_frontports'
class DeviceTypeRearPortsView(DeviceTypeComponentsView):
child_model = RearPortTemplate
table = tables.RearPortTemplateTable
filterset = filtersets.RearPortTemplateFilterSet
viewname = 'dcim:devicetype_rearports'
class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
child_model = DeviceBayTemplate
table = tables.DeviceBayTemplateTable
filterset = filtersets.DeviceBayTemplateFilterSet
viewname = 'dcim:devicetype_devicebays'
class DeviceTypeEditView(generic.ObjectEditView):
@@ -1751,6 +1776,14 @@ class InterfaceView(generic.ObjectView):
orderable=False
)
# Get bridge interfaces
bridge_interfaces = Interface.objects.restrict(request.user, 'view').filter(bridge=instance)
bridge_interfaces_tables = tables.InterfaceTable(
bridge_interfaces,
exclude=('device', 'parent'),
orderable=False
)
# Get child interfaces
child_interfaces = Interface.objects.restrict(request.user, 'view').filter(parent=instance)
child_interfaces_tables = tables.InterfaceTable(
@@ -1775,6 +1808,7 @@ class InterfaceView(generic.ObjectView):
return {
'ipaddress_table': ipaddress_table,
'bridge_interfaces_table': bridge_interfaces_tables,
'child_interfaces_table': child_interfaces_tables,
'vlan_table': vlan_table,
}
@@ -2022,8 +2056,9 @@ class DeviceBayPopulateView(generic.ObjectEditView):
device_bay.installed_device = form.cleaned_data['installed_device']
device_bay.save()
messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay))
return_url = self.get_return_url(request)
return redirect('dcim:device', pk=device_bay.device.pk)
return redirect(return_url)
return render(request, 'dcim/devicebay_populate.html', {
'device_bay': device_bay,

View File

@@ -23,15 +23,18 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
}),
('Banners', {
'fields': ('BANNER_LOGIN', 'BANNER_TOP', 'BANNER_BOTTOM'),
'classes': ('monospace',),
}),
('Pagination', {
'fields': ('PAGINATE_COUNT', 'MAX_PAGE_SIZE'),
}),
('Validation', {
'fields': ('CUSTOM_VALIDATORS',),
'classes': ('monospace',),
}),
('NAPALM', {
'fields': ('NAPALM_USERNAME', 'NAPALM_PASSWORD', 'NAPALM_TIMEOUT', 'NAPALM_ARGS'),
'classes': ('monospace',),
}),
('Miscellaneous', {
'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'MAPS_URL'),

View File

@@ -5,10 +5,10 @@ from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers
from dcim.api.nested_serializers import (
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer,
NestedRackSerializer, NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer, NestedRegionSerializer,
NestedSiteSerializer, NestedSiteGroupSerializer,
)
from dcim.models import Device, DeviceRole, DeviceType, Platform, Rack, Region, Site, SiteGroup
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
from extras.choices import *
from extras.models import *
from extras.utils import FeatureQuery
@@ -61,7 +61,7 @@ class WebhookSerializer(ValidatedModelSerializer):
fields = [
'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url',
'enabled', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
'conditions', 'ssl_verification', 'ca_file_path',
'conditions', 'ssl_verification', 'ca_file_path', 'created', 'last_updated',
]
@@ -82,7 +82,8 @@ class CustomFieldSerializer(ValidatedModelSerializer):
model = CustomField
fields = [
'id', 'url', 'display', 'content_types', 'type', 'name', 'label', 'description', 'required', 'filter_logic',
'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices',
'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'created',
'last_updated',
]
@@ -100,7 +101,7 @@ class CustomLinkSerializer(ValidatedModelSerializer):
model = CustomLink
fields = [
'id', 'url', 'display', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name',
'button_class', 'new_window',
'button_class', 'new_window', 'created', 'last_updated',
]
@@ -118,7 +119,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
model = ExportTemplate
fields = [
'id', 'url', 'display', 'content_type', 'name', 'description', 'template_code', 'mime_type',
'file_extension', 'as_attachment',
'file_extension', 'as_attachment', 'created', 'last_updated',
]
@@ -132,7 +133,9 @@ class TagSerializer(ValidatedModelSerializer):
class Meta:
model = Tag
fields = ['id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tagged_items']
fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tagged_items', 'created', 'last_updated',
]
#

View File

@@ -4,6 +4,7 @@ from django_rq.queues import get_connection
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.routers import APIRootView
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
@@ -382,6 +383,7 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
"""
Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects.
"""
permission_classes = (IsAuthenticated,)
queryset = ContentType.objects.order_by('app_label', 'model')
serializer_class = serializers.ContentTypeSerializer
filterset_class = filtersets.ContentTypeFilterSet

View File

@@ -62,7 +62,7 @@ class CustomFieldFilterSet(BaseFilterSet):
class Meta:
model = CustomField
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight']
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -103,7 +103,7 @@ class ExportTemplateFilterSet(BaseFilterSet):
class Meta:
model = ExportTemplate
fields = ['id', 'content_type', 'name']
fields = ['id', 'content_type', 'name', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -177,14 +177,15 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
class Meta:
model = Tag
fields = ['id', 'name', 'slug', 'color']
fields = ['id', 'name', 'slug', 'color', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(slug__icontains=value)
Q(slug__icontains=value) |
Q(description__icontains=value)
)
def _content_type(self, queryset, name, values):
@@ -317,6 +318,11 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
to_field_name='slug',
label='Tenant (slug)',
)
tag_id = django_filters.ModelMultipleChoiceFilter(
field_name='tags',
queryset=Tag.objects.all(),
label='Tag',
)
tag = django_filters.ModelMultipleChoiceFilter(
field_name='tags__slug',
queryset=Tag.objects.all(),

View File

@@ -4,7 +4,9 @@ from django.contrib.contenttypes.models import ContentType
from extras.choices import *
from extras.models import *
from extras.utils import FeatureQuery
from utilities.forms import BulkEditForm, BulkEditNullBooleanSelect, ColorField, ContentTypeChoiceField, StaticSelect
from utilities.forms import (
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, ContentTypeChoiceField, StaticSelect,
)
__all__ = (
'ConfigContextBulkEditForm',
@@ -44,7 +46,7 @@ class CustomLinkBulkEditForm(BulkEditForm):
)
content_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'),
limit_choices_to=FeatureQuery('custom_links'),
required=False
)
new_window = forms.NullBooleanField(
@@ -55,7 +57,7 @@ class CustomLinkBulkEditForm(BulkEditForm):
required=False
)
button_class = forms.ChoiceField(
choices=CustomLinkButtonClassChoices,
choices=add_blank_choice(CustomLinkButtonClassChoices),
required=False,
widget=StaticSelect()
)
@@ -71,7 +73,7 @@ class ExportTemplateBulkEditForm(BulkEditForm):
)
content_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'),
limit_choices_to=FeatureQuery('export_templates'),
required=False
)
description = forms.CharField(
@@ -117,21 +119,25 @@ class WebhookBulkEditForm(BulkEditForm):
widget=BulkEditNullBooleanSelect()
)
http_method = forms.ChoiceField(
choices=WebhookHttpMethodChoices,
required=False
choices=add_blank_choice(WebhookHttpMethodChoices),
required=False,
label='HTTP method'
)
payload_url = forms.CharField(
required=False
required=False,
label='Payload URL'
)
ssl_verification = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect()
widget=BulkEditNullBooleanSelect(),
label='SSL verification'
)
secret = forms.CharField(
required=False
)
ca_file_path = forms.CharField(
required=False
required=False,
label='CA file path'
)
class Meta:
@@ -185,7 +191,7 @@ class JournalEntryBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput
)
kind = forms.ChoiceField(
choices=JournalEntryKindChoices,
choices=add_blank_choice(JournalEntryKindChoices),
required=False
)
comments = forms.CharField(

View File

@@ -3,9 +3,10 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.safestring import mark_safe
from extras.choices import CustomFieldTypeChoices
from extras.models import *
from extras.utils import FeatureQuery
from utilities.forms import CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField
__all__ = (
'CustomFieldCSVForm',
@@ -22,6 +23,10 @@ class CustomFieldCSVForm(CSVModelForm):
limit_choices_to=FeatureQuery('custom_fields'),
help_text="One or more assigned object types"
)
type = CSVChoiceField(
choices=CustomFieldTypeChoices,
help_text='Field data type (e.g. text, integer, etc.)'
)
choices = SimpleArrayField(
base_field=forms.CharField(),
required=False,
@@ -32,7 +37,7 @@ class CustomFieldCSVForm(CSVModelForm):
model = CustomField
fields = (
'name', 'label', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', 'default',
'choices', 'weight',
'choices', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
)

View File

@@ -4,7 +4,7 @@ from django.db.models import Q
from extras.choices import *
from extras.models import *
from utilities.forms import BootstrapMixin, BulkEditForm, CSVModelForm, FilterForm
from utilities.forms import BootstrapMixin, BulkEditBaseForm, CSVModelForm
__all__ = (
'CustomFieldModelCSVForm',
@@ -34,6 +34,9 @@ class CustomFieldsMixin:
raise NotImplementedError(f"{self.__class__.__name__} must specify a model class.")
return ContentType.objects.get_for_model(self.model)
def _get_custom_fields(self, content_type):
return CustomField.objects.filter(content_types=content_type)
def _get_form_field(self, customfield):
return customfield.to_form_field()
@@ -41,10 +44,7 @@ class CustomFieldsMixin:
"""
Append form fields for all CustomFields assigned to this object type.
"""
content_type = self._get_content_type()
# Append form fields; assign initial values if modifying and existing object
for customfield in CustomField.objects.filter(content_types=content_type):
for customfield in self._get_custom_fields(self._get_content_type()):
field_name = f'cf_{customfield.name}'
self.fields[field_name] = self._get_form_field(customfield)
@@ -86,40 +86,37 @@ class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm):
return customfield.to_form_field(for_csv_import=True)
class CustomFieldModelBulkEditForm(BulkEditForm):
class CustomFieldModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, BulkEditBaseForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def _get_form_field(self, customfield):
return customfield.to_form_field(set_initial=False, enforce_required=False)
self.custom_fields = []
self.obj_type = ContentType.objects.get_for_model(self.model)
# Add all applicable CustomFields to the form
custom_fields = CustomField.objects.filter(content_types=self.obj_type)
for cf in custom_fields:
def _append_customfield_fields(self):
"""
Append form fields for all CustomFields assigned to this object type.
"""
for customfield in self._get_custom_fields(self._get_content_type()):
# Annotate non-required custom fields as nullable
if not cf.required:
self.nullable_fields.append(cf.name)
self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False)
# Annotate this as a custom field
self.custom_fields.append(cf.name)
if not customfield.required:
self.nullable_fields.append(customfield.name)
self.fields[customfield.name] = self._get_form_field(customfield)
# Annotate the field in the list of CustomField form fields
self.custom_fields.append(customfield.name)
class CustomFieldModelFilterForm(FilterForm):
class CustomFieldModelFilterForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
q = forms.CharField(
required=False,
label='Search'
)
def __init__(self, *args, **kwargs):
self.obj_type = ContentType.objects.get_for_model(self.model)
super().__init__(*args, **kwargs)
# Add all applicable CustomFields to the form
self.custom_field_filters = []
custom_fields = CustomField.objects.filter(content_types=self.obj_type).exclude(
def _get_custom_fields(self, content_type):
return CustomField.objects.filter(content_types=content_type).exclude(
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
Q(type=CustomFieldTypeChoices.TYPE_JSON)
)
for cf in custom_fields:
field_name = f'cf_{cf.name}'
self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False)
self.custom_field_filters.append(field_name)
def _get_form_field(self, customfield):
return customfield.to_form_field(set_initial=False, enforce_required=False)

View File

@@ -62,7 +62,7 @@ class CustomLinkFilterForm(FilterForm):
]
content_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'),
limit_choices_to=FeatureQuery('custom_links'),
required=False
)
weight = forms.IntegerField(
@@ -83,7 +83,7 @@ class ExportTemplateFilterForm(FilterForm):
]
content_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'),
limit_choices_to=FeatureQuery('export_templates'),
required=False
)
mime_type = forms.CharField(
@@ -109,7 +109,7 @@ class WebhookFilterForm(FilterForm):
]
content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'),
limit_choices_to=FeatureQuery('webhooks'),
required=False
)
http_method = forms.MultipleChoiceField(
@@ -155,7 +155,7 @@ class TagFilterForm(FilterForm):
class ConfigContextFilterForm(FilterForm):
field_groups = [
['q', 'tag'],
['q', 'tag_id'],
['region_id', 'site_group_id', 'site_id'],
['device_type_id', 'platform_id', 'role_id'],
['cluster_group_id', 'cluster_id'],
@@ -164,69 +164,57 @@ class ConfigContextFilterForm(FilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Regions'),
fetch_trigger='open'
label=_('Regions')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site groups'),
fetch_trigger='open'
label=_('Site groups')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
label=_('Sites'),
fetch_trigger='open'
label=_('Sites')
)
device_type_id = DynamicModelMultipleChoiceField(
queryset=DeviceType.objects.all(),
required=False,
label=_('Device types'),
fetch_trigger='open'
label=_('Device types')
)
role_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
label=_('Roles'),
fetch_trigger='open'
label=_('Roles')
)
platform_id = DynamicModelMultipleChoiceField(
queryset=Platform.objects.all(),
required=False,
label=_('Platforms'),
fetch_trigger='open'
label=_('Platforms')
)
cluster_group_id = DynamicModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(),
required=False,
label=_('Cluster groups'),
fetch_trigger='open'
label=_('Cluster groups')
)
cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(),
required=False,
label=_('Clusters'),
fetch_trigger='open'
label=_('Clusters')
)
tenant_group_id = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
label=_('Tenant groups'),
fetch_trigger='open'
label=_('Tenant groups')
)
tenant_id = DynamicModelMultipleChoiceField(
queryset=Tenant.objects.all(),
required=False,
label=_('Tenant'),
fetch_trigger='open'
label=_('Tenant')
)
tag = DynamicModelMultipleChoiceField(
tag_id = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
to_field_name='slug',
required=False,
label=_('Tags'),
fetch_trigger='open'
label=_('Tags')
)
@@ -263,8 +251,7 @@ class JournalEntryFilterForm(FilterForm):
label=_('User'),
widget=APISelectMultiple(
api_url='/api/users/users/',
),
fetch_trigger='open'
)
)
assigned_object_type_id = DynamicModelMultipleChoiceField(
queryset=ContentType.objects.all(),
@@ -272,8 +259,7 @@ class JournalEntryFilterForm(FilterForm):
label=_('Object Type'),
widget=APISelectMultiple(
api_url='/api/extras/content-types/',
),
fetch_trigger='open'
)
)
kind = forms.ChoiceField(
choices=add_blank_choice(JournalEntryKindChoices),
@@ -310,8 +296,7 @@ class ObjectChangeFilterForm(FilterForm):
label=_('User'),
widget=APISelectMultiple(
api_url='/api/users/users/',
),
fetch_trigger='open'
)
)
changed_object_type_id = DynamicModelMultipleChoiceField(
queryset=ContentType.objects.all(),
@@ -319,6 +304,5 @@ class ObjectChangeFilterForm(FilterForm):
label=_('Object Type'),
widget=APISelectMultiple(
api_url='/api/extras/content-types/',
),
fetch_trigger='open'
)
)

View File

@@ -7,8 +7,8 @@ from extras.models import *
from extras.utils import FeatureQuery
from tenancy.models import Tenant, TenantGroup
from utilities.forms import (
add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField,
ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect,
add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField,
DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect,
)
from virtualization.models import Cluster, ClusterGroup
@@ -41,6 +41,10 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
('Values', ('default', 'choices')),
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
)
widgets = {
'type': StaticSelect(),
'filter_logic': StaticSelect(),
}
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
@@ -57,6 +61,7 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
('Templates', ('link_text', 'link_url')),
)
widgets = {
'button_class': StaticSelect(),
'link_text': forms.Textarea(attrs={'class': 'font-monospace'}),
'link_url': forms.Textarea(attrs={'class': 'font-monospace'}),
}
@@ -77,7 +82,7 @@ class ExportTemplateForm(BootstrapMixin, forms.ModelForm):
model = ExportTemplate
fields = '__all__'
fieldsets = (
('Custom Link', ('name', 'content_type', 'description')),
('Export Template', ('name', 'content_type', 'description')),
('Template', ('template_code',)),
('Rendering', ('mime_type', 'file_extension', 'as_attachment')),
)
@@ -96,8 +101,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
model = Webhook
fields = '__all__'
fieldsets = (
('Webhook', ('name', 'enabled')),
('Assigned Models', ('content_types',)),
('Webhook', ('name', 'content_types', 'enabled')),
('Events', ('type_create', 'type_update', 'type_delete')),
('HTTP Request', (
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
@@ -105,7 +109,13 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
('Conditions', ('conditions',)),
('SSL', ('ssl_verification', 'ca_file_path')),
)
labels = {
'type_create': 'Creations',
'type_update': 'Updates',
'type_delete': 'Deletions',
}
widgets = {
'http_method': StaticSelect(),
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
}

View File

@@ -70,10 +70,23 @@ class Command(BaseCommand):
return namespace
def handle(self, **options):
namespace = self.get_namespace()
# If Python code has been passed, execute it and exit.
if options['command']:
exec(options['command'], self.get_namespace())
exec(options['command'], namespace)
return
shell = code.interact(banner=BANNER_TEXT, local=self.get_namespace())
# Try to enable tab-complete
try:
import readline
import rlcompleter
except ModuleNotFoundError:
pass
else:
readline.set_completer(rlcompleter.Completer(namespace).complete)
readline.parse_and_bind('tab: complete')
# Run interactive shell
shell = code.interact(banner=BANNER_TEXT, local=namespace)
return shell

View File

@@ -0,0 +1,21 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('extras', '0066_customfield_name_validation'),
]
operations = [
migrations.AlterField(
model_name='customfield',
name='validation_maximum',
field=models.IntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='customfield',
name='validation_minimum',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@@ -16,7 +16,8 @@ from extras.utils import FeatureQuery, extras_features
from netbox.models import ChangeLoggedModel
from utilities import filters
from utilities.forms import (
CSVChoiceField, DatePicker, LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice,
CSVChoiceField, CSVMultipleChoiceField, DatePicker, LaxURLField, StaticSelectMultiple, StaticSelect,
add_blank_choice,
)
from utilities.querysets import RestrictedQuerySet
from utilities.validators import validate_regex
@@ -96,13 +97,13 @@ class CustomField(ChangeLoggedModel):
default=100,
help_text='Fields with higher weights appear lower in a form.'
)
validation_minimum = models.PositiveIntegerField(
validation_minimum = models.IntegerField(
blank=True,
null=True,
verbose_name='Minimum value',
help_text='Minimum allowed value (for numeric fields)'
)
validation_maximum = models.PositiveIntegerField(
validation_maximum = models.IntegerField(
blank=True,
null=True,
verbose_name='Maximum value',
@@ -238,7 +239,7 @@ class CustomField(ChangeLoggedModel):
"""
Return a form field suitable for setting a CustomField's value for an object.
set_initial: Set initial date for the field. This should be False when generating a field for bulk editing.
set_initial: Set initial data for the field. This should be False when generating a field for bulk editing.
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
"""
@@ -287,7 +288,7 @@ class CustomField(ChangeLoggedModel):
choices=choices, required=required, initial=initial, widget=StaticSelect()
)
else:
field_class = CSVChoiceField if for_csv_import else forms.MultipleChoiceField
field_class = CSVMultipleChoiceField if for_csv_import else forms.MultipleChoiceField
field = field_class(
choices=choices, required=required, initial=initial, widget=StaticSelectMultiple()
)

View File

@@ -229,6 +229,24 @@ class CustomLink(ChangeLoggedModel):
def get_absolute_url(self):
return reverse('extras:customlink', args=[self.pk])
def render(self, context):
"""
Render the CustomLink given the provided context, and return the text, link, and link_target.
:param context: The context passed to Jinja2
"""
text = render_jinja2(self.link_text, context)
if not text:
return {}
link = render_jinja2(self.link_url, context)
link_target = ' target="_blank"' if self.new_window else ''
return {
'text': text,
'link': link,
'link_target': link_target,
}
@extras_features('webhooks', 'export_templates')
class ExportTemplate(ChangeLoggedModel):

View File

@@ -21,7 +21,7 @@ from extras.models import JobResult
from ipam.formfields import IPAddressFormField, IPNetworkFormField
from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
from utilities.exceptions import AbortTransaction
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms import add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from .context_managers import change_logging
from .forms import ScriptForm
@@ -164,16 +164,22 @@ class ChoiceVar(ScriptVariable):
def __init__(self, choices, *args, **kwargs):
super().__init__(*args, **kwargs)
# Set field choices
self.field_attrs['choices'] = choices
# Set field choices, adding a blank choice to avoid forced selections
self.field_attrs['choices'] = add_blank_choice(choices)
class MultiChoiceVar(ChoiceVar):
class MultiChoiceVar(ScriptVariable):
"""
Like ChoiceVar, but allows for the selection of multiple choices.
"""
form_field = forms.MultipleChoiceField
def __init__(self, choices, *args, **kwargs):
super().__init__(*args, **kwargs)
# Set field choices
self.field_attrs['choices'] = choices
class ObjectVar(ScriptVariable):
"""
@@ -253,6 +259,10 @@ class BaseScript:
Base model for custom scripts. User classes should inherit from this model if they want to extend Script
functionality for use in other subclasses.
"""
# Prevent django from instantiating the class on all accesses
do_not_call_in_templates = True
class Meta:
pass
@@ -274,7 +284,7 @@ class BaseScript:
@classproperty
def name(self):
return getattr(self.Meta, 'name', self.__class__.__name__)
return getattr(self.Meta, 'name', self.__name__)
@classproperty
def full_name(self):
@@ -290,12 +300,21 @@ class BaseScript:
@classmethod
def _get_vars(cls):
vars = OrderedDict()
vars = {}
for name, attr in cls.__dict__.items():
if name not in vars and issubclass(attr.__class__, ScriptVariable):
vars[name] = attr
return vars
# Order variables according to field_order
field_order = getattr(cls.Meta, 'field_order', None)
if not field_order:
return vars
ordered_vars = {
field: vars.pop(field) for field in field_order if field in vars
}
ordered_vars.update(vars)
return ordered_vars
def run(self, data, commit):
raise NotImplementedError("The script must define a run() method.")

View File

@@ -29,8 +29,13 @@ CONFIGCONTEXT_ACTIONS = """
{% endif %}
"""
OBJECTCHANGE_FULL_NAME = """
{% load helpers %}
{{ record.user.get_full_name|placeholder }}
"""
OBJECTCHANGE_OBJECT = """
{% if record.changed_object.get_absolute_url %}
{% if record.changed_object and record.changed_object.get_absolute_url %}
<a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
{% else %}
{{ record.object_repr }}
@@ -58,7 +63,7 @@ class CustomFieldTable(BaseTable):
model = CustomField
fields = (
'pk', 'id', 'name', 'content_types', 'label', 'type', 'required', 'weight', 'default',
'description', 'filter_logic', 'choices',
'description', 'filter_logic', 'choices', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'content_types', 'label', 'type', 'required', 'description')
@@ -79,7 +84,7 @@ class CustomLinkTable(BaseTable):
model = CustomLink
fields = (
'pk', 'id', 'name', 'content_type', 'link_text', 'link_url', 'weight', 'group_name',
'button_class', 'new_window',
'button_class', 'new_window', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'content_type', 'group_name', 'button_class', 'new_window')
@@ -100,6 +105,7 @@ class ExportTemplateTable(BaseTable):
model = ExportTemplate
fields = (
'pk', 'id', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
@@ -134,7 +140,7 @@ class WebhookTable(BaseTable):
model = Webhook
fields = (
'pk', 'id', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method',
'payload_url', 'secret', 'ssl_validation', 'ca_file_path',
'payload_url', 'secret', 'ssl_validation', 'ca_file_path', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method',
@@ -156,7 +162,7 @@ class TagTable(BaseTable):
class Meta(BaseTable.Meta):
model = Tag
fields = ('pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'actions')
fields = ('pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'actions', 'created', 'last_updated',)
default_columns = ('pk', 'name', 'items', 'slug', 'color', 'description', 'actions')
@@ -193,7 +199,7 @@ class ConfigContextTable(BaseTable):
model = ConfigContext
fields = (
'pk', 'id', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles',
'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants',
'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'weight', 'is_active', 'description')
@@ -203,6 +209,14 @@ class ObjectChangeTable(BaseTable):
linkify=True,
format=settings.SHORT_DATETIME_FORMAT
)
user_name = tables.Column(
verbose_name='Username'
)
full_name = tables.TemplateColumn(
template_code=OBJECTCHANGE_FULL_NAME,
verbose_name='Full Name',
orderable=False
)
action = ChoiceFieldColumn()
changed_object_type = ContentTypeColumn(
verbose_name='Type'
@@ -218,7 +232,7 @@ class ObjectChangeTable(BaseTable):
class Meta(BaseTable.Meta):
model = ObjectChange
fields = ('id', 'time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'request_id')
fields = ('id', 'time', 'user_name', 'full_name', 'action', 'changed_object_type', 'object_repr', 'request_id')
class ObjectJournalTable(BaseTable):

View File

@@ -62,16 +62,14 @@ def custom_links(context, obj):
# Add non-grouped links
else:
try:
text_rendered = render_jinja2(cl.link_text, link_context)
if text_rendered:
link_rendered = render_jinja2(cl.link_url, link_context)
link_target = ' target="_blank"' if cl.new_window else ''
rendered = cl.render(link_context)
if rendered:
template_code += LINK_BUTTON.format(
link_rendered, link_target, cl.button_class, text_rendered
rendered['link'], rendered['link_target'], cl.button_class, rendered['text']
)
except Exception as e:
template_code += '<a class="btn btn-sm btn-outline-dark" disabled="disabled" title="{}">' \
'<i class="mdi mdi-alert"></i> {}</a>\n'.format(e, cl.name)
template_code += f'<a class="btn btn-sm btn-outline-dark" disabled="disabled" title="{e}">' \
f'<i class="mdi mdi-alert"></i> {cl.name}</a>\n'
# Add grouped links to template
for group, links in group_names.items():
@@ -80,17 +78,15 @@ def custom_links(context, obj):
for cl in links:
try:
text_rendered = render_jinja2(cl.link_text, link_context)
if text_rendered:
link_target = ' target="_blank"' if cl.new_window else ''
link_rendered = render_jinja2(cl.link_url, link_context)
rendered = cl.render(link_context)
if rendered:
links_rendered.append(
GROUP_LINK.format(link_rendered, link_target, text_rendered)
GROUP_LINK.format(rendered['link'], rendered['link_target'], rendered['text'])
)
except Exception as e:
links_rendered.append(
'<li><a class="dropdown-item" disabled="disabled" title="{}"><span class="text-muted">'
'<i class="mdi mdi-alert"></i> {}</span></a></li>'.format(e, cl.name)
f'<li><a class="dropdown-item" disabled="disabled" title="{e}"><span class="text-muted">'
f'<i class="mdi mdi-alert"></i> {cl.name}</span></a></li>'
)
if links_rendered:

View File

@@ -608,7 +608,6 @@ class CreatedUpdatedFilterTest(APITestCase):
class ContentTypeTest(APITestCase):
@override_settings(EXEMPT_VIEW_PERMISSIONS=['contenttypes.contenttype'])
def test_list_objects(self):
contenttype_count = ContentType.objects.count()
@@ -616,7 +615,6 @@ class ContentTypeTest(APITestCase):
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(response.data['count'], contenttype_count)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['contenttypes.contenttype'])
def test_get_object(self):
contenttype = ContentType.objects.first()

View File

@@ -25,49 +25,68 @@ class CustomFieldTest(TestCase):
def test_simple_fields(self):
DATA = (
{
'field_type': CustomFieldTypeChoices.TYPE_TEXT,
'field_value': 'Foobar!',
'empty_value': '',
'field': {
'type': CustomFieldTypeChoices.TYPE_TEXT,
},
'value': 'Foobar!',
},
{
'field_type': CustomFieldTypeChoices.TYPE_LONGTEXT,
'field_value': 'Text with **Markdown**',
'empty_value': '',
'field': {
'type': CustomFieldTypeChoices.TYPE_LONGTEXT,
},
'value': 'Text with **Markdown**',
},
{
'field_type': CustomFieldTypeChoices.TYPE_INTEGER,
'field_value': 0,
'empty_value': None,
'field': {
'type': CustomFieldTypeChoices.TYPE_INTEGER,
},
'value': 0,
},
{
'field_type': CustomFieldTypeChoices.TYPE_INTEGER,
'field_value': 42,
'empty_value': None,
'field': {
'type': CustomFieldTypeChoices.TYPE_INTEGER,
'validation_minimum': 1,
'validation_maximum': 100,
},
'value': 42,
},
{
'field_type': CustomFieldTypeChoices.TYPE_BOOLEAN,
'field_value': True,
'empty_value': None,
'field': {
'type': CustomFieldTypeChoices.TYPE_INTEGER,
'validation_minimum': -100,
'validation_maximum': -1,
},
'value': -42,
},
{
'field_type': CustomFieldTypeChoices.TYPE_BOOLEAN,
'field_value': False,
'empty_value': None,
'field': {
'type': CustomFieldTypeChoices.TYPE_BOOLEAN,
},
'value': True,
},
{
'field_type': CustomFieldTypeChoices.TYPE_DATE,
'field_value': '2016-06-23',
'empty_value': None,
'field': {
'type': CustomFieldTypeChoices.TYPE_BOOLEAN,
},
'value': False,
},
{
'field_type': CustomFieldTypeChoices.TYPE_URL,
'field_value': 'http://example.com/',
'empty_value': '',
'field': {
'type': CustomFieldTypeChoices.TYPE_DATE,
},
'value': '2016-06-23',
},
{
'field_type': CustomFieldTypeChoices.TYPE_JSON,
'field_value': '{"foo": 1, "bar": 2}',
'empty_value': 'null',
'field': {
'type': CustomFieldTypeChoices.TYPE_URL,
},
'value': 'http://example.com/',
},
{
'field': {
'type': CustomFieldTypeChoices.TYPE_JSON,
},
'value': '{"foo": 1, "bar": 2}',
},
)
@@ -76,7 +95,7 @@ class CustomFieldTest(TestCase):
for data in DATA:
# Create a custom field
cf = CustomField(type=data['field_type'], name='my_field', required=False)
cf = CustomField(name='my_field', required=False, **data['field'])
cf.save()
cf.content_types.set([obj_type])
@@ -85,12 +104,12 @@ class CustomFieldTest(TestCase):
self.assertIsNone(site.custom_field_data[cf.name])
# Assign a value to the first Site
site.custom_field_data[cf.name] = data['field_value']
site.custom_field_data[cf.name] = data['value']
site.save()
# Retrieve the stored value
site.refresh_from_db()
self.assertEqual(site.custom_field_data[cf.name], data['field_value'])
self.assertEqual(site.custom_field_data[cf.name], data['value'])
# Delete the stored value
site.custom_field_data.pop(cf.name)
@@ -103,13 +122,14 @@ class CustomFieldTest(TestCase):
def test_select_field(self):
obj_type = ContentType.objects.get_for_model(Site)
choices = ['Option A', 'Option B', 'Option C']
# Create a custom field
cf = CustomField(
type=CustomFieldTypeChoices.TYPE_SELECT,
name='my_field',
required=False,
choices=['Option A', 'Option B', 'Option C']
choices=choices
)
cf.save()
cf.content_types.set([obj_type])
@@ -119,12 +139,47 @@ class CustomFieldTest(TestCase):
self.assertIsNone(site.custom_field_data[cf.name])
# Assign a value to the first Site
site.custom_field_data[cf.name] = 'Option A'
site.custom_field_data[cf.name] = choices[0]
site.save()
# Retrieve the stored value
site.refresh_from_db()
self.assertEqual(site.custom_field_data[cf.name], 'Option A')
self.assertEqual(site.custom_field_data[cf.name], choices[0])
# Delete the stored value
site.custom_field_data.pop(cf.name)
site.save()
site.refresh_from_db()
self.assertIsNone(site.custom_field_data.get(cf.name))
# Delete the custom field
cf.delete()
def test_multiselect_field(self):
obj_type = ContentType.objects.get_for_model(Site)
choices = ['Option A', 'Option B', 'Option C']
# Create a custom field
cf = CustomField(
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
name='my_field',
required=False,
choices=choices
)
cf.save()
cf.content_types.set([obj_type])
# Check that the field has a null initial value
site = Site.objects.first()
self.assertIsNone(site.custom_field_data[cf.name])
# Assign a value to the first Site
site.custom_field_data[cf.name] = [choices[0], choices[1]]
site.save()
# Retrieve the stored value
site.refresh_from_db()
self.assertEqual(site.custom_field_data[cf.name], [choices[0], choices[1]])
# Delete the stored value
site.custom_field_data.pop(cf.name)
@@ -578,6 +633,9 @@ class CustomFieldImportTest(TestCase):
CustomField(name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choices=[
'Choice A', 'Choice B', 'Choice C',
]),
CustomField(name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choices=[
'Choice A', 'Choice B', 'Choice C',
]),
)
for cf in custom_fields:
cf.save()
@@ -588,19 +646,20 @@ class CustomFieldImportTest(TestCase):
Import a Site in CSV format, including a value for each CustomField.
"""
data = (
('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_json', 'cf_select'),
('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', 'True', '2020-01-01', 'http://example.com/1', '{"foo": 123}', 'Choice A'),
('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', 'False', '2020-01-02', 'http://example.com/2', '{"bar": 456}', 'Choice B'),
('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', ''),
('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect'),
('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', 'True', '2020-01-01', 'http://example.com/1', '{"foo": 123}', 'Choice A', '"Choice A,Choice B"'),
('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', 'False', '2020-01-02', 'http://example.com/2', '{"bar": 456}', 'Choice B', '"Choice B,Choice C"'),
('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', ''),
)
csv_data = '\n'.join(','.join(row) for row in data)
response = self.client.post(reverse('dcim:site_import'), {'csv': csv_data})
self.assertEqual(response.status_code, 200)
self.assertEqual(Site.objects.count(), 3)
# Validate data for site 1
site1 = Site.objects.get(name='Site 1')
self.assertEqual(len(site1.custom_field_data), 8)
self.assertEqual(len(site1.custom_field_data), 9)
self.assertEqual(site1.custom_field_data['text'], 'ABC')
self.assertEqual(site1.custom_field_data['longtext'], 'Foo')
self.assertEqual(site1.custom_field_data['integer'], 123)
@@ -609,10 +668,11 @@ class CustomFieldImportTest(TestCase):
self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1')
self.assertEqual(site1.custom_field_data['json'], {"foo": 123})
self.assertEqual(site1.custom_field_data['select'], 'Choice A')
self.assertEqual(site1.custom_field_data['multiselect'], ['Choice A', 'Choice B'])
# Validate data for site 2
site2 = Site.objects.get(name='Site 2')
self.assertEqual(len(site2.custom_field_data), 8)
self.assertEqual(len(site2.custom_field_data), 9)
self.assertEqual(site2.custom_field_data['text'], 'DEF')
self.assertEqual(site2.custom_field_data['longtext'], 'Bar')
self.assertEqual(site2.custom_field_data['integer'], 456)
@@ -621,6 +681,7 @@ class CustomFieldImportTest(TestCase):
self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2')
self.assertEqual(site2.custom_field_data['json'], {"bar": 456})
self.assertEqual(site2.custom_field_data['select'], 'Choice B')
self.assertEqual(site2.custom_field_data['multiselect'], ['Choice B', 'Choice C'])
# No custom field data should be set for site 3
site3 = Site.objects.get(name='Site 3')

View File

@@ -12,7 +12,7 @@ from extras.filtersets import *
from extras.models import *
from ipam.models import IPAddress
from tenancy.models import Tenant, TenantGroup
from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests
from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests, create_tags
from virtualization.models import Cluster, ClusterGroup, ClusterType
@@ -153,8 +153,8 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
export_templates = (
ExportTemplate(name='Export Template 1', content_type=content_types[0], template_code='TESTING'),
ExportTemplate(name='Export Template 2', content_type=content_types[1], template_code='TESTING'),
ExportTemplate(name='Export Template 1', content_type=content_types[0], template_code='TESTING', description='foobar1'),
ExportTemplate(name='Export Template 2', content_type=content_types[1], template_code='TESTING', description='foobar2'),
ExportTemplate(name='Export Template 3', content_type=content_types[2], template_code='TESTING'),
)
ExportTemplate.objects.bulk_create(export_templates)
@@ -167,6 +167,10 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
params = {'content_type': ContentType.objects.get(model='site').pk}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
queryset = ImageAttachment.objects.all()
@@ -429,6 +433,8 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
)
Tenant.objects.bulk_create(tenants)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
for i in range(0, 3):
is_active = bool(i % 2)
c = ConfigContext.objects.create(
@@ -446,6 +452,7 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
c.clusters.set([clusters[i]])
c.tenant_groups.set([tenant_groups[i]])
c.tenants.set([tenants[i]])
c.tags.set([tags[i]])
def test_name(self):
params = {'name': ['Config Context 1', 'Config Context 2']}
@@ -516,13 +523,20 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant_(self):
def test_tenant(self):
tenants = Tenant.objects.all()[:2]
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tags(self):
tags = Tag.objects.all()[:2]
params = {'tag_id': [tags[0].pk, tags[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tag': [tags[0].slug, tags[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Tag.objects.all()
@@ -532,8 +546,8 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
tags = (
Tag(name='Tag 1', slug='tag-1', color='ff0000'),
Tag(name='Tag 2', slug='tag-2', color='00ff00'),
Tag(name='Tag 1', slug='tag-1', color='ff0000', description='foobar1'),
Tag(name='Tag 2', slug='tag-2', color='00ff00', description='foobar2'),
Tag(name='Tag 3', slug='tag-3', color='0000ff'),
)
Tag.objects.bulk_create(tags)
@@ -557,6 +571,10 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'color': ['ff0000', '00ff00']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_content_type(self):
params = {'content_type': ['dcim.site', 'circuits.provider']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@@ -39,10 +39,10 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
cls.csv_data = (
'name,label,type,content_types,weight,filter_logic,choices',
'field4,Field 4,text,dcim.site,100,exact,',
'field5,Field 5,integer,dcim.site,100,exact,',
'field6,Field 6,select,dcim.site,100,exact,"A,B,C"',
'name,label,type,content_types,weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex',
'field4,Field 4,text,dcim.site,100,exact,,,,[a-z]{3}',
'field5,Field 5,integer,dcim.site,100,exact,,1,100,',
'field6,Field 6,select,dcim.site,100,exact,"A,B,C",,,',
)
cls.bulk_edit_data = {

View File

@@ -10,6 +10,7 @@ from rq import Worker
from netbox.views import generic
from utilities.forms import ConfirmationForm
from utilities.htmx import is_htmx
from utilities.tables import paginate_table
from utilities.utils import copy_safe_request, count_related, normalize_querydict, shallow_compare_dict
from utilities.views import ContentTypePermissionRequiredMixin
@@ -447,7 +448,8 @@ class ObjectChangeLogView(View):
)
objectchanges_table = tables.ObjectChangeTable(
data=objectchanges,
orderable=False
orderable=False,
user=request.user
)
paginate_table(objectchanges_table, request)
@@ -471,6 +473,7 @@ class ObjectChangeLogView(View):
class ImageAttachmentEditView(generic.ObjectEditView):
queryset = ImageAttachment.objects.all()
model_form = forms.ImageAttachmentForm
template_name = 'extras/imageattachment_edit.html'
def alter_obj(self, instance, request, args, kwargs):
if not instance.pk:
@@ -693,16 +696,26 @@ class ReportResultView(ContentTypePermissionRequiredMixin, View):
def get(self, request, job_result_pk):
report_content_type = ContentType.objects.get(app_label='extras', model='report')
jobresult = get_object_or_404(JobResult.objects.all(), pk=job_result_pk, obj_type=report_content_type)
result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk, obj_type=report_content_type)
# Retrieve the Report and attach the JobResult to it
module, report_name = jobresult.name.split('.')
module, report_name = result.name.split('.')
report = get_report(module, report_name)
report.result = jobresult
report.result = result
# If this is an HTMX request, return only the result HTML
if is_htmx(request):
response = render(request, 'extras/htmx/report_result.html', {
'report': report,
'result': result,
})
if result.completed:
response.status_code = 286
return response
return render(request, 'extras/report_result.html', {
'report': report,
'result': jobresult,
'result': result,
})
@@ -820,6 +833,16 @@ class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View)
script = self._get_script(result.name)
# If this is an HTMX request, return only the result HTML
if is_htmx(request):
response = render(request, 'extras/htmx/script_result.html', {
'script': script,
'result': result,
})
if result.completed:
response.status_code = 286
return response
return render(request, 'extras/script_result.html', {
'script': script,
'result': result,

View File

@@ -126,7 +126,7 @@ class FHRPGroupSerializer(PrimaryModelSerializer):
class FHRPGroupAssignmentSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail')
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
group = NestedFHRPGroupSerializer()
interface_type = ContentTypeField(
queryset=ContentType.objects.all()

View File

@@ -135,13 +135,23 @@ class FHRPGroupProtocolChoices(ChoiceSet):
PROTOCOL_HSRP = 'hsrp'
PROTOCOL_GLBP = 'glbp'
PROTOCOL_CARP = 'carp'
PROTOCOL_CLUSTERXL = 'clusterxl'
PROTOCOL_OTHER = 'other'
CHOICES = (
(PROTOCOL_VRRP2, 'VRRPv2'),
(PROTOCOL_VRRP3, 'VRRPv3'),
(PROTOCOL_HSRP, 'HSRP'),
(PROTOCOL_GLBP, 'GLBP'),
(PROTOCOL_CARP, 'CARP'),
('Standard', (
(PROTOCOL_VRRP2, 'VRRPv2'),
(PROTOCOL_VRRP3, 'VRRPv3'),
(PROTOCOL_CARP, 'CARP'),
)),
('CheckPoint', (
(PROTOCOL_CLUSTERXL, 'ClusterXL'),
)),
('Cisco', (
(PROTOCOL_HSRP, 'HSRP'),
(PROTOCOL_GLBP, 'GLBP'),
)),
(PROTOCOL_OTHER, 'Other'),
)
@@ -187,8 +197,10 @@ class ServiceProtocolChoices(ChoiceSet):
PROTOCOL_TCP = 'tcp'
PROTOCOL_UDP = 'udp'
PROTOCOL_SCTP = 'sctp'
CHOICES = (
(PROTOCOL_TCP, 'TCP'),
(PROTOCOL_UDP, 'UDP'),
(PROTOCOL_SCTP, 'SCTP'),
)

View File

@@ -65,6 +65,7 @@ FHRP_PROTOCOL_ROLE_MAPPINGS = {
FHRPGroupProtocolChoices.PROTOCOL_HSRP: IPAddressRoleChoices.ROLE_HSRP,
FHRPGroupProtocolChoices.PROTOCOL_GLBP: IPAddressRoleChoices.ROLE_GLBP,
FHRPGroupProtocolChoices.PROTOCOL_CARP: IPAddressRoleChoices.ROLE_CARP,
FHRPGroupProtocolChoices.PROTOCOL_OTHER: IPAddressRoleChoices.ROLE_VIP,
}

View File

@@ -75,7 +75,7 @@ class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = VRF
fields = ['id', 'name', 'rd', 'enforce_unique']
fields = ['id', 'name', 'rd', 'enforce_unique', 'description']
class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
@@ -117,7 +117,7 @@ class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = RouteTarget
fields = ['id', 'name']
fields = ['id', 'name', 'description']
class RIRFilterSet(OrganizationalModelFilterSet):
@@ -155,7 +155,7 @@ class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = Aggregate
fields = ['id', 'date_added']
fields = ['id', 'date_added', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -203,12 +203,16 @@ class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
class Meta:
model = ASN
fields = ['id', 'asn']
fields = ['id', 'asn', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = Q(description__icontains=value)
try:
qs_filter |= Q(asn=int(value))
except ValueError:
pass
return queryset.filter(qs_filter)
@@ -221,7 +225,7 @@ class RoleFilterSet(OrganizationalModelFilterSet):
class Meta:
model = Role
fields = ['id', 'name', 'slug']
fields = ['id', 'name', 'slug', 'description']
class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
@@ -330,7 +334,7 @@ class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
)
vlan_vid = django_filters.NumberFilter(
field_name='vlan__vid',
label='VLAN number (1-4095)',
label='VLAN number (1-4094)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
queryset=Role.objects.all(),
@@ -350,7 +354,7 @@ class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = Prefix
fields = ['id', 'is_pool', 'mark_utilized']
fields = ['id', 'is_pool', 'mark_utilized', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -456,7 +460,7 @@ class IPRangeFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
class Meta:
model = IPRange
fields = ['id']
fields = ['id', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -835,7 +839,7 @@ class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = VLAN
fields = ['id', 'vid', 'name']
fields = ['id', 'vid', 'name', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -887,7 +891,7 @@ class ServiceFilterSet(PrimaryModelFilterSet):
class Meta:
model = Service
fields = ['id', 'name', 'protocol']
fields = ['id', 'name', 'protocol', 'description']
def search(self, queryset, name, value):
if not value.strip():

View File

@@ -302,7 +302,8 @@ class IPAddressBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
)
dns_name = forms.CharField(
max_length=255,
required=False
required=False,
label='DNS name'
)
description = forms.CharField(
max_length=100,

View File

@@ -375,7 +375,7 @@ class VLANCSVForm(CustomFieldModelCSVForm):
model = VLAN
fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description')
help_texts = {
'vid': 'Numeric VLAN ID (1-4095)',
'vid': 'Numeric VLAN ID (1-4094)',
'name': 'VLAN name',
}

View File

@@ -48,14 +48,12 @@ class VRFFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
import_target_id = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
required=False,
label=_('Import targets'),
fetch_trigger='open'
label=_('Import targets')
)
export_target_id = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
required=False,
label=_('Export targets'),
fetch_trigger='open'
label=_('Export targets')
)
tag = TagFilterField(model)
@@ -70,14 +68,12 @@ class RouteTargetFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
importing_vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('Imported by VRF'),
fetch_trigger='open'
label=_('Imported by VRF')
)
exporting_vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('Exported by VRF'),
fetch_trigger='open'
label=_('Exported by VRF')
)
tag = TagFilterField(model)
@@ -110,8 +106,7 @@ class AggregateFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
rir_id = DynamicModelMultipleChoiceField(
queryset=RIR.objects.all(),
required=False,
label=_('RIR'),
fetch_trigger='open'
label=_('RIR')
)
tag = TagFilterField(model)
@@ -127,14 +122,12 @@ class ASNFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
rir_id = DynamicModelMultipleChoiceField(
queryset=RIR.objects.all(),
required=False,
label=_('RIR'),
fetch_trigger='open'
label=_('RIR')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
@@ -180,14 +173,12 @@ class PrefixFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
queryset=VRF.objects.all(),
required=False,
label=_('Assigned VRF'),
null_option='Global',
fetch_trigger='open'
null_option='Global'
)
present_in_vrf_id = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('Present in VRF'),
fetch_trigger='open'
label=_('Present in VRF')
)
status = forms.MultipleChoiceField(
choices=PrefixStatusChoices,
@@ -197,14 +188,12 @@ class PrefixFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -213,15 +202,13 @@ class PrefixFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
role_id = DynamicModelMultipleChoiceField(
queryset=Role.objects.all(),
required=False,
null_option='None',
label=_('Role'),
fetch_trigger='open'
label=_('Role')
)
is_pool = forms.NullBooleanField(
required=False,
@@ -257,8 +244,7 @@ class IPRangeFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
queryset=VRF.objects.all(),
required=False,
label=_('Assigned VRF'),
null_option='Global',
fetch_trigger='open'
null_option='Global'
)
status = forms.MultipleChoiceField(
choices=PrefixStatusChoices,
@@ -269,8 +255,7 @@ class IPRangeFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
queryset=Role.objects.all(),
required=False,
null_option='None',
label=_('Role'),
fetch_trigger='open'
label=_('Role')
)
tag = TagFilterField(model)
@@ -308,14 +293,12 @@ class IPAddressFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
queryset=VRF.objects.all(),
required=False,
label=_('Assigned VRF'),
null_option='Global',
fetch_trigger='open'
null_option='Global'
)
present_in_vrf_id = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('Present in VRF'),
fetch_trigger='open'
label=_('Present in VRF')
)
status = forms.MultipleChoiceField(
choices=IPAddressStatusChoices,
@@ -376,32 +359,27 @@ class VLANGroupFilterForm(CustomFieldModelFilterForm):
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
sitegroup = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
location = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
required=False,
label=_('Location'),
fetch_trigger='open'
label=_('Location')
)
rack = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
required=False,
label=_('Rack'),
fetch_trigger='open'
label=_('Rack')
)
tag = TagFilterField(model)
@@ -417,14 +395,12 @@ class VLANFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -433,8 +409,7 @@ class VLANFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'region': '$region'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
group_id = DynamicModelMultipleChoiceField(
queryset=VLANGroup.objects.all(),
@@ -443,8 +418,7 @@ class VLANFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'region': '$region'
},
label=_('VLAN group'),
fetch_trigger='open'
label=_('VLAN group')
)
status = forms.MultipleChoiceField(
choices=VLANStatusChoices,
@@ -455,8 +429,7 @@ class VLANFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
queryset=Role.objects.all(),
required=False,
null_option='None',
label=_('Role'),
fetch_trigger='open'
label=_('Role')
)
vid = forms.IntegerField(
required=False,

View File

@@ -222,7 +222,7 @@ class PrefixForm(TenancyForm, CustomFieldModelForm):
label='VLAN group',
null_option='None',
query_params={
'site_id': '$site'
'site': '$site'
},
initial_params={
'vlans': '$vlan'
@@ -471,6 +471,8 @@ class IPAddressForm(TenancyForm, CustomFieldModelForm):
})
elif selected_objects:
self.instance.assigned_object = self.cleaned_data[selected_objects[0]]
else:
self.instance.assigned_object = None
# Primary IP assignment is only available if an interface has been assigned.
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
@@ -578,7 +580,7 @@ class FHRPGroupForm(CustomFieldModelForm):
vrf=self.cleaned_data['ip_vrf'],
address=self.cleaned_data['ip_address'],
status=self.cleaned_data['ip_status'],
role=FHRP_PROTOCOL_ROLE_MAPPINGS[self.cleaned_data['protocol']],
role=FHRP_PROTOCOL_ROLE_MAPPINGS.get(self.cleaned_data['protocol'], IPAddressRoleChoices.ROLE_VIP),
assigned_object=instance
)
ipaddress.save()
@@ -590,6 +592,8 @@ class FHRPGroupForm(CustomFieldModelForm):
return instance
def clean(self):
super().clean()
ip_vrf = self.cleaned_data.get('ip_vrf')
ip_address = self.cleaned_data.get('ip_address')
ip_status = self.cleaned_data.get('ip_status')
@@ -626,8 +630,7 @@ class FHRPGroupAssignmentForm(BootstrapMixin, forms.ModelForm):
class VLANGroupForm(CustomFieldModelForm):
scope_type = ContentTypeChoiceField(
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
required=False,
widget=StaticSelect
required=False
)
region = DynamicModelChoiceField(
queryset=Region.objects.all(),

View File

@@ -50,7 +50,7 @@ class Migration(migrations.Migration):
('status', models.CharField(default='active', max_length=50)),
('role', models.CharField(blank=True, max_length=50)),
('assigned_object_id', models.PositiveIntegerField(blank=True, null=True)),
('dns_name', models.CharField(blank=True, max_length=255, validators=[django.core.validators.RegexValidator(code='invalid', message='Only alphanumeric characters, hyphens, periods, and underscores are allowed in DNS names', regex='^[0-9A-Za-z._-]+$')])),
('dns_name', models.CharField(blank=True, max_length=255, validators=[django.core.validators.RegexValidator(code='invalid', message='Only alphanumeric characters, asterisks, hyphens, periods, and underscores are allowed in DNS names', regex='^([0-9A-Za-z_-]+|\\*)(\\.[0-9A-Za-z_-]+)*\\.?$')])),
('description', models.CharField(blank=True, max_length=200)),
],
options={

View File

@@ -32,6 +32,28 @@ __all__ = (
)
class GetAvailablePrefixesMixin:
def get_available_prefixes(self):
"""
Return all available Prefixes within this aggregate as an IPSet.
"""
prefix = netaddr.IPSet(self.prefix)
child_prefixes = netaddr.IPSet([child.prefix for child in self.get_child_prefixes()])
available_prefixes = prefix - child_prefixes
return available_prefixes
def get_first_available_prefix(self):
"""
Return the first available child prefix within the prefix (or None).
"""
available_prefixes = self.get_available_prefixes()
if not available_prefixes:
return None
return available_prefixes.iter_cidrs()[0]
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class RIR(OrganizationalModel):
"""
@@ -103,14 +125,33 @@ class ASN(PrimaryModel):
verbose_name_plural = 'ASNs'
def __str__(self):
return f'AS{self.asn}'
return f'AS{self.asn_with_asdot}'
def get_absolute_url(self):
return reverse('ipam:asn', args=[self.pk])
@property
def asn_asdot(self):
"""
Return ASDOT notation for AS numbers greater than 16 bits.
"""
if self.asn > 65535:
return f'{self.asn // 65536}.{self.asn % 65536}'
return self.asn
@property
def asn_with_asdot(self):
"""
Return both plain and ASDOT notation, where applicable.
"""
if self.asn > 65535:
return f'{self.asn} ({self.asn // 65536}.{self.asn % 65536})'
else:
return self.asn
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Aggregate(PrimaryModel):
class Aggregate(GetAvailablePrefixesMixin, PrimaryModel):
"""
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
the hierarchy and track the overall utilization of available address space. Each Aggregate is assigned to a RIR.
@@ -207,7 +248,7 @@ class Aggregate(PrimaryModel):
"""
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
utilization = int(float(child_prefixes.size) / self.prefix.size * 100)
utilization = float(child_prefixes.size) / self.prefix.size * 100
return min(utilization, 100)
@@ -245,7 +286,7 @@ class Role(OrganizationalModel):
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Prefix(PrimaryModel):
class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
"""
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role. A Prefix can also be
@@ -458,16 +499,6 @@ class Prefix(PrimaryModel):
else:
return IPAddress.objects.filter(address__net_host_contained=str(self.prefix), vrf=self.vrf)
def get_available_prefixes(self):
"""
Return all available Prefixes within this prefix as an IPSet.
"""
prefix = netaddr.IPSet(self.prefix)
child_prefixes = netaddr.IPSet([child.prefix for child in self.get_child_prefixes()])
available_prefixes = prefix - child_prefixes
return available_prefixes
def get_available_ips(self):
"""
Return all available IPs within this prefix as an IPSet.
@@ -494,15 +525,6 @@ class Prefix(PrimaryModel):
return available_ips
def get_first_available_prefix(self):
"""
Return the first available child prefix within the prefix (or None).
"""
available_prefixes = self.get_available_prefixes()
if not available_prefixes:
return None
return available_prefixes.iter_cidrs()[0]
def get_first_available_ip(self):
"""
Return the first available IP within the prefix (or None).
@@ -526,7 +548,7 @@ class Prefix(PrimaryModel):
vrf=self.vrf
)
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
utilization = int(float(child_prefixes.size) / self.prefix.size * 100)
utilization = float(child_prefixes.size) / self.prefix.size * 100
else:
# Compile an IPSet to avoid counting duplicate IPs
child_ips = netaddr.IPSet(
@@ -536,7 +558,7 @@ class Prefix(PrimaryModel):
prefix_size = self.prefix.size
if self.prefix.version == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
prefix_size -= 2
utilization = int(float(child_ips.size) / prefix_size * 100)
utilization = float(child_ips.size) / prefix_size * 100
return min(utilization, 100)

View File

@@ -27,8 +27,8 @@ class FHRPGroupTable(BaseTable):
orderable=False,
verbose_name='IP Addresses'
)
interface_count = tables.Column(
verbose_name='Interfaces'
member_count = tables.Column(
verbose_name='Members'
)
tags = TagColumn(
url_name='ipam:fhrpgroup_list'
@@ -37,16 +37,16 @@ class FHRPGroupTable(BaseTable):
class Meta(BaseTable.Meta):
model = FHRPGroup
fields = (
'pk', 'group_id', 'protocol', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'interface_count',
'tags',
'pk', 'group_id', 'protocol', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'member_count',
'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'group_id', 'protocol', 'auth_type', 'description', 'ip_addresses', 'interface_count')
default_columns = ('pk', 'group_id', 'protocol', 'auth_type', 'description', 'ip_addresses', 'member_count')
class FHRPGroupAssignmentTable(BaseTable):
pk = ToggleColumn()
interface_parent = tables.Column(
accessor=tables.A('interface.parent_object'),
accessor=tables.A('interface__parent_object'),
linkify=True,
orderable=False,
verbose_name='Parent'
@@ -60,7 +60,7 @@ class FHRPGroupAssignmentTable(BaseTable):
)
actions = ButtonsColumn(
model=FHRPGroupAssignment,
buttons=('edit', 'delete', 'foo')
buttons=('edit', 'delete')
)
class Meta(BaseTable.Meta):

View File

@@ -93,7 +93,10 @@ class RIRTable(BaseTable):
class Meta(BaseTable.Meta):
model = RIR
fields = ('pk', 'id', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'actions')
fields = (
'pk', 'id', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'actions', 'created',
'last_updated',
)
default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions')
@@ -106,17 +109,34 @@ class ASNTable(BaseTable):
asn = tables.Column(
linkify=True
)
asn_asdot = tables.Column(
accessor=tables.A('asn_asdot'),
linkify=True,
verbose_name='ASDOT'
)
site_count = LinkedCountColumn(
viewname='dcim:site_list',
url_params={'asn_id': 'pk'},
verbose_name='Site Count'
)
sites = tables.ManyToManyColumn(
linkify_item=True,
verbose_name='Sites'
)
tenant = TenantColumn()
tags = TagColumn(
url_name='ipam:asn_list'
)
actions = ButtonsColumn(ASN)
class Meta(BaseTable.Meta):
model = ASN
fields = ('pk', 'asn', 'rir', 'site_count', 'tenant', 'description', 'actions')
default_columns = ('pk', 'asn', 'rir', 'site_count', 'sites', 'tenant', 'actions')
fields = (
'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'tenant', 'description', 'sites', 'actions', 'created',
'last_updated', 'tags',
)
default_columns = ('pk', 'asn', 'rir', 'site_count', 'sites', 'description', 'tenant', 'actions')
#
@@ -147,7 +167,10 @@ class AggregateTable(BaseTable):
class Meta(BaseTable.Meta):
model = Aggregate
fields = ('pk', 'id', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description', 'tags')
fields = (
'pk', 'id', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description', 'tags',
'created', 'last_updated',
)
default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description')
@@ -165,6 +188,11 @@ class RoleTable(BaseTable):
url_params={'role_id': 'pk'},
verbose_name='Prefixes'
)
iprange_count = LinkedCountColumn(
viewname='ipam:iprange_list',
url_params={'role_id': 'pk'},
verbose_name='IP Ranges'
)
vlan_count = LinkedCountColumn(
viewname='ipam:vlan_list',
url_params={'role_id': 'pk'},
@@ -177,8 +205,11 @@ class RoleTable(BaseTable):
class Meta(BaseTable.Meta):
model = Role
fields = ('pk', 'id', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'tags', 'actions')
default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'actions')
fields = (
'pk', 'id', 'name', 'slug', 'prefix_count', 'iprange_count', 'vlan_count', 'description', 'weight', 'tags',
'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'prefix_count', 'iprange_count', 'vlan_count', 'description', 'actions')
#
@@ -264,8 +295,8 @@ class PrefixTable(BaseTable):
class Meta(BaseTable.Meta):
model = Prefix
fields = (
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan_group',
'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'tags',
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site',
'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description',
@@ -306,7 +337,7 @@ class IPRangeTable(BaseTable):
model = IPRange
fields = (
'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
'utilization', 'tags',
'utilization', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
@@ -341,7 +372,7 @@ class IPAddressTable(BaseTable):
verbose_name='Interface'
)
assigned_object_parent = tables.Column(
accessor='assigned_object.parent_object',
accessor='assigned_object__parent_object',
linkify=True,
orderable=False,
verbose_name='Device/VM'
@@ -364,7 +395,7 @@ class IPAddressTable(BaseTable):
model = IPAddress
fields = (
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'assigned', 'dns_name', 'description',
'tags',
'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',

View File

@@ -31,5 +31,8 @@ class ServiceTable(BaseTable):
class Meta(BaseTable.Meta):
model = Service
fields = ('pk', 'id', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'tags')
fields = (
'pk', 'id', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'tags', 'created',
'last_updated',
)
default_columns = ('pk', 'name', 'parent', 'protocol', 'ports', 'description')

View File

@@ -84,7 +84,10 @@ class VLANGroupTable(BaseTable):
class Meta(BaseTable.Meta):
model = VLANGroup
fields = ('pk', 'id', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'tags', 'actions')
fields = (
'pk', 'id', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'tags', 'actions',
'created', 'last_updated',
)
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions')
@@ -125,7 +128,10 @@ class VLANTable(BaseTable):
class Meta(BaseTable.Meta):
model = VLAN
fields = ('pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags')
fields = (
'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags',
'created', 'last_updated',
)
default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')
row_attrs = {
'class': lambda record: 'success' if not isinstance(record, VLAN) else '',

View File

@@ -47,7 +47,8 @@ class VRFTable(BaseTable):
class Meta(BaseTable.Meta):
model = VRF
fields = (
'pk', 'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'tags',
'pk', 'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets',
'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'rd', 'tenant', 'description')
@@ -68,5 +69,5 @@ class RouteTargetTable(BaseTable):
class Meta(BaseTable.Meta):
model = RouteTarget
fields = ('pk', 'id', 'name', 'tenant', 'description', 'tags')
fields = ('pk', 'id', 'name', 'tenant', 'description', 'tags', 'created', 'last_updated',)
default_columns = ('pk', 'name', 'tenant', 'description')

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