Compare commits

..

296 Commits

Author SHA1 Message Date
Jeremy Stretch
85563e21db Remove obsolete initial_data fixtures (no longer maintained) 2019-12-12 14:30:43 -05:00
Jeremy Stretch
9352867cec Fix up the installation docs 2019-12-12 14:27:12 -05:00
Jeremy Stretch
93837c5b9e Merge pull request #3756 from netbox-community/csv-import-tests
CSV import tests
2019-12-12 12:09:31 -05:00
Jeremy Stretch
aaf5523378 Add view tests for device components 2019-12-12 11:58:57 -05:00
Jeremy Stretch
8a22f83bc0 Add virtualization CSV import tests 2019-12-12 11:34:02 -05:00
Jeremy Stretch
7550d53d02 Add tenancy CSV import tests 2019-12-12 11:29:41 -05:00
Jeremy Stretch
821cd58825 Add secrets CSV import tests 2019-12-12 11:26:48 -05:00
Jeremy Stretch
526412a1ba Add IPAM CSV import tests 2019-12-12 10:51:17 -05:00
Jeremy Stretch
fae2469dd7 Add DCIM CSV import tests 2019-12-12 10:30:15 -05:00
Jeremy Stretch
27fd351fc2 Add CSV import tests for circuits 2019-12-12 10:08:49 -05:00
John Anderson
ea5c9df095 fixed svg gradient scaling and css flickering issue 2019-12-12 00:30:22 -05:00
Jeremy Stretch
5aa17bc42a Closes #3706: Increase available_power maximum value on PowerFeed 2019-12-11 21:12:18 -05:00
Jeremy Stretch
7bbd9be389 Tweak elevation font 2019-12-11 21:03:24 -05:00
Jeremy Stretch
dbf9a2b452 Fix bug with rendering devices taller than 1U 2019-12-11 20:19:01 -05:00
Jeremy Stretch
e4b8ea8905 Change render_format to render 2019-12-11 20:05:16 -05:00
Jeremy Stretch
fd8913f09b Fix bug in migration 2019-12-11 20:01:10 -05:00
John Anderson
645383509b change render_format to render for svg elevations 2019-12-11 17:33:58 -05:00
Jeremy Stretch
77a5f6ef81 Touched up the release notes 2019-12-11 17:22:41 -05:00
Jeremy Stretch
2ec7259a69 Rack elevation endpoint should return JSON by default; fix typo 2019-12-11 17:17:20 -05:00
Jeremy Stretch
b6f8c248de Addressed lingering TODOs 2019-12-11 17:07:56 -05:00
Jeremy Stretch
b16be577e3 CSVChoiceField should default to a blank string instead of None 2019-12-11 17:04:48 -05:00
Jeremy Stretch
590bbbce7f Fix bug left over from work on #3569 2019-12-11 16:16:14 -05:00
Jeremy Stretch
577b4593de Changelog for #3664 2019-12-11 16:14:41 -05:00
Jeremy Stretch
44e5a63a2a Merge pull request #3755 from netbox-community/3664-configcontext-tags
3664 configcontext tags
2019-12-11 16:10:48 -05:00
Jeremy Stretch
19363add30 Represent and assign ConfigContext tags by their slugs 2019-12-11 16:04:43 -05:00
Jeremy Stretch
fd88ba65b2 Cleanup for #3664 2019-12-11 15:55:33 -05:00
Jeremy Stretch
ce4a5a38a3 Introduce is_taggable utility function for identifying taggable models 2019-12-11 15:52:35 -05:00
John Anderson
05938d60f0 Merge pull request #3754 from netbox-community/2248-svg-rack-elevations
2248 svg rack elevations
2019-12-11 14:26:32 -05:00
Jeremy Stretch
8b189abd58 Merge pull request #3752 from kobayashi/3664
implement 3664
2019-12-11 14:14:48 -05:00
John Anderson
7f788eaa06 review updates to svg rendering 2019-12-11 13:39:10 -05:00
Jeremy Stretch
9f204f72ef Changelog and documentation for #1814 2019-12-11 11:01:31 -05:00
Jeremy Stretch
e02ac133eb Added import error handling and config vlaidation warning for store config 2019-12-11 10:51:32 -05:00
John Anderson
c09aefd509 updated changelog with deprecation notice of rack units endpoint 2019-12-11 10:39:46 -05:00
John Anderson
e0c81fd837 changelog for #2248 2019-12-11 10:34:44 -05:00
Jeremy Stretch
9a8c8012be Merge pull request #3666 from steffann/1814-Ability_to_use_object_store_for_images
Add support for S3 storage for media
2019-12-11 10:20:09 -05:00
Sander Steffann
f1e75b0fbb Implement storage configuration as suggested by @jeremystretch 2019-12-11 16:09:32 +01:00
John Anderson
b4d724b5ae drf-yasg updates for rack elevations 2019-12-11 09:45:08 -05:00
Sander Steffann
f7a1595ce5 Merge branch '1814-Ability_to_use_object_store_for_images' of github.com:steffann/netbox into 1814-Ability_to_use_object_store_for_images 2019-12-11 15:23:47 +01:00
Sander Steffann
378883df2b Fix code for PEP8 2019-12-11 15:19:32 +01:00
Sander Steffann
dafa2513e3 Add support for S3 storage for media 2019-12-11 15:19:32 +01:00
kobayashi
5710f297f1 implement 3664 2019-12-11 04:58:42 -05:00
Jeremy Stretch
0d8fd45587 Updated static resources 2019-12-10 15:56:52 -05:00
Jeremy Stretch
01b6e73f9b Update dependency versions for DRF & drf_yasg 2019-12-10 15:39:57 -05:00
Jeremy Stretch
b0fb474aa2 Update dependency versions for v2.7 2019-12-10 15:16:20 -05:00
Jeremy Stretch
0787713298 Add deprecation warning for Python 3.5 2019-12-10 13:44:45 -05:00
Jeremy Stretch
6221bc8f43 Merge pull request #3750 from netbox-community/3655-role-descriptions
Add description field to organizational role models
2019-12-10 13:29:44 -05:00
Jeremy Stretch
a6904dc5d5 Add description field to CircuitType (#3655) 2019-12-10 13:25:14 -05:00
Jeremy Stretch
3d5fb2491a Chnagelog for #3655 2019-12-10 13:09:41 -05:00
Jeremy Stretch
24bdd10b4f Add description field to SecretRole model (#3655) 2019-12-10 13:03:09 -05:00
Jeremy Stretch
7845d9b25f Add description field to Role model (#3655) 2019-12-10 12:59:10 -05:00
Jeremy Stretch
1f288175e4 Add description field to RackRole and DeviceRole models (#3655) 2019-12-10 12:53:28 -05:00
Jeremy Stretch
f0a6c881bc Fix inclusion of legacy IDs on choice fields 2019-12-10 12:07:54 -05:00
Jeremy Stretch
9452cebc6a Merge branch 'develop' into develop-2.7 2019-12-10 11:51:10 -05:00
Jeremy Stretch
3c36bec298 Post-release version bump 2019-12-10 10:50:46 -05:00
Jeremy Stretch
6a2651991a Release v2.6.8 2019-12-10 10:42:48 -05:00
Jeremy Stretch
12657ebd7c Cable.status to slug (#3569) 2019-12-10 09:55:10 -05:00
John Anderson
d8dd5f00c1 removed rack elevations viewset 2019-12-10 03:19:26 -05:00
John Anderson
1ec191db92 initial cleanup of rack elevations 2019-12-10 03:18:10 -05:00
John Anderson
20c8abe8da Merge branch 'develop-2.7' into 2248-svg-rack-elevations 2019-12-10 02:59:04 -05:00
Jeremy Stretch
086813fc3e Changelog for #2669 2019-12-09 17:28:21 -05:00
Jeremy Stretch
606a73a547 Merge pull request #3740 from netbox-community/2669-device-vm-names
Allow non-unique device and VM names
2019-12-09 17:26:32 -05:00
Jeremy Stretch
f25e2a1922 Fixes #3644: Fix exception when connecting a cable to a RearPort with no corresponding FrontPort 2019-12-09 15:42:04 -05:00
Jeremy Stretch
740cc8b601 Remove deprecated context parameter from from_db_value 2019-12-09 12:32:51 -05:00
Jeremy Stretch
5ac4a3ba79 Omit default uniqueness validator from VirtualMachineSerializer, which implies required fields 2019-12-09 12:11:42 -05:00
Jeremy Stretch
2e37b19e9f #2269: Allow non-unique VirtualMachine names 2019-12-09 11:59:30 -05:00
Jeremy Stretch
a8dd060e32 #2269: Allow non-unique Device names 2019-12-09 11:41:03 -05:00
Jeremy Stretch
95edec5448 #3722: Tweak ordering of permitted characters to avoid creating a regex range 2019-12-09 10:02:56 -05:00
John Anderson
b0adce998b Merge pull request #3702 from hellerve/rack-elevations-api
Rack elevations API
2019-12-09 01:02:40 -05:00
John Anderson
6fe1b9b37c update openapi field type for choice field value fields to string 2019-12-09 00:41:43 -05:00
hellerve
44b042e3fc dcim api: fix face default value in rackviewset 2019-12-08 18:24:13 +01:00
hellerve
e5b49e25b9 dcim api: add feedback from @jeremystretch to rack elevations api 2019-12-08 18:14:59 +01:00
hellerve
b54a05d255 dcim: make linter happy 2019-12-08 17:59:40 +01:00
hellerve
66b6f586f1 tests: update to reflect absence of utility functions 2019-12-08 17:59:40 +01:00
hellerve
82819d5429 dcim: remove elevation getters 2019-12-08 17:59:38 +01:00
hellerve
bc859c6139 css: purge outdated rack styling 2019-12-08 17:58:36 +01:00
hellerve
e1fdc7773a requirements: fix svgwrite version 2019-12-08 17:58:35 +01:00
hellerve
5d5b7e0dd9 dcim: refactor reservations and make them resizable 2019-12-08 17:58:21 +01:00
hellerve
77890ad775 dcim: add inline stylesheet to rack elevation api view 2019-12-08 17:58:21 +01:00
hellerve
1a9b9f50d8 dcim: fix fonts & texts in svg 2019-12-08 17:58:21 +01:00
hellerve
808f5c95e3 dcim: make front and rear work (references #2248) 2019-12-08 17:58:21 +01:00
hellerve
d11de6d021 dcim: add rack-elevations api endpoint (references #2248) 2019-12-08 17:58:20 +01:00
Jeremy Stretch
88c7d95b08 ClusterForm should inherit from TenancyForm 2019-12-06 16:47:29 -05:00
Jeremy Stretch
2956eff051 Closes #648: Pre-populate forms when selecting "create and add another" 2019-12-06 16:40:39 -05:00
Jeremy Stretch
b05566e96f Implement tag replication for #33 2019-12-06 16:22:56 -05:00
Jeremy Stretch
446acbdf82 Closes #33: Add ability to clone objects (pre-populate form fields) 2019-12-06 16:13:52 -05:00
Jeremy Stretch
f2076c9572 Fixed git commit hook 2019-12-06 12:54:13 -05:00
Jeremy Stretch
d71e6698f4 Extend git commit hook to check for missing schema migrations 2019-12-06 12:42:59 -05:00
Jeremy Stretch
9b26225fdd #3720: Update migration to add powerfeeds to termination_type limit list (does not impact database) 2019-12-06 12:29:31 -05:00
Jeremy Stretch
f8c5ca942a #3722: Update migration with new validator (does not impact database) 2019-12-06 12:19:29 -05:00
Jeremy Stretch
47fefbec07 Default to localhost in example Redis configs (needed for CI to work) 2019-12-06 12:00:51 -05:00
Jeremy Stretch
45917f8014 Closes #3408: Remove WEBHOOKS_ENABLE configuration setting 2019-12-06 11:52:28 -05:00
Jeremy Stretch
dd3e7397a4 Add ITA plug/outlet types (#792) 2019-12-06 11:26:44 -05:00
Jeremy Stretch
7518174374 Closes #3731: Change Graph.type to a ContentType foreign key field 2019-12-06 10:32:59 -05:00
Sander Steffann
02a009b7a7 Don't redefine exception but split the code 2019-12-06 16:32:18 +01:00
Jeremy Stretch
16353ce63c Correct stalebot config file name 2019-12-05 21:43:22 -05:00
Jeremy Stretch
7a4b202064 Fixes #3725: Enforce client validation for minimum service port number 2019-12-05 21:22:34 -05:00
Jeremy Stretch
a97ebc6d4c Fixes #3722: Allow the underscore character in IPAddress DNS names 2019-12-05 21:14:29 -05:00
Jeremy Stretch
9e765b1704 Fixes #3730: Add link to docs page for custom links 2019-12-05 21:07:43 -05:00
Jeremy Stretch
ec9443edb8 Changelog updates 2019-12-05 20:52:30 -05:00
Jeremy Stretch
dcb2c4722c Merge pull request #3732 from netbox-community/3569-api-choice-slugs
Replace API integer API choice values with slugs
2019-12-05 17:53:47 -05:00
Jeremy Stretch
b0c0adf6e7 Adapt device component import forms from #3711 2019-12-05 17:49:44 -05:00
Jeremy Stretch
17898a4c57 Merge branch 'develop-2.7' into 3569-api-choice-slugs 2019-12-05 17:43:11 -05:00
Jeremy Stretch
edbf562803 Annotate all migration operation lists 2019-12-05 17:42:33 -05:00
Jeremy Stretch
5d772d7055 Webhook.http_content_type to slug (#3569) 2019-12-05 17:11:59 -05:00
Daniel Sheppard
8cbd2f5c2d Add list view for device components (#3719)
* Initial Work on #3564

* #3564 - Fixup issue with filter on interface

* #3564 - Fix PEP8 errors

* #3564 - Finalize fields, readjust order, reduce repetition

* #3564 - Update Changelog

* #3564 - Fix extra space

* #3564 - Change interface table ordering

* #3564 - Minor cleanup

* #3564 - Add Import Links

* Fix PEP8
2019-12-05 17:10:49 -05:00
Jeremy Stretch
89e720cb77 ExportTemplate.template_language to slug (#3569) 2019-12-05 17:01:00 -05:00
Jeremy Stretch
2583823e5f Delete obsolete user action types 2019-12-05 16:50:44 -05:00
Jeremy Stretch
33890e6b97 Remain consistent with original action strings (e.g. 'created' instead of 'create') 2019-12-05 16:42:10 -05:00
Jeremy Stretch
a2b0da2608 Fix changelog table action labels 2019-12-05 16:37:22 -05:00
Jeremy Stretch
2bcbcd3458 ObjectChange.action to slug (#3569) 2019-12-05 16:30:15 -05:00
Jeremy Stretch
8efde811e9 Fix PowerFeed field defaults 2019-12-05 16:05:45 -05:00
Jeremy Stretch
4e1ee270cf Extend CustomField migration to update CustomFieldChoice.field.limit_choices_to 2019-12-05 16:02:52 -05:00
Jeremy Stretch
7a3c725f51 Convert BUTTON_CLASS_CHOICES to a ChoiceSet 2019-12-05 15:59:16 -05:00
Jeremy Stretch
fe2c682dc3 Changelog updates 2019-12-05 15:54:29 -05:00
Sander Steffann
adb25fd7d7 822 bulk import of device components (#3711)
Closes #822: CSV import for device components

* Implement CSV import for netbox-community#822

* Comment out default_return_url until there is a proper target

* Fix the default value of `enabled` when not included in the import

* rear_port is definitely required here

* Power Ports don't have a type (yet)

* Add import for console-ports and console-server-ports

* Add import for device-bays
2019-12-05 15:36:11 -05:00
Jeremy Stretch
bfea77baa5 CustomField.filter_logic to slug 2019-12-04 21:09:02 -05:00
Jeremy Stretch
3ff22bea56 CustomField.type to slug 2019-12-04 21:01:50 -05:00
Jeremy Stretch
ca11b9a2f5 VirtualMachine.status to slug 2019-12-04 20:40:18 -05:00
John Anderson
1048d3909b fixes #3724 - allow filtering interfaces by more than one device name 2019-12-04 02:00:08 -05:00
Jeremy Stretch
4ecbfc4e5e Service.protocol to slug (#3569) 2019-11-27 22:27:06 -05:00
Jeremy Stretch
213bd1555a VLAN.status to slug (#3569) 2019-11-27 22:15:59 -05:00
Jeremy Stretch
14a7a33cc2 IPAddress.role to slug (#3569) 2019-11-27 22:09:16 -05:00
Jeremy Stretch
ba8f324b12 IPAddress.status to slug (#3569) 2019-11-27 21:54:01 -05:00
Jeremy Stretch
929c0648d0 Prefix.status to slug (#3569) 2019-11-27 21:46:53 -05:00
Jeremy Stretch
21fe5902a8 PowerOutlet.feed_leg to slug (#3569) 2019-11-27 21:30:11 -05:00
Jeremy Stretch
b2caaa6733 Fixes #3312: Fix validation error when editing power cables in bulk 2019-11-27 09:19:34 -05:00
Jeremy Stretch
15722c1871 Fixes #3720: Correctly indicate power feed terminations on cable list 2019-11-26 16:56:11 -05:00
Jeremy Stretch
1d18948307 Fixes #3709: Prevent exception when importing an invalid cable definition 2019-11-26 16:46:51 -05:00
Jeremy Stretch
8c7f6c62b0 PowerFeed.status to slug (#3569) 2019-11-25 21:22:14 -05:00
Jeremy Stretch
2dc07ca30d PowerFeed.phase to slug (#3569) 2019-11-25 21:14:04 -05:00
Jeremy Stretch
bb8b012397 PowerFeed.supply to slug (#3569) 2019-11-25 21:08:34 -05:00
Jeremy Stretch
62494f295e PowerFeed.type to slug (#3569) 2019-11-25 21:03:11 -05:00
Jeremy Stretch
9872a46583 Rack.outer_unit to slug (#3569) 2019-11-25 20:54:24 -05:00
Jeremy Stretch
4846557d61 Cable.length_unit to slug (#3569) 2019-11-25 20:40:29 -05:00
Jeremy Stretch
79a40e22c9 Cable.type to slug (#3569) 2019-11-25 19:57:13 -05:00
Jeremy Stretch
dead5b42be Front/RearPort.type to slug (#3569) 2019-11-25 19:39:25 -05:00
Jeremy Stretch
bcc34f6099 Device.face to slug (#3569) 2019-11-25 19:23:43 -05:00
Jeremy Stretch
6a451e0c0e Merge pull request #3701 from hSaria/3697-custom-scripts-example-typo
Fixes #3697: Correct field_order attribute name
2019-11-22 08:45:21 -05:00
Jeremy Stretch
5c95927a43 Site.status to slug (#3569) 2019-11-21 22:54:01 -05:00
Jeremy Stretch
3fa4ceadb0 Interface.mode to slug (#3569) 2019-11-21 22:50:01 -05:00
Jeremy Stretch
f93cd17fee Consolidate #3569 field migrations by model 2019-11-21 22:26:35 -05:00
Jeremy Stretch
5f5081f719 Interface.type to slug (#3569) 2019-11-21 22:11:02 -05:00
Jeremy Stretch
180d3d0029 Resolved migration discrepancies when dealing with NULL values 2019-11-21 21:44:30 -05:00
Jeremy Stretch
4e2863e4ec Move CircuitTermination.term_side choices to a ChoiceSet 2019-11-21 21:28:59 -05:00
hSaria
b46e52eccc Fixed #3697: Correct field_order attribute name 2019-11-20 17:03:12 +00:00
Jeremy Stretch
f3a41df395 #3455: Correct related_name on Cluster.tenant 2019-11-18 22:12:29 -05:00
Jeremy Stretch
afd82fd9d3 DeviceType.subdevice_role to slug (#3569) 2019-11-18 22:08:33 -05:00
Jeremy Stretch
fbd12e1887 Create a separate migration for each field 2019-11-18 21:41:04 -05:00
Jeremy Stretch
4f0d82ac16 Standardize migration names for #3569 2019-11-18 21:04:12 -05:00
Jeremy Stretch
88b8cf3360 Tweak migrations to handle NULL values 2019-11-18 20:56:22 -05:00
Jeremy Stretch
a8db07e0a8 Device.face to slug (#3569) 2019-11-16 21:46:07 -05:00
Jeremy Stretch
c79c29e769 Rack.status to slug (#3569) 2019-11-15 22:03:41 -05:00
Jeremy Stretch
e1e09bff9b Correct Rack.type migration logic 2019-11-15 21:50:33 -05:00
Jeremy Stretch
07aa036fe8 Convert RACK_WIDTH_CHOICES to ChoiceSet 2019-11-15 21:33:56 -05:00
Jeremy Stretch
8d27b62114 Rack.status to slug (#3569) 2019-11-15 21:31:57 -05:00
Jeremy Stretch
62b45494b6 Convert all DCIM choice classes to ChoiceSets 2019-11-15 21:17:01 -05:00
Jeremy Stretch
de986ec392 Merge pull request #3687 from kobayashi/3679
fix url expressions
2019-11-14 22:03:03 -05:00
Jeremy Stretch
4b415caad2 Changelog for #3663 2019-11-14 22:01:37 -05:00
Jeremy Stretch
bfede60f3d Rename CreatedUpdatedFilter to CreatedUpdatedFilterSet 2019-11-14 22:00:12 -05:00
Jeremy Stretch
03b8759597 'base_name' deprecated in DRF v3.9.0 2019-11-14 21:58:37 -05:00
Jeremy Stretch
bdd623f82c Merge pull request #3680 from struppinet/develop
Closes #3663: API filter by created, last_updated
2019-11-14 21:57:26 -05:00
kobayashi
3c8083ed7f fix url expressions 2019-11-13 00:48:47 -05:00
kobayashi
5904f1f8ee Changelog for #3329 2019-11-12 09:47:31 -05:00
kobayashi
cc848a3f01 Merge pull request #3684 from bbock/docs-remove-p3p
Docs: Remove obsolete P3P policy
2019-11-12 09:38:46 -05:00
Bernhard Bock
1aaa101fb5 Docs: Remove obsolete P3P policy
P3P is obsolete (https://www.w3.org/TR/P3P11/), therefore the HTTP header
should be removed from the recommended config in the installation docs.
2019-11-08 15:13:32 +01:00
struppi
0319450643 Closes #3663: rename filter class 2019-11-07 22:41:09 +01:00
struppi
099774d667 Closes #3663: PEP8 fixes 2019-11-07 22:38:51 +01:00
Jeremy Stretch
e09ad6915f Circuit.status (#3569) 2019-11-07 11:11:10 -05:00
Jeremy Stretch
a2a6b754be Introduce ChoiceSet class for field choices 2019-11-07 10:33:10 -05:00
Jeremy Stretch
d392982fe7 Extend DeviceType import test with power port/outlet types 2019-11-06 16:59:01 -05:00
Jeremy Stretch
8cb7eb0382 Convert console port types to slugs (#3569) 2019-11-06 16:56:46 -05:00
Jeremy Stretch
48dcd2d2b9 Merge pull request #3675 from netbox-community/792-power-types
Introduce power port and outlet types
2019-11-06 16:12:37 -05:00
Jeremy Stretch
6ccaa0d9bd Initial work on #792 2019-11-06 15:30:54 -05:00
Jeremy Stretch
b1761f7856 #3139: Add a message indicating why the user is redirected 2019-11-06 10:01:42 -05:00
Jeremy Stretch
1bc38f66ae Changelog entries for #3139 and #3457 2019-11-06 09:58:30 -05:00
Jeremy Stretch
b4433a8471 Merge pull request #3667 from steffann/3139-disable-user-password-change-if-come-in-with-ldap-auth
Hide password change page when user is logged in using LDAP
2019-11-06 09:57:25 -05:00
Jeremy Stretch
053f49c76a Merge pull request #3673 from ananace/cable-color-display
3457 Display cable colors in device interface list
2019-11-06 09:53:59 -05:00
Jeremy Stretch
fbde6187ea Fixes #3669: Include weight field in prefix/VLAN role form 2019-11-06 09:39:21 -05:00
Jeremy Stretch
5eb5c4bac5 Fixes #3674: Include comments on PowerFeed view 2019-11-06 09:26:49 -05:00
Jeremy Stretch
fb4283ed53 Move alternative installations to the GitHub wiki 2019-11-06 09:05:12 -05:00
Alexander Olofsson
bbd65988f9 3457 Display cable colors in device interface list 2019-11-06 10:15:55 +01:00
struppi
a11fa44170 Closes #3663: fix inheritance error 2019-11-04 21:00:44 +01:00
struppi
99a542e4e4 Closes #3663: API filter by created, last_updated 2019-11-04 20:51:56 +01:00
Sander Steffann
a6f2d5b414 Fix code for PEP8 2019-11-03 16:12:39 +03:00
Sander Steffann
7f779e3942 Hide password change page when user is logged in using LDAP 2019-11-03 16:05:53 +03:00
Sander Steffann
7306d56902 Add support for S3 storage for media 2019-11-03 14:16:12 +03:00
Jeremy Stretch
b8f1585976 Merge branch 'develop' into develop-2.7 2019-11-01 16:19:36 -04:00
Jeremy Stretch
a2a83a4a4c Post-release version bump 2019-11-01 15:54:17 -04:00
Jeremy Stretch
f6fbf66c8c Merge branch 'master' into develop 2019-11-01 15:39:24 -04:00
Jeremy Stretch
3deedfd177 Release v2.6.7 2019-11-01 15:37:05 -04:00
Jeremy Stretch
e9d5ff095c Suppress migration messages during tests; fix typo 2019-11-01 15:08:59 -04:00
Jeremy Stretch
43d6bfa15f Corrected test 2019-11-01 15:08:20 -04:00
Jeremy Stretch
875e09013c Move TreeNodeMultipleChoiceFilter tests to utilities (follow-up to #3616) 2019-11-01 15:01:24 -04:00
Jeremy Stretch
ee854eff2c Changelog for #3357 2019-11-01 14:31:03 -04:00
Jeremy Stretch
b176e0fafd Merge pull request #3616 from kobayashi/3357
allow null region filtering
2019-11-01 14:29:32 -04:00
Jeremy Stretch
fb24a4d420 Merge pull request #3660 from tyler-8/gunicorn_options
Add max_requests details to example config
2019-11-01 12:32:39 -04:00
Jeremy Stretch
391c42300e Closes #3659: Add filtering for objects in admin UI 2019-11-01 12:22:39 -04:00
Tyler Bigler
2a8fa01a57 Increase request counts for max_requests 2019-11-01 12:09:44 -04:00
Jeremy Stretch
bd4b496d14 Tweak PR template to indicate placement of issue number 2019-11-01 11:52:43 -04:00
Jeremy Stretch
cf5c24ee2d Add new Interface type from #3619 2019-11-01 11:49:17 -04:00
Jeremy Stretch
66f4aac0e8 Changelog for #3619 2019-11-01 11:45:22 -04:00
Jeremy Stretch
267caa348e Merge pull request #3657 from Netnod/3619-new-400G-osfp-interface-type
3619 new 400 g osfp interface type
2019-11-01 11:43:43 -04:00
Tyler Bigler
7bf09a3085 Remove redundant word 2019-11-01 10:26:53 -04:00
Tyler Bigler
a0de0696c5 Add max_requests details to example config 2019-11-01 10:23:25 -04:00
Jeremy Stretch
a63383dc08 Reorganized navigation menu 2019-10-30 16:39:32 -04:00
Jeremy Stretch
0ad19081ac Move slug-based choices to choices.py 2019-10-30 16:31:04 -04:00
Emil Palm
aa6b2b8407 Merge remote-tracking branch 'upstream/develop' into 3619-new-400G-osfp-interface-type 2019-10-30 14:31:17 -05:00
Emil Palm
3e6726ff97 Add IFACE_TYPE_400GE_OSPF 2019-10-30 14:30:23 -05:00
Jeremy Stretch
bb30f64252 Closes #1865: Add console port and console server port types 2019-10-30 14:25:55 -04:00
Jeremy Stretch
c4dd76a748 Updated v2.7 release notes 2019-10-30 10:34:26 -04:00
Jeremy Stretch
56a248e601 Merge pull request #3654 from netbox-community/3538-scripts-api
3538: Add custom script API endpoints
2019-10-30 09:35:22 -04:00
Jeremy Stretch
0f65cf23a5 Only use module.name for human-facing display 2019-10-30 09:13:26 -04:00
Jeremy Stretch
fd3f718a0a Add tests for custom script API 2019-10-29 16:54:27 -04:00
Jeremy Stretch
93d28e6a72 Improve script output serialization 2019-10-29 16:17:59 -04:00
Jeremy Stretch
60ec3fde9d Closes #3652: Limit next/previous rack by assigned rack group 2019-10-29 15:17:00 -04:00
Jeremy Stretch
1cfb8aea23 Initial work on #3538: script execution API 2019-10-28 15:02:21 -04:00
Jeremy Stretch
846acf7e9d Changelog for #3629 2019-10-28 13:19:01 -04:00
kobayashi
fb9279cc74 Merge pull request #3645 from BegBlev/llpd-port-id
Retrieve port-id in LLDP tab
2019-10-28 12:19:10 -04:00
kobayashi
d2aa9b8e79 filtering multiple regions with null 2019-10-28 02:24:44 -04:00
Tyler Bigler
eaeb52de20 Add CONN_MAX_AGE to documentation (#3642)
* Add CONN_MAX_AGE to sample configurations

* Correct alignment

* Restore ghost space

* Correct alignment.

* Use stable docs url
2019-10-25 13:11:48 -04:00
Jeremy Stretch
fdbf41e9fd Fixes #3643: Update all Django documentation links to 'stable' version 2019-10-25 11:09:30 -04:00
Jeremy Stretch
092346c819 Changelog for #3309 2019-10-25 09:57:17 -04:00
Jeremy Stretch
5a5f51fe48 Merge pull request #3632 from netbox-community/3309-changelog-middleware
Rewrite change logging middleware
2019-10-25 09:47:32 -04:00
Jeremy Stretch
800df1ebbe Fixes #3636: Add missing rack_group field to PowerFeed CSV export 2019-10-25 09:27:33 -04:00
John Anderson
8eec67e73a #3635 changelog 2019-10-24 23:31:06 -04:00
John Anderson
33b420652d Merge pull request #3639 from sdktr/patch-1
Fix #3635 - cache circuits.*
2019-10-24 21:16:28 -04:00
Stefan de Kooter
42b679d9a3 Fix #3635 - cache circuits.*
Enable caching for items under the circuits app
2019-10-25 00:46:05 +02:00
Jeremy Stretch
ae4f17264f Fixes #3460: Extend upgrade script to validate Python dependencies 2019-10-23 17:12:32 -04:00
Jeremy Stretch
c7d8083ac6 Closes #3594: Add ChoiceVar for custom scripts 2019-10-23 15:59:27 -04:00
Jeremy Stretch
95fec1a87c Merge pull request #3627 from bdlamprecht/pg_dump-documentation
Adding note on invalidating cache
2019-10-23 12:24:27 -04:00
Jeremy Stretch
657a7aef42 Changelog for #3455 2019-10-23 11:55:45 -04:00
Jeremy Stretch
ea0d232169 Merge pull request #3572 from frelon/cluster-tenant
Add tenancy to cluster
2019-10-23 11:53:22 -04:00
Jeremy Stretch
7467347af1 Fixes #3596: Prevent server error when reassigning a device to a new device bay 2019-10-23 09:28:00 -04:00
Jeremy Stretch
0ebc2e4ac0 Fix reporting of custom fields in webhook data on object deletion 2019-10-22 16:12:25 -04:00
Jeremy Stretch
ccb9f7bfe2 Rewrote ObjectChangeMiddleware to remove the curried handle_deleted_object() function 2019-10-22 15:10:49 -04:00
kobayashi
766b5dff24 allow null region filtering 2019-10-22 00:41:49 -04:00
Vincent Catros
68e0329c1b Retrieve port-id in LLDP tab 2019-10-21 11:57:23 +02:00
Brady Lamprecht
fd99994fdb Adding note on invalidating cache 2019-10-18 14:59:55 -06:00
Jeremy Stretch
f21a63382c Changelog for #3340 2019-10-18 09:08:56 -04:00
Jeremy Stretch
6f0581598b Merge pull request #3613 from kobayashi/3340
modified front port connection type list
2019-10-18 09:07:22 -04:00
kobayashi
244e85e836 modify patch panel port connection type list 2019-10-18 00:01:21 -04:00
Jeremy Stretch
1df6713ad5 Minor improvements pertaining to CII best practices 2019-10-17 20:56:37 -04:00
Jeremy Stretch
c6893731ad Merge pull request #3621 from netbox-community/451-devicetype-import
Enable YAML/JSON-based DeviceType import
2019-10-17 16:43:15 -04:00
Jeremy Stretch
2ffbced47e Rework InterfaceTypes and PortTypes classes 2019-10-17 16:38:31 -04:00
Jeremy Stretch
bfce177a07 Merge pull request #3604 from netbox-community/3282-seperate-redis-config
implements #3282 - seperate webhooks and caching redis configs
2019-10-17 16:03:26 -04:00
Jeremy Stretch
8ca182571c Rebase schema migrations 2019-10-17 15:53:10 -04:00
Jeremy Stretch
801dd384d8 Merge branch 'develop' into develop-2.7 2019-10-17 15:51:33 -04:00
Jeremy Stretch
adc96702db Deleted errant import of graphviz 2019-10-17 15:43:49 -04:00
Jeremy Stretch
af73ce75ce Merge pull request #3607 from netbox-community/3606-stale-bot
implemented #3606 - added stale bot config
2019-10-17 14:32:40 -04:00
Jeremy Stretch
f08968da49 Exempt issues tagged with "status: blocked" 2019-10-17 14:28:27 -04:00
Fredrik Lönnegren
18e0bd8937 Fix typo 2019-10-16 08:22:06 +02:00
John Anderson
24344ccfaf Merge pull request #3615 from markkuleinio/develop
Update examples in webhooks.md
2019-10-15 14:27:20 -04:00
Markku Leiniö
91f045a2e4 Update examples in webhooks.md 2019-10-15 20:51:57 +03:00
Jeremy Stretch
050dfb279d Merge pull request #3597 from dgarros/patch-1
Update pillow version to 6.2.0
2019-10-15 11:54:44 -04:00
Jeremy Stretch
f9b7f18be6 Merge pull request #3609 from ScanPlusGmbH/missing-variable
Add SCRIPTS_ROOT to configuration.example.py
2019-10-14 11:58:28 -04:00
Tobias Genannt
a7380ba353 Add SCRIPTS_ROOT to configuration.example.py
Fixes #3608 by adding the new variable to the example configuration.
2019-10-14 09:29:04 +02:00
John Anderson
b8feba1070 implemented #3606 - added stale bot config 2019-10-13 04:12:58 -04:00
John Anderson
64575fec42 Merge pull request #3605 from netbox-community/3445-webhooks-additional-headers
implemented #3445 - Add support for additional user defined headers t…
2019-10-13 03:15:43 -04:00
John Anderson
c7d9bf839e implemented #3445 - Add support for additional user defined headers to be added to webhook requests 2019-10-13 03:09:58 -04:00
John Anderson
0d15ac15ae implements #3282 - seperate webhooks and caching redis configs 2019-10-13 02:49:54 -04:00
John Anderson
b013a60b12 Merge branch 'develop' into develop-2.7 2019-10-13 02:03:16 -04:00
John Anderson
8480c0da53 Merge pull request #3603 from netbox-community/3499-webhooks-ca-filepath
implemented #3499 - Add  to Webhook model to support user supplied CA certificate verification of webhook requests
2019-10-13 01:50:10 -04:00
John Anderson
5e88313276 typo in change log 2019-10-13 01:45:20 -04:00
John Anderson
09d7d38b04 implemented #3499 - Add to Webhook model to support user supplied CA certificate verrification of webhook requests 2019-10-13 01:43:08 -04:00
Damien Garros
4cc29729f9 Update pillow version to 6.2.0
A new CVE just got reporter regarding Pillow 
http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-16865
it's affecting all version prior to 6.2.0
2019-10-11 13:45:37 -04:00
Jeremy Stretch
d787c353f3 Added slug choices for interface and port types 2019-10-10 23:24:44 -04:00
Jeremy Stretch
553fe0fe97 Merge branch 'develop' into 451-devicetype-import 2019-10-10 13:50:57 -04:00
Jeremy Stretch
aeeb49a6a7 Merge branch 'develop' into develop-2.7 2019-10-10 13:41:10 -04:00
Jeremy Stretch
b25faec159 Post-release version bump 2019-10-10 12:42:57 -04:00
Fredrik Lönnegren
3262805938 Add tenancy to cluster
fix pep8
2019-10-07 16:20:14 +02:00
Jeremy Stretch
807d849657 PEP8 fix 2019-10-01 17:07:17 -04:00
Jeremy Stretch
6892b79366 Enforce object creation permissions 2019-10-01 16:54:10 -04:00
Jeremy Stretch
88d61db384 Fix YAMLLoadWarning 2019-10-01 16:39:11 -04:00
Jeremy Stretch
ee4e68b082 Rewrote test for DeviceType import 2019-10-01 16:36:31 -04:00
Jeremy Stretch
edc1b52f65 Adopted a different approach to importing related objects 2019-09-27 16:51:12 -04:00
Jeremy Stretch
36d4f0d259 Fix typo 2019-09-25 16:39:04 -04:00
Jeremy Stretch
5f3528cf74 Capture MultiObjectField default form field values 2019-09-25 16:19:22 -04:00
Jeremy Stretch
47f1febfc9 Capture import form field default values 2019-09-25 16:06:09 -04:00
Jeremy Stretch
93154abb31 Merge branch 'develop' into 451-devicetype-import 2019-09-25 13:44:48 -04:00
Jeremy Stretch
778e5bed3c Merge branch 'develop' into develop-2.7 2019-09-25 13:44:29 -04:00
Jeremy Stretch
0615d368f2 Force validation of individual objects within a MultiObjectField 2019-09-24 16:51:59 -04:00
Jeremy Stretch
30ee232654 Move JSON/YAML data valdiation to ImportForm 2019-09-24 16:13:52 -04:00
Jeremy Stretch
2621f17cde Remove legacy CSV-based DeviceType import 2019-09-24 16:03:10 -04:00
Jeremy Stretch
15b2a7eab0 Fix form rendering; enable toggling of redirect to imported object 2019-09-24 15:58:23 -04:00
Jeremy Stretch
5049c6c0a1 Add test for DeviceType import 2019-09-20 15:57:44 -04:00
Jeremy Stretch
60b70b6c7b Add RearPortTemplate power_port field 2019-09-20 15:16:14 -04:00
Jeremy Stretch
5266fc67c9 Extend DeviceType import to include related objects 2019-09-20 14:02:14 -04:00
Jeremy Stretch
56dcadb69b Merged v2.6.4 2019-09-20 08:35:14 -04:00
Jeremy Stretch
f8fdca4968 Initial work on JSON/YAML-based DeviceType import 2019-09-13 16:18:29 -04:00
Daniel Sheppard
9e258dd31e Fixes: #2902 - Implements systemd (#3017)
* Closes #2902 - Migrate to systemd from supervisord
* Closes #2902 - Migrate to systemd from supervisord

* Update systemd unit and environment file
* Add gunicorn.conf
* Update documentation and CHANGELOG.  Moved parameters around on service file
* Update Gitignore
2019-09-06 11:46:35 -05:00
Jeremy Stretch
480db83f39 Renumber remove_topology_maps migration 2019-09-05 10:25:44 -04:00
Jeremy Stretch
cc0f0c4843 Merge v2.6.3 2019-09-04 16:45:33 -04:00
John Anderson
a6511632ad closes #3407 - Added code coverage reporting to the CI pipeline 2019-08-14 20:28:23 -04:00
Jeremy Stretch
15b55f5e62 Remove deprecated form_factor accessor on Interface and InterfaceTemplate 2019-08-08 21:35:59 -04:00
Jeremy Stretch
dccda62f2d Closes #2745: Remove topology maps 2019-08-08 21:33:20 -04:00
Jeremy Stretch
ef432754ee Started 2.7 branch 2019-08-08 21:19:14 -04:00
978 changed files with 88097 additions and 3639 deletions

View File

@@ -9,8 +9,7 @@
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ACCEPTED BUG REPORT OR
FEATURE REQUEST, IT WILL BE MARKED AS INVALID AND CLOSED.
-->
### Fixes:
### Fixes: <ISSUE NUMBER GOES HERE>
<!--
Please include a summary of the proposed changes below.
-->

23
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 14
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- "status: accepted"
- "status: gathering feedback"
- "status: blocked"
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. NetBox
is governed by a small group of core maintainers which means not all opened
issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
This issue has been automatically closed due to lack of activity. In an
effort to reduce noise, please do not comment any further. Note that the
core maintainers may elect to reopen this issue at a later date if deemed
necessary.

4
.gitignore vendored
View File

@@ -12,5 +12,9 @@
fabfile.py
*.swp
gunicorn_config.py
gunicorn.conf
netbox.log
netbox.pid
.DS_Store
.vscode
.coverage

View File

@@ -10,6 +10,7 @@ python:
install:
- pip install -r requirements.txt
- pip install pycodestyle
- pip install coverage
before_script:
- psql --version
- psql -U postgres -c 'SELECT version();'

View File

@@ -24,7 +24,7 @@ already been fixed.
to see if the bug you've found has already been reported. If you think you may
be experiencing a reported issue that hasn't already been resolved, please
click "add a reaction" in the top right corner of the issue and add a thumbs
up (+1). You mightalso want to add a comment describing how it's affecting your
up (+1). You might also want to add a comment describing how it's affecting your
installation. This will allow us to prioritize bugs based on how many users are
affected.
@@ -99,6 +99,8 @@ any work that's already in progress.
* Any pull request which does _not_ relate to an accepted issue will be closed.
* All major new functionality must include relevant tests where applicable.
* When submitting a pull request, please be sure to work off of the `develop`
branch, rather than `master`. The `develop` branch is used for ongoing
development, while `master` is used for tagging new stable releases.
@@ -118,6 +120,30 @@ feedback. **Do not** comment on an issue just to show your support (give the
top post a :+1: instead) or ask for an ETA. These comments will be deleted to
reduce noise in the discussion.
## Issue Lifecycle
When a correctly formatted issue is submitted it is evaluated by a moderator
who may elect to immediately label the issue as accepted in addition to another
issue type label. In other cases, the issue may be labeled as "status: gathering feedback"
which will often be accompanied by a comment from a moderator asking for further dialog from the community.
If an issue is labeled as "status: revisions needed" a moderator has identified a problem with
the issue itself and is asking for the submitter himself to update the original post with
the requested information. If the original post is not updated in a reasonable amount of time,
the issue will be closed as invalid.
The core maintainers group has chosen to make use of the GitHub Stale bot to aid in issue management.
* Issues will be marked as stale after 14 days of no activity.
* Then after 7 more days of inactivity, the issue will be closed.
* Any issue bearing one of the following labels will be exempt from all Stale bot actions:
* `status: accepted`
* `status: gathering feedback`
* `status: blocked`
It is natural that some new issues get more attention than others. Often this is a metric of an issues's
overall usefulness to the project. In other cases in which issues merely get lost in the shuffle,
notifications from Stale bot can bring renewed attention to potentially meaningful issues.
## Maintainer Guidance
* Maintainers are expected to contribute at least four hours per week to the

View File

@@ -3,7 +3,8 @@
NetBox is an IP address management (IPAM) and data center infrastructure
management (DCIM) tool. Initially conceived by the network engineering team at
[DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically
to address the needs of network and infrastructure engineers.
to address the needs of network and infrastructure engineers. It is intended to
function as a domain-specific source of truth for network operations.
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/)
Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a
@@ -35,12 +36,14 @@ Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for
instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/netbox-community/netbox/releases)
and run `upgrade.sh`.
## Alternative Installations
# Providing Feedback
* [Docker container](https://github.com/netbox-community/netbox-docker) (via [@cimnine](https://github.com/cimnine))
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))
* [Kubernetes deployment](https://github.com/CENGN/netbox-kubernetes) (via [@CENGN](https://github.com/CENGN))
Feature requests and bug reports must be submitted as GiHub issues. (Please be
sure to use the [appropriate template](https://github.com/netbox-community/netbox/issues/new/choose).)
For general discussion, please consider joining our [mailing list](https://groups.google.com/forum/#!forum/netbox-discuss).
If you are interested in contributing to the development of NetBox, please read
our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
# Related projects

View File

@@ -22,14 +22,14 @@ django-filter
# https://github.com/django-mptt/django-mptt
django-mptt
# Django integration for RQ (Reqis queuing)
# https://github.com/rq/django-rq
django-rq
# Prometheus metrics library for Django
# https://github.com/korfuri/django-prometheus
django-prometheus
# Django integration for RQ (Reqis queuing)
# https://github.com/rq/django-rq
django-rq
# Abstraction models for rendering and paginating HTML tables
# https://github.com/jieter/django-tables2
django-tables2
@@ -54,10 +54,6 @@ djangorestframework
# https://github.com/axnsan12/drf-yasg
drf-yasg[validation]
# Python interface to the graphviz graph rendering utility
# https://github.com/xflr6/graphviz
graphviz
# Simple markup language for rendering HTML
# https://github.com/Python-Markdown/markdown
# py-gfm requires Markdown<3.0
@@ -82,3 +78,11 @@ py-gfm
# Extensive cryptographic library (fork of pycrypto)
# https://github.com/Legrandin/pycryptodome
pycryptodome
# In-memory key/value store used for caching and queuing
# https://github.com/andymccurdy/redis-py
redis
# Python Package to write SVG files - used for rack elevations
# https://github.com/mozman/svgwrite
svgwrite

16
contrib/gunicorn.py Normal file
View File

@@ -0,0 +1,16 @@
# The IP address (typically localhost) and port that the Netbox WSGI process should listen on
bind = '127.0.0.1:8001'
# Number of gunicorn workers to spawn. This should typically be 2n+1, where
# n is the number of CPU cores present.
workers = 5
# Number of threads per worker process
threads = 3
# Timeout (in seconds) for a request to complete
timeout = 120
# The maximum number of requests a worker can handle before being respawned
max_requests = 5000
max_requests_jitter = 500

22
contrib/netbox-rq.service Normal file
View File

@@ -0,0 +1,22 @@
[Unit]
Description=NetBox Request Queue Worker
Documentation=https://netbox.readthedocs.io/en/stable/
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/netbox
ExecStart=/usr/bin/python3 /opt/netbox/netbox/manage.py rqworker
Restart=on-failure
RestartSec=30
PrivateTmp=true
[Install]
WantedBy=multi-user.target

22
contrib/netbox.service Normal file
View File

@@ -0,0 +1,22 @@
[Unit]
Description=NetBox WSGI Service
Documentation=https://netbox.readthedocs.io/en/stable/
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=www-data
Group=www-data
PIDFile=/var/tmp/netbox.pid
WorkingDirectory=/opt/netbox
ExecStart=/usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox --config /opt/netbox/gunicorn.py netbox.wsgi
Restart=on-failure
RestartSec=30
PrivateTmp=true
[Install]
WantedBy=multi-user.target

View File

@@ -119,6 +119,23 @@ Stored a numeric integer. Options include:
A true/false flag. This field has no options beyond the defaults.
### ChoiceVar
A set of choices from which the user can select one.
* `choices` - A list of `(value, label)` tuples representing the available choices. For example:
```python
CHOICES = (
('n', 'North'),
('s', 'South'),
('e', 'East'),
('w', 'West')
)
direction = ChoiceVar(choices=CHOICES)
```
### ObjectVar
A NetBox object. The list of available objects is defined by the queryset parameter. Each instance of this variable is limited to a single object type.
@@ -165,7 +182,7 @@ class NewBranchScript(Script):
class Meta:
name = "New Branch"
description = "Provision a new branch site"
fields = ['site_name', 'switch_count', 'switch_model']
field_order = ['site_name', 'switch_count', 'switch_model']
site_name = StringVar(
description="Name of the new site"

View File

@@ -4,7 +4,7 @@ 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.
Export templates are written in [Django's template language](https://docs.djangoproject.com/en/1.9/ref/templates/language/), which is very similar to Jinja2. The list of objects returned from the database 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:
Export templates are written in [Django's template language](https://docs.djangoproject.com/en/stable/ref/templates/language/), which is very similar to Jinja2. The list of objects returned from the database 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:
```
{% for rack in queryset %}

View File

@@ -1,17 +0,0 @@
# Topology Maps
NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps.
Each topology map is associated with a site. A site can have multiple topology maps, which might each illustrate a different aspect of its infrastructure (for example, production versus backend infrastructure).
To define the scope of a topology map, decide which devices you want to include. The map will only include interface connections with both points terminated on an included device. Specify the devices to include in the **device patterns** field by entering a list of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) matching device names. For example, if you wanted to include "mgmt-switch1" through "mgmt-switch99", you might use the regex `mgmt-switch\d+`.
Each line of the **device patterns** field represents a hierarchical layer within the topology map. For example, you might map a traditional network with core, distribution, and access tiers like this:
```
core-switch-[abcd]
dist-switch\d
access-switch\d+;oob-switch\d+
```
Note that you can combine multiple regexes onto one line using semicolons. The order in which regexes are listed on a line is significant: devices matching the first regex will be rendered first, and subsequent groups will be rendered to the right of those.

View File

@@ -11,8 +11,10 @@ The webhook POST request is structured as so (assuming `application/json` as the
```no-highlight
{
"event": "created",
"signal_received_timestamp": 1508769597,
"model": "Site"
"timestamp": "2019-10-12 12:51:29.746944",
"username": "admin",
"model": "site",
"request_id": "43d8e212-94c7-4f67-b544-0dcde4fc0f43",
"data": {
...
}
@@ -24,8 +26,10 @@ The webhook POST request is structured as so (assuming `application/json` as the
```no-highlight
{
"event": "deleted",
"signal_received_timestamp": 1508781858.544069,
"model": "Site",
"timestamp": "2019-10-12 12:55:44.030750",
"username": "johnsmith",
"model": "site",
"request_id": "e9bb83b2-ebe4-4346-b13f-07144b1a00b4",
"data": {
"asn": None,
"comments": "",

View File

@@ -4,7 +4,7 @@ NetBox includes a Python shell within which objects can be directly queried, cre
./manage.py nbshell
```
This will launch a customized version of [the built-in Django shell](https://docs.djangoproject.com/en/dev/ref/django-admin/#shell) with all relevant NetBox models pre-loaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.)
This will launch a customized version of [the built-in Django shell](https://docs.djangoproject.com/en/stable/ref/django-admin/#shell) with all relevant NetBox models pre-loaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.)
```
$ ./manage.py nbshell
@@ -28,7 +28,7 @@ DCIM:
## Querying Objects
Objects are retrieved by forming a [Django queryset](https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-objects). The base queryset for an object takes the form `<model>.objects.all()`, which will return a (truncated) list of all objects of that type.
Objects are retrieved by forming a [Django queryset](https://docs.djangoproject.com/en/stable/topics/db/queries/#retrieving-objects). The base queryset for an object takes the form `<model>.objects.all()`, which will return a (truncated) list of all objects of that type.
```
>>> Device.objects.all()
@@ -99,7 +99,7 @@ This approach can span multiple levels of relations. For example, the following
```
!!! note
While the above query is functional, it is very inefficient. There are ways to optimize such requests, however they are out of the scope of this document. For more information, see the [Django queryset method reference](https://docs.djangoproject.com/en/dev/ref/models/querysets/) documentation.
While the above query is functional, it is very inefficient. There are ways to optimize such requests, however they are out of the scope of this document. For more information, see the [Django queryset method reference](https://docs.djangoproject.com/en/stable/ref/models/querysets/) documentation.
Reverse relationships can be traversed as well. For example, the following will find all devices with an interface named "em0":
@@ -137,7 +137,7 @@ To return the inverse of a filtered queryset, use `exclude()` instead of `filter
```
!!! info
The examples above are intended only to provide a cursory introduction to queryset filtering. For an exhaustive list of the available filters, please consult the [Django queryset API docs](https://docs.djangoproject.com/en/dev/ref/models/querysets/).
The examples above are intended only to provide a cursory introduction to queryset filtering. For an exhaustive list of the available filters, please consult the [Django queryset API docs](https://docs.djangoproject.com/en/stable/ref/models/querysets/).
## Creating and Updating Objects

View File

@@ -39,6 +39,11 @@ If you want to export only the database schema, and not the data itself (e.g. fo
```no-highlight
pg_dump -s netbox > netbox_schema.sql
```
If you are migrating your instance of NetBox to a different machine, please make sure you invalidate the cache by performing this command:
```no-highlight
python3 manage.py invalidate all
```
---

View File

@@ -139,7 +139,7 @@ Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce uni
By default, all messages of INFO severity or higher will be logged to the console. Additionally, if `DEBUG` is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in `ADMINS`.
The Django framework on which NetBox runs allows for the customization of logging, e.g. to write logs to file. Please consult the [Django logging documentation](https://docs.djangoproject.com/en/1.11/topics/logging/) for more information on configuring this setting. Below is an example which will write all INFO and higher messages to a file:
The Django framework on which NetBox runs allows for the customization of logging, e.g. to write logs to file. Please consult the [Django logging documentation](https://docs.djangoproject.com/en/stable/topics/logging/) for more information on configuring this setting. Below is an example which will write all INFO and higher messages to a file:
```
LOGGING = {
@@ -293,6 +293,26 @@ Session data is used to track authenticated users when they access NetBox. By de
---
## STORAGE_BACKEND
Default: None (local storage)
The backend storage engine for handling uploaded files (e.g. image attachments). NetBox supports integration with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) package, which provides backends for several popular file storage services. If not configured, local filesystem storage will be used.
The configuration parameters for the specified storage backend are defined under the `STORAGE_CONFIG` setting.
---
## STORAGE_CONFIG
Default: Empty
A dictionary of configuration parameters for the storage backend configured as `STORAGE_BACKEND`. The specific parameters to be used here are specific to each backend; see the [`django-storages` documentation](https://django-storages.readthedocs.io/en/stable/) for more detail.
If `STORAGE_BACKEND` is not defined, this setting will be ignored.
---
## TIME_ZONE
Default: UTC
@@ -301,17 +321,9 @@ The time zone NetBox will use when dealing with dates and times. It is recommend
---
## WEBHOOKS_ENABLED
Default: False
Enable this option to run the webhook backend. See the docs section on the webhook backend [here](../../additional-features/webhooks/) for more information on setup and use.
---
## Date and Time Formatting
You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date).
You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date).
Defaults:

View File

@@ -2,7 +2,7 @@
## ALLOWED_HOSTS
This is a list of valid fully-qualified domain names (FQDNs) that is used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different (e.g. when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server). NetBox will not permit access to the server via any other hostnames (or IPs). The value of this option is also used to set `CSRF_TRUSTED_ORIGINS`, which restricts `HTTP POST` to the same set of hosts (more about this [here](https://docs.djangoproject.com/en/1.9/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS)). Keep in mind that NetBox, by default, has `USE_X_FORWARDED_HOST = True` (in `netbox/netbox/settings.py`) which means that if you're using a reverse proxy, it's the FQDN used to reach that reverse proxy which needs to be in this list (more about this [here](https://docs.djangoproject.com/en/1.9/ref/settings/#allowed-hosts)).
This is a list of valid fully-qualified domain names (FQDNs) that is used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different (e.g. when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server). NetBox will not permit access to the server via any other hostnames (or IPs). The value of this option is also used to set `CSRF_TRUSTED_ORIGINS`, which restricts `HTTP POST` to the same set of hosts (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS)). Keep in mind that NetBox, by default, has `USE_X_FORWARDED_HOST = True` (in `netbox/netbox/settings.py`) which means that if you're using a reverse proxy, it's the FQDN used to reach that reverse proxy which needs to be in this list (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#allowed-hosts)).
Example:
@@ -21,16 +21,18 @@ NetBox requires access to a PostgreSQL database service to store data. This serv
* `PASSWORD` - PostgreSQL password
* `HOST` - Name or IP address of the database server (use `localhost` if running locally)
* `PORT` - TCP port of the PostgreSQL service; leave blank for default port (5432)
* `CONN_MAX_AGE` - Number in seconds for Netbox to keep database connections open. 150-300 seconds is typically a good starting point ([more info](https://docs.djangoproject.com/en/stable/ref/databases/#persistent-connections)).
Example:
```
```python
DATABASE = {
'NAME': 'netbox', # Database name
'USER': 'netbox', # PostgreSQL username
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
'HOST': 'localhost', # Database server
'PORT': '', # Database port (leave blank for default)
'CONN_MAX_AGE': 300, # Max database connection age
}
```
@@ -40,40 +42,48 @@ DATABASE = {
[Redis](https://redis.io/) is an in-memory data store similar to memcached. While Redis has been an optional component of
NetBox since the introduction of webhooks in version 2.4, it is required starting in 2.6 to support NetBox's caching
functionality (as well as other planned features).
functionality (as well as other planned features). In 2.7, the connection settings were broken down into two sections for
webhooks and caching, allowing the user to connect to different Redis instances/databases per feature.
Redis is configured using a configuration setting similar to `DATABASE`:
Redis is configured using a configuration setting similar to `DATABASE` and these settings are the same for both of the `webhooks` and `caching` subsections:
* `HOST` - Name or IP address of the Redis server (use `localhost` if running locally)
* `PORT` - TCP port of the Redis service; leave blank for default port (6379)
* `PASSWORD` - Redis password (if set)
* `DATABASE` - Numeric database ID for webhooks
* `CACHE_DATABASE` - Numeric database ID for caching
* `DATABASE` - Numeric database ID
* `DEFAULT_TIMEOUT` - Connection timeout in seconds
* `SSL` - Use SSL connection to Redis
Example:
```
```python
REDIS = {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 0,
'CACHE_DATABASE': 1,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
'webhooks': {
'HOST': 'redis.example.com',
'PORT': 1234,
'PASSWORD': 'foobar',
'DATABASE': 0,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
},
'caching': {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 1,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
}
}
```
!!! note:
If you were using these settings in a prior release with webhooks, the `DATABASE` setting remains the same but
an additional `CACHE_DATABASE` setting has been added with a default value of 1 to support the caching backend. The
`DATABASE` setting will be renamed in a future release of NetBox to better relay the meaning of the setting.
If you are upgrading from a version prior to v2.7, please note that the Redis connection configuration settings have
changed. Manual modification to bring the `REDIS` section inline with the above specification is necessary
!!! warning:
It is highly recommended to keep the webhook and cache databases seperate. Using the same database number for both may result in webhook
processing data being lost in cache flushing events.
It is highly recommended to keep the webhook and cache databases separate. Using the same database number on the
same Redis instance for both may result in webhook processing data being lost during cache flushing events.
---

View File

@@ -1,6 +1,6 @@
# Style Guide
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations. See `scripts/cibuild.sh`.
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations. See `scripts/cibuild.sh`.
## PEP 8 Exceptions

View File

@@ -4,7 +4,7 @@ NetBox requires a PostgreSQL database to store data. This can be hosted locally
The installation instructions provided here have been tested to work on Ubuntu 18.04 and CentOS 7.5. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
!!! warning
NetBox v2.2 and later requires PostgreSQL 9.4 or higher.
NetBox requires PostgreSQL 9.4 or higher.
# Installation

View File

@@ -5,14 +5,14 @@ This section of the documentation discusses installing and configuring the NetBo
**Ubuntu**
```no-highlight
# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev redis-server zlib1g-dev
# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev redis-server zlib1g-dev
```
**CentOS**
```no-highlight
# yum install -y epel-release
# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config redis
# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel openssl-devel redhat-rpm-config redis
# easy_install-3.6 pip
# ln -s /usr/bin/python36 /usr/bin/python3
```
@@ -90,6 +90,14 @@ NetBox supports integration with the [NAPALM automation](https://napalm-automati
# pip3 install napalm
```
## Remote File Storage (Optional)
By default, NetBox will use the local filesystem to storage uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired backend](../../configuration/optional-settings/#storage_backend) in `configuration.py`.
```no-highlight
# pip3 install django-storages
```
# Configuration
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
@@ -129,6 +137,7 @@ DATABASE = {
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
'HOST': 'localhost', # Database server
'PORT': '', # Database port (leave blank for default)
'CONN_MAX_AGE': 300, # Max database connection age
}
```
@@ -138,13 +147,22 @@ Redis is a in-memory key-value store required as part of the NetBox installation
```python
REDIS = {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 0,
'CACHE_DATABASE': 1,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
'webhooks': {
'HOST': 'redis.example.com',
'PORT': 1234,
'PASSWORD': 'foobar',
'DATABASE': 0,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
},
'caching': {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 1,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
}
}
```
@@ -194,27 +212,7 @@ Superuser created successfully.
```no-highlight
# python3 manage.py collectstatic --no-input
You have requested to collect static files at the destination
location as specified in your settings:
/opt/netbox/netbox/static
This will overwrite existing files!
Are you sure you want to do this?
Type 'yes' to continue, or 'no' to cancel: yes
```
# Load Initial Data (Optional)
NetBox ships with some initial data to help you get started: RIR definitions, common devices roles, etc. You can delete any seed data that you don't want to keep.
!!! note
This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch.
```no-highlight
# python3 manage.py loaddata initial_data
Installed 43 object(s) from 4 fixture(s)
959 static files copied to '/opt/netbox/netbox/static'.
```
# Test the Application
@@ -236,3 +234,11 @@ Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on
!!! warning
If the test service does not run, or you cannot reach the NetBox home page, something has gone wrong. Do not proceed with the rest of this guide until the installation has been corrected.
Note that the initial UI will be locked down for non-authenticated users.
![NetBox UI as seen by a non-authenticated user](../media/installation/netbox_ui_guest.png)
After logging in as the superuser you created earlier, all areas of the UI will be available.
![NetBox UI as seen by an administrator](../media/installation/netbox_ui_admin.png)

View File

@@ -1,4 +1,4 @@
We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence.
We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll use systemd to enable service persistence.
!!! info
For the sake of brevity, only Ubuntu 18.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.
@@ -32,7 +32,6 @@ server {
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
}
}
```
@@ -108,45 +107,53 @@ Install gunicorn:
# pip3 install gunicorn
```
Save the following configuration in the root netbox installation path as `gunicorn_config.py` (e.g. `/opt/netbox/gunicorn_config.py` per our example installation). Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.
Copy `contrib/gunicorn.conf` to `/opt/netbox/gunicorn.conf`. We make a copy of this file to ensure that any changes to it do not get overwritten by a future upgrade.
```no-highlight
command = '/usr/bin/gunicorn'
pythonpath = '/opt/netbox/netbox'
bind = '127.0.0.1:8001'
workers = 3
user = 'www-data'
# cp contrib/gunicorn.py /opt/netbox/gunicorn.py
```
# supervisord Installation
You may wish to edit this file to change the bound IP address or port number, or to make performance-related adjustments.
Install supervisor:
# systemd configuration
We'll use systemd to control the daemonization of NetBox services. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory:
```no-highlight
# apt-get install -y supervisor
# cp contrib/*.service /etc/systemd/system/
```
Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command` and `directory` paths as needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.
!!! note
These service files assume that gunicorn is installed at `/usr/local/bin/gunicorn`. If the output of `which gunicorn` indicates a different path, you'll need to correct the `ExecStart` path in both files.
Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
```no-highlight
[program:netbox]
command = gunicorn -c /opt/netbox/gunicorn_config.py netbox.wsgi
directory = /opt/netbox/netbox/
user = www-data
[program:netbox-rqworker]
command = python3 /opt/netbox/netbox/manage.py rqworker
directory = /opt/netbox/netbox/
user = www-data
# systemctl daemon-reload
# systemctl start netbox.service
# systemctl start netbox-rq.service
# systemctl enable netbox.service
# systemctl enable netbox-rq.service
```
Then, restart the supervisor service to detect and run the gunicorn service:
You can use the command `systemctl status netbox` to verify that the WSGI service is running:
```no-highlight
# service supervisor restart
```
# systemctl status netbox.service
● netbox.service - NetBox WSGI Service
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2019-12-12 19:23:40 UTC; 25s ago
Docs: https://netbox.readthedocs.io/en/stable/
Main PID: 11993 (gunicorn)
Tasks: 6 (limit: 2362)
CGroup: /system.slice/netbox.service
├─11993 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
├─12015 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
├─12016 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
...
```
At this point, you should be able to connect to the nginx HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running.
At this point, you should be able to connect to the HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running.
!!! info
Please keep in mind that the configurations provided here are bare minimums required to get NetBox up and running. You will almost certainly want to make some changes to better suit your production environment.
Please keep in mind that the configurations provided here are bare minimums required to get NetBox up and running. You may want to make adjustments to better suit your production environment.

View File

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

View File

@@ -0,0 +1,100 @@
# Migration
Migration is not required, as supervisord will still continue to function.
## Ubuntu
### Remove supervisord:
```no-highlight
# apt-get remove -y supervisord
```
### systemd configuration:
Copy or link contrib/netbox.service and contrib/netbox-rq.service to /etc/systemd/system/netbox.service and /etc/systemd/system/netbox-rq.service
```no-highlight
# cp contrib/netbox.service /etc/systemd/system/netbox.service
# cp contrib/netbox-rq.service /etc/systemd/system/netbox-rq.service
```
Edit /etc/systemd/system/netbox.service and /etc/systemd/system/netbox-rq.service. Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`). If using CentOS/RHEL. Change the username from `www-data` to `nginx` or `apache`:
```no-highlight
/usr/local/bin/gunicorn --pid ${PidPath} --pythonpath ${WorkingDirectory}/netbox --config ${ConfigPath} netbox.wsgi
```
```no-highlight
User=www-data
Group=www-data
```
Copy contrib/netbox.env to /etc/sysconfig/netbox.env
```no-highlight
# cp contrib/netbox.env /etc/sysconfig/netbox.env
```
Edit /etc/sysconfig/netbox.env and change the settings as required. Update the `WorkingDirectory` variable if needed.
```no-highlight
# Name is the Process Name
#
Name = 'Netbox'
# ConfigPath is the path to the gunicorn config file.
#
ConfigPath=/opt/netbox/gunicorn.conf
# WorkingDirectory is the Working Directory for Netbox.
#
WorkingDirectory=/opt/netbox/
# PidPath is the path to the pid for the netbox WSGI
#
PidPath=/var/run/netbox.pid
```
Copy contrib/gunicorn.conf to gunicorn.conf
```no-highlight
# cp contrib/gunicorn.conf to gunicorn.conf
```
Edit gunicorn.conf and change the settings as required.
```
# Bind is the ip and port that the Netbox WSGI should bind to
#
bind='127.0.0.1:8001'
# Workers is the number of workers that GUnicorn should spawn.
# Workers should be: cores * 2 + 1. So if you have 8 cores, it would be 17.
#
workers=3
# Threads
# The number of threads for handling requests
#
threads=3
# Timeout is the timeout between gunicorn receiving a request and returning a response (or failing with a 500 error)
#
timeout=120
# ErrorLog
# ErrorLog is the logfile for the ErrorLog
#
errorlog='/opt/netbox/netbox.log'
```
Finally, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
```no-highlight
# systemctl daemon-reload
# systemctl start netbox.service
# systemctl start netbox-rq.service
# systemctl enable netbox.service
# systemctl enable netbox-rq.service
```

View File

@@ -84,14 +84,12 @@ This script:
# Restart the WSGI Service
Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`:
Finally, restart the WSGI services to run the new code. If you followed this guide for the initial installation, this is done using `systemctl:
```no-highlight
# sudo supervisorctl restart netbox
# sudo systemctl restart netbox
# sudo systemctl restart netbox-rqworker
```
If using webhooks, also restart the Redis worker:
```no-highlight
# sudo supervisorctl restart netbox-rqworker
```
!!! note
It's possible you are still using supervisord instead of the linux native systemd. If you are still using supervisord you can restart the services by either restarting supervisord or by using supervisorctl to restart netbox.

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -1 +1 @@
version-2.6.md
version-2.7.md

View File

@@ -1,3 +1,51 @@
# v2.6.8 (2019-12-10)
## Enhancements
* [#3139](https://github.com/netbox-community/netbox/issues/3139) - Disable password change form for LDAP-authenticated users
* [#3457](https://github.com/netbox-community/netbox/issues/3457) - Display cable colors on device view
* [#3329](https://github.com/netbox-community/netbox/issues/3329) - Remove obsolete P3P policy header
* [#3663](https://github.com/netbox-community/netbox/issues/3663) - Add query filters for `created` and `last_updated` fields
* [#3722](https://github.com/netbox-community/netbox/issues/3722) - Allow the underscore character in IPAddress DNS names
## Bug Fixes
* [#3312](https://github.com/netbox-community/netbox/issues/3312) - Fix validation error when editing power cables in bulk
* [#3644](https://github.com/netbox-community/netbox/issues/3644) - Fix exception when connecting a cable to a RearPort with no corresponding FrontPort
* [#3669](https://github.com/netbox-community/netbox/issues/3669) - Include `weight` field in prefix/VLAN role form
* [#3674](https://github.com/netbox-community/netbox/issues/3674) - Include comments on PowerFeed view
* [#3679](https://github.com/netbox-community/netbox/issues/3679) - Fix link for assigned ipaddress in interface page
* [#3709](https://github.com/netbox-community/netbox/issues/3709) - Prevent exception when importing an invalid cable definition
* [#3720](https://github.com/netbox-community/netbox/issues/3720) - Correctly indicate power feed terminations on cable list
* [#3724](https://github.com/netbox-community/netbox/issues/3724) - Fix API filtering of interfaces by more than one device name
* [#3725](https://github.com/netbox-community/netbox/issues/3725) - Enforce client validation for minimum service port number
---
# v2.6.7 (2019-11-01)
## Enhancements
* [#3445](https://github.com/netbox-community/netbox/issues/3445) - Add support for additional user defined headers to be added to webhook requests
* [#3499](https://github.com/netbox-community/netbox/issues/3499) - Add `ca_file_path` to Webhook model to support user supplied CA certificate verification of webhook requests
* [#3594](https://github.com/netbox-community/netbox/issues/3594) - Add ChoiceVar for custom scripts
* [#3619](https://github.com/netbox-community/netbox/issues/3619) - Add 400GE OSFP interface type
* [#3659](https://github.com/netbox-community/netbox/issues/3659) - Add filtering for objects in admin UI
## Bug Fixes
* [#3309](https://github.com/netbox-community/netbox/issues/3309) - Rewrite change logging middleware to resolve sporadic testing failures
* [#3340](https://github.com/netbox-community/netbox/issues/3340) - Add missing options to connect front ports to console ports
* [#3357](https://github.com/netbox-community/netbox/issues/3357) - Enable filter sites/devices/VMs by null region
* [#3460](https://github.com/netbox-community/netbox/issues/3460) - Extend upgrade script to validate Python dependencies
* [#3596](https://github.com/netbox-community/netbox/issues/3596) - Prevent server error when reassigning a device to a new device bay
* [#3629](https://github.com/netbox-community/netbox/issues/3629) - Use `get_lldp_neighors_detail` to validation LLDP neighbors
* [#3635](https://github.com/netbox-community/netbox/issues/3635) - Add missing cache support for the circuits app
* [#3636](https://github.com/netbox-community/netbox/issues/3636) - Add missing `rack_group` field to PowerFeed CSV export
* [#3652](https://github.com/netbox-community/netbox/issues/3652) - Limit next/previous rack by assigned rack group
---
# v2.6.6 (2019-10-10)
## Notes

View File

@@ -0,0 +1,258 @@
# v2.7.0 (FUTURE)
**Note:** NetBox v2.7 is the last major release that will support Python 3.5. Beginning with NetBox v2.8, Python 3.6 or
higher will be required.
## New Features
### Enhanced Device Type Import ([#451](https://github.com/netbox-community/netbox/issues/451))
NetBox now supports the import of device types and related component templates using a definition written in YAML or
JSON. For example, the following will create a new device type with four network interfaces, two power ports, and a
console port:
```yaml
manufacturer: Acme
model: Packet Shooter 9000
slug: packet-shooter-9000
u_height: 1
interfaces:
- name: ge-0/0/0
type: 1000base-t
- name: ge-0/0/1
type: 1000base-t
- name: ge-0/0/2
type: 1000base-t
- name: ge-0/0/3
type: 1000base-t
power-ports:
- name: PSU0
- name: PSU1
console-ports:
- name: Console
```
This new functionality replaces the existing CSV-based import form, which did not allow for component template import.
### Bulk Import of Device Components ([#822](https://github.com/netbox-community/netbox/issues/822))
NetBox now supports the bulk import of device components such as console ports, power ports, and interfaces across
multiple devices. Device components can be imported in CSV-format.
Here's an example bulk import of interfaces to several devices:
```
device,name,type
Switch1,Vlan100,Virtual
Switch1,Vlan200,Virtual
Switch2,Vlan100,Virtual
Switch2,Vlan200,Virtual
```
### External File Storage ([#1814](https://github.com/netbox-community/netbox/issues/1814))
In prior releases, the only option for storing uploaded files (e.g. image attachments) was to save them to the local
filesystem on the NetBox server. This release introduces support for several remote storage backends provided by the
[`django-storages`](https://django-storages.readthedocs.io/en/stable/) library. These include:
* Amazon S3
* ApacheLibcloud
* Azure Storage
* DigitalOcean Spaces
* Dropbox
* FTP
* Google Cloud Storage
* SFTP
To enable remote file storage, first install `django-storages`:
```
pip install django-storages
```
Then, set the appropriate storage backend and its configuration in `configuration.py`. Here's an example using Amazon
S3:
```python
STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage'
STORAGE_CONFIG = {
'AWS_ACCESS_KEY_ID': '<Key>',
'AWS_SECRET_ACCESS_KEY': '<Secret>',
'AWS_STORAGE_BUCKET_NAME': 'netbox',
'AWS_S3_REGION_NAME': 'eu-west-1',
}
```
Thanks to [@steffann](https://github.com/steffann) for contributing this work!
## Changes
### Rack Elevations Rendered via SVG ([#2248](https://github.com/netbox-community/netbox/issues/2248))
NetBox v2.7 introduces a new method of rendering rack elevations as an
[SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics) via a REST API endpoint. This replaces the prior method of
rendering elevations using pure HTML which was cumbersome and had several shortcomings. Allowing elevations to be
rendered as an SVG image in the API allows users to retrieve and make use of the drawings in their own tooling. This
also opens the door to other feature requests related to rack elevations in the NetBox backlog.
This feature implements a new REST API endpoint:
```
/api/dcim/racks/<id>/elevation/
```
By default, this endpoint returns a paginated JSON response representing each rack unit in the given elevation. This is
the same response returned by the rack units detail endpoint and for this reason the rack units endpoint has been
deprecated and will be removed in v2.8 (see [#3753](https://github.com/netbox-community/netbox/issues/3753)):
```
/api/dcim/racks/<id>/units/
```
In order to render the elevation as an SVG, include the `render=svg` query parameter in the request. You may also
control the width of the elevation drawing in pixels with `unit_width=<width in pixels>` and the height of each rack
unit with `unit_height=<height in pixels>`. The `unit_width` defaults to `230` and the `unit_height` default to `20`
which produces elevations the same size as those that appear in the NetBox Web UI. The query parameter `face` is used to
request either the `front` or `rear` of the elevation and defaults to `front`.
Here is an example of the request url for an SVG rendering using the default parameters to render the front of the
elevation:
```
/api/dcim/racks/<id>/elevation/?render=svg
```
Here is an example of the request url for an SVG rendering of the rear of the elevation having a width of 300 pixels and
per unit height of 35 pixels:
```
/api/dcim/racks/<id>/elevation/?render=svg&face=rear&unit_width=300&unit_height=35
```
Thanks to [@hellerve](https://github.com/hellerve) for doing the heavy lifting on this!
### Topology Maps Removed ([#2745](https://github.com/netbox-community/netbox/issues/2745))
The topology maps feature has been removed to help focus NetBox development efforts.
### Redis Configuration ([#3282](https://github.com/netbox-community/netbox/issues/3282))
v2.6.0 introduced caching and added the `CACHE_DATABASE` option to the existing `REDIS` database configuration section.
This did not however, allow for using two different Redis connections for the seperate caching and webhooks features.
This change separates the Redis connection configurations in the `REDIS` section into distinct `webhooks` and `caching`
subsections. This requires modification of the `REDIS` section of the `configuration.py` file as follows:
Old Redis configuration:
```python
REDIS = {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 0,
'CACHE_DATABASE': 1,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
}
```
New Redis configuration:
```python
REDIS = {
'webhooks': {
'HOST': 'redis.example.com',
'PORT': 1234,
'PASSWORD': 'foobar',
'DATABASE': 0,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
},
'caching': {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 1,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
}
}
```
Note that `CACHE_DATABASE` has been removed and the connection settings have been duplicated for both `webhooks` and
`caching`. This allows the user to make use of separate Redis instances and/or databases if desired. Full connection
details are required in both sections, even if they are the same.
### WEBHOOKS_ENABLED Configuration Setting Removed ([#3408](https://github.com/netbox-community/netbox/issues/3408))
As `django-rq` is now a required library, NetBox assumes that the RQ worker process is running. The installation and
upgrade documentation has been updated to reflect this, and the `WEBHOOKS_ENABLED` configuration parameter is no longer
used. Please ensure that both the NetBox WSGI service and the RQ worker process are running on all production
installations.
### API Choice Fields Now Use String Values ([#3569](https://github.com/netbox-community/netbox/issues/3569))
NetBox's REST API presents fields which reference a particular choice as a dictionary with two keys: `value` and
`label`. In previous versions, `value` was an integer which represented the particular choice in the database. This has
been changed to a more human-friendly "slug" string, which is essentially a simplified version of the choice's `label`.
For example, The site status field was previously represented as:
```json
"status": {
"value": 1,
"label": "Active"
},
```
Beginning with v2.7.0, it now looks like this:
```json
"status": {
"value": "active",
"label": "Active"
},
```
This change allows for much more intuitive representation of values, and obviates the need for API consumers to maintain
a mapping of static integer values.
Note that that all v2.7 releases will continue to accept the legacy integer values in write requests (POST, PUT, and
PATCH) to maintain backward compatibility. This behavior will be discontinued beginning in v2.8.0.
## Enhancements
* [#33](https://github.com/digitalocean/netbox/issues/33) - Add ability to clone objects (pre-populate form fields)
* [#648](https://github.com/digitalocean/netbox/issues/648) - Pre-populate forms when selecting "create and add another"
* [#792](https://github.com/digitalocean/netbox/issues/792) - Add power port and power outlet types
* [#1865](https://github.com/digitalocean/netbox/issues/1865) - Add console port and console server port types
* [#2669](https://github.com/digitalocean/netbox/issues/2669) - Relax uniqueness constraint on device and VM names
* [#2902](https://github.com/digitalocean/netbox/issues/2902) - Replace `supervisord` with `systemd`
* [#3455](https://github.com/digitalocean/netbox/issues/3455) - Add tenant assignment to cluster
* [#3564](https://github.com/digitalocean/netbox/issues/3564) - Add list views for device components
* [#3538](https://github.com/digitalocean/netbox/issues/3538) - Introduce a REST API endpoint for executing custom
scripts
* [#3655](https://github.com/digitalocean/netbox/issues/3655) - Add `description` field to organizational models
* [#3664](https://github.com/digitalocean/netbox/issues/3664) - Enable applying configuration contexts by tags
* [#3706](https://github.com/digitalocean/netbox/issues/3706) - Increase `available_power` maximum value on PowerFeed
* [#3731](https://github.com/digitalocean/netbox/issues/3731) - Change Graph.type to a ContentType foreign key field
## API Changes
* Choice fields now use human-friendly strings for their values instead of integers (see
[#3569](https://github.com/netbox-community/netbox/issues/3569)).
* Introduced `/api/extras/scripts/` endpoint for retrieving and executing custom scripts
* circuits.CircuitType: Added field `description`
* dcim.ConsolePort: Added field `type`
* dcim.ConsolePortTemplate: Added field `type`
* dcim.ConsoleServerPort: Added field `type`
* dcim.ConsoleServerPortTemplate: Added field `type`
* dcim.DeviceRole: Added field `description`
* dcim.PowerPort: Added field `type`
* dcim.PowerPortTemplate: Added field `type`
* dcim.PowerOutlet: Added field `type`
* dcim.PowerOutletTemplate: Added field `type`
* dcim.RackRole: Added field `description`
* extras.Graph: The `type` field has been changed to a content type foreign key. Models are specified as
`<app>.<model>`; e.g. `dcim.site`.
* ipam.Role: Added field `description`
* secrets.SecretRole: Added field `description`
* virtualization.Cluster: Added field `tenant`

View File

@@ -31,6 +31,7 @@ pages:
- Change Logging: 'additional-features/change-logging.md'
- Context Data: 'additional-features/context-data.md'
- Custom Fields: 'additional-features/custom-fields.md'
- Custom Links: 'additional-features/custom-links.md'
- Custom Scripts: 'additional-features/custom-scripts.md'
- Export Templates: 'additional-features/export-templates.md'
- Graphs: 'additional-features/graphs.md'

View File

@@ -1,7 +1,7 @@
from rest_framework import serializers
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
from circuits.constants import CIRCUIT_STATUS_CHOICES
from circuits.choices import CircuitStatusChoices
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
from dcim.api.serializers import ConnectedEndpointSerializer
@@ -36,12 +36,12 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
class Meta:
model = CircuitType
fields = ['id', 'name', 'slug', 'circuit_count']
fields = ['id', 'name', 'slug', 'description', 'circuit_count']
class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
provider = NestedProviderSerializer()
status = ChoiceField(choices=CIRCUIT_STATUS_CHOICES, required=False)
status = ChoiceField(choices=CircuitStatusChoices, required=False)
type = NestedCircuitTypeSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
tags = TagListSerializerField(required=False)

View File

@@ -7,7 +7,7 @@ from circuits import filters
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph, GRAPH_TYPE_PROVIDER
from extras.models import Graph
from utilities.api import FieldChoicesViewSet, ModelViewSet
from . import serializers
@@ -40,7 +40,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
A convenience method for rendering graphs for a particular provider.
"""
provider = get_object_or_404(Provider, pk=pk)
queryset = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER)
queryset = Graph.objects.filter(type__model='provider')
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider})
return Response(serializer.data)

View File

@@ -0,0 +1,48 @@
from utilities.choices import ChoiceSet
#
# Circuits
#
class CircuitStatusChoices(ChoiceSet):
STATUS_DEPROVISIONING = 'deprovisioning'
STATUS_ACTIVE = 'active'
STATUS_PLANNED = 'planned'
STATUS_PROVISIONING = 'provisioning'
STATUS_OFFLINE = 'offline'
STATUS_DECOMMISSIONED = 'decommissioned'
CHOICES = (
(STATUS_PLANNED, 'Planned'),
(STATUS_PROVISIONING, 'Provisioning'),
(STATUS_ACTIVE, 'Active'),
(STATUS_OFFLINE, 'Offline'),
(STATUS_DEPROVISIONING, 'Deprovisioning'),
(STATUS_DECOMMISSIONED, 'Decommissioned'),
)
LEGACY_MAP = {
STATUS_DEPROVISIONING: 0,
STATUS_ACTIVE: 1,
STATUS_PLANNED: 2,
STATUS_PROVISIONING: 3,
STATUS_OFFLINE: 4,
STATUS_DECOMMISSIONED: 5,
}
#
# CircuitTerminations
#
class CircuitTerminationSideChoices(ChoiceSet):
SIDE_A = 'A'
SIDE_Z = 'Z'
CHOICES = (
(SIDE_A, 'A'),
(SIDE_Z, 'Z')
)

View File

@@ -1,23 +0,0 @@
# Circuit statuses
CIRCUIT_STATUS_DEPROVISIONING = 0
CIRCUIT_STATUS_ACTIVE = 1
CIRCUIT_STATUS_PLANNED = 2
CIRCUIT_STATUS_PROVISIONING = 3
CIRCUIT_STATUS_OFFLINE = 4
CIRCUIT_STATUS_DECOMMISSIONED = 5
CIRCUIT_STATUS_CHOICES = [
[CIRCUIT_STATUS_PLANNED, 'Planned'],
[CIRCUIT_STATUS_PROVISIONING, 'Provisioning'],
[CIRCUIT_STATUS_ACTIVE, 'Active'],
[CIRCUIT_STATUS_OFFLINE, 'Offline'],
[CIRCUIT_STATUS_DEPROVISIONING, 'Deprovisioning'],
[CIRCUIT_STATUS_DECOMMISSIONED, 'Decommissioned'],
]
# CircuitTermination sides
TERM_SIDE_A = 'A'
TERM_SIDE_Z = 'Z'
TERM_SIDE_CHOICES = (
(TERM_SIDE_A, 'A'),
(TERM_SIDE_Z, 'Z'),
)

View File

@@ -2,14 +2,14 @@ import django_filters
from django.db.models import Q
from dcim.models import Region, Site
from extras.filters import CustomFieldFilterSet
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
from .constants import *
from .choices import *
from .models import Circuit, CircuitTermination, CircuitType, Provider
class ProviderFilter(CustomFieldFilterSet):
class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@@ -54,7 +54,7 @@ class CircuitTypeFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug']
class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet):
class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@@ -84,7 +84,7 @@ class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet):
label='Circuit type (slug)',
)
status = django_filters.MultipleChoiceFilter(
choices=CIRCUIT_STATUS_CHOICES,
choices=CircuitStatusChoices,
null_value=None
)
site_id = django_filters.ModelMultipleChoiceFilter(

View File

@@ -1,26 +0,0 @@
[
{
"model": "circuits.circuittype",
"pk": 1,
"fields": {
"name": "Internet",
"slug": "internet"
}
},
{
"model": "circuits.circuittype",
"pk": 2,
"fields": {
"name": "Private WAN",
"slug": "private-wan"
}
},
{
"model": "circuits.circuittype",
"pk": 3,
"fields": {
"name": "Out-of-Band",
"slug": "out-of-band"
}
}
]

View File

@@ -9,7 +9,7 @@ from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField,
FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple
)
from .constants import *
from .choices import CircuitStatusChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -128,7 +128,7 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = CircuitType
fields = [
'name', 'slug',
'name', 'slug', 'description',
]
@@ -194,7 +194,7 @@ class CircuitCSVForm(forms.ModelForm):
}
)
status = CSVChoiceField(
choices=CIRCUIT_STATUS_CHOICES,
choices=CircuitStatusChoices,
required=False,
help_text='Operational status'
)
@@ -235,7 +235,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
)
)
status = forms.ChoiceField(
choices=add_blank_choice(CIRCUIT_STATUS_CHOICES),
choices=add_blank_choice(CircuitStatusChoices),
required=False,
initial='',
widget=StaticSelect2()
@@ -292,7 +292,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
)
)
status = forms.MultipleChoiceField(
choices=CIRCUIT_STATUS_CHOICES,
choices=CircuitStatusChoices,
required=False,
widget=StaticSelect2Multiple()
)

View File

@@ -0,0 +1,39 @@
from django.db import migrations, models
CIRCUIT_STATUS_CHOICES = (
(0, 'deprovisioning'),
(1, 'active'),
(2, 'planned'),
(3, 'provisioning'),
(4, 'offline'),
(5, 'decommissioned')
)
def circuit_status_to_slug(apps, schema_editor):
Circuit = apps.get_model('circuits', 'Circuit')
for id, slug in CIRCUIT_STATUS_CHOICES:
Circuit.objects.filter(status=str(id)).update(status=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('circuits', '0015_custom_tag_models'),
]
operations = [
# Circuit.status
migrations.AlterField(
model_name='circuit',
name='status',
field=models.CharField(default='active', max_length=50),
),
migrations.RunPython(
code=circuit_status_to_slug
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.6 on 2019-12-10 18:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0016_3569_circuit_fields'),
]
operations = [
migrations.AddField(
model_name='circuittype',
name='description',
field=models.CharField(blank=True, max_length=100),
),
]

View File

@@ -3,13 +3,13 @@ from django.db import models
from django.urls import reverse
from taggit.managers import TaggableManager
from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES
from dcim.constants import CONNECTION_STATUS_CHOICES
from dcim.fields import ASNField
from dcim.models import CableTermination
from extras.models import CustomFieldModel, ObjectChange, TaggedItem
from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object
from .constants import *
from .choices import *
class Provider(ChangeLoggedModel, CustomFieldModel):
@@ -57,7 +57,12 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
tags = TaggableManager(through=TaggedItem)
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
csv_headers = [
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
]
clone_fields = [
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
]
class Meta:
ordering = ['name']
@@ -93,8 +98,12 @@ class CircuitType(ChangeLoggedModel):
slug = models.SlugField(
unique=True
)
description = models.CharField(
max_length=100,
blank=True,
)
csv_headers = ['name', 'slug']
csv_headers = ['name', 'slug', 'description']
class Meta:
ordering = ['name']
@@ -109,6 +118,7 @@ class CircuitType(ChangeLoggedModel):
return (
self.name,
self.slug,
self.description,
)
@@ -132,9 +142,10 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
on_delete=models.PROTECT,
related_name='circuits'
)
status = models.PositiveSmallIntegerField(
choices=CIRCUIT_STATUS_CHOICES,
default=CIRCUIT_STATUS_ACTIVE
status = models.CharField(
max_length=50,
choices=CircuitStatusChoices,
default=CircuitStatusChoices.STATUS_ACTIVE
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
@@ -170,6 +181,18 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
csv_headers = [
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
]
clone_fields = [
'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
]
STATUS_CLASS_MAP = {
CircuitStatusChoices.STATUS_DEPROVISIONING: 'warning',
CircuitStatusChoices.STATUS_ACTIVE: 'success',
CircuitStatusChoices.STATUS_PLANNED: 'info',
CircuitStatusChoices.STATUS_PROVISIONING: 'primary',
CircuitStatusChoices.STATUS_OFFLINE: 'danger',
CircuitStatusChoices.STATUS_DECOMMISSIONED: 'default',
}
class Meta:
ordering = ['provider', 'cid']
@@ -195,7 +218,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
)
def get_status_class(self):
return STATUS_CLASSES[self.status]
return self.STATUS_CLASS_MAP.get(self.status)
def _get_termination(self, side):
for ct in self.terminations.all():
@@ -220,7 +243,7 @@ class CircuitTermination(CableTermination):
)
term_side = models.CharField(
max_length=1,
choices=TERM_SIDE_CHOICES,
choices=CircuitTerminationSideChoices,
verbose_name='Termination'
)
site = models.ForeignKey(

View File

@@ -50,12 +50,14 @@ class CircuitTypeTable(BaseTable):
name = tables.LinkColumn()
circuit_count = tables.Column(verbose_name='Circuits')
actions = tables.TemplateColumn(
template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
template_code=CIRCUITTYPE_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = CircuitType
fields = ('pk', 'name', 'circuit_count', 'slug', 'actions')
fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
#

View File

@@ -1,10 +1,10 @@
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from rest_framework import status
from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z
from circuits.choices import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
from extras.constants import GRAPH_TYPE_PROVIDER
from dcim.models import Site
from extras.models import Graph
from utilities.testing import APITestCase
@@ -28,16 +28,20 @@ class ProviderTest(APITestCase):
def test_get_provider_graphs(self):
provider_ct = ContentType.objects.get(app_label='circuits', model='provider')
self.graph1 = Graph.objects.create(
type=GRAPH_TYPE_PROVIDER, name='Test Graph 1',
type=provider_ct,
name='Test Graph 1',
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1'
)
self.graph2 = Graph.objects.create(
type=GRAPH_TYPE_PROVIDER, name='Test Graph 2',
type=provider_ct,
name='Test Graph 2',
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=2'
)
self.graph3 = Graph.objects.create(
type=GRAPH_TYPE_PROVIDER, name='Test Graph 3',
type=provider_ct,
name='Test Graph 3',
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=3'
)
@@ -250,7 +254,7 @@ class CircuitTest(APITestCase):
'cid': 'TEST0004',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE,
'status': CircuitStatusChoices.STATUS_ACTIVE,
}
url = reverse('circuits-api:circuit-list')
@@ -270,19 +274,19 @@ class CircuitTest(APITestCase):
'cid': 'TEST0004',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE,
'status': CircuitStatusChoices.STATUS_ACTIVE,
},
{
'cid': 'TEST0005',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE,
'status': CircuitStatusChoices.STATUS_ACTIVE,
},
{
'cid': 'TEST0006',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE,
'status': CircuitStatusChoices.STATUS_ACTIVE,
},
]
@@ -336,16 +340,28 @@ class CircuitTerminationTest(APITestCase):
self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype)
self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
self.circuittermination1 = CircuitTermination.objects.create(
circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
circuit=self.circuit1,
term_side=CircuitTerminationSideChoices.SIDE_A,
site=self.site1,
port_speed=1000000
)
self.circuittermination2 = CircuitTermination.objects.create(
circuit=self.circuit1, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000
circuit=self.circuit1,
term_side=CircuitTerminationSideChoices.SIDE_Z,
site=self.site2,
port_speed=1000000
)
self.circuittermination3 = CircuitTermination.objects.create(
circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
circuit=self.circuit2,
term_side=CircuitTerminationSideChoices.SIDE_A,
site=self.site1,
port_speed=1000000
)
self.circuittermination4 = CircuitTermination.objects.create(
circuit=self.circuit2, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000
circuit=self.circuit2,
term_side=CircuitTerminationSideChoices.SIDE_Z,
site=self.site2,
port_speed=1000000
)
def test_get_circuittermination(self):
@@ -366,7 +382,7 @@ class CircuitTerminationTest(APITestCase):
data = {
'circuit': self.circuit3.pk,
'term_side': TERM_SIDE_A,
'term_side': CircuitTerminationSideChoices.SIDE_A,
'site': self.site1.pk,
'port_speed': 1000000,
}
@@ -385,12 +401,15 @@ class CircuitTerminationTest(APITestCase):
def test_update_circuittermination(self):
circuittermination5 = CircuitTermination.objects.create(
circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
circuit=self.circuit3,
term_side=CircuitTerminationSideChoices.SIDE_A,
site=self.site1,
port_speed=1000000
)
data = {
'circuit': self.circuit3.pk,
'term_side': TERM_SIDE_Z,
'term_side': CircuitTerminationSideChoices.SIDE_Z,
'site': self.site2.pk,
'port_speed': 1000000,
}

View File

@@ -10,7 +10,12 @@ from utilities.testing import create_test_user
class ProviderTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['circuits.view_provider'])
user = create_test_user(
permissions=[
'circuits.view_provider',
'circuits.add_provider',
]
)
self.client = Client()
self.client.force_login(user)
@@ -36,11 +41,30 @@ class ProviderTestCase(TestCase):
response = self.client.get(provider.get_absolute_url())
self.assertEqual(response.status_code, 200)
def test_provider_import(self):
csv_data = (
"name,slug",
"Provider 4,provider-4",
"Provider 5,provider-5",
"Provider 6,provider-6",
)
response = self.client.post(reverse('circuits:provider_import'), {'csv': '\n'.join(csv_data)})
self.assertEqual(response.status_code, 200)
self.assertEqual(Provider.objects.count(), 6)
class CircuitTypeTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['circuits.view_circuittype'])
user = create_test_user(
permissions=[
'circuits.view_circuittype',
'circuits.add_circuittype',
]
)
self.client = Client()
self.client.force_login(user)
@@ -57,11 +81,30 @@ class CircuitTypeTestCase(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_circuittype_import(self):
csv_data = (
"name,slug",
"Circuit Type 4,circuit-type-4",
"Circuit Type 5,circuit-type-5",
"Circuit Type 6,circuit-type-6",
)
response = self.client.post(reverse('circuits:circuittype_import'), {'csv': '\n'.join(csv_data)})
self.assertEqual(response.status_code, 200)
self.assertEqual(CircuitType.objects.count(), 6)
class CircuitTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['circuits.view_circuit'])
user = create_test_user(
permissions=[
'circuits.view_circuit',
'circuits.add_circuit',
]
)
self.client = Client()
self.client.force_login(user)
@@ -93,3 +136,17 @@ class CircuitTestCase(TestCase):
circuit = Circuit.objects.first()
response = self.client.get(circuit.get_absolute_url())
self.assertEqual(response.status_code, 200)
def test_circuit_import(self):
csv_data = (
"cid,provider,type",
"Circuit 4,Provider 1,Circuit Type 1",
"Circuit 5,Provider 1,Circuit Type 1",
"Circuit 6,Provider 1,Circuit Type 1",
)
response = self.client.post(reverse('circuits:circuit_import'), {'csv': '\n'.join(csv_data)})
self.assertEqual(response.status_code, 200)
self.assertEqual(Circuit.objects.count(), 6)

View File

@@ -6,13 +6,13 @@ from django.db.models import Count, OuterRef, Subquery
from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import View
from extras.models import Graph, GRAPH_TYPE_PROVIDER
from extras.models import Graph
from utilities.forms import ConfirmationForm
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from . import filters, forms, tables
from .constants import TERM_SIDE_A, TERM_SIDE_Z
from .choices import CircuitTerminationSideChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -36,7 +36,7 @@ class ProviderView(PermissionRequiredMixin, View):
provider = get_object_or_404(Provider, slug=slug)
circuits = Circuit.objects.filter(provider=provider).prefetch_related('type', 'tenant', 'terminations__site')
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
show_graphs = Graph.objects.filter(type__model='provider').exists()
return render(request, 'circuits/provider.html', {
'provider': provider,
@@ -151,12 +151,12 @@ class CircuitView(PermissionRequiredMixin, View):
termination_a = CircuitTermination.objects.prefetch_related(
'site__region', 'connected_endpoint__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_A
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
).first()
termination_z = CircuitTermination.objects.prefetch_related(
'site__region', 'connected_endpoint__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_Z
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
).first()
return render(request, 'circuits/circuit.html', {
@@ -212,8 +212,12 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
def circuit_terminations_swap(request, pk):
circuit = get_object_or_404(Circuit, pk=pk)
termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first()
termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first()
termination_a = CircuitTermination.objects.filter(
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
).first()
termination_z = CircuitTermination.objects.filter(
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
).first()
if not termination_a and not termination_z:
messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
return redirect('circuits:circuit', pk=circuit.pk)

View File

@@ -4,6 +4,7 @@ from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
from dcim.choices import *
from dcim.constants import *
from dcim.models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@@ -67,7 +68,7 @@ class RegionSerializer(serializers.ModelSerializer):
class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
status = ChoiceField(choices=SITE_STATUS_CHOICES, required=False)
status = ChoiceField(choices=SiteStatusChoices, required=False)
region = NestedRegionSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
time_zone = TimeZoneField(required=False)
@@ -107,18 +108,18 @@ class RackRoleSerializer(ValidatedModelSerializer):
class Meta:
model = RackRole
fields = ['id', 'name', 'slug', 'color', 'rack_count']
fields = ['id', 'name', 'slug', 'color', 'description', 'rack_count']
class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
site = NestedSiteSerializer()
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=RACK_STATUS_CHOICES, required=False)
status = ChoiceField(choices=RackStatusChoices, required=False)
role = NestedRackRoleSerializer(required=False, allow_null=True)
type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True)
width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
outer_unit = ChoiceField(choices=RACK_DIMENSION_UNIT_CHOICES, required=False)
type = ChoiceField(choices=RackTypeChoices, required=False, allow_null=True)
width = ChoiceField(choices=RackWidthChoices, required=False)
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, required=False)
tags = TagListSerializerField(required=False)
device_count = serializers.IntegerField(read_only=True)
powerfeed_count = serializers.IntegerField(read_only=True)
@@ -156,7 +157,7 @@ class RackUnitSerializer(serializers.Serializer):
"""
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(read_only=True)
face = serializers.IntegerField(read_only=True)
face = ChoiceField(choices=DeviceFaceChoices, read_only=True)
device = NestedDeviceSerializer(read_only=True)
@@ -170,6 +171,31 @@ class RackReservationSerializer(ValidatedModelSerializer):
fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
class RackElevationDetailFilterSerializer(serializers.Serializer):
face = serializers.ChoiceField(
choices=DeviceFaceChoices,
default=DeviceFaceChoices.FACE_FRONT
)
render = serializers.ChoiceField(
choices=RackElevationDetailRenderChoices,
default=RackElevationDetailRenderChoices.RENDER_JSON
)
unit_width = serializers.IntegerField(
default=RACK_ELEVATION_UNIT_WIDTH_DEFAULT
)
unit_height = serializers.IntegerField(
default=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT
)
exclude = serializers.IntegerField(
required=False,
default=None
)
expand_devices = serializers.BooleanField(
required=False,
default=True
)
#
# Device types
#
@@ -186,7 +212,7 @@ class ManufacturerSerializer(ValidatedModelSerializer):
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
manufacturer = NestedManufacturerSerializer()
subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True)
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, required=False, allow_null=True)
tags = TagListSerializerField(required=False)
device_count = serializers.IntegerField(read_only=True)
@@ -200,58 +226,72 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
required=False
)
class Meta:
model = ConsolePortTemplate
fields = ['id', 'device_type', 'name']
fields = ['id', 'device_type', 'name', 'type']
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
required=False
)
class Meta:
model = ConsoleServerPortTemplate
fields = ['id', 'device_type', 'name']
fields = ['id', 'device_type', 'name', 'type']
class PowerPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
choices=PowerPortTypeChoices,
required=False
)
class Meta:
model = PowerPortTemplate
fields = ['id', 'device_type', 'name', 'maximum_draw', 'allocated_draw']
fields = ['id', 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw']
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
choices=PowerOutletTypeChoices,
required=False
)
power_port = PowerPortTemplateSerializer(
required=False
)
feed_leg = ChoiceField(
choices=POWERFEED_LEG_CHOICES,
choices=PowerOutletFeedLegChoices,
required=False,
allow_null=True
)
class Meta:
model = PowerOutletTemplate
fields = ['id', 'device_type', 'name', 'power_port', 'feed_leg']
fields = ['id', 'device_type', 'name', 'type', 'power_port', 'feed_leg']
class InterfaceTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
# TODO: Remove in v2.7 (backward-compatibility for form_factor)
form_factor = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
type = ChoiceField(choices=InterfaceTypeChoices, required=False)
class Meta:
model = InterfaceTemplate
fields = ['id', 'device_type', 'name', 'type', 'form_factor', 'mgmt_only']
fields = ['id', 'device_type', 'name', 'type', 'mgmt_only']
class RearPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=PORT_TYPE_CHOICES)
type = ChoiceField(choices=PortTypeChoices)
class Meta:
model = RearPortTemplate
@@ -260,7 +300,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
class FrontPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=PORT_TYPE_CHOICES)
type = ChoiceField(choices=PortTypeChoices)
rear_port = NestedRearPortTemplateSerializer()
class Meta:
@@ -286,7 +326,9 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
class Meta:
model = DeviceRole
fields = ['id', 'name', 'slug', 'color', 'vm_role', 'device_count', 'virtualmachine_count']
fields = [
'id', 'name', 'slug', 'color', 'vm_role', 'description', 'device_count', 'virtualmachine_count',
]
class PlatformSerializer(ValidatedModelSerializer):
@@ -309,8 +351,8 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
platform = NestedPlatformSerializer(required=False, allow_null=True)
site = NestedSiteSerializer()
rack = NestedRackSerializer(required=False, allow_null=True)
face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True)
status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False)
face = ChoiceField(choices=DeviceFaceChoices, required=False, allow_null=True)
status = ChoiceField(choices=DeviceStatusChoices, required=False)
primary_ip = NestedIPAddressSerializer(read_only=True)
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
@@ -372,37 +414,49 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
required=False
)
cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False)
class Meta:
model = ConsoleServerPort
fields = [
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
'cable', 'tags',
'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
'connection_status', 'cable', 'tags',
]
class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
required=False
)
cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False)
class Meta:
model = ConsolePort
fields = [
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
'cable', 'tags',
'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
'connection_status', 'cable', 'tags',
]
class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
type = ChoiceField(
choices=PowerOutletTypeChoices,
required=False
)
power_port = NestedPowerPortSerializer(
required=False
)
feed_leg = ChoiceField(
choices=POWERFEED_LEG_CHOICES,
choices=PowerOutletFeedLegChoices,
required=False,
allow_null=True
)
@@ -416,31 +470,33 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta:
model = PowerOutlet
fields = [
'id', 'device', 'name', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type',
'id', 'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type',
'connected_endpoint', 'connection_status', 'cable', 'tags',
]
class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
type = ChoiceField(
choices=PowerPortTypeChoices,
required=False
)
cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False)
class Meta:
model = PowerPort
fields = [
'id', 'device', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type',
'id', 'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type',
'connected_endpoint', 'connection_status', 'cable', 'tags',
]
class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
# TODO: Remove in v2.7 (backward-compatibility for form_factor)
form_factor = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
type = ChoiceField(choices=InterfaceTypeChoices, required=False)
lag = NestedInterfaceSerializer(required=False, allow_null=True)
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_null=True)
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
tagged_vlans = SerializedPKRelatedField(
queryset=VLAN.objects.all(),
@@ -454,9 +510,9 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta:
model = Interface
fields = [
'id', 'device', 'name', 'type', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode',
'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses',
'id', 'device', 'name', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan',
'tagged_vlans', 'tags', 'count_ipaddresses',
]
# TODO: This validation should be handled by Interface.clean()
@@ -482,7 +538,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer):
device = NestedDeviceSerializer()
type = ChoiceField(choices=PORT_TYPE_CHOICES)
type = ChoiceField(choices=PortTypeChoices)
cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False)
@@ -504,7 +560,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer):
device = NestedDeviceSerializer()
type = ChoiceField(choices=PORT_TYPE_CHOICES)
type = ChoiceField(choices=PortTypeChoices)
rear_port = FrontPortRearPortSerializer()
cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False)
@@ -556,8 +612,8 @@ class CableSerializer(ValidatedModelSerializer):
)
termination_a = serializers.SerializerMethodField(read_only=True)
termination_b = serializers.SerializerMethodField(read_only=True)
status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False, allow_null=True)
status = ChoiceField(choices=CableStatusChoices, required=False)
length_unit = ChoiceField(choices=CableLengthUnitChoices, required=False, allow_null=True)
class Meta:
model = Cable
@@ -662,20 +718,20 @@ class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer):
default=None
)
type = ChoiceField(
choices=POWERFEED_TYPE_CHOICES,
default=POWERFEED_TYPE_PRIMARY
choices=PowerFeedTypeChoices,
default=PowerFeedTypeChoices.TYPE_PRIMARY
)
status = ChoiceField(
choices=POWERFEED_STATUS_CHOICES,
default=POWERFEED_STATUS_ACTIVE
choices=PowerFeedStatusChoices,
default=PowerFeedStatusChoices.STATUS_ACTIVE
)
supply = ChoiceField(
choices=POWERFEED_SUPPLY_CHOICES,
default=POWERFEED_SUPPLY_AC
choices=PowerFeedSupplyChoices,
default=PowerFeedSupplyChoices.SUPPLY_AC
)
phase = ChoiceField(
choices=POWERFEED_PHASE_CHOICES,
default=POWERFEED_PHASE_SINGLE
choices=PowerFeedPhaseChoices,
default=PowerFeedPhaseChoices.PHASE_SINGLE
)
tags = TagListSerializerField(
required=False

View File

@@ -2,8 +2,8 @@ from collections import OrderedDict
from django.conf import settings
from django.db.models import Count, F
from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.http import HttpResponseForbidden, HttpResponseBadRequest, HttpResponse
from django.shortcuts import get_object_or_404, reverse
from drf_yasg import openapi
from drf_yasg.openapi import Parameter
from drf_yasg.utils import swagger_auto_schema
@@ -13,7 +13,7 @@ from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ViewSet
from circuits.models import Circuit
from dcim import filters
from dcim import constants, filters
from dcim.models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
@@ -23,12 +23,12 @@ from dcim.models import (
)
from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet
from extras.constants import GRAPH_TYPE_DEVICE, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from extras.models import Graph
from ipam.models import Prefix, VLAN
from utilities.api import (
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable,
)
from utilities.custom_inspectors import NullablePaginatorInspector
from utilities.utils import get_subquery
from virtualization.models import VirtualMachine
from . import serializers
@@ -42,16 +42,20 @@ from .exceptions import MissingFilterException
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(Cable, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']),
(ConsolePort, ['connection_status']),
(ConsolePort, ['type', 'connection_status']),
(ConsolePortTemplate, ['type']),
(ConsoleServerPort, ['type']),
(ConsoleServerPortTemplate, ['type']),
(Device, ['face', 'status']),
(DeviceType, ['subdevice_role']),
(FrontPort, ['type']),
(FrontPortTemplate, ['type']),
(Interface, ['type', 'mode']),
(InterfaceTemplate, ['type']),
(PowerOutlet, ['feed_leg']),
(PowerOutletTemplate, ['feed_leg']),
(PowerPort, ['connection_status']),
(PowerOutlet, ['type', 'feed_leg']),
(PowerOutletTemplate, ['type', 'feed_leg']),
(PowerPort, ['type', 'connection_status']),
(PowerPortTemplate, ['type']),
(Rack, ['outer_unit', 'status', 'type', 'width']),
(RearPort, ['type']),
(RearPortTemplate, ['type']),
@@ -129,7 +133,7 @@ class SiteViewSet(CustomFieldModelViewSet):
A convenience method for rendering graphs for a particular site.
"""
site = get_object_or_404(Site, pk=pk)
queryset = Graph.objects.filter(type=GRAPH_TYPE_SITE)
queryset = Graph.objects.filter(type__model='site')
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site})
return Response(serializer.data)
@@ -172,13 +176,15 @@ class RackViewSet(CustomFieldModelViewSet):
serializer_class = serializers.RackSerializer
filterset_class = filters.RackFilter
@swagger_auto_schema(deprecated=True)
@action(detail=True)
def units(self, request, pk=None):
"""
List rack units (by rack)
"""
# TODO: Remove this action detail route in v2.8
rack = get_object_or_404(Rack, pk=pk)
face = request.GET.get('face', 0)
face = request.GET.get('face', 'front')
exclude_pk = request.GET.get('exclude', None)
if exclude_pk is not None:
try:
@@ -197,6 +203,39 @@ class RackViewSet(CustomFieldModelViewSet):
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
return self.get_paginated_response(rack_units.data)
@swagger_auto_schema(
responses={200: serializers.RackUnitSerializer(many=True)},
query_serializer=serializers.RackElevationDetailFilterSerializer
)
@action(detail=True)
def elevation(self, request, pk=None):
"""
Rack elevation representing the list of rack units. Also supports rendering the elevation as an SVG.
"""
rack = get_object_or_404(Rack, pk=pk)
serializer = serializers.RackElevationDetailFilterSerializer(data=request.GET)
if not serializer.is_valid():
return Response(serializer.errors, 400)
data = serializer.validated_data
if data['render'] == 'svg':
# Render and return the elevation as an SVG drawing with the correct content type
drawing = rack.get_elevation_svg(data['face'], data['unit_width'], data['unit_height'])
return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
else:
# Return a JSON representation of the rack units in the elevation
elevation = rack.get_rack_units(
face=data['face'],
exclude=data['exclude'],
expand_devices=data['expand_devices']
)
page = self.paginate_queryset(elevation)
if page is not None:
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
return self.get_paginated_response(rack_units.data)
#
# Rack reservations
@@ -353,7 +392,7 @@ class DeviceViewSet(CustomFieldModelViewSet):
A convenience method for rendering graphs for a particular Device.
"""
device = get_object_or_404(Device, pk=pk)
queryset = Graph.objects.filter(type=GRAPH_TYPE_DEVICE)
queryset = Graph.objects.filter(type__model='device')
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device})
return Response(serializer.data)
@@ -475,7 +514,7 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
A convenience method for rendering graphs for a particular interface.
"""
interface = get_object_or_404(Interface, pk=pk)
queryset = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE)
queryset = Graph.objects.filter(type__model='interface')
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface})
return Response(serializer.data)

1044
netbox/dcim/choices.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,377 +1,25 @@
from .choices import InterfaceTypeChoices
# Rack types
RACK_TYPE_2POST = 100
RACK_TYPE_4POST = 200
RACK_TYPE_CABINET = 300
RACK_TYPE_WALLFRAME = 1000
RACK_TYPE_WALLCABINET = 1100
RACK_TYPE_CHOICES = (
(RACK_TYPE_2POST, '2-post frame'),
(RACK_TYPE_4POST, '4-post frame'),
(RACK_TYPE_CABINET, '4-post cabinet'),
(RACK_TYPE_WALLFRAME, 'Wall-mounted frame'),
(RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'),
)
# Rack widths
RACK_WIDTH_19IN = 19
RACK_WIDTH_23IN = 23
RACK_WIDTH_CHOICES = (
(RACK_WIDTH_19IN, '19 inches'),
(RACK_WIDTH_23IN, '23 inches'),
)
# Rack faces
RACK_FACE_FRONT = 0
RACK_FACE_REAR = 1
RACK_FACE_CHOICES = [
[RACK_FACE_FRONT, 'Front'],
[RACK_FACE_REAR, 'Rear'],
]
# Rack statuses
RACK_STATUS_RESERVED = 0
RACK_STATUS_AVAILABLE = 1
RACK_STATUS_PLANNED = 2
RACK_STATUS_ACTIVE = 3
RACK_STATUS_DEPRECATED = 4
RACK_STATUS_CHOICES = [
[RACK_STATUS_ACTIVE, 'Active'],
[RACK_STATUS_PLANNED, 'Planned'],
[RACK_STATUS_RESERVED, 'Reserved'],
[RACK_STATUS_AVAILABLE, 'Available'],
[RACK_STATUS_DEPRECATED, 'Deprecated'],
]
# Device rack position
DEVICE_POSITION_CHOICES = [
# Rack.u_height is limited to 100
(i, 'Unit {}'.format(i)) for i in range(1, 101)
]
# Parent/child device roles
SUBDEVICE_ROLE_PARENT = True
SUBDEVICE_ROLE_CHILD = False
SUBDEVICE_ROLE_CHOICES = (
(None, 'None'),
(SUBDEVICE_ROLE_PARENT, 'Parent'),
(SUBDEVICE_ROLE_CHILD, 'Child'),
)
# Interface types
# Virtual
IFACE_TYPE_VIRTUAL = 0
IFACE_TYPE_LAG = 200
# Ethernet
IFACE_TYPE_100ME_FIXED = 800
IFACE_TYPE_1GE_FIXED = 1000
IFACE_TYPE_1GE_GBIC = 1050
IFACE_TYPE_1GE_SFP = 1100
IFACE_TYPE_2GE_FIXED = 1120
IFACE_TYPE_5GE_FIXED = 1130
IFACE_TYPE_10GE_FIXED = 1150
IFACE_TYPE_10GE_CX4 = 1170
IFACE_TYPE_10GE_SFP_PLUS = 1200
IFACE_TYPE_10GE_XFP = 1300
IFACE_TYPE_10GE_XENPAK = 1310
IFACE_TYPE_10GE_X2 = 1320
IFACE_TYPE_25GE_SFP28 = 1350
IFACE_TYPE_40GE_QSFP_PLUS = 1400
IFACE_TYPE_50GE_QSFP28 = 1420
IFACE_TYPE_100GE_CFP = 1500
IFACE_TYPE_100GE_CFP2 = 1510
IFACE_TYPE_100GE_CFP4 = 1520
IFACE_TYPE_100GE_CPAK = 1550
IFACE_TYPE_100GE_QSFP28 = 1600
IFACE_TYPE_200GE_CFP2 = 1650
IFACE_TYPE_200GE_QSFP56 = 1700
IFACE_TYPE_400GE_QSFP_DD = 1750
# Wireless
IFACE_TYPE_80211A = 2600
IFACE_TYPE_80211G = 2610
IFACE_TYPE_80211N = 2620
IFACE_TYPE_80211AC = 2630
IFACE_TYPE_80211AD = 2640
# Cellular
IFACE_TYPE_GSM = 2810
IFACE_TYPE_CDMA = 2820
IFACE_TYPE_LTE = 2830
# SONET
IFACE_TYPE_SONET_OC3 = 6100
IFACE_TYPE_SONET_OC12 = 6200
IFACE_TYPE_SONET_OC48 = 6300
IFACE_TYPE_SONET_OC192 = 6400
IFACE_TYPE_SONET_OC768 = 6500
IFACE_TYPE_SONET_OC1920 = 6600
IFACE_TYPE_SONET_OC3840 = 6700
# Fibrechannel
IFACE_TYPE_1GFC_SFP = 3010
IFACE_TYPE_2GFC_SFP = 3020
IFACE_TYPE_4GFC_SFP = 3040
IFACE_TYPE_8GFC_SFP_PLUS = 3080
IFACE_TYPE_16GFC_SFP_PLUS = 3160
IFACE_TYPE_32GFC_SFP28 = 3320
IFACE_TYPE_128GFC_QSFP28 = 3400
# InfiniBand
IFACE_FF_INFINIBAND_SDR = 7010
IFACE_FF_INFINIBAND_DDR = 7020
IFACE_FF_INFINIBAND_QDR = 7030
IFACE_FF_INFINIBAND_FDR10 = 7040
IFACE_FF_INFINIBAND_FDR = 7050
IFACE_FF_INFINIBAND_EDR = 7060
IFACE_FF_INFINIBAND_HDR = 7070
IFACE_FF_INFINIBAND_NDR = 7080
IFACE_FF_INFINIBAND_XDR = 7090
# Serial
IFACE_TYPE_T1 = 4000
IFACE_TYPE_E1 = 4010
IFACE_TYPE_T3 = 4040
IFACE_TYPE_E3 = 4050
# Stacking
IFACE_TYPE_STACKWISE = 5000
IFACE_TYPE_STACKWISE_PLUS = 5050
IFACE_TYPE_FLEXSTACK = 5100
IFACE_TYPE_FLEXSTACK_PLUS = 5150
IFACE_TYPE_JUNIPER_VCP = 5200
IFACE_TYPE_SUMMITSTACK = 5300
IFACE_TYPE_SUMMITSTACK128 = 5310
IFACE_TYPE_SUMMITSTACK256 = 5320
IFACE_TYPE_SUMMITSTACK512 = 5330
# Other
IFACE_TYPE_OTHER = 32767
IFACE_TYPE_CHOICES = [
[
'Virtual interfaces',
[
[IFACE_TYPE_VIRTUAL, 'Virtual'],
[IFACE_TYPE_LAG, 'Link Aggregation Group (LAG)'],
],
],
[
'Ethernet (fixed)',
[
[IFACE_TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_TYPE_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'],
[IFACE_TYPE_5GE_FIXED, '5GBASE-T (5GE)'],
[IFACE_TYPE_10GE_FIXED, '10GBASE-T (10GE)'],
[IFACE_TYPE_10GE_CX4, '10GBASE-CX4 (10GE)'],
]
],
[
'Ethernet (modular)',
[
[IFACE_TYPE_1GE_GBIC, 'GBIC (1GE)'],
[IFACE_TYPE_1GE_SFP, 'SFP (1GE)'],
[IFACE_TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'],
[IFACE_TYPE_10GE_XFP, 'XFP (10GE)'],
[IFACE_TYPE_10GE_XENPAK, 'XENPAK (10GE)'],
[IFACE_TYPE_10GE_X2, 'X2 (10GE)'],
[IFACE_TYPE_25GE_SFP28, 'SFP28 (25GE)'],
[IFACE_TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_TYPE_50GE_QSFP28, 'QSFP28 (50GE)'],
[IFACE_TYPE_100GE_CFP, 'CFP (100GE)'],
[IFACE_TYPE_100GE_CFP2, 'CFP2 (100GE)'],
[IFACE_TYPE_200GE_CFP2, 'CFP2 (200GE)'],
[IFACE_TYPE_100GE_CFP4, 'CFP4 (100GE)'],
[IFACE_TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'],
[IFACE_TYPE_100GE_QSFP28, 'QSFP28 (100GE)'],
[IFACE_TYPE_200GE_QSFP56, 'QSFP56 (200GE)'],
[IFACE_TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'],
]
],
[
'Wireless',
[
[IFACE_TYPE_80211A, 'IEEE 802.11a'],
[IFACE_TYPE_80211G, 'IEEE 802.11b/g'],
[IFACE_TYPE_80211N, 'IEEE 802.11n'],
[IFACE_TYPE_80211AC, 'IEEE 802.11ac'],
[IFACE_TYPE_80211AD, 'IEEE 802.11ad'],
]
],
[
'Cellular',
[
[IFACE_TYPE_GSM, 'GSM'],
[IFACE_TYPE_CDMA, 'CDMA'],
[IFACE_TYPE_LTE, 'LTE'],
]
],
[
'SONET',
[
[IFACE_TYPE_SONET_OC3, 'OC-3/STM-1'],
[IFACE_TYPE_SONET_OC12, 'OC-12/STM-4'],
[IFACE_TYPE_SONET_OC48, 'OC-48/STM-16'],
[IFACE_TYPE_SONET_OC192, 'OC-192/STM-64'],
[IFACE_TYPE_SONET_OC768, 'OC-768/STM-256'],
[IFACE_TYPE_SONET_OC1920, 'OC-1920/STM-640'],
[IFACE_TYPE_SONET_OC3840, 'OC-3840/STM-1234'],
]
],
[
'FibreChannel',
[
[IFACE_TYPE_1GFC_SFP, 'SFP (1GFC)'],
[IFACE_TYPE_2GFC_SFP, 'SFP (2GFC)'],
[IFACE_TYPE_4GFC_SFP, 'SFP (4GFC)'],
[IFACE_TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
[IFACE_TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
[IFACE_TYPE_32GFC_SFP28, 'SFP28 (32GFC)'],
[IFACE_TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'],
]
],
[
'InfiniBand',
[
[IFACE_FF_INFINIBAND_SDR, 'SDR (2 Gbps)'],
[IFACE_FF_INFINIBAND_DDR, 'DDR (4 Gbps)'],
[IFACE_FF_INFINIBAND_QDR, 'QDR (8 Gbps)'],
[IFACE_FF_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'],
[IFACE_FF_INFINIBAND_FDR, 'FDR (13.5 Gbps)'],
[IFACE_FF_INFINIBAND_EDR, 'EDR (25 Gbps)'],
[IFACE_FF_INFINIBAND_HDR, 'HDR (50 Gbps)'],
[IFACE_FF_INFINIBAND_NDR, 'NDR (100 Gbps)'],
[IFACE_FF_INFINIBAND_XDR, 'XDR (250 Gbps)'],
]
],
[
'Serial',
[
[IFACE_TYPE_T1, 'T1 (1.544 Mbps)'],
[IFACE_TYPE_E1, 'E1 (2.048 Mbps)'],
[IFACE_TYPE_T3, 'T3 (45 Mbps)'],
[IFACE_TYPE_E3, 'E3 (34 Mbps)'],
]
],
[
'Stacking',
[
[IFACE_TYPE_STACKWISE, 'Cisco StackWise'],
[IFACE_TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'],
[IFACE_TYPE_FLEXSTACK, 'Cisco FlexStack'],
[IFACE_TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
[IFACE_TYPE_JUNIPER_VCP, 'Juniper VCP'],
[IFACE_TYPE_SUMMITSTACK, 'Extreme SummitStack'],
[IFACE_TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'],
[IFACE_TYPE_SUMMITSTACK256, 'Extreme SummitStack-256'],
[IFACE_TYPE_SUMMITSTACK512, 'Extreme SummitStack-512'],
]
],
[
'Other',
[
[IFACE_TYPE_OTHER, 'Other'],
]
],
]
#
# Interface type groups
#
VIRTUAL_IFACE_TYPES = [
IFACE_TYPE_VIRTUAL,
IFACE_TYPE_LAG,
InterfaceTypeChoices.TYPE_VIRTUAL,
InterfaceTypeChoices.TYPE_LAG,
]
WIRELESS_IFACE_TYPES = [
IFACE_TYPE_80211A,
IFACE_TYPE_80211G,
IFACE_TYPE_80211N,
IFACE_TYPE_80211AC,
IFACE_TYPE_80211AD,
InterfaceTypeChoices.TYPE_80211A,
InterfaceTypeChoices.TYPE_80211G,
InterfaceTypeChoices.TYPE_80211N,
InterfaceTypeChoices.TYPE_80211AC,
InterfaceTypeChoices.TYPE_80211AD,
]
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
IFACE_MODE_ACCESS = 100
IFACE_MODE_TAGGED = 200
IFACE_MODE_TAGGED_ALL = 300
IFACE_MODE_CHOICES = [
[IFACE_MODE_ACCESS, 'Access'],
[IFACE_MODE_TAGGED, 'Tagged'],
[IFACE_MODE_TAGGED_ALL, 'Tagged All'],
]
# Pass-through port types
PORT_TYPE_8P8C = 1000
PORT_TYPE_110_PUNCH = 1100
PORT_TYPE_BNC = 1200
PORT_TYPE_ST = 2000
PORT_TYPE_SC = 2100
PORT_TYPE_SC_APC = 2110
PORT_TYPE_FC = 2200
PORT_TYPE_LC = 2300
PORT_TYPE_LC_APC = 2310
PORT_TYPE_MTRJ = 2400
PORT_TYPE_MPO = 2500
PORT_TYPE_LSH = 2600
PORT_TYPE_LSH_APC = 2610
PORT_TYPE_CHOICES = [
[
'Copper',
[
[PORT_TYPE_8P8C, '8P8C'],
[PORT_TYPE_110_PUNCH, '110 Punch'],
[PORT_TYPE_BNC, 'BNC'],
],
],
[
'Fiber Optic',
[
[PORT_TYPE_FC, 'FC'],
[PORT_TYPE_LC, 'LC'],
[PORT_TYPE_LC_APC, 'LC/APC'],
[PORT_TYPE_LSH, 'LSH'],
[PORT_TYPE_LSH_APC, 'LSH/APC'],
[PORT_TYPE_MPO, 'MPO'],
[PORT_TYPE_MTRJ, 'MTRJ'],
[PORT_TYPE_SC, 'SC'],
[PORT_TYPE_SC_APC, 'SC/APC'],
[PORT_TYPE_ST, 'ST'],
]
]
]
# Device statuses
DEVICE_STATUS_OFFLINE = 0
DEVICE_STATUS_ACTIVE = 1
DEVICE_STATUS_PLANNED = 2
DEVICE_STATUS_STAGED = 3
DEVICE_STATUS_FAILED = 4
DEVICE_STATUS_INVENTORY = 5
DEVICE_STATUS_DECOMMISSIONING = 6
DEVICE_STATUS_CHOICES = [
[DEVICE_STATUS_ACTIVE, 'Active'],
[DEVICE_STATUS_OFFLINE, 'Offline'],
[DEVICE_STATUS_PLANNED, 'Planned'],
[DEVICE_STATUS_STAGED, 'Staged'],
[DEVICE_STATUS_FAILED, 'Failed'],
[DEVICE_STATUS_INVENTORY, 'Inventory'],
[DEVICE_STATUS_DECOMMISSIONING, 'Decommissioning'],
]
# Site statuses
SITE_STATUS_ACTIVE = 1
SITE_STATUS_PLANNED = 2
SITE_STATUS_RETIRED = 4
SITE_STATUS_CHOICES = [
[SITE_STATUS_ACTIVE, 'Active'],
[SITE_STATUS_PLANNED, 'Planned'],
[SITE_STATUS_RETIRED, 'Retired'],
]
# Bootstrap CSS classes for device/rack statuses
STATUS_CLASSES = {
0: 'warning',
1: 'success',
2: 'info',
3: 'primary',
4: 'danger',
5: 'default',
6: 'warning',
}
# Console/power/interface connection statuses
CONNECTION_STATUS_PLANNED = False
CONNECTION_STATUS_CONNECTED = True
@@ -382,59 +30,10 @@ CONNECTION_STATUS_CHOICES = [
# Cable endpoint types
CABLE_TERMINATION_TYPES = [
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination',
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport',
'circuittermination', 'powerfeed',
]
# Cable types
CABLE_TYPE_CAT3 = 1300
CABLE_TYPE_CAT5 = 1500
CABLE_TYPE_CAT5E = 1510
CABLE_TYPE_CAT6 = 1600
CABLE_TYPE_CAT6A = 1610
CABLE_TYPE_CAT7 = 1700
CABLE_TYPE_DAC_ACTIVE = 1800
CABLE_TYPE_DAC_PASSIVE = 1810
CABLE_TYPE_COAXIAL = 1900
CABLE_TYPE_MMF = 3000
CABLE_TYPE_MMF_OM1 = 3010
CABLE_TYPE_MMF_OM2 = 3020
CABLE_TYPE_MMF_OM3 = 3030
CABLE_TYPE_MMF_OM4 = 3040
CABLE_TYPE_SMF = 3500
CABLE_TYPE_SMF_OS1 = 3510
CABLE_TYPE_SMF_OS2 = 3520
CABLE_TYPE_AOC = 3800
CABLE_TYPE_POWER = 5000
CABLE_TYPE_CHOICES = (
(
'Copper', (
(CABLE_TYPE_CAT3, 'CAT3'),
(CABLE_TYPE_CAT5, 'CAT5'),
(CABLE_TYPE_CAT5E, 'CAT5e'),
(CABLE_TYPE_CAT6, 'CAT6'),
(CABLE_TYPE_CAT6A, 'CAT6a'),
(CABLE_TYPE_CAT7, 'CAT7'),
(CABLE_TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
(CABLE_TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
(CABLE_TYPE_COAXIAL, 'Coaxial'),
),
),
(
'Fiber', (
(CABLE_TYPE_MMF, 'Multimode Fiber'),
(CABLE_TYPE_MMF_OM1, 'Multimode Fiber (OM1)'),
(CABLE_TYPE_MMF_OM2, 'Multimode Fiber (OM2)'),
(CABLE_TYPE_MMF_OM3, 'Multimode Fiber (OM3)'),
(CABLE_TYPE_MMF_OM4, 'Multimode Fiber (OM4)'),
(CABLE_TYPE_SMF, 'Singlemode Fiber'),
(CABLE_TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'),
(CABLE_TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'),
(CABLE_TYPE_AOC, 'Active Optical Cabling (AOC)'),
),
),
(CABLE_TYPE_POWER, 'Power'),
)
CABLE_TERMINATION_TYPE_CHOICES = {
# (API endpoint, human-friendly name)
'consoleport': ('console-ports', 'Console port'),
@@ -457,56 +56,68 @@ COMPATIBLE_TERMINATION_TYPES = {
'circuittermination': ['interface', 'frontport', 'rearport'],
}
LENGTH_UNIT_METER = 1200
LENGTH_UNIT_CENTIMETER = 1100
LENGTH_UNIT_MILLIMETER = 1000
LENGTH_UNIT_FOOT = 2100
LENGTH_UNIT_INCH = 2000
CABLE_LENGTH_UNIT_CHOICES = (
(LENGTH_UNIT_METER, 'Meters'),
(LENGTH_UNIT_CENTIMETER, 'Centimeters'),
(LENGTH_UNIT_FOOT, 'Feet'),
(LENGTH_UNIT_INCH, 'Inches'),
)
RACK_DIMENSION_UNIT_CHOICES = (
(LENGTH_UNIT_MILLIMETER, 'Millimeters'),
(LENGTH_UNIT_INCH, 'Inches'),
)
# Power feeds
POWERFEED_TYPE_PRIMARY = 1
POWERFEED_TYPE_REDUNDANT = 2
POWERFEED_TYPE_CHOICES = (
(POWERFEED_TYPE_PRIMARY, 'Primary'),
(POWERFEED_TYPE_REDUNDANT, 'Redundant'),
)
POWERFEED_SUPPLY_AC = 1
POWERFEED_SUPPLY_DC = 2
POWERFEED_SUPPLY_CHOICES = (
(POWERFEED_SUPPLY_AC, 'AC'),
(POWERFEED_SUPPLY_DC, 'DC'),
)
POWERFEED_PHASE_SINGLE = 1
POWERFEED_PHASE_3PHASE = 3
POWERFEED_PHASE_CHOICES = (
(POWERFEED_PHASE_SINGLE, 'Single phase'),
(POWERFEED_PHASE_3PHASE, 'Three-phase'),
)
POWERFEED_STATUS_OFFLINE = 0
POWERFEED_STATUS_ACTIVE = 1
POWERFEED_STATUS_PLANNED = 2
POWERFEED_STATUS_FAILED = 4
POWERFEED_STATUS_CHOICES = (
(POWERFEED_STATUS_ACTIVE, 'Active'),
(POWERFEED_STATUS_OFFLINE, 'Offline'),
(POWERFEED_STATUS_PLANNED, 'Planned'),
(POWERFEED_STATUS_FAILED, 'Failed'),
)
POWERFEED_LEG_A = 1
POWERFEED_LEG_B = 2
POWERFEED_LEG_C = 3
POWERFEED_LEG_CHOICES = (
(POWERFEED_LEG_A, 'A'),
(POWERFEED_LEG_B, 'B'),
(POWERFEED_LEG_C, 'C'),
)
RACK_ELEVATION_STYLE = """
* {
font-family: sans-serif;
font-size: 13px;
}
rect {
box-sizing: border-box;
}
text {
text-anchor: middle;
dominant-baseline: middle;
}
.rack {
background-color: #f0f0f0;
fill: none;
stroke: black;
stroke-width: 3px;
}
.slot {
fill: #f7f7f7;
stroke: #a0a0a0;
}
.slot:hover {
fill: #fff;
}
.slot+.add-device {
fill: none;
}
.slot:hover+.add-device {
fill: blue;
}
.add-device:hover {
fill: blue;
}
.add-device:hover+.slot {
fill: #fff;
}
.reserved {
fill: url(#reserved);
}
.reserved:hover {
fill: url(#reserved);
}
.occupied {
fill: url(#occupied);
}
.occupied:hover {
fill: url(#occupied);
}
.blocked {
fill: url(#blocked);
}
.blocked:hover {
fill: url(#blocked);
}
.blocked:hover+.add-device {
fill: none;
}
"""
# Rack Elevation SVG Size
RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 230
RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 20

View File

@@ -22,7 +22,7 @@ class MACAddressField(models.Field):
def python_type(self):
return EUI
def from_db_value(self, value, expression, connection, context):
def from_db_value(self, value, expression, connection):
return self.to_python(value)
def to_python(self, value):

View File

@@ -2,15 +2,16 @@ import django_filters
from django.contrib.auth.models import User
from django.db.models import Q
from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter
from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter, CreatedUpdatedFilterSet
from tenancy.filtersets import TenancyFilterSet
from tenancy.models import Tenant
from utilities.constants import COLOR_CHOICES
from utilities.filters import (
MultiValueMACAddressFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter,
TreeNodeMultipleChoiceFilter,
MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter,
TagFilter, TreeNodeMultipleChoiceFilter,
)
from virtualization.models import Cluster
from .choices import *
from .constants import *
from .models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@@ -38,7 +39,7 @@ class RegionFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug']
class SiteFilter(TenancyFilterSet, CustomFieldFilterSet):
class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@@ -48,7 +49,7 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet):
label='Search',
)
status = django_filters.MultipleChoiceFilter(
choices=SITE_STATUS_CHOICES,
choices=SiteStatusChoices,
null_value=None
)
region_id = TreeNodeMultipleChoiceFilter(
@@ -116,7 +117,7 @@ class RackRoleFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'color']
class RackFilter(TenancyFilterSet, CustomFieldFilterSet):
class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@@ -146,7 +147,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet):
label='Group',
)
status = django_filters.MultipleChoiceFilter(
choices=RACK_STATUS_CHOICES,
choices=RackStatusChoices,
null_value=None
)
role_id = django_filters.ModelMultipleChoiceFilter(
@@ -251,7 +252,7 @@ class ManufacturerFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug']
class DeviceTypeFilter(CustomFieldFilterSet):
class DeviceTypeFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@@ -346,28 +347,28 @@ class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
model = ConsolePortTemplate
fields = ['id', 'name']
fields = ['id', 'name', 'type']
class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
model = ConsoleServerPortTemplate
fields = ['id', 'name']
fields = ['id', 'name', 'type']
class PowerPortTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
model = PowerPortTemplate
fields = ['id', 'name', 'maximum_draw', 'allocated_draw']
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
class PowerOutletTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
model = PowerOutletTemplate
fields = ['id', 'name', 'feed_leg']
fields = ['id', 'name', 'type', 'feed_leg']
class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
@@ -423,7 +424,7 @@ class PlatformFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'napalm_driver']
class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet):
class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@@ -510,7 +511,7 @@ class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilter
label='Device model (slug)',
)
status = django_filters.MultipleChoiceFilter(
choices=DEVICE_STATUS_CHOICES,
choices=DeviceStatusChoices,
null_value=None
)
is_full_depth = django_filters.BooleanFilter(
@@ -620,6 +621,26 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
method='search',
label='Search',
)
region_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__region',
queryset=Region.objects.all(),
label='Region (ID)',
)
region = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__region__in',
queryset=Region.objects.all(),
label='Region name (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(),
label='Site name (slug)',
)
device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
label='Device (ID)',
@@ -641,6 +662,10 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
class ConsolePortFilter(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
@@ -653,6 +678,10 @@ class ConsolePortFilter(DeviceComponentFilterSet):
class ConsoleServerPortFilter(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
@@ -665,6 +694,10 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet):
class PowerPortFilter(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PowerPortTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
@@ -677,6 +710,10 @@ class PowerPortFilter(DeviceComponentFilterSet):
class PowerOutletFilter(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PowerOutletTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
@@ -696,7 +733,28 @@ class InterfaceFilter(django_filters.FilterSet):
method='search',
label='Search',
)
device = django_filters.CharFilter(
region_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__region',
queryset=Region.objects.all(),
label='Region (ID)',
)
region = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__region__in',
queryset=Region.objects.all(),
label='Region name (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',
to_field_name='slug',
queryset=Site.objects.all(),
label='Site name (slug)',
)
device = MultiValueCharFilter(
method='filter_device',
field_name='name',
label='Device',
@@ -731,7 +789,7 @@ class InterfaceFilter(django_filters.FilterSet):
label='Assigned VID'
)
type = django_filters.MultipleChoiceFilter(
choices=IFACE_TYPE_CHOICES,
choices=InterfaceTypeChoices,
null_value=None
)
@@ -749,8 +807,10 @@ class InterfaceFilter(django_filters.FilterSet):
def filter_device(self, queryset, name, value):
try:
device = Device.objects.get(**{name: value})
vc_interface_ids = device.vc_interfaces.values_list('id', flat=True)
devices = Device.objects.filter(**{'{}__in'.format(name): value})
vc_interface_ids = []
for device in devices:
vc_interface_ids.extend(device.vc_interfaces.values_list('id', flat=True))
return queryset.filter(pk__in=vc_interface_ids)
except Device.DoesNotExist:
return queryset.none()
@@ -922,10 +982,10 @@ class CableFilter(django_filters.FilterSet):
label='Search',
)
type = django_filters.MultipleChoiceFilter(
choices=CABLE_TYPE_CHOICES
choices=CableTypeChoices
)
status = django_filters.MultipleChoiceFilter(
choices=CONNECTION_STATUS_CHOICES
choices=CableStatusChoices
)
color = django_filters.MultipleChoiceFilter(
choices=COLOR_CHOICES
@@ -1096,7 +1156,7 @@ class PowerPanelFilter(django_filters.FilterSet):
return queryset.filter(qs_filter)
class PowerFeedFilter(CustomFieldFilterSet):
class PowerFeedFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'

View File

@@ -1910,7 +1910,7 @@
"site": 1,
"rack": 1,
"position": 1,
"face": 0,
"face": "front",
"status": true,
"primary_ip4": 1,
"primary_ip6": null,
@@ -1931,7 +1931,7 @@
"site": 1,
"rack": 1,
"position": 17,
"face": 0,
"face": "rear",
"status": true,
"primary_ip4": 5,
"primary_ip6": null,
@@ -1952,7 +1952,7 @@
"site": 1,
"rack": 1,
"position": 33,
"face": 0,
"face": "rear",
"status": true,
"primary_ip4": null,
"primary_ip6": null,
@@ -1973,7 +1973,7 @@
"site": 1,
"rack": 1,
"position": 34,
"face": 0,
"face": "rear",
"status": true,
"primary_ip4": null,
"primary_ip6": null,
@@ -1994,7 +1994,7 @@
"site": 1,
"rack": 2,
"position": 34,
"face": 0,
"face": "rear",
"status": true,
"primary_ip4": null,
"primary_ip6": null,
@@ -2015,7 +2015,7 @@
"site": 1,
"rack": 2,
"position": 33,
"face": 0,
"face": "rear",
"status": true,
"primary_ip4": null,
"primary_ip6": null,
@@ -2036,7 +2036,7 @@
"site": 1,
"rack": 2,
"position": 1,
"face": 0,
"face": "rear",
"status": true,
"primary_ip4": 3,
"primary_ip6": null,
@@ -2057,7 +2057,7 @@
"site": 1,
"rack": 2,
"position": 17,
"face": 0,
"face": "rear",
"status": true,
"primary_ip4": 19,
"primary_ip6": null,
@@ -2078,7 +2078,7 @@
"site": 1,
"rack": 1,
"position": 42,
"face": 0,
"face": "rear",
"status": true,
"primary_ip4": null,
"primary_ip6": null,
@@ -2099,7 +2099,7 @@
"site": 1,
"rack": 1,
"position": null,
"face": null,
"face": "",
"status": true,
"primary_ip4": null,
"primary_ip6": null,
@@ -2120,7 +2120,7 @@
"site": 1,
"rack": 2,
"position": null,
"face": null,
"face": "",
"status": true,
"primary_ip4": null,
"primary_ip6": null,

View File

@@ -1,195 +0,0 @@
[
{
"model": "dcim.devicerole",
"pk": 1,
"fields": {
"name": "Console Server",
"slug": "console-server",
"color": "009688"
}
},
{
"model": "dcim.devicerole",
"pk": 2,
"fields": {
"name": "Core Switch",
"slug": "core-switch",
"color": "2196f3"
}
},
{
"model": "dcim.devicerole",
"pk": 3,
"fields": {
"name": "Distribution Switch",
"slug": "distribution-switch",
"color": "2196f3"
}
},
{
"model": "dcim.devicerole",
"pk": 4,
"fields": {
"name": "Access Switch",
"slug": "access-switch",
"color": "2196f3"
}
},
{
"model": "dcim.devicerole",
"pk": 5,
"fields": {
"name": "Management Switch",
"slug": "management-switch",
"color": "ff9800"
}
},
{
"model": "dcim.devicerole",
"pk": 6,
"fields": {
"name": "Firewall",
"slug": "firewall",
"color": "f44336"
}
},
{
"model": "dcim.devicerole",
"pk": 7,
"fields": {
"name": "Router",
"slug": "router",
"color": "9c27b0"
}
},
{
"model": "dcim.devicerole",
"pk": 8,
"fields": {
"name": "Server",
"slug": "server",
"color": "9e9e9e"
}
},
{
"model": "dcim.devicerole",
"pk": 9,
"fields": {
"name": "PDU",
"slug": "pdu",
"color": "607d8b"
}
},
{
"model": "dcim.manufacturer",
"pk": 1,
"fields": {
"name": "APC",
"slug": "apc"
}
},
{
"model": "dcim.manufacturer",
"pk": 2,
"fields": {
"name": "Cisco",
"slug": "cisco"
}
},
{
"model": "dcim.manufacturer",
"pk": 3,
"fields": {
"name": "Dell",
"slug": "dell"
}
},
{
"model": "dcim.manufacturer",
"pk": 4,
"fields": {
"name": "HP",
"slug": "hp"
}
},
{
"model": "dcim.manufacturer",
"pk": 5,
"fields": {
"name": "Juniper",
"slug": "juniper"
}
},
{
"model": "dcim.manufacturer",
"pk": 6,
"fields": {
"name": "Arista",
"slug": "arista"
}
},
{
"model": "dcim.manufacturer",
"pk": 7,
"fields": {
"name": "Opengear",
"slug": "opengear"
}
},
{
"model": "dcim.manufacturer",
"pk": 8,
"fields": {
"name": "Super Micro",
"slug": "super-micro"
}
},
{
"model": "dcim.platform",
"pk": 1,
"fields": {
"name": "Cisco IOS",
"slug": "cisco-ios"
}
},
{
"model": "dcim.platform",
"pk": 2,
"fields": {
"name": "Cisco NX-OS",
"slug": "cisco-nx-os"
}
},
{
"model": "dcim.platform",
"pk": 3,
"fields": {
"name": "Juniper Junos",
"slug": "juniper-junos"
}
},
{
"model": "dcim.platform",
"pk": 4,
"fields": {
"name": "Arista EOS",
"slug": "arista-eos"
}
},
{
"model": "dcim.platform",
"pk": 5,
"fields": {
"name": "Linux",
"slug": "linux"
}
},
{
"model": "dcim.platform",
"pk": 6,
"fields": {
"name": "Opengear",
"slug": "opengear"
}
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -174,8 +174,8 @@ class Migration(migrations.Migration):
('length', models.PositiveSmallIntegerField(blank=True, null=True)),
('length_unit', models.PositiveSmallIntegerField(blank=True, null=True)),
('_abs_length', models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True)),
('termination_a_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
('termination_b_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
('termination_a_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination', 'powerfeed']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
('termination_b_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination', 'powerfeed']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
],
),
migrations.AlterUniqueTogether(

View File

@@ -1,3 +1,5 @@
import sys
from django.db import migrations, models
import django.db.models.deletion
@@ -5,14 +7,15 @@ import django.db.models.deletion
def cache_cable_devices(apps, schema_editor):
Cable = apps.get_model('dcim', 'Cable')
print("\nUpdatng cable device terminations...")
if 'test' not in sys.argv:
print("\nUpdating cable device terminations...")
cable_count = Cable.objects.count()
# Cache A/B termination devices on all existing Cables. Note that the custom save() method on Cable is not
# available during a migration, so we replicate its logic here.
for i, cable in enumerate(Cable.objects.all(), start=1):
if not i % 1000:
if not i % 1000 and 'test' not in sys.argv:
print("[{}/{}]".format(i, cable_count))
termination_a_model = apps.get_model(cable.termination_a_type.app_label, cable.termination_a_type.model)

View File

@@ -0,0 +1,33 @@
# Generated by Django 2.2.6 on 2019-10-30 17:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0075_cable_devices'),
]
operations = [
migrations.AddField(
model_name='consoleport',
name='type',
field=models.CharField(blank=True, max_length=50),
),
migrations.AddField(
model_name='consoleporttemplate',
name='type',
field=models.CharField(blank=True, max_length=50),
),
migrations.AddField(
model_name='consoleserverport',
name='type',
field=models.CharField(blank=True, max_length=50),
),
migrations.AddField(
model_name='consoleserverporttemplate',
name='type',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 2.2.6 on 2019-11-06 19:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0076_console_port_types'),
]
operations = [
migrations.AddField(
model_name='poweroutlet',
name='type',
field=models.CharField(blank=True, max_length=50),
),
migrations.AddField(
model_name='poweroutlettemplate',
name='type',
field=models.CharField(blank=True, max_length=50),
),
migrations.AddField(
model_name='powerport',
name='type',
field=models.CharField(blank=True, max_length=50),
),
migrations.AddField(
model_name='powerporttemplate',
name='type',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@@ -0,0 +1,35 @@
from django.db import migrations, models
SITE_STATUS_CHOICES = (
(1, 'active'),
(2, 'planned'),
(4, 'retired'),
)
def site_status_to_slug(apps, schema_editor):
Site = apps.get_model('dcim', 'Site')
for id, slug in SITE_STATUS_CHOICES:
Site.objects.filter(status=str(id)).update(status=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0077_power_types'),
]
operations = [
# Site.status
migrations.AlterField(
model_name='site',
name='status',
field=models.CharField(default='active', max_length=50),
),
migrations.RunPython(
code=site_status_to_slug
),
]

View File

@@ -0,0 +1,92 @@
from django.db import migrations, models
RACK_TYPE_CHOICES = (
(100, '2-post-frame'),
(200, '4-post-frame'),
(300, '4-post-cabinet'),
(1000, 'wall-frame'),
(1100, 'wall-cabinet'),
)
RACK_STATUS_CHOICES = (
(0, 'reserved'),
(1, 'available'),
(2, 'planned'),
(3, 'active'),
(4, 'deprecated'),
)
RACK_DIMENSION_CHOICES = (
(1000, 'mm'),
(2000, 'in'),
)
def rack_type_to_slug(apps, schema_editor):
Rack = apps.get_model('dcim', 'Rack')
for id, slug in RACK_TYPE_CHOICES:
Rack.objects.filter(type=str(id)).update(type=slug)
def rack_status_to_slug(apps, schema_editor):
Rack = apps.get_model('dcim', 'Rack')
for id, slug in RACK_STATUS_CHOICES:
Rack.objects.filter(status=str(id)).update(status=slug)
def rack_outer_unit_to_slug(apps, schema_editor):
Rack = apps.get_model('dcim', 'Rack')
for id, slug in RACK_DIMENSION_CHOICES:
Rack.objects.filter(status=str(id)).update(status=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0078_3569_site_fields'),
]
operations = [
# Rack.type
migrations.AlterField(
model_name='rack',
name='type',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=rack_type_to_slug
),
migrations.AlterField(
model_name='rack',
name='type',
field=models.CharField(blank=True, max_length=50),
),
# Rack.status
migrations.AlterField(
model_name='rack',
name='status',
field=models.CharField(default='active', max_length=50),
),
migrations.RunPython(
code=rack_status_to_slug
),
# Rack.outer_unit
migrations.AlterField(
model_name='rack',
name='outer_unit',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=rack_outer_unit_to_slug
),
migrations.AlterField(
model_name='rack',
name='outer_unit',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@@ -0,0 +1,39 @@
from django.db import migrations, models
SUBDEVICE_ROLE_CHOICES = (
('true', 'parent'),
('false', 'child'),
)
def devicetype_subdevicerole_to_slug(apps, schema_editor):
DeviceType = apps.get_model('dcim', 'DeviceType')
for boolean, slug in SUBDEVICE_ROLE_CHOICES:
DeviceType.objects.filter(subdevice_role=boolean).update(subdevice_role=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0079_3569_rack_fields'),
]
operations = [
# DeviceType.subdevice_role
migrations.AlterField(
model_name='devicetype',
name='subdevice_role',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=devicetype_subdevicerole_to_slug
),
migrations.AlterField(
model_name='devicetype',
name='subdevice_role',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@@ -0,0 +1,65 @@
from django.db import migrations, models
DEVICE_FACE_CHOICES = (
(0, 'front'),
(1, 'rear'),
)
DEVICE_STATUS_CHOICES = (
(0, 'offline'),
(1, 'active'),
(2, 'planned'),
(3, 'staged'),
(4, 'failed'),
(5, 'inventory'),
(6, 'decommissioning'),
)
def device_face_to_slug(apps, schema_editor):
Device = apps.get_model('dcim', 'Device')
for id, slug in DEVICE_FACE_CHOICES:
Device.objects.filter(face=str(id)).update(face=slug)
def device_status_to_slug(apps, schema_editor):
Device = apps.get_model('dcim', 'Device')
for id, slug in DEVICE_STATUS_CHOICES:
Device.objects.filter(status=str(id)).update(status=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0080_3569_devicetype_fields'),
]
operations = [
# Device.face
migrations.AlterField(
model_name='device',
name='face',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=device_face_to_slug
),
migrations.AlterField(
model_name='device',
name='face',
field=models.CharField(blank=True, max_length=50),
),
# Device.status
migrations.AlterField(
model_name='device',
name='status',
field=models.CharField(default='active', max_length=50),
),
migrations.RunPython(
code=device_status_to_slug
),
]

View File

@@ -0,0 +1,147 @@
from django.db import migrations, models
INTERFACE_TYPE_CHOICES = (
(0, 'virtual'),
(200, 'lag'),
(800, '100base-tx'),
(1000, '1000base-t'),
(1050, '1000base-x-gbic'),
(1100, '1000base-x-sfp'),
(1120, '2.5gbase-t'),
(1130, '5gbase-t'),
(1150, '10gbase-t'),
(1170, '10gbase-cx4'),
(1200, '10gbase-x-sfpp'),
(1300, '10gbase-x-xfp'),
(1310, '10gbase-x-xenpak'),
(1320, '10gbase-x-x2'),
(1350, '25gbase-x-sfp28'),
(1400, '40gbase-x-qsfpp'),
(1420, '50gbase-x-sfp28'),
(1500, '100gbase-x-cfp'),
(1510, '100gbase-x-cfp2'),
(1520, '100gbase-x-cfp4'),
(1550, '100gbase-x-cpak'),
(1600, '100gbase-x-qsfp28'),
(1650, '200gbase-x-cfp2'),
(1700, '200gbase-x-qsfp56'),
(1750, '400gbase-x-qsfpdd'),
(1800, '400gbase-x-osfp'),
(2600, 'ieee802.11a'),
(2610, 'ieee802.11g'),
(2620, 'ieee802.11n'),
(2630, 'ieee802.11ac'),
(2640, 'ieee802.11ad'),
(2810, 'gsm'),
(2820, 'cdma'),
(2830, 'lte'),
(6100, 'sonet-oc3'),
(6200, 'sonet-oc12'),
(6300, 'sonet-oc48'),
(6400, 'sonet-oc192'),
(6500, 'sonet-oc768'),
(6600, 'sonet-oc1920'),
(6700, 'sonet-oc3840'),
(3010, '1gfc-sfp'),
(3020, '2gfc-sfp'),
(3040, '4gfc-sfp'),
(3080, '8gfc-sfpp'),
(3160, '16gfc-sfpp'),
(3320, '32gfc-sfp28'),
(3400, '128gfc-sfp28'),
(7010, 'inifiband-sdr'),
(7020, 'inifiband-ddr'),
(7030, 'inifiband-qdr'),
(7040, 'inifiband-fdr10'),
(7050, 'inifiband-fdr'),
(7060, 'inifiband-edr'),
(7070, 'inifiband-hdr'),
(7080, 'inifiband-ndr'),
(7090, 'inifiband-xdr'),
(4000, 't1'),
(4010, 'e1'),
(4040, 't3'),
(4050, 'e3'),
(5000, 'cisco-stackwise'),
(5050, 'cisco-stackwise-plus'),
(5100, 'cisco-flexstack'),
(5150, 'cisco-flexstack-plus'),
(5200, 'juniper-vcp'),
(5300, 'extreme-summitstack'),
(5310, 'extreme-summitstack-128'),
(5320, 'extreme-summitstack-256'),
(5330, 'extreme-summitstack-512'),
)
INTERFACE_MODE_CHOICES = (
(100, 'access'),
(200, 'tagged'),
(300, 'tagged-all'),
)
def interfacetemplate_type_to_slug(apps, schema_editor):
InterfaceTemplate = apps.get_model('dcim', 'InterfaceTemplate')
for id, slug in INTERFACE_TYPE_CHOICES:
InterfaceTemplate.objects.filter(type=id).update(type=slug)
def interface_type_to_slug(apps, schema_editor):
Interface = apps.get_model('dcim', 'Interface')
for id, slug in INTERFACE_TYPE_CHOICES:
Interface.objects.filter(type=id).update(type=slug)
def interface_mode_to_slug(apps, schema_editor):
Interface = apps.get_model('dcim', 'Interface')
for id, slug in INTERFACE_MODE_CHOICES:
Interface.objects.filter(mode=id).update(mode=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0081_3569_device_fields'),
]
operations = [
# InterfaceTemplate.type
migrations.AlterField(
model_name='interfacetemplate',
name='type',
field=models.CharField(max_length=50),
),
migrations.RunPython(
code=interfacetemplate_type_to_slug
),
# Interface.type
migrations.AlterField(
model_name='interface',
name='type',
field=models.CharField(max_length=50),
),
migrations.RunPython(
code=interface_type_to_slug
),
# Interface.mode
migrations.AlterField(
model_name='interface',
name='mode',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=interface_mode_to_slug
),
migrations.AlterField(
model_name='interface',
name='mode',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@@ -0,0 +1,93 @@
from django.db import migrations, models
PORT_TYPE_CHOICES = (
(1000, '8p8c'),
(1100, '110-punch'),
(1200, 'bnc'),
(2000, 'st'),
(2100, 'sc'),
(2110, 'sc-apc'),
(2200, 'fc'),
(2300, 'lc'),
(2310, 'lc-apc'),
(2400, 'mtrj'),
(2500, 'mpo'),
(2600, 'lsh'),
(2610, 'lsh-apc'),
)
def frontporttemplate_type_to_slug(apps, schema_editor):
FrontPortTemplate = apps.get_model('dcim', 'FrontPortTemplate')
for id, slug in PORT_TYPE_CHOICES:
FrontPortTemplate.objects.filter(type=id).update(type=slug)
def rearporttemplate_type_to_slug(apps, schema_editor):
RearPortTemplate = apps.get_model('dcim', 'RearPortTemplate')
for id, slug in PORT_TYPE_CHOICES:
RearPortTemplate.objects.filter(type=id).update(type=slug)
def frontport_type_to_slug(apps, schema_editor):
FrontPort = apps.get_model('dcim', 'FrontPort')
for id, slug in PORT_TYPE_CHOICES:
FrontPort.objects.filter(type=id).update(type=slug)
def rearport_type_to_slug(apps, schema_editor):
RearPort = apps.get_model('dcim', 'RearPort')
for id, slug in PORT_TYPE_CHOICES:
RearPort.objects.filter(type=id).update(type=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0082_3569_interface_fields'),
]
operations = [
# FrontPortTemplate.type
migrations.AlterField(
model_name='frontporttemplate',
name='type',
field=models.CharField(max_length=50),
),
migrations.RunPython(
code=frontporttemplate_type_to_slug
),
# RearPortTemplate.type
migrations.AlterField(
model_name='rearporttemplate',
name='type',
field=models.CharField(max_length=50),
),
migrations.RunPython(
code=rearporttemplate_type_to_slug
),
# FrontPort.type
migrations.AlterField(
model_name='frontport',
name='type',
field=models.CharField(max_length=50),
),
migrations.RunPython(
code=frontport_type_to_slug
),
# RearPort.type
migrations.AlterField(
model_name='rearport',
name='type',
field=models.CharField(max_length=50),
),
migrations.RunPython(
code=rearport_type_to_slug
),
]

View File

@@ -0,0 +1,106 @@
from django.db import migrations, models
CABLE_TYPE_CHOICES = (
(1300, 'cat3'),
(1500, 'cat5'),
(1510, 'cat5e'),
(1600, 'cat6'),
(1610, 'cat6a'),
(1700, 'cat7'),
(1800, 'dac-active'),
(1810, 'dac-passive'),
(1900, 'coaxial'),
(3000, 'mmf'),
(3010, 'mmf-om1'),
(3020, 'mmf-om2'),
(3030, 'mmf-om3'),
(3040, 'mmf-om4'),
(3500, 'smf'),
(3510, 'smf-os1'),
(3520, 'smf-os2'),
(3800, 'aoc'),
(5000, 'power'),
)
CABLE_STATUS_CHOICES = (
(True, 'connected'),
(False, 'planned'),
)
CABLE_LENGTH_UNIT_CHOICES = (
(1200, 'm'),
(1100, 'cm'),
(2100, 'ft'),
(2000, 'in'),
)
def cable_type_to_slug(apps, schema_editor):
Cable = apps.get_model('dcim', 'Cable')
for id, slug in CABLE_TYPE_CHOICES:
Cable.objects.filter(type=id).update(type=slug)
def cable_status_to_slug(apps, schema_editor):
Cable = apps.get_model('dcim', 'Cable')
for bool, slug in CABLE_STATUS_CHOICES:
Cable.objects.filter(status=str(bool)).update(status=slug)
def cable_length_unit_to_slug(apps, schema_editor):
Cable = apps.get_model('dcim', 'Cable')
for id, slug in CABLE_LENGTH_UNIT_CHOICES:
Cable.objects.filter(length_unit=id).update(length_unit=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0082_3569_port_fields'),
]
operations = [
# Cable.type
migrations.AlterField(
model_name='cable',
name='type',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=cable_type_to_slug
),
migrations.AlterField(
model_name='cable',
name='type',
field=models.CharField(blank=True, max_length=50),
),
# Cable.status
migrations.AlterField(
model_name='cable',
name='status',
field=models.CharField(default='connected', max_length=50),
),
migrations.RunPython(
code=cable_status_to_slug
),
# Cable.length_unit
migrations.AlterField(
model_name='cable',
name='length_unit',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=cable_length_unit_to_slug
),
migrations.AlterField(
model_name='cable',
name='length_unit',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@@ -0,0 +1,100 @@
from django.db import migrations, models
POWERFEED_STATUS_CHOICES = (
(0, 'offline'),
(1, 'active'),
(2, 'planned'),
(4, 'failed'),
)
POWERFEED_TYPE_CHOICES = (
(1, 'primary'),
(2, 'redundant'),
)
POWERFEED_SUPPLY_CHOICES = (
(1, 'ac'),
(2, 'dc'),
)
POWERFEED_PHASE_CHOICES = (
(1, 'single-phase'),
(3, 'three-phase'),
)
def powerfeed_status_to_slug(apps, schema_editor):
PowerFeed = apps.get_model('dcim', 'PowerFeed')
for id, slug in POWERFEED_STATUS_CHOICES:
PowerFeed.objects.filter(status=id).update(status=slug)
def powerfeed_type_to_slug(apps, schema_editor):
PowerFeed = apps.get_model('dcim', 'PowerFeed')
for id, slug in POWERFEED_TYPE_CHOICES:
PowerFeed.objects.filter(type=id).update(type=slug)
def powerfeed_supply_to_slug(apps, schema_editor):
PowerFeed = apps.get_model('dcim', 'PowerFeed')
for id, slug in POWERFEED_SUPPLY_CHOICES:
PowerFeed.objects.filter(supply=id).update(supply=slug)
def powerfeed_phase_to_slug(apps, schema_editor):
PowerFeed = apps.get_model('dcim', 'PowerFeed')
for id, slug in POWERFEED_PHASE_CHOICES:
PowerFeed.objects.filter(phase=id).update(phase=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0083_3569_cable_fields'),
]
operations = [
# PowerFeed.status
migrations.AlterField(
model_name='powerfeed',
name='status',
field=models.CharField(default='active', max_length=50),
),
migrations.RunPython(
code=powerfeed_status_to_slug
),
# PowerFeed.type
migrations.AlterField(
model_name='powerfeed',
name='type',
field=models.CharField(default='primary', max_length=50),
),
migrations.RunPython(
code=powerfeed_type_to_slug
),
# PowerFeed.supply
migrations.AlterField(
model_name='powerfeed',
name='supply',
field=models.CharField(default='ac', max_length=50),
),
migrations.RunPython(
code=powerfeed_supply_to_slug
),
# PowerFeed.phase
migrations.AlterField(
model_name='powerfeed',
name='phase',
field=models.CharField(default='single-phase', max_length=50),
),
migrations.RunPython(
code=powerfeed_phase_to_slug
),
]

View File

@@ -0,0 +1,62 @@
from django.db import migrations, models
POWEROUTLET_FEED_LEG_CHOICES_CHOICES = (
(1, 'A'),
(2, 'B'),
(3, 'C'),
)
def poweroutlettemplate_feed_leg_to_slug(apps, schema_editor):
PowerOutletTemplate = apps.get_model('dcim', 'PowerOutletTemplate')
for id, slug in POWEROUTLET_FEED_LEG_CHOICES_CHOICES:
PowerOutletTemplate.objects.filter(feed_leg=id).update(feed_leg=slug)
def poweroutlet_feed_leg_to_slug(apps, schema_editor):
PowerOutlet = apps.get_model('dcim', 'PowerOutlet')
for id, slug in POWEROUTLET_FEED_LEG_CHOICES_CHOICES:
PowerOutlet.objects.filter(feed_leg=id).update(feed_leg=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0084_3569_powerfeed_fields'),
]
operations = [
# PowerOutletTemplate.feed_leg
migrations.AlterField(
model_name='poweroutlettemplate',
name='feed_leg',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=poweroutlettemplate_feed_leg_to_slug
),
migrations.AlterField(
model_name='poweroutlettemplate',
name='feed_leg',
field=models.CharField(blank=True, max_length=50),
),
# PowerOutlet.feed_leg
migrations.AlterField(
model_name='poweroutlet',
name='feed_leg',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=poweroutlet_feed_leg_to_slug
),
migrations.AlterField(
model_name='poweroutlet',
name='feed_leg',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 2.2.6 on 2019-12-09 15:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0006_custom_tag_models'),
('dcim', '0085_3569_poweroutlet_fields'),
]
operations = [
migrations.AlterField(
model_name='device',
name='name',
field=models.CharField(blank=True, max_length=64, null=True),
),
migrations.AlterUniqueTogether(
name='device',
unique_together={('rack', 'position', 'face'), ('virtual_chassis', 'vc_position'), ('site', 'tenant', 'name')},
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 2.2.6 on 2019-12-10 17:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0086_device_name_nonunique'),
]
operations = [
migrations.AddField(
model_name='devicerole',
name='description',
field=models.CharField(blank=True, max_length=100),
),
migrations.AddField(
model_name='rackrole',
name='description',
field=models.CharField(blank=True, max_length=100),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.8 on 2019-12-12 02:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0087_role_descriptions'),
]
operations = [
migrations.AlterField(
model_name='powerfeed',
name='available_power',
field=models.PositiveIntegerField(default=0, editable=False),
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -45,7 +45,7 @@ def update_connected_endpoints(instance, **kwargs):
# Check if this Cable has formed a complete path. If so, update both endpoints.
endpoint_a, endpoint_b, path_status = instance.get_path_endpoints()
if endpoint_a is not None and endpoint_b is not None:
if getattr(endpoint_a, 'is_path_endpoint', False) and getattr(endpoint_b, 'is_path_endpoint', False):
endpoint_a.connected_endpoint = endpoint_b
endpoint_a.connection_status = path_status
endpoint_a.save()

View File

@@ -156,10 +156,6 @@ DEVICE_PRIMARY_IP = """
{{ record.primary_ip4.address.ip|default:"" }}
"""
SUBDEVICE_ROLE_TEMPLATE = """
{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}&mdash;{% endif %}
"""
DEVICETYPE_INSTANCES_TEMPLATE = """
<a href="{% url 'dcim:device_list' %}?manufacturer_id={{ record.manufacturer_id }}&device_type_id={{ record.pk }}">{{ record.instance_count }}</a>
"""
@@ -181,8 +177,10 @@ VIRTUALCHASSIS_ACTIONS = """
CABLE_TERMINATION_PARENT = """
{% if value.device %}
<a href="{{ value.device.get_absolute_url }}">{{ value.device }}</a>
{% else %}
{% elif value.circuit %}
<a href="{{ value.circuit.get_absolute_url }}">{{ value.circuit }}</a>
{% elif value.power_panel %}
<a href="{{ value.power_panel.get_absolute_url }}">{{ value.power_panel }}</a>
{% endif %}
"""
@@ -274,16 +272,17 @@ class RackGroupTable(BaseTable):
class RackRoleTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
rack_count = tables.Column(verbose_name='Racks')
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}},
verbose_name='')
color = tables.TemplateColumn(COLOR_LABEL)
actions = tables.TemplateColumn(
template_code=RACKROLE_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = RackRole
fields = ('pk', 'name', 'rack_count', 'color', 'slug', 'actions')
fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions')
#
@@ -391,10 +390,6 @@ class DeviceTypeTable(BaseTable):
verbose_name='Device Type'
)
is_full_depth = BooleanColumn(verbose_name='Full Depth')
subdevice_role = tables.TemplateColumn(
template_code=SUBDEVICE_ROLE_TEMPLATE,
verbose_name='Subdevice Role'
)
instance_count = tables.TemplateColumn(
template_code=DEVICETYPE_INSTANCES_TEMPLATE,
verbose_name='Instances'
@@ -422,10 +417,19 @@ class ConsolePortTemplateTable(BaseTable):
class Meta(BaseTable.Meta):
model = ConsolePortTemplate
fields = ('pk', 'name', 'actions')
fields = ('pk', 'name', 'type', 'actions')
empty_text = "None"
class ConsolePortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = ConsolePort
fields = ('device', 'name', 'description')
empty_text = False
class ConsoleServerPortTemplateTable(BaseTable):
pk = ToggleColumn()
actions = tables.TemplateColumn(
@@ -440,6 +444,15 @@ class ConsoleServerPortTemplateTable(BaseTable):
empty_text = "None"
class ConsoleServerPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = ConsoleServerPort
fields = ('device', 'name', 'description')
empty_text = False
class PowerPortTemplateTable(BaseTable):
pk = ToggleColumn()
actions = tables.TemplateColumn(
@@ -450,10 +463,19 @@ class PowerPortTemplateTable(BaseTable):
class Meta(BaseTable.Meta):
model = PowerPortTemplate
fields = ('pk', 'name', 'maximum_draw', 'allocated_draw', 'actions')
fields = ('pk', 'name', 'type', 'maximum_draw', 'allocated_draw', 'actions')
empty_text = "None"
class PowerPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = PowerPort
fields = ('device', 'name', 'description', 'maximum_draw', 'allocated_draw')
empty_text = False
class PowerOutletTemplateTable(BaseTable):
pk = ToggleColumn()
actions = tables.TemplateColumn(
@@ -464,10 +486,19 @@ class PowerOutletTemplateTable(BaseTable):
class Meta(BaseTable.Meta):
model = PowerOutletTemplate
fields = ('pk', 'name', 'power_port', 'feed_leg', 'actions')
fields = ('pk', 'name', 'type', 'power_port', 'feed_leg', 'actions')
empty_text = "None"
class PowerOutletImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = PowerOutlet
fields = ('device', 'name', 'description', 'power_port', 'feed_leg')
empty_text = False
class InterfaceTemplateTable(BaseTable):
pk = ToggleColumn()
mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}")
@@ -483,6 +514,16 @@ class InterfaceTemplateTable(BaseTable):
empty_text = "None"
class InterfaceImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
virtual_machine = tables.LinkColumn('virtualization:virtualmachine', args=[Accessor('virtual_machine.pk')], verbose_name='Virtual Machine')
class Meta(BaseTable.Meta):
model = Interface
fields = ('device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'mode')
empty_text = False
class FrontPortTemplateTable(BaseTable):
pk = ToggleColumn()
rear_port_position = tables.Column(
@@ -500,6 +541,15 @@ class FrontPortTemplateTable(BaseTable):
empty_text = "None"
class FrontPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = FrontPort
fields = ('device', 'name', 'description', 'type', 'rear_port', 'rear_port_position')
empty_text = False
class RearPortTemplateTable(BaseTable):
pk = ToggleColumn()
actions = tables.TemplateColumn(
@@ -514,6 +564,15 @@ class RearPortTemplateTable(BaseTable):
empty_text = "None"
class RearPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = RearPort
fields = ('device', 'name', 'description', 'type', 'position')
empty_text = False
class DeviceBayTemplateTable(BaseTable):
pk = ToggleColumn()
actions = tables.TemplateColumn(
@@ -556,7 +615,7 @@ class DeviceRoleTable(BaseTable):
class Meta(BaseTable.Meta):
model = DeviceRole
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'slug', 'actions')
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions')
#
@@ -643,11 +702,28 @@ class DeviceImportTable(BaseTable):
# Device components
#
class DeviceComponentDetailTable(BaseTable):
pk = ToggleColumn()
cable = tables.LinkColumn()
class Meta(BaseTable.Meta):
order_by = ('device', 'name')
fields = ('pk', 'device', 'name', 'type', 'description', 'cable')
sequence = ('pk', 'device', 'name', 'type', 'description', 'cable')
class ConsolePortTable(BaseTable):
class Meta(BaseTable.Meta):
model = ConsolePort
fields = ('name',)
fields = ('name', 'type')
class ConsolePortDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, ConsolePortTable.Meta):
pass
class ConsoleServerPortTable(BaseTable):
@@ -657,18 +733,39 @@ class ConsoleServerPortTable(BaseTable):
fields = ('name', 'description')
class ConsoleServerPortDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, ConsoleServerPortTable.Meta):
pass
class PowerPortTable(BaseTable):
class Meta(BaseTable.Meta):
model = PowerPort
fields = ('name',)
fields = ('name', 'type')
class PowerPortDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, PowerPortTable.Meta):
pass
class PowerOutletTable(BaseTable):
class Meta(BaseTable.Meta):
model = PowerOutlet
fields = ('name', 'description')
fields = ('name', 'type', 'description')
class PowerOutletDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, PowerOutletTable.Meta):
pass
class InterfaceTable(BaseTable):
@@ -678,6 +775,15 @@ class InterfaceTable(BaseTable):
fields = ('name', 'type', 'lag', 'enabled', 'mgmt_only', 'description')
class InterfaceDetailTable(DeviceComponentDetailTable):
parent = tables.LinkColumn(order_by=('device', 'virtual_machine'))
class Meta(InterfaceTable.Meta):
order_by = ('parent', 'name')
fields = ('pk', 'parent', 'name', 'type', 'description', 'cable')
sequence = ('pk', 'parent', 'name', 'type', 'description', 'cable')
class FrontPortTable(BaseTable):
class Meta(BaseTable.Meta):
@@ -686,6 +792,13 @@ class FrontPortTable(BaseTable):
empty_text = "None"
class FrontPortDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, FrontPortTable.Meta):
pass
class RearPortTable(BaseTable):
class Meta(BaseTable.Meta):
@@ -694,6 +807,13 @@ class RearPortTable(BaseTable):
empty_text = "None"
class RearPortDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, RearPortTable.Meta):
pass
class DeviceBayTable(BaseTable):
class Meta(BaseTable.Meta):
@@ -701,6 +821,26 @@ class DeviceBayTable(BaseTable):
fields = ('name',)
class DeviceBayDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
installed_device = tables.LinkColumn()
class Meta(DeviceBayTable.Meta):
fields = ('pk', 'name', 'device', 'installed_device')
sequence = ('pk', 'name', 'device', 'installed_device')
exclude = ('cable',)
class DeviceBayImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
installed_device = tables.LinkColumn('dcim:device', args=[Accessor('installed_device.pk')], verbose_name='Installed Device')
class Meta(BaseTable.Meta):
model = DeviceBay
fields = ('device', 'name', 'installed_device', 'description')
empty_text = False
#
# Cables
#
@@ -718,7 +858,7 @@ class CableTable(BaseTable):
orderable=False,
verbose_name='Termination A'
)
termination_a = tables.Column(
termination_a = tables.LinkColumn(
accessor=Accessor('termination_a'),
orderable=False,
verbose_name=''
@@ -729,7 +869,7 @@ class CableTable(BaseTable):
orderable=False,
verbose_name='Termination B'
)
termination_b = tables.Column(
termination_b = tables.LinkColumn(
accessor=Accessor('termination_b'),
orderable=False,
verbose_name=''

View File

@@ -1,8 +1,10 @@
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from netaddr import IPNetwork
from rest_framework import status
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.choices import *
from dcim.constants import *
from dcim.models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@@ -11,7 +13,7 @@ from dcim.models import (
Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
)
from ipam.models import IPAddress, VLAN
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from extras.models import Graph
from utilities.testing import APITestCase
from virtualization.models import Cluster, ClusterType
@@ -138,16 +140,20 @@ class SiteTest(APITestCase):
def test_get_site_graphs(self):
site_ct = ContentType.objects.get_for_model(Site)
self.graph1 = Graph.objects.create(
type=GRAPH_TYPE_SITE, name='Test Graph 1',
type=site_ct,
name='Test Graph 1',
source='http://example.com/graphs.py?site={{ obj.slug }}&foo=1'
)
self.graph2 = Graph.objects.create(
type=GRAPH_TYPE_SITE, name='Test Graph 2',
type=site_ct,
name='Test Graph 2',
source='http://example.com/graphs.py?site={{ obj.slug }}&foo=2'
)
self.graph3 = Graph.objects.create(
type=GRAPH_TYPE_SITE, name='Test Graph 3',
type=site_ct,
name='Test Graph 3',
source='http://example.com/graphs.py?site={{ obj.slug }}&foo=3'
)
@@ -180,7 +186,7 @@ class SiteTest(APITestCase):
'name': 'Test Site 4',
'slug': 'test-site-4',
'region': self.region1.pk,
'status': SITE_STATUS_ACTIVE,
'status': SiteStatusChoices.STATUS_ACTIVE,
}
url = reverse('dcim-api:site-list')
@@ -200,19 +206,19 @@ class SiteTest(APITestCase):
'name': 'Test Site 4',
'slug': 'test-site-4',
'region': self.region1.pk,
'status': SITE_STATUS_ACTIVE,
'status': SiteStatusChoices.STATUS_ACTIVE,
},
{
'name': 'Test Site 5',
'slug': 'test-site-5',
'region': self.region1.pk,
'status': SITE_STATUS_ACTIVE,
'status': SiteStatusChoices.STATUS_ACTIVE,
},
{
'name': 'Test Site 6',
'slug': 'test-site-6',
'region': self.region1.pk,
'status': SITE_STATUS_ACTIVE,
'status': SiteStatusChoices.STATUS_ACTIVE,
},
]
@@ -2416,16 +2422,20 @@ class InterfaceTest(APITestCase):
def test_get_interface_graphs(self):
interface_ct = ContentType.objects.get_for_model(Interface)
self.graph1 = Graph.objects.create(
type=GRAPH_TYPE_INTERFACE, name='Test Graph 1',
type=interface_ct,
name='Test Graph 1',
source='http://example.com/graphs.py?interface={{ obj.name }}&foo=1'
)
self.graph2 = Graph.objects.create(
type=GRAPH_TYPE_INTERFACE, name='Test Graph 2',
type=interface_ct,
name='Test Graph 2',
source='http://example.com/graphs.py?interface={{ obj.name }}&foo=2'
)
self.graph3 = Graph.objects.create(
type=GRAPH_TYPE_INTERFACE, name='Test Graph 3',
type=interface_ct,
name='Test Graph 3',
source='http://example.com/graphs.py?interface={{ obj.name }}&foo=3'
)
@@ -2473,7 +2483,7 @@ class InterfaceTest(APITestCase):
data = {
'device': self.device.pk,
'name': 'Test Interface 4',
'mode': IFACE_MODE_TAGGED,
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan3.id,
'tagged_vlans': [self.vlan1.id, self.vlan2.id],
}
@@ -2520,21 +2530,21 @@ class InterfaceTest(APITestCase):
{
'device': self.device.pk,
'name': 'Test Interface 4',
'mode': IFACE_MODE_TAGGED,
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan2.id,
'tagged_vlans': [self.vlan1.id],
},
{
'device': self.device.pk,
'name': 'Test Interface 5',
'mode': IFACE_MODE_TAGGED,
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan2.id,
'tagged_vlans': [self.vlan1.id],
},
{
'device': self.device.pk,
'name': 'Test Interface 6',
'mode': IFACE_MODE_TAGGED,
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan2.id,
'tagged_vlans': [self.vlan1.id],
},
@@ -2553,7 +2563,7 @@ class InterfaceTest(APITestCase):
def test_update_interface(self):
lag_interface = Interface.objects.create(
device=self.device, name='Test LAG Interface', type=IFACE_TYPE_LAG
device=self.device, name='Test LAG Interface', type=InterfaceTypeChoices.TYPE_LAG
)
data = {
@@ -2590,11 +2600,11 @@ class DeviceBayTest(APITestCase):
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
self.devicetype1 = DeviceType.objects.create(
manufacturer=manufacturer, model='Parent Device Type', slug='parent-device-type',
subdevice_role=SUBDEVICE_ROLE_PARENT
subdevice_role=SubdeviceRoleChoices.ROLE_PARENT
)
self.devicetype2 = DeviceType.objects.create(
manufacturer=manufacturer, model='Child Device Type', slug='child-device-type',
subdevice_role=SUBDEVICE_ROLE_CHILD
subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
)
devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
@@ -2841,7 +2851,7 @@ class CableTest(APITestCase):
)
for device in [self.device1, self.device2]:
for i in range(0, 10):
Interface(device=device, type=IFACE_TYPE_1GE_FIXED, name='eth{}'.format(i)).save()
Interface(device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth{}'.format(i)).save()
self.cable1 = Cable(
termination_a=self.device1.interfaces.get(name='eth0'),
@@ -2885,7 +2895,7 @@ class CableTest(APITestCase):
'termination_a_id': interface_a.pk,
'termination_b_type': 'dcim.interface',
'termination_b_id': interface_b.pk,
'status': CONNECTION_STATUS_PLANNED,
'status': CableStatusChoices.STATUS_PLANNED,
'label': 'Test Cable 4',
}
@@ -2939,7 +2949,7 @@ class CableTest(APITestCase):
data = {
'label': 'Test Cable X',
'status': CONNECTION_STATUS_CONNECTED,
'status': CableStatusChoices.STATUS_CONNECTED,
}
url = reverse('dcim-api:cable-detail', kwargs={'pk': self.cable1.pk})
@@ -3033,16 +3043,16 @@ class ConnectionTest(APITestCase):
device=self.device2, name='Test Console Server Port 1'
)
rearport1 = RearPort.objects.create(
device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C
device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C
)
frontport1 = FrontPort.objects.create(
device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1
device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1
)
rearport2 = RearPort.objects.create(
device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C
device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C
)
frontport2 = FrontPort.objects.create(
device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2
device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2
)
url = reverse('dcim-api:cable-list')
@@ -3161,16 +3171,16 @@ class ConnectionTest(APITestCase):
device=self.device2, name='Test Interface 2'
)
rearport1 = RearPort.objects.create(
device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C
device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C
)
frontport1 = FrontPort.objects.create(
device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1
device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1
)
rearport2 = RearPort.objects.create(
device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C
device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C
)
frontport2 = FrontPort.objects.create(
device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2
device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2
)
url = reverse('dcim-api:cable-list')
@@ -3272,16 +3282,16 @@ class ConnectionTest(APITestCase):
circuit=circuit, term_side='A', site=self.site, port_speed=10000
)
rearport1 = RearPort.objects.create(
device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C
device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C
)
frontport1 = FrontPort.objects.create(
device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1
device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1
)
rearport2 = RearPort.objects.create(
device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C
device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C
)
frontport2 = FrontPort.objects.create(
device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2
device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2
)
url = reverse('dcim-api:cable-list')
@@ -3410,23 +3420,23 @@ class VirtualChassisTest(APITestCase):
device_type=device_type, device_role=device_role, name='StackSwitch9', site=site
)
for i in range(0, 13):
Interface.objects.create(device=self.device1, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device1, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device2, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device2, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device3, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device3, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device4, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device4, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device5, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device5, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device6, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device6, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device7, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device7, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device8, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device8, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device9, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device9, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
# Create two VirtualChassis with three members each
self.vc1 = VirtualChassis.objects.create(master=self.device1, domain='test-domain-1')
@@ -3678,22 +3688,22 @@ class PowerFeedTest(APITestCase):
site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 2'
)
self.powerfeed1 = PowerFeed.objects.create(
power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=POWERFEED_TYPE_PRIMARY
power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=PowerFeedTypeChoices.TYPE_PRIMARY
)
self.powerfeed2 = PowerFeed.objects.create(
power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=POWERFEED_TYPE_REDUNDANT
power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=PowerFeedTypeChoices.TYPE_REDUNDANT
)
self.powerfeed3 = PowerFeed.objects.create(
power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=POWERFEED_TYPE_PRIMARY
power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=PowerFeedTypeChoices.TYPE_PRIMARY
)
self.powerfeed4 = PowerFeed.objects.create(
power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=POWERFEED_TYPE_REDUNDANT
power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=PowerFeedTypeChoices.TYPE_REDUNDANT
)
self.powerfeed5 = PowerFeed.objects.create(
power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=POWERFEED_TYPE_PRIMARY
power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=PowerFeedTypeChoices.TYPE_PRIMARY
)
self.powerfeed6 = PowerFeed.objects.create(
power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=POWERFEED_TYPE_REDUNDANT
power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=PowerFeedTypeChoices.TYPE_REDUNDANT
)
def test_get_powerfeed(self):
@@ -3726,7 +3736,7 @@ class PowerFeedTest(APITestCase):
'name': 'Test Power Feed 4A',
'power_panel': self.powerpanel1.pk,
'rack': self.rack4.pk,
'type': POWERFEED_TYPE_PRIMARY,
'type': PowerFeedTypeChoices.TYPE_PRIMARY,
}
url = reverse('dcim-api:powerfeed-list')
@@ -3746,13 +3756,13 @@ class PowerFeedTest(APITestCase):
'name': 'Test Power Feed 4A',
'power_panel': self.powerpanel1.pk,
'rack': self.rack4.pk,
'type': POWERFEED_TYPE_PRIMARY,
'type': PowerFeedTypeChoices.TYPE_PRIMARY,
},
{
'name': 'Test Power Feed 4B',
'power_panel': self.powerpanel1.pk,
'rack': self.rack4.pk,
'type': POWERFEED_TYPE_REDUNDANT,
'type': PowerFeedTypeChoices.TYPE_REDUNDANT,
},
]
@@ -3769,7 +3779,7 @@ class PowerFeedTest(APITestCase):
data = {
'name': 'Test Power Feed X',
'rack': self.rack4.pk,
'type': POWERFEED_TYPE_REDUNDANT,
'type': PowerFeedTypeChoices.TYPE_REDUNDANT,
}
url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk})

View File

@@ -21,10 +21,10 @@ class DeviceTestCase(TestCase):
'device_type': get_id(DeviceType, 'qfx5100-48s'),
'site': get_id(Site, 'test1'),
'rack': '1',
'face': RACK_FACE_FRONT,
'face': DeviceFaceChoices.FACE_FRONT,
'position': 41,
'platform': get_id(Platform, 'juniper-junos'),
'status': DEVICE_STATUS_ACTIVE,
'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertTrue(test.is_valid(), test.fields['position'].choices)
self.assertTrue(test.save())
@@ -38,10 +38,10 @@ class DeviceTestCase(TestCase):
'device_type': get_id(DeviceType, 'qfx5100-48s'),
'site': get_id(Site, 'test1'),
'rack': '1',
'face': RACK_FACE_FRONT,
'face': DeviceFaceChoices.FACE_FRONT,
'position': 1,
'platform': get_id(Platform, 'juniper-junos'),
'status': DEVICE_STATUS_ACTIVE,
'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertFalse(test.is_valid())
@@ -54,10 +54,10 @@ class DeviceTestCase(TestCase):
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'site': get_id(Site, 'test1'),
'rack': '1',
'face': None,
'face': '',
'position': None,
'platform': None,
'status': DEVICE_STATUS_ACTIVE,
'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertTrue(test.is_valid())
self.assertTrue(test.save())
@@ -71,10 +71,10 @@ class DeviceTestCase(TestCase):
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'site': get_id(Site, 'test1'),
'rack': '1',
'face': RACK_FACE_REAR,
'face': DeviceFaceChoices.FACE_REAR,
'position': None,
'platform': None,
'status': DEVICE_STATUS_ACTIVE,
'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertTrue(test.is_valid())
self.assertTrue(test.save())

View File

@@ -1,6 +1,7 @@
from django.test import TestCase
from dcim.models import *
from tenancy.models import Tenant
class RackTestCase(TestCase):
@@ -87,7 +88,7 @@ class RackTestCase(TestCase):
site=self.site1,
rack=rack1,
position=43,
face=RACK_FACE_FRONT,
face=DeviceFaceChoices.FACE_FRONT,
)
device1.save()
@@ -117,7 +118,7 @@ class RackTestCase(TestCase):
site=self.site1,
rack=self.rack,
position=10,
face=RACK_FACE_REAR,
face=DeviceFaceChoices.FACE_REAR,
)
device1.save()
@@ -125,14 +126,14 @@ class RackTestCase(TestCase):
self.assertEqual(list(self.rack.units), list(reversed(range(1, 43))))
# Validate inventory (front face)
rack1_inventory_front = self.rack.get_front_elevation()
rack1_inventory_front = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_FRONT)
self.assertEqual(rack1_inventory_front[-10]['device'], device1)
del(rack1_inventory_front[-10])
for u in rack1_inventory_front:
self.assertIsNone(u['device'])
# Validate inventory (rear face)
rack1_inventory_rear = self.rack.get_rear_elevation()
rack1_inventory_rear = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_REAR)
self.assertEqual(rack1_inventory_rear[-10]['device'], device1)
del(rack1_inventory_rear[-10])
for u in rack1_inventory_rear:
@@ -146,7 +147,7 @@ class RackTestCase(TestCase):
site=self.site1,
rack=self.rack,
position=None,
face=None,
face='',
)
self.assertTrue(pdu)
@@ -187,20 +188,20 @@ class DeviceTestCase(TestCase):
device_type=self.device_type,
name='Power Outlet 1',
power_port=ppt,
feed_leg=POWERFEED_LEG_A
feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A
).save()
InterfaceTemplate(
device_type=self.device_type,
name='Interface 1',
type=IFACE_TYPE_1GE_FIXED,
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
mgmt_only=True
).save()
rpt = RearPortTemplate(
device_type=self.device_type,
name='Rear Port 1',
type=PORT_TYPE_8P8C,
type=PortTypeChoices.TYPE_8P8C,
positions=8
)
rpt.save()
@@ -208,7 +209,7 @@ class DeviceTestCase(TestCase):
FrontPortTemplate(
device_type=self.device_type,
name='Front Port 1',
type=PORT_TYPE_8P8C,
type=PortTypeChoices.TYPE_8P8C,
rear_port=rpt,
rear_port_position=2
).save()
@@ -251,27 +252,27 @@ class DeviceTestCase(TestCase):
device=d,
name='Power Outlet 1',
power_port=pp,
feed_leg=POWERFEED_LEG_A
feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A
)
Interface.objects.get(
device=d,
name='Interface 1',
type=IFACE_TYPE_1GE_FIXED,
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
mgmt_only=True
)
rp = RearPort.objects.get(
device=d,
name='Rear Port 1',
type=PORT_TYPE_8P8C,
type=PortTypeChoices.TYPE_8P8C,
positions=8
)
FrontPort.objects.get(
device=d,
name='Front Port 1',
type=PORT_TYPE_8P8C,
type=PortTypeChoices.TYPE_8P8C,
rear_port=rp,
rear_port_position=2
)
@@ -281,6 +282,42 @@ class DeviceTestCase(TestCase):
name='Device Bay 1'
)
def test_device_duplicate_name_per_site(self):
device1 = Device(
site=self.site,
device_type=self.device_type,
device_role=self.device_role,
name='Test Device 1'
)
device1.save()
device2 = Device(
site=device1.site,
device_type=device1.device_type,
device_role=device1.device_role,
name=device1.name
)
# Two devices assigned to the same Site and no Tenant should fail validation
with self.assertRaises(ValidationError):
device2.full_clean()
tenant = Tenant.objects.create(name='Test Tenant 1', slug='test-tenant-1')
device1.tenant = tenant
device1.save()
device2.tenant = tenant
# Two devices assigned to the same Site and the same Tenant should fail validation
with self.assertRaises(ValidationError):
device2.full_clean()
device2.tenant = None
# Two devices assigned to the same Site and different Tenants should pass validation
device2.full_clean()
device2.save()
class CableTestCase(TestCase):
@@ -379,7 +416,7 @@ class CableTestCase(TestCase):
"""
A cable cannot terminate to a virtual interface
"""
virtual_interface = Interface(device=self.device1, name="V1", type=IFACE_TYPE_VIRTUAL)
virtual_interface = Interface(device=self.device1, name="V1", type=InterfaceTypeChoices.TYPE_VIRTUAL)
cable = Cable(termination_a=self.interface2, termination_b=virtual_interface)
with self.assertRaises(ValidationError):
cable.clean()
@@ -388,7 +425,7 @@ class CableTestCase(TestCase):
"""
A cable cannot terminate to a wireless interface
"""
wireless_interface = Interface(device=self.device1, name="W1", type=IFACE_TYPE_80211A)
wireless_interface = Interface(device=self.device1, name="W1", type=InterfaceTypeChoices.TYPE_80211A)
cable = Cable(termination_a=self.interface2, termination_b=wireless_interface)
with self.assertRaises(ValidationError):
cable.clean()
@@ -421,16 +458,16 @@ class CablePathTestCase(TestCase):
device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=site
)
self.rear_port1 = RearPort.objects.create(
device=self.panel1, name='Rear Port 1', type=PORT_TYPE_8P8C
device=self.panel1, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C
)
self.front_port1 = FrontPort.objects.create(
device=self.panel1, name='Front Port 1', type=PORT_TYPE_8P8C, rear_port=self.rear_port1
device=self.panel1, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port1
)
self.rear_port2 = RearPort.objects.create(
device=self.panel2, name='Rear Port 2', type=PORT_TYPE_8P8C
device=self.panel2, name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C
)
self.front_port2 = FrontPort.objects.create(
device=self.panel2, name='Front Port 2', type=PORT_TYPE_8P8C, rear_port=self.rear_port2
device=self.panel2, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port2
)
def test_path_completion(self):
@@ -450,7 +487,11 @@ class CablePathTestCase(TestCase):
self.assertIsNone(interface1.connection_status)
# Third segment
cable3 = Cable(termination_a=self.front_port2, termination_b=self.interface2, status=CONNECTION_STATUS_PLANNED)
cable3 = Cable(
termination_a=self.front_port2,
termination_b=self.interface2,
status=CableStatusChoices.STATUS_PLANNED
)
cable3.save()
interface1 = Interface.objects.get(pk=self.interface1.pk)
self.assertEqual(interface1.connected_endpoint, self.interface2)

File diff suppressed because it is too large Load Diff

View File

@@ -82,7 +82,7 @@ urlpatterns = [
# Device types
path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
path(r'device-types/import/', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
path(r'device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'),
path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
path(r'device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
@@ -171,49 +171,58 @@ urlpatterns = [
path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
path(r'devices/<int:pk>/console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
path(r'devices/<int:pk>/console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
path(r'console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
path(r'console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
path(r'console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
path(r'console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
path(r'console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
path(r'console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
# Console server ports
path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
path(r'devices/<int:pk>/console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
path(r'devices/<int:pk>/console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'),
path(r'devices/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
path(r'console-server-ports/', views.ConsoleServerPortListView.as_view(), name='consoleserverport_list'),
path(r'console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
path(r'console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
path(r'console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
path(r'console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
path(r'console-server-ports/import/', views.ConsoleServerPortBulkImportView.as_view(), name='consoleserverport_import'),
# Power ports
path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
path(r'devices/<int:pk>/power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'),
path(r'devices/<int:pk>/power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
path(r'power-ports/', views.PowerPortListView.as_view(), name='powerport_list'),
path(r'power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
path(r'power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
path(r'power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
path(r'power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
path(r'power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
# Power outlets
path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
path(r'devices/<int:pk>/power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
path(r'devices/<int:pk>/power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'),
path(r'devices/<int:pk>/power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
path(r'power-outlets/', views.PowerOutletListView.as_view(), name='poweroutlet_list'),
path(r'power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
path(r'power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
path(r'power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
path(r'power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
path(r'power-outlets/import/', views.PowerOutletBulkImportView.as_view(), name='poweroutlet_import'),
# Interfaces
path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
path(r'devices/<int:pk>/interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
path(r'devices/<int:pk>/interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
path(r'devices/<int:pk>/interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
path(r'interfaces/', views.InterfaceListView.as_view(), name='interface_list'),
path(r'interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
path(r'interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
path(r'interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
@@ -222,40 +231,47 @@ urlpatterns = [
path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
path(r'interfaces/import/', views.InterfaceBulkImportView.as_view(), name='interface_import'),
# Front ports
# path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
path(r'devices/<int:pk>/front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'),
path(r'devices/<int:pk>/front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
path(r'devices/<int:pk>/front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
path(r'front-ports/', views.FrontPortListView.as_view(), name='frontport_list'),
path(r'front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
path(r'front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
path(r'front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
path(r'front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
path(r'front-ports/import/', views.FrontPortBulkImportView.as_view(), name='frontport_import'),
# Rear ports
# path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
path(r'devices/<int:pk>/rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'),
path(r'devices/<int:pk>/rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
path(r'devices/<int:pk>/rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
path(r'rear-ports/', views.RearPortListView.as_view(), name='rearport_list'),
path(r'rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
path(r'rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
path(r'rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
path(r'rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
path(r'rear-ports/import/', views.RearPortBulkImportView.as_view(), name='rearport_import'),
# Device bays
path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
path(r'devices/<int:pk>/bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
path(r'devices/<int:pk>/bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
path(r'device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'),
path(r'device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
path(r'device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
path(r'device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
path(r'device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
path(r'device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'),
# Inventory items
path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'),

View File

@@ -1,3 +1,4 @@
from collections import OrderedDict
import re
from django.conf import settings
@@ -16,8 +17,7 @@ from django.utils.safestring import mark_safe
from django.views.generic import View
from circuits.models import Circuit
from extras.constants import GRAPH_TYPE_DEVICE, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from extras.models import Graph, TopologyMap
from extras.models import Graph
from extras.views import ObjectConfigContextView
from ipam.models import Prefix, VLAN
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
@@ -26,7 +26,7 @@ from utilities.paginator import EnhancedPaginator
from utilities.utils import csv_format
from utilities.views import (
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
ObjectDeleteView, ObjectEditView, ObjectListView,
ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from virtualization.models import VirtualMachine
from . import filters, forms, tables
@@ -208,14 +208,12 @@ class SiteView(PermissionRequiredMixin, View):
'vm_count': VirtualMachine.objects.filter(cluster__site=site).count(),
}
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
topology_maps = TopologyMap.objects.filter(site=site)
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists()
show_graphs = Graph.objects.filter(type__model='site').exists()
return render(request, 'dcim/site.html', {
'site': site,
'stats': stats,
'rack_groups': rack_groups,
'topology_maps': topology_maps,
'show_graphs': show_graphs,
})
@@ -404,8 +402,12 @@ class RackView(PermissionRequiredMixin, View):
position__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer')
next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
if rack.group:
peer_racks = Rack.objects.filter(site=rack.site, group=rack.group)
else:
peer_racks = Rack.objects.filter(site=rack.site, group__isnull=True)
next_rack = peer_racks.filter(name__gt=rack.name).order_by('name').first()
prev_rack = peer_racks.filter(name__lt=rack.name).order_by('-name').first()
reservations = RackReservation.objects.filter(rack=rack)
power_feeds = PowerFeed.objects.filter(rack=rack).prefetch_related('power_panel')
@@ -417,8 +419,6 @@ class RackView(PermissionRequiredMixin, View):
'nonracked_devices': nonracked_devices,
'next_rack': next_rack,
'prev_rack': prev_rack,
'front_elevation': rack.get_front_elevation(),
'rear_elevation': rack.get_rear_elevation(),
})
@@ -655,11 +655,31 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
default_return_url = 'dcim:devicetype_list'
class DeviceTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_devicetype'
model_form = forms.DeviceTypeCSVForm
table = tables.DeviceTypeTable
default_return_url = 'dcim:devicetype_list'
class DeviceTypeImportView(PermissionRequiredMixin, ObjectImportView):
permission_required = [
'dcim.add_devicetype',
'dcim.add_consoleporttemplate',
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
'dcim.add_devicebaytemplate',
]
model = DeviceType
model_form = forms.DeviceTypeImportForm
related_object_forms = OrderedDict((
('console-ports', forms.ConsolePortTemplateImportForm),
('console-server-ports', forms.ConsoleServerPortTemplateImportForm),
('power-ports', forms.PowerPortTemplateImportForm),
('power-outlets', forms.PowerOutletTemplateImportForm),
('interfaces', forms.InterfaceTemplateImportForm),
('rear-ports', forms.RearPortTemplateImportForm),
('front-ports', forms.FrontPortTemplateImportForm),
('device-bays', forms.DeviceBayTemplateImportForm),
))
default_return_url = 'dcim:devicetype_import'
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
@@ -1035,8 +1055,8 @@ class DeviceView(PermissionRequiredMixin, View):
'secrets': secrets,
'vc_members': vc_members,
'related_devices': related_devices,
'show_graphs': Graph.objects.filter(type=GRAPH_TYPE_DEVICE).exists(),
'show_interface_graphs': Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists(),
'show_graphs': Graph.objects.filter(type__model='device').exists(),
'show_interface_graphs': Graph.objects.filter(type__model='interface').exists(),
})
@@ -1174,6 +1194,15 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Console ports
#
class ConsolePortListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_consoleport'
queryset = ConsolePort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.ConsolePortFilter
filter_form = forms.ConsolePortFilterForm
table = tables.ConsolePortDetailTable
template_name = 'dcim/device_component_list.html'
class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleport'
parent_model = Device
@@ -1195,6 +1224,13 @@ class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = ConsolePort
class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_consoleport'
model_form = forms.ConsolePortCSVForm
table = tables.ConsolePortImportTable
default_return_url = 'dcim:consoleport_list'
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleport'
queryset = ConsolePort.objects.all()
@@ -1206,6 +1242,15 @@ class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Console server ports
#
class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_consoleserverport'
queryset = ConsoleServerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.ConsoleServerPortFilter
filter_form = forms.ConsoleServerPortFilterForm
table = tables.ConsoleServerPortDetailTable
template_name = 'dcim/device_component_list.html'
class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleserverport'
parent_model = Device
@@ -1227,6 +1272,13 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = ConsoleServerPort
class ConsoleServerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_consoleserverport'
model_form = forms.ConsoleServerPortCSVForm
table = tables.ConsoleServerPortImportTable
default_return_url = 'dcim:consoleserverport_list'
class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_consoleserverport'
queryset = ConsoleServerPort.objects.all()
@@ -1258,6 +1310,15 @@ class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Power ports
#
class PowerPortListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_powerport'
queryset = PowerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.PowerPortFilter
filter_form = forms.PowerPortFilterForm
table = tables.PowerPortDetailTable
template_name = 'dcim/device_component_list.html'
class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_powerport'
parent_model = Device
@@ -1279,6 +1340,13 @@ class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = PowerPort
class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_powerport'
model_form = forms.PowerPortCSVForm
table = tables.PowerPortImportTable
default_return_url = 'dcim:powerport_list'
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerport'
queryset = PowerPort.objects.all()
@@ -1290,6 +1358,15 @@ class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Power outlets
#
class PowerOutletListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_poweroutlet'
queryset = PowerOutlet.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.PowerOutletFilter
filter_form = forms.PowerOutletFilterForm
table = tables.PowerOutletDetailTable
template_name = 'dcim/device_component_list.html'
class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_poweroutlet'
parent_model = Device
@@ -1311,6 +1388,13 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = PowerOutlet
class PowerOutletBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_poweroutlet'
model_form = forms.PowerOutletCSVForm
table = tables.PowerOutletImportTable
default_return_url = 'dcim:poweroutlet_list'
class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_poweroutlet'
queryset = PowerOutlet.objects.all()
@@ -1342,6 +1426,15 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Interfaces
#
class InterfaceListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_interface'
queryset = Interface.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.InterfaceFilter
filter_form = forms.InterfaceFilterForm
table = tables.InterfaceDetailTable
template_name = 'dcim/device_component_list.html'
class InterfaceView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_interface'
@@ -1400,6 +1493,13 @@ class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = Interface
class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_interface'
model_form = forms.InterfaceCSVForm
table = tables.InterfaceImportTable
default_return_url = 'dcim:interface_list'
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interface'
queryset = Interface.objects.all()
@@ -1431,6 +1531,15 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Front ports
#
class FrontPortListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_frontport'
queryset = FrontPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.FrontPortFilter
filter_form = forms.FrontPortFilterForm
table = tables.FrontPortDetailTable
template_name = 'dcim/device_component_list.html'
class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_frontport'
parent_model = Device
@@ -1452,6 +1561,13 @@ class FrontPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = FrontPort
class FrontPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_frontport'
model_form = forms.FrontPortCSVForm
table = tables.FrontPortImportTable
default_return_url = 'dcim:frontport_list'
class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_frontport'
queryset = FrontPort.objects.all()
@@ -1483,6 +1599,15 @@ class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Rear ports
#
class RearPortListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_rearport'
queryset = RearPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.RearPortFilter
filter_form = forms.RearPortFilterForm
table = tables.RearPortDetailTable
template_name = 'dcim/device_component_list.html'
class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_rearport'
parent_model = Device
@@ -1504,6 +1629,13 @@ class RearPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = RearPort
class RearPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_rearport'
model_form = forms.RearPortCSVForm
table = tables.RearPortImportTable
default_return_url = 'dcim:rearport_list'
class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rearport'
queryset = RearPort.objects.all()
@@ -1535,6 +1667,17 @@ class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Device bays
#
class DeviceBayListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_devicebay'
queryset = DeviceBay.objects.prefetch_related(
'device', 'device__site', 'installed_device', 'installed_device__site'
)
filter = filters.DeviceBayFilter
filter_form = forms.DeviceBayFilterForm
table = tables.DeviceBayDetailTable
template_name = 'dcim/device_component_list.html'
class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_devicebay'
parent_model = Device
@@ -1625,6 +1768,13 @@ class DeviceBayDepopulateView(PermissionRequiredMixin, View):
})
class DeviceBayBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_devicebay'
model_form = forms.DeviceBayCSVForm
table = tables.DeviceBayImportTable
default_return_url = 'dcim:devicebay_list'
class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_devicebay'
queryset = DeviceBay.objects.all()

View File

@@ -1,15 +1 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
default_app_config = 'extras.apps.ExtrasConfig'
# check that django-rq is installed and we can connect to redis
if settings.WEBHOOKS_ENABLED:
try:
import django_rq
except ImportError:
raise ImproperlyConfigured(
"django-rq is not installed! You must install this package per "
"the documentation to use the webhook backend."
)

View File

@@ -3,7 +3,7 @@ from django.contrib import admin
from netbox.admin import admin_site
from utilities.forms import LaxURLField
from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, TopologyMap, Webhook
from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, Webhook
def order_content_types(field):
@@ -40,6 +40,9 @@ class WebhookAdmin(admin.ModelAdmin):
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update',
'type_delete', 'ssl_verification',
]
list_filter = [
'enabled', 'type_create', 'type_update', 'type_delete', 'obj_type',
]
form = WebhookForm
def models(self, obj):
@@ -70,7 +73,12 @@ class CustomFieldChoiceAdmin(admin.TabularInline):
@admin.register(CustomField, site=admin_site)
class CustomFieldAdmin(admin.ModelAdmin):
inlines = [CustomFieldChoiceAdmin]
list_display = ['name', 'models', 'type', 'required', 'filter_logic', 'default', 'weight', 'description']
list_display = [
'name', 'models', 'type', 'required', 'filter_logic', 'default', 'weight', 'description',
]
list_filter = [
'type', 'required', 'obj_type',
]
form = CustomFieldForm
def models(self, obj):
@@ -106,7 +114,12 @@ class CustomLinkForm(forms.ModelForm):
@admin.register(CustomLink, site=admin_site)
class CustomLinkAdmin(admin.ModelAdmin):
list_display = ['name', 'content_type', 'group_name', 'weight']
list_display = [
'name', 'content_type', 'group_name', 'weight',
]
list_filter = [
'content_type',
]
form = CustomLinkForm
@@ -116,7 +129,12 @@ class CustomLinkAdmin(admin.ModelAdmin):
@admin.register(Graph, site=admin_site)
class GraphAdmin(admin.ModelAdmin):
list_display = ['name', 'type', 'weight', 'source']
list_display = [
'name', 'type', 'weight', 'source',
]
list_filter = [
'type',
]
#
@@ -139,17 +157,10 @@ class ExportTemplateForm(forms.ModelForm):
@admin.register(ExportTemplate, site=admin_site)
class ExportTemplateAdmin(admin.ModelAdmin):
list_display = ['name', 'content_type', 'description', 'mime_type', 'file_extension']
list_display = [
'name', 'content_type', 'description', 'mime_type', 'file_extension',
]
list_filter = [
'content_type',
]
form = ExportTemplateForm
#
# Topology maps
#
@admin.register(TopologyMap, site=admin_site)
class TopologyMapAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'site']
prepopulated_fields = {
'slug': ['name'],
}

View File

@@ -5,7 +5,7 @@ from django.db import transaction
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from extras.constants import *
from extras.choices import *
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
from utilities.api import ValidatedModelSerializer
@@ -37,7 +37,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
if value not in [None, '']:
# Validate integer
if cf.type == CF_TYPE_INTEGER:
if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
try:
int(value)
except ValueError:
@@ -46,13 +46,13 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
)
# Validate boolean
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
if cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
raise ValidationError(
"Invalid value for boolean field {}: {}".format(field_name, value)
)
# Validate date
if cf.type == CF_TYPE_DATE:
if cf.type == CustomFieldTypeChoices.TYPE_DATE:
try:
datetime.strptime(value, '%Y-%m-%d')
except ValueError:
@@ -61,7 +61,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
)
# Validate selected choice
if cf.type == CF_TYPE_SELECT:
if cf.type == CustomFieldTypeChoices.TYPE_SELECT:
try:
value = int(value)
except ValueError:
@@ -97,13 +97,13 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
def __init__(self, *args, **kwargs):
def _populate_custom_fields(instance, fields):
custom_fields = {f.name: None for f in fields}
for cfv in instance.custom_field_values.all():
if cfv.field.type == CF_TYPE_SELECT:
custom_fields[cfv.field.name] = CustomFieldChoiceSerializer(cfv.value).data
instance.custom_fields = {}
for field in fields:
value = instance.cf.get(field.name)
if field.type == CustomFieldTypeChoices.TYPE_SELECT and value is not None:
instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data
else:
custom_fields[cfv.field.name] = cfv.value
instance.custom_fields = custom_fields
instance.custom_fields[field.name] = value
super().__init__(*args, **kwargs)

View File

@@ -8,10 +8,10 @@ from dcim.api.nested_serializers import (
NestedRegionSerializer, NestedSiteSerializer,
)
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
from extras.choices import *
from extras.constants import *
from extras.models import (
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
Tag
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
)
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
from tenancy.models import Tenant, TenantGroup
@@ -28,7 +28,9 @@ from .nested_serializers import *
#
class GraphSerializer(ValidatedModelSerializer):
type = ChoiceField(choices=GRAPH_TYPE_CHOICES)
type = ContentTypeField(
queryset=ContentType.objects.all()
)
class Meta:
model = Graph
@@ -38,7 +40,9 @@ class GraphSerializer(ValidatedModelSerializer):
class RenderedGraphSerializer(serializers.ModelSerializer):
embed_url = serializers.SerializerMethodField()
embed_link = serializers.SerializerMethodField()
type = ChoiceField(choices=GRAPH_TYPE_CHOICES)
type = ContentTypeField(
queryset=ContentType.objects.all()
)
class Meta:
model = Graph
@@ -57,8 +61,8 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
class ExportTemplateSerializer(ValidatedModelSerializer):
template_language = ChoiceField(
choices=TEMPLATE_LANGUAGE_CHOICES,
default=TEMPLATE_LANGUAGE_JINJA2
choices=ExportTemplateLanguageChoices,
default=ExportTemplateLanguageChoices.LANGUAGE_JINJA2
)
class Meta:
@@ -69,18 +73,6 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
]
#
# Topology maps
#
class TopologyMapSerializer(ValidatedModelSerializer):
site = NestedSiteSerializer()
class Meta:
model = TopologyMap
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
#
# Tags
#
@@ -181,12 +173,18 @@ class ConfigContextSerializer(ValidatedModelSerializer):
required=False,
many=True
)
tags = serializers.SlugRelatedField(
queryset=Tag.objects.all(),
slug_field='slug',
required=False,
many=True
)
class Meta:
model = ConfigContext
fields = [
'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms',
'tenant_groups', 'tenants', 'data',
'tenant_groups', 'tenants', 'tags', 'data',
]
@@ -213,6 +211,52 @@ class ReportDetailSerializer(ReportSerializer):
result = ReportResultSerializer()
#
# Scripts
#
class ScriptSerializer(serializers.Serializer):
id = serializers.SerializerMethodField(read_only=True)
name = serializers.SerializerMethodField(read_only=True)
description = serializers.SerializerMethodField(read_only=True)
vars = serializers.SerializerMethodField(read_only=True)
def get_id(self, instance):
return '{}.{}'.format(instance.__module__, instance.__name__)
def get_name(self, instance):
return getattr(instance.Meta, 'name', instance.__name__)
def get_description(self, instance):
return getattr(instance.Meta, 'description', '')
def get_vars(self, instance):
return {
k: v.__class__.__name__ for k, v in instance._get_vars().items()
}
class ScriptInputSerializer(serializers.Serializer):
data = serializers.JSONField()
commit = serializers.BooleanField()
class ScriptLogMessageSerializer(serializers.Serializer):
status = serializers.SerializerMethodField(read_only=True)
message = serializers.SerializerMethodField(read_only=True)
def get_status(self, instance):
return LOG_LEVEL_CODES.get(instance[0])
def get_message(self, instance):
return instance[1]
class ScriptOutputSerializer(serializers.Serializer):
log = ScriptLogMessageSerializer(many=True, read_only=True)
output = serializers.CharField(read_only=True)
#
# Change logging
#
@@ -222,7 +266,7 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
read_only=True
)
action = ChoiceField(
choices=OBJECTCHANGE_ACTION_CHOICES,
choices=ObjectChangeActionChoices,
read_only=True
)
changed_object_type = ContentTypeField(

View File

@@ -18,7 +18,7 @@ router.APIRootView = ExtrasRootView
router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
# Custom field choices
router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, base_name='custom-field-choice')
router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice')
# Graphs
router.register(r'graphs', views.GraphViewSet)
@@ -26,9 +26,6 @@ router.register(r'graphs', views.GraphViewSet)
# Export templates
router.register(r'export-templates', views.ExportTemplateViewSet)
# Topology maps
router.register(r'topology-maps', views.TopologyMapViewSet)
# Tags
router.register(r'tags', views.TagViewSet)
@@ -41,6 +38,9 @@ router.register(r'config-contexts', views.ConfigContextViewSet)
# Reports
router.register(r'reports', views.ReportViewSet, basename='report')
# Scripts
router.register(r'scripts', views.ScriptViewSet, basename='script')
# Change logging
router.register(r'object-changes', views.ObjectChangeViewSet)

View File

@@ -2,8 +2,8 @@ from collections import OrderedDict
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.http import Http404
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
@@ -11,10 +11,10 @@ from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
from extras import filters
from extras.models import (
ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
Tag,
ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
)
from extras.reports import get_report, get_reports
from extras.scripts import get_script, get_scripts
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
from . import serializers
@@ -115,34 +115,6 @@ class ExportTemplateViewSet(ModelViewSet):
filterset_class = filters.ExportTemplateFilter
#
# Topology maps
#
class TopologyMapViewSet(ModelViewSet):
queryset = TopologyMap.objects.prefetch_related('site')
serializer_class = serializers.TopologyMapSerializer
filterset_class = filters.TopologyMapFilter
@action(detail=True)
def render(self, request, pk):
tmap = get_object_or_404(TopologyMap, pk=pk)
img_format = 'png'
try:
data = tmap.render(img_format=img_format)
except Exception as e:
return HttpResponse(
"There was an error generating the requested graph: %s" % e
)
response = HttpResponse(data, content_type='image/{}'.format(img_format))
response['Content-Disposition'] = 'inline; filename="{}.{}"'.format(tmap.slug, img_format)
return response
#
# Tags
#
@@ -252,6 +224,56 @@ class ReportViewSet(ViewSet):
return Response(serializer.data)
#
# Scripts
#
class ScriptViewSet(ViewSet):
permission_classes = [IsAuthenticatedOrLoginNotRequired]
_ignore_model_permissions = True
exclude_from_schema = True
lookup_value_regex = '[^/]+' # Allow dots
def _get_script(self, pk):
module_name, script_name = pk.split('.')
script = get_script(module_name, script_name)
if script is None:
raise Http404
return script
def list(self, request):
flat_list = []
for script_list in get_scripts().values():
flat_list.extend(script_list.values())
serializer = serializers.ScriptSerializer(flat_list, many=True, context={'request': request})
return Response(serializer.data)
def retrieve(self, request, pk):
script = self._get_script(pk)
serializer = serializers.ScriptSerializer(script, context={'request': request})
return Response(serializer.data)
def post(self, request, pk):
"""
Run a Script identified as "<module>.<script>".
"""
script = self._get_script(pk)()
input_serializer = serializers.ScriptInputSerializer(data=request.data)
if input_serializer.is_valid():
output = script.run(input_serializer.data['data'])
script.output = output
output_serializer = serializers.ScriptOutputSerializer(script)
return Response(output_serializer.data)
return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
#
# Change logging
#

View File

@@ -1,6 +1,7 @@
from django.apps import AppConfig
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
import redis
class ExtrasConfig(AppConfig):
@@ -10,26 +11,18 @@ class ExtrasConfig(AppConfig):
import extras.signals
# Check that we can connect to the configured Redis database if webhooks are enabled.
if settings.WEBHOOKS_ENABLED:
try:
import redis
except ImportError:
raise ImproperlyConfigured(
"WEBHOOKS_ENABLED is True but the redis Python package is not installed. (Try 'pip install "
"redis'.)"
)
try:
rs = redis.Redis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
db=settings.REDIS_DATABASE,
password=settings.REDIS_PASSWORD or None,
ssl=settings.REDIS_SSL,
)
rs.ping()
except redis.exceptions.ConnectionError:
raise ImproperlyConfigured(
"Unable to connect to the Redis database. Check that the Redis configuration has been defined in "
"configuration.py."
)
# Check that we can connect to the configured Redis database.
try:
rs = redis.Redis(
host=settings.WEBHOOKS_REDIS_HOST,
port=settings.WEBHOOKS_REDIS_PORT,
db=settings.WEBHOOKS_REDIS_DATABASE,
password=settings.WEBHOOKS_REDIS_PASSWORD or None,
ssl=settings.WEBHOOKS_REDIS_SSL,
)
rs.ping()
except redis.exceptions.ConnectionError:
raise ImproperlyConfigured(
"Unable to connect to the Redis database. Check that the Redis configuration has been defined in "
"configuration.py."
)

140
netbox/extras/choices.py Normal file
View File

@@ -0,0 +1,140 @@
from utilities.choices import ChoiceSet
#
# CustomFields
#
class CustomFieldTypeChoices(ChoiceSet):
TYPE_TEXT = 'text'
TYPE_INTEGER = 'integer'
TYPE_BOOLEAN = 'boolean'
TYPE_DATE = 'date'
TYPE_URL = 'url'
TYPE_SELECT = 'select'
CHOICES = (
(TYPE_TEXT, 'Text'),
(TYPE_INTEGER, 'Integer'),
(TYPE_BOOLEAN, 'Boolean (true/false)'),
(TYPE_DATE, 'Date'),
(TYPE_URL, 'URL'),
(TYPE_SELECT, 'Selection'),
)
LEGACY_MAP = {
TYPE_TEXT: 100,
TYPE_INTEGER: 200,
TYPE_BOOLEAN: 300,
TYPE_DATE: 400,
TYPE_URL: 500,
TYPE_SELECT: 600,
}
class CustomFieldFilterLogicChoices(ChoiceSet):
FILTER_DISABLED = 'disabled'
FILTER_LOOSE = 'loose'
FILTER_EXACT = 'exact'
CHOICES = (
(FILTER_DISABLED, 'Disabled'),
(FILTER_LOOSE, 'Loose'),
(FILTER_EXACT, 'Exact'),
)
LEGACY_MAP = {
FILTER_DISABLED: 0,
FILTER_LOOSE: 1,
FILTER_EXACT: 2,
}
#
# CustomLinks
#
class CustomLinkButtonClassChoices(ChoiceSet):
CLASS_DEFAULT = 'default'
CLASS_PRIMARY = 'primary'
CLASS_SUCCESS = 'success'
CLASS_INFO = 'info'
CLASS_WARNING = 'warning'
CLASS_DANGER = 'danger'
CLASS_LINK = 'link'
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)'),
)
#
# ObjectChanges
#
class ObjectChangeActionChoices(ChoiceSet):
ACTION_CREATE = 'create'
ACTION_UPDATE = 'update'
ACTION_DELETE = 'delete'
CHOICES = (
(ACTION_CREATE, 'Created'),
(ACTION_UPDATE, 'Updated'),
(ACTION_DELETE, 'Deleted'),
)
LEGACY_MAP = {
ACTION_CREATE: 1,
ACTION_UPDATE: 2,
ACTION_DELETE: 3,
}
#
# ExportTemplates
#
class ExportTemplateLanguageChoices(ChoiceSet):
LANGUAGE_DJANGO = 'django'
LANGUAGE_JINJA2 = 'jinja2'
CHOICES = (
(LANGUAGE_DJANGO, 'Django'),
(LANGUAGE_JINJA2, 'Jinja2'),
)
LEGACY_MAP = {
LANGUAGE_DJANGO: 10,
LANGUAGE_JINJA2: 20,
}
#
# Webhooks
#
class WebhookContentTypeChoices(ChoiceSet):
CONTENTTYPE_JSON = 'application/json'
CONTENTTYPE_FORMDATA = 'application/x-www-form-urlencoded'
CHOICES = (
(CONTENTTYPE_JSON, 'JSON'),
(CONTENTTYPE_FORMDATA, 'Form data'),
)
LEGACY_MAP = {
CONTENTTYPE_JSON: 1,
CONTENTTYPE_FORMDATA: 2,
}

View File

@@ -19,32 +19,6 @@ CUSTOMFIELD_MODELS = [
'virtualization.virtualmachine',
]
# Custom field types
CF_TYPE_TEXT = 100
CF_TYPE_INTEGER = 200
CF_TYPE_BOOLEAN = 300
CF_TYPE_DATE = 400
CF_TYPE_URL = 500
CF_TYPE_SELECT = 600
CUSTOMFIELD_TYPE_CHOICES = (
(CF_TYPE_TEXT, 'Text'),
(CF_TYPE_INTEGER, 'Integer'),
(CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
(CF_TYPE_DATE, 'Date'),
(CF_TYPE_URL, 'URL'),
(CF_TYPE_SELECT, 'Selection'),
)
# Custom field filter logic choices
CF_FILTER_DISABLED = 0
CF_FILTER_LOOSE = 1
CF_FILTER_EXACT = 2
CF_FILTER_CHOICES = (
(CF_FILTER_DISABLED, 'Disabled'),
(CF_FILTER_LOOSE, 'Loose'),
(CF_FILTER_EXACT, 'Exact'),
)
# Custom links
CUSTOMLINK_MODELS = [
'circuits.circuit',
@@ -68,35 +42,6 @@ CUSTOMLINK_MODELS = [
'virtualization.virtualmachine',
]
BUTTON_CLASS_DEFAULT = 'default'
BUTTON_CLASS_PRIMARY = 'primary'
BUTTON_CLASS_SUCCESS = 'success'
BUTTON_CLASS_INFO = 'info'
BUTTON_CLASS_WARNING = 'warning'
BUTTON_CLASS_DANGER = 'danger'
BUTTON_CLASS_LINK = 'link'
BUTTON_CLASS_CHOICES = (
(BUTTON_CLASS_DEFAULT, 'Default'),
(BUTTON_CLASS_PRIMARY, 'Primary (blue)'),
(BUTTON_CLASS_SUCCESS, 'Success (green)'),
(BUTTON_CLASS_INFO, 'Info (aqua)'),
(BUTTON_CLASS_WARNING, 'Warning (orange)'),
(BUTTON_CLASS_DANGER, 'Danger (red)'),
(BUTTON_CLASS_LINK, 'None (link)'),
)
# Graph types
GRAPH_TYPE_INTERFACE = 100
GRAPH_TYPE_DEVICE = 150
GRAPH_TYPE_PROVIDER = 200
GRAPH_TYPE_SITE = 300
GRAPH_TYPE_CHOICES = (
(GRAPH_TYPE_INTERFACE, 'Interface'),
(GRAPH_TYPE_DEVICE, 'Device'),
(GRAPH_TYPE_PROVIDER, 'Provider'),
(GRAPH_TYPE_SITE, 'Site'),
)
# Models which support export templates
EXPORTTEMPLATE_MODELS = [
'circuits.circuit',
@@ -128,52 +73,6 @@ EXPORTTEMPLATE_MODELS = [
'virtualization.virtualmachine',
]
# ExportTemplate language choices
TEMPLATE_LANGUAGE_DJANGO = 10
TEMPLATE_LANGUAGE_JINJA2 = 20
TEMPLATE_LANGUAGE_CHOICES = (
(TEMPLATE_LANGUAGE_DJANGO, 'Django'),
(TEMPLATE_LANGUAGE_JINJA2, 'Jinja2'),
)
# Topology map types
TOPOLOGYMAP_TYPE_NETWORK = 1
TOPOLOGYMAP_TYPE_CONSOLE = 2
TOPOLOGYMAP_TYPE_POWER = 3
TOPOLOGYMAP_TYPE_CHOICES = (
(TOPOLOGYMAP_TYPE_NETWORK, 'Network'),
(TOPOLOGYMAP_TYPE_CONSOLE, 'Console'),
(TOPOLOGYMAP_TYPE_POWER, 'Power'),
)
# Change log actions
OBJECTCHANGE_ACTION_CREATE = 1
OBJECTCHANGE_ACTION_UPDATE = 2
OBJECTCHANGE_ACTION_DELETE = 3
OBJECTCHANGE_ACTION_CHOICES = (
(OBJECTCHANGE_ACTION_CREATE, 'Created'),
(OBJECTCHANGE_ACTION_UPDATE, 'Updated'),
(OBJECTCHANGE_ACTION_DELETE, 'Deleted'),
)
# User action types
ACTION_CREATE = 1
ACTION_IMPORT = 2
ACTION_EDIT = 3
ACTION_BULK_EDIT = 4
ACTION_DELETE = 5
ACTION_BULK_DELETE = 6
ACTION_BULK_CREATE = 7
ACTION_CHOICES = (
(ACTION_CREATE, 'created'),
(ACTION_BULK_CREATE, 'bulk created'),
(ACTION_IMPORT, 'imported'),
(ACTION_EDIT, 'modified'),
(ACTION_BULK_EDIT, 'bulk edited'),
(ACTION_DELETE, 'deleted'),
(ACTION_BULK_DELETE, 'bulk deleted'),
)
# Report logging levels
LOG_DEFAULT = 0
LOG_SUCCESS = 10
@@ -188,14 +87,6 @@ LOG_LEVEL_CODES = {
LOG_FAILURE: 'failure',
}
# webhook content types
WEBHOOK_CT_JSON = 1
WEBHOOK_CT_X_WWW_FORM_ENCODED = 2
WEBHOOK_CT_CHOICES = (
(WEBHOOK_CT_JSON, 'application/json'),
(WEBHOOK_CT_X_WWW_FORM_ENCODED, 'application/x-www-form-urlencoded'),
)
# Models which support registered webhooks
WEBHOOK_MODELS = [
'circuits.circuit',

View File

@@ -4,8 +4,8 @@ from django.db.models import Q
from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup
from .constants import *
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag, TopologyMap
from .choices import *
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag
class CustomFieldFilter(django_filters.Filter):
@@ -25,7 +25,7 @@ class CustomFieldFilter(django_filters.Filter):
return queryset
# Selection fields get special treatment (values must be integers)
if self.cf_type == CF_TYPE_SELECT:
if self.cf_type == CustomFieldTypeChoices.TYPE_SELECT:
try:
# Treat 0 as None
if int(value) == 0:
@@ -42,7 +42,8 @@ class CustomFieldFilter(django_filters.Filter):
return queryset.none()
# Apply the assigned filter logic (exact or loose)
if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
if (self.cf_type == CustomFieldTypeChoices.TYPE_BOOLEAN or
self.filter_logic == CustomFieldFilterLogicChoices.FILTER_EXACT):
queryset = queryset.filter(
custom_field_values__field__name=self.field_name,
custom_field_values__serialized_value=value
@@ -65,7 +66,11 @@ class CustomFieldFilterSet(django_filters.FilterSet):
super().__init__(*args, **kwargs)
obj_type = ContentType.objects.get_for_model(self._meta.model)
custom_fields = CustomField.objects.filter(obj_type=obj_type).exclude(filter_logic=CF_FILTER_DISABLED)
custom_fields = CustomField.objects.filter(
obj_type=obj_type
).exclude(
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
)
for cf in custom_fields:
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
@@ -103,24 +108,6 @@ class TagFilter(django_filters.FilterSet):
)
class TopologyMapFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='site',
queryset=Site.objects.all(),
label='Site',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
class Meta:
model = TopologyMap
fields = ['name', 'slug']
class ConfigContextFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
@@ -192,6 +179,12 @@ class ConfigContextFilter(django_filters.FilterSet):
to_field_name='slug',
label='Tenant (slug)',
)
tag = django_filters.ModelMultipleChoiceFilter(
field_name='tags__slug',
queryset=Tag.objects.all(),
to_field_name='slug',
label='Tag (slug)',
)
class Meta:
model = ConfigContext
@@ -241,3 +234,24 @@ class ObjectChangeFilter(django_filters.FilterSet):
Q(user_name__icontains=value) |
Q(object_repr__icontains=value)
)
class CreatedUpdatedFilterSet(django_filters.FilterSet):
created = django_filters.DateFilter()
created__gte = django_filters.DateFilter(
field_name='created',
lookup_expr='gte'
)
created__lte = django_filters.DateFilter(
field_name='created',
lookup_expr='lte'
)
last_updated = django_filters.DateTimeFilter()
last_updated__gte = django_filters.DateTimeFilter(
field_name='last_updated',
lookup_expr='gte'
)
last_updated__lte = django_filters.DateTimeFilter(
field_name='last_updated',
lookup_expr='lte'
)

View File

@@ -13,7 +13,7 @@ from utilities.forms import (
CommentField, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, StaticSelect2,
BOOLEAN_WITH_BLANK_CHOICES,
)
from .constants import *
from .choices import *
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
@@ -28,18 +28,18 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
field_dict = OrderedDict()
custom_fields = CustomField.objects.filter(obj_type=content_type)
if filterable_only:
custom_fields = custom_fields.exclude(filter_logic=CF_FILTER_DISABLED)
custom_fields = custom_fields.exclude(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED)
for cf in custom_fields:
field_name = 'cf_{}'.format(str(cf.name))
initial = cf.default if not bulk_edit else None
# Integer
if cf.type == CF_TYPE_INTEGER:
if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
field = forms.IntegerField(required=cf.required, initial=initial)
# Boolean
elif cf.type == CF_TYPE_BOOLEAN:
elif cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
choices = (
(None, '---------'),
(1, 'True'),
@@ -56,11 +56,11 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
)
# Date
elif cf.type == CF_TYPE_DATE:
elif cf.type == CustomFieldTypeChoices.TYPE_DATE:
field = forms.DateField(required=cf.required, initial=initial, help_text="Date format: YYYY-MM-DD")
# Select
elif cf.type == CF_TYPE_SELECT:
elif cf.type == CustomFieldTypeChoices.TYPE_SELECT:
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
if not cf.required or bulk_edit or filterable_only:
choices = [(None, '---------')] + choices
@@ -74,7 +74,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice)
# URL
elif cf.type == CF_TYPE_URL:
elif cf.type == CustomFieldTypeChoices.TYPE_URL:
field = LaxURLField(required=cf.required, initial=initial)
# Text
@@ -237,6 +237,14 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm):
#
class ConfigContextForm(BootstrapMixin, forms.ModelForm):
tags = forms.ModelMultipleChoiceField(
queryset=Tag.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/extras/tags/"
)
)
data = JSONField(
label=''
)
@@ -245,7 +253,7 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
model = ConfigContext
fields = [
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups',
'tenants', 'data',
'tenants', 'tags', 'data',
]
widgets = {
'regions': APISelectMultiple(
@@ -265,7 +273,7 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
),
'tenants': APISelectMultiple(
api_url="/api/tenancy/tenants/"
)
),
}
@@ -346,6 +354,14 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
value_field="slug",
)
)
tag = FilterChoiceField(
queryset=Tag.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/extras/tags/",
value_field="slug",
)
)
#
@@ -400,7 +416,7 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
)
)
action = forms.ChoiceField(
choices=add_blank_choice(OBJECTCHANGE_ACTION_CHOICES),
choices=add_blank_choice(ObjectChangeActionChoices),
required=False
)
user = forms.ModelChoiceField(

View File

@@ -1,15 +1,17 @@
import random
import threading
import uuid
from copy import deepcopy
from datetime import timedelta
from django.conf import settings
from django.db.models.signals import post_delete, post_save
from django.db.models.signals import pre_delete, post_save
from django.utils import timezone
from django.utils.functional import curry
from django_prometheus.models import model_deletes, model_inserts, model_updates
from .constants import *
from extras.utils import is_taggable
from utilities.querysets import DummyQuerySet
from .choices import ObjectChangeActionChoices
from .models import ObjectChange
from .signals import purge_changelog
from .webhooks import enqueue_webhooks
@@ -19,33 +21,34 @@ _thread_locals = threading.local()
def handle_changed_object(sender, instance, **kwargs):
"""
Fires when an object is created or updated
Fires when an object is created or updated.
"""
# Queue the object and a new ObjectChange for processing once the request completes
if hasattr(instance, 'to_objectchange'):
action = OBJECTCHANGE_ACTION_CREATE if kwargs['created'] else OBJECTCHANGE_ACTION_UPDATE
objectchange = instance.to_objectchange(action)
_thread_locals.changed_objects.append(
(instance, objectchange)
)
# Queue the object for processing once the request completes
action = ObjectChangeActionChoices.ACTION_CREATE if kwargs['created'] else ObjectChangeActionChoices.ACTION_UPDATE
_thread_locals.changed_objects.append(
(instance, action)
)
def _handle_deleted_object(request, sender, instance, **kwargs):
def handle_deleted_object(sender, instance, **kwargs):
"""
Fires when an object is deleted
Fires when an object is deleted.
"""
# Record an Object Change
if hasattr(instance, 'to_objectchange'):
objectchange = instance.to_objectchange(OBJECTCHANGE_ACTION_DELETE)
objectchange.user = request.user
objectchange.request_id = request.id
objectchange.save()
# Cache custom fields prior to copying the instance
if hasattr(instance, 'cache_custom_fields'):
instance.cache_custom_fields()
# Enqueue webhooks
enqueue_webhooks(instance, request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
# Create a copy of the object being deleted
copy = deepcopy(instance)
# Increment metric counters
model_deletes.labels(instance._meta.model_name).inc()
# Preserve tags
if is_taggable(instance):
copy.tags = DummyQuerySet(instance.tags.all())
# Queue the copy of the object for processing once the request completes
_thread_locals.changed_objects.append(
(copy, ObjectChangeActionChoices.ACTION_DELETE)
)
def purge_objectchange_cache(sender, **kwargs):
@@ -81,12 +84,9 @@ class ObjectChangeMiddleware(object):
# the same request.
request.id = uuid.uuid4()
# Signals don't include the request context, so we're currying it into the post_delete function ahead of time.
handle_deleted_object = curry(_handle_deleted_object, request)
# Connect our receivers to the post_save and post_delete signals.
post_save.connect(handle_changed_object, dispatch_uid='cache_changed_object')
post_delete.connect(handle_deleted_object, dispatch_uid='cache_deleted_object')
post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object')
pre_delete.connect(handle_deleted_object, dispatch_uid='handle_deleted_object')
# Provide a hook for purging the change cache
purge_changelog.connect(purge_objectchange_cache)
@@ -98,22 +98,31 @@ class ObjectChangeMiddleware(object):
if not _thread_locals.changed_objects:
return response
# Create records for any cached objects that were created/updated.
for obj, objectchange in _thread_locals.changed_objects:
# Create records for any cached objects that were changed.
for instance, action in _thread_locals.changed_objects:
# Record the change
objectchange.user = request.user
objectchange.request_id = request.id
objectchange.save()
# Refresh cached custom field values
if action in [ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE]:
if hasattr(instance, 'cache_custom_fields'):
instance.cache_custom_fields()
# Record an ObjectChange if applicable
if hasattr(instance, 'to_objectchange'):
objectchange = instance.to_objectchange(action)
objectchange.user = request.user
objectchange.request_id = request.id
objectchange.save()
# Enqueue webhooks
enqueue_webhooks(obj, request.user, request.id, objectchange.action)
enqueue_webhooks(instance, request.user, request.id, action)
# Increment metric counters
if objectchange.action == OBJECTCHANGE_ACTION_CREATE:
model_inserts.labels(obj._meta.model_name).inc()
elif objectchange.action == OBJECTCHANGE_ACTION_UPDATE:
model_updates.labels(obj._meta.model_name).inc()
if action == ObjectChangeActionChoices.ACTION_CREATE:
model_inserts.labels(instance._meta.model_name).inc()
elif action == ObjectChangeActionChoices.ACTION_UPDATE:
model_updates.labels(instance._meta.model_name).inc()
elif action == ObjectChangeActionChoices.ACTION_DELETE:
model_deletes.labels(instance._meta.model_name).inc()
# Housekeeping: 1% chance of clearing out expired ObjectChanges. This applies only to requests which result in
# one or more changes being logged.

View File

@@ -8,8 +8,6 @@ import django.db.models.deletion
import extras.models
from django.db.utils import OperationalError
from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT
def verify_postgresql_version(apps, schema_editor):
"""

View File

@@ -2,21 +2,19 @@
# Generated by Django 1.11.9 on 2018-02-21 19:48
from django.db import migrations, models
from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT
def is_filterable_to_filter_logic(apps, schema_editor):
CustomField = apps.get_model('extras', 'CustomField')
CustomField.objects.filter(is_filterable=False).update(filter_logic=CF_FILTER_DISABLED)
CustomField.objects.filter(is_filterable=True).update(filter_logic=CF_FILTER_LOOSE)
CustomField.objects.filter(is_filterable=False).update(filter_logic=0)
CustomField.objects.filter(is_filterable=True).update(filter_logic=1)
# Select fields match on primary key only
CustomField.objects.filter(is_filterable=True, type=CF_TYPE_SELECT).update(filter_logic=CF_FILTER_EXACT)
CustomField.objects.filter(is_filterable=True, type=600).update(filter_logic=2)
def filter_logic_to_is_filterable(apps, schema_editor):
CustomField = apps.get_model('extras', 'CustomField')
CustomField.objects.filter(filter_logic=CF_FILTER_DISABLED).update(is_filterable=False)
CustomField.objects.exclude(filter_logic=CF_FILTER_DISABLED).update(is_filterable=True)
CustomField.objects.filter(filter_logic=0).update(is_filterable=False)
CustomField.objects.exclude(filter_logic=0).update(is_filterable=True)
class Migration(migrations.Migration):

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2 on 2019-10-13 05:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('extras', '0025_objectchange_time_index'),
]
operations = [
migrations.AddField(
model_name='webhook',
name='ca_file_path',
field=models.CharField(blank=True, max_length=4096, null=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.2 on 2019-10-13 07:06
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('extras', '0026_webhook_ca_file_path'),
]
operations = [
migrations.AddField(
model_name='webhook',
name='additional_headers',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,16 @@
# Generated by Django 2.2 on 2019-08-09 01:26
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('extras', '0027_webhook_additional_headers'),
]
operations = [
migrations.DeleteModel(
name='TopologyMap',
),
]

View File

@@ -0,0 +1,69 @@
from django.db import migrations, models
import django.db.models.deletion
CUSTOMFIELD_TYPE_CHOICES = (
(100, 'text'),
(200, 'integer'),
(300, 'boolean'),
(400, 'date'),
(500, 'url'),
(600, 'select')
)
CUSTOMFIELD_FILTER_LOGIC_CHOICES = (
(0, 'disabled'),
(1, 'integer'),
(2, 'exact'),
)
def customfield_type_to_slug(apps, schema_editor):
CustomField = apps.get_model('extras', 'CustomField')
for id, slug in CUSTOMFIELD_TYPE_CHOICES:
CustomField.objects.filter(type=str(id)).update(type=slug)
def customfield_filter_logic_to_slug(apps, schema_editor):
CustomField = apps.get_model('extras', 'CustomField')
for id, slug in CUSTOMFIELD_FILTER_LOGIC_CHOICES:
CustomField.objects.filter(filter_logic=str(id)).update(filter_logic=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('extras', '0028_remove_topology_maps'),
]
operations = [
# CustomField.type
migrations.AlterField(
model_name='customfield',
name='type',
field=models.CharField(default='text', max_length=50),
),
migrations.RunPython(
code=customfield_type_to_slug
),
# Update CustomFieldChoice.field.limit_choices_to
migrations.AlterField(
model_name='customfieldchoice',
name='field',
field=models.ForeignKey(limit_choices_to={'type': 'select'}, on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField'),
),
# CustomField.filter_logic
migrations.AlterField(
model_name='customfield',
name='filter_logic',
field=models.CharField(default='loose', max_length=50),
),
migrations.RunPython(
code=customfield_filter_logic_to_slug
),
]

View File

@@ -0,0 +1,36 @@
from django.db import migrations, models
OBJECTCHANGE_ACTION_CHOICES = (
(1, 'create'),
(2, 'update'),
(3, 'delete'),
)
def objectchange_action_to_slug(apps, schema_editor):
ObjectChange = apps.get_model('extras', 'ObjectChange')
for id, slug in OBJECTCHANGE_ACTION_CHOICES:
ObjectChange.objects.filter(action=str(id)).update(action=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('extras', '0029_3569_customfield_fields'),
]
operations = [
# ObjectChange.action
migrations.AlterField(
model_name='objectchange',
name='action',
field=models.CharField(max_length=50),
),
migrations.RunPython(
code=objectchange_action_to_slug
),
]

View File

@@ -0,0 +1,35 @@
from django.db import migrations, models
EXPORTTEMPLATE_LANGUAGE_CHOICES = (
(10, 'django'),
(20, 'jinja2'),
)
def exporttemplate_language_to_slug(apps, schema_editor):
ExportTemplate = apps.get_model('extras', 'ExportTemplate')
for id, slug in EXPORTTEMPLATE_LANGUAGE_CHOICES:
ExportTemplate.objects.filter(template_language=str(id)).update(template_language=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('extras', '0030_3569_objectchange_fields'),
]
operations = [
# ExportTemplate.template_language
migrations.AlterField(
model_name='exporttemplate',
name='template_language',
field=models.CharField(default='jinja2', max_length=50),
),
migrations.RunPython(
code=exporttemplate_language_to_slug
),
]

View File

@@ -0,0 +1,35 @@
from django.db import migrations, models
WEBHOOK_CONTENTTYPE_CHOICES = (
(1, 'application/json'),
(2, 'application/x-www-form-urlencoded'),
)
def webhook_contenttype_to_slug(apps, schema_editor):
Webhook = apps.get_model('extras', 'Webhook')
for id, slug in WEBHOOK_CONTENTTYPE_CHOICES:
Webhook.objects.filter(http_content_type=str(id)).update(http_content_type=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('extras', '0031_3569_exporttemplate_fields'),
]
operations = [
# Webhook.http_content_type
migrations.AlterField(
model_name='webhook',
name='http_content_type',
field=models.CharField(default='application/json', max_length=50),
),
migrations.RunPython(
code=webhook_contenttype_to_slug
),
]

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