Compare commits

...

359 Commits

Author SHA1 Message Date
Jeremy Stretch
b55c85b2af Merge pull request #7220 from netbox-community/develop
Release v3.0.2
2021-09-08 16:45:05 -04:00
jeremystretch
0d1d14bcd6 Release v3.0.2 2021-09-08 16:32:37 -04:00
Jeremy Stretch
8c1a01d5ab Merge pull request #7218 from netbox-community/7162-base-path-bug2
Fixes #7162: Decouple base path rendering from API request logic
2021-09-08 16:19:49 -04:00
jeremystretch
cf8fdacfa3 Refactor connection toggle to use API URLs 2021-09-08 14:25:14 -04:00
jeremystretch
2c1745ce28 Refactor checkJobStatus() to use API URLs provided via context 2021-09-08 14:17:27 -04:00
jeremystretch
950ce94653 Refactor ApiRequest to take only full URLs; update TableConfigForm 2021-09-08 13:59:25 -04:00
thatmattlove
851f8a1585 Fixes #7191: Access SlimSelect's internal options when getting current options so selection state is maintained 2021-09-08 09:54:38 -07:00
jeremystretch
d40d1638af Fixes #7179: Prevent obscuring "connect" pop-up for interfaces under device view 2021-09-08 11:27:14 -04:00
Jeremy Stretch
26ceeb61ef Merge pull request #7206 from netbox-community/7205-applied-filters
Handle `null_option` in `applied_filters` template tag
2021-09-08 11:22:54 -04:00
Jeremy Stretch
a39a9c9b56 Merge branch 'develop' into 7205-applied-filters 2021-09-08 11:12:29 -04:00
jeremystretch
45988b9818 Minor cleanup for get_selected_values() 2021-09-08 11:11:52 -04:00
Jeremy Stretch
7234b3bbf8 Merge pull request #7204 from netbox-community/7202-verify-static-assets
Verify integrity of bundled assets in CI
2021-09-08 10:23:51 -04:00
Jeremy Stretch
513ecd7e26 Merge branch 'develop' into 7202-verify-static-assets 2021-09-08 10:13:07 -04:00
jeremystretch
e12314ba60 Fix test user permissions for API pagination tests 2021-09-08 09:57:53 -04:00
jeremystretch
9226302742 Fixes #7209: Allow unlimited API results when MAX_PAGE_SIZE is disabled 2021-09-08 09:38:23 -04:00
jeremystretch
0e8c6ee522 Changelog for #7189 2021-09-08 08:33:30 -04:00
thatmattlove
a9c1c8968e Return cleaned null_option value as None in dynamic multi-select field 2021-09-07 18:43:36 -07:00
thatmattlove
6a15c2ae86 Remove invalid (for Python <3.9) type annotation 2021-09-07 18:37:55 -07:00
thatmattlove
752de0d9c0 Fixes #7205: Handle null_option when getting selected form values in applied_filters template tag 2021-09-07 18:30:45 -07:00
thatmattlove
49617a595d #7205: Handle null_option in dynamic multi-select choices field 2021-09-07 18:28:49 -07:00
thatmattlove
2a293d5d02 Fixes #7188: Re-add missing support for null_option on API select 2021-09-07 18:19:32 -07:00
thatmattlove
9d99ede024 Fixes #7202: Verify integrity of bundled assets in CI 2021-09-07 16:20:36 -07:00
thatmattlove
4a13ee6f40 Fixes #7176: Reset query parameters on APISelect when deep copied 2021-09-07 14:13:53 -07:00
Jeremy Stretch
2ba840c72c Merge pull request #7200 from pierrechev/develop
Enable the alternate connection factory for Redis Sentinel
2021-09-07 16:58:11 -04:00
jeremystretch
46cd55151d Use shallow git clone for production installations 2021-09-07 16:35:48 -04:00
jeremystretch
4d9691c8e5 Extend feature request template to request more detail 2021-09-07 16:25:10 -04:00
jeremystretch
f1687ef53d Remove obsolete entries from .gitignore 2021-09-07 16:21:01 -04:00
Daniel Sheppard
2fb55374b9 Fixes #7193 - Fixes issue with viewing child prefixes when prefix (flat) column is selected and there are available prefixes. 2021-09-07 14:53:12 -05:00
Pierre Chevallereau
312246fec2 Enable the alternate connection factory (https://github.com/jazzband/django-redis#use-the-sentinel-connection-factory) 2021-09-07 16:07:25 +02:00
jeremystretch
27c0e6dd5e Fixes #7164: Fix styling of "decommissioned" label for circuits 2021-09-03 13:52:48 -04:00
jeremystretch
0d7986e082 Fixes #7169: Fix CSV import file upload 2021-09-03 13:46:38 -04:00
Jeremy Stretch
94300b221e Merge pull request #7160 from netbox-community/7149-secrets-changelog
Fixes #7149: Delete all changelog records referencing the old secrets app
2021-09-03 12:51:13 -04:00
jeremystretch
a1110b07de Fixes #7153: Allow clearing of assigned device type images 2021-09-02 16:48:54 -04:00
jeremystretch
a3069239e9 Fixes #7159: Remove NAPALM link 2021-09-02 15:55:41 -04:00
jeremystretch
69f083428d Fixes #7149: Delete all changelog records referencing the old secrets app 2021-09-02 15:27:05 -04:00
thatmattlove
113358f2de Fixes #7148: Handle array values when constructing API URLs 2021-09-02 08:59:26 -07:00
thatmattlove
caa2813d0d Fix regression from ddff193 causing invalid selections 2021-09-02 07:56:08 -07:00
Jeremy Stretch
481046c8b8 Merge pull request #7133 from slowy07/minor-changes
fix: typo spelling grammar
2021-09-02 09:48:58 -04:00
slowy07
83f70dc28c fix: typo spelling grammar
Signed-off-by: slowy07 <slowy.arfy@gmail.com>
2021-09-02 12:01:43 +07:00
thatmattlove
8ede7a9297 Update changelog for #7131 2021-09-01 17:05:50 -07:00
thatmattlove
ddff193786 #7123: Handle empty_option on API Select 2021-09-01 17:02:43 -07:00
thatmattlove
774dff07ee Fixes #7131: Only execute scope selector field visibility logic on specified views 2021-09-01 15:27:37 -07:00
thatmattlove
4b14b31853 Use url_name instead of request.path for view-based styles 2021-09-01 15:22:38 -07:00
jeremystretch
b0addfbe13 PRVB 2021-09-01 15:22:03 -04:00
Jeremy Stretch
593874b45f Merge pull request #7130 from netbox-community/develop
Release v3.0.1
2021-09-01 15:10:17 -04:00
jeremystretch
b207f28402 Release v3.0.1 2021-09-01 14:53:57 -04:00
thatmattlove
7bdde47473 Fixes #7124: Fix duplicate static query param values in API Select 2021-09-01 11:48:13 -07:00
thatmattlove
a2eb0d80d2 #7084: Fix issue where hidden VLAN form fields were incorrectly included in the form submission 2021-09-01 11:41:35 -07:00
jeremystretch
6f94198934 #7123: Remove "Global" placeholder for null VRF field 2021-09-01 13:52:32 -04:00
jeremystretch
707e51d855 #7084: Catch ValueErrors when initializing dynamic form fields 2021-09-01 13:33:41 -04:00
jeremystretch
528df76747 #7082: Handle stale content types 2021-09-01 12:55:25 -04:00
jeremystretch
662c896480 #7113: Add bulk actions under child prefixes view; general cleanup 2021-09-01 11:06:50 -04:00
Jeremy Stretch
29eb2383d6 Merge pull request #7115 from sdktr/7113-fix-prefix-iprange-bulkedit
Fixes #7113: iprange bulk options within Prefix view
2021-09-01 10:56:11 -04:00
Jeremy Stretch
9772c5705f Merge branch 'develop' into 7113-fix-prefix-iprange-bulkedit 2021-09-01 10:55:52 -04:00
jeremystretch
d2fe59ae8f Fixes #7109: Ensure human readability of exceptions raised during REST API requests 2021-09-01 10:43:12 -04:00
jeremystretch
f63dcb1f08 #7091: Simplify access to BASE_PATH variable 2021-09-01 09:34:33 -04:00
Stefan de Kooter
6f66b27507 Changelog for #7113 2021-09-01 11:00:30 +02:00
Stefan de Kooter
909d127c27 Fixes #7113: Correct links to IPRanges bulk actions within Prefix view 2021-09-01 10:25:37 +02:00
Stefan de Kooter
20ef18f98f Fixes #7113: Add permissions to IPRanges bulk actions within Prefix view 2021-09-01 10:24:35 +02:00
thatmattlove
a33e47780b Remove legacy script tags from templates 2021-09-01 00:27:10 -07:00
thatmattlove
691c66d2f5 Fixes #7107: Fix missing search button and search results in IP Address assignment "Assign IP" tab 2021-09-01 00:11:48 -07:00
thatmattlove
14d87a3584 Fixes #7041: Properly format JSON config object returned from a NAPALM device 2021-09-01 00:03:53 -07:00
thatmattlove
d743dc160a Fixes #7080: Re-add missing image preview element 2021-08-31 17:05:02 -07:00
thatmattlove
2b263b054c Fixes #7106: Fix incorrect "Map It" button URL on a site's Physical Address field 2021-08-31 16:27:02 -07:00
thatmattlove
b95e8350d2 Fixes #7092: Fix missing object permissions on Prefix IP Addresses view 2021-08-31 16:03:22 -07:00
thatmattlove
5235866d05 Changelog for #7081, #7091 2021-08-31 15:23:12 -07:00
thatmattlove
093a86bc38 Fixes #7081: Properly handle pre-selected values even when they're outside of pagination limits 2021-08-31 15:13:53 -07:00
thatmattlove
5b87232f59 #7081: Fix APISelect loading of paginated data 2021-08-31 15:13:53 -07:00
thatmattlove
679bbd3e76 Fixes #7091: Ensure API requests from the UI are aware of BASE_PATH 2021-08-31 15:13:53 -07:00
Jeremy Stretch
515b6bf71a Merge pull request #7105 from sdktr/7090-fix-cablebulkedit-length-field
Fix #7090: cable bulk edit form - allow decimal input on length field
2021-08-31 16:12:04 -04:00
Stefan de Kooter
9c389d9dcb Changelog #7090 fix whitespace 2021-08-31 22:01:15 +02:00
Stefan de Kooter
f1e4273a23 Changelog for #7090 2021-08-31 21:24:07 +02:00
Stefan de Kooter
4618cc2b22 Merge branch 'develop' of github.com:netbox-community/netbox into 7090-fix-cablebulkedit-length-field 2021-08-31 21:18:35 +02:00
Stefan de Kooter
1909f0c733 Fix #7090: Cable Bulk Edit, length field should be decimal 2021-08-31 21:17:50 +02:00
Jeremy Stretch
840ea36f70 Merge pull request #7103 from candlerb/candlerb/7102
Redirect users on error to the GitHub discussion forum
2021-08-31 15:08:08 -04:00
jeremystretch
a8cdb3895b Fixes #7093: Multi-select custom field filters should employ exact match 2021-08-31 15:03:39 -04:00
Brian Candler
349733c6dd Redirect users on error to the GitHub discussion forum
Fixes #7102
2021-08-31 19:51:53 +01:00
jeremystretch
1c09ffdd1f Fixes #7101: Enforce MAX_PAGE_SIZE for table and REST API pagination 2021-08-31 13:52:04 -04:00
Daniel Sheppard
c4c6fa6042 Fix misplacement of method for #7089 2021-08-31 11:41:50 -05:00
Daniel Sheppard
86da6c6c14 Fixes #7089 - Adds Q filter to ContentTypeFilterSet 2021-08-31 11:31:40 -05:00
jeremystretch
7b7b01a26b Changelog for #7075 2021-08-31 11:44:51 -04:00
jeremystretch
415313ac2f Fixes #7082: Avoid exception when referencing invalid content type in table 2021-08-31 11:43:44 -04:00
jeremystretch
7db2b9d091 Fixes #7072: Fix table configuration under prefix child object views 2021-08-31 11:15:41 -04:00
jeremystretch
8036d1e5a5 Fixes #7078: Restore styling on server error page 2021-08-31 09:50:24 -04:00
jeremystretch
65c9339687 Fixes #7083: Correct labeling for VM memory attribute 2021-08-31 09:44:59 -04:00
jeremystretch
3090981335 Fixes #7084: Fix KeyError exception when editing access VLAN on an interface 2021-08-31 09:44:59 -04:00
jeremystretch
4f36885c5e Fixes #7096: Home links should honor BASE_PATH configuration 2021-08-31 09:44:59 -04:00
thatmattlove
db2993035d Fixes #7075: Wrap label selectors in quotes to ensure IDs with spaces are properly selected 2021-08-30 17:48:33 -07:00
jeremystretch
bf05bc2986 #7070: Fix filterset test 2021-08-30 17:22:48 -04:00
jeremystretch
88b230f0e4 Fixes #7071: Fix exception when removing a primary IP from a device/VM 2021-08-30 16:55:31 -04:00
jeremystretch
deb53d771d Fixes #7070: Fix exception when filtering by prefix max length in UI 2021-08-30 16:51:07 -04:00
Jeremy Stretch
fd16c47d2e Merge pull request #7069 from netbox-community/develop
Release v3.0.0
2021-08-30 14:43:47 -04:00
jeremystretch
b78451742f Release v3.0.0 2021-08-30 14:22:00 -04:00
jeremystretch
6f23ab5603 Better copy/paste support for installation docs 2021-08-30 14:15:21 -04:00
thatmattlove
5e67627e6b Fix file input font-size 2021-08-30 11:09:44 -07:00
thatmattlove
19e77ed456 Pin Bootstrap 5 to 5.0.2 2021-08-30 11:02:47 -07:00
thatmattlove
ed0f792f04 Fixes #7068: Disable sourcemaps on CSS files, use external sourcemaps 2021-08-30 10:56:02 -07:00
thatmattlove
deda1691e9 Fixes #7066: Migrate division statements in Sass from / to math.div 2021-08-30 09:54:58 -07:00
thatmattlove
94d2ad120c Fixes #7066: Resolve dependency issue between TypeScript/ESLint 2021-08-30 09:54:06 -07:00
jeremystretch
ab1a5f32ef Update references to NAPALM in docs 2021-08-30 11:51:18 -04:00
jeremystretch
2a1de5e28c Delete extraneous v2.11 release notes 2021-08-30 11:35:06 -04:00
thatmattlove
f78fdd6900 Fixes #7063: Update security dependencies, move esbuild to devDependencies, update clipboard 2021-08-30 08:14:24 -07:00
jeremystretch
6b43eafcb4 Update UI screenshots 2021-08-30 10:52:11 -04:00
jeremystretch
844cd154b9 Update dependencies & release notes 2021-08-30 10:30:48 -04:00
Jeremy Stretch
e05fa5c302 Merge pull request #7061 from netbox-community/feature
v3.0 release prep
2021-08-30 10:16:56 -04:00
jeremystretch
f5f74944dd Merge branch 'develop' into feature 2021-08-30 10:05:12 -04:00
jeremystretch
556efcc1d7 Fixes #7045: Fix navigation menu rendering under Chrome 2021-08-30 09:56:05 -04:00
Matt Love
25d1fe2c8d Improve APISelect query parameter handling (#7040)
* Fixes #7035: Refactor APISelect query_param logic

* Add filter_fields to extras.ObjectVar & fix default value handling

* Update ObjectVar docs to reflect new filter_fields attribute

* Revert changes from 89b7f3f

* Maintain current `query_params` API for form fields, transform data structure in widget

* Revert changes from d0208d4
2021-08-30 09:43:32 -04:00
Jeremy Stretch
1a478150d6 Merge pull request #7050 from netbox-community/7034-vlangroup-scope-selectors
Fixes #7034: Update VLAN Scope parent selectors and run change handler on load
2021-08-27 11:42:23 -04:00
jeremystretch
e5643fb1e2 JS & changelog updates for #7034 2021-08-27 11:36:29 -04:00
jeremystretch
13e633778a Closes #7042: Show count of journal entries in tab under object view 2021-08-27 10:36:06 -04:00
jeremystretch
bb57600f0f Fixes #7019: Enable searching VM interfaces by description 2021-08-27 10:14:12 -04:00
jeremystretch
9813f3b696 Clean up custom script templates 2021-08-26 15:04:24 -04:00
jeremystretch
3203db07b7 UI cleanup 2021-08-26 14:48:24 -04:00
jeremystretch
94b8d36065 Introduce ContentTypesColumn for custom field and webhook tables 2021-08-26 12:55:37 -04:00
thatmattlove
0d61dcb1bc Fixes #7034: Update VLAN Scope parent selectors and run change handler on load 2021-08-26 00:11:58 -07:00
jeremystretch
58203dbcfa List device/VM component names first in tables by default 2021-08-25 15:18:00 -04:00
jeremystretch
66619cdc2f Clean up object edit forms 2021-08-25 15:03:19 -04:00
jeremystretch
99cba25108 Misc UI cleanup ahead of v3.0 release 2021-08-25 13:50:59 -04:00
jeremystretch
2fb1d388e3 Omit node 15.x from CI builds 2021-08-24 21:32:28 -04:00
Jeremy Stretch
6a4ed099fc Merge pull request #7031 from netbox-community/object-filter-forms
Object filter forms
2021-08-24 21:31:25 -04:00
jeremystretch
d184ed4712 Enable filtering device components by location 2021-08-24 21:10:30 -04:00
jeremystretch
125a562189 Fix RegionFilterForm model 2021-08-24 20:44:00 -04:00
Matt
a02ba5f7bb Fix incorrect classes in device config & status templates 2021-08-24 14:53:36 -07:00
Matt
2e90f22529 Clean up TypeScript file structure, fix missing VLAN tag visibility logic 2021-08-24 14:53:36 -07:00
jeremystretch
bd681f5908 Clean up object filter forms 2021-08-24 17:29:16 -04:00
jeremystretch
85b61c0b7e Bump django-timezone-field to 4.2.1 2021-08-24 15:52:04 -04:00
jeremystretch
d11ea67bdd Update design of user profile section 2021-08-24 15:24:03 -04:00
jeremystretch
52603c087b Remove unnecessary component creation templates 2021-08-24 14:51:12 -04:00
jeremystretch
545474a1a3 Clean up object edit forms 2021-08-24 13:59:54 -04:00
Jeremy Stretch
b63c838c74 Merge pull request #7024 from netbox-community/feature-precommit-ui
Run UI Lint, Type, and Formatting Checks in Pre-Commit and CI
2021-08-24 12:48:20 -04:00
Matt
1c6fdea27f Improve docs styling 2021-08-24 07:30:52 -07:00
Matt
9d0e6f0c30 Exclude node_modules from CI build 2021-08-24 06:40:00 -07:00
Matt
1d0c72f5fa Add UI checks to pre-commit and CI 2021-08-24 00:41:10 -07:00
Matt
c221b9b4d4 Add UI development docs & update front-end scripts 2021-08-24 00:30:04 -07:00
Matt
a0ba8380c9 Fix eslint misconfiguration and corresponding errors 2021-08-24 00:27:45 -07:00
Matt
82a209bc5b Remove screenshots from docs 2021-08-23 18:24:39 -07:00
jeremystretch
2a338110f2 Remove unused aggregate list template 2021-08-23 16:53:06 -04:00
jeremystretch
e890944160 Use badge template tag for numeric values 2021-08-23 16:47:08 -04:00
jeremystretch
542e01775e Merge branch 'develop' into feature 2021-08-23 15:46:22 -04:00
Jeremy Stretch
9cc4992fad Merge pull request #7018 from netbox-community/develop
Release v2.11.12
2021-08-23 15:36:04 -04:00
jeremystretch
6518d87200 Release v2.11.12 2021-08-23 15:16:42 -04:00
jeremystretch
499005f84d Merge branch 'develop' into feature 2021-08-23 13:23:39 -04:00
jeremystretch
8497965cf7 Fixes #6326: Enable filtering assigned VLANs by group in interface edit form 2021-08-23 12:49:32 -04:00
jeremystretch
0b0ab9277c Fixes #6776: Fix erroneous webhook dispatch on failure to save objects 2021-08-23 12:06:43 -04:00
jeremystretch
75c62ff729 Print request index after webhook data dump 2021-08-23 11:32:47 -04:00
Jeremy Stretch
aef8c5fbb5 Merge pull request #6965 from bluikko/poweroutlet-hardwired
Add hardwired PowerOutlet
2021-08-23 10:04:28 -04:00
jeremystretch
cfa4f5677b Fixes #7012: Fix hidden "add components" dropdown on devices list 2021-08-23 09:41:43 -04:00
jeremystretch
8131feae8a Closes #7011: Add search field to VM interfaces filter form 2021-08-23 09:36:05 -04:00
Matt
a3d5e04946 Fixes #6990: Fix query param and query filter handling in API select 2021-08-20 16:25:31 -07:00
jeremystretch
1fc3c6d9d2 Fixes #6974: Show contextual label for IP address role 2021-08-20 16:12:09 -04:00
jeremystretch
53a5bc2221 Fixes #6929: Introduce LOGIN_PERSISTENCE configuration parameter to persist user sessions 2021-08-20 16:06:37 -04:00
Matt
12f3c2596f Fixes #6966: Migrate to stock fonts 2021-08-20 12:57:41 -07:00
jeremystretch
87dad41c37 Tweak logo size on mobile 2021-08-20 15:21:26 -04:00
jeremystretch
4dbb18d408 Update change log 2021-08-20 15:20:55 -04:00
Matt
a7cb75d73d Fixes #6999: Properly align controls on sm and md breakpoints 2021-08-20 12:15:07 -07:00
Matt
517c0e2fe6 Fixes #6996: Make search bar full width on small screens 2021-08-20 08:54:00 -07:00
Matt
84db2e90ab Fixes #6998: Properly handle merge and replace actions in API Select 2021-08-20 08:41:30 -07:00
Matt
9d469874c0 #6881: Improve device IP address styles 2021-08-20 08:06:41 -07:00
jeremystretch
d850aa0773 Changelog for #6790 2021-08-20 09:17:53 -04:00
Jeremy Stretch
9baebfa241 Merge pull request #6790 from WillIrvine/issue-6632
Fixes #6632  - Allow a /32 prefix to contain a /32 ipaddress
2021-08-20 09:16:05 -04:00
Matt
9e1d2da449 Fixes #7001: Focus the main content container when the page loads 2021-08-19 14:13:54 -07:00
Matt
a71604e79f Closes #6881: Wrap interface IP addresses in a badge that displays status and/or role 2021-08-19 12:35:30 -07:00
Matt
9a8d33e6bf Fixes #6979: Don't show 'Create & Add Another' button when editing/creating a circuit 2021-08-18 16:49:15 -07:00
Matt
09d745d987 Fixes #6976: Improve handling of printing layouts/styling 2021-08-18 16:17:50 -07:00
Matt
8199bb6b62 Fixes: #6982: Remove inherited background-color on disabled options 2021-08-18 14:57:42 -07:00
Matt
643939ea1e Rebundle scripts after rebase 2021-08-18 14:53:28 -07:00
Matt
9b3498d87a Add visibility toggle for object depth indicators 2021-08-18 14:51:49 -07:00
Matt
e4a162b054 Improve prefix hierarchy/depth styling 2021-08-18 14:51:36 -07:00
jeremystretch
bd47d0850e Changelog for #6856 2021-08-18 14:38:30 -04:00
jeremystretch
be3b4f0d3e #6856: Remove ?limit=0 from API queries 2021-08-18 14:35:12 -04:00
Matt
664b02d735 Fixes #6856: Properly handle existence of next property in API select responses 2021-08-17 16:50:29 -07:00
jeremystretch
6d1b981ecb Closes #6975: Reduce footer height 2021-08-17 12:02:30 -04:00
jeremystretch
ac6b1bf422 Fixes #6977: Truncate global search dropdown on small screens 2021-08-17 11:49:32 -04:00
jeremystretch
10847e2956 Optimize addition/removal of default custom field values 2021-08-16 14:48:56 -04:00
jeremystretch
9b0258fef4 Fixes #6686: Force assignment of null custom field values to objects 2021-08-16 14:38:06 -04:00
jeremystretch
5b89cdc868 Fixes #5968: Model forms should save empty custom field values as null 2021-08-16 13:45:46 -04:00
bluikko
5a8cedd63f Add hardwired PowerOutlet 2021-08-16 11:30:13 +07:00
jeremystretch
3feba2997f Closes #6872: Add table configuration button to child prefixes view 2021-08-13 15:56:14 -04:00
jeremystretch
fce419526d Closes #6748: Add site group filter to devices list 2021-08-13 15:26:06 -04:00
jeremystretch
e8fb86a283 Release v3.0-beta2 2021-08-13 14:19:43 -04:00
jeremystretch
90a820e0cf Add "clear all" option for applied filters 2021-08-13 13:50:11 -04:00
jeremystretch
9f59f99663 Set max width for object edit forms 2021-08-13 13:35:23 -04:00
jeremystretch
a6150f2578 Remove select widget hover effect 2021-08-13 11:31:51 -04:00
jeremystretch
b784705cd3 Tweak nav submenu heading color 2021-08-13 11:20:00 -04:00
jeremystretch
0609bcaaf0 Reduce base font size 2021-08-13 11:17:37 -04:00
jeremystretch
7727ec91f4 #6934: Correct prefix utilization and available IP reporting to account for child IP ranges 2021-08-13 10:43:25 -04:00
jeremystretch
5365c866ff #6934: Account for child IP ranges when calculating prefix utilization 2021-08-13 10:33:58 -04:00
jeremystretch
e1fbe89b41 Reduce form text size 2021-08-13 09:56:06 -04:00
jeremystretch
a72e23eddf Fix custom script layout 2021-08-13 09:43:23 -04:00
jeremystretch
dcd49fd97b Fixes #6953: Remove change log tab from non-applicable object views 2021-08-13 09:13:09 -04:00
Jeremy Stretch
1b074d2d53 Merge pull request #6933 from netbox-community/6912-static-resources
Fixes #6912: Fix static asset references when BASE_PATH is in use
2021-08-12 15:15:18 -04:00
jeremystretch
aed07a8ec5 Merge v2.11.11 2021-08-12 11:51:04 -04:00
jeremystretch
1b12185a39 PRVB 2021-08-12 11:47:59 -04:00
Jeremy Stretch
2e895c734e Merge pull request #6947 from netbox-community/develop
Release v2.11.11
2021-08-12 11:44:51 -04:00
Jeremy Stretch
11a9dc57fc Merge branch 'master' into develop 2021-08-12 11:31:29 -04:00
jeremystretch
badd92a50e Update GitHub issue templates 2021-08-12 11:28:55 -04:00
jeremystretch
b2faf8044d Release v2.11.11 2021-08-12 11:22:57 -04:00
jeremystretch
3105e9545a Fixes #6918: Fix return URL persistence when adding multiple objects sequentially 2021-08-12 10:12:42 -04:00
jeremystretch
42c71984f9 Fixes #6896: Fix validation of IP address assigned as device/VM primary via NAT relation 2021-08-11 21:15:45 -04:00
jeremystretch
736da4bcad Merge branch 'develop' into feature 2021-08-10 21:03:10 -04:00
jeremystretch
db359719a9 Closes #6921: Employ a sandbox when rendering Jinja2 code for increased security 2021-08-10 20:52:45 -04:00
jeremystretch
7bceeb714b Fixes #6935: Remove extraneous columns from inventory item and device bay tables 2021-08-10 20:35:39 -04:00
jeremystretch
35b8fc6e83 Fixes #6936: Add missing parent column to inventory item import form 2021-08-10 20:24:57 -04:00
jeremystretch
6d27e11043 #6934: Include child IP ranges under prefix view 2021-08-10 16:26:14 -04:00
jeremystretch
b8e387ce98 #6912: Remove absolute publicPath reference 2021-08-10 14:03:07 -04:00
jeremystretch
c7ebad0fbb Closes #6931: Include applied filters on object list view 2021-08-10 13:11:35 -04:00
jeremystretch
1bb596fc7e Fixes #6908: Allow assignment of scope to VLAN groups upon import 2021-08-09 09:54:27 -04:00
jeremystretch
7bcebd5b0f Fixes #6910: Fix exception on invalid CSV import column name 2021-08-09 09:20:22 -04:00
jeremystretch
a8b6902829 Fixes #6909: Remove extraneous site column from VLAN group import form 2021-08-09 09:17:08 -04:00
Jeremy Stretch
71e6dc8275 Merge pull request #6920 from candlerb/candlerb/6919
Change example ADMINS to show a tuple
2021-08-09 08:56:44 -04:00
Jeremy Stretch
564640213e Merge pull request #6915 from candlerb/candlerb/libpq-dev
Documentation consistency on installation of libpq-dev(el)
2021-08-09 08:51:54 -04:00
Brian Candler
b04f262642 Change example ADMINS to show a tuple
Fixes #6919
2021-08-09 07:37:46 +01:00
Brian Candler
b802127801 Documentation consistency on installation of libpq-dev(el) 2021-08-08 10:19:30 +01:00
Matt
6845fb0f00 Improve object view on small screens 2021-08-06 17:56:38 -07:00
Matt
a312311be9 Improve sidenav link styles 2021-08-06 17:46:49 -07:00
Jeremy Stretch
fe54acef51 v3.0 nav menu tweaks (#6906)
* Clean up nav menu spacing & link colors

* Shrink NetBox icon & collapsed sidebar

* Fix gap between scrollbar and righthand window border
2021-08-06 17:43:02 -04:00
jeremystretch
ef057b3e45 Fix footer fonts 2021-08-06 16:49:17 -04:00
jeremystretch
84ab233571 Fix wrapping of table controls on device interfaces view 2021-08-06 16:40:00 -04:00
jeremystretch
cf381d732d Use red border for confirmation dialog 2021-08-06 16:19:21 -04:00
jeremystretch
8653b0f3d0 Tabify object add/edit views 2021-08-06 16:16:19 -04:00
jeremystretch
65659fb676 Badges use secondary BG by default; add custom option 2021-08-06 15:41:26 -04:00
Jeremy Stretch
939bcfec4b Improve object list layout (#6907)
* Split object list and filters into tabs

* Use object_list template for connections, rack elevations

* Include custom field filters in grouped filter form

* Annotate number of applied filters on tab

* Rearrange table controls
2021-08-06 15:35:14 -04:00
jeremystretch
6ce8dd5ac3 Closes #6823: Improve table configuration form layout 2021-08-06 12:46:57 -04:00
jeremystretch
63f4d81bc0 Remove errant buttons block from cable view 2021-08-06 12:33:21 -04:00
jeremystretch
d0fbbbfb37 Merge branch 'develop' into feature 2021-08-06 10:06:52 -04:00
jeremystretch
f23dc2d405 Fixes #6902: Populate device field when cloning device components 2021-08-06 09:55:47 -04:00
jeremystretch
34aa231436 Closes #6899: Add filterset tests for Token 2021-08-06 09:41:49 -04:00
jeremystretch
51d1b6e0d6 Fixes #6901: Correct example REST API request 2021-08-06 08:39:57 -04:00
jeremystretch
7c8612aadd Update application architecture diagram 2021-08-05 15:51:24 -04:00
Joel McGuire
d347b97f20 Fixes #6887 Add Examples in the Lookup Expression Docs (#6898)
Fixes #6887 Add Examples in the Lookup Expression Docs

Co-authored-by: joel <joelmcguire@email.arizona.edu>
2021-08-05 13:28:32 -04:00
jeremystretch
42b961229f Fixes #6894: Fix available IP generation for prefix assigned to a VRF 2021-08-05 13:23:14 -04:00
Matt
79f726e6cd #6797: Fix various mobile layout issues 2021-08-05 09:59:13 -07:00
Matt
31cd6898d4 #6797: Fix search result layout on small screens when there are no results 2021-08-05 09:40:02 -07:00
Matt
7608ee8450 #6797: Fix initial sidenav handling on smaller screens 2021-08-05 09:35:36 -07:00
Matt
da67a35328 #6797: Automatically collapse inactive sections in the sidenav 2021-08-05 09:28:25 -07:00
jeremystretch
46d0af6cef Fixes #6892: Fix validation of unit ranges when creating a rack reservation 2021-08-05 11:12:08 -04:00
Matt
0ea9c65007 Add common Bootstrap components to window so they can be consumed by plugins 2021-08-04 23:46:34 -07:00
jeremystretch
57dc4c207f Fixes #6832: Support config context rendering under GraphQL API 2021-08-04 15:55:55 -04:00
Matt
582b69de74 #6797: Improve object edit form field layout 2021-08-04 10:57:01 -07:00
Matt
0cf9be2a8d Remove deprecated advanced search template 2021-08-04 10:41:43 -07:00
Matt
0bf39590e3 #6797: Fix object list layout when there is no filter form 2021-08-04 10:40:39 -07:00
Matt
2debeb7475 #6797: Fix empty filter panels 2021-08-04 10:37:59 -07:00
jeremystretch
ee8fd701ae Changelog for #6883 2021-08-04 13:26:53 -04:00
Jeremy Stretch
9379324b07 Merge pull request #6885 from bellwood/6883_add_power_outlet_port_c21_c22
Add power outlet/port choice for C21/C22
2021-08-04 12:59:21 -04:00
Brian Ellwood
55cdbd57cc Add power outlet/port choice for C21/C22
Resolves #6883
2021-08-04 12:06:39 -04:00
jeremystretch
11836cdfb1 Fixes #6871: Support dynamic tag types in GraphQL API 2021-08-03 16:29:34 -04:00
Jeremy Stretch
c411d2a9f1 Merge pull request #6873 from netbox-community/6829-graphql-reverse-relations
Closes #6829: GraphQL reverse generic relations
2021-08-03 16:22:45 -04:00
jeremystretch
1b612816cc Merge branch 'feature' into 6829-graphql-reverse-relations 2021-08-03 16:05:31 -04:00
Matt
051abc00c4 Fix bulk_import button class in test view after naming change in e8ba4b0 2021-08-03 12:43:28 -07:00
Matt
f7ee5e8d78 Fix button class in test view after naming change in e8ba4b0 2021-08-03 12:06:13 -07:00
jeremystretch
cc26bc4858 Changelog for #6829 2021-08-03 14:56:22 -04:00
jeremystretch
88d2441ab3 Add changelog GraphQL relation for changelogged models 2021-08-03 14:51:56 -04:00
Matt
6842879985 #6797: Improve object view styling & responsiveness 2021-08-03 11:41:46 -07:00
jeremystretch
1518a460d5 Rename base Graphene types to match base models 2021-08-03 14:37:39 -04:00
jeremystretch
ea86321da8 Add journal_entries to Graphene object types for all primary models 2021-08-03 13:58:08 -04:00
jeremystretch
c416fce400 Refactor base Graphene object types 2021-08-03 13:49:12 -04:00
Matt
ae28df8abd #6797: Place custom links below native controls 2021-08-03 10:25:27 -07:00
Matt
e8ba4b0564 #6797: Improve controls & custom link styling 2021-08-03 10:21:06 -07:00
Matt
53e21ceed4 #6797: Improve global search styles 2021-08-03 09:19:24 -07:00
jeremystretch
735286d3b0 Add vlan_groups to Region, SiteGroup, Site, Location, Rack, ClusterGroup, Cluster 2021-08-03 11:49:22 -04:00
jeremystretch
8ad958708f Add image_attachments to Device, Location, Rack, Site 2021-08-03 11:38:18 -04:00
Matt
58862e115c Closes #6863: Add search fields back to filter forms 2021-08-03 08:32:53 -07:00
jeremystretch
0df67dbc12 Add ip_addresses relation on InterfaceType, VMInterfaceType 2021-08-03 11:27:14 -04:00
thatmattlove
8bdfa34c7d Merge branch 'feature-object-filter' into feature 2021-08-03 06:57:54 -07:00
thatmattlove
06c730f4dc Merge branch 'feature' into feature-object-filter
# Conflicts:
#	netbox/project-static/dist/netbox-dark.css
#	netbox/project-static/dist/netbox-light.css
#	netbox/project-static/styles/netbox.scss
#	netbox/project-static/styles/select.scss
2021-08-03 06:57:22 -07:00
checktheroads
afc8d5bbbf Fix PEP8 formatting error 2021-08-02 02:31:30 -07:00
checktheroads
1de46f592c Various styling improvements 2021-08-02 02:18:31 -07:00
checktheroads
863048cda2 Deprecate collapsible advanced search and re-implement field-based filtering on object views 2021-08-01 21:24:22 -07:00
checktheroads
0b09365d0d #6797: Improve form error/django messages handling 2021-08-01 13:30:16 -07:00
checktheroads
8e3ab8d5c5 #6797: Improve global bg/color transition 2021-08-01 12:01:40 -07:00
checktheroads
9cf560ceec #6797: Improve table highlight, toast, and alert styling 2021-08-01 11:53:35 -07:00
checktheroads
c3a75d98d4 #6797: Improve sidenav state handling before load 2021-08-01 11:12:07 -07:00
checktheroads
261372289a #6797: Fix sidenav jumpy/glitchy behavior on page reload when pinned 2021-08-01 00:27:27 -07:00
checktheroads
b86edd4a20 #6797: Improve sidenav parent link color 2021-08-01 00:02:20 -07:00
checktheroads
374cf146e2 #6797: Fix login page layout issue 2021-07-31 23:56:56 -07:00
checktheroads
08ed545065 Closes #6855: Bundle and locally serve GraphiQL JS/CSS 2021-07-31 23:49:48 -07:00
checktheroads
80836c725c Fix navigation_menu typing & dataclass property defaults 2021-07-31 22:16:04 -07:00
Jeremy Stretch
9fa2acfe85 Merge pull request #6847 from netbox-community/6834-graphiql-ui
Closes #6834: Customize GraphiQL view
2021-07-30 15:23:59 -04:00
jeremystretch
3ba122afd4 Merge feature 2021-07-30 15:13:55 -04:00
jeremystretch
76df55dfc0 Fixes #6740: Add import button to VM interfaces list 2021-07-30 10:28:56 -04:00
Jeremy Stretch
49a949aa97 Merge pull request #6836 from Ursadon/patch-1
Escaping angle brackets in a device config file
2021-07-30 10:09:04 -04:00
checktheroads
5413263eff #6797: Properly update API select query parameters when values already exist on the element 2021-07-30 01:25:29 -07:00
checktheroads
772c76e0a4 #6797: Don't show depth indicator in API select placeholder 2021-07-30 01:03:26 -07:00
checktheroads
5463fa7390 Closes #6808: Determine option disabled status via disabled-indicator attribute 2021-07-30 00:56:54 -07:00
checktheroads
d18c83beb0 #6828: Fix various mobile UI issues 2021-07-30 00:35:38 -07:00
checktheroads
7aa89c2e73 #6797: Fix new sidenav styles 2021-07-29 18:11:48 -07:00
checktheroads
007d660ce1 Merge branch 'feature-sidebar' into feature
# Conflicts:
#	netbox/project-static/dist/netbox.js
#	netbox/project-static/dist/netbox.js.map
2021-07-29 17:39:07 -07:00
checktheroads
3752cb3e56 #6797: Implement new sidebar 2021-07-29 17:33:10 -07:00
jeremystretch
cdf8d91e1b #6797: Fit device type images to available space 2021-07-29 15:19:42 -04:00
jeremystretch
d082442851 Update REST API web UI title 2021-07-29 15:06:09 -04:00
jeremystretch
689f67b1a8 #6834: Add favicon to REST API web UI 2021-07-29 15:02:52 -04:00
jeremystretch
744f47cb98 Fixes #6846: Form-driven REST API calls should use brief mode 2021-07-29 14:50:30 -04:00
jeremystretch
81e1b7490e #6834: Add title, favicon to GraphiQL view 2021-07-29 13:48:06 -04:00
jeremystretch
22d160b1da Fix display of circuit termination provider network 2021-07-29 11:14:12 -04:00
jeremystretch
c323105696 Fixes #6827: Restore circuit termination connection dropdown 2021-07-29 11:08:15 -04:00
jeremystretch
f6746c7530 Clean up cable connection form 2021-07-29 10:48:12 -04:00
jeremystretch
52c4d54481 Clean up cable trace view 2021-07-29 10:08:43 -04:00
jeremystretch
4c3f584fa6 Fix trace component borders 2021-07-29 09:59:01 -04:00
jeremystretch
2e7d912bdd #6797: Add cable type, length to SVG trace 2021-07-29 09:49:31 -04:00
jeremystretch
288bf477ce Bump GitHub stale action to v4.0 2021-07-29 09:07:23 -04:00
Ursadon
27f3816fc6 Escaping angle brackets in a device config file
The configuration file may contain brackets (">" or "<"), which must be escaped
2021-07-29 15:45:32 +07:00
jeremystretch
33d40d4253 #6797: Improve utilization graph display for small values 2021-07-28 16:42:44 -04:00
jeremystretch
c7e0abc3fb Merge v2.11.10 2021-07-28 16:26:04 -04:00
jeremystretch
18a4232783 PRVB 2021-07-28 16:00:38 -04:00
Jeremy Stretch
15ed575207 Merge pull request #6830 from netbox-community/develop
Release v2.11.10
2021-07-28 15:56:23 -04:00
jeremystretch
eae4502708 Release v2.11.10 2021-07-28 15:17:45 -04:00
jeremystretch
78ebf04be0 Shrink NetBox logo on docs main page 2021-07-28 15:12:17 -04:00
jeremystretch
49a596073e Tweak GitHub repo icon & name in docs 2021-07-28 15:07:46 -04:00
jeremystretch
95783cc128 Closes #6644: Add 6P/4P pass-through port types 2021-07-28 11:54:25 -04:00
jeremystretch
8d9d3a9e7d Changelog and cleanup for #6560 2021-07-28 11:44:13 -04:00
Jeremy Stretch
ea0de4b01d Merge pull request #6561 from abigley/csv_feature
CSV file import
2021-07-28 10:48:30 -04:00
jeremystretch
72aaf76cf4 Closes #6702: Update reference nginx config to support IPv6 2021-07-28 10:31:59 -04:00
jeremystretch
78e282d406 Fixes #6771: Add count of inventory items to manufacturer view 2021-07-28 10:25:52 -04:00
jeremystretch
0c214932ba Fixes #6812: Limit reported prefix utilization to 100% 2021-07-28 09:55:40 -04:00
jeremystretch
a1eb4dc807 Fixes #5627: Fix filtering of interface connections list 2021-07-27 16:21:56 -04:00
jeremystretch
e92f13977c Changelog for #6785 2021-07-27 16:17:59 -04:00
Jeremy Stretch
5db283700f Merge pull request #6789 from bellwood/patch-1
Add AC Hardwire option to PowerPortTypeChoices
2021-07-27 16:14:01 -04:00
Jeremy Stretch
6e79e5608e Merge pull request #6810 from tamaszl/patch-1
Update 6-ldap.md - AUTH_LDAP_USER_DN_TEMPLATE to none for windows 2012+
2021-07-27 16:12:36 -04:00
jeremystretch
8355270a1a Fixes #6822: Use consistent maximum value for interface MTU 2021-07-27 16:04:51 -04:00
checktheroads
5a8835f41a Merge branch 'feature' into feature-sidebar 2021-07-26 14:47:31 -07:00
checktheroads
2d32aeb972 Migrate to collapsed sidebar layout 2021-07-26 14:46:05 -07:00
Brian Ellwood
1c38d63c50 Update choices.py 2021-07-26 15:03:43 -04:00
bluikko
4f6944424b Add dev server firewall configuration for EL distros (#6772)
* Add dev server firewall configuration for EL distros

* Fix typo in previous

* Indent the firewall block in install docs
2021-07-26 13:26:46 -04:00
jeremystretch
fc01bedd45 Fixes #6811: Fix exception when editing users 2021-07-26 09:37:58 -04:00
tamaszl
7ab916b527 Update 6-ldap.md - AUTH_LDAP_USER_DN_TEMPLATE to none for windows 2012+
changed     When using Windows Server 2012, `AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
to Windows Server 2012+
2021-07-25 18:44:21 -07:00
checktheroads
51c1f4b214 #6797: Make default border-radius less rounded 2021-07-24 18:13:08 -07:00
checktheroads
0b80d85c6c #6797: Fix API select styles 2021-07-24 18:11:01 -07:00
checktheroads
4489e130f2 #6797: Fix toast header style 2021-07-24 17:23:37 -07:00
checktheroads
e1cc00ad17 #6797: Fix <small/> element font-size 2021-07-24 17:19:47 -07:00
checktheroads
49191261a1 #6797: Fix incorrect color select label color 2021-07-24 17:13:02 -07:00
checktheroads
0479d5a02a #6797: Improve toast styles 2021-07-24 17:08:18 -07:00
checktheroads
189e733f81 #6797: Fix color flashing when server mode doesn't match select mode or client preference 2021-07-24 10:31:46 -07:00
checktheroads
bf2d535356 Fix incorrect rack elevation file name regression from 0572d03 2021-07-24 01:40:23 -07:00
checktheroads
05cfdd0b69 #6797: Fix search result layout 2021-07-24 01:21:14 -07:00
checktheroads
a60e8d3e12 #6797: Fix Safari anchor element styling issue 2021-07-24 01:07:53 -07:00
checktheroads
7b3d285884 #6797: Fix alert coloring in dark mode 2021-07-24 00:59:11 -07:00
checktheroads
5ba053a1c0 #6797: Fix duplicate ID on searchbar fields 2021-07-24 00:41:49 -07:00
checktheroads
7d5f647cd3 #6797: Improve home page shading 2021-07-24 00:38:28 -07:00
checktheroads
0572d03003 Migrate from ParcelJS to esbuild for UI bundling 2021-07-24 00:00:38 -07:00
jeremystretch
f25649955e Exclude NPM files from git (v3.0+) 2021-07-23 13:45:56 -04:00
jeremystretch
04d6a4a371 Introduce "adding models" section to development docs 2021-07-23 13:43:33 -04:00
jeremystretch
a8140d1f70 Closes #6781: Disable database query caching by default 2021-07-23 11:34:24 -04:00
jeremystretch
d1af15037c Fixes #6759: Fix assignment of parent interfaces for bulk import 2021-07-23 11:24:32 -04:00
jeremystretch
cca76550d6 Fixes #6794: Fix device name display on device status view 2021-07-23 11:18:50 -04:00
jeremystretch
2ff3d0d5a2 Fixes #6774: Fix A/Z assignment when swapping circuit terminations 2021-07-23 11:13:21 -04:00
WillIrvine
ffae2c5f18 Fixes #6632 2021-07-23 11:08:41 +12:00
Brian Ellwood
e300fad340 Add AC Hardwire option to PowerPortTypeChoices
Resolves FR #6785
2021-07-22 19:04:34 -04:00
Alyssa Bigley
1e7b76005c cleaned up validation error method 2021-06-14 15:23:42 -04:00
Alyssa Bigley
0a661596b3 moved duplicated code in CSV Fields into functions in forms/utils.py 2021-06-14 14:07:37 -04:00
Alyssa Bigley
934543b595 Caught and handled ValidationError 2021-06-11 13:42:26 -04:00
Alyssa Bigley
55b7cf21cc changed name of csv_file variable and started work on ValidationError 2021-06-10 14:41:33 -04:00
Alyssa Bigley
3549fc07f6 removed unnecessary use of seek() 2021-06-07 14:29:38 -04:00
Alyssa Bigley
ecd84d7c43 edited docstring for CSVFileField 2021-06-07 14:06:32 -04:00
Alyssa Bigley
c2b2b059e6 CSV import implemented using CSVFileField 2021-06-07 14:06:32 -04:00
Alyssa Bigley
6ff5a1db42 cleaned up csv parsing 2021-06-07 14:06:31 -04:00
Alyssa Bigley
2bc68707b5 csv parse using python csv library 2021-06-07 14:06:31 -04:00
Alyssa Bigley
0c9376039c working csv upload first draft 2021-06-07 14:06:31 -04:00
Alyssa Bigley
e1fe3ca14a CSV Upload as second field in existing form 2021-06-07 14:06:31 -04:00
346 changed files with 11112 additions and 12419 deletions

View File

@@ -17,7 +17,7 @@ body:
What version of NetBox are you currently running? (If you don't have access to the most
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
before opening a bug report to see if your issue has already been addressed.)
placeholder: v2.11.9
placeholder: v3.0.2
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: v2.11.9
placeholder: v3.0.2
validations:
required: true
- type: dropdown
@@ -30,8 +30,10 @@ body:
attributes:
label: Proposed functionality
description: >
Describe in detail the new feature or behavior you'd like to propose. Include any specific
changes to work flows, data models, or the user interface.
Describe in detail the new feature or behavior you are proposing. Include any specific changes
to work flows, data models, and/or the user interface. The more detail you provide here, the
greater chance your proposal has of being discussed. Feature requests which don't include an
actionable implementation plan will be rejected.
validations:
required: true
- type: textarea

View File

@@ -6,6 +6,7 @@ jobs:
strategy:
matrix:
python-version: [3.7, 3.8, 3.9]
node-version: [14.x]
services:
redis:
image: redis
@@ -33,12 +34,18 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies & set up configuration
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pycodestyle coverage
ln -s configuration.testing.py netbox/netbox/configuration.py
yarn --cwd netbox/project-static
- name: Build documentation
run: mkdocs build
@@ -47,7 +54,13 @@ jobs:
run: python netbox/manage.py collectstatic --no-input
- name: Check PEP8 compliance
run: pycodestyle --ignore=W504,E501 netbox/
run: pycodestyle --ignore=W504,E501 --exclude=node_modules netbox/
- name: Check UI ESLint, TypeScript, and Prettier Compliance
run: yarn --cwd netbox/project-static validate
- name: Validate Static Asset Integrity
run: scripts/verify-bundles.sh
- name: Run tests
run: coverage run --source="netbox/" netbox/manage.py test netbox/

View File

@@ -8,7 +8,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
- uses: actions/stale@v4
with:
close-issue-message: >
This issue has been automatically closed due to lack of activity. In an

3
.gitignore vendored
View File

@@ -1,10 +1,9 @@
*.pyc
*.swp
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/netbox/project-static/.cache
/netbox/project-static/node_modules
/netbox/project-static/docs/*
!/netbox/project-static/docs/.info
/netbox/netbox/configuration.py

View File

@@ -54,13 +54,15 @@ our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
### Screenshots
![Screenshot of Main Page](docs/media/home-light.png "Main Page")
![Screenshot of main page (light mode)](docs/media/screenshots/home-light.png "Main page (light mode)")
![Screenshot of Rack Elevation](docs/media/rack-dark.png "Rack Elevation")
![Screenshot of main page (dark mode)](docs/media/screenshots/home-dark.png "Main page (dark mode)")
![Screenshot of Prefix Hierarchy](docs/media/prefixes-light.png "Prefix Hierarchy")
![Screenshot of rack elevation](docs/media/screenshots/rack.png "Rack elevation")
![Screenshot of Cable Tracing](docs/media/cable-dark.png "Cable Tracing")
![Screenshot of prefixes hierarchy](docs/media/screenshots/prefixes-list.png "Prefixes hierarchy")
![Screenshot of cable trace](docs/media/screenshots/cable-trace.png "Cable tracing")
### Related projects

View File

@@ -1,5 +1,5 @@
server {
listen 443 ssl;
listen [::]:443 ssl ipv6only=off;
# CHANGE THIS TO YOUR SERVER'S NAME
server_name netbox.example.com;
@@ -23,7 +23,7 @@ server {
server {
# Redirect HTTP traffic to HTTPS
listen 80;
listen [::]:80 ipv6only=off;
server_name _;
return 301 https://$host$request_uri;
}

View File

@@ -1,6 +1,6 @@
# NAPALM
NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally.
NetBox supports integration with the [NAPALM automation](https://github.com/napalm-automation/napalm) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally.
The NetBox UI will display tabs for status, LLDP neighbors, and configuration under the device view if the following conditions are met:

View File

@@ -26,4 +26,4 @@ For the exhaustive list of exposed metrics, visit the `/metrics` endpoint on you
When deploying NetBox in a multiprocess manner (e.g. running multiple Gunicorn workers) the Prometheus client library requires the use of a shared directory to collect metrics from all worker processes. To configure this, first create or designate a local directory to which the worker processes have read and write access, and then configure your WSGI service (e.g. Gunicorn) to define this path as the `prometheus_multiproc_dir` environment variable.
!!! warning
If having accurate long-term metrics in a multiprocess environment is crucial to your deployment, it's recommended you use the `uwsgi` library instead of `gunicorn`. The issue lies in the way `gunicorn` tracks worker processes (vs `uwsgi`) which helps manage the metrics files created by the above configurations. If you're using NetBox with gunicorn in a containerized enviroment following the one-process-per-container methodology, then you will likely not need to change to `uwsgi`. More details can be found in [issue #3779](https://github.com/netbox-community/netbox/issues/3779#issuecomment-590547562).
If having accurate long-term metrics in a multiprocess environment is crucial to your deployment, it's recommended you use the `uwsgi` library instead of `gunicorn`. The issue lies in the way `gunicorn` tracks worker processes (vs `uwsgi`) which helps manage the metrics files created by the above configurations. If you're using NetBox with gunicorn in a containerized environment following the one-process-per-container methodology, then you will likely not need to change to `uwsgi`. More details can be found in [issue #3779](https://github.com/netbox-community/netbox/issues/3779#issuecomment-590547562).

View File

@@ -2,6 +2,9 @@
A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are managed under Logging > Webhooks.
!!! warning
Webhooks support the inclusion of user-submitted code to generate custom headers and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users.
## Configuration
* **Name** - A unique name for the webhook. The name is not included with outbound messages.

View File

@@ -273,6 +273,16 @@ LOGGING = {
---
## LOGIN_PERSISTENCE
Default: False
If true, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days.
Note that enabling this setting causes NetBox to update a user's session in the database (or file, as configured per [`SESSION_FILE_PATH`](#session_file_path)) with each request, which may introduce significant overhead in very active environments. It also permits an active user to remain authenticated to NetBox indefinitely.
---
## LOGIN_REQUIRED
Default: False
@@ -333,7 +343,7 @@ Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Pr
## NAPALM_PASSWORD
NetBox will use these credentials when authenticating to remote devices via the [NAPALM library](https://napalm-automation.net/), if installed. Both parameters are optional.
NetBox will use these credentials when authenticating to remote devices via the supported [NAPALM integration](../additional-features/napalm.md), if installed. Both parameters are optional.
!!! note
If SSH public key authentication has been set up on the remote device(s) for the system account under which NetBox runs, these parameters are not needed.

View File

@@ -17,6 +17,9 @@ When viewing a device named Router4, this link would render as:
Custom links appear as buttons in the top right corner of the page. Numeric weighting can be used to influence the ordering of links.
!!! warning
Custom links rely on user-created code to generate arbitrary HTML output, which may be dangerous. Only grant permission to create or modify custom links to trusted users.
## Context Data
The following context data is available within the template when rendering a custom link's text or URL.

View File

@@ -4,10 +4,13 @@ NetBox allows users to define custom templates that can be used when exporting o
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension.
Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/).
!!! note
The name `table` is reserved for internal use.
Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/).
!!! warning
Export templates are rendered using user-submitted code, which may pose security risks under certain conditions. Only grant permission to create or modify export templates to trusted users.
The list of objects returned from the database when rendering an export template is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example:

View File

@@ -0,0 +1,85 @@
# Adding Models
## 1. Define the model class
Models within each app are stored in either `models.py` or within a submodule under the `models/` directory. When creating a model, be sure to subclass the [appropriate base model](models.md) from `netbox.models`. This will typically be PrimaryModel or OrganizationalModel. Remember to add the model class to the `__all__` listing for the module.
Each model should define, at a minimum:
* A `__str__()` method returning a user-friendly string representation of the instance
* A `get_absolute_url()` method returning an instance's direct URL (using `reverse()`)
* A `Meta` class specifying a deterministic ordering (if ordered by fields other than the primary ID)
## 2. Define field choices
If the model has one or more fields with static choices, define those choices in `choices.py` by subclassing `utilities.choices.ChoiceSet`.
## 3. Generate database migrations
Once your model definition is complete, generate database migrations by running `manage.py -n $NAME --no-header`. Always specify a short unique name when generating migrations.
!!! info
Set `DEVELOPER = True` in your NetBox configuration to enable the creation of new migrations.
## 4. Add all standard views
Most models will need view classes created in `views.py` to serve the following operations:
* List view
* Detail view
* Edit view
* Delete view
* Bulk import
* Bulk edit
* Bulk delete
## 5. Add URL paths
Add the relevant URL path for each view created in the previous step to `urls.py`.
## 6. 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.
Every model FilterSet should define a `q` filter to support general search queries.
## 7. Create the table
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
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
For NetBox releases prior to v3.0, add the relevant link(s) to the navigation menu template. For later releases, add the relevant items in `netbox/netbox/navigation_menu.py`.
## 10. REST API components
Create the following for each model:
* Detailed (full) model serializer in `api/serializers.py`
* Nested serializer in `api/nested_serializers.py`
* API view in `api/views.py`
* Endpoint route in `api/urls.py`
## 11. GraphQL API components (v3.0+)
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
Add tests for the following:
* UI views
* API views
* Filter sets
## 13. 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.
Also add your model to the index in `docs/development/models.md`.

View File

@@ -34,11 +34,11 @@ class Foo(models.Model):
## 3. Update relevant querysets
If you're adding a relational field (e.g. `ForeignKey`) and intend to include the data when retreiving a list of objects, be sure to include the field using `prefetch_related()` as appropriate. This will optimize the view and avoid extraneous database queries.
If you're adding a relational field (e.g. `ForeignKey`) and intend to include the data when retrieving a list of objects, be sure to include the field using `prefetch_related()` as appropriate. This will optimize the view and avoid extraneous database queries.
## 4. Update API serializer
Extend the model's API serializer in `<app>.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal represenation of the model.
Extend the model's API serializer in `<app>.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal representation of the model.
## 5. Add field to forms

View File

@@ -0,0 +1,99 @@
# Web UI Development
## Front End Technologies
The NetBox UI is built on languages and frameworks:
### Styling & HTML Elements
#### [Bootstrap](https://getbootstrap.com/) 5
The majority of the NetBox UI is made up of stock Bootstrap components, with some styling modifications and custom components added on an as-needed basis. Bootstrap uses [Sass](https://sass-lang.com/), and NetBox extends Bootstrap's core Sass files for theming and customization.
### Client-side Scripting
#### [TypeScript](https://www.typescriptlang.org/)
All client-side scripting is transpiled from TypeScript to JavaScript and served by Django. In development, TypeScript is an _extremely_ effective tool for accurately describing and checking the code, which leads to significantly fewer bugs, a better development experience, and more predictable/readable code.
As part of the [bundling](#bundling) process, Bootstrap's JavaScript plugins are imported and bundled alongside NetBox's front-end code.
!!! danger "NetBox is jQuery-free"
Following the Bootstrap team's deprecation of jQuery in Bootstrap 5, NetBox also no longer uses jQuery in front-end code.
## Guidance
NetBox generally follows the following guidelines for front-end code:
- Bootstrap utility classes may be used to solve one-off issues or to implement singular components, as long as the class list does not exceed 4-5 classes. If an element needs more than 5 utility classes, a custom SCSS class should be added that contains the required style properties.
- Custom classes must be commented, explaining the general purpose of the class and where it is used.
- Reuse SCSS variables whenever possible. CSS values should (almost) never be hard-coded.
- All TypeScript functions must have, at a minimum, a basic [JSDoc](https://jsdoc.app/) description of what the function is for and where it is used. If possible, document all function arguments via [`@param` JSDoc block tags](https://jsdoc.app/tags-param.html).
- Expanding on NetBox's [dependency policy](style-guide.md#introducing-new-dependencies), new front-end dependencies should be avoided unless absolutely necessary. Every new front-end dependency adds to the CSS/JavaScript file size that must be loaded by the client and this should be minimized as much as possible. If adding a new dependency is unavoidable, use a tool like [Bundlephobia](https://bundlephobia.com/) to ensure the smallest possible library is used.
- All UI elements must be usable on all common screen sizes, including mobile devices. Be sure to test newly implemented solutions (JavaScript included) on as many screen sizes and device types as possible.
- NetBox aligns with Bootstrap's [supported Browsers and Devices](https://getbootstrap.com/docs/5.1/getting-started/browsers-devices/) list.
## UI Development
To contribute to the NetBox UI, you'll need to review the main [Getting Started guide](getting-started.md) in order to set up your base environment.
### Tools
Once you have a working NetBox development environment, you'll need to install a few more tools to work with the NetBox UI:
- [NodeJS](https://nodejs.org/en/download/) (the LTS release should suffice)
- [Yarn](https://yarnpkg.com/getting-started/install) (version 1)
After Node and Yarn are installed on your system, you'll need to install all the NetBox UI dependencies:
```console
$ cd netbox/project-static
$ yarn
```
!!! warning "Check Your Working Directory"
You need to be in the `netbox/project-static` directory to run the below `yarn` commands.
### Bundling
In order for the TypeScript and Sass (SCSS) source files to be usable by a browser, they must first be transpiled (TypeScript → JavaScript, Sass → CSS), bundled, and minified. After making changes to TypeScript or Sass source files, run `yarn bundle`.
`yarn bundle` is a wrapper around the following subcommands, any of which can be run individually:
| Command | Action |
| :-------------------- | :---------------------------------------------- |
| `yarn bundle` | Bundle TypeScript and Sass (SCSS) source files. |
| `yarn bundle:styles` | Bundle Sass (SCSS) source files only. |
| `yarn bundle:scripts` | Bundle TypeScript source files only. |
All output files will be written to `netbox/project-static/dist`, where Django will pick them up when `manage.py collectstatic` is run.
!!! info "Remember to re-run `manage.py collectstatic`"
If you're running the development web server — `manage.py runserver` — you'll need to run `manage.py collectstatic` to see your changes.
### Linting, Formatting & Type Checking
Before committing any changes to TypeScript files, and periodically throughout the development process, you should run `yarn validate` to catch formatting, code quality, or type errors.
!!! tip "IDE Integrations"
If you're using an IDE, it is strongly recommended to install [ESLint](https://eslint.org/docs/user-guide/integrations), [TypeScript](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Editor-Support), and [Prettier](https://prettier.io/docs/en/editors.html) integrations, if available. Most of them will automatically check and/or correct issues in the code as you develop, which can significantly increase your productivity as a contributor.
`yarn validate` is a wrapper around the following subcommands, any of which can be run individually:
| Command | Action |
| :--------------------------------- | :--------------------------------------------------------------- |
| `yarn validate` | Run all validation. |
| `yarn validate:lint` | Validate TypeScript code via [ESLint](https://eslint.org/) only. |
| `yarn validate:types` | Validate TypeScript code compilation only. |
| `yarn validate:formatting` | Validate code formatting of JavaScript & Sass/SCSS files. |
| `yarn validate:formatting:styles` | Validate code formatting Sass/SCSS only. |
| `yarn validate:formatting:scripts` | Validate code formatting TypeScript only. |
You can also run the following commands to automatically fix formatting issues:
| Command | Action |
| :-------------------- | :---------------------------------------------- |
| `yarn format` | Format TypeScript and Sass (SCSS) source files. |
| `yarn format:styles` | Format Sass (SCSS) source files only. |
| `yarn format:scripts` | Format TypeScript source files only. |

View File

@@ -11,9 +11,19 @@ table {
width: 100%;
}
th {
background-color: #f0f0f0;
padding: 6px;
font-weight: bold;
}
td {
padding: 6px;
}
/* Remove table header coloring. */
.md-typeset table:not([class]) th {
color: unset !important;
background-color: unset !important;
}
thead tr {
/* Colorize table headers. */
background-color: var(--md-code-bg-color);
color: var(--md-code-fg-color);
}

View File

@@ -45,7 +45,7 @@ NetBox provides both a singular and plural query field for each object type:
* `$OBJECT`: Returns a single object. Must specify the object's unique ID as `(id: 123)`.
* `$OBJECT_list`: Returns a list of objects, optionally filtered by given parameters.
For example, query `device(id:123)` to fetch a specific device (identified by its unique ID), and query `device_list` (with an optional set of fitlers) to fetch all devices.
For example, query `device(id:123)` to fetch a specific device (identified by its unique ID), and query `device_list` (with an optional set of filters) to fetch all devices.
For more detail on constructing GraphQL queries, see the [Graphene documentation](https://docs.graphene-python.org/en/latest/).

View File

@@ -1,4 +1,4 @@
![NetBox](netbox_logo.svg "NetBox logo")
![NetBox](netbox_logo.svg "NetBox logo"){style="height: 100px; margin-bottom: 3em"}
# What is NetBox?

View File

@@ -11,13 +11,13 @@ This section entails the installation and configuration of a local PostgreSQL da
```no-highlight
sudo apt update
sudo apt install -y postgresql libpq-dev
sudo apt install -y postgresql
```
=== "CentOS"
```no-highlight
sudo yum install -y postgresql-server libpq-devel
sudo yum install -y postgresql-server
sudo postgresql-setup --initdb
```
@@ -40,28 +40,28 @@ sudo systemctl enable postgresql
## Database Creation
At a minimum, we need to create a database for NetBox and assign it a username and password for authentication. This is done with the following commands.
At a minimum, we need to create a database for NetBox and assign it a username and password for authentication. Start by invoking the PostgreSQL shell as the system Postgres user.
```no-highlight
sudo -u postgres psql
```
Within the shell, enter the following commands to create the database and user (role), substituting your own value for the password:
```postgresql
CREATE DATABASE netbox;
CREATE USER netbox WITH PASSWORD 'J5brHrAXFLQSif0K';
GRANT ALL PRIVILEGES ON DATABASE netbox TO netbox;
```
!!! danger
**Do not use the password from the example.** Choose a strong, random password to ensure secure database authentication for your NetBox installation.
```no-highlight
$ sudo -u postgres psql
psql (12.5 (Ubuntu 12.5-0ubuntu0.20.04.1))
Type "help" for help.
postgres=# CREATE DATABASE netbox;
CREATE DATABASE
postgres=# CREATE USER netbox WITH PASSWORD 'J5brHrAXFLQSif0K';
CREATE ROLE
postgres=# GRANT ALL PRIVILEGES ON DATABASE netbox TO netbox;
GRANT
postgres=# \q
```
Once complete, enter `\q` to exit the PostgreSQL shell.
## Verify Service Status
You can verify that authentication works issuing the following command and providing the configured password. (Replace `localhost` with your database server if using a remote database.)
You can verify that authentication works by executing the `psql` command and passing the configured username and password. (Replace `localhost` with your database server if using a remote database.)
```no-highlight
$ psql --username netbox --password --host localhost netbox

View File

@@ -28,6 +28,7 @@ You may wish to modify the Redis configuration at `/etc/redis.conf` or `/etc/red
Use the `redis-cli` utility to ensure the Redis service is functional:
```no-highlight
$ redis-cli ping
PONG
redis-cli ping
```
If successful, you should receive a `PONG` response from the server.

View File

@@ -18,7 +18,7 @@ Begin by installing all system packages required by NetBox and its dependencies.
=== "CentOS"
```no-highlight
sudo yum install -y gcc python36 python36-devel python3-pip libxml2-devel libxslt-devel libffi-devel openssl-devel redhat-rpm-config
sudo yum install -y gcc python36 python36-devel python3-pip libxml2-devel libxslt-devel libffi-devel libpq-devel openssl-devel redhat-rpm-config
```
Before continuing with either platform, update pip (Python's package management tool) to its latest release:
@@ -36,23 +36,21 @@ This documentation provides two options for installing NetBox: from a downloadab
Download the [latest stable release](https://github.com/netbox-community/netbox/releases) from GitHub as a tarball or ZIP archive and extract it to your desired path. In this example, we'll use `/opt/netbox` as the NetBox root.
```no-highlight
$ sudo wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz
$ sudo tar -xzf vX.Y.Z.tar.gz -C /opt
$ sudo ln -s /opt/netbox-X.Y.Z/ /opt/netbox
$ ls -l /opt | grep netbox
lrwxrwxrwx 1 root root 13 Jul 20 13:44 netbox -> netbox-2.9.0/
drwxr-xr-x 2 root root 4096 Jul 20 13:44 netbox-2.9.0
sudo wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz
sudo tar -xzf vX.Y.Z.tar.gz -C /opt
sudo ln -s /opt/netbox-X.Y.Z/ /opt/netbox
```
!!! note
It is recommended to install NetBox in a directory named for its version number. For example, NetBox v2.9.0 would be installed into `/opt/netbox-2.9.0`, and a symlink from `/opt/netbox/` would point to this location. This allows for future releases to be installed in parallel without interrupting the current installation. When changing to the new release, only the symlink needs to be updated.
It is recommended to install NetBox in a directory named for its version number. For example, NetBox v3.0.0 would be installed into `/opt/netbox-3.0.0`, and a symlink from `/opt/netbox/` would point to this location. (You can verify this configuration with the command `ls -l /opt | grep netbox`.) This allows for future releases to be installed in parallel without interrupting the current installation. When changing to the new release, only the symlink needs to be updated.
### Option B: Clone the Git Repository
Create the base directory for the NetBox installation. For this guide, we'll use `/opt/netbox`.
```no-highlight
sudo mkdir -p /opt/netbox/ && cd /opt/netbox/
sudo mkdir -p /opt/netbox/
cd /opt/netbox/
```
If `git` is not already installed, install it:
@@ -72,19 +70,22 @@ If `git` is not already installed, install it:
Next, clone the **master** branch of the NetBox GitHub repository into the current directory. (This branch always holds the current stable release.)
```no-highlight
sudo git clone -b master https://github.com/netbox-community/netbox.git .
sudo git clone -b master --depth 1 https://github.com/netbox-community/netbox.git .
```
The screen below should be the result:
!!! note
The `git clone` command above utilizes a "shallow clone" to retrieve only the most recent commit. If you need to download the entire history, omit the `--depth 1` argument.
The `git clone` command should generate output similar to the following:
```
Cloning into '.'...
remote: Counting objects: 1994, done.
remote: Compressing objects: 100% (150/150), done.
remote: Total 1994 (delta 80), reused 0 (delta 0), pack-reused 1842
Receiving objects: 100% (1994/1994), 472.36 KiB | 0 bytes/s, done.
Resolving deltas: 100% (1495/1495), done.
Checking connectivity... done.
remote: Enumerating objects: 996, done.
remote: Counting objects: 100% (996/996), done.
remote: Compressing objects: 100% (935/935), done.
remote: Total 996 (delta 148), reused 386 (delta 34), pack-reused 0
Receiving objects: 100% (996/996), 4.26 MiB | 9.81 MiB/s, done.
Resolving deltas: 100% (148/148), done.
```
!!! note
@@ -200,7 +201,7 @@ All Python packages required by NetBox are listed in `requirements.txt` and will
### NAPALM
The [NAPALM automation](https://napalm-automation.net/) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device.
Integration with the [NAPALM automation](../additional-features/napalm.md) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device.
```no-highlight
sudo sh -c "echo 'napalm' >> /opt/netbox/local_requirements.txt"
@@ -250,13 +251,8 @@ Once the virtual environment has been activated, you should notice the string `(
Next, we'll create a superuser account using the `createsuperuser` Django management command (via `manage.py`). Specifying an email address for the user is not required, but be sure to use a very strong password.
```no-highlight
(venv) $ cd /opt/netbox/netbox
(venv) $ python3 manage.py createsuperuser
Username: admin
Email address: admin@example.com
Password:
Password (again):
Superuser created successfully.
cd /opt/netbox/netbox
python3 manage.py createsuperuser
```
## Schedule the Housekeeping Task
@@ -276,18 +272,31 @@ See the [housekeeping documentation](../administration/housekeeping.md) for furt
At this point, we should be able to run NetBox's development server for testing. We can check by starting a development instance:
```no-highlight
(venv) $ python3 manage.py runserver 0.0.0.0:8000 --insecure
python3 manage.py runserver 0.0.0.0:8000 --insecure
```
If successful, you should see output similar to the following:
```no-highlight
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
November 17, 2020 - 16:08:13
Django version 3.1.3, using settings 'netbox.settings'
Starting development server at http://0.0.0.0:8000/
August 30, 2021 - 18:02:23
Django version 3.2.6, using settings 'netbox.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
```
Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on port 8000; for example, <http://127.0.0.1:8000/>. You should be greeted with the NetBox home page. Try logging in using the username and password specified when creating a superuser.
!!! note
By default RHEL based distros will likely block your testing attempts with firewalld. The development server port can be opened with `firewall-cmd` (add `--permanent` if you want the rule to survive server restarts):
```no-highlight
firewall-cmd --zone=public --add-port=8000/tcp
```
!!! danger
The development server is for development and testing purposes only. It is neither performant nor secure enough for production use. **Do not use it in production.**

View File

@@ -14,7 +14,7 @@ While the provided configuration should suffice for most initial installations,
## systemd Setup
We'll use systemd to control both gunicorn and NetBox's background worker process. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory and reload the systemd dameon:
We'll use systemd to control both gunicorn and NetBox's background worker process. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory and reload the systemd daemon:
```no-highlight
sudo cp -v /opt/netbox/contrib/*.service /etc/systemd/system/
@@ -31,18 +31,23 @@ sudo systemctl enable netbox netbox-rq
You can use the command `systemctl status netbox` to verify that the WSGI service is running:
```no-highlight
# systemctl status netbox.service
systemctl status netbox.service
```
You should see output similar to the following:
```no-highlight
● netbox.service - NetBox WSGI Service
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2020-11-17 16:18:23 UTC; 3min 35s ago
Active: active (running) since Mon 2021-08-30 04:02:36 UTC; 14h ago
Docs: https://netbox.readthedocs.io/en/stable/
Main PID: 22836 (gunicorn)
Tasks: 6 (limit: 2345)
Memory: 339.3M
Main PID: 1140492 (gunicorn)
Tasks: 19 (limit: 4683)
Memory: 666.2M
CGroup: /system.slice/netbox.service
├─22836 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid>
├─22854 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid>
├─22855 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid>
├─1140492 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va>
├─1140513 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va>
├─1140514 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va>
...
```

View File

@@ -74,7 +74,7 @@ STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the
### User Authentication
!!! info
When using Windows Server 2012, `AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
When using Windows Server 2012+, `AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
```python
from django_auth_ldap.config import LDAPSearch

Binary file not shown.

Before

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 769 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 819 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 916 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 911 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 559 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -4,6 +4,6 @@ A platform defines the type of software running on a device or virtual machine.
Platforms may optionally be limited by manufacturer: If a platform is assigned to a particular manufacturer, it can only be assigned to devices with a type belonging to that manufacturer.
The platform model is also used to indicate which [NAPALM](https://napalm-automation.net/) driver and any associated arguments NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform.
The platform model is also used to indicate which NAPALM driver (if any) and any associated arguments NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform.
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.

View File

@@ -121,7 +121,7 @@ A new API endpoint has been added at `/api/ipam/prefixes/<pk>/available-ips/`. A
#### NAPALM Integration ([#1348](https://github.com/netbox-community/netbox/issues/1348))
The [NAPALM automation](https://napalm-automation.net/) library provides an abstracted interface for pulling live data (e.g. uptime, software version, running config, LLDP neighbors, etc.) from network devices. The NetBox API has been extended to support executing read-only NAPALM methods on devices defined in NetBox. To enable this functionality, ensure that NAPALM has been installed (`pip install napalm`) and the `NETBOX_USERNAME` and `NETBOX_PASSWORD` [configuration parameters](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/#netbox_username) have been set in configuration.py.
The [NAPALM automation](https://github.com/napalm-automation/napalm) library provides an abstracted interface for pulling live data (e.g. uptime, software version, running config, LLDP neighbors, etc.) from network devices. The NetBox API has been extended to support executing read-only NAPALM methods on devices defined in NetBox. To enable this functionality, ensure that NAPALM has been installed (`pip install napalm`) and the `NETBOX_USERNAME` and `NETBOX_PASSWORD` [configuration parameters](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/#netbox_username) have been set in configuration.py.
### Enhancements

View File

@@ -1,14 +1,74 @@
# NetBox v2.11
## v2.11.10 (FUTURE)
## v2.11.12 (2021-08-23)
### Enhancements
* [#6748](https://github.com/netbox-community/netbox/issues/6748) - Add site group filter to devices list
* [#6790](https://github.com/netbox-community/netbox/issues/6790) - Recognize a /32 IPv4 address as a child of a /32 IPv4 prefix
* [#6872](https://github.com/netbox-community/netbox/issues/6872) - Add table configuration button to child prefixes view
* [#6929](https://github.com/netbox-community/netbox/issues/6929) - Introduce `LOGIN_PERSISTENCE` configuration parameter to persist user sessions
* [#7011](https://github.com/netbox-community/netbox/issues/7011) - Add search field to VM interfaces filter form
### Bug Fixes
* [#5968](https://github.com/netbox-community/netbox/issues/5968) - Model forms should save empty custom field values as null
* [#6326](https://github.com/netbox-community/netbox/issues/6326) - Enable filtering assigned VLANs by group in interface edit form
* [#6686](https://github.com/netbox-community/netbox/issues/6686) - Force assignment of null custom field values to objects
* [#6776](https://github.com/netbox-community/netbox/issues/6776) - Fix erroneous webhook dispatch on failure to save objects
* [#6974](https://github.com/netbox-community/netbox/issues/6974) - Show contextual label for IP address role
* [#7012](https://github.com/netbox-community/netbox/issues/7012) - Fix hidden "add components" dropdown on devices list
---
## v2.11.11 (2021-08-12)
### Enhancements
* [#6883](https://github.com/netbox-community/netbox/issues/6883) - Add C21 & C22 power types
* [#6921](https://github.com/netbox-community/netbox/issues/6921) - Employ a sandbox when rendering Jinja2 code for increased security
### Bug Fixes
* [#6740](https://github.com/netbox-community/netbox/issues/6740) - Add import button to VM interfaces list
* [#6892](https://github.com/netbox-community/netbox/issues/6892) - Fix validation of unit ranges when creating a rack reservation
* [#6896](https://github.com/netbox-community/netbox/issues/6896) - Fix validation of IP address assigned as device/VM primary via NAT relation
* [#6902](https://github.com/netbox-community/netbox/issues/6902) - Populate device field when cloning device components
* [#6908](https://github.com/netbox-community/netbox/issues/6908) - Allow assignment of scope to VLAN groups upon import
* [#6909](https://github.com/netbox-community/netbox/issues/6909) - Remove extraneous `site` column from VLAN group import form
* [#6910](https://github.com/netbox-community/netbox/issues/6910) - Fix exception on invalid CSV import column name
* [#6918](https://github.com/netbox-community/netbox/issues/6918) - Fix return URL persistence when adding multiple objects sequentially
* [#6935](https://github.com/netbox-community/netbox/issues/6935) - Remove extraneous columns from inventory item and device bay tables
* [#6936](https://github.com/netbox-community/netbox/issues/6936) - Add missing `parent` column to inventory item import form
---
## v2.11.10 (2021-07-28)
### Enhancements
* [#6560](https://github.com/netbox-community/netbox/issues/6560) - Enable CSV import via uploaded file
* [#6644](https://github.com/netbox-community/netbox/issues/6644) - Add 6P/4P pass-through port types
* [#6771](https://github.com/netbox-community/netbox/issues/6771) - Add count of inventory items to manufacturer view
* [#6785](https://github.com/netbox-community/netbox/issues/6785) - Add "hardwired" type for power port types
### Bug Fixes
* [#5442](https://github.com/netbox-community/netbox/issues/5442) - Fix assignment of permissions based on LDAP groups
* [#5627](https://github.com/netbox-community/netbox/issues/5627) - Fix filtering of interface connections list
* [#6759](https://github.com/netbox-community/netbox/issues/6759) - Fix assignment of parent interfaces for bulk import
* [#6773](https://github.com/netbox-community/netbox/issues/6773) - Add missing `display` field to rack unit serializer
* [#6774](https://github.com/netbox-community/netbox/issues/6774) - Fix A/Z assignment when swapping circuit terminations
* [#6777](https://github.com/netbox-community/netbox/issues/6777) - Fix default value validation for custom text fields
* [#6778](https://github.com/netbox-community/netbox/issues/6778) - Rack reservation should display rack's location
* [#6780](https://github.com/netbox-community/netbox/issues/6780) - Include rack location in navigation breadcrumbs
* [#6794](https://github.com/netbox-community/netbox/issues/6794) - Fix device name display on device status view
* [#6812](https://github.com/netbox-community/netbox/issues/6812) - Limit reported prefix utilization to 100%
* [#6822](https://github.com/netbox-community/netbox/issues/6822) - Use consistent maximum value for interface MTU
### Other Changes
* [#6781](https://github.com/netbox-community/netbox/issues/6781) - Database query caching is now disabled by default
---

View File

@@ -1,9 +1,61 @@
# NetBox v3.0
## v3.0-beta1 (2021-07-23)
## v3.0.2 (2021-09-08)
### Bug Fixes
* [#7131](https://github.com/netbox-community/netbox/issues/7131) - Fix issue where Site fields were hidden when editing a VLAN group
* [#7148](https://github.com/netbox-community/netbox/issues/7148) - Fix issue where static query parameters with multiple values were not queried properly
* [#7153](https://github.com/netbox-community/netbox/issues/7153) - Allow clearing of assigned device type images
* [#7162](https://github.com/netbox-community/netbox/issues/7162) - Ensure consistent treatment of `BASE_PATH` for UI-driven API requests
* [#7164](https://github.com/netbox-community/netbox/issues/7164) - Fix styling of "decommissioned" label for circuits
* [#7169](https://github.com/netbox-community/netbox/issues/7169) - Fix CSV import file upload
* [#7176](https://github.com/netbox-community/netbox/issues/7176) - Fix issue where query parameters were duplicated across different forms of the same type
* [#7179](https://github.com/netbox-community/netbox/issues/7179) - Prevent obscuring "connect" pop-up for interfaces under device view
* [#7188](https://github.com/netbox-community/netbox/issues/7188) - Fix issue where select fields with `null_option` did not render or send the null option
* [#7189](https://github.com/netbox-community/netbox/issues/7189) - Set connection factory for django-redis when Sentinel is in use
* [#7191](https://github.com/netbox-community/netbox/issues/7191) - Fix issue where API-backed multi-select elements cleared selected options when adding new options
* [#7193](https://github.com/netbox-community/netbox/issues/7193) - Fix prefix (flat) template issue when viewing child prefixes with prefixes available
* [#7205](https://github.com/netbox-community/netbox/issues/7205) - Fix issue where selected fields with `null_option` set were not added to applied filters
* [#7209](https://github.com/netbox-community/netbox/issues/7209) - Allow unlimited API results when `MAX_PAGE_SIZE` is disabled
---
## v3.0.1 (2021-09-01)
### Bug Fixes
* [#7041](https://github.com/netbox-community/netbox/issues/7041) - Properly format JSON config object returned from a NAPALM device
* [#7070](https://github.com/netbox-community/netbox/issues/7070) - Fix exception when filtering by prefix max length in UI
* [#7071](https://github.com/netbox-community/netbox/issues/7071) - Fix exception when removing a primary IP from a device/VM
* [#7072](https://github.com/netbox-community/netbox/issues/7072) - Fix table configuration under prefix child object views
* [#7075](https://github.com/netbox-community/netbox/issues/7075) - Fix UI bug when a custom field has a space in the name
* [#7080](https://github.com/netbox-community/netbox/issues/7080) - Fix missing image previews
* [#7081](https://github.com/netbox-community/netbox/issues/7081) - Fix UI bug that did not properly request and handle paginated data
* [#7082](https://github.com/netbox-community/netbox/issues/7082) - Avoid exception when referencing invalid content type in table
* [#7083](https://github.com/netbox-community/netbox/issues/7083) - Correct labeling for VM memory attribute
* [#7084](https://github.com/netbox-community/netbox/issues/7084) - Fix KeyError exception when editing access VLAN on an interface
* [#7084](https://github.com/netbox-community/netbox/issues/7084) - Fix issue where hidden VLAN form fields were incorrectly included in the form submission
* [#7089](https://github.com/netbox-community/netbox/issues/7089) - Fix filtering of change log by content type
* [#7090](https://github.com/netbox-community/netbox/issues/7090) - Allow decimal input on length field when bulk editing cables
* [#7091](https://github.com/netbox-community/netbox/issues/7091) - Ensure API requests from the UI are aware of `BASE_PATH`
* [#7092](https://github.com/netbox-community/netbox/issues/7092) - Fix missing bulk edit buttons on Prefix IP Addresses table
* [#7093](https://github.com/netbox-community/netbox/issues/7093) - Multi-select custom field filters should employ exact match
* [#7096](https://github.com/netbox-community/netbox/issues/7096) - Home links should honor `BASE_PATH` configuration
* [#7101](https://github.com/netbox-community/netbox/issues/7101) - Enforce `MAX_PAGE_SIZE` for table and REST API pagination
* [#7106](https://github.com/netbox-community/netbox/issues/7106) - Fix incorrect "Map It" button URL on a site's physical address field
* [#7107](https://github.com/netbox-community/netbox/issues/7107) - Fix missing search button and search results in IP address assignment "Assign IP" tab
* [#7109](https://github.com/netbox-community/netbox/issues/7109) - Ensure human readability of exceptions raised during REST API requests
* [#7113](https://github.com/netbox-community/netbox/issues/7113) - Show bulk edit/delete actions for prefix child objects
* [#7123](https://github.com/netbox-community/netbox/issues/7123) - Remove "Global" placeholder for null VRF field
* [#7124](https://github.com/netbox-community/netbox/issues/7124) - Fix duplicate static query param values in API Select
---
## v3.0.0 (2021-08-30)
!!! warning "Existing Deployments Must Upgrade from v2.11"
Upgrading an existing NetBox deployment to version 3.0 **must** be done from version 2.11.0 or later. If attempting to upgrade a deployment of NetBox v2.10 or earlier, first upgrade to a NetBox v2.11 release, and then upgrade from v2.11 to v3.0. This will avoid any problems with the database migration optimizations implemented in version 3.0.
Upgrading an existing NetBox deployment to version 3.0 **must** be done from version 2.11.0 or later. If attempting to upgrade a deployment of NetBox v2.10 or earlier, first upgrade to a NetBox v2.11 release, and then upgrade from v2.11 to v3.0. This will avoid any problems with the database migration optimizations implemented in version 3.0. (This is not necessary for _new_ installations.)
### Breaking Changes
@@ -129,11 +181,11 @@ The new REST API endpoint `/api/users/tokens/` has been added, which includes a
$ curl -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
https://netbox/api/users/tokens/provision/
{
https://netbox/api/users/tokens/provision/ \
--data '{
"username": "hankhill",
"password: "I<3C3H8",
}
}'
```
If the supplied credentials are valid, NetBox will create and return a new token for the user.
@@ -178,6 +230,16 @@ Note that NetBox's `rqworker` process will _not_ service custom queues by defaul
* [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths
* [#6328](https://github.com/netbox-community/netbox/issues/6328) - Build and serve documentation locally
### Bug Fixes (from v3.2-beta2)
* [#6977](https://github.com/netbox-community/netbox/issues/6977) - Truncate global search dropdown on small screens
* [#6979](https://github.com/netbox-community/netbox/issues/6979) - Hide "create & add another" button for circuit terminations
* [#6982](https://github.com/netbox-community/netbox/issues/6982) - Fix styling of empty dropdown list under dark mode
* [#6996](https://github.com/netbox-community/netbox/issues/6996) - Global search bar should be full width on mobile
* [#7001](https://github.com/netbox-community/netbox/issues/7001) - Fix page focus on load
* [#7034](https://github.com/netbox-community/netbox/issues/7034) - Fix toggling of VLAN group scope selector fields
* [#7045](https://github.com/netbox-community/netbox/issues/7045) - Fix navigation menu rendering under Chrome
### Other Changes
* [#5223](https://github.com/netbox-community/netbox/issues/5223) - Remove the console/power/interface connections REST API endpoints

View File

@@ -39,11 +39,11 @@ To provision a token via the REST API, make a `POST` request to the `/api/users/
$ curl -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
https://netbox/api/users/tokens/provision/
{
https://netbox/api/users/tokens/provision/ \
--data '{
"username": "hankhill",
"password: "I<3C3H8",
}
}'
```
Note that we are _not_ passing an existing REST API token with this request. If the supplied credentials are valid, a new REST API token will be automatically created for the user. Note that the key will be automatically generated, and write ability will be enabled.

View File

@@ -69,6 +69,12 @@ Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions:
| `gt` | Greater than |
| `gte` | Greater than or equal to |
Here is an example of a numeric field lookup expression that will return all VLANs with a VLAN ID greater than 900:
```no-highlight
GET /api/ipam/vlans/?vid__gt=900
```
### String Fields
String based (char) fields (Name, Address, etc) support these lookup expressions:
@@ -86,7 +92,17 @@ String based (char) fields (Name, Address, etc) support these lookup expressions
| `nie` | Inverse exact match (case-insensitive) |
| `empty` | Is empty (boolean) |
Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name:
```no-highlight
GET /api/dcim/devices/?name__ic=switch
```
### Foreign Keys & Other Fields
Certain other fields, namely foreign key relationships support just the negation
expression: `n`.
expression: `n`. Here is an example of a lookup expression on a foreign key, it would return all the VLANs that don't have a VLAN Group ID of 3203:
```no-highlight
GET /api/ipam/vlans/?group_id__n=3203
```

View File

@@ -1,36 +0,0 @@
# Screenshots
## Light Mode
### Home Page
![Home Page](../media/home-light.png)
### Rack Elevation
![Rack Elevation](../media/rack-light.png)
### Prefixes
![Prefixes](../media/prefixes-light.png)
### Cable Trace
![Cable Trace](../media/cable-light.png)
## Dark Mode
### Home Page
![Home Page](../media/home-dark.png)
### Rack Elevation
![Rack Elevation](../media/rack-dark.png)
### Prefixes
![Prefixes](../media/prefixes-dark.png)
### Cable Trace
![Cable Trace](../media/cable-dark.png)

View File

@@ -1,12 +1,15 @@
site_name: NetBox Documentation
site_dir: netbox/project-static/docs
site_url: https://netbox.readthedocs.io/
repo_name: netbox-community/netbox
repo_url: https://github.com/netbox-community/netbox
python:
install:
- requirements: docs/requirements.txt
theme:
name: material
icon:
repo: fontawesome/brands/github
palette:
- scheme: default
toggle:
@@ -26,6 +29,7 @@ extra_css:
- extra.css
markdown_extensions:
- admonition
- attr_list
- markdown_include.include:
headingOffset: 1
- pymdownx.emoji:
@@ -94,10 +98,12 @@ nav:
- Getting Started: 'development/getting-started.md'
- Style Guide: 'development/style-guide.md'
- Models: 'development/models.md'
- Adding Models: 'development/adding-models.md'
- Extending Models: 'development/extending-models.md'
- Signals: 'development/signals.md'
- Application Registry: 'development/application-registry.md'
- User Preferences: 'development/user-preferences.md'
- Web UI: 'development/web-ui.md'
- Release Checklist: 'development/release-checklist.md'
- Release Notes:
- Version 3.0: 'release-notes/version-3.0.md'
@@ -113,4 +119,3 @@ nav:
- Version 2.2: 'release-notes/version-2.2.md'
- Version 2.1: 'release-notes/version-2.1.md'
- Version 2.0: 'release-notes/version-2.0.md'
- Screenshots: 'screenshots/index.md'

View File

@@ -29,7 +29,7 @@ class CircuitStatusChoices(ChoiceSet):
STATUS_PLANNED: 'info',
STATUS_PROVISIONING: 'primary',
STATUS_OFFLINE: 'danger',
STATUS_DECOMMISSIONED: 'default',
STATUS_DECOMMISSIONED: 'secondary',
}

View File

@@ -107,21 +107,36 @@ class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBu
class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
model = Provider
field_groups = [
['region_id', 'site_id'],
['asn', 'tag'],
['q', 'tag'],
['region_id', 'site_group_id', 'site_id'],
['asn'],
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region')
label=_('Region'),
fetch_trigger='open'
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region_id'
'region_id': '$region_id',
'site_group_id': '$site_group_id',
},
label=_('Site')
label=_('Site'),
fetch_trigger='open'
)
asn = forms.IntegerField(
required=False,
@@ -194,11 +209,20 @@ class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
class ProviderNetworkFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
model = ProviderNetwork
field_order = ['provider_id']
field_groups = (
('q', 'tag'),
('provider_id',),
)
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider')
label=_('Provider'),
fetch_trigger='open'
)
tag = TagFilterField(model)
@@ -354,26 +378,29 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBul
class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
model = Circuit
field_order = [
'type_id', 'provider_id', 'provider_network_id', 'status', 'region_id', 'site_id', 'tenant_group_id', 'tenant_id',
'commit_rate',
]
field_groups = [
['type_id', 'status', 'commit_rate'],
['q', 'tag'],
['provider_id', 'provider_network_id'],
['region_id', 'site_id'],
['type_id', 'status', 'commit_rate'],
['region_id', 'site_group_id', 'site_id'],
['tenant_group_id', 'tenant_id'],
['tag']
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
type_id = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(),
required=False,
label=_('Type')
label=_('Type'),
fetch_trigger='open'
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider')
label=_('Provider'),
fetch_trigger='open'
)
provider_network_id = DynamicModelMultipleChoiceField(
queryset=ProviderNetwork.objects.all(),
@@ -381,7 +408,8 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
query_params={
'provider_id': '$provider_id'
},
label=_('Provider network')
label=_('Provider network'),
fetch_trigger='open'
)
status = forms.MultipleChoiceField(
choices=CircuitStatusChoices,
@@ -391,15 +419,24 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region')
label=_('Region'),
fetch_trigger='open'
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region_id'
'region_id': '$region_id',
'site_group_id': '$site_group_id',
},
label=_('Site')
label=_('Site'),
fetch_trigger='open'
)
commit_rate = forms.IntegerField(
required=False,

View File

@@ -1,5 +1,5 @@
from circuits import filtersets, models
from netbox.graphql.types import BaseObjectType, ObjectType, TaggedObjectType
from netbox.graphql.types import ObjectType, OrganizationalObjectType, PrimaryObjectType
__all__ = (
'CircuitTerminationType',
@@ -10,7 +10,7 @@ __all__ = (
)
class CircuitTerminationType(BaseObjectType):
class CircuitTerminationType(ObjectType):
class Meta:
model = models.CircuitTermination
@@ -18,7 +18,7 @@ class CircuitTerminationType(BaseObjectType):
filterset_class = filtersets.CircuitTerminationFilterSet
class CircuitType(TaggedObjectType):
class CircuitType(PrimaryObjectType):
class Meta:
model = models.Circuit
@@ -26,7 +26,7 @@ class CircuitType(TaggedObjectType):
filterset_class = filtersets.CircuitFilterSet
class CircuitTypeType(ObjectType):
class CircuitTypeType(OrganizationalObjectType):
class Meta:
model = models.CircuitType
@@ -34,7 +34,7 @@ class CircuitTypeType(ObjectType):
filterset_class = filtersets.CircuitTypeFilterSet
class ProviderType(TaggedObjectType):
class ProviderType(PrimaryObjectType):
class Meta:
model = models.Provider
@@ -42,7 +42,7 @@ class ProviderType(TaggedObjectType):
filterset_class = filtersets.ProviderFilterSet
class ProviderNetworkType(TaggedObjectType):
class ProviderNetworkType(PrimaryObjectType):
class Meta:
model = models.ProviderNetwork

View File

@@ -287,6 +287,10 @@ class CircuitSwapTerminations(generic.ObjectEditView):
termination_z.save()
termination_a.term_side = 'Z'
termination_a.save()
circuit.refresh_from_db()
circuit.termination_a = termination_z
circuit.termination_z = termination_a
circuit.save()
elif termination_a:
termination_a.term_side = 'Z'
termination_a.save()
@@ -300,9 +304,6 @@ class CircuitSwapTerminations(generic.ObjectEditView):
circuit.termination_z = None
circuit.save()
print(f'term A: {circuit.termination_a}')
print(f'term Z: {circuit.termination_z}')
messages.success(request, f"Swapped terminations for circuit {circuit}.")
return redirect('circuits:circuit', pk=circuit.pk)

View File

@@ -22,7 +22,7 @@ from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.exceptions import ServiceUnavailable
from netbox.api.metadata import ContentTypeMetadata
from utilities.api import get_serializer_for_model
from utilities.utils import count_related
from utilities.utils import count_related, decode_dict
from virtualization.models import VirtualMachine
from . import serializers
from .exceptions import MissingFilterException
@@ -498,7 +498,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
response[method] = {'error': 'Only get_* NAPALM methods are supported'}
continue
try:
response[method] = getattr(d, method)()
response[method] = decode_dict(getattr(d, method)())
except NotImplementedError:
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
except Exception as e:

View File

@@ -252,6 +252,7 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_IEC_C14 = 'iec-60320-c14'
TYPE_IEC_C16 = 'iec-60320-c16'
TYPE_IEC_C20 = 'iec-60320-c20'
TYPE_IEC_C22 = 'iec-60320-c22'
# IEC 60309
TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h'
TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h'
@@ -341,6 +342,8 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_DC = 'dc-terminal'
# Proprietary
TYPE_SAF_D_GRID = 'saf-d-grid'
# Other
TYPE_HARDWIRED = 'hardwired'
CHOICES = (
('IEC 60320', (
@@ -349,6 +352,7 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_IEC_C14, 'C14'),
(TYPE_IEC_C16, 'C16'),
(TYPE_IEC_C20, 'C20'),
(TYPE_IEC_C22, 'C22'),
)),
('IEC 60309', (
(TYPE_IEC_PNE4H, 'P+N+E 4H'),
@@ -447,6 +451,9 @@ class PowerPortTypeChoices(ChoiceSet):
('Proprietary', (
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
)),
('Other', (
(TYPE_HARDWIRED, 'Hardwired'),
)),
)
@@ -462,6 +469,7 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_IEC_C13 = 'iec-60320-c13'
TYPE_IEC_C15 = 'iec-60320-c15'
TYPE_IEC_C19 = 'iec-60320-c19'
TYPE_IEC_C21 = 'iec-60320-c21'
# IEC 60309
TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h'
TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h'
@@ -545,6 +553,8 @@ class PowerOutletTypeChoices(ChoiceSet):
# Proprietary
TYPE_HDOT_CX = 'hdot-cx'
TYPE_SAF_D_GRID = 'saf-d-grid'
# Other
TYPE_HARDWIRED = 'hardwired'
CHOICES = (
('IEC 60320', (
@@ -553,6 +563,7 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_IEC_C13, 'C13'),
(TYPE_IEC_C15, 'C15'),
(TYPE_IEC_C19, 'C19'),
(TYPE_IEC_C21, 'C21'),
)),
('IEC 60309', (
(TYPE_IEC_PNE4H, 'P+N+E 4H'),
@@ -645,6 +656,9 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_HDOT_CX, 'HDOT Cx'),
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
)),
('Other', (
(TYPE_HARDWIRED, 'Hardwired'),
)),
)
@@ -917,6 +931,11 @@ class PortTypeChoices(ChoiceSet):
TYPE_8P6C = '8p6c'
TYPE_8P4C = '8p4c'
TYPE_8P2C = '8p2c'
TYPE_6P6C = '6p6c'
TYPE_6P4C = '6p4c'
TYPE_6P2C = '6p2c'
TYPE_4P4C = '4p4c'
TYPE_4P2C = '4p2c'
TYPE_GG45 = 'gg45'
TYPE_TERA4P = 'tera-4p'
TYPE_TERA2P = 'tera-2p'
@@ -948,6 +967,11 @@ class PortTypeChoices(ChoiceSet):
(TYPE_8P6C, '8P6C'),
(TYPE_8P4C, '8P4C'),
(TYPE_8P2C, '8P2C'),
(TYPE_6P6C, '6P6C'),
(TYPE_6P4C, '6P4C'),
(TYPE_6P2C, '6P2C'),
(TYPE_4P4C, '4P4C'),
(TYPE_4P2C, '4P2C'),
(TYPE_GG45, 'GG45'),
(TYPE_TERA4P, 'TERA 4P'),
(TYPE_TERA2P, 'TERA 2P'),

View File

@@ -29,7 +29,7 @@ REARPORT_POSITIONS_MAX = 1024
#
INTERFACE_MTU_MIN = 1
INTERFACE_MTU_MAX = 32767 # Max value of a signed 16-bit integer
INTERFACE_MTU_MAX = 65536
VIRTUAL_IFACE_TYPES = [
InterfaceTypeChoices.TYPE_VIRTUAL,

View File

@@ -831,6 +831,17 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
to_field_name='slug',
label='Site name (slug)',
)
location_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__location',
queryset=Location.objects.all(),
label='Location (ID)',
)
location = django_filters.ModelMultipleChoiceFilter(
field_name='device__location__slug',
queryset=Location.objects.all(),
to_field_name='slug',
label='Location (slug)',
)
device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
label='Device (ID)',
@@ -1053,39 +1064,6 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
method='search',
label='Search',
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='device__site__region',
lookup_expr='in',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='device__site__region',
lookup_expr='in',
to_field_name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site name (slug)',
)
device_id = django_filters.ModelChoiceFilter(
queryset=Device.objects.all(),
label='Device (ID)',
)
device = django_filters.ModelChoiceFilter(
queryset=Device.objects.all(),
to_field_name='name',
label='Device (name)',
)
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItem.objects.all(),
label='Parent inventory item (ID)',

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,13 @@
from dcim import filtersets, models
from netbox.graphql.types import BaseObjectType, ObjectType, TaggedObjectType
from extras.graphql.mixins import (
ChangelogMixin, ConfigContextMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin,
)
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, PrimaryObjectType
__all__ = (
'CableType',
'ComponentObjectType',
'ConsolePortType',
'ConsolePortTemplateType',
'ConsoleServerPortType',
@@ -38,7 +43,40 @@ __all__ = (
)
class CableType(TaggedObjectType):
#
# Base types
#
class ComponentObjectType(
ChangelogMixin,
CustomFieldsMixin,
TagsMixin,
BaseObjectType
):
"""
Base type for device/VM components
"""
class Meta:
abstract = True
class ComponentTemplateObjectType(
ChangelogMixin,
BaseObjectType
):
"""
Base type for device/VM components
"""
class Meta:
abstract = True
#
# Model types
#
class CableType(PrimaryObjectType):
class Meta:
model = models.Cable
@@ -52,7 +90,7 @@ class CableType(TaggedObjectType):
return self.length_unit or None
class ConsolePortType(TaggedObjectType):
class ConsolePortType(ComponentObjectType):
class Meta:
model = models.ConsolePort
@@ -63,7 +101,7 @@ class ConsolePortType(TaggedObjectType):
return self.type or None
class ConsolePortTemplateType(BaseObjectType):
class ConsolePortTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.ConsolePortTemplate
@@ -74,7 +112,7 @@ class ConsolePortTemplateType(BaseObjectType):
return self.type or None
class ConsoleServerPortType(TaggedObjectType):
class ConsoleServerPortType(ComponentObjectType):
class Meta:
model = models.ConsoleServerPort
@@ -85,7 +123,7 @@ class ConsoleServerPortType(TaggedObjectType):
return self.type or None
class ConsoleServerPortTemplateType(BaseObjectType):
class ConsoleServerPortTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.ConsoleServerPortTemplate
@@ -96,7 +134,7 @@ class ConsoleServerPortTemplateType(BaseObjectType):
return self.type or None
class DeviceType(TaggedObjectType):
class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, PrimaryObjectType):
class Meta:
model = models.Device
@@ -107,7 +145,7 @@ class DeviceType(TaggedObjectType):
return self.face or None
class DeviceBayType(TaggedObjectType):
class DeviceBayType(ComponentObjectType):
class Meta:
model = models.DeviceBay
@@ -115,7 +153,7 @@ class DeviceBayType(TaggedObjectType):
filterset_class = filtersets.DeviceBayFilterSet
class DeviceBayTemplateType(BaseObjectType):
class DeviceBayTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.DeviceBayTemplate
@@ -123,7 +161,7 @@ class DeviceBayTemplateType(BaseObjectType):
filterset_class = filtersets.DeviceBayTemplateFilterSet
class DeviceRoleType(ObjectType):
class DeviceRoleType(OrganizationalObjectType):
class Meta:
model = models.DeviceRole
@@ -131,7 +169,7 @@ class DeviceRoleType(ObjectType):
filterset_class = filtersets.DeviceRoleFilterSet
class DeviceTypeType(TaggedObjectType):
class DeviceTypeType(PrimaryObjectType):
class Meta:
model = models.DeviceType
@@ -142,7 +180,7 @@ class DeviceTypeType(TaggedObjectType):
return self.subdevice_role or None
class FrontPortType(TaggedObjectType):
class FrontPortType(ComponentObjectType):
class Meta:
model = models.FrontPort
@@ -150,7 +188,7 @@ class FrontPortType(TaggedObjectType):
filterset_class = filtersets.FrontPortFilterSet
class FrontPortTemplateType(BaseObjectType):
class FrontPortTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.FrontPortTemplate
@@ -158,7 +196,7 @@ class FrontPortTemplateType(BaseObjectType):
filterset_class = filtersets.FrontPortTemplateFilterSet
class InterfaceType(TaggedObjectType):
class InterfaceType(IPAddressesMixin, ComponentObjectType):
class Meta:
model = models.Interface
@@ -169,7 +207,7 @@ class InterfaceType(TaggedObjectType):
return self.mode or None
class InterfaceTemplateType(BaseObjectType):
class InterfaceTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.InterfaceTemplate
@@ -177,7 +215,7 @@ class InterfaceTemplateType(BaseObjectType):
filterset_class = filtersets.InterfaceTemplateFilterSet
class InventoryItemType(TaggedObjectType):
class InventoryItemType(ComponentObjectType):
class Meta:
model = models.InventoryItem
@@ -185,7 +223,7 @@ class InventoryItemType(TaggedObjectType):
filterset_class = filtersets.InventoryItemFilterSet
class LocationType(ObjectType):
class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectType):
class Meta:
model = models.Location
@@ -193,7 +231,7 @@ class LocationType(ObjectType):
filterset_class = filtersets.LocationFilterSet
class ManufacturerType(ObjectType):
class ManufacturerType(OrganizationalObjectType):
class Meta:
model = models.Manufacturer
@@ -201,7 +239,7 @@ class ManufacturerType(ObjectType):
filterset_class = filtersets.ManufacturerFilterSet
class PlatformType(ObjectType):
class PlatformType(OrganizationalObjectType):
class Meta:
model = models.Platform
@@ -209,7 +247,7 @@ class PlatformType(ObjectType):
filterset_class = filtersets.PlatformFilterSet
class PowerFeedType(TaggedObjectType):
class PowerFeedType(PrimaryObjectType):
class Meta:
model = models.PowerFeed
@@ -217,7 +255,7 @@ class PowerFeedType(TaggedObjectType):
filterset_class = filtersets.PowerFeedFilterSet
class PowerOutletType(TaggedObjectType):
class PowerOutletType(ComponentObjectType):
class Meta:
model = models.PowerOutlet
@@ -231,7 +269,7 @@ class PowerOutletType(TaggedObjectType):
return self.type or None
class PowerOutletTemplateType(BaseObjectType):
class PowerOutletTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.PowerOutletTemplate
@@ -245,7 +283,7 @@ class PowerOutletTemplateType(BaseObjectType):
return self.type or None
class PowerPanelType(TaggedObjectType):
class PowerPanelType(PrimaryObjectType):
class Meta:
model = models.PowerPanel
@@ -253,7 +291,7 @@ class PowerPanelType(TaggedObjectType):
filterset_class = filtersets.PowerPanelFilterSet
class PowerPortType(TaggedObjectType):
class PowerPortType(ComponentObjectType):
class Meta:
model = models.PowerPort
@@ -264,7 +302,7 @@ class PowerPortType(TaggedObjectType):
return self.type or None
class PowerPortTemplateType(BaseObjectType):
class PowerPortTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.PowerPortTemplate
@@ -275,7 +313,7 @@ class PowerPortTemplateType(BaseObjectType):
return self.type or None
class RackType(TaggedObjectType):
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
class Meta:
model = models.Rack
@@ -289,7 +327,7 @@ class RackType(TaggedObjectType):
return self.outer_unit or None
class RackReservationType(TaggedObjectType):
class RackReservationType(PrimaryObjectType):
class Meta:
model = models.RackReservation
@@ -297,7 +335,7 @@ class RackReservationType(TaggedObjectType):
filterset_class = filtersets.RackReservationFilterSet
class RackRoleType(ObjectType):
class RackRoleType(OrganizationalObjectType):
class Meta:
model = models.RackRole
@@ -305,7 +343,7 @@ class RackRoleType(ObjectType):
filterset_class = filtersets.RackRoleFilterSet
class RearPortType(TaggedObjectType):
class RearPortType(ComponentObjectType):
class Meta:
model = models.RearPort
@@ -313,7 +351,7 @@ class RearPortType(TaggedObjectType):
filterset_class = filtersets.RearPortFilterSet
class RearPortTemplateType(BaseObjectType):
class RearPortTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.RearPortTemplate
@@ -321,7 +359,7 @@ class RearPortTemplateType(BaseObjectType):
filterset_class = filtersets.RearPortTemplateFilterSet
class RegionType(ObjectType):
class RegionType(VLANGroupsMixin, OrganizationalObjectType):
class Meta:
model = models.Region
@@ -329,7 +367,7 @@ class RegionType(ObjectType):
filterset_class = filtersets.RegionFilterSet
class SiteType(TaggedObjectType):
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
class Meta:
model = models.Site
@@ -337,7 +375,7 @@ class SiteType(TaggedObjectType):
filterset_class = filtersets.SiteFilterSet
class SiteGroupType(ObjectType):
class SiteGroupType(VLANGroupsMixin, OrganizationalObjectType):
class Meta:
model = models.SiteGroup
@@ -345,7 +383,7 @@ class SiteGroupType(ObjectType):
filterset_class = filtersets.SiteGroupFilterSet
class VirtualChassisType(TaggedObjectType):
class VirtualChassisType(PrimaryObjectType):
class Meta:
model = models.VirtualChassis

View File

@@ -237,6 +237,8 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
help_text='Port speed in bits per second'
)
clone_fields = ['device', 'type', 'speed']
class Meta:
ordering = ('device', '_name')
unique_together = ('device', 'name')
@@ -267,6 +269,8 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
help_text='Port speed in bits per second'
)
clone_fields = ['device', 'type', 'speed']
class Meta:
ordering = ('device', '_name')
unique_together = ('device', 'name')
@@ -303,6 +307,8 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint):
help_text="Allocated power draw (watts)"
)
clone_fields = ['device', 'maximum_draw', 'allocated_draw']
class Meta:
ordering = ('device', '_name')
unique_together = ('device', 'name')
@@ -399,6 +405,8 @@ class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
help_text="Phase (for three-phase feeds)"
)
clone_fields = ['device', 'type', 'power_port', 'feed_leg']
class Meta:
ordering = ('device', '_name')
unique_together = ('device', 'name')
@@ -435,7 +443,10 @@ class BaseInterface(models.Model):
mtu = models.PositiveIntegerField(
blank=True,
null=True,
validators=[MinValueValidator(1), MaxValueValidator(65536)],
validators=[
MinValueValidator(INTERFACE_MTU_MIN),
MaxValueValidator(INTERFACE_MTU_MAX)
],
verbose_name='MTU'
)
mode = models.CharField(
@@ -522,6 +533,8 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
related_query_name='interface'
)
clone_fields = ['device', 'parent', 'lag', 'type', 'mgmt_only']
class Meta:
ordering = ('device', CollateAsChar('_name'))
unique_together = ('device', 'name')
@@ -638,6 +651,8 @@ class FrontPort(ComponentModel, CableTermination):
]
)
clone_fields = ['device', 'type']
class Meta:
ordering = ('device', '_name')
unique_together = (
@@ -684,6 +699,7 @@ class RearPort(ComponentModel, CableTermination):
MaxValueValidator(REARPORT_POSITIONS_MAX)
]
)
clone_fields = ['device', 'type', 'positions']
class Meta:
ordering = ('device', '_name')
@@ -721,6 +737,8 @@ class DeviceBay(ComponentModel):
null=True
)
clone_fields = ['device']
class Meta:
ordering = ('device', '_name')
unique_together = ('device', 'name')
@@ -803,6 +821,8 @@ class InventoryItem(MPTTModel, ComponentModel):
objects = TreeManager()
clone_fields = ['device', 'parent', 'manufacturer', 'part_id']
class Meta:
ordering = ('device__id', 'parent__id', '_name')
unique_together = ('device', 'parent', 'name')

View File

@@ -175,6 +175,12 @@ class Rack(PrimaryModel):
comments = models.TextField(
blank=True
)
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='rack'
)
images = GenericRelation(
to='extras.ImageAttachment'
)

View File

@@ -53,6 +53,12 @@ class Region(NestedGroupModel):
max_length=200,
blank=True
)
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='region'
)
def get_absolute_url(self):
return reverse('dcim:region', args=[self.pk])
@@ -95,6 +101,12 @@ class SiteGroup(NestedGroupModel):
max_length=200,
blank=True
)
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='site_group'
)
def get_absolute_url(self):
return reverse('dcim:sitegroup', args=[self.pk])
@@ -210,6 +222,12 @@ class Site(PrimaryModel):
comments = models.TextField(
blank=True
)
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='site'
)
images = GenericRelation(
to='extras.ImageAttachment'
)
@@ -267,6 +285,12 @@ class Location(NestedGroupModel):
max_length=200,
blank=True
)
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='location'
)
images = GenericRelation(
to='extras.ImageAttachment'
)

View File

@@ -446,10 +446,18 @@ class CableTraceSVG:
if connector is not None:
# Cable
cable_labels = [
f'Cable {connector}',
connector.get_status_display()
]
if connector.type:
cable_labels.append(connector.get_type_display())
if connector.length and connector.length_unit:
cable_labels.append(f'{connector.length} {connector.get_length_unit_display()}')
cable = self._draw_cable(
color=connector.color or '000000',
url=connector.get_absolute_url(),
labels=[f'Cable {connector}', connector.get_status_display()]
labels=cable_labels
)
connectors.append(cable)

View File

@@ -242,10 +242,6 @@ class DeviceComponentTable(BaseTable):
linkify=True,
order_by=('_name',)
)
cable = tables.Column(
linkify=True
)
mark_connected = BooleanColumn()
class Meta(BaseTable.Meta):
order_by = ('device', 'name')
@@ -292,10 +288,10 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta):
model = ConsolePort
fields = (
'pk', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'pk', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'speed', 'description')
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
class DeviceConsolePortTable(ConsolePortTable):
@@ -336,10 +332,10 @@ class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta):
model = ConsoleServerPort
fields = (
'pk', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'pk', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'speed', 'description')
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
class DeviceConsoleServerPortTable(ConsoleServerPortTable):
@@ -381,10 +377,10 @@ class PowerPortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta):
model = PowerPort
fields = (
'pk', 'device', 'name', 'label', 'type', 'description', 'mark_connected', 'maximum_draw', 'allocated_draw',
'pk', 'name', 'device', 'label', 'type', 'description', 'mark_connected', 'maximum_draw', 'allocated_draw',
'cable', 'cable_color', 'cable_peer', 'connection', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
class DevicePowerPortTable(PowerPortTable):
@@ -432,10 +428,10 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta):
model = PowerOutlet
fields = (
'pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected', 'cable',
'pk', 'name', 'device', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected', 'cable',
'cable_color', 'cable_peer', 'connection', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')
default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description')
class DevicePowerOutletTable(PowerOutletTable):
@@ -494,11 +490,11 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
class Meta(DeviceComponentTable.Meta):
model = Interface
fields = (
'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
'pk', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
'untagged_vlan', 'tagged_vlans',
)
default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
class DeviceInterfaceTable(InterfaceTable):
@@ -563,11 +559,11 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
class Meta(DeviceComponentTable.Meta):
model = FrontPort
fields = (
'pk', 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
'mark_connected', 'cable', 'cable_color', 'cable_peer', 'tags',
)
default_columns = (
'pk', 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
)
@@ -614,10 +610,10 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):
class Meta(DeviceComponentTable.Meta):
model = RearPort
fields = (
'pk', 'device', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable',
'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable',
'cable_color', 'cable_peer', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'color', 'description')
default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description')
class DeviceRearPortTable(RearPortTable):
@@ -666,8 +662,8 @@ class DeviceBayTable(DeviceComponentTable):
class Meta(DeviceComponentTable.Meta):
model = DeviceBay
fields = ('pk', 'device', 'name', 'label', 'status', 'installed_device', 'description', 'tags')
default_columns = ('pk', 'device', 'name', 'label', 'status', 'installed_device', 'description')
fields = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags')
default_columns = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description')
class DeviceDeviceBayTable(DeviceBayTable):
@@ -712,10 +708,10 @@ class InventoryItemTable(DeviceComponentTable):
class Meta(BaseTable.Meta):
model = InventoryItem
fields = (
'pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
'pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
'discovered', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
default_columns = ('pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
class DeviceInventoryItemTable(InventoryItemTable):

View File

@@ -41,9 +41,19 @@ DEVICEBAY_STATUS = """
"""
INTERFACE_IPADDRESSES = """
{% for ip in record.ip_addresses.all %}
<a href="{{ ip.get_absolute_url }}">{{ ip }}</a><br />
{% endfor %}
<div class="table-badge-group">
{% for ip in record.ip_addresses.all %}
<a
class="table-badge{% if ip.status != 'active' %} badge bg-{{ ip.get_status_class }}{% elif ip.role %} badge bg-{{ ip.get_role_class }}{% endif %}"
href="{{ ip.get_absolute_url }}"
{% if ip.status != 'active'%}data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_status_display }}"
{% elif ip.role %}data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_role_display }}"
{% endif %}
>
{{ ip }}
</a>
{% endfor %}
</div>
"""
INTERFACE_TAGGED_VLANS = """

View File

@@ -1512,10 +1512,18 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
locations = (
Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Location 2', slug='location-2', site=sites[1]),
Location(name='Location 3', slug='location-3', site=sites[2]),
)
for location in locations:
location.save()
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -1584,6 +1592,13 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'device': [devices[0].name, devices[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cabled(self):
params = {'cabled': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1624,10 +1639,18 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
locations = (
Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Location 2', slug='location-2', site=sites[1]),
Location(name='Location 3', slug='location-3', site=sites[2]),
)
for location in locations:
location.save()
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -1689,6 +1712,13 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -1736,10 +1766,18 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
locations = (
Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Location 2', slug='location-2', site=sites[1]),
Location(name='Location 3', slug='location-3', site=sites[2]),
)
for location in locations:
location.save()
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -1809,6 +1847,13 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -1856,10 +1901,18 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
locations = (
Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Location 2', slug='location-2', site=sites[1]),
Location(name='Location 3', slug='location-3', site=sites[2]),
)
for location in locations:
location.save()
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -1925,6 +1978,13 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -1972,10 +2032,18 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
locations = (
Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Location 2', slug='location-2', site=sites[1]),
Location(name='Location 3', slug='location-3', site=sites[2]),
)
for location in locations:
location.save()
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -2082,6 +2150,13 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -2143,10 +2218,18 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
locations = (
Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Location 2', slug='location-2', site=sites[1]),
Location(name='Location 3', slug='location-3', site=sites[2]),
)
for location in locations:
location.save()
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -2217,6 +2300,13 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -2264,10 +2354,18 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
locations = (
Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Location 2', slug='location-2', site=sites[1]),
Location(name='Location 3', slug='location-3', site=sites[2]),
)
for location in locations:
location.save()
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -2332,6 +2430,13 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -2379,10 +2484,18 @@ class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests):
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
locations = (
Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Location 2', slug='location-2', site=sites[1]),
Location(name='Location 3', slug='location-3', site=sites[2]),
)
for location in locations:
location.save()
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
)
Device.objects.bulk_create(devices)
@@ -2426,6 +2539,13 @@ class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -2474,10 +2594,18 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
)
Site.objects.bulk_create(sites)
locations = (
Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Location 2', slug='location-2', site=sites[1]),
Location(name='Location 3', slug='location-3', site=sites[2]),
)
for location in locations:
location.save()
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]),
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
)
Device.objects.bulk_create(devices)
@@ -2541,13 +2669,19 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_device(self):
# TODO: Allow multiple values
device = Device.objects.first()
params = {'device_id': device.pk}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'device': device.name}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'device': [devices[0].name, devices[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_parent_id(self):
parent_items = InventoryItem.objects.filter(parent__isnull=True)[:2]

View File

@@ -1469,7 +1469,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
'enabled': False,
'lag': interfaces[3].pk,
'mac_address': EUI('01:02:03:04:05:06'),
'mtu': 2000,
'mtu': 65000,
'mgmt_only': True,
'description': 'A front port',
'mode': InterfaceModeChoices.MODE_TAGGED,
@@ -1741,10 +1741,10 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
}
cls.csv_data = (
"device,name",
"Device 1,Inventory Item 4",
"Device 1,Inventory Item 5",
"Device 1,Inventory Item 6",
"device,name,parent",
"Device 1,Inventory Item 4,Inventory Item 1",
"Device 1,Inventory Item 5,Inventory Item 2",
"Device 1,Inventory Item 6,Inventory Item 3",
)

View File

@@ -696,6 +696,9 @@ class ManufacturerView(generic.ObjectView):
).annotate(
instance_count=count_related(Device, 'device_type')
)
inventory_items = InventoryItem.objects.restrict(request.user, 'view').filter(
manufacturer=instance
)
devicetypes_table = tables.DeviceTypeTable(devicetypes)
devicetypes_table.columns.hide('manufacturer')
@@ -703,6 +706,7 @@ class ManufacturerView(generic.ObjectView):
return {
'devicetypes_table': devicetypes_table,
'inventory_item_count': inventory_items.count(),
}
@@ -872,7 +876,6 @@ class ConsolePortTemplateCreateView(generic.ComponentCreateView):
queryset = ConsolePortTemplate.objects.all()
form = forms.ConsolePortTemplateCreateForm
model_form = forms.ConsolePortTemplateForm
template_name = 'dcim/device_component_add.html'
class ConsolePortTemplateEditView(generic.ObjectEditView):
@@ -907,7 +910,6 @@ class ConsoleServerPortTemplateCreateView(generic.ComponentCreateView):
queryset = ConsoleServerPortTemplate.objects.all()
form = forms.ConsoleServerPortTemplateCreateForm
model_form = forms.ConsoleServerPortTemplateForm
template_name = 'dcim/device_component_add.html'
class ConsoleServerPortTemplateEditView(generic.ObjectEditView):
@@ -942,7 +944,6 @@ class PowerPortTemplateCreateView(generic.ComponentCreateView):
queryset = PowerPortTemplate.objects.all()
form = forms.PowerPortTemplateCreateForm
model_form = forms.PowerPortTemplateForm
template_name = 'dcim/device_component_add.html'
class PowerPortTemplateEditView(generic.ObjectEditView):
@@ -977,7 +978,6 @@ class PowerOutletTemplateCreateView(generic.ComponentCreateView):
queryset = PowerOutletTemplate.objects.all()
form = forms.PowerOutletTemplateCreateForm
model_form = forms.PowerOutletTemplateForm
template_name = 'dcim/device_component_add.html'
class PowerOutletTemplateEditView(generic.ObjectEditView):
@@ -1012,7 +1012,6 @@ class InterfaceTemplateCreateView(generic.ComponentCreateView):
queryset = InterfaceTemplate.objects.all()
form = forms.InterfaceTemplateCreateForm
model_form = forms.InterfaceTemplateForm
template_name = 'dcim/device_component_add.html'
class InterfaceTemplateEditView(generic.ObjectEditView):
@@ -1047,7 +1046,6 @@ class FrontPortTemplateCreateView(generic.ComponentCreateView):
queryset = FrontPortTemplate.objects.all()
form = forms.FrontPortTemplateCreateForm
model_form = forms.FrontPortTemplateForm
template_name = 'dcim/device_component_add.html'
class FrontPortTemplateEditView(generic.ObjectEditView):
@@ -1082,7 +1080,6 @@ class RearPortTemplateCreateView(generic.ComponentCreateView):
queryset = RearPortTemplate.objects.all()
form = forms.RearPortTemplateCreateForm
model_form = forms.RearPortTemplateForm
template_name = 'dcim/device_component_add.html'
class RearPortTemplateEditView(generic.ObjectEditView):
@@ -1117,7 +1114,6 @@ class DeviceBayTemplateCreateView(generic.ComponentCreateView):
queryset = DeviceBayTemplate.objects.all()
form = forms.DeviceBayTemplateCreateForm
model_form = forms.DeviceBayTemplateForm
template_name = 'dcim/device_component_add.html'
class DeviceBayTemplateEditView(generic.ObjectEditView):
@@ -1634,7 +1630,6 @@ class ConsolePortCreateView(generic.ComponentCreateView):
queryset = ConsolePort.objects.all()
form = forms.ConsolePortCreateForm
model_form = forms.ConsolePortForm
template_name = 'dcim/device_component_add.html'
class ConsolePortEditView(generic.ObjectEditView):
@@ -1694,7 +1689,6 @@ class ConsoleServerPortCreateView(generic.ComponentCreateView):
queryset = ConsoleServerPort.objects.all()
form = forms.ConsoleServerPortCreateForm
model_form = forms.ConsoleServerPortForm
template_name = 'dcim/device_component_add.html'
class ConsoleServerPortEditView(generic.ObjectEditView):
@@ -1754,7 +1748,6 @@ class PowerPortCreateView(generic.ComponentCreateView):
queryset = PowerPort.objects.all()
form = forms.PowerPortCreateForm
model_form = forms.PowerPortForm
template_name = 'dcim/device_component_add.html'
class PowerPortEditView(generic.ObjectEditView):
@@ -1814,7 +1807,6 @@ class PowerOutletCreateView(generic.ComponentCreateView):
queryset = PowerOutlet.objects.all()
form = forms.PowerOutletCreateForm
model_form = forms.PowerOutletForm
template_name = 'dcim/device_component_add.html'
class PowerOutletEditView(generic.ObjectEditView):
@@ -1909,28 +1901,30 @@ class InterfaceCreateView(generic.ComponentCreateView):
queryset = Interface.objects.all()
form = forms.InterfaceCreateForm
model_form = forms.InterfaceForm
template_name = 'dcim/device_component_add.html'
template_name = 'dcim/interface_create.html'
def post(self, request):
"""
Override inherited post() method to handle request to assign newly created
interface objects (first object) to an IP Address object.
"""
logger = logging.getLogger('netbox.dcim.views.InterfaceCreateView')
form = self.form(request.POST, initial=request.GET)
new_objs = self.validate_form(request, form)
if form.is_valid() and not form.errors:
if '_addanother' in request.POST:
return redirect(request.get_full_path())
elif new_objs is not None and '_assignip' in request.POST and len(new_objs) >= 1 and request.user.has_perm('ipam.add_ipaddress'):
elif new_objs is not None and '_assignip' in request.POST and len(new_objs) >= 1 and \
request.user.has_perm('ipam.add_ipaddress'):
first_obj = new_objs[0].pk
return redirect(f'/ipam/ip-addresses/add/?interface={first_obj}&return_url={self.get_return_url(request)}')
return redirect(
f'/ipam/ip-addresses/add/?interface={first_obj}&return_url={self.get_return_url(request)}'
)
else:
return redirect(self.get_return_url(request))
return render(request, self.template_name, {
'component_type': self.queryset.model._meta.verbose_name,
'obj_type': self.queryset.model._meta.verbose_name,
'form': form,
'return_url': self.get_return_url(request),
})
@@ -1993,7 +1987,6 @@ class FrontPortCreateView(generic.ComponentCreateView):
queryset = FrontPort.objects.all()
form = forms.FrontPortCreateForm
model_form = forms.FrontPortForm
template_name = 'dcim/device_component_add.html'
class FrontPortEditView(generic.ObjectEditView):
@@ -2053,7 +2046,6 @@ class RearPortCreateView(generic.ComponentCreateView):
queryset = RearPort.objects.all()
form = forms.RearPortCreateForm
model_form = forms.RearPortForm
template_name = 'dcim/device_component_add.html'
class RearPortEditView(generic.ObjectEditView):
@@ -2113,7 +2105,6 @@ class DeviceBayCreateView(generic.ComponentCreateView):
queryset = DeviceBay.objects.all()
form = forms.DeviceBayCreateForm
model_form = forms.DeviceBayForm
template_name = 'dcim/device_component_add.html'
class DeviceBayEditView(generic.ObjectEditView):
@@ -2239,7 +2230,6 @@ class InventoryItemCreateView(generic.ComponentCreateView):
queryset = InventoryItem.objects.all()
form = forms.InventoryItemCreateForm
model_form = forms.InventoryItemForm
template_name = 'dcim/device_component_add.html'
class InventoryItemDeleteView(generic.ObjectDeleteView):
@@ -2537,6 +2527,7 @@ class ConsoleConnectionsListView(generic.ObjectListView):
filterset_form = forms.ConsoleConnectionFilterForm
table = tables.ConsoleConnectionTable
template_name = 'dcim/connections_list.html'
action_buttons = ('export',)
def extra_context(self):
return {
@@ -2550,6 +2541,7 @@ class PowerConnectionsListView(generic.ObjectListView):
filterset_form = forms.PowerConnectionFilterForm
table = tables.PowerConnectionTable
template_name = 'dcim/connections_list.html'
action_buttons = ('export',)
def extra_context(self):
return {
@@ -2558,15 +2550,12 @@ class PowerConnectionsListView(generic.ObjectListView):
class InterfaceConnectionsListView(generic.ObjectListView):
queryset = Interface.objects.filter(
# Avoid duplicate connections by only selecting the lower PK in a connected pair
_path__isnull=False,
pk__lt=F('_path__destination_id')
).order_by('device')
queryset = Interface.objects.filter(_path__isnull=False).order_by('device')
filterset = filtersets.InterfaceConnectionFilterSet
filterset_form = forms.InterfaceConnectionFilterForm
table = tables.InterfaceConnectionTable
template_name = 'dcim/connections_list.html'
action_buttons = ('export',)
def extra_context(self):
return {

View File

@@ -46,28 +46,40 @@ class CustomFieldFilterLogicChoices(ChoiceSet):
class CustomLinkButtonClassChoices(ChoiceSet):
CLASS_DEFAULT = 'outline-dark'
CLASS_PRIMARY = 'primary'
CLASS_SUCCESS = 'success'
CLASS_INFO = 'info'
CLASS_WARNING = 'warning'
CLASS_DANGER = 'danger'
CLASS_LINK = 'link'
CLASS_LINK = 'ghost-dark'
CLASS_BLUE = 'blue'
CLASS_INDIGO = 'indigo'
CLASS_PURPLE = 'purple'
CLASS_PINK = 'pink'
CLASS_RED = 'red'
CLASS_ORANGE = 'orange'
CLASS_YELLOW = 'yellow'
CLASS_GREEN = 'green'
CLASS_TEAL = 'teal'
CLASS_CYAN = 'cyan'
CLASS_GRAY = 'secondary'
CHOICES = (
(CLASS_DEFAULT, 'Default'),
(CLASS_PRIMARY, 'Primary (blue)'),
(CLASS_SUCCESS, 'Success (green)'),
(CLASS_INFO, 'Info (aqua)'),
(CLASS_WARNING, 'Warning (orange)'),
(CLASS_DANGER, 'Danger (red)'),
(CLASS_LINK, 'None (link)'),
(CLASS_LINK, 'Link'),
(CLASS_BLUE, 'Blue'),
(CLASS_INDIGO, 'Indigo'),
(CLASS_PURPLE, 'Purple'),
(CLASS_PINK, 'Pink'),
(CLASS_RED, 'Red'),
(CLASS_ORANGE, 'Orange'),
(CLASS_YELLOW, 'Yellow'),
(CLASS_GREEN, 'Green'),
(CLASS_TEAL, 'Teal'),
(CLASS_CYAN, 'Cyan'),
(CLASS_GRAY, 'Gray'),
)
#
# ObjectChanges
#
class ObjectChangeActionChoices(ChoiceSet):
ACTION_CREATE = 'create'

View File

@@ -2,7 +2,7 @@ from contextlib import contextmanager
from django.db.models.signals import m2m_changed, pre_delete, post_save
from extras.signals import _handle_changed_object, _handle_deleted_object
from extras.signals import clear_webhooks, _clear_webhook_queue, _handle_changed_object, _handle_deleted_object
from utilities.utils import curry
from .webhooks import flush_webhooks
@@ -20,11 +20,13 @@ def change_logging(request):
# Curry signals receivers to pass the current request
handle_changed_object = curry(_handle_changed_object, request, webhook_queue)
handle_deleted_object = curry(_handle_deleted_object, request, webhook_queue)
clear_webhook_queue = curry(_clear_webhook_queue, webhook_queue)
# Connect our receivers to the post_save and post_delete signals.
post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object')
m2m_changed.connect(handle_changed_object, dispatch_uid='handle_changed_object')
pre_delete.connect(handle_deleted_object, dispatch_uid='handle_deleted_object')
clear_webhooks.connect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')
yield
@@ -33,6 +35,7 @@ def change_logging(request):
post_save.disconnect(handle_changed_object, dispatch_uid='handle_changed_object')
m2m_changed.disconnect(handle_changed_object, dispatch_uid='handle_changed_object')
pre_delete.disconnect(handle_deleted_object, dispatch_uid='handle_deleted_object')
clear_webhooks.disconnect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')
# Flush queued webhooks to RQ
flush_webhooks(webhook_queue)

View File

@@ -14,6 +14,7 @@ EXACT_FILTER_TYPES = (
CustomFieldTypeChoices.TYPE_DATE,
CustomFieldTypeChoices.TYPE_INTEGER,
CustomFieldTypeChoices.TYPE_SELECT,
CustomFieldTypeChoices.TYPE_MULTISELECT,
)
@@ -35,7 +36,9 @@ class CustomFieldFilter(django_filters.Filter):
self.field_name = f'custom_field_data__{self.field_name}'
if custom_field.type not in EXACT_FILTER_TYPES:
if custom_field.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
self.lookup_expr = 'has_key'
elif custom_field.type not in EXACT_FILTER_TYPES:
if custom_field.filter_logic == CustomFieldFilterLogicChoices.FILTER_LOOSE:
self.lookup_expr = 'icontains'

View File

@@ -367,7 +367,19 @@ class JobResultFilterSet(BaseFilterSet):
#
class ContentTypeFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
class Meta:
model = ContentType
fields = ['id', 'app_label', 'model']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(app_label__icontains=value) |
Q(model__icontains=value)
)

View File

@@ -77,17 +77,25 @@ class CustomFieldBulkEditForm(BootstrapMixin, BulkEditForm):
class CustomFieldFilterForm(BootstrapMixin, forms.Form):
field_groups = [
['q'],
['type', 'content_types'],
['weight', 'required'],
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields')
limit_choices_to=FeatureQuery('custom_fields'),
required=False
)
type = forms.MultipleChoiceField(
choices=CustomFieldTypeChoices,
required=False,
widget=StaticSelectMultiple()
widget=StaticSelectMultiple(),
label=_('Field type')
)
weight = forms.IntegerField(
required=False
@@ -117,6 +125,10 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window')),
('Templates', ('link_text', 'link_url')),
)
widgets = {
'link_text': forms.Textarea(attrs={'class': 'font-monospace'}),
'link_url': forms.Textarea(attrs={'class': 'font-monospace'}),
}
help_texts = {
'link_text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>. '
'Links which render as empty text will not be displayed.',
@@ -167,12 +179,18 @@ class CustomLinkBulkEditForm(BootstrapMixin, BulkEditForm):
class CustomLinkFilterForm(BootstrapMixin, forms.Form):
field_groups = [
['content_type'],
['weight', 'new_window'],
['q'],
['content_type', 'weight', 'new_window'],
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
content_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields')
limit_choices_to=FeatureQuery('custom_fields'),
required=False
)
weight = forms.IntegerField(
required=False
@@ -203,6 +221,9 @@ class ExportTemplateForm(BootstrapMixin, forms.ModelForm):
('Template', ('template_code',)),
('Rendering', ('mime_type', 'file_extension', 'as_attachment')),
)
widgets = {
'template_code': forms.Textarea(attrs={'class': 'font-monospace'}),
}
class ExportTemplateCSVForm(CSVModelForm):
@@ -252,15 +273,22 @@ class ExportTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
class ExportTemplateFilterForm(BootstrapMixin, forms.Form):
field_groups = [
['content_type', 'mime_type'],
['file_extension', 'as_attachment'],
['q'],
['content_type', 'mime_type', 'file_extension', 'as_attachment'],
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
content_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields')
limit_choices_to=FeatureQuery('custom_fields'),
required=False
)
mime_type = forms.CharField(
required=False
required=False,
label=_('MIME type')
)
file_extension = forms.CharField(
required=False
@@ -295,6 +323,10 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
)),
('SSL', ('ssl_verification', 'ca_file_path')),
)
widgets = {
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
}
class WebhookCSVForm(CSVModelForm):
@@ -358,17 +390,25 @@ class WebhookBulkEditForm(BootstrapMixin, BulkEditForm):
class WebhookFilterForm(BootstrapMixin, forms.Form):
field_groups = [
['content_types', 'http_method'],
['enabled', 'type_create', 'type_update', 'type_delete'],
['q'],
['content_types', 'http_method', 'enabled'],
['type_create', 'type_update', 'type_delete'],
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields')
limit_choices_to=FeatureQuery('custom_fields'),
required=False
)
http_method = forms.MultipleChoiceField(
choices=WebhookHttpMethodChoices,
required=False,
widget=StaticSelectMultiple()
widget=StaticSelectMultiple(),
label=_('HTTP method')
)
enabled = forms.NullBooleanField(
required=False,
@@ -456,7 +496,11 @@ class CustomFieldModelForm(CustomFieldsMixin, forms.ModelForm):
# Save custom field data on instance
for cf_name in self.custom_fields:
self.instance.custom_field_data[cf_name[3:]] = self.cleaned_data.get(cf_name)
key = cf_name[3:] # Strip "cf_" from field name
value = self.cleaned_data.get(cf_name)
empty_values = self.fields[cf_name].empty_values
# Convert "empty" values to null
self.instance.custom_field_data[key] = value if value not in empty_values else None
return super().clean()
@@ -495,12 +539,14 @@ class CustomFieldModelFilterForm(forms.Form):
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(
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
)
for cf in custom_fields:
field_name = 'cf_{}'.format(cf.name)
self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False)
self.custom_field_filters.append(field_name)
#
@@ -663,71 +709,84 @@ class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
field_order = [
'region_id', 'site_group_id', 'site_id', 'role_id', 'platform_id', 'cluster_group_id', 'cluster_id',
'tenant_group_id', 'tenant_id',
]
field_groups = [
['q', 'tag'],
['region_id', 'site_group_id', 'site_id'],
['device_type_id', 'role_id', 'platform_id'],
['device_type_id', 'platform_id', 'role_id'],
['cluster_group_id', 'cluster_id'],
['tenant_group_id', 'tenant_id', 'tag']
['tenant_group_id', 'tenant_id']
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Regions')
label=_('Regions'),
fetch_trigger='open'
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site groups')
label=_('Site groups'),
fetch_trigger='open'
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
label=_('Sites')
label=_('Sites'),
fetch_trigger='open'
)
device_type_id = DynamicModelMultipleChoiceField(
queryset=DeviceType.objects.all(),
required=False,
label=_('Device types')
label=_('Device types'),
fetch_trigger='open'
)
role_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
label=_('Roles')
label=_('Roles'),
fetch_trigger='open'
)
platform_id = DynamicModelMultipleChoiceField(
queryset=Platform.objects.all(),
required=False,
label=_('Platforms')
label=_('Platforms'),
fetch_trigger='open'
)
cluster_group_id = DynamicModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(),
required=False,
label=_('Cluster groups')
label=_('Cluster groups'),
fetch_trigger='open'
)
cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(),
required=False,
label=_('Clusters')
label=_('Clusters'),
fetch_trigger='open'
)
tenant_group_id = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
label=_('Tenant groups')
label=_('Tenant groups'),
fetch_trigger='open'
)
tenant_id = DynamicModelMultipleChoiceField(
queryset=Tenant.objects.all(),
required=False,
label=_('Tenant')
label=_('Tenant'),
fetch_trigger='open'
)
tag = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
to_field_name='slug',
required=False,
label=_('Tags')
label=_('Tags'),
fetch_trigger='open'
)
@@ -801,9 +860,15 @@ class JournalEntryBulkEditForm(BootstrapMixin, BulkEditForm):
class JournalEntryFilterForm(BootstrapMixin, forms.Form):
model = JournalEntry
field_groups = [
['q'],
['created_before', 'created_after', 'created_by_id'],
['assigned_object_type_id', 'kind']
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
created_after = forms.DateTimeField(
required=False,
label=_('After'),
@@ -820,7 +885,8 @@ class JournalEntryFilterForm(BootstrapMixin, forms.Form):
label=_('User'),
widget=APISelectMultiple(
api_url='/api/users/users/',
)
),
fetch_trigger='open'
)
assigned_object_type_id = DynamicModelMultipleChoiceField(
queryset=ContentType.objects.all(),
@@ -828,7 +894,8 @@ class JournalEntryFilterForm(BootstrapMixin, forms.Form):
label=_('Object Type'),
widget=APISelectMultiple(
api_url='/api/extras/content-types/',
)
),
fetch_trigger='open'
)
kind = forms.ChoiceField(
choices=add_blank_choice(JournalEntryKindChoices),
@@ -844,9 +911,15 @@ class JournalEntryFilterForm(BootstrapMixin, forms.Form):
class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
model = ObjectChange
field_groups = [
['q'],
['time_before', 'time_after', 'action'],
['user_id', 'changed_object_type_id'],
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
time_after = forms.DateTimeField(
required=False,
label=_('After'),
@@ -868,7 +941,8 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
label=_('User'),
widget=APISelectMultiple(
api_url='/api/users/users/',
)
),
fetch_trigger='open'
)
changed_object_type_id = DynamicModelMultipleChoiceField(
queryset=ContentType.objects.all(),
@@ -876,7 +950,8 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
label=_('Object Type'),
widget=APISelectMultiple(
api_url='/api/extras/content-types/',
)
),
fetch_trigger='open'
)

View File

@@ -0,0 +1,53 @@
import graphene
from graphene.types.generic import GenericScalar
__all__ = (
'ChangelogMixin',
'ConfigContextMixin',
'CustomFieldsMixin',
'ImageAttachmentsMixin',
'JournalEntriesMixin',
'TagsMixin',
)
class ChangelogMixin:
changelog = graphene.List('extras.graphql.types.ObjectChangeType')
def resolve_changelog(self, info):
return self.object_changes.restrict(info.context.user, 'view')
class ConfigContextMixin:
config_context = GenericScalar()
def resolve_config_context(self, info):
return self.get_config_context()
class CustomFieldsMixin:
custom_fields = GenericScalar()
def resolve_custom_fields(self, info):
return self.custom_field_data
class ImageAttachmentsMixin:
image_attachments = graphene.List('extras.graphql.types.ImageAttachmentType')
def resolve_image_attachments(self, info):
return self.images.restrict(info.context.user, 'view')
class JournalEntriesMixin:
journal_entries = graphene.List('extras.graphql.types.JournalEntryType')
def resolve_journal_entries(self, info):
return self.journal_entries.restrict(info.context.user, 'view')
class TagsMixin:
tags = graphene.List('extras.graphql.types.TagType')
def resolve_tags(self, info):
return self.tags.all()

View File

@@ -1,5 +1,5 @@
from extras import filtersets, models
from netbox.graphql.types import BaseObjectType
from netbox.graphql.types import BaseObjectType, ObjectType
__all__ = (
'ConfigContextType',
@@ -8,12 +8,13 @@ __all__ = (
'ExportTemplateType',
'ImageAttachmentType',
'JournalEntryType',
'ObjectChangeType',
'TagType',
'WebhookType',
)
class ConfigContextType(BaseObjectType):
class ConfigContextType(ObjectType):
class Meta:
model = models.ConfigContext
@@ -21,7 +22,7 @@ class ConfigContextType(BaseObjectType):
filterset_class = filtersets.ConfigContextFilterSet
class CustomFieldType(BaseObjectType):
class CustomFieldType(ObjectType):
class Meta:
model = models.CustomField
@@ -29,7 +30,7 @@ class CustomFieldType(BaseObjectType):
filterset_class = filtersets.CustomFieldFilterSet
class CustomLinkType(BaseObjectType):
class CustomLinkType(ObjectType):
class Meta:
model = models.CustomLink
@@ -37,7 +38,7 @@ class CustomLinkType(BaseObjectType):
filterset_class = filtersets.CustomLinkFilterSet
class ExportTemplateType(BaseObjectType):
class ExportTemplateType(ObjectType):
class Meta:
model = models.ExportTemplate
@@ -53,7 +54,7 @@ class ImageAttachmentType(BaseObjectType):
filterset_class = filtersets.ImageAttachmentFilterSet
class JournalEntryType(BaseObjectType):
class JournalEntryType(ObjectType):
class Meta:
model = models.JournalEntry
@@ -61,7 +62,15 @@ class JournalEntryType(BaseObjectType):
filterset_class = filtersets.JournalEntryFilterSet
class TagType(BaseObjectType):
class ObjectChangeType(BaseObjectType):
class Meta:
model = models.ObjectChange
fields = '__all__'
filterset_class = filtersets.ObjectChangeFilterSet
class TagType(ObjectType):
class Meta:
model = models.Tag
@@ -69,7 +78,7 @@ class TagType(BaseObjectType):
filterset_class = filtersets.TagFilterSet
class WebhookType(BaseObjectType):
class WebhookType(ObjectType):
class Meta:
model = models.Webhook

View File

@@ -37,12 +37,10 @@ class WebhookHandler(BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(b'Webhook received!\n')
request_counter += 1
# Print the request headers to stdout
# Print the request headers
if self.show_headers:
for k, v in self.headers.items():
print('{}: {}'.format(k, v))
print(f'{k}: {v}')
print()
# Print the request body (if any)
@@ -55,8 +53,11 @@ class WebhookHandler(BaseHTTPRequestHandler):
else:
print('(No body)')
print(f'Completed request #{request_counter}')
print('------------')
request_counter += 1
class Command(BaseCommand):
help = "Start a simple listener to display received HTTP requests"

View File

@@ -0,0 +1,26 @@
from django.db import migrations
def clear_secrets_changelog(apps, schema_editor):
"""
Delete all ObjectChange records referencing a model within the old secrets app (pre-v3.0).
"""
ContentType = apps.get_model('contenttypes', 'ContentType')
ObjectChange = apps.get_model('extras', 'ObjectChange')
content_type_ids = ContentType.objects.filter(app_label='secrets').values_list('id', flat=True)
ObjectChange.objects.filter(changed_object_type__in=content_type_ids).delete()
class Migration(migrations.Migration):
dependencies = [
('extras', '0061_extras_change_logging'),
]
operations = [
migrations.RunPython(
code=clear_secrets_changelog,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -35,7 +35,6 @@ class CustomField(ChangeLoggedModel):
content_types = models.ManyToManyField(
to=ContentType,
related_name='custom_fields',
verbose_name='Object(s)',
limit_choices_to=FeatureQuery('custom_fields'),
help_text='The object(s) to which this field applies.'
)
@@ -125,6 +124,30 @@ class CustomField(ChangeLoggedModel):
# Cache instance's original name so we can check later whether it has changed
self._name = self.name
def populate_initial_data(self, content_types):
"""
Populate initial custom field data upon either a) the creation of a new CustomField, or
b) the assignment of an existing CustomField to new object types.
"""
for ct in content_types:
model = ct.model_class()
instances = model.objects.exclude(**{f'custom_field_data__contains': self.name})
for instance in instances:
instance.custom_field_data[self.name] = self.default
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
def remove_stale_data(self, content_types):
"""
Delete custom field data which is no longer relevant (either because the CustomField is
no longer assigned to a model, or because it has been deleted).
"""
for ct in content_types:
model = ct.model_class()
instances = model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False})
for instance in instances:
del(instance.custom_field_data[self.name])
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
def rename_object_data(self, old_name, new_name):
"""
Called when a CustomField has been renamed. Updates all assigned object data.
@@ -137,17 +160,6 @@ class CustomField(ChangeLoggedModel):
instance.custom_field_data[new_name] = instance.custom_field_data.pop(old_name)
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
def remove_stale_data(self, content_types):
"""
Delete custom field data which is no longer relevant (either because the CustomField is
no longer assigned to a model, or because it has been deleted).
"""
for ct in content_types:
model = ct.model_class()
for obj in model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False}):
del(obj.custom_field_data[self.name])
obj.save()
def clean(self):
super().clean()

View File

@@ -91,7 +91,7 @@ class Webhook(ChangeLoggedModel):
blank=True,
help_text="User-supplied HTTP headers to be sent with the request in addition to the HTTP content type. "
"Headers should be defined in the format <code>Name: Value</code>. Jinja2 template processing is "
"support with the same context as the request body (below)."
"supported with the same context as the request body (below)."
)
body_template = models.TextField(
blank=True,
@@ -249,7 +249,8 @@ class ExportTemplate(ChangeLoggedModel):
blank=True
)
template_code = models.TextField(
help_text='The list of objects being exported is passed as a context variable named <code>queryset</code>.'
help_text='Jinja2 template code. The list of objects being exported is passed as a context variable named '
'<code>queryset</code>.'
)
mime_type = models.CharField(
max_length=50,

View File

@@ -1,9 +1,10 @@
import logging
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.dispatch import receiver
from django.dispatch import receiver, Signal
from django_prometheus.models import model_deletes, model_inserts, model_updates
from prometheus_client import Counter
from netbox.signals import post_clean
from .choices import ObjectChangeActionChoices
@@ -15,6 +16,10 @@ from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook
# Change logging/webhooks
#
# Define a custom signal that can be sent to clear any queued webhooks
clear_webhooks = Signal()
def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs):
"""
Fires when an object is created or updated.
@@ -95,10 +100,28 @@ def _handle_deleted_object(request, webhook_queue, sender, instance, **kwargs):
model_deletes.labels(instance._meta.model_name).inc()
def _clear_webhook_queue(webhook_queue, sender, **kwargs):
"""
Delete any queued webhooks (e.g. because of an aborted bulk transaction)
"""
logger = logging.getLogger('webhooks')
logger.info(f"Clearing {len(webhook_queue)} queued webhooks ({sender})")
webhook_queue.clear()
#
# Custom fields
#
def handle_cf_added_obj_types(instance, action, pk_set, **kwargs):
"""
Handle the population of default/null values when a CustomField is added to one or more ContentTypes.
"""
if action == 'post_add':
instance.populate_initial_data(ContentType.objects.filter(pk__in=pk_set))
def handle_cf_removed_obj_types(instance, action, pk_set, **kwargs):
"""
Handle the cleanup of old custom field data when a CustomField is removed from one or more ContentTypes.
@@ -122,9 +145,10 @@ def handle_cf_deleted(instance, **kwargs):
instance.remove_stale_data(instance.content_types.all())
m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through)
post_save.connect(handle_cf_renamed, sender=CustomField)
pre_delete.connect(handle_cf_deleted, sender=CustomField)
m2m_changed.connect(handle_cf_added_obj_types, sender=CustomField.content_types.through)
m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through)
#

View File

@@ -2,7 +2,8 @@ import django_tables2 as tables
from django.conf import settings
from utilities.tables import (
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ContentTypeColumn, ToggleColumn,
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ContentTypeColumn, ContentTypesColumn,
ToggleColumn,
)
from .models import *
@@ -37,14 +38,16 @@ class CustomFieldTable(BaseTable):
name = tables.Column(
linkify=True
)
content_types = ContentTypesColumn()
required = BooleanColumn()
class Meta(BaseTable.Meta):
model = CustomField
fields = (
'pk', 'name', 'label', 'type', 'required', 'weight', 'default', 'description', 'filter_logic', 'choices',
'pk', 'name', 'content_types', 'label', 'type', 'required', 'weight', 'default', 'description',
'filter_logic', 'choices',
)
default_columns = ('pk', 'name', 'label', 'type', 'required', 'description')
default_columns = ('pk', 'name', 'content_types', 'label', 'type', 'required', 'description')
#
@@ -98,19 +101,30 @@ class WebhookTable(BaseTable):
name = tables.Column(
linkify=True
)
content_types = ContentTypesColumn()
enabled = BooleanColumn()
type_create = BooleanColumn()
type_update = BooleanColumn()
type_delete = BooleanColumn()
type_create = BooleanColumn(
verbose_name='Create'
)
type_update = BooleanColumn(
verbose_name='Update'
)
type_delete = BooleanColumn(
verbose_name='Delete'
)
ssl_validation = BooleanColumn(
verbose_name='SSL Validation'
)
class Meta(BaseTable.Meta):
model = Webhook
fields = (
'pk', 'name', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method', 'payload_url',
'secret', 'ssl_validation', 'ca_file_path',
'pk', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method',
'payload_url', 'secret', 'ssl_validation', 'ca_file_path',
)
default_columns = (
'pk', 'name', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method', 'payload_url',
'pk', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method',
'payload_url',
)

View File

@@ -10,10 +10,10 @@ from utilities.utils import render_jinja2
register = template.Library()
LINK_BUTTON = '<a href="{}"{} class="btn btn-sm btn-{} m-1">{}</a>\n'
LINK_BUTTON = '<a href="{}"{} class="btn btn-sm btn-{}">{}</a>\n'
GROUP_BUTTON = """
<div class="dropdown m-1">
<div class="dropdown">
<button
class="btn btn-sm btn-{} dropdown-toggle"
type="button"

View File

@@ -42,8 +42,11 @@ class CustomFieldTest(TestCase):
cf.save()
cf.content_types.set([obj_type])
# Assign a value to the first Site
# 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] = data['field_value']
site.save()
@@ -73,8 +76,11 @@ class CustomFieldTest(TestCase):
cf.save()
cf.content_types.set([obj_type])
# Assign a value to the first Site
# 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] = 'Option A'
site.save()
@@ -675,7 +681,12 @@ class CustomFieldFilterTest(TestCase):
cf.content_types.set([obj_type])
# Selection filtering
cf = CustomField(name='cf8', type=CustomFieldTypeChoices.TYPE_URL, choices=['Foo', 'Bar', 'Baz'])
cf = CustomField(name='cf8', type=CustomFieldTypeChoices.TYPE_SELECT, choices=['Foo', 'Bar', 'Baz'])
cf.save()
cf.content_types.set([obj_type])
# Multiselect filtering
cf = CustomField(name='cf9', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choices=['A', 'AA', 'B', 'C'])
cf.save()
cf.content_types.set([obj_type])
@@ -689,6 +700,7 @@ class CustomFieldFilterTest(TestCase):
'cf6': 'http://foo.example.com/',
'cf7': 'http://foo.example.com/',
'cf8': 'Foo',
'cf9': ['A', 'B'],
}),
Site(name='Site 2', slug='site-2', custom_field_data={
'cf1': 200,
@@ -699,9 +711,9 @@ class CustomFieldFilterTest(TestCase):
'cf6': 'http://bar.example.com/',
'cf7': 'http://bar.example.com/',
'cf8': 'Bar',
'cf9': ['AA', 'B'],
}),
Site(name='Site 3', slug='site-3', custom_field_data={
}),
Site(name='Site 3', slug='site-3'),
])
def test_filter_integer(self):
@@ -724,3 +736,10 @@ class CustomFieldFilterTest(TestCase):
def test_filter_select(self):
self.assertEqual(self.filterset({'cf_cf8': 'Foo'}, self.queryset).qs.count(), 1)
self.assertEqual(self.filterset({'cf_cf8': 'Bar'}, self.queryset).qs.count(), 1)
self.assertEqual(self.filterset({'cf_cf8': 'Baz'}, self.queryset).qs.count(), 0)
def test_filter_multiselect(self):
self.assertEqual(self.filterset({'cf_cf9': 'A'}, self.queryset).qs.count(), 1)
self.assertEqual(self.filterset({'cf_cf9': 'B'}, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset({'cf_cf9': 'C'}, self.queryset).qs.count(), 0)

View File

@@ -0,0 +1,53 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from dcim.forms import SiteForm
from dcim.models import Site
from extras.choices import CustomFieldTypeChoices
from extras.models import CustomField
class CustomFieldModelFormTest(TestCase):
@classmethod
def setUpTestData(cls):
obj_type = ContentType.objects.get_for_model(Site)
CHOICES = ('A', 'B', 'C')
cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT)
cf_text.content_types.set([obj_type])
cf_integer = CustomField.objects.create(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER)
cf_integer.content_types.set([obj_type])
cf_boolean = CustomField.objects.create(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
cf_boolean.content_types.set([obj_type])
cf_date = CustomField.objects.create(name='date', type=CustomFieldTypeChoices.TYPE_DATE)
cf_date.content_types.set([obj_type])
cf_url = CustomField.objects.create(name='url', type=CustomFieldTypeChoices.TYPE_URL)
cf_url.content_types.set([obj_type])
cf_select = CustomField.objects.create(name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choices=CHOICES)
cf_select.content_types.set([obj_type])
cf_multiselect = CustomField.objects.create(name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT,
choices=CHOICES)
cf_multiselect.content_types.set([obj_type])
def test_empty_values(self):
"""
Test that empty custom field values are stored as null
"""
form = SiteForm({
'name': 'Site 1',
'slug': 'site-1',
'status': 'active',
})
self.assertTrue(form.is_valid())
instance = form.save()
for field_type, _ in CustomFieldTypeChoices.CHOICES:
self.assertIn(field_type, instance.custom_field_data)
self.assertIsNone(instance.custom_field_data[field_type])

View File

@@ -75,13 +75,13 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
cls.csv_data = (
"name,content_type,weight,button_class,link_text,link_url",
"Custom Link 4,dcim.site,100,primary,Link 4,http://exmaple.com/?4",
"Custom Link 5,dcim.site,100,primary,Link 5,http://exmaple.com/?5",
"Custom Link 6,dcim.site,100,primary,Link 6,http://exmaple.com/?6",
"Custom Link 4,dcim.site,100,blue,Link 4,http://exmaple.com/?4",
"Custom Link 5,dcim.site,100,blue,Link 5,http://exmaple.com/?5",
"Custom Link 6,dcim.site,100,blue,Link 6,http://exmaple.com/?6",
)
cls.bulk_edit_data = {
'button_class': CustomLinkButtonClassChoices.CLASS_INFO,
'button_class': CustomLinkButtonClassChoices.CLASS_CYAN,
'weight': 200,
}

View File

@@ -176,6 +176,7 @@ class AvailableIPsMixin:
serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={
'request': request,
'parent': parent,
'vrf': parent.vrf,
})
return Response(serializer.data)

View File

@@ -331,7 +331,7 @@ class AvailableIPSerializer(serializers.Serializer):
return OrderedDict([
('family', self.context['parent'].family),
('address', f"{instance}/{self.context['parent'].mask_length}"),
('vrf', self.context['parent'].vrf),
('vrf', vrf),
])

View File

@@ -216,7 +216,7 @@ class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
children = MultiValueNumberFilter(
field_name='_children'
)
mask_length = django_filters.NumberFilter(
mask_length = MultiValueNumberFilter(
field_name='prefix',
lookup_expr='net_mask_length'
)

View File

@@ -4,15 +4,16 @@ from django.utils.translation import gettext as _
from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
from extras.forms import (
AddRemoveTagsForm, CustomFieldModelBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldModelFilterForm,
AddRemoveTagsForm, CustomFieldModelBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm,
CustomFieldModelFilterForm,
)
from extras.models import Tag
from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, ContentTypeChoiceField, CSVChoiceField,
CSVModelChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField,
NumericArrayField, ReturnURLForm, SlugField, StaticSelect, StaticSelectMultiple, TagFilterField,
CSVContentTypeField, CSVModelChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
ExpandableIPAddressField, NumericArrayField, SlugField, StaticSelect, StaticSelectMultiple, TagFilterField,
BOOLEAN_WITH_BLANK_CHOICES,
)
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
@@ -106,21 +107,27 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEdi
class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
model = VRF
field_order = ['import_target_id', 'export_target_id', 'tenant_group_id', 'tenant_id']
field_groups = [
['q', 'tag'],
['import_target_id', 'export_target_id'],
['tenant_group_id', 'tenant_id'],
['tag']
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
import_target_id = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
required=False,
label=_('Import targets')
label=_('Import targets'),
fetch_trigger='open'
)
export_target_id = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
required=False,
label=_('Export targets')
label=_('Export targets'),
fetch_trigger='open'
)
tag = TagFilterField(model)
@@ -140,6 +147,10 @@ class RouteTargetForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
fields = [
'name', 'description', 'tenant_group', 'tenant', 'tags',
]
fieldsets = (
('Route Target', ('name', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')),
)
class RouteTargetCSVForm(CustomFieldModelCSVForm):
@@ -177,20 +188,27 @@ class RouteTargetBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode
class RouteTargetFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
model = RouteTarget
field_order = ['name', 'tenant_group_id', 'tenant_id', 'importing_vrfs', 'exporting_vrfs']
field_groups = [
['q', 'tag'],
['importing_vrf_id', 'exporting_vrf_id'],
['tenant_group_id', 'tenant_id'],
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
importing_vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('Imported by VRF')
label=_('Imported by VRF'),
fetch_trigger='open'
)
exporting_vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('Exported by VRF')
label=_('Exported by VRF'),
fetch_trigger='open'
)
tag = TagFilterField(model)
@@ -331,11 +349,16 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB
class AggregateFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
model = Aggregate
field_order = ['family', 'rir', 'tenant_group_id', 'tenant_id']
field_groups = [
['q', 'tag'],
['family', 'rir_id'],
['tenant_group_id', 'tenant_id']
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
family = forms.ChoiceField(
required=False,
choices=add_blank_choice(IPAddressFamilyChoices),
@@ -345,7 +368,8 @@ class AggregateFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFil
rir_id = DynamicModelMultipleChoiceField(
queryset=RIR.objects.all(),
required=False,
label=_('RIR')
label=_('RIR'),
fetch_trigger='open'
)
tag = TagFilterField(model)
@@ -467,11 +491,6 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
'status': StaticSelect(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['vrf'].empty_label = 'Global'
class PrefixCSVForm(CustomFieldModelCSVForm):
vrf = CSVModelChoiceField(
@@ -604,16 +623,18 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulk
class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
model = Prefix
field_order = [
'within_include', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'region_id',
'site_group_id', 'site_id', 'role_id', 'tenant_group_id', 'tenant_id', 'is_pool', 'mark_utilized',
]
field_groups = [
['role_id', 'within_include', 'family', 'mask_length'],
['vrf_id', 'present_in_vrf_id', 'is_pool', 'mark_utilized'],
['q', 'tag'],
['within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized'],
['vrf_id', 'present_in_vrf_id'],
['region_id', 'site_group_id', 'site_id'],
['tenant_group_id', 'tenant_id', 'status', 'tag']
['tenant_group_id', 'tenant_id']
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
mask_length__lte = forms.IntegerField(
widget=forms.HiddenInput()
)
@@ -632,22 +653,24 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilter
label=_('Address family'),
widget=StaticSelect()
)
mask_length = forms.ChoiceField(
mask_length = forms.MultipleChoiceField(
required=False,
choices=PREFIX_MASK_LENGTH_CHOICES,
label=_('Mask length'),
widget=StaticSelect()
widget=StaticSelectMultiple()
)
vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('Assigned VRF'),
null_option='Global'
null_option='Global',
fetch_trigger='open'
)
present_in_vrf_id = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('Present in VRF')
label=_('Present in VRF'),
fetch_trigger='open'
)
status = forms.MultipleChoiceField(
choices=PrefixStatusChoices,
@@ -657,12 +680,14 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilter
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region')
label=_('Region'),
fetch_trigger='open'
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
label=_('Site group'),
fetch_trigger='open'
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -671,13 +696,15 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilter
query_params={
'region_id': '$region_id'
},
label=_('Site')
label=_('Site'),
fetch_trigger='open'
)
role_id = DynamicModelMultipleChoiceField(
queryset=Role.objects.all(),
required=False,
null_option='None',
label=_('Role')
label=_('Role'),
fetch_trigger='open'
)
is_pool = forms.NullBooleanField(
required=False,
@@ -728,11 +755,6 @@ class IPRangeForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
'status': StaticSelect(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['vrf'].empty_label = 'Global'
class IPRangeCSVForm(CustomFieldModelCSVForm):
vrf = CSVModelChoiceField(
@@ -801,13 +823,16 @@ class IPRangeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBul
class IPRangeFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
model = IPRange
field_order = [
'family', 'vrf_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
]
field_groups = [
['q', 'tag'],
['family', 'vrf_id', 'status', 'role_id'],
['tenant_group_id', 'tenant_id', 'tag'],
['tenant_group_id', 'tenant_id'],
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
family = forms.ChoiceField(
required=False,
choices=add_blank_choice(IPAddressFamilyChoices),
@@ -818,7 +843,8 @@ class IPRangeFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
queryset=VRF.objects.all(),
required=False,
label=_('Assigned VRF'),
null_option='Global'
null_option='Global',
fetch_trigger='open'
)
status = forms.MultipleChoiceField(
choices=PrefixStatusChoices,
@@ -829,7 +855,8 @@ class IPRangeFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
queryset=Role.objects.all(),
required=False,
null_option='None',
label=_('Role')
label=_('Role'),
fetch_trigger='open'
)
tag = TagFilterField(model)
@@ -838,7 +865,7 @@ class IPRangeFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
# IP addresses
#
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm):
class IPAddressForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False,
@@ -989,8 +1016,6 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
super().__init__(*args, **kwargs)
self.fields['vrf'].empty_label = 'Global'
# Initialize primary_for_parent if IP address is already assigned
if self.instance.pk and self.instance.assigned_object:
parent = self.instance.assigned_object.parent_object
@@ -1065,10 +1090,6 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
'role': StaticSelect(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['vrf'].empty_label = 'Global'
class IPAddressCSVForm(CustomFieldModelCSVForm):
vrf = CSVModelChoiceField(
@@ -1219,8 +1240,7 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
vrf_id = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label='VRF',
empty_label='Global'
label='VRF'
)
q = forms.CharField(
required=False,
@@ -1231,15 +1251,20 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
model = IPAddress
field_order = [
'parent', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'role',
'q', 'parent', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'role',
'assigned_to_interface', 'tenant_group_id', 'tenant_id',
]
field_groups = [
['parent', 'family', 'mask_length'],
['status', 'vrf_id', 'present_in_vrf_id'],
['role', 'assigned_to_interface'],
['q', 'tag'],
['parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface'],
['vrf_id', 'present_in_vrf_id'],
['tenant_group_id', 'tenant_id'],
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
parent = forms.CharField(
required=False,
widget=forms.TextInput(
@@ -1265,12 +1290,14 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFil
queryset=VRF.objects.all(),
required=False,
label=_('Assigned VRF'),
null_option='Global'
null_option='Global',
fetch_trigger='open'
)
present_in_vrf_id = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('Present in VRF')
label=_('Present in VRF'),
fetch_trigger='open'
)
status = forms.MultipleChoiceField(
choices=IPAddressStatusChoices,
@@ -1369,6 +1396,10 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
'clustergroup', 'cluster',
]
fieldsets = (
('VLAN Group', ('name', 'slug', 'description')),
('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
)
widgets = {
'scope_type': StaticSelect,
}
@@ -1396,17 +1427,19 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
class VLANGroupCSVForm(CustomFieldModelCSVForm):
site = CSVModelChoiceField(
queryset=Site.objects.all(),
required=False,
to_field_name='name',
help_text='Assigned site'
)
slug = SlugField()
scope_type = CSVContentTypeField(
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
required=False,
label='Scope type (app & model)'
)
class Meta:
model = VLANGroup
fields = ('name', 'slug', 'scope_type', 'scope_id', 'description')
labels = {
'scope_id': 'Scope ID',
}
class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
@@ -1429,37 +1462,43 @@ class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
field_groups = [
['region', 'sitegroup', 'site'],
['location', 'rack']
['q'],
['region', 'sitegroup', 'site', 'location', 'rack']
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region')
label=_('Region'),
fetch_trigger='open'
)
sitegroup = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
label=_('Site group'),
fetch_trigger='open'
)
site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
label=_('Site')
label=_('Site'),
fetch_trigger='open'
)
location = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
required=False,
label=_('Location')
label=_('Location'),
fetch_trigger='open'
)
rack = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
required=False,
label=_('Rack')
label=_('Rack'),
fetch_trigger='open'
)
@@ -1641,23 +1680,28 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
model = VLAN
field_order = [
'region_id', 'site_group_id', 'site_id', 'group_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
]
field_groups = [
['q', 'tag'],
['region_id', 'site_group_id', 'site_id'],
['group_id', 'role_id', 'status'],
['group_id', 'status', 'role_id'],
['tenant_group_id', 'tenant_id'],
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region')
label=_('Region'),
fetch_trigger='open'
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
label=_('Site group'),
fetch_trigger='open'
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@@ -1666,7 +1710,8 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
query_params={
'region': '$region'
},
label=_('Site')
label=_('Site'),
fetch_trigger='open'
)
group_id = DynamicModelMultipleChoiceField(
queryset=VLANGroup.objects.all(),
@@ -1675,7 +1720,8 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
query_params={
'region': '$region'
},
label=_('VLAN group')
label=_('VLAN group'),
fetch_trigger='open'
)
status = forms.MultipleChoiceField(
choices=VLANStatusChoices,
@@ -1686,7 +1732,8 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
queryset=Role.objects.all(),
required=False,
null_option='None',
label=_('Role')
label=_('Role'),
fetch_trigger='open'
)
tag = TagFilterField(model)
@@ -1740,6 +1787,15 @@ class ServiceForm(BootstrapMixin, CustomFieldModelForm):
class ServiceFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
model = Service
field_groups = (
('q', 'tag'),
('protocol', 'port'),
)
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
protocol = forms.ChoiceField(
choices=add_blank_choice(ServiceProtocolChoices),
required=False,

View File

@@ -0,0 +1,20 @@
import graphene
__all__ = (
'IPAddressesMixin',
'VLANGroupsMixin',
)
class IPAddressesMixin:
ip_addresses = graphene.List('ipam.graphql.types.IPAddressType')
def resolve_ip_addresses(self, info):
return self.ip_addresses.restrict(info.context.user, 'view')
class VLANGroupsMixin:
vlan_groups = graphene.List('ipam.graphql.types.VLANGroupType')
def resolve_vlan_groups(self, info):
return self.vlan_groups.restrict(info.context.user, 'view')

View File

@@ -1,5 +1,5 @@
from ipam import filtersets, models
from netbox.graphql.types import ObjectType, TaggedObjectType
from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType
__all__ = (
'AggregateType',
@@ -16,7 +16,7 @@ __all__ = (
)
class AggregateType(TaggedObjectType):
class AggregateType(PrimaryObjectType):
class Meta:
model = models.Aggregate
@@ -24,7 +24,7 @@ class AggregateType(TaggedObjectType):
filterset_class = filtersets.AggregateFilterSet
class IPAddressType(TaggedObjectType):
class IPAddressType(PrimaryObjectType):
class Meta:
model = models.IPAddress
@@ -35,7 +35,7 @@ class IPAddressType(TaggedObjectType):
return self.role or None
class IPRangeType(TaggedObjectType):
class IPRangeType(PrimaryObjectType):
class Meta:
model = models.IPRange
@@ -46,7 +46,7 @@ class IPRangeType(TaggedObjectType):
return self.role or None
class PrefixType(TaggedObjectType):
class PrefixType(PrimaryObjectType):
class Meta:
model = models.Prefix
@@ -54,7 +54,7 @@ class PrefixType(TaggedObjectType):
filterset_class = filtersets.PrefixFilterSet
class RIRType(ObjectType):
class RIRType(OrganizationalObjectType):
class Meta:
model = models.RIR
@@ -62,7 +62,7 @@ class RIRType(ObjectType):
filterset_class = filtersets.RIRFilterSet
class RoleType(ObjectType):
class RoleType(OrganizationalObjectType):
class Meta:
model = models.Role
@@ -70,7 +70,7 @@ class RoleType(ObjectType):
filterset_class = filtersets.RoleFilterSet
class RouteTargetType(TaggedObjectType):
class RouteTargetType(PrimaryObjectType):
class Meta:
model = models.RouteTarget
@@ -78,7 +78,7 @@ class RouteTargetType(TaggedObjectType):
filterset_class = filtersets.RouteTargetFilterSet
class ServiceType(TaggedObjectType):
class ServiceType(PrimaryObjectType):
class Meta:
model = models.Service
@@ -86,7 +86,7 @@ class ServiceType(TaggedObjectType):
filterset_class = filtersets.ServiceFilterSet
class VLANType(TaggedObjectType):
class VLANType(PrimaryObjectType):
class Meta:
model = models.VLAN
@@ -94,7 +94,7 @@ class VLANType(TaggedObjectType):
filterset_class = filtersets.VLANFilterSet
class VLANGroupType(ObjectType):
class VLANGroupType(OrganizationalObjectType):
class Meta:
model = models.VLANGroup
@@ -102,7 +102,7 @@ class VLANGroupType(ObjectType):
filterset_class = filtersets.VLANGroupFilterSet
class VRFType(TaggedObjectType):
class VRFType(PrimaryObjectType):
class Meta:
model = models.VRF

View File

@@ -151,7 +151,7 @@ class NetHostContained(Lookup):
lhs, lhs_params = self.process_lhs(qn, connection)
rhs, rhs_params = self.process_rhs(qn, connection)
params = lhs_params + rhs_params
return 'CAST(HOST(%s) AS INET) << %s' % (lhs, rhs), params
return 'CAST(HOST(%s) AS INET) <<= %s' % (lhs, rhs), params
class NetFamily(Transform):

View File

@@ -163,7 +163,9 @@ 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])
return int(float(child_prefixes.size) / self.prefix.size * 100)
utilization = int(float(child_prefixes.size) / self.prefix.size * 100)
return min(utilization, 100)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@@ -394,6 +396,16 @@ class Prefix(PrimaryModel):
else:
return Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
def get_child_ranges(self):
"""
Return all IPRanges within this Prefix and VRF.
"""
return IPRange.objects.filter(
vrf=self.vrf,
start_address__net_host_contained=str(self.prefix),
end_address__net_host_contained=str(self.prefix)
)
def get_child_ips(self):
"""
Return all IPAddresses within this Prefix and VRF. If this Prefix is a container in the global table, return
@@ -423,7 +435,10 @@ class Prefix(PrimaryModel):
prefix = netaddr.IPSet(self.prefix)
child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()])
available_ips = prefix - child_ips
child_ranges = netaddr.IPSet()
for iprange in self.get_child_ranges():
child_ranges.add(iprange.range)
available_ips = prefix - child_ips - child_ranges
# IPv6, pool, or IPv4 /31-/32 sets are fully usable
if self.family == 6 or self.is_pool or (self.family == 4 and self.prefix.prefixlen >= 31):
@@ -469,14 +484,21 @@ class Prefix(PrimaryModel):
vrf=self.vrf
)
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
return int(float(child_prefixes.size) / self.prefix.size * 100)
utilization = int(float(child_prefixes.size) / self.prefix.size * 100)
else:
# Compile an IPSet to avoid counting duplicate IPs
child_count = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()]).size
child_ips = netaddr.IPSet()
for iprange in self.get_child_ranges():
child_ips.add(iprange.range)
for ip in self.get_child_ips():
child_ips.add(ip.address.ip)
prefix_size = self.prefix.size
if self.prefix.version == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
prefix_size -= 2
return int(float(child_count) / prefix_size * 100)
utilization = int(float(child_ips.size) / prefix_size * 100)
return min(utilization, 100)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
@@ -589,6 +611,10 @@ class IPRange(PrimaryModel):
def family(self):
return self.start_address.version if self.start_address else None
@property
def range(self):
return netaddr.IPRange(self.start_address.ip, self.end_address.ip)
@property
def mask_length(self):
return self.start_address.prefixlen if self.start_address else None
@@ -797,18 +823,15 @@ class IPAddress(PrimaryModel):
# Check for primary IP assignment that doesn't match the assigned device/VM
if self.pk:
device = Device.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
if device:
if getattr(self.assigned_object, 'device', None) != device:
raise ValidationError({
'interface': f"IP address is primary for device {device} but not assigned to it!"
})
vm = VirtualMachine.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
if vm:
if getattr(self.assigned_object, 'virtual_machine', None) != vm:
raise ValidationError({
'vminterface': f"IP address is primary for virtual machine {vm} but not assigned to it!"
})
for cls, attr in ((Device, 'device'), (VirtualMachine, 'virtual_machine')):
parent = cls.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
if parent and getattr(self.assigned_object, attr, None) != parent:
# Check for a NAT relationship
if not self.nat_inside or getattr(self.nat_inside.assigned_object, attr, None) != parent:
raise ValidationError({
'interface': f"IP address is primary for {cls._meta.model_name} {parent} but "
f"not assigned to it!"
})
# Validate IP status selection
if self.status == IPAddressStatusChoices.STATUS_SLAAC and self.family != 6:

View File

@@ -15,12 +15,25 @@ AVAILABLE_LABEL = mark_safe('<span class="badge bg-success">Available</span>')
PREFIX_LINK = """
{% load helpers %}
{% for i in record.depth|as_range %}
<i class="mdi mdi-circle-small"></i>
{% endfor %}
{% if record.depth %}
<div class="record-depth">
{% for i in record.depth|as_range %}
<span>•</span>
{% endfor %}
</div>
{% endif %}
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.site %}&site={{ object.site.pk }}{% endif %}{% if object.tenant %}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
"""
PREFIXFLAT_LINK = """
{% load helpers %}
{% if record.pk %}
<a href="{% url 'ipam:prefix' pk=record.pk %}">{{ record.prefix }}</a>
{% else %}
&mdash;
{% endif %}
"""
PREFIX_ROLE_LINK = """
{% if record.role %}
<a href="{% url 'ipam:prefix_list' %}?role={{ record.role.slug }}">{{ record.role }}</a>
@@ -277,10 +290,10 @@ class PrefixTable(BaseTable):
template_code=PREFIX_LINK,
attrs={'td': {'class': 'text-nowrap'}}
)
prefix_flat = tables.Column(
accessor=Accessor('prefix'),
linkify=True,
verbose_name='Prefix (Flat)'
prefix_flat = tables.TemplateColumn(
template_code=PREFIXFLAT_LINK,
attrs={'td': {'class': 'text-nowrap'}},
verbose_name='Prefix (Flat)',
)
depth = tables.Column(
accessor=Accessor('_depth'),
@@ -544,7 +557,7 @@ class VLANTable(BaseTable):
class Meta(BaseTable.Meta):
model = VLAN
fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description')
fields = ('pk', 'vid', 'name', 'site', 'group', 'tenant', 'status', 'role', 'description')
row_attrs = {
'class': lambda record: 'success' if not isinstance(record, VLAN) else '',
}
@@ -562,8 +575,8 @@ class VLANDetailTable(VLANTable):
)
class Meta(VLANTable.Meta):
fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags')
default_columns = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
fields = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags')
default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')
class VLANMembersTable(BaseTable):

View File

@@ -216,9 +216,10 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
"""
Test retrieval of all available prefixes within a parent prefix.
"""
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/24'))
Prefix.objects.create(prefix=IPNetwork('192.0.2.64/26'))
Prefix.objects.create(prefix=IPNetwork('192.0.2.192/27'))
vrf = VRF.objects.create(name='VRF 1')
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/24'), vrf=vrf)
Prefix.objects.create(prefix=IPNetwork('192.0.2.64/26'), vrf=vrf)
Prefix.objects.create(prefix=IPNetwork('192.0.2.192/27'), vrf=vrf)
url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk})
self.add_permissions('ipam.view_prefix')
@@ -232,7 +233,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
"""
Test retrieval of the first available prefix within a parent prefix.
"""
vrf = VRF.objects.create(name='Test VRF 1', rd='1234')
vrf = VRF.objects.create(name='VRF 1')
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), vrf=vrf, is_pool=True)
url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk})
self.add_permissions('ipam.add_prefix')
@@ -269,17 +270,18 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
"""
Test the creation of available prefixes within a parent prefix.
"""
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), is_pool=True)
vrf = VRF.objects.create(name='VRF 1')
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), vrf=vrf, is_pool=True)
url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk})
self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
# Try to create five /30s (only four are available)
data = [
{'prefix_length': 30, 'description': 'Test Prefix 1'},
{'prefix_length': 30, 'description': 'Test Prefix 2'},
{'prefix_length': 30, 'description': 'Test Prefix 3'},
{'prefix_length': 30, 'description': 'Test Prefix 4'},
{'prefix_length': 30, 'description': 'Test Prefix 5'},
{'prefix_length': 30, 'description': 'Prefix 1'},
{'prefix_length': 30, 'description': 'Prefix 2'},
{'prefix_length': 30, 'description': 'Prefix 3'},
{'prefix_length': 30, 'description': 'Prefix 4'},
{'prefix_length': 30, 'description': 'Prefix 5'},
]
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
@@ -299,7 +301,8 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
"""
Test retrieval of all available IP addresses within a parent prefix.
"""
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/29'), is_pool=True)
vrf = VRF.objects.create(name='VRF 1')
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/29'), vrf=vrf, is_pool=True)
url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
self.add_permissions('ipam.view_prefix', 'ipam.view_ipaddress')
@@ -318,7 +321,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
"""
Test retrieval of the first available IP address within a parent prefix.
"""
vrf = VRF.objects.create(name='Test VRF 1', rd='1234')
vrf = VRF.objects.create(name='VRF 1')
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/30'), vrf=vrf, is_pool=True)
url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
self.add_permissions('ipam.view_prefix', 'ipam.add_ipaddress')
@@ -342,7 +345,8 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
"""
Test the creation of available IP addresses within a parent prefix.
"""
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/29'), is_pool=True)
vrf = VRF.objects.create(name='VRF 1')
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/29'), vrf=vrf, is_pool=True)
url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
self.add_permissions('ipam.view_prefix', 'ipam.add_ipaddress')

View File

@@ -451,7 +451,7 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_mask_length(self):
params = {'mask_length': '24'}
params = {'mask_length': ['24']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_vrf(self):

View File

@@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
from ipam.choices import IPAddressRoleChoices, PrefixStatusChoices
from ipam.models import Aggregate, IPAddress, Prefix, RIR, VLAN, VLANGroup, VRF
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, RIR, VLAN, VLANGroup, VRF
class TestAggregate(TestCase):
@@ -72,6 +72,23 @@ class TestPrefix(TestCase):
# VRF container is limited to its own VRF
self.assertSetEqual(child_prefix_pks, {prefixes[2].pk})
def test_get_child_ranges(self):
prefix = Prefix(prefix='192.168.0.16/28')
prefix.save()
ranges = IPRange.objects.bulk_create((
IPRange(start_address=IPNetwork('192.168.0.1/24'), end_address=IPNetwork('192.168.0.10/24'), size=10), # No overlap
IPRange(start_address=IPNetwork('192.168.0.11/24'), end_address=IPNetwork('192.168.0.17/24'), size=7), # Partial overlap
IPRange(start_address=IPNetwork('192.168.0.18/24'), end_address=IPNetwork('192.168.0.23/24'), size=6), # Full overlap
IPRange(start_address=IPNetwork('192.168.0.24/24'), end_address=IPNetwork('192.168.0.30/24'), size=7), # Full overlap
IPRange(start_address=IPNetwork('192.168.0.31/24'), end_address=IPNetwork('192.168.0.40/24'), size=10), # Partial overlap
))
child_ranges = prefix.get_child_ranges()
self.assertEqual(len(child_ranges), 2)
self.assertEqual(child_ranges[0], ranges[2])
self.assertEqual(child_ranges[1], ranges[3])
def test_get_child_ips(self):
vrfs = VRF.objects.bulk_create((
VRF(name='VRF 1'),
@@ -125,17 +142,17 @@ class TestPrefix(TestCase):
IPAddress(address=IPNetwork('10.0.0.3/26')),
IPAddress(address=IPNetwork('10.0.0.5/26')),
IPAddress(address=IPNetwork('10.0.0.7/26')),
IPAddress(address=IPNetwork('10.0.0.9/26')),
IPAddress(address=IPNetwork('10.0.0.11/26')),
IPAddress(address=IPNetwork('10.0.0.13/26')),
))
IPRange.objects.create(
start_address=IPNetwork('10.0.0.9/26'),
end_address=IPNetwork('10.0.0.12/26')
)
missing_ips = IPSet([
'10.0.0.2/32',
'10.0.0.4/32',
'10.0.0.6/32',
'10.0.0.8/32',
'10.0.0.10/32',
'10.0.0.12/32',
'10.0.0.13/32',
'10.0.0.14/32',
])
available_ips = parent_prefix.get_available_ips()
@@ -168,27 +185,30 @@ class TestPrefix(TestCase):
IPAddress.objects.create(address=IPNetwork('10.0.0.4/24'))
self.assertEqual(parent_prefix.get_first_available_ip(), '10.0.0.5/24')
def test_get_utilization(self):
# Container Prefix
prefix = Prefix.objects.create(
prefix=IPNetwork('10.0.0.0/24'),
status=PrefixStatusChoices.STATUS_CONTAINER
)
Prefix.objects.bulk_create((
def test_get_utilization_container(self):
prefixes = (
Prefix(prefix=IPNetwork('10.0.0.0/24'), status=PrefixStatusChoices.STATUS_CONTAINER),
Prefix(prefix=IPNetwork('10.0.0.0/26')),
Prefix(prefix=IPNetwork('10.0.0.128/26')),
))
self.assertEqual(prefix.get_utilization(), 50)
# Non-container Prefix
prefix.status = PrefixStatusChoices.STATUS_ACTIVE
prefix.save()
IPAddress.objects.bulk_create(
# Create 32 IPAddresses within the Prefix
[IPAddress(address=IPNetwork('10.0.0.{}/24'.format(i))) for i in range(1, 33)]
)
self.assertEqual(prefix.get_utilization(), 12) # ~= 12%
Prefix.objects.bulk_create(prefixes)
self.assertEqual(prefixes[0].get_utilization(), 50) # 50% utilization
def test_get_utilization_noncontainer(self):
prefix = Prefix.objects.create(
prefix=IPNetwork('10.0.0.0/24'),
status=PrefixStatusChoices.STATUS_ACTIVE
)
# Create 32 child IPs
IPAddress.objects.bulk_create([
IPAddress(address=IPNetwork(f'10.0.0.{i}/24')) for i in range(1, 33)
])
self.assertEqual(prefix.get_utilization(), 12) # 12.5% utilization
# Create a child range with 32 additional IPs
IPRange.objects.create(start_address=IPNetwork('10.0.0.33/24'), end_address=IPNetwork('10.0.0.64/24'))
self.assertEqual(prefix.get_utilization(), 25) # 25% utilization
#
# Uniqueness enforcement tests

View File

@@ -391,10 +391,10 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
}
cls.csv_data = (
"name,slug,description",
"VLAN Group 4,vlan-group-4,Fourth VLAN group",
"VLAN Group 5,vlan-group-5,Fifth VLAN group",
"VLAN Group 6,vlan-group-6,Sixth VLAN group",
f"name,slug,scope_type,scope_id,description",
f"VLAN Group 4,vlan-group-4,,,Fourth VLAN group",
f"VLAN Group 5,vlan-group-5,dcim.site,{sites[0].pk},Fifth VLAN group",
f"VLAN Group 6,vlan-group-6,dcim.site,{sites[1].pk},Sixth VLAN group",
)
cls.bulk_edit_data = {

View File

@@ -77,6 +77,7 @@ urlpatterns = [
path('prefixes/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}),
path('prefixes/<int:pk>/journal/', ObjectJournalView.as_view(), name='prefix_journal', kwargs={'model': Prefix}),
path('prefixes/<int:pk>/prefixes/', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'),
path('prefixes/<int:pk>/ip-ranges/', views.PrefixIPRangesView.as_view(), name='prefix_ipranges'),
path('prefixes/<int:pk>/ip-addresses/', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
# IP ranges

View File

@@ -4,6 +4,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from dcim.models import Device, Interface
from netbox.views import generic
from utilities.forms import TableConfigForm
from utilities.tables import paginate_table
from utilities.utils import count_related
from virtualization.models import VirtualMachine, VMInterface
@@ -207,23 +208,6 @@ class AggregateListView(generic.ObjectListView):
filterset = filtersets.AggregateFilterSet
filterset_form = forms.AggregateFilterForm
table = tables.AggregateDetailTable
template_name = 'ipam/aggregate_list.html'
def extra_context(self):
ipv4_total = 0
ipv6_total = 0
for aggregate in self.queryset:
if aggregate.prefix.version == 6:
# Report equivalent /64s for IPv6 to keep things sane
ipv6_total += int(aggregate.prefix.size / 2 ** 64)
else:
ipv4_total += aggregate.prefix.size
return {
'ipv4_total': ipv4_total,
'ipv6_total': ipv6_total,
}
class AggregateView(generic.ObjectView):
@@ -412,30 +396,58 @@ class PrefixPrefixesView(generic.ObjectView):
if child_prefixes and request.GET.get('show_available', 'true') == 'true':
child_prefixes = add_available_prefixes(instance.prefix, child_prefixes)
prefix_table = tables.PrefixDetailTable(child_prefixes)
table = tables.PrefixDetailTable(child_prefixes, user=request.user)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
prefix_table.columns.show('pk')
paginate_table(prefix_table, request)
table.columns.show('pk')
paginate_table(table, request)
bulk_querystring = 'vrf_id={}&within={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
# Compile permissions list for rendering the object table
permissions = {
'add': request.user.has_perm('ipam.add_prefix'),
'change': request.user.has_perm('ipam.change_prefix'),
'delete': request.user.has_perm('ipam.delete_prefix'),
}
bulk_querystring = 'vrf_id={}&within={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
return {
'first_available_prefix': instance.get_first_available_prefix(),
'prefix_table': prefix_table,
'table': table,
'permissions': permissions,
'bulk_querystring': bulk_querystring,
'active_tab': 'prefixes',
'first_available_prefix': instance.get_first_available_prefix(),
'show_available': request.GET.get('show_available', 'true') == 'true',
}
class PrefixIPRangesView(generic.ObjectView):
queryset = Prefix.objects.all()
template_name = 'ipam/prefix/ip_ranges.html'
def get_extra_context(self, request, instance):
# Find all IPRanges belonging to this Prefix
ip_ranges = instance.get_child_ranges().restrict(request.user, 'view').prefetch_related('vrf')
table = tables.IPRangeTable(ip_ranges, user=request.user)
if request.user.has_perm('ipam.change_iprange') or request.user.has_perm('ipam.delete_iprange'):
table.columns.show('pk')
paginate_table(table, request)
bulk_querystring = 'vrf_id={}&parent={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
# Compile permissions list for rendering the object table
permissions = {
'change': request.user.has_perm('ipam.change_iprange'),
'delete': request.user.has_perm('ipam.delete_iprange'),
}
return {
'table': table,
'permissions': permissions,
'bulk_querystring': bulk_querystring,
'active_tab': 'ip-ranges',
}
class PrefixIPAddressesView(generic.ObjectView):
queryset = Prefix.objects.all()
template_name = 'ipam/prefix/ip_addresses.html'
@@ -450,26 +462,25 @@ class PrefixIPAddressesView(generic.ObjectView):
if request.GET.get('show_available', 'true') == 'true':
ipaddresses = add_available_ipaddresses(instance.prefix, ipaddresses, instance.is_pool)
ip_table = tables.IPAddressTable(ipaddresses)
table = tables.IPAddressTable(ipaddresses, user=request.user)
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
ip_table.columns.show('pk')
paginate_table(ip_table, request)
table.columns.show('pk')
paginate_table(table, request)
bulk_querystring = 'vrf_id={}&parent={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
# Compile permissions list for rendering the object table
permissions = {
'add': request.user.has_perm('ipam.add_ipaddress'),
'change': request.user.has_perm('ipam.change_ipaddress'),
'delete': request.user.has_perm('ipam.delete_ipaddress'),
}
bulk_querystring = 'vrf_id={}&parent={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
return {
'first_available_ip': instance.get_first_available_ip(),
'ip_table': ip_table,
'table': table,
'permissions': permissions,
'bulk_querystring': bulk_querystring,
'active_tab': 'ip-addresses',
'first_available_ip': instance.get_first_available_ip(),
'show_available': request.GET.get('show_available', 'true') == 'true',
}
@@ -778,7 +789,6 @@ class VLANGroupView(generic.ObjectView):
class VLANGroupEditView(generic.ObjectEditView):
queryset = VLANGroup.objects.all()
model_form = forms.VLANGroupForm
template_name = 'ipam/vlangroup_edit.html'
class VLANGroupDeleteView(generic.ObjectDeleteView):

View File

@@ -34,7 +34,6 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
return list(queryset[self.offset:])
def get_limit(self, request):
if self.limit_query_param:
try:
limit = int(request.query_params[self.limit_query_param])

View File

@@ -69,7 +69,7 @@ SECRET_KEY = ''
# Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of
# application errors (assuming correct email settings are provided).
ADMINS = [
# ['John Doe', 'jdoe@example.com'],
# ('John Doe', 'jdoe@example.com'),
]
# URL schemes that are allowed within links in NetBox
@@ -163,6 +163,10 @@ INTERNAL_IPS = ('127.0.0.1', '::1')
# https://docs.djangoproject.com/en/stable/topics/logging/
LOGGING = {}
# Automatically reset the lifetime of a valid session upon each authenticated request. Enables users to remain
# authenticated to NetBox indefinitely.
LOGIN_PERSISTENCE = False
# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
# are permitted to access most data in NetBox but not make any changes.
LOGIN_REQUIRED = False

View File

@@ -1,12 +1,12 @@
import graphene
from django.contrib.contenttypes.models import ContentType
from graphene.types.generic import GenericScalar
from graphene_django import DjangoObjectType
from extras.graphql.mixins import ChangelogMixin, CustomFieldsMixin, JournalEntriesMixin, TagsMixin
__all__ = (
'BaseObjectType',
'ObjectType',
'TaggedObjectType',
'OrganizationalObjectType',
'PrimaryObjectType',
)
@@ -27,30 +27,41 @@ class BaseObjectType(DjangoObjectType):
return queryset.restrict(info.context.user, 'view')
class ObjectType(BaseObjectType):
class ObjectType(
ChangelogMixin,
BaseObjectType
):
"""
Extends BaseObjectType with support for custom field data.
Base GraphQL object type for unclassified models which support change logging
"""
custom_fields = GenericScalar()
class Meta:
abstract = True
def resolve_custom_fields(self, info):
return self.custom_field_data
class TaggedObjectType(ObjectType):
class OrganizationalObjectType(
ChangelogMixin,
CustomFieldsMixin,
BaseObjectType
):
"""
Extends ObjectType with support for Tags
Base type for organizational models
"""
tags = graphene.List(graphene.String)
class Meta:
abstract = True
def resolve_tags(self, info):
return self.tags.all()
class PrimaryObjectType(
ChangelogMixin,
CustomFieldsMixin,
JournalEntriesMixin,
TagsMixin,
BaseObjectType
):
"""
Base type for primary models
"""
class Meta:
abstract = True
#

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