mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-03 02:57:45 -06:00
Compare commits
192 Commits
v2.4-beta1
...
v2.4.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcf22831e2 | ||
|
|
cde6e9757b | ||
|
|
f2d9a3e0a1 | ||
|
|
b917e8d3b0 | ||
|
|
3b26ce6501 | ||
|
|
1b2d3bf08b | ||
|
|
492bc9f86e | ||
|
|
a457a73826 | ||
|
|
ac36339491 | ||
|
|
dbbf7ab664 | ||
|
|
66400a98f1 | ||
|
|
aa50e2e385 | ||
|
|
118b8db209 | ||
|
|
967feb6931 | ||
|
|
e1e41a768a | ||
|
|
c333af33dc | ||
|
|
9e5b482b1d | ||
|
|
771747147c | ||
|
|
bc49979243 | ||
|
|
d46b3e2446 | ||
|
|
2804d89c5e | ||
|
|
fd32a71131 | ||
|
|
1556fd0e92 | ||
|
|
5dce7c4e48 | ||
|
|
4bfc32ec99 | ||
|
|
ff65f7fd7b | ||
|
|
cd2aee3053 | ||
|
|
f224ad2959 | ||
|
|
9d9318f38a | ||
|
|
f43d861b50 | ||
|
|
17714b0c12 | ||
|
|
9914576eaa | ||
|
|
bf8eff11ea | ||
|
|
a6c78b99c4 | ||
|
|
6a56ffc650 | ||
|
|
05059606c5 | ||
|
|
a2ff21fab9 | ||
|
|
134370f48d | ||
|
|
c7fa610842 | ||
|
|
242cb7c7cb | ||
|
|
edb49c7f0a | ||
|
|
3e0a7e7f8a | ||
|
|
cfab9a6a0a | ||
|
|
91b5f6d799 | ||
|
|
d5488ca7da | ||
|
|
f9911bff0d | ||
|
|
d5239191fe | ||
|
|
db7148350e | ||
|
|
c51c20a301 | ||
|
|
f4485dc72a | ||
|
|
f59682a7c9 | ||
|
|
507a023f41 | ||
|
|
ea7386b04b | ||
|
|
81479ac177 | ||
|
|
c7acddbc5c | ||
|
|
1905516536 | ||
|
|
64f34d9cd7 | ||
|
|
98bdb0cb3c | ||
|
|
bba88b2be4 | ||
|
|
12dfd4b6e0 | ||
|
|
209e721219 | ||
|
|
7a27dbb374 | ||
|
|
d039b9e23d | ||
|
|
af796871aa | ||
|
|
d8580d6174 | ||
|
|
24520717e4 | ||
|
|
bd5e860be0 | ||
|
|
1bdfcd1dbe | ||
|
|
b4f29978b3 | ||
|
|
f1bc88fc0c | ||
|
|
aee01af2a1 | ||
|
|
ab37264ae1 | ||
|
|
78b8a582f8 | ||
|
|
42a970e452 | ||
|
|
8f127e86ac | ||
|
|
33e45a5292 | ||
|
|
475e0e6c1e | ||
|
|
5e97f87a64 | ||
|
|
4ae7f2337a | ||
|
|
5e5b9683f9 | ||
|
|
0b971aaf84 | ||
|
|
2f33e9724d | ||
|
|
5f7e21faa0 | ||
|
|
d4ccaf60cf | ||
|
|
27e8841a6d | ||
|
|
45ab08aa76 | ||
|
|
c5339c255a | ||
|
|
d6ce4fc9f3 | ||
|
|
5ea41fa7f9 | ||
|
|
249c3d0e81 | ||
|
|
722d0d5554 | ||
|
|
9876a2efcd | ||
|
|
f7aa259995 | ||
|
|
f664998d9b | ||
|
|
c2416411c1 | ||
|
|
524da5a2ff | ||
|
|
838af2b2d8 | ||
|
|
64e86013bf | ||
|
|
a1e8397b6b | ||
|
|
7476f522e1 | ||
|
|
acb06ee4f1 | ||
|
|
674a0d481e | ||
|
|
40efa55ec7 | ||
|
|
3166d12994 | ||
|
|
58a97b4e39 | ||
|
|
7b8051d42f | ||
|
|
55f93184d3 | ||
|
|
684a0d9e36 | ||
|
|
a85e6370a8 | ||
|
|
0497539ef2 | ||
|
|
431361efad | ||
|
|
e82bf66a76 | ||
|
|
c8a73b5b15 | ||
|
|
b518258e6d | ||
|
|
a1d45023ab | ||
|
|
b7c2a26155 | ||
|
|
86a67e7f32 | ||
|
|
ba3ae0d80a | ||
|
|
beac676a6e | ||
|
|
97b6038be2 | ||
|
|
0907a6ce5b | ||
|
|
9eb433a4ef | ||
|
|
d04727f4b5 | ||
|
|
206c465e02 | ||
|
|
688c421c55 | ||
|
|
93ce0ce670 | ||
|
|
c2573774bf | ||
|
|
6e037e91d3 | ||
|
|
d665d4d62a | ||
|
|
29d9b32b67 | ||
|
|
00d218118c | ||
|
|
02b6ffd59a | ||
|
|
aa0e4406eb | ||
|
|
d0349f00ad | ||
|
|
d77214fef6 | ||
|
|
09a03565d7 | ||
|
|
6159994552 | ||
|
|
82189de78e | ||
|
|
a1f624c1cc | ||
|
|
328958876a | ||
|
|
68f73c7f94 | ||
|
|
ec4d28ac6c | ||
|
|
957074a134 | ||
|
|
c4f7e8121a | ||
|
|
6436d703f5 | ||
|
|
ec0cb7a8bc | ||
|
|
e98f0c39d1 | ||
|
|
50a451eddc | ||
|
|
a5a7358d26 | ||
|
|
f9452163c5 | ||
|
|
3067c3f262 | ||
|
|
7a64404299 | ||
|
|
2bda399982 | ||
|
|
74731bc6ae | ||
|
|
7cb287d6c6 | ||
|
|
aa8f734bd1 | ||
|
|
f6d1163ddd | ||
|
|
5be30bd278 | ||
|
|
fa7b7288c9 | ||
|
|
9cc03aaa9a | ||
|
|
1bda56ea23 | ||
|
|
64a34ced72 | ||
|
|
e05d379101 | ||
|
|
a355783377 | ||
|
|
88239e0b0d | ||
|
|
5c63a499d5 | ||
|
|
50496b1a59 | ||
|
|
f7b0d22f86 | ||
|
|
ad95b86fdd | ||
|
|
43e1e0dbc8 | ||
|
|
f731900e2f | ||
|
|
b1bcaa33e7 | ||
|
|
17873706b7 | ||
|
|
e0ad2b4555 | ||
|
|
f89d91783b | ||
|
|
3ffe36e5ed | ||
|
|
be393a9d10 | ||
|
|
27eefd8705 | ||
|
|
097e0f38ff | ||
|
|
ce26b566a4 | ||
|
|
0e14bc1e02 | ||
|
|
ce6796ed9b | ||
|
|
c90cecc2fb | ||
|
|
b6bbcb0609 | ||
|
|
23f6832d9c | ||
|
|
88dace75a1 | ||
|
|
8eb140fd65 | ||
|
|
1f09f3d096 | ||
|
|
66be85a41f | ||
|
|
814c11167e | ||
|
|
57ddd5086f | ||
|
|
c171547037 |
49
.github/ISSUE_TEMPLATE.md
vendored
49
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,49 +0,0 @@
|
||||
<!--
|
||||
Before opening a new issue, please search through the existing issues to
|
||||
see if your topic has already been addressed. Note that you may need to
|
||||
remove the "is:open" filter from the search bar to include closed issues.
|
||||
|
||||
Check the appropriate type for your issue below by placing an x between the
|
||||
brackets. For assistance with installation issues, or for any other issues
|
||||
other than those listed below, please raise your topic for discussion on
|
||||
our mailing list:
|
||||
|
||||
https://groups.google.com/forum/#!forum/netbox-discuss
|
||||
|
||||
Please note that issues which do not fall under any of the below categories
|
||||
will be closed. Due to an excessive backlog of feature requests, we are
|
||||
not currently accepting any proposals which extend NetBox's feature scope.
|
||||
|
||||
Do not prepend any sort of tag to your issue's title. An administrator will
|
||||
review your issue and assign labels as appropriate.
|
||||
--->
|
||||
### Issue type
|
||||
[ ] Feature request <!-- An enhancement of existing functionality -->
|
||||
[ ] Bug report <!-- Unexpected or erroneous behavior -->
|
||||
[ ] Documentation <!-- A modification to the documentation -->
|
||||
[ ] Housekeeping <!-- Changes pertaining to the codebase itself -->
|
||||
|
||||
<!--
|
||||
Please describe the environment in which you are running NetBox. (Be sure
|
||||
to verify that you are running the latest stable release of NetBox before
|
||||
submitting a bug report.) If you are submitting a bug report and have made
|
||||
any changes to the code base, please first validate that your bug can be
|
||||
recreated while running an official release.
|
||||
-->
|
||||
### Environment
|
||||
* Python version: <!-- Example: 3.5.4 -->
|
||||
* NetBox version: <!-- Example: 2.3.5 -->
|
||||
|
||||
<!--
|
||||
BUG REPORTS must include:
|
||||
* A list of the steps needed for someone else to reproduce the bug
|
||||
* A description of the expected and observed behavior
|
||||
* Any relevant error messages (screenshots may also help)
|
||||
|
||||
FEATURE REQUESTS must include:
|
||||
* A detailed description of the proposed functionality
|
||||
* A use case for the new feature
|
||||
* A rough description of any necessary changes to the database schema
|
||||
* Any relevant third-party libraries which would be needed
|
||||
-->
|
||||
### Description
|
||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: 🐛 Bug Report
|
||||
about: Report a reproducible bug in the current release of NetBox
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
NOTE: This form is only for reproducible bugs. If you need assistance with
|
||||
NetBox installation, or if you have a general question, DO NOT open an
|
||||
issue. Instead, post to our mailing list:
|
||||
|
||||
https://groups.google.com/forum/#!forum/netbox-discuss
|
||||
|
||||
Please describe the environment in which you are running NetBox. Be sure
|
||||
that you are running an unmodified instance of the latest stable release
|
||||
before submitting a bug report.
|
||||
-->
|
||||
### Environment
|
||||
* Python version: <!-- Example: 3.5.4 -->
|
||||
* NetBox version: <!-- Example: 2.3.6 -->
|
||||
|
||||
<!--
|
||||
Describe in detail the steps that someone else can take to reproduce this
|
||||
bug using the current stable release of NetBox (or the current beta release
|
||||
where applicable).
|
||||
-->
|
||||
### Steps to Reproduce
|
||||
|
||||
|
||||
<!-- What did you expect to happen? -->
|
||||
### Expected Behavior
|
||||
|
||||
|
||||
<!-- What happened instead? -->
|
||||
### Observed Behavior
|
||||
18
.github/ISSUE_TEMPLATE/documentation_change.md
vendored
Normal file
18
.github/ISSUE_TEMPLATE/documentation_change.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: 📖 Documentation Change
|
||||
about: Suggest an addition or modification to the NetBox documentation
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Please indicate the nature of the change by placing an X in one of the
|
||||
boxes below.
|
||||
-->
|
||||
### Change Type
|
||||
[ ] Addition
|
||||
[ ] Correction
|
||||
[ ] Deprecation
|
||||
[ ] Cleanup (formatting, typos, etc.)
|
||||
|
||||
<!-- Describe the proposed change(s). -->
|
||||
### Proposed Changes
|
||||
54
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
54
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: ✨ Feature Request
|
||||
about: Propose a new NetBox feature or enhancement
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
NOTE: This form is only for proposing specific new features or enhancements.
|
||||
If you have a general idea or question, please post to our mailing list
|
||||
instead of opening an issue:
|
||||
|
||||
https://groups.google.com/forum/#!forum/netbox-discuss
|
||||
|
||||
NOTE: Due to an excessive backlog of feature requests, we are not currently
|
||||
accepting any proposals which significantly extend NetBox's feature scope.
|
||||
|
||||
Please describe the environment in which you are running NetBox. Be sure
|
||||
that you are running an unmodified instance of the latest stable release
|
||||
before submitting a bug report.
|
||||
-->
|
||||
### Environment
|
||||
* Python version: <!-- Example: 3.5.4 -->
|
||||
* NetBox version: <!-- Example: 2.3.6 -->
|
||||
|
||||
<!--
|
||||
Describe in detail the new functionality you are proposing. Include any
|
||||
specific changes to work flows, data models, or the user interface.
|
||||
-->
|
||||
### Proposed Functionality
|
||||
|
||||
|
||||
<!--
|
||||
Convey an example use case for your proposed feature. Write from the
|
||||
perspective of a NetBox user who would benefit from the proposed
|
||||
functionality and describe how.
|
||||
--->
|
||||
### Use Case
|
||||
|
||||
|
||||
<!--
|
||||
Note any changes to the database schema necessary to support the new
|
||||
feature. For example, does the proposal require adding a new model or
|
||||
field? (Not all new features require database changes.)
|
||||
--->
|
||||
### Database Changes
|
||||
|
||||
|
||||
<!--
|
||||
List any new dependencies on external libraries or services that this new
|
||||
feature would introduce. For example, does the proposal require the
|
||||
installation of a new Python package? (Not all new features introduce new
|
||||
dependencies.)
|
||||
-->
|
||||
### External Dependencies
|
||||
17
.github/ISSUE_TEMPLATE/housekeeping.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/housekeeping.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: 🏡 Housekeeping
|
||||
about: A change pertaining to the codebase itself
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
NOTE: This type of issue should be opened only by those reasonably familiar
|
||||
with NetBox's code base and interested in contributing to its development.
|
||||
|
||||
Describe the proposed change(s) in detail.
|
||||
-->
|
||||
### Proposed Changes
|
||||
|
||||
|
||||
<!-- Provide justification for the proposed change(s). -->
|
||||
### Justification
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -6,6 +6,8 @@
|
||||
be able to accept.
|
||||
|
||||
Please indicate the relevant feature request or bug report below.
|
||||
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ACCEPTED BUG REPORT OR
|
||||
FEATURE REQUEST, IT WILL BE MARKED AS INVALID AND CLOSED.
|
||||
-->
|
||||
### Fixes:
|
||||
|
||||
|
||||
1654
CHANGELOG.md
Normal file
1654
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -91,11 +91,13 @@ appropriate labels will be applied for categorization.
|
||||
|
||||
## Submitting Pull Requests
|
||||
|
||||
* Be sure to open an issue before starting work on a pull request, and discuss
|
||||
your idea with the NetBox maintainers before beginning work. This will help
|
||||
prevent wasting time on something that might we might not be able to implement.
|
||||
When suggesting a new feature, also make sure it won't conflict with any work
|
||||
that's already in progress.
|
||||
* Be sure to open an issue **before** starting work on a pull request, and
|
||||
discuss your idea with the NetBox maintainers before beginning work. This will
|
||||
help prevent wasting time on something that might we might not be able to
|
||||
implement. When suggesting a new feature, also make sure it won't conflict with
|
||||
any work that's already in progress.
|
||||
|
||||
* Any pull request which does _not_ relate to an accepted issue will be closed.
|
||||
|
||||
* 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
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
Django
|
||||
# django-filter-1.1.0 breaks with Django-2.1
|
||||
Django>=1.11,<2.1
|
||||
django-cors-headers
|
||||
django-debug-toolbar
|
||||
# django-filter-2.0.0 drops Python 2 support (blocked by #2000)
|
||||
django-filter==1.1.0
|
||||
django-mptt
|
||||
django-tables2
|
||||
django-taggit
|
||||
django-taggit-serializer
|
||||
django-timezone-field
|
||||
# https://github.com/encode/django-rest-framework/issues/6053
|
||||
djangorestframework==3.8.1
|
||||
drf-yasg[validation]
|
||||
graphviz
|
||||
|
||||
9
docs/additional-features/change-logging.md
Normal file
9
docs/additional-features/change-logging.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Change Logging
|
||||
|
||||
Every time an object in NetBox is created, updated, or deleted, a serialized copy of that object is saved to the database, along with meta data including the current time and the user associated with the change. These records form a running changelog both for each individual object as well as NetBox as a whole (Organization > Changelog).
|
||||
|
||||
A serialized representation is included for each object in JSON format. This is similar to how objects are conveyed within the REST API, but does not include any nested representations. For instance, the `tenant` field of a site will record only the tenant's ID, not a representation of the tenant.
|
||||
|
||||
When a request is made, a random request ID is generated and attached to any change records resulting from the request. For example, editing multiple objects in bulk will create a change record for each object, and each of those objects will be assigned the same request ID. This makes it easy to identify all the change records associated with a particular request.
|
||||
|
||||
Change records are exposed in the API via the read-only endpoint `/api/extras/object-changes/`. They may also be exported in CSV format.
|
||||
3
docs/additional-features/context-data.md
Normal file
3
docs/additional-features/context-data.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Contextual Configuration Data
|
||||
|
||||
Sometimes it is desirable to associate arbitrary data with a group of devices to aid in their configuration. For example, you might want to associate a set of syslog servers for all devices at a particular site. Context data enables the association of arbitrary data to devices and virtual machines grouped by region, site, role, platform, and/or tenant. Context data is arranged hierarchically, so that data with a higher weight can be entered to override more general lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object.
|
||||
26
docs/additional-features/custom-fields.md
Normal file
26
docs/additional-features/custom-fields.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Custom Fields
|
||||
|
||||
Each object in NetBox is represented in the database as a discrete table, and each attribute of an object exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows.
|
||||
|
||||
However, some users might want to associate with objects attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number pointing to the support ticket that was opened to have it installed. This is certainly a legitimate use for NetBox, but it's perhaps not a common enough need to warrant expanding the internal data schema. Instead, you can create a custom field to hold this data.
|
||||
|
||||
Custom fields must be created through the admin UI under Extras > Custom Fields. To create a new custom field, select the object(s) to which you want it to apply, and the type of field it will be. NetBox supports six field types:
|
||||
|
||||
* Free-form text (up to 255 characters)
|
||||
* Integer
|
||||
* Boolean (true/false)
|
||||
* Date
|
||||
* URL
|
||||
* Selection
|
||||
|
||||
Assign the field a name. This should be a simple database-friendly string, e.g. `tps_report`. You may optionally assign the field a human-friendly label (e.g. "TPS report") as well; the label will be displayed on forms. If a description is provided, it will appear beneath the field in a form.
|
||||
|
||||
Marking the field as required will require the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields. (The default value has no effect for selection fields.)
|
||||
|
||||
When creating a selection field, you should create at least two choices. These choices will be arranged first by weight, with lower weights appearing higher in the list, and then alphabetically.
|
||||
|
||||
## Using Custom Fields
|
||||
|
||||
When a single object is edited, the form will include any custom fields which have been defined for the object type. These fields are included in the "Custom Fields" panel. On the backend, each custom field value is saved separately from the core object as an independent database call, so it's best to avoid adding too many custom fields per object.
|
||||
|
||||
When editing multiple objects, custom field values are saved in bulk. There is no significant difference in overhead when saving a custom field value for 100 objects versus one object. However, the bulk operation must be performed separately for each custom field.
|
||||
52
docs/additional-features/export-templates.md
Normal file
52
docs/additional-features/export-templates.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Export Templates
|
||||
|
||||
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Extras > Export Templates under the admin interface.
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
{% for rack in queryset %}
|
||||
Rack: {{ rack.name }}
|
||||
Site: {{ rack.site.name }}
|
||||
Height: {{ rack.u_height }}U
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
|
||||
|
||||
A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.
|
||||
|
||||
## Example
|
||||
|
||||
Here's an example device export template that will generate a simple Nagios configuration from a list of devices.
|
||||
|
||||
```
|
||||
{% for device in queryset %}{% if device.status and device.primary_ip %}define host{
|
||||
use generic-switch
|
||||
host_name {{ device.name }}
|
||||
address {{ device.primary_ip.address.ip }}
|
||||
}
|
||||
{% endif %}{% endfor %}
|
||||
```
|
||||
|
||||
The generated output will look something like this:
|
||||
|
||||
```
|
||||
define host{
|
||||
use generic-switch
|
||||
host_name switch1
|
||||
address 192.0.2.1
|
||||
}
|
||||
define host{
|
||||
use generic-switch
|
||||
host_name switch2
|
||||
address 192.0.2.2
|
||||
}
|
||||
define host{
|
||||
use generic-switch
|
||||
host_name switch3
|
||||
address 192.0.2.3
|
||||
}
|
||||
```
|
||||
25
docs/additional-features/graphs.md
Normal file
25
docs/additional-features/graphs.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Graphs
|
||||
|
||||
NetBox does not have the ability to generate graphs natively, but this feature allows you to embed contextual graphs from an external resources (such as a monitoring system) inside the site, provider, and interface views. Each embedded graph must be defined with the following parameters:
|
||||
|
||||
* **Type:** Site, provider, or interface. This determines in which view the graph will be displayed.
|
||||
* **Weight:** Determines the order in which graphs are displayed (lower weights are displayed first). Graphs with equal weights will be ordered alphabetically by name.
|
||||
* **Name:** The title to display above the graph.
|
||||
* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
|
||||
* **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`.
|
||||
|
||||
## Examples
|
||||
|
||||
You only need to define one graph object for each graph you want to include when viewing an object. For example, if you want to include a graph of traffic through an interface over the past five minutes, your graph source might looks like this:
|
||||
|
||||
```
|
||||
https://my.nms.local/graphs/?node={{ obj.device.name }}&interface={{ obj.name }}&duration=5m
|
||||
```
|
||||
|
||||
You can define several graphs to provide multiple contexts when viewing an object. For example:
|
||||
|
||||
```
|
||||
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
|
||||
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=24h
|
||||
https://my.nms.local/graphs/?type=errors&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
|
||||
```
|
||||
24
docs/additional-features/tags.md
Normal file
24
docs/additional-features/tags.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Tags
|
||||
|
||||
Tags are free-form text labels which can be applied to a variety of objects within NetBox. Tags are created on-demand as they are assigned to objects. Use commas to separate tags when adding multiple tags to an object 9for example: `Inventoried, Monitored`). Use double quotes around a multi-word tag when adding only one tag, e.g. `"Core Switch"`.
|
||||
|
||||
Each tag has a label and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be `dunder-mifflin-inc`. The slug is generated automatically and makes tags easier to work with as URL parameters.
|
||||
|
||||
Objects can be filtered by the tags they have applied. For example, the following API request will retrieve all devices tagged as "monitored":
|
||||
|
||||
```
|
||||
GET /api/dcim/devices/?tag=monitored
|
||||
```
|
||||
|
||||
Tags are included in the API representation of an object as a list of plain strings:
|
||||
|
||||
```
|
||||
{
|
||||
...
|
||||
"tags": [
|
||||
"Core Switch",
|
||||
"Monitored"
|
||||
],
|
||||
...
|
||||
}
|
||||
```
|
||||
17
docs/additional-features/topology-maps.md
Normal file
17
docs/additional-features/topology-maps.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# 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.
|
||||
57
docs/additional-features/webhooks.md
Normal file
57
docs/additional-features/webhooks.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Webhooks
|
||||
|
||||
A webhook defines an HTTP request that is sent to an external application when certain types of objects are created, updated, and/or deleted in NetBox. When a webhook is triggered, a POST request is sent to its configured URL. This request will include a full representation of the object being modified for consumption by the receiver. Webhooks are configured via the admin UI under Extras > Webhooks.
|
||||
|
||||
An optional secret key can be configured for each webhook. This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key. This digest can be used by the receiver to authenticate the request's content.
|
||||
|
||||
## Requests
|
||||
|
||||
The webhook POST request is structured as so (assuming `application/json` as the Content-Type):
|
||||
|
||||
```no-highlight
|
||||
{
|
||||
"event": "created",
|
||||
"signal_received_timestamp": 1508769597,
|
||||
"model": "Site"
|
||||
"data": {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`data` is the serialized representation of the model instance(s) from the event. The same serializers from the NetBox API are used. So an example of the payload for a Site delete event would be:
|
||||
|
||||
```no-highlight
|
||||
{
|
||||
"event": "deleted",
|
||||
"signal_received_timestamp": 1508781858.544069,
|
||||
"model": "Site",
|
||||
"data": {
|
||||
"asn": None,
|
||||
"comments": "",
|
||||
"contact_email": "",
|
||||
"contact_name": "",
|
||||
"contact_phone": "",
|
||||
"count_circuits": 0,
|
||||
"count_devices": 0,
|
||||
"count_prefixes": 0,
|
||||
"count_racks": 0,
|
||||
"count_vlans": 0,
|
||||
"custom_fields": {},
|
||||
"facility": "",
|
||||
"id": 54,
|
||||
"name": "test",
|
||||
"physical_address": "",
|
||||
"region": None,
|
||||
"shipping_address": "",
|
||||
"slug": "test",
|
||||
"tenant": None
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A request is considered successful if the response status code is any one of a list of "good" statuses defined in the [requests library](https://github.com/requests/requests/blob/205755834d34a8a6ecf2b0b5b2e9c3e6a7f4e4b6/requests/models.py#L688), otherwise the request is marked as having failed. The user may manually retry a failed request.
|
||||
|
||||
## Backend Status
|
||||
|
||||
Django-rq includes a status page in the admin site which can be used to view the result of processed webhooks and manually retry any failed webhooks. Access it from http://netbox.local/admin/webhook-backend-status/.
|
||||
63
docs/administration/replicating-netbox.md
Normal file
63
docs/administration/replicating-netbox.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Replicating the Database
|
||||
|
||||
NetBox uses [PostgreSQL](https://www.postgresql.org/) for its database, so general PostgreSQL best practices will apply to NetBox. You can dump and restore the database using the `pg_dump` and `psql` utilities, respectively.
|
||||
|
||||
!!! note
|
||||
The examples below assume that your database is named `netbox`.
|
||||
|
||||
## Export the Database
|
||||
|
||||
Use the `pg_dump` utility to export the entire database to a file:
|
||||
|
||||
```no-highlight
|
||||
pg_dump netbox > netbox.sql
|
||||
```
|
||||
|
||||
When replicating a production database for development purposes, you may find it convenient to exclude changelog data, which can easily account for the bulk of a database's size. To do this, exclude the `extras_objectchange` table data from the export. The table will still be included in the output file, but will not be populated with any data.
|
||||
|
||||
```no-highlight
|
||||
pg_dump --exclude-table-data=extras_objectchange netbox > netbox.sql
|
||||
```
|
||||
|
||||
## Load an Exported Database
|
||||
|
||||
!!! warning
|
||||
This will destroy and replace any existing instance of the database.
|
||||
|
||||
```no-highlight
|
||||
psql -c 'drop database netbox'
|
||||
psql -c 'create database netbox'
|
||||
psql netbox < netbox.sql
|
||||
```
|
||||
|
||||
Keep in mind that PostgreSQL user accounts and permissions are not included with the dump: You will need to create those manually if you want to fully replicate the original database (see the [installation docs](installation/1-postgresql.md)). When setting up a development instance of NetBox, it's strongly recommended to use different credentials anyway.
|
||||
|
||||
## Export the Database Schema
|
||||
|
||||
If you want to export only the database schema, and not the data itself (e.g. for development reference), do the following:
|
||||
|
||||
```no-highlight
|
||||
pg_dump -s netbox > netbox_schema.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Replicating Media
|
||||
|
||||
NetBox stored uploaded files (such as image attachments) in its media directory. To fully replicate an instance of NetBox, you'll need to copy both the database and the media files.
|
||||
|
||||
## Archive the Media Directory
|
||||
|
||||
Execute the following command from the root of the NetBox installation path (typically `/opt/netbox`):
|
||||
|
||||
```no-highlight
|
||||
tar -czf netbox_media.tar.gz netbox/media/
|
||||
```
|
||||
|
||||
## Restore the Media Directory
|
||||
|
||||
To extract the saved archive into a new installation, run the following from the installation root:
|
||||
|
||||
```no-highlight
|
||||
tar -xf netbox_media.tar.gz
|
||||
```
|
||||
16
docs/configuration/index.md
Normal file
16
docs/configuration/index.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# NetBox Configuration
|
||||
|
||||
NetBox's local configuration is stored in `netbox/netbox/configuration.py`. An example configuration is provided at `netbox/netbox/configuration.example.py`. You may copy or rename the example configuration and make changes as appropriate. NetBox will not run without a configuration file.
|
||||
|
||||
While NetBox has many configuration settings, only a few of them must be defined at the time of installation.
|
||||
|
||||
* [Required settings](required-settings.md)
|
||||
* [Optional settings](optional-settings.md)
|
||||
|
||||
## Changing the Configuration
|
||||
|
||||
Configuration settings may be changed at any time. However, the NetBox service must be restarted before the changes will take effect:
|
||||
|
||||
```no-highlight
|
||||
# sudo supervisorctl restart netbox
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
The following are optional settings which may be declared in `netbox/netbox/configuration.py`.
|
||||
# Optional Configuration Settings
|
||||
|
||||
## ADMINS
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
NetBox's local configuration is held in `netbox/netbox/configuration.py`. An example configuration is provided at `netbox/netbox/configuration.example.py`. You may copy or rename the example configuration and make changes as appropriate. NetBox will not run without a configuration file.
|
||||
# Required Configuration Settings
|
||||
|
||||
## ALLOWED_HOSTS
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
The circuits component of NetBox deals with the management of long-haul Internet and private transit links and providers.
|
||||
|
||||
# Providers
|
||||
|
||||
A provider is any entity which provides some form of connectivity. While this obviously includes carriers which offer Internet and private transit service, it might also include Internet exchange (IX) points and even organizations with whom you peer directly.
|
||||
|
||||
Each provider may be assigned an autonomous system number (ASN), an account number, and contact information.
|
||||
Each provider may be assigned an autonomous system number (ASN), an account number, and relevant contact information.
|
||||
|
||||
---
|
||||
|
||||
# Circuits
|
||||
|
||||
A circuit represents a single physical data link connecting two endpoints. Each circuit belongs to a provider and must be assigned a circuit ID which is unique to that provider.
|
||||
A circuit represents a single _physical_ link connecting exactly two endpoints. (A circuit with more than two endpoints is a virtual circuit, which is not currently supported by NetBox.) Each circuit belongs to a provider and must be assigned a circuit ID which is unique to that provider.
|
||||
|
||||
### Circuit Types
|
||||
## Circuit Types
|
||||
|
||||
Circuits are classified by type. For example, you might define circuit types for:
|
||||
|
||||
@@ -23,7 +21,7 @@ Circuits are classified by type. For example, you might define circuit types for
|
||||
|
||||
Circuit types are fully customizable.
|
||||
|
||||
### Circuit Terminations
|
||||
## Circuit Terminations
|
||||
|
||||
A circuit may have one or two terminations, annotated as the "A" and "Z" sides of the circuit. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites.
|
||||
|
||||
@@ -31,3 +29,6 @@ Each circuit termination is tied to a site, and optionally to a specific device
|
||||
|
||||
!!! note
|
||||
A circuit represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit.
|
||||
|
||||
!!! note
|
||||
A circuit may terminate only to a physical interface. Circuits may not terminate to LAG interfaces, which are virtual interfaces: You must define each physical circuit within a service bundle separately and terminate it to its actual physical interface.
|
||||
120
docs/core-functionality/devices.md
Normal file
120
docs/core-functionality/devices.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Device Types
|
||||
|
||||
A device type represents a particular make and model of hardware that exists in the real world. Device types define the physical attributes of a device (rack height and depth) and its individual components (console, power, and network interfaces).
|
||||
|
||||
Device types are instantiated as devices installed within racks. For example, you might define a device type to represent a Juniper EX4300-48T network switch with 48 Ethernet interfaces. You can then create multiple devices of this type named "switch1," "switch2," and so on. Each device will inherit the components (such as interfaces) of its device type at the time of creation. (However, changes made to a device type will **not** apply to instances of that device type retroactively.)
|
||||
|
||||
The device type model includes three flags which inform what type of components may be added to it:
|
||||
|
||||
* `is_console_server`: This device type has console server ports
|
||||
* `is_pdu`: This device type has power outlets
|
||||
* `is_network_device`: This device type has network interfaces
|
||||
|
||||
Some devices house child devices which share physical resources, like space and power, but which functional independently from one another. A common example of this is blade server chassis. Each device type is designated as one of the following:
|
||||
|
||||
* A parent device (which has device bays)
|
||||
* A child device (which must be installed in a device bay)
|
||||
* Neither
|
||||
|
||||
!!! note
|
||||
This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane.
|
||||
|
||||
## Manufacturers
|
||||
|
||||
Each device type must be assigned to a manufacturer. The model number of a device type must be unique to its manufacturer.
|
||||
|
||||
## Component Templates
|
||||
|
||||
Each device type is assigned a number of component templates which define the physical components within a device. These are:
|
||||
|
||||
* Console ports
|
||||
* Console server ports
|
||||
* Power ports
|
||||
* Power outlets
|
||||
* Network interfaces
|
||||
* Device bays (which house child devices)
|
||||
|
||||
Whenever a new device is created, its components are automatically created per the templates assigned to its device type. For example, a Juniper EX4300-48T device type might have the following component templates defined:
|
||||
|
||||
* One template for a console port ("Console")
|
||||
* Two templates for power ports ("PSU0" and "PSU1")
|
||||
* 48 templates for 1GE interfaces ("ge-0/0/0" through "ge-0/0/47")
|
||||
* Four templates for 10GE interfaces ("xe-0/2/0" through "xe-0/2/3")
|
||||
|
||||
Once component templates have been created, every new device that you create as an instance of this type will automatically be assigned each of the components listed above.
|
||||
|
||||
!!! note
|
||||
Assignment of components from templates occurs only at the time of device creation. If you modify the templates of a device type, it will not affect devices which have already been created. However, you always have the option of adding, modifying, or deleting components on existing devices.
|
||||
|
||||
---
|
||||
|
||||
# Devices
|
||||
|
||||
Every piece of hardware which is installed within a rack exists in NetBox as a device. Devices are measured in rack units (U) and can be half depth or full depth. A device may have a height of 0U: These devices do not consume vertical rack space and cannot be assigned to a particular rack unit. A common example of a 0U device is a vertically-mounted PDU.
|
||||
|
||||
When assigning a multi-U device to a rack, it is considered to be mounted in the lowest-numbered rack unit which it occupies. For example, a 3U device which occupies U8 through U10 is said to be mounted in U8. This logic applies to racks with both ascending and descending unit numbering.
|
||||
|
||||
A device is said to be full depth if its installation on one rack face prevents the installation of any other device on the opposite face within the same rack unit(s). This could be either because the device is physically too deep to allow a device behind it, or because the installation of an opposing device would impede airflow.
|
||||
|
||||
## Device Roles
|
||||
|
||||
Devices can be organized by functional roles. These roles are fully cusomizable. For example, you might create roles for core switches, distribution switches, and access switches.
|
||||
|
||||
---
|
||||
|
||||
# Device Components
|
||||
|
||||
There are six types of device components which comprise all of the interconnection logic with NetBox:
|
||||
|
||||
* Console ports
|
||||
* Console server ports
|
||||
* Power ports
|
||||
* Power outlets
|
||||
* Network interfaces
|
||||
* Device bays
|
||||
|
||||
## Console
|
||||
|
||||
Console ports connect only to console server ports. Console connections can be marked as either *planned* or *connected*.
|
||||
|
||||
## Power
|
||||
|
||||
Power ports connect only to power outlets. Power connections can be marked as either *planned* or *connected*.
|
||||
|
||||
## Interfaces
|
||||
|
||||
Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. Each type of connection can be classified as either *planned* or *connected*.
|
||||
|
||||
Each interface is a assigned a form factor denoting its physical properties. Two special form factors exist: the "virtual" form factor can be used to designate logical interfaces (such as SVIs), and the "LAG" form factor can be used to desinate link aggregation groups to which physical interfaces can be assigned.
|
||||
|
||||
Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management). Fields are also provided to store an interface's MTU and MAC address.
|
||||
|
||||
VLANs can be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.)
|
||||
|
||||
## Device Bays
|
||||
|
||||
Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
|
||||
|
||||
---
|
||||
|
||||
# Platforms
|
||||
|
||||
A platform defines the type of software running on a device or virtual machine. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15.
|
||||
|
||||
The platform model is also used to indicate which [NAPALM](https://napalm-automation.net/) driver NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform. See the [API documentation](api/napalm-integration.md) for more information on NAPALM integration.
|
||||
|
||||
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
|
||||
|
||||
---
|
||||
|
||||
# Inventory Items
|
||||
|
||||
Inventory items represent hardware components installed within a device, such as a power supply or CPU. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Like device types, each item can optionally be assigned a manufacturer.
|
||||
|
||||
---
|
||||
|
||||
# Virtual Chassis
|
||||
|
||||
A virtual chassis represents a set of devices which share a single control plane: a stack of switches which are managed as a single device, for example. Each device in the virtual chassis is assigned a position and (optionally) a priority. Exactly one device is designated the virtual chassis master: This device will typically be assigned a name, secrets, services, and other attributes related to its management.
|
||||
|
||||
It's important to recognize the distinction between a virtual chassis and a chassis-based device. For instance, a virtual chassis is not used to model a chassis switch with removable line cards such as the Juniper EX9208, as its line cards are _not_ physically separate devices capable of operating independently.
|
||||
93
docs/core-functionality/ipam.md
Normal file
93
docs/core-functionality/ipam.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Aggregates
|
||||
|
||||
The first step to documenting your IP space is to define its scope by creating aggregates. Aggregates establish the root of your IP address hierarchy by defining the top-level allocations that you're interested in managing. Most organizations will want to track some commonly-used private IP spaces, such as:
|
||||
|
||||
* 10.0.0.0/8 (RFC 1918)
|
||||
* 100.64.0.0/10 (RFC 6598)
|
||||
* 172.16.0.0/20 (RFC 1918)
|
||||
* 192.168.0.0/16 (RFC 1918)
|
||||
* One or more /48s within fd00::/8 (IPv6 unique local addressing)
|
||||
|
||||
In addition to one or more of these, you'll want to create an aggregate for each globally-routable space your organization has been allocated. These aggregates should match the allocations recorded in public WHOIS databases.
|
||||
|
||||
Each IP prefix will be automatically arranged under its parent aggregate if one exists. Note that it's advised to create aggregates only for IP ranges actually allocated to your organization (or marked for private use): There is no need to define aggregates for provider-assigned space which is only used on Internet circuits, for example.
|
||||
|
||||
Aggregates cannot overlap with one another: They can only exist side-by-side. For instance, you cannot define both 10.0.0.0/8 and 10.16.0.0/16 as aggregates, because they overlap. 10.16.0.0/16 in this example would be created as a prefix and automatically grouped under 10.0.0.0/8. Remember, the purpose of aggregates is to establish the root of your IP addressing hierarchy.
|
||||
|
||||
## Regional Internet Registries (RIRs)
|
||||
|
||||
[Regional Internet registries](https://en.wikipedia.org/wiki/Regional_Internet_registry) are responsible for the allocation of globally-routable address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for internal use, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space. There also exist lower-tier registries which serve a particular geographic area.
|
||||
|
||||
Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own). The RIR model includes a boolean flag which indicates whether the RIR allocates only private IP space.
|
||||
|
||||
For example, suppose your organization has been allocated 104.131.0.0/16 by ARIN. It also makes use of RFC 1918 addressing internally. You would first create RIRs named ARIN and RFC 1918, then create an aggregate for each of these top-level prefixes, assigning it to its respective RIR.
|
||||
|
||||
---
|
||||
|
||||
# Prefixes
|
||||
|
||||
A prefix is an IPv4 or IPv6 network and mask expressed in CIDR notation (e.g. 192.0.2.0/24). A prefix entails only the "network portion" of an IP address: All bits in the address not covered by the mask must be zero. (In other words, a prefix cannot be a specific IP address.)
|
||||
|
||||
Prefixes are automatically arranged by their parent aggregates. Additionally, each prefix can be assigned to a particular site and VRF (routing table). All prefixes not assigned to a VRF will appear in the "global" table.
|
||||
|
||||
Each prefix can be assigned a status and a role. These terms are often used interchangeably so it's important to recognize the difference between them. The **status** defines a prefix's operational state. Statuses are hard-coded in NetBox and can be one of the following:
|
||||
|
||||
* Container - A summary of child prefixes
|
||||
* Active - Provisioned and in use
|
||||
* Reserved - Designated for future use
|
||||
* Deprecated - No longer in use
|
||||
|
||||
On the other hand, a prefix's **role** defines its function. Role assignment is optional and roles are fully customizable. For example, you might create roles to differentiate between production and development infrastructure.
|
||||
|
||||
A prefix may also be assigned to a VLAN. This association is helpful for identifying which prefixes are included when reviewing a list of VLANs.
|
||||
|
||||
The prefix model include a "pool" flag. If enabled, NetBox will treat this prefix as a range (such as a NAT pool) wherein every IP address is valid and assignable. This logic is used for identifying available IP addresses within a prefix. If this flag is disabled, NetBox will assume that the first and last (broadcast) address within the prefix are unusable.
|
||||
|
||||
---
|
||||
|
||||
# IP Addresses
|
||||
|
||||
An IP address comprises a single host address (either IPv4 or IPv6) and its subnet mask. Its mask should match exactly how the IP address is configured on an interface in the real world.
|
||||
|
||||
Like prefixes, an IP address can optionally be assigned to a VRF (otherwise, it will appear in the "global" table). IP addresses are automatically organized under parent prefixes within their respective VRFs.
|
||||
|
||||
Also like prefixes, each IP address can be assigned a status and a role. Statuses are hard-coded in NetBox and include the following:
|
||||
|
||||
* Active
|
||||
* Reserved
|
||||
* Deprecated
|
||||
* DHCP
|
||||
|
||||
Each IP address can optionally be assigned a special role. Roles are used to indicate some special attribute of an IP address: for example, it is used as a loopback, or is a virtual IP maintained using VRRP. (Note that this differs in purpose from a _functional_ role, and thus cannot be customized.) Available roles include:
|
||||
|
||||
* Loopback
|
||||
* Secondary
|
||||
* Anycast
|
||||
* VIP
|
||||
* VRRP
|
||||
* HSRP
|
||||
* GLBP
|
||||
|
||||
An IP address can be assigned to a device or virtual machine interface, and an interface may have multiple IP addresses assigned to it. Further, each device and virtual machine may have one of its interface IPs designated as its primary IP address (one for IPv4 and one for IPv6).
|
||||
|
||||
## Network Address Translation (NAT)
|
||||
|
||||
An IP address can be designated as the network address translation (NAT) inside IP address for exactly one other IP address. This is useful primarily to denote a translation between public and private IP addresses. This relationship is followed in both directions: For example, if 10.0.0.1 is assigned as the inside IP for 192.0.2.1, 192.0.2.1 will be displayed as the outside IP for 10.0.0.1.
|
||||
|
||||
!!! note
|
||||
NetBox does not support tracking one-to-many NAT relationships (also called port address translation). This type of policy requires additional logic to model and cannot be fully represented by IP address alone.
|
||||
|
||||
---
|
||||
|
||||
# Virtual Routing and Forwarding (VRF)
|
||||
|
||||
A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain. Each VRF is essentially a separate routing table. VRFs are commonly used to isolate customers or organizations from one another within a network, or to route overlapping address space (e.g. multiple instances of the 10.0.0.0/8 space).
|
||||
|
||||
Each VRF is assigned a unique name and route distinguisher (RD). The RD is expected to take one of the forms prescribed in [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), however its formatting is not strictly enforced.
|
||||
|
||||
Each prefix and IP address may be assigned to one (and only one) VRF. If you have a prefix or IP address which exists in multiple VRFs, you will need to create a separate instance of it in NetBox for each VRF. Any IP prefix or address not assigned to a VRF is said to belong to the "global" table.
|
||||
|
||||
By default, NetBox will allow duplicate prefixes to be assigned to a VRF. This behavior can be disabled by setting the "enforce unique" flag on the VRF model.
|
||||
|
||||
!!! note
|
||||
Enforcement of unique IP space can be toggled for global table (non-VRF prefixes) using the `ENFORCE_GLOBAL_UNIQUE` configuration setting.
|
||||
@@ -1,14 +1,12 @@
|
||||
"Secrets" are small amounts of data that must be kept confidential; for example, passwords and SNMP community strings. NetBox provides encrypted storage of secret data.
|
||||
|
||||
# Secrets
|
||||
|
||||
A secret represents a single credential or other string which must be stored securely. Each secret is assigned to a device within NetBox. The plaintext value of a secret is encrypted to a ciphertext immediately prior to storage within the database using a 256-bit AES master key. A SHA256 hash of the plaintext is also stored along with each ciphertext to validate the decrypted plaintext.
|
||||
A secret represents a single credential or other sensitive string of characters which must be stored securely. Each secret is assigned to a device within NetBox. The plaintext value of a secret is encrypted to a ciphertext immediately prior to storage within the database using a 256-bit AES master key. A SHA256 hash of the plaintext is also stored along with each ciphertext to validate the decrypted plaintext.
|
||||
|
||||
Each secret can also store an optional name parameter, which is not encrypted. This may be useful for storing user names.
|
||||
|
||||
### Roles
|
||||
## Roles
|
||||
|
||||
Each secret is assigned a functional role which indicates what it is used for. Typical roles might include:
|
||||
Each secret is assigned a functional role which indicates what it is used for. Secret roles are customizable. Typical roles might include:
|
||||
|
||||
* Login credentials
|
||||
* SNMP community strings
|
||||
5
docs/core-functionality/services.md
Normal file
5
docs/core-functionality/services.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Services
|
||||
|
||||
A service represents a layer four TCP or UDP service available on a device or virtual machine. For example, you might want to document that an HTTP service is running on a device. Each service includes a name, protocol, and port number; for example, "SSH (TCP/22)" or "DNS (UDP/53)."
|
||||
|
||||
A service may optionally be bound to one or more specific IP addresses belonging to its parent device or VM. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address.)
|
||||
49
docs/core-functionality/sites-and-racks.md
Normal file
49
docs/core-functionality/sites-and-racks.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Sites
|
||||
|
||||
How you choose to use sites will depend on the nature of your organization, but typically a site will equate to a building or campus. For example, a chain of banks might create a site to represent each of its branches, a site for its corporate headquarters, and two additional sites for its presence in two colocation facilities.
|
||||
|
||||
Each site must be assigned one of the following operational statuses:
|
||||
|
||||
* Active
|
||||
* Planned
|
||||
* Retired
|
||||
|
||||
The site model provides a facility ID field which can be used to annotate a facility ID (such as a datacenter name) associated with the site. Each site may also have an autonomous system (AS) number and time zone associated with it. (Time zones are provided by the [pytz](https://pypi.org/project/pytz/) package.)
|
||||
|
||||
The site model also includes several fields for storing contact and address information.
|
||||
|
||||
## Regions
|
||||
|
||||
Sites can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned.
|
||||
|
||||
---
|
||||
|
||||
# Racks
|
||||
|
||||
The rack model represents a physical two- or four-post equipment rack in which equipment is mounted. Each rack must be assigned to a site. Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending or descending order.
|
||||
|
||||
Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, "M204.313") whereas internally you refer to is simply as "R113." A unique serial number may also be associated with each rack.
|
||||
|
||||
A rack must be designated as one of the following types:
|
||||
|
||||
* 2-post frame
|
||||
* 4-post frame
|
||||
* 4-post cabinet
|
||||
* Wall-mounted frame
|
||||
* Wall-mounted cabinet
|
||||
|
||||
Each rack has two faces (front and rear) on which devices can be mounted. Rail-to-rail width may be 19 or 23 inches.
|
||||
|
||||
## Rack Groups
|
||||
|
||||
Racks can be arranged into groups. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site represents a campus, each group might represent a building within a campus. If each site represents a building, each rack group might equate to a floor or room.
|
||||
|
||||
Each rack group must be assigned to a parent site. Hierarchical recursion of rack groups is not currently supported.
|
||||
|
||||
## Rack Roles
|
||||
|
||||
Each rack can optionally be assigned a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. Rack roles are fully customizable.
|
||||
|
||||
## Rack Space Reservations
|
||||
|
||||
Users can reserve units within a rack for future use. Multiple non-contiguous rack units can be associated with a single reservation (but reservations cannot span multiple racks). A rack reservation may optionally designate a specific tenant.
|
||||
20
docs/core-functionality/tenancy.md
Normal file
20
docs/core-functionality/tenancy.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Tenants
|
||||
|
||||
A tenant represents a discrete entity for administrative purposes. Typically, tenants are used to represent individual customers or internal departments within an organization. The following objects can be assigned to tenants:
|
||||
|
||||
* Sites
|
||||
* Racks
|
||||
* Rack reservations
|
||||
* Devices
|
||||
* VRFs
|
||||
* Prefixes
|
||||
* IP addresses
|
||||
* VLANs
|
||||
* Circuits
|
||||
* Virtual machines
|
||||
|
||||
Tenant assignment is used to signify ownership of an object in NetBox. As such, each object may only be owned by a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't *belong* to any particular customer, so tenant assignment would not be appropriate.
|
||||
|
||||
### Tenant Groups
|
||||
|
||||
Tenants can be organized by custom groups. For instance, you might create one group called "Customers" and one called "Acquisitions." The assignment of tenants to groups is optional.
|
||||
27
docs/core-functionality/virtual-machines.md
Normal file
27
docs/core-functionality/virtual-machines.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Clusters
|
||||
|
||||
A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type, and may optionally be assigned to a group and/or site.
|
||||
|
||||
Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular VM may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device.
|
||||
|
||||
## Cluster Types
|
||||
|
||||
A cluster type represents a technology or mechanism by which a cluster is formed. For example, you might create a cluster type named "VMware vSphere" for a locally hosted cluster or "DigitalOcean NYC3" for one hosted by a cloud provider.
|
||||
|
||||
## Cluster Groups
|
||||
|
||||
Cluster groups may be created for the purpose of organizing clusters. The assignment of clusters to groups is optional.
|
||||
|
||||
---
|
||||
|
||||
# Virtual Machines
|
||||
|
||||
A virtual machine represents a virtual compute instance hosted within a cluster. Each VM must be associated with exactly one cluster.
|
||||
|
||||
Like devices, each VM can be assigned a platform and have interfaces created on it. VM interfaces behave similarly to device interfaces, and can be assigned IP addresses, VLANs, and services. However, given their virtual nature, they cannot be connected to other interfaces. Unlike physical devices, VMs cannot be assigned console or power ports, device bays, or inventory items.
|
||||
|
||||
The following resources can be defined for each VM:
|
||||
|
||||
* vCPU count
|
||||
* Memory (MB)
|
||||
* Disk space (GB)
|
||||
15
docs/core-functionality/vlans.md
Normal file
15
docs/core-functionality/vlans.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# VLANs
|
||||
|
||||
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). Each VLAN may be assigned to a site and/or VLAN group.
|
||||
|
||||
Each VLAN must be assigned one of the following operational statuses:
|
||||
|
||||
* Active
|
||||
* Reserved
|
||||
* Deprecated
|
||||
|
||||
Each VLAN may also be assigned a functional role. Prefixes and VLANs share the same set of customizable roles.
|
||||
|
||||
## VLAN Groups
|
||||
|
||||
VLAN groups can be used to organize VLANs within NetBox. Groups can also be used to enforce uniqueness: Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs (including VLANs which belong to a common site). For example, you can create two VLANs with ID 123, but they cannot both be assigned to the same group.
|
||||
@@ -1,124 +0,0 @@
|
||||
Data center infrastructure management (DCIM) entails all physical assets: sites, racks, devices, cabling, etc.
|
||||
|
||||
# Sites
|
||||
|
||||
How you choose to use sites will depend on the nature of your organization, but typically a site will equate to a building or campus. For example, a chain of banks might create a site to represent each of its branches, a site for its corporate headquarters, and two additional sites for its presence in two colocation facilities.
|
||||
|
||||
Sites can be assigned an optional facility ID to identify the actual facility housing colocated equipment, and an Autonomous System (AS) number.
|
||||
|
||||
### Regions
|
||||
|
||||
Sites can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned.
|
||||
|
||||
---
|
||||
|
||||
# Racks
|
||||
|
||||
The rack model represents a physical two- or four-post equipment rack in which equipment is mounted. Each rack is assigned to a site. Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U, but NetBox allows you to define racks of arbitrary height. Each rack has two faces (front and rear) on which devices can be mounted.
|
||||
|
||||
Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, "M204.313") whereas internally you refer to is simply as "R113." The facility ID can alternatively be used to store a rack's serial number.
|
||||
|
||||
The available rack types include 2- and 4-post frames, 4-post cabinet, and wall-mounted frame and cabinet. Rail-to-rail width may be 19 or 23 inches.
|
||||
|
||||
### Rack Groups
|
||||
|
||||
Racks can be arranged into groups. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site represents a campus, each group might represent a building within a campus. If each site represents a building, each rack group might equate to a floor or room.
|
||||
|
||||
Each group is assigned to a parent site for easy navigation. Hierarchical recursion of rack groups is not supported.
|
||||
|
||||
### Rack Roles
|
||||
|
||||
Each rack can optionally be assigned a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. Rack roles are fully customizable.
|
||||
|
||||
### Rack Space Reservations
|
||||
|
||||
Users can reserve units within a rack for future use. Multiple non-contiguous rack units can be associated with a single reservation (but reservations cannot span multiple racks).
|
||||
|
||||
---
|
||||
|
||||
# Device Types
|
||||
|
||||
A device type represents a particular hardware model that exists in the real world. Device types describe the physical attributes of a device (rack height and depth), its class (e.g. console server, PDU, etc.), and its individual components (console, power, and data).
|
||||
|
||||
Device types are instantiated as devices installed within racks. For example, you might define a device type to represent a Juniper EX4300-48T network switch with 48 Ethernet interfaces. You can then create multiple devices of this type named "switch1," "switch2," and so on. Each device will inherit the components (such as interfaces) of its device type.
|
||||
|
||||
A device type can be a parent, child, or neither. Parent devices house child devices in device bays. This relationship is used to model things like blade servers, where child devices function independently but share physical resources like rack space and power. Note that this is **not** intended to model chassis-based devices, wherein child members share a common control plane.
|
||||
|
||||
### Manufacturers
|
||||
|
||||
Each device type belongs to one manufacturer; e.g. Cisco, Opengear, or APC. The model number of a device type must be unique to its manufacturer.
|
||||
|
||||
### Component Templates
|
||||
|
||||
Each device type is assigned a number of component templates which define the physical interfaces a device has. These are:
|
||||
|
||||
* Console ports
|
||||
* Console server ports
|
||||
* Power ports
|
||||
* Power outlets
|
||||
* Interfaces
|
||||
* Device bays
|
||||
|
||||
Whenever a new device is created, it is automatically assigned components per the templates assigned to its device type. For example, a Juniper EX4300-48T device type might have the following component templates:
|
||||
|
||||
* One template for a console port ("Console")
|
||||
* Two templates for power ports ("PSU0" and "PSU1")
|
||||
* 48 templates for 1GE interfaces ("ge-0/0/0" through "ge-0/0/47")
|
||||
* Four templates for 10GE interfaces ("xe-0/2/0" through "xe-0/2/3")
|
||||
|
||||
Once component templates have been created, every new device that you create as an instance of this type will automatically be assigned each of the components listed above.
|
||||
|
||||
!!! note
|
||||
Assignment of components from templates occurs only at the time of device creation. If you modify the templates of a device type, it will not affect devices which have already been created. However, you always have the option of adding, modifying, or deleting components of existing devices individually.
|
||||
|
||||
---
|
||||
|
||||
# Devices
|
||||
|
||||
Every piece of hardware which is installed within a rack exists in NetBox as a device. Devices are measured in rack units (U) and depth. 0U devices which can be installed in a rack but don't consume vertical rack space (such as a vertically-mounted power distribution unit) can also be defined.
|
||||
|
||||
When assigning a multi-U device to a rack, it is considered to be mounted in the lowest-numbered rack unit which it occupies. For example, a 3U device which occupies U8 through U10 shows as being mounted in U8. This logic applies to racks with both ascending and descending unit numbering.
|
||||
|
||||
A device is said to be "full depth" if its installation on one rack face prevents the installation of any other device on the opposite face within the same rack unit(s). This could be either because the device is physically too deep to allow a device behind it, or because the installation of an opposing device would impede air flow.
|
||||
|
||||
### Roles
|
||||
|
||||
NetBox allows for the definition of arbitrary device roles by which devices can be organized. For example, you might create roles for core switches, distribution switches, and access switches. In the interest of simplicity, a device can belong to only one role.
|
||||
|
||||
### Platforms
|
||||
|
||||
A device's platform is used to denote the type of software running on it. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15.
|
||||
|
||||
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
|
||||
|
||||
### Inventory Items
|
||||
|
||||
Inventory items represent hardware components installed within a device, such as a power supply or CPU. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Each item can optionally be assigned a manufacturer.
|
||||
|
||||
!!! note
|
||||
Prior to version 2.0, inventory items were called modules.
|
||||
|
||||
### Components
|
||||
|
||||
There are six types of device components which comprise all of the interconnection logic with NetBox:
|
||||
|
||||
* Console ports
|
||||
* Console server ports
|
||||
* Power ports
|
||||
* Power outlets
|
||||
* Interfaces
|
||||
* Device bays
|
||||
|
||||
Console ports connect only to console server ports, and power ports connect only to power outlets. Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. (The relationship between two interfaces is actually represented in the database by an InterfaceConnection object, but this is transparent to the user.) Each type of connection can be classified as either *planned* or *connected*. This allows for easily denoting connections which have not yet been installed.
|
||||
|
||||
Each interface is a assigned a form factor denoting its physical properties. Two special form factors exist: the "virtual" form factor can be used to designate logical interfaces (such as SVIs), and the "LAG" form factor can be used to desinate link aggregation groups to which physical interfaces can be assigned. Each interface can also be designated as management-only (for out-of-band management) and assigned a short description.
|
||||
|
||||
Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear on rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
|
||||
|
||||
---
|
||||
|
||||
# Virtual Chassis
|
||||
|
||||
A virtual chassis represents a set of devices which share a single control plane: for example, a stack of switches which are managed as a single device. Each device in the virtual chassis is assigned a position and (optionally) a priority. Exactly one device is designated the virtual chassis master: This device will typically be assigned a name, secrets, services, and other attributes related to its management.
|
||||
|
||||
It's important to recognize the distinction between a virtual chassis and a chassis-based device. For instance, a virtual chassis is not used to model a chassis switch with removable line cards such as the Juniper EX9208, as its line cards are _not_ physically separate devices capable of operating independently.
|
||||
@@ -1,198 +0,0 @@
|
||||
This section entails features of NetBox which are not crucial to its primary functions, but provide additional value.
|
||||
|
||||
# Tags
|
||||
|
||||
Tags are freeform labels which can be assigned to a variety of objects in NetBox. Tags can be used to categorize and filter objects in addition to built-in and custom fields. Each tag consists of a text label, as well as an auto-generated URL-friendly slug value. Objects can be filtered by the tags assigned to them. Tags can be used across different object types.
|
||||
|
||||
# Custom Fields
|
||||
|
||||
Each object in NetBox is represented in the database as a discrete table, and each attribute of an object exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows.
|
||||
|
||||
However, some users might want to associate with objects attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number pointing to the support ticket that was opened to have it installed. This is certainly a legitimate use for NetBox, but it's perhaps not a common enough need to warrant expanding the internal data schema. Instead, you can create a custom field to hold this data.
|
||||
|
||||
Custom fields must be created through the admin UI under Extras > Custom Fields. To create a new custom field, select the object(s) to which you want it to apply, and the type of field it will be. NetBox supports six field types:
|
||||
|
||||
* Free-form text (up to 255 characters)
|
||||
* Integer
|
||||
* Boolean (true/false)
|
||||
* Date
|
||||
* URL
|
||||
* Selection
|
||||
|
||||
Assign the field a name. This should be a simple database-friendly string, e.g. `tps_report`. You may optionally assign the field a human-friendly label (e.g. "TPS report") as well; the label will be displayed on forms. If a description is provided, it will appear beneath the field in a form.
|
||||
|
||||
Marking the field as required will require the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields. (The default value has no effect for selection fields.)
|
||||
|
||||
When creating a selection field, you should create at least two choices. These choices will be arranged first by weight, with lower weights appearing higher in the list, and then alphabetically.
|
||||
|
||||
## Using Custom Fields
|
||||
|
||||
When a single object is edited, the form will include any custom fields which have been defined for the object type. These fields are included in the "Custom Fields" panel. On the backend, each custom field value is saved separately from the core object as an independent database call, so it's best to avoid adding too many custom fields per object.
|
||||
|
||||
When editing multiple objects, custom field values are saved in bulk. There is no significant difference in overhead when saving a custom field value for 100 objects versus one object. However, the bulk operation must be performed separately for each custom field.
|
||||
|
||||
# Contextual Configuration Data
|
||||
|
||||
Sometimes it is desirable to associate arbitrary data with a group of devices to aid in their configuration. (For example, you might want to associate a set of syslog servers for all devices at a particular site.) Context data enables the association of arbitrary data (expressed in JSON format) to devices and virtual machines grouped by region, site, role, platform, and/or tenancy. Context data is arranged hierarchically, so that data with a higher weight can be entered to override more general lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object.
|
||||
|
||||
# Export Templates
|
||||
|
||||
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Extras > Export Templates under the admin interface.
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
{% for rack in queryset %}
|
||||
Rack: {{ rack.name }}
|
||||
Site: {{ rack.site.name }}
|
||||
Height: {{ rack.u_height }}U
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
|
||||
|
||||
A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.
|
||||
|
||||
## Example
|
||||
|
||||
Here's an example device export template that will generate a simple Nagios configuration from a list of devices.
|
||||
|
||||
```
|
||||
{% for device in queryset %}{% if device.status and device.primary_ip %}define host{
|
||||
use generic-switch
|
||||
host_name {{ device.name }}
|
||||
address {{ device.primary_ip.address.ip }}
|
||||
}
|
||||
{% endif %}{% endfor %}
|
||||
```
|
||||
|
||||
The generated output will look something like this:
|
||||
|
||||
```
|
||||
define host{
|
||||
use generic-switch
|
||||
host_name switch1
|
||||
address 192.0.2.1
|
||||
}
|
||||
define host{
|
||||
use generic-switch
|
||||
host_name switch2
|
||||
address 192.0.2.2
|
||||
}
|
||||
define host{
|
||||
use generic-switch
|
||||
host_name switch3
|
||||
address 192.0.2.3
|
||||
}
|
||||
```
|
||||
|
||||
# Graphs
|
||||
|
||||
NetBox does not have the ability to generate graphs natively, but this feature allows you to embed contextual graphs from an external resources (such as a monitoring system) inside the site, provider, and interface views. Each embedded graph must be defined with the following parameters:
|
||||
|
||||
* **Type:** Site, provider, or interface. This determines in which view the graph will be displayed.
|
||||
* **Weight:** Determines the order in which graphs are displayed (lower weights are displayed first). Graphs with equal weights will be ordered alphabetically by name.
|
||||
* **Name:** The title to display above the graph.
|
||||
* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
|
||||
* **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`.
|
||||
|
||||
## Examples
|
||||
|
||||
You only need to define one graph object for each graph you want to include when viewing an object. For example, if you want to include a graph of traffic through an interface over the past five minutes, your graph source might looks like this:
|
||||
|
||||
```
|
||||
https://my.nms.local/graphs/?node={{ obj.device.name }}&interface={{ obj.name }}&duration=5m
|
||||
```
|
||||
|
||||
You can define several graphs to provide multiple contexts when viewing an object. For example:
|
||||
|
||||
```
|
||||
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
|
||||
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=24h
|
||||
https://my.nms.local/graphs/?type=errors&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
|
||||
```
|
||||
|
||||
# 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.
|
||||
|
||||
# Image Attachments
|
||||
|
||||
Certain objects within NetBox (namely sites, racks, and devices) can have photos or other images attached to them. (Note that _only_ image files are supported.) Each attachment may optionally be assigned a name; if omitted, the attachment will be represented by its file name.
|
||||
|
||||
!!! note
|
||||
If you experience a server error while attempting to upload an image attachment, verify that the system user NetBox runs as has write permission to the media root directory (`netbox/media/`).
|
||||
|
||||
# Webhooks
|
||||
|
||||
A webhook defines an HTTP request that is sent to an external application when certain types of objects are created, updated, and/or deleted in NetBox. When a webhook is triggered, a POST request is sent to its configured URL. This request will include a full representation of the object being modified for consumption by the receiver. Webhooks are configured via the admin UI under Extras > Webhooks.
|
||||
|
||||
An optional secret key can be configured for each webhook. This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key. This digest can be used by the receiver to authenticate the request's content.
|
||||
|
||||
## Requests
|
||||
|
||||
The webhook POST request is structured as so (assuming `application/json` as the Content-Type):
|
||||
|
||||
```no-highlight
|
||||
{
|
||||
"event": "created",
|
||||
"signal_received_timestamp": 1508769597,
|
||||
"model": "Site"
|
||||
"data": {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`data` is the serialized representation of the model instance(s) from the event. The same serializers from the NetBox API are used. So an example of the payload for a Site delete event would be:
|
||||
|
||||
```no-highlight
|
||||
{
|
||||
"event": "deleted",
|
||||
"signal_received_timestamp": 1508781858.544069,
|
||||
"model": "Site",
|
||||
"data": {
|
||||
"asn": None,
|
||||
"comments": "",
|
||||
"contact_email": "",
|
||||
"contact_name": "",
|
||||
"contact_phone": "",
|
||||
"count_circuits": 0,
|
||||
"count_devices": 0,
|
||||
"count_prefixes": 0,
|
||||
"count_racks": 0,
|
||||
"count_vlans": 0,
|
||||
"custom_fields": {},
|
||||
"facility": "",
|
||||
"id": 54,
|
||||
"name": "test",
|
||||
"physical_address": "",
|
||||
"region": None,
|
||||
"shipping_address": "",
|
||||
"slug": "test",
|
||||
"tenant": None
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A request is considered successful if the response status code is any one of a list of "good" statuses defined in the [requests library](https://github.com/requests/requests/blob/205755834d34a8a6ecf2b0b5b2e9c3e6a7f4e4b6/requests/models.py#L688), otherwise the request is marked as having failed. The user may manually retry a failed request.
|
||||
|
||||
## Backend Status
|
||||
|
||||
Django-rq includes a status page in the admin site which can be used to view the result of processed webhooks and manually retry any failed webhooks. Access it from http://netbox.local/admin/webhook-backend-status/.
|
||||
@@ -1,99 +0,0 @@
|
||||
IP address management (IPAM) entails the allocation of IP networks, addresses, and related numeric resources.
|
||||
|
||||
# VRFs
|
||||
|
||||
A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain within a network. Each VRF is essentially a separate routing table: the same IP prefix or address can exist in multiple VRFs. VRFs are commonly used to isolate customers or organizations from one another within a network.
|
||||
|
||||
Each VRF is assigned a name and a unique route distinguisher (RD). VRFs are an optional feature of NetBox: Any IP prefix or address not assigned to a VRF is said to belong to the "global" table.
|
||||
|
||||
!!! note
|
||||
By default, NetBox allows for overlapping IP space both in the global table and within each VRF. Unique space enforcement can be toggled per-VRF as well as in the global table using the `ENFORCE_GLOBAL_UNIQUE` configuration setting.
|
||||
|
||||
---
|
||||
|
||||
# Aggregates
|
||||
|
||||
IP address space is organized as a hierarchy, with more-specific (smaller) prefixes arranged as child nodes under less-specific (larger) prefixes. For example:
|
||||
|
||||
* 10.0.0.0/8
|
||||
* 10.1.0.0/16
|
||||
* 10.1.2.0/24
|
||||
|
||||
The root of the IPv4 hierarchy is 0.0.0.0/0, which encompasses all possible IPv4 addresses (and similarly, ::/0 for IPv6). However, even the largest organizations use only a small fraction of the global address space. Therefore, it makes sense to track in NetBox only the address space which is of interest to your organization.
|
||||
|
||||
Aggregates serve as arbitrary top-level nodes in the IP space hierarchy. They allow you to easily construct your IP scheme without any clutter of unused address space. For instance, most organizations utilize some portion of the private IPv4 space set aside in RFC 1918. So, you might define three aggregates for this space:
|
||||
|
||||
* 10.0.0.0/8
|
||||
* 172.16.0.0/12
|
||||
* 192.168.0.0/16
|
||||
|
||||
Additionally, you might define an aggregate for each large swath of public IPv4 space your organization uses. You'd also create aggregates for both globally routable and unique local IPv6 space. (Most organizations will not have a need to track IPv6 link local space.)
|
||||
|
||||
Prefixes you create in NetBox (discussed below) will be automatically organized under their respective aggregates. Any space within an aggregate which is not covered by an existing prefix will be annotated as available for allocation. Total utilization for each aggregate is displayed in the aggregates list.
|
||||
|
||||
Aggregates cannot overlap with one another; they can only exist in parallel. For instance, you cannot define both 10.0.0.0/8 and 10.16.0.0/16 as aggregates, because they overlap. 10.16.0.0/16 in this example would be created as a prefix and automatically grouped under 10.0.0.0/8.
|
||||
|
||||
### RIRs
|
||||
|
||||
Regional Internet Registries (RIRs) are responsible for the allocation of global address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for private or internal use only, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space.
|
||||
|
||||
Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own). Each RIR can be annotated as representing only private space.
|
||||
|
||||
---
|
||||
|
||||
# Prefixes
|
||||
|
||||
A prefix is an IPv4 or IPv6 network and mask expressed in CIDR notation (e.g. 192.0.2.0/24). A prefix entails only the "network portion" of an IP address; all bits in the address not covered by the mask must be zero.
|
||||
|
||||
Each prefix may be assigned to one VRF; prefixes not assigned to a VRF are assigned to the "global" table. Prefixes are also organized under their respective aggregates, irrespective of VRF assignment.
|
||||
|
||||
A prefix may optionally be assigned to one VLAN; a VLAN may have multiple prefixes assigned to it. Each prefix may also be assigned a short description.
|
||||
|
||||
### Statuses
|
||||
|
||||
Each prefix is assigned an operational status. This is one of the following:
|
||||
|
||||
* Container - A summary of child prefixes
|
||||
* Active - Provisioned and in use
|
||||
* Reserved - Designated for future use
|
||||
* Deprecated - No longer in use
|
||||
|
||||
### Roles
|
||||
|
||||
Whereas a status describes a prefix's operational state, a role describes its function. For example, roles might include:
|
||||
|
||||
* Access segment
|
||||
* Infrastructure
|
||||
* NAT
|
||||
* Lab
|
||||
* Out-of-band
|
||||
|
||||
Role assignment is optional and roles are fully customizable.
|
||||
|
||||
---
|
||||
|
||||
# IP Addresses
|
||||
|
||||
An IP address comprises a single address (either IPv4 or IPv6) and its subnet mask. Its mask should match exactly how the IP address is configured on an interface in the real world.
|
||||
|
||||
Like prefixes, an IP address can optionally be assigned to a VRF (or it will appear in the "global" table). IP addresses are automatically organized under parent prefixes within their respective VRFs. Each IP address can also be assigned a short description.
|
||||
|
||||
An IP address can be assigned to a device's interface; an interface may have multiple IP addresses assigned to it. Further, each device may have one of its interface IPs designated as its primary IP address (for both IPv4 and IPv6).
|
||||
|
||||
One IP address can be designated as the network address translation (NAT) IP address for exactly one other IP address. This is useful primarily to denote the public address for a private internal IP. Tracking one-to-many NAT (or PAT) assignments is not supported.
|
||||
|
||||
---
|
||||
|
||||
# VLANs
|
||||
|
||||
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). Each VLAN may be assigned to a site and/or VLAN group. Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role, and may include a short description.
|
||||
|
||||
### VLAN Groups
|
||||
|
||||
VLAN groups can be employed for administrative organization within NetBox. Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs, including within a site.
|
||||
|
||||
---
|
||||
|
||||
# Services
|
||||
|
||||
A service represents a TCP or UDP service available on a device or virtual machine. Each service must be defined with a name, protocol, and port number; for example, "SSH (TCP/22)." A service may optionally be bound to one or more specific IP addresses belonging to its parent. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address.)
|
||||
@@ -1,20 +0,0 @@
|
||||
NetBox supports the assignment of resources to tenant organizations. Typically, these are used to represent individual customers of or internal departments within the organization using NetBox.
|
||||
|
||||
# Tenants
|
||||
|
||||
A tenant represents a discrete organization. The following objects can be assigned to tenants:
|
||||
|
||||
* Sites
|
||||
* Racks
|
||||
* Devices
|
||||
* VRFs
|
||||
* Prefixes
|
||||
* IP addresses
|
||||
* VLANs
|
||||
* Circuits
|
||||
|
||||
If a prefix or IP address is not assigned to a tenant, it will appear to inherit the tenant to which its parent VRF is assigned, if any.
|
||||
|
||||
### Tenant Groups
|
||||
|
||||
Tenants can be grouped by type. For instance, you might create one group called "Customers" and one called "Acquisitions." The assignment of tenants to groups is optional.
|
||||
@@ -1,29 +0,0 @@
|
||||
NetBox supports the definition of virtual machines arranged in clusters. A cluster can optionally have physical host devices associated with it.
|
||||
|
||||
# Clusters
|
||||
|
||||
A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type, and may optionally be assigned an organizational group.
|
||||
|
||||
Physical devices (from NetBox's DCIM component) may be associated with clusters as hosts. This allows users to track on which host(s) a particular VM may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device.
|
||||
|
||||
### Cluster Types
|
||||
|
||||
A cluster type represents a technology or mechanism by which a cluster is formed. For example, you might create a cluster type named "VMware vSphere" for a locally hosted cluster or "DigitalOcean NYC3" for one hosted by a cloud provider.
|
||||
|
||||
### Cluster Groups
|
||||
|
||||
Cluster groups may be created for the purpose of organizing clusters.
|
||||
|
||||
---
|
||||
|
||||
# Virtual Machines
|
||||
|
||||
A virtual machine represents a virtual compute instance hosted within a cluster. Each VM must be associated with exactly one cluster.
|
||||
|
||||
Like devices, each VM can have interfaces created on it. These behave similarly to device interfaces, and can be assigned IP addresses, however given their virtual nature they cannot be connected to other interfaces. VMs can also be assigned layer four services. Unlike physical devices, VMs cannot be assigned console or power ports, or device bays.
|
||||
|
||||
The following resources can be defined for each VM:
|
||||
|
||||
* vCPU count
|
||||
* Memory (MB)
|
||||
* Disk space (GB)
|
||||
37
docs/development/index.md
Normal file
37
docs/development/index.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# NetBox Development
|
||||
|
||||
NetBox is maintained as a [GitHub project](https://github.com/digitalocean/netbox) under the Apache 2 license. Users are encouraged to submit GitHub issues for feature requests and bug reports, however we are very selective about pull requests. Please see the `CONTRIBUTING` guide for more direction on contributing to NetBox.
|
||||
|
||||
## Communication
|
||||
|
||||
Communication among developers should always occur via public channels:
|
||||
|
||||
* [GitHub issues](https://github.com/digitalocean/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in an issue.
|
||||
* [The mailing list](https://groups.google.com/forum/#!forum/netbox-discuss) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
|
||||
* [#netbox on NetworkToCode](http://slack.networktocode.com/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
|
||||
|
||||
## Governance
|
||||
|
||||
NetBox follows the [benevolent dictator](http://oss-watch.ac.uk/resources/benevolentdictatorgovernancemodel) model of governance, with [Jeremy Stretch](https://github.com/jeremystretch) ultimately responsible for all changes to the code base. While community contributions are welcomed and encouraged, the lead maintainer's primary role is to ensure the project's long-term maintainability and continued focus on its primary functions (in other words, avoid scope creep).
|
||||
|
||||
## Project Structure
|
||||
|
||||
All development of the current NetBox release occurs in the `develop` branch; releases are packaged from the `master` branch. The `master` branch should _always_ represent the current stable release in its entirety, such that installing NetBox by either downloading a packaged release or cloning the `master` branch provides the same code base.
|
||||
|
||||
NetBox components are arranged into functional subsections called _apps_ (a carryover from Django verancular). Each app holds the models, views, and templates relevant to a particular function:
|
||||
|
||||
* `circuits`: Communications circuits and providers (not to be confused with power circuits)
|
||||
* `dcim`: Datacenter infrastructure management (sites, racks, and devices)
|
||||
* `extras`: Additional features not considered part of the core data model
|
||||
* `ipam`: IP address management (VRFs, prefixes, IP addresses, and VLANs)
|
||||
* `secrets`: Encrypted storage of sensitive data (e.g. login credentials)
|
||||
* `tenancy`: Tenants (such as customers) to which NetBox objects may be assigned
|
||||
* `utilities`: Resources which are not user-facing (extendable classes, etc.)
|
||||
* `virtualization`: Virtual machines and clusters
|
||||
|
||||
## 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/). The following exceptions are noted:
|
||||
|
||||
* [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations. See `scripts/cibuild.sh`.
|
||||
* Constants may be imported via wildcard (for example, `from .constants import *`).
|
||||
81
docs/development/release-checklist.md
Normal file
81
docs/development/release-checklist.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Minor Version Bumps
|
||||
|
||||
## Update Requirements
|
||||
|
||||
Required Python packages are maintained in two files. `base_requirements.txt` contains a list of all the packages required by NetBox. Some of them may be pinned to a specific version of the package due to a known issue. For example:
|
||||
|
||||
```
|
||||
# https://github.com/encode/django-rest-framework/issues/6053
|
||||
djangorestframework==3.8.1
|
||||
```
|
||||
|
||||
The other file is `requirements.txt`, which lists each of the required packages pinned to its current stable version. When NetBox is installed, the Python environment is configured to match this file. This helps ensure that a new release of a dependency doesn't break NetBox.
|
||||
|
||||
Every minor version release should refresh `requirements.txt` so that it lists the most recent stable release of each package. To do this:
|
||||
|
||||
1. Create a new virtual environment.
|
||||
2. Install the latest version of all required packages via pip:
|
||||
|
||||
```
|
||||
pip install -U -r base_requirements.txt
|
||||
```
|
||||
|
||||
3. Run all tests and check that the UI and API function as expected.
|
||||
4. Update the package versions in `requirements.txt` as appropriate.
|
||||
|
||||
## Update Static Libraries
|
||||
|
||||
Update the following static libraries to their most recent stable release:
|
||||
|
||||
* Bootstrap 3
|
||||
* Font Awesome 4
|
||||
* jQuery
|
||||
* jQuery UI
|
||||
|
||||
## Manually Perform a New Install
|
||||
|
||||
Create a new installation of NetBox by following [the current documentation](http://netbox.readthedocs.io/en/latest/). This should be a manual process, so that issues with the documentation can be identified and corrected.
|
||||
|
||||
## Close the Release Milestone
|
||||
|
||||
Close the release milestone on GitHub. Ensure that there are no remaining open issues associated with it.
|
||||
|
||||
---
|
||||
|
||||
# All Releases
|
||||
|
||||
## Verify CI Build Status
|
||||
|
||||
Ensure that continuous integration testing on the `develop` branch is completing successfully.
|
||||
|
||||
## Update Version and Changelog
|
||||
|
||||
Update the `VERSION` constant in `settings.py` to the new release version and add the current date to the release notes in `CHANGELOG.md`.
|
||||
|
||||
## Submit a Pull Request
|
||||
|
||||
Submit a pull request title **"Release vX.Y.X"** to merge the `develop` branch into `master`. Include a brief change log listing the features, improvements, and/or bugs addressed in the release.
|
||||
|
||||
Once CI has completed on the PR, merge it.
|
||||
|
||||
## Create a New Release
|
||||
|
||||
Draft a [new release](https://github.com/digitalocean/netbox/releases/new) with the following parameters.
|
||||
|
||||
* **Tag:** Current version (e.g. `v2.3.4`)
|
||||
* **Target:** `master`
|
||||
* **Title:** Version and date (e.g. `v2.3.4 - 2018-08-02`)
|
||||
|
||||
Copy the description from the pull request into the release notes.
|
||||
|
||||
## Update the Development Version
|
||||
|
||||
On the `develop` branch, update `VERSION` in `settings.py` to point to the next release. For example, if you just released v2.3.4, set:
|
||||
|
||||
```
|
||||
VERSION = 'v2.3.5-dev'
|
||||
```
|
||||
|
||||
## Announce the Release
|
||||
|
||||
Announce the release on the [mailing list](https://groups.google.com/forum/#!forum/netbox-discuss). Include a link to the release and the (HTML-formatted) release notes.
|
||||
@@ -1,3 +1,5 @@
|
||||

|
||||
|
||||
# What is NetBox?
|
||||
|
||||
NetBox is an open source web application designed to help manage and document computer networks. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. It encompasses the following aspects of network management:
|
||||
@@ -10,7 +12,7 @@ NetBox is an open source web application designed to help manage and document co
|
||||
* **Data circuits** - Long-haul communications circuits and providers
|
||||
* **Secrets** - Encrypted storage of sensitive credentials
|
||||
|
||||
# What NetBox Isn't
|
||||
# What NetBox Is Not
|
||||
|
||||
While NetBox strives to cover many areas of network management, the scope of its feature set is necessarily limited. This ensures that development focuses on core functionality and that scope creep is reasonably contained. To that end, it might help to provide some examples of functionality that NetBox **does not** provide:
|
||||
|
||||
@@ -42,13 +44,15 @@ When given a choice between a relatively simple [80% solution](https://en.wikipe
|
||||
|
||||
NetBox is built on the [Django](https://djangoproject.com/) Python framework and utilizes a [PostgreSQL](https://www.postgresql.org/) database. It runs as a WSGI service behind your choice of HTTP server.
|
||||
|
||||
| Function | Component |
|
||||
|--------------|-------------------|
|
||||
| HTTP Service | nginx or Apache |
|
||||
| WSGI Service | gunicorn or uWSGI |
|
||||
| Application | Django/Python |
|
||||
| Database | PostgreSQL 9.4+ |
|
||||
| Function | Component |
|
||||
|--------------------|-------------------|
|
||||
| HTTP service | nginx or Apache |
|
||||
| WSGI service | gunicorn or uWSGI |
|
||||
| Application | Django/Python |
|
||||
| Database | PostgreSQL 9.4+ |
|
||||
| Task queuing | Redis/django-rq |
|
||||
| Live device access | NAPALM |
|
||||
|
||||
# Getting Started
|
||||
|
||||
See the [installation guide](installation/postgresql.md) for help getting NetBox up and running quickly.
|
||||
See the [installation guide](installation/index.md) for help getting NetBox up and running quickly.
|
||||
|
||||
@@ -56,7 +56,7 @@ To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https:
|
||||
## Option B: Apache
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y apache2
|
||||
# apt-get install -y apache2 libapache2-mod-wsgi-py3
|
||||
```
|
||||
|
||||
Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately):
|
||||
14
docs/installation/index.md
Normal file
14
docs/installation/index.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Installation
|
||||
|
||||
The following sections detail how to set up a new instance of NetBox:
|
||||
|
||||
1. [PostgreSQL database](1-postgresql.md)
|
||||
2. [NetBox components](2-netbox.md)
|
||||
3. [HTTP dameon](3-http-daemon.md)
|
||||
4. [LDAP authentication](4-ldap.md) (optional)
|
||||
|
||||
# Upgrading
|
||||
|
||||
If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md).
|
||||
|
||||
NetBox v2.5 and later requires Python 3. Please see the instruction for [migrating to Python 3](migrating-to-python3.md) if you are still using Python 2.
|
||||
@@ -3,34 +3,36 @@
|
||||
!!! warning
|
||||
Beginning with v2.5, NetBox will no longer support Python 2. It is strongly recommended that you upgrade to Python 3 as soon as possible.
|
||||
|
||||
Remove Python 2 packages
|
||||
## Ubuntu
|
||||
|
||||
Remove the Python2 version of gunicorn:
|
||||
|
||||
```no-highlight
|
||||
# apt-get remove --purge -y python-dev python-pip
|
||||
# pip uninstall -y gunicorn
|
||||
```
|
||||
|
||||
Install Python 3 packages
|
||||
Install Python3 and pip3, Python's package management tool:
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y python3 python3-dev python3-pip
|
||||
# apt-get update
|
||||
# apt-get install -y python3 python3-dev python3-setuptools
|
||||
# easy_install3 pip
|
||||
```
|
||||
|
||||
Install Python Packages
|
||||
Install the Python3 packages required by NetBox:
|
||||
|
||||
```no-highlight
|
||||
# cd /opt/netbox
|
||||
# pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
Gunicorn Update
|
||||
Replace gunicorn with the Python3 version:
|
||||
|
||||
```no-highlight
|
||||
# pip uninstall gunicorn
|
||||
# pip3 install gunicorn
|
||||
```
|
||||
|
||||
Re-install LDAP Module (optional if using LDAP for auth)
|
||||
If using LDAP authentication, install the `django-auth-ldap` package:
|
||||
|
||||
```no-highlight
|
||||
sudo pip3 install django-auth-ldap
|
||||
# pip3 install django-auth-ldap
|
||||
```
|
||||
|
||||
74
mkdocs.yml
74
mkdocs.yml
@@ -1,35 +1,51 @@
|
||||
site_name: NetBox
|
||||
repo_url: https://github.com/digitalocean/netbox
|
||||
|
||||
pages:
|
||||
- 'Introduction': 'index.md'
|
||||
- 'Installation':
|
||||
- 'PostgreSQL': 'installation/postgresql.md'
|
||||
- 'NetBox': 'installation/netbox.md'
|
||||
- 'Web Server': 'installation/web-server.md'
|
||||
- 'LDAP (Optional)': 'installation/ldap.md'
|
||||
- 'Upgrading': 'installation/upgrading.md'
|
||||
- 'Migrating to Python3': 'installation/migrating-to-python3.md'
|
||||
- 'Configuration':
|
||||
- 'Mandatory Settings': 'configuration/mandatory-settings.md'
|
||||
- 'Optional Settings': 'configuration/optional-settings.md'
|
||||
- 'Data Model':
|
||||
- 'Circuits': 'data-model/circuits.md'
|
||||
- 'DCIM': 'data-model/dcim.md'
|
||||
- 'IPAM': 'data-model/ipam.md'
|
||||
- 'Secrets': 'data-model/secrets.md'
|
||||
- 'Tenancy': 'data-model/tenancy.md'
|
||||
- 'Virtualization': 'data-model/virtualization.md'
|
||||
- 'Extras': 'data-model/extras.md'
|
||||
- 'API':
|
||||
- 'Overview': 'api/overview.md'
|
||||
- 'Authentication': 'api/authentication.md'
|
||||
- 'Working with Secrets': 'api/working-with-secrets.md'
|
||||
- 'Examples': 'api/examples.md'
|
||||
- 'Miscellaneous':
|
||||
- 'Reports': 'miscellaneous/reports.md'
|
||||
- 'Shell': 'miscellaneous/shell.md'
|
||||
- 'Development':
|
||||
- 'Utility Views': 'development/utility-views.md'
|
||||
- Introduction: 'index.md'
|
||||
- Installation:
|
||||
- Installing NetBox: 'installation/index.md'
|
||||
- 1. PostgreSQL: 'installation/1-postgresql.md'
|
||||
- 2. NetBox: 'installation/2-netbox.md'
|
||||
- 3. HTTP Daemon: 'installation/3-http-daemon.md'
|
||||
- 4. LDAP (Optional): 'installation/4-ldap.md'
|
||||
- Upgrading NetBox: 'installation/upgrading.md'
|
||||
- Migrating to Python3: 'installation/migrating-to-python3.md'
|
||||
- Configuration:
|
||||
- Configuring NetBox: 'configuration/index.md'
|
||||
- Required Settings: 'configuration/required-settings.md'
|
||||
- Optional Settings: 'configuration/optional-settings.md'
|
||||
- Core Functionality:
|
||||
- IP Address Management: 'core-functionality/ipam.md'
|
||||
- VLANs: 'core-functionality/vlans.md'
|
||||
- Sites and Racks: 'core-functionality/sites-and-racks.md'
|
||||
- Devices: 'core-functionality/devices.md'
|
||||
- Virtual Machines: 'core-functionality/virtual-machines.md'
|
||||
- Services: 'core-functionality/services.md'
|
||||
- Circuits: 'core-functionality/circuits.md'
|
||||
- Secrets: 'core-functionality/secrets.md'
|
||||
- Tenancy: 'core-functionality/tenancy.md'
|
||||
- Additional Features:
|
||||
- Tags: 'additional-features/tags.md'
|
||||
- Custom Fields: 'additional-features/custom-fields.md'
|
||||
- Context Data: 'additional-features/context-data.md'
|
||||
- Export Templates: 'additional-features/export-templates.md'
|
||||
- Graphs: 'additional-features/graphs.md'
|
||||
- Topology Maps: 'additional-features/topology-maps.md'
|
||||
- Reports: 'additional-features/reports.md'
|
||||
- Webhooks: 'additional-features/webhooks.md'
|
||||
- Change Logging: 'additional-features/change-logging.md'
|
||||
- Administration:
|
||||
- Replicating NetBox: 'administration/replicating-netbox.md'
|
||||
- API:
|
||||
- Overview: 'api/overview.md'
|
||||
- Authentication: 'api/authentication.md'
|
||||
- Working with Secrets: 'api/working-with-secrets.md'
|
||||
- Examples: 'api/examples.md'
|
||||
- Development:
|
||||
- Introduction: 'development/index.md'
|
||||
- Utility Views: 'development/utility-views.md'
|
||||
- Release Checklist: 'development/release-checklist.md'
|
||||
|
||||
markdown_extensions:
|
||||
- admonition:
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import serializers
|
||||
from taggit.models import Tag
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from circuits.constants import CIRCUIT_STATUS_CHOICES
|
||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from utilities.api import ChoiceFieldSerializer, TagField, ValidatedModelSerializer, WritableNestedSerializer
|
||||
from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderSerializer(CustomFieldModelSerializer):
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
class ProviderSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
@@ -57,12 +57,12 @@ class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitSerializer(CustomFieldModelSerializer):
|
||||
class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
provider = NestedProviderSerializer()
|
||||
status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES, required=False)
|
||||
status = ChoiceField(choices=CIRCUIT_STATUS_CHOICES, required=False)
|
||||
type = NestedCircuitTypeSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
|
||||
@@ -9,8 +9,3 @@ class CircuitsConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
import circuits.signals
|
||||
|
||||
# register webhook signals
|
||||
from extras.webhooks import register_signals
|
||||
from .models import Circuit, Provider
|
||||
register_signals([Circuit, Provider])
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.14 on 2018-07-31 02:25
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('circuits', '0001_initial'), ('circuits', '0002_auto_20160622_1821'), ('circuits', '0003_provider_32bit_asn_support'), ('circuits', '0004_circuit_add_tenant'), ('circuits', '0005_circuit_add_upstream_speed'), ('circuits', '0006_terminations'), ('circuits', '0007_circuit_add_description'), ('circuits', '0008_circuittermination_interface_protect_on_delete'), ('circuits', '0009_unicode_literals'), ('circuits', '0010_circuit_status')]
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0001_initial'),
|
||||
('dcim', '0022_color_names_to_rgb'),
|
||||
('tenancy', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Provider',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateField(auto_now_add=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('asn', dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN')),
|
||||
('account', models.CharField(blank=True, max_length=30, verbose_name='Account number')),
|
||||
('portal_url', models.URLField(blank=True, verbose_name='Portal')),
|
||||
('noc_contact', models.TextField(blank=True, verbose_name='NOC contact')),
|
||||
('admin_contact', models.TextField(blank=True, verbose_name='Admin contact')),
|
||||
('comments', models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CircuitType',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Circuit',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateField(auto_now_add=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('cid', models.CharField(max_length=50, verbose_name='Circuit ID')),
|
||||
('install_date', models.DateField(blank=True, null=True, verbose_name='Date installed')),
|
||||
('commit_rate', models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.Provider')),
|
||||
('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.CircuitType')),
|
||||
('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.Tenant')),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('status', models.PositiveSmallIntegerField(choices=[[2, 'Planned'], [3, 'Provisioning'], [1, 'Active'], [4, 'Offline'], [0, 'Deprovisioning'], [5, 'Decommissioned']], default=1))
|
||||
],
|
||||
options={
|
||||
'ordering': ['provider', 'cid'],
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='circuit',
|
||||
unique_together=set([('provider', 'cid')]),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CircuitTermination',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('term_side', models.CharField(choices=[('A', 'A'), ('Z', 'Z')], max_length=1, verbose_name='Termination')),
|
||||
('port_speed', models.PositiveIntegerField(verbose_name='Port speed (Kbps)')),
|
||||
('upstream_speed', models.PositiveIntegerField(blank=True, help_text='Upstream speed, if different from port speed', null=True, verbose_name='Upstream speed (Kbps)')),
|
||||
('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name='Cross-connect ID')),
|
||||
('pp_info', models.CharField(blank=True, max_length=100, verbose_name='Patch panel/port(s)')),
|
||||
('circuit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.Circuit')),
|
||||
('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_termination', to='dcim.Interface')),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='dcim.Site')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['circuit', 'term_side'],
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='circuittermination',
|
||||
unique_together=set([('circuit', 'term_side')]),
|
||||
),
|
||||
]
|
||||
@@ -60,7 +60,6 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
serializer = 'circuits.api.serializers.ProviderSerializer'
|
||||
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
||||
|
||||
class Meta:
|
||||
@@ -99,7 +98,6 @@ class CircuitType(ChangeLoggedModel):
|
||||
unique=True
|
||||
)
|
||||
|
||||
serializer = 'circuits.api.serializers.CircuitTypeSerializer'
|
||||
csv_headers = ['name', 'slug']
|
||||
|
||||
class Meta:
|
||||
@@ -174,7 +172,6 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
serializer = 'circuits.api.serializers.CircuitSerializer'
|
||||
csv_headers = [
|
||||
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||
]
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from dcim.models import Site
|
||||
from extras.constants import GRAPH_TYPE_PROVIDER
|
||||
from extras.models import Graph
|
||||
from users.models import Token
|
||||
from utilities.testing import HttpStatusMixin
|
||||
from utilities.testing import APITestCase
|
||||
|
||||
|
||||
class ProviderTest(HttpStatusMixin, APITestCase):
|
||||
class ProviderTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(ProviderTest, self).setUp()
|
||||
|
||||
self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
|
||||
self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
|
||||
@@ -128,13 +123,11 @@ class ProviderTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(Provider.objects.count(), 2)
|
||||
|
||||
|
||||
class CircuitTypeTest(HttpStatusMixin, APITestCase):
|
||||
class CircuitTypeTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(CircuitTypeTest, self).setUp()
|
||||
|
||||
self.circuittype1 = CircuitType.objects.create(name='Test Circuit Type 1', slug='test-circuit-type-1')
|
||||
self.circuittype2 = CircuitType.objects.create(name='Test Circuit Type 2', slug='test-circuit-type-2')
|
||||
@@ -195,13 +188,11 @@ class CircuitTypeTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(CircuitType.objects.count(), 2)
|
||||
|
||||
|
||||
class CircuitTest(HttpStatusMixin, APITestCase):
|
||||
class CircuitTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(CircuitTest, self).setUp()
|
||||
|
||||
self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
|
||||
self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
|
||||
@@ -303,13 +294,11 @@ class CircuitTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(Circuit.objects.count(), 2)
|
||||
|
||||
|
||||
class CircuitTerminationTest(HttpStatusMixin, APITestCase):
|
||||
class CircuitTerminationTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(CircuitTerminationTest, self).setUp()
|
||||
|
||||
provider = Provider.objects.create(name='Test Provider', slug='test-provider')
|
||||
circuittype = CircuitType.objects.create(name='Test Circuit Type', slug='test-circuit-type')
|
||||
|
||||
@@ -76,7 +76,7 @@ class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'circuits.change_provider'
|
||||
cls = Provider
|
||||
queryset = Provider.objects.all()
|
||||
filter = filters.ProviderFilter
|
||||
table = tables.ProviderTable
|
||||
form = forms.ProviderBulkEditForm
|
||||
@@ -85,7 +85,7 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_provider'
|
||||
cls = Provider
|
||||
queryset = Provider.objects.all()
|
||||
filter = filters.ProviderFilter
|
||||
table = tables.ProviderTable
|
||||
default_return_url = 'circuits:provider_list'
|
||||
@@ -121,7 +121,6 @@ class CircuitTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuittype'
|
||||
cls = CircuitType
|
||||
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
|
||||
table = tables.CircuitTypeTable
|
||||
default_return_url = 'circuits:circuittype_list'
|
||||
@@ -193,7 +192,6 @@ class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'circuits.change_circuit'
|
||||
cls = Circuit
|
||||
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
|
||||
filter = filters.CircuitFilter
|
||||
table = tables.CircuitTable
|
||||
@@ -203,7 +201,6 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuit'
|
||||
cls = Circuit
|
||||
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
|
||||
filter = filters.CircuitFilter
|
||||
table = tables.CircuitTable
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from taggit.models import Tag
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination
|
||||
from dcim.constants import (
|
||||
@@ -20,7 +20,7 @@ from ipam.models import IPAddress, VLAN
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from users.api.serializers import NestedUserSerializer
|
||||
from utilities.api import (
|
||||
ChoiceFieldSerializer, SerializedPKRelatedField, TagField, TimeZoneField, ValidatedModelSerializer,
|
||||
ChoiceField, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer,
|
||||
WritableNestedSerializer,
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
@@ -50,12 +50,12 @@ class RegionSerializer(serializers.ModelSerializer):
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteSerializer(CustomFieldModelSerializer):
|
||||
status = ChoiceFieldSerializer(choices=SITE_STATUS_CHOICES, required=False)
|
||||
class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
status = ChoiceField(choices=SITE_STATUS_CHOICES, required=False)
|
||||
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
time_zone = TimeZoneField(required=False)
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
@@ -118,14 +118,14 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackSerializer(CustomFieldModelSerializer):
|
||||
class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
site = NestedSiteSerializer()
|
||||
group = NestedRackGroupSerializer(required=False, allow_null=True)
|
||||
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||
type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES, required=False)
|
||||
width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES, required=False)
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True)
|
||||
width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
@@ -220,12 +220,12 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||
# Device types
|
||||
#
|
||||
|
||||
class DeviceTypeSerializer(CustomFieldModelSerializer):
|
||||
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
manufacturer = NestedManufacturerSerializer()
|
||||
interface_ordering = ChoiceFieldSerializer(choices=IFACE_ORDERING_CHOICES, required=False)
|
||||
subdevice_role = ChoiceFieldSerializer(choices=SUBDEVICE_ROLE_CHOICES, required=False)
|
||||
interface_ordering = ChoiceField(choices=IFACE_ORDERING_CHOICES, required=False)
|
||||
subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True)
|
||||
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
@@ -299,7 +299,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES, required=False)
|
||||
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
@@ -362,7 +362,7 @@ class NestedPlatformSerializer(WritableNestedSerializer):
|
||||
#
|
||||
|
||||
# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
|
||||
class DeviceIPAddressSerializer(serializers.ModelSerializer):
|
||||
class DeviceIPAddressSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||
|
||||
class Meta:
|
||||
@@ -371,7 +371,7 @@ class DeviceIPAddressSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
# Cannot import virtualization.api.NestedClusterSerializer due to circular dependency
|
||||
class NestedClusterSerializer(serializers.ModelSerializer):
|
||||
class NestedClusterSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
||||
|
||||
class Meta:
|
||||
@@ -380,7 +380,7 @@ class NestedClusterSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
# Cannot import NestedVirtualChassisSerializer due to circular dependency
|
||||
class DeviceVirtualChassisSerializer(serializers.ModelSerializer):
|
||||
class DeviceVirtualChassisSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||
master = NestedDeviceSerializer()
|
||||
|
||||
@@ -389,22 +389,22 @@ class DeviceVirtualChassisSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'master']
|
||||
|
||||
|
||||
class DeviceSerializer(CustomFieldModelSerializer):
|
||||
class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
device_role = NestedDeviceRoleSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||
site = NestedSiteSerializer()
|
||||
rack = NestedRackSerializer(required=False, allow_null=True)
|
||||
face = ChoiceFieldSerializer(choices=RACK_FACE_CHOICES, required=False)
|
||||
status = ChoiceFieldSerializer(choices=DEVICE_STATUS_CHOICES, required=False)
|
||||
face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True)
|
||||
status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False)
|
||||
primary_ip = DeviceIPAddressSerializer(read_only=True)
|
||||
primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip6 = DeviceIPAddressSerializer(required=False, allow_null=True)
|
||||
parent_device = serializers.SerializerMethodField()
|
||||
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
||||
virtual_chassis = DeviceVirtualChassisSerializer(required=False, allow_null=True)
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
@@ -459,9 +459,9 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
class ConsoleServerPortSerializer(ValidatedModelSerializer):
|
||||
class ConsoleServerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
@@ -482,10 +482,10 @@ class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
# Console ports
|
||||
#
|
||||
|
||||
class ConsolePortSerializer(ValidatedModelSerializer):
|
||||
class ConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
cs_port = NestedConsoleServerPortSerializer(required=False, allow_null=True)
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
@@ -496,9 +496,9 @@ class ConsolePortSerializer(ValidatedModelSerializer):
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
class PowerOutletSerializer(ValidatedModelSerializer):
|
||||
class PowerOutletSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
@@ -519,10 +519,10 @@ class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
# Power ports
|
||||
#
|
||||
|
||||
class PowerPortSerializer(ValidatedModelSerializer):
|
||||
class PowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
power_outlet = NestedPowerOutletSerializer(required=False, allow_null=True)
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
@@ -569,14 +569,14 @@ class InterfaceVLANSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
||||
|
||||
|
||||
class InterfaceSerializer(ValidatedModelSerializer):
|
||||
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES, required=False)
|
||||
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||
interface_connection = serializers.SerializerMethodField(read_only=True)
|
||||
circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True)
|
||||
mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES, required=False)
|
||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
|
||||
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
queryset=VLAN.objects.all(),
|
||||
@@ -584,7 +584,7 @@ class InterfaceSerializer(ValidatedModelSerializer):
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
@@ -640,10 +640,10 @@ class InterfaceSerializer(ValidatedModelSerializer):
|
||||
# Device bays
|
||||
#
|
||||
|
||||
class DeviceBaySerializer(ValidatedModelSerializer):
|
||||
class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
installed_device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
@@ -662,12 +662,12 @@ class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
class InventoryItemSerializer(ValidatedModelSerializer):
|
||||
class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
# Provide a default value to satisfy UniqueTogetherValidator
|
||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||
manufacturer = NestedManufacturerSerializer()
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
@@ -684,7 +684,7 @@ class InventoryItemSerializer(ValidatedModelSerializer):
|
||||
class InterfaceConnectionSerializer(ValidatedModelSerializer):
|
||||
interface_a = NestedInterfaceSerializer()
|
||||
interface_b = NestedInterfaceSerializer()
|
||||
connection_status = ChoiceFieldSerializer(choices=CONNECTION_STATUS_CHOICES, required=False)
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
@@ -704,7 +704,7 @@ class ContextualInterfaceConnectionSerializer(serializers.ModelSerializer):
|
||||
A read-only representation of an InterfaceConnection from the perspective of either of its two connected Interfaces.
|
||||
"""
|
||||
interface = serializers.SerializerMethodField(read_only=True)
|
||||
connection_status = ChoiceFieldSerializer(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
@@ -718,9 +718,9 @@ class ContextualInterfaceConnectionSerializer(serializers.ModelSerializer):
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisSerializer(ValidatedModelSerializer):
|
||||
class VirtualChassisSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
master = NestedDeviceSerializer()
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
|
||||
@@ -260,7 +260,7 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
import napalm
|
||||
except ImportError:
|
||||
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
||||
from napalm.base.exceptions import ConnectAuthError, ModuleImportError
|
||||
from napalm.base.exceptions import ModuleImportError
|
||||
|
||||
# Validate the configured driver
|
||||
try:
|
||||
@@ -274,16 +274,8 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
if not request.user.has_perm('dcim.napalm_read'):
|
||||
return HttpResponseForbidden()
|
||||
|
||||
# Validate requested NAPALM methods
|
||||
# Connect to the device
|
||||
napalm_methods = request.GET.getlist('method')
|
||||
for method in napalm_methods:
|
||||
if not hasattr(driver, method):
|
||||
return HttpResponseBadRequest("Unknown NAPALM method: {}".format(method))
|
||||
elif not method.startswith('get_'):
|
||||
return HttpResponseBadRequest("Unsupported NAPALM method: {}".format(method))
|
||||
|
||||
# Connect to the device and execute the requested methods
|
||||
# TODO: Improve error handling
|
||||
response = OrderedDict([(m, None) for m in napalm_methods])
|
||||
ip_address = str(device.primary_ip.address.ip)
|
||||
optional_args = settings.NAPALM_ARGS.copy()
|
||||
@@ -298,12 +290,23 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
)
|
||||
try:
|
||||
d.open()
|
||||
for method in napalm_methods:
|
||||
response[method] = getattr(d, method)()
|
||||
except Exception as e:
|
||||
raise ServiceUnavailable("Error connecting to the device at {}: {}".format(ip_address, e))
|
||||
|
||||
# Validate and execute each specified NAPALM method
|
||||
for method in napalm_methods:
|
||||
if not hasattr(driver, method):
|
||||
response[method] = {'error': 'Unknown NAPALM method'}
|
||||
continue
|
||||
if not method.startswith('get_'):
|
||||
response[method] = {'error': 'Only get_* NAPALM methods are supported'}
|
||||
continue
|
||||
try:
|
||||
response[method] = getattr(d, method)()
|
||||
except NotImplementedError:
|
||||
response[method] = {'error': 'Method not implemented for NAPALM driver {}'.format(driver)}
|
||||
d.close()
|
||||
|
||||
return Response(response)
|
||||
|
||||
|
||||
|
||||
@@ -10,8 +10,3 @@ class DCIMConfig(AppConfig):
|
||||
def ready(self):
|
||||
|
||||
import dcim.signals
|
||||
|
||||
# register webhook signals
|
||||
from extras.webhooks import register_signals
|
||||
from .models import Site, Rack, RackGroup, Device, Interface
|
||||
register_signals([Site, Rack, Device, Interface, RackGroup])
|
||||
|
||||
@@ -93,6 +93,11 @@ IFACE_FF_STACKWISE_PLUS = 5050
|
||||
IFACE_FF_FLEXSTACK = 5100
|
||||
IFACE_FF_FLEXSTACK_PLUS = 5150
|
||||
IFACE_FF_JUNIPER_VCP = 5200
|
||||
IFACE_FF_SUMMITSTACK = 5300
|
||||
IFACE_FF_SUMMITSTACK128 = 5310
|
||||
IFACE_FF_SUMMITSTACK256 = 5320
|
||||
IFACE_FF_SUMMITSTACK512 = 5330
|
||||
|
||||
# Other
|
||||
IFACE_FF_OTHER = 32767
|
||||
|
||||
@@ -168,6 +173,10 @@ IFACE_FF_CHOICES = [
|
||||
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
|
||||
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
|
||||
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
|
||||
[IFACE_FF_SUMMITSTACK, 'Extreme SummitStack'],
|
||||
[IFACE_FF_SUMMITSTACK128, 'Extreme SummitStack-128'],
|
||||
[IFACE_FF_SUMMITSTACK256, 'Extreme SummitStack-256'],
|
||||
[IFACE_FF_SUMMITSTACK512, 'Extreme SummitStack-512'],
|
||||
]
|
||||
],
|
||||
[
|
||||
|
||||
@@ -112,6 +112,10 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class RackGroupFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@@ -127,6 +131,15 @@ class RackGroupFilter(django_filters.FilterSet):
|
||||
model = RackGroup
|
||||
fields = ['site_id', 'name', 'slug']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(slug__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class RackRoleFilter(django_filters.FilterSet):
|
||||
|
||||
@@ -521,7 +534,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
Q(name__icontains=value) |
|
||||
Q(serial__icontains=value.strip()) |
|
||||
Q(inventory_items__serial__icontains=value.strip()) |
|
||||
Q(asset_tag=value.strip()) |
|
||||
Q(asset_tag__icontains=value.strip()) |
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
|
||||
@@ -1795,7 +1795,7 @@ class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
|
||||
# Compile VLAN choices
|
||||
vlan_choices = []
|
||||
|
||||
# Add global VLANs
|
||||
# Add non-grouped global VLANs
|
||||
global_vlans = VLAN.objects.filter(site=None, group=None).exclude(pk__in=assigned_vlans)
|
||||
vlan_choices.append((
|
||||
'Global', [(vlan.pk, vlan) for vlan in global_vlans])
|
||||
@@ -1808,16 +1808,15 @@ class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
|
||||
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
|
||||
)
|
||||
|
||||
parent = self.instance.parent
|
||||
if parent is not None:
|
||||
site = getattr(self.instance.parent, 'site', None)
|
||||
if site is not None:
|
||||
|
||||
# Add site VLANs
|
||||
if parent.site:
|
||||
site_vlans = VLAN.objects.filter(site=parent.site, group=None).exclude(pk__in=assigned_vlans)
|
||||
vlan_choices.append((parent.site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
|
||||
# Add non-grouped site VLANs
|
||||
site_vlans = VLAN.objects.filter(site=site, group=None).exclude(pk__in=assigned_vlans)
|
||||
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
|
||||
|
||||
# Add grouped site VLANs
|
||||
for group in VLANGroup.objects.filter(site=parent.site):
|
||||
for group in VLANGroup.objects.filter(site=site):
|
||||
site_group_vlans = VLAN.objects.filter(group=group).exclude(pk__in=assigned_vlans)
|
||||
vlan_choices.append((
|
||||
'{} / {}'.format(group.site.name, group.name),
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.14 on 2018-07-31 02:06
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import utilities.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('dcim', '0002_auto_20160622_1821'), ('dcim', '0003_auto_20160628_1721'), ('dcim', '0004_auto_20160701_2049'), ('dcim', '0005_auto_20160706_1722'), ('dcim', '0006_add_device_primary_ip4_ip6'), ('dcim', '0007_device_copy_primary_ip'), ('dcim', '0008_device_remove_primary_ip'), ('dcim', '0009_site_32bit_asn_support'), ('dcim', '0010_devicebay_installed_device_set_null'), ('dcim', '0011_devicetype_part_number'), ('dcim', '0012_site_rack_device_add_tenant'), ('dcim', '0013_add_interface_form_factors'), ('dcim', '0014_rack_add_type_width'), ('dcim', '0015_rack_add_u_height_validator'), ('dcim', '0016_module_add_manufacturer'), ('dcim', '0017_rack_add_role'), ('dcim', '0018_device_add_asset_tag'), ('dcim', '0019_new_iface_form_factors'), ('dcim', '0020_rack_desc_units'), ('dcim', '0021_add_ff_flexstack'), ('dcim', '0022_color_names_to_rgb')]
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0001_initial'),
|
||||
('ipam', '0001_initial'),
|
||||
('tenancy', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='rack',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Rack'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='device_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cs_port_templates', to='dcim.DeviceType'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='device',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cs_ports', to='dcim.Device'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleporttemplate',
|
||||
name='device_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='console_port_templates', to='dcim.DeviceType'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='cs_port',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_console', to='dcim.ConsoleServerPort', verbose_name=b'Console server port'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='device',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='console_ports', to='dcim.Device'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rackgroup',
|
||||
unique_together=set([('site', 'name'), ('site', 'slug')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rack',
|
||||
unique_together=set([('site', 'facility_id'), ('site', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='powerporttemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='powerport',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='poweroutlettemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='poweroutlet',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='module',
|
||||
unique_together=set([('device', 'parent', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='interfacetemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='mac_address',
|
||||
field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name=b'MAC Address'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='interface',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='subdevice_role',
|
||||
field=models.NullBooleanField(choices=[(None, b'None'), (True, b'Parent'), (False, b'Child')], default=None, help_text=b'Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name=b'Parent/child status'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='devicetype',
|
||||
unique_together=set([('manufacturer', 'slug'), ('manufacturer', 'model')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='device',
|
||||
unique_together=set([('rack', 'position', 'face')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleserverporttemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleserverport',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleporttemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleport',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DeviceBay',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, verbose_name=b'Name')),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bays', to='dcim.Device')),
|
||||
('installed_device', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_bay', to='dcim.Device')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DeviceBayTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bay_templates', to='dcim.DeviceType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='devicebaytemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='devicebay',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='primary_ip4',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name=b'Primary IPv4'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='primary_ip6',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name=b'Primary IPv6'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='asn',
|
||||
field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicebay',
|
||||
name='installed_device',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent_bay', to='dcim.Device'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='part_number',
|
||||
field=models.CharField(blank=True, help_text=b'Discrete part number (optional)', max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sites', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='type',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[(100, b'2-post frame'), (200, b'4-post frame'), (300, b'4-post cabinet'), (1000, b'Wall-mounted frame'), (1100, b'Wall-mounted cabinet')], null=True, verbose_name=b'Type'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='width',
|
||||
field=models.PositiveSmallIntegerField(choices=[(19, b'19 inches'), (23, b'23 inches')], default=19, help_text=b'Rail-to-rail width', verbose_name=b'Width'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='u_height',
|
||||
field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name=b'Height (U)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='module',
|
||||
name='manufacturer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='modules', to='dcim.Manufacturer'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RackRole',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('color', utilities.fields.ColorField(max_length=6)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='role',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='dcim.RackRole'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='asset_tag',
|
||||
field=utilities.fields.NullableCharField(blank=True, help_text=b'A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name=b'Asset tag'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='desc_units',
|
||||
field=models.BooleanField(default=False, help_text=b'Units are numbered top-to-bottom', verbose_name=b'Descending units'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='position',
|
||||
field=models.PositiveSmallIntegerField(blank=True, help_text=b'The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Position (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicerole',
|
||||
name='color',
|
||||
field=utilities.fields.ColorField(max_length=6),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,436 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.14 on 2018-07-31 02:13
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
from django.conf import settings
|
||||
import django.contrib.postgres.fields
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import mptt.fields
|
||||
import utilities.fields
|
||||
|
||||
|
||||
def copy_site_from_rack(apps, schema_editor):
|
||||
Device = apps.get_model('dcim', 'Device')
|
||||
for device in Device.objects.all():
|
||||
device.site = device.rack.site
|
||||
device.save()
|
||||
|
||||
|
||||
def rpc_client_to_napalm_driver(apps, schema_editor):
|
||||
"""
|
||||
Migrate legacy RPC clients to their respective NAPALM drivers
|
||||
"""
|
||||
Platform = apps.get_model('dcim', 'Platform')
|
||||
|
||||
Platform.objects.filter(rpc_client='juniper-junos').update(napalm_driver='junos')
|
||||
Platform.objects.filter(rpc_client='cisco-ios').update(napalm_driver='ios')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('dcim', '0023_devicetype_comments'), ('dcim', '0024_site_add_contact_fields'), ('dcim', '0025_devicetype_add_interface_ordering'), ('dcim', '0026_add_rack_reservations'), ('dcim', '0027_device_add_site'), ('dcim', '0028_device_copy_rack_to_site'), ('dcim', '0029_allow_rackless_devices'), ('dcim', '0030_interface_add_lag'), ('dcim', '0031_regions'), ('dcim', '0032_device_increase_name_length'), ('dcim', '0033_rackreservation_rack_editable'), ('dcim', '0034_rename_module_to_inventoryitem'), ('dcim', '0035_device_expand_status_choices'), ('dcim', '0036_add_ff_juniper_vcp'), ('dcim', '0037_unicode_literals'), ('dcim', '0038_wireless_interfaces'), ('dcim', '0039_interface_add_enabled_mtu'), ('dcim', '0040_inventoryitem_add_asset_tag_description'), ('dcim', '0041_napalm_integration'), ('dcim', '0042_interface_ff_10ge_cx4'), ('dcim', '0043_device_component_name_lengths')]
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('dcim', '0022_color_names_to_rgb'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='contact_email',
|
||||
field=models.EmailField(blank=True, max_length=254, verbose_name=b'Contact E-mail'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='contact_name',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='contact_phone',
|
||||
field=models.CharField(blank=True, max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='interface_ordering',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, b'Slot/position'], [2, b'Name (alphabetically)']], default=1),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RackReservation',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('units', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(), size=None)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('description', models.CharField(max_length=100)),
|
||||
('rack', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='dcim.Rack')),
|
||||
('user', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['created'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='site',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=copy_site_from_rack,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='rack',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Rack'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='site',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='lag',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name='Parent LAG'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Region',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('lft', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('rght', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('level', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.Region')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='region',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='dcim.Region'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='name',
|
||||
field=utilities.fields.NullableCharField(blank=True, max_length=64, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rackreservation',
|
||||
name='rack',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='dcim.Rack'),
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='Module',
|
||||
new_name='InventoryItem',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='device',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventory_items', to='dcim.Device'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child_items', to='dcim.InventoryItem'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='manufacturer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_items', to='dcim.Manufacturer'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='status',
|
||||
field=models.PositiveIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [2, 'Planned'], [3, 'Staged'], [4, 'Failed'], [5, 'Inventory']], default=1, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='cs_port',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_console', to='dcim.ConsoleServerPort', verbose_name='Console server port'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='asset_tag',
|
||||
field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name='Asset tag'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='face',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[[0, 'Front'], [1, 'Rear']], null=True, verbose_name='Rack face'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='position',
|
||||
field=models.PositiveSmallIntegerField(blank=True, help_text='The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Position (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='primary_ip4',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name='Primary IPv4'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='primary_ip6',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name='Primary IPv6'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='serial',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicebay',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='interface_ordering',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, 'Slot/position'], [2, 'Name (alphabetically)']], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_console_server',
|
||||
field=models.BooleanField(default=False, help_text='This type of device has console server ports', verbose_name='Is a console server'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_full_depth',
|
||||
field=models.BooleanField(default=True, help_text='Device consumes both front and rear rack faces', verbose_name='Is full depth'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_network_device',
|
||||
field=models.BooleanField(default=True, help_text='This type of device has network interfaces', verbose_name='Is a network device'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_pdu',
|
||||
field=models.BooleanField(default=False, help_text='This type of device has power outlets', verbose_name='Is a PDU'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='part_number',
|
||||
field=models.CharField(blank=True, help_text='Discrete part number (optional)', max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='subdevice_role',
|
||||
field=models.NullBooleanField(choices=[(None, 'None'), (True, 'Parent'), (False, 'Child')], default=None, help_text='Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name='Parent/child status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='u_height',
|
||||
field=models.PositiveSmallIntegerField(default=1, verbose_name='Height (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='mac_address',
|
||||
field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name='MAC Address'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='mgmt_only',
|
||||
field=models.BooleanField(default=False, help_text='This interface is used only for out-of-band management', verbose_name='OOB Management'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfaceconnection',
|
||||
name='connection_status',
|
||||
field=models.BooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='mgmt_only',
|
||||
field=models.BooleanField(default=False, verbose_name='Management only'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='discovered',
|
||||
field=models.BooleanField(default=False, verbose_name='Discovered'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='part_id',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Part ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='serial',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='rpc_client',
|
||||
field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='RPC client'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerport',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='desc_units',
|
||||
field=models.BooleanField(default=False, help_text='Units are numbered top-to-bottom', verbose_name='Descending units'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='facility_id',
|
||||
field=utilities.fields.NullableCharField(blank=True, max_length=30, null=True, verbose_name='Facility ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='type',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[(100, '2-post frame'), (200, '4-post frame'), (300, '4-post cabinet'), (1000, 'Wall-mounted frame'), (1100, 'Wall-mounted cabinet')], null=True, verbose_name='Type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='u_height',
|
||||
field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Height (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='width',
|
||||
field=models.PositiveSmallIntegerField(choices=[(19, '19 inches'), (23, '23 inches')], default=19, help_text='Rail-to-rail width', verbose_name='Width'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='asn',
|
||||
field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='contact_email',
|
||||
field=models.EmailField(blank=True, max_length=254, verbose_name='Contact E-mail'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='enabled',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='mtu',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='asset_tag',
|
||||
field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this item', max_length=50, null=True, unique=True, verbose_name='Asset tag'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='device',
|
||||
options={'ordering': ['name'], 'permissions': (('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'))},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='napalm_driver',
|
||||
field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices.', max_length=50, verbose_name='NAPALM driver'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='rpc_client',
|
||||
field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='Legacy RPC client'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=rpc_client_to_napalm_driver,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleporttemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleserverport',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicebaytemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlet',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerport',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerporttemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,146 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.14 on 2018-07-31 02:17
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import timezone_field.fields
|
||||
import utilities.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('dcim', '0044_virtualization'), ('dcim', '0045_devicerole_vm_role'), ('dcim', '0046_rack_lengthen_facility_id'), ('dcim', '0047_more_100ge_form_factors'), ('dcim', '0048_rack_serial'), ('dcim', '0049_rackreservation_change_user'), ('dcim', '0050_interface_vlan_tagging'), ('dcim', '0051_rackreservation_tenant'), ('dcim', '0052_virtual_chassis'), ('dcim', '0053_platform_manufacturer'), ('dcim', '0054_site_status_timezone_description'), ('dcim', '0055_virtualchassis_ordering')]
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0043_device_component_name_lengths'),
|
||||
('ipam', '0020_ipaddress_add_role_carp'),
|
||||
('virtualization', '0001_virtualization'),
|
||||
('tenancy', '0003_unicode_literals'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='cluster',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='virtualization.Cluster'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='virtual_machine',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='virtualization.VirtualMachine'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='device',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicerole',
|
||||
name='vm_role',
|
||||
field=models.BooleanField(default=True, help_text='Virtual machines may be assigned to this role', verbose_name='VM Role'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='facility_id',
|
||||
field=utilities.fields.NullableCharField(blank=True, max_length=50, null=True, verbose_name='Facility ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='serial',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rackreservation',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='mode',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[[100, 'Access'], [200, 'Tagged'], [300, 'Tagged All']], null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='tagged_vlans',
|
||||
field=models.ManyToManyField(blank=True, related_name='interfaces_as_tagged', to='ipam.VLAN', verbose_name='Tagged VLANs'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='untagged_vlan',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces_as_untagged', to='ipam.VLAN', verbose_name='Untagged VLAN'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackreservation',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rackreservations', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VirtualChassis',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('domain', models.CharField(blank=True, max_length=30)),
|
||||
('master', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'virtual chassis',
|
||||
'ordering': ['master'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='virtual_chassis',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='dcim.VirtualChassis'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='vc_position',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='vc_priority',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='device',
|
||||
unique_together=set([('rack', 'position', 'face'), ('virtual_chassis', 'vc_position')]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='manufacturer',
|
||||
field=models.ForeignKey(blank=True, help_text='Optionally limit this platform to devices of a certain manufacturer', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='platforms', to='dcim.Manufacturer'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='napalm_driver',
|
||||
field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices', max_length=50, verbose_name='NAPALM driver'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [2, 'Planned'], [4, 'Retired']], default=1),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='time_zone',
|
||||
field=timezone_field.fields.TimeZoneField(blank=True),
|
||||
),
|
||||
]
|
||||
29
netbox/dcim/migrations/0062_interface_mtu.py
Normal file
29
netbox/dcim/migrations/0062_interface_mtu.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 2.0.8 on 2018-08-22 14:23
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0061_platform_napalm_args'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='mtu',
|
||||
field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65536)], verbose_name='MTU'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[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']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[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']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
]
|
||||
@@ -7,7 +7,7 @@ from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.postgres.fields import ArrayField, JSONField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Count, Q, ObjectDoesNotExist
|
||||
@@ -79,7 +79,6 @@ class Region(MPTTModel, ChangeLoggedModel):
|
||||
unique=True
|
||||
)
|
||||
|
||||
serializer = 'dcim.api.serializers.RegionSerializer'
|
||||
csv_headers = ['name', 'slug', 'parent']
|
||||
|
||||
class MPTTMeta:
|
||||
@@ -201,7 +200,6 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
objects = SiteManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
serializer = 'dcim.api.serializers.SiteSerializer'
|
||||
csv_headers = [
|
||||
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
||||
'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||
@@ -287,7 +285,6 @@ class RackGroup(ChangeLoggedModel):
|
||||
related_name='rack_groups'
|
||||
)
|
||||
|
||||
serializer = 'dcim.api.serializers.RackGroupSerializer'
|
||||
csv_headers = ['site', 'name', 'slug']
|
||||
|
||||
class Meta:
|
||||
@@ -325,7 +322,6 @@ class RackRole(ChangeLoggedModel):
|
||||
)
|
||||
color = ColorField()
|
||||
|
||||
serializer = 'dcim.api.serializers.RackRoleSerializer'
|
||||
csv_headers = ['name', 'slug', 'color']
|
||||
|
||||
class Meta:
|
||||
@@ -432,7 +428,6 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
objects = RackManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
serializer = 'dcim.api.serializers.RackSerializer'
|
||||
csv_headers = [
|
||||
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'serial', 'width', 'u_height',
|
||||
'desc_units', 'comments',
|
||||
@@ -636,8 +631,6 @@ class RackReservation(ChangeLoggedModel):
|
||||
max_length=100
|
||||
)
|
||||
|
||||
serializer = 'dcim.api.serializers.RackReservationSerializer'
|
||||
|
||||
class Meta:
|
||||
ordering = ['created']
|
||||
|
||||
@@ -697,7 +690,6 @@ class Manufacturer(ChangeLoggedModel):
|
||||
unique=True
|
||||
)
|
||||
|
||||
serializer = 'dcim.api.serializers.ManufacturerSerializer'
|
||||
csv_headers = ['name', 'slug']
|
||||
|
||||
class Meta:
|
||||
@@ -792,7 +784,6 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
serializer = 'dcim.api.serializers.DeviceTypeSerializer'
|
||||
csv_headers = [
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
||||
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments',
|
||||
@@ -1076,7 +1067,6 @@ class DeviceRole(ChangeLoggedModel):
|
||||
help_text='Virtual machines may be assigned to this role'
|
||||
)
|
||||
|
||||
serializer = 'dcim.api.serializers.DeviceRoleSerializer'
|
||||
csv_headers = ['name', 'slug', 'color', 'vm_role']
|
||||
|
||||
class Meta:
|
||||
@@ -1085,9 +1075,6 @@ class DeviceRole(ChangeLoggedModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?role={}".format(reverse('dcim:device_list'), self.slug)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
@@ -1138,7 +1125,6 @@ class Platform(ChangeLoggedModel):
|
||||
verbose_name='Legacy RPC client'
|
||||
)
|
||||
|
||||
serializer = 'dcim.api.serializers.PlatformSerializer'
|
||||
csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args']
|
||||
|
||||
class Meta:
|
||||
@@ -1305,7 +1291,6 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
objects = DeviceManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
serializer = 'dcim.api.serializers.DeviceSerializer'
|
||||
csv_headers = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
|
||||
@@ -1824,9 +1809,10 @@ class Interface(ComponentModel):
|
||||
blank=True,
|
||||
verbose_name='MAC Address'
|
||||
)
|
||||
mtu = models.PositiveSmallIntegerField(
|
||||
mtu = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(65536)],
|
||||
verbose_name='MTU'
|
||||
)
|
||||
mgmt_only = models.BooleanField(
|
||||
@@ -1861,8 +1847,6 @@ class Interface(ComponentModel):
|
||||
objects = InterfaceQuerySet.as_manager()
|
||||
tags = TaggableManager()
|
||||
|
||||
serializer = 'dcim.api.serializers.InterfaceSerializer'
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
@@ -1950,11 +1934,20 @@ class Interface(ComponentModel):
|
||||
"""
|
||||
Include the connected Interface (if any).
|
||||
"""
|
||||
|
||||
# It's possible that an Interface can be deleted _after_ its parent Device/VM, in which case trying to resolve
|
||||
# the component parent will raise DoesNotExist. For more discussion, see
|
||||
# https://github.com/digitalocean/netbox/issues/2323
|
||||
try:
|
||||
parent_obj = self.get_component_parent()
|
||||
except ObjectDoesNotExist:
|
||||
parent_obj = None
|
||||
|
||||
ObjectChange(
|
||||
user=user,
|
||||
request_id=request_id,
|
||||
changed_object=self,
|
||||
related_object=self.get_component_parent(),
|
||||
related_object=parent_obj,
|
||||
action=action,
|
||||
object_data=serialize_object(self, extra={
|
||||
'connected_interface': self.connected_interface.pk if self.connection else None,
|
||||
@@ -2079,6 +2072,7 @@ class InterfaceConnection(models.Model):
|
||||
(self.interface_a, self.interface_b),
|
||||
(self.interface_b, self.interface_a),
|
||||
)
|
||||
|
||||
for interface, peer_interface in interfaces:
|
||||
if action == OBJECTCHANGE_ACTION_DELETE:
|
||||
connection_data = {
|
||||
@@ -2089,11 +2083,17 @@ class InterfaceConnection(models.Model):
|
||||
'connected_interface': peer_interface.pk,
|
||||
'connection_status': self.connection_status
|
||||
}
|
||||
|
||||
try:
|
||||
parent_obj = interface.parent
|
||||
except ObjectDoesNotExist:
|
||||
parent_obj = None
|
||||
|
||||
ObjectChange(
|
||||
user=user,
|
||||
request_id=request_id,
|
||||
changed_object=interface,
|
||||
related_object=interface.parent,
|
||||
related_object=parent_obj,
|
||||
action=OBJECTCHANGE_ACTION_UPDATE,
|
||||
object_data=serialize_object(interface, extra=connection_data)
|
||||
).save()
|
||||
@@ -2266,7 +2266,6 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
serializer = 'dcim.api.serializers.VirtualChassisSerializer'
|
||||
csv_headers = ['master', 'domain']
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -11,13 +11,8 @@ def assign_virtualchassis_master(instance, created, **kwargs):
|
||||
"""
|
||||
When a VirtualChassis is created, automatically assign its master device to the VC.
|
||||
"""
|
||||
# Default to 1 but don't overwrite an existing position (see #2087)
|
||||
if instance.master.vc_position is not None:
|
||||
vc_position = instance.master.vc_position
|
||||
else:
|
||||
vc_position = 1
|
||||
if created:
|
||||
Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=vc_position)
|
||||
Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=None)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=VirtualChassis)
|
||||
|
||||
@@ -432,7 +432,6 @@ class DeviceBayTemplateTable(BaseTable):
|
||||
|
||||
class DeviceRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
device_count = tables.TemplateColumn(
|
||||
template_code=DEVICEROLE_DEVICE_COUNT,
|
||||
accessor=Accessor('devices.count'),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
from netaddr import IPNetwork
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from dcim.constants import (
|
||||
IFACE_FF_1GE_FIXED, IFACE_FF_LAG, IFACE_MODE_TAGGED, SITE_STATUS_ACTIVE, SUBDEVICE_ROLE_CHILD,
|
||||
@@ -15,19 +14,17 @@ from dcim.models import (
|
||||
InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site, VirtualChassis,
|
||||
)
|
||||
from ipam.models import VLAN
|
||||
from ipam.models import IPAddress, VLAN
|
||||
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from users.models import Token
|
||||
from utilities.testing import HttpStatusMixin
|
||||
from utilities.testing import APITestCase
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
|
||||
|
||||
class RegionTest(HttpStatusMixin, APITestCase):
|
||||
class RegionTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(RegionTest, self).setUp()
|
||||
|
||||
self.region1 = Region.objects.create(name='Test Region 1', slug='test-region-1')
|
||||
self.region2 = Region.objects.create(name='Test Region 2', slug='test-region-2')
|
||||
@@ -114,13 +111,11 @@ class RegionTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(Region.objects.count(), 2)
|
||||
|
||||
|
||||
class SiteTest(HttpStatusMixin, APITestCase):
|
||||
class SiteTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(SiteTest, self).setUp()
|
||||
|
||||
self.region1 = Region.objects.create(name='Test Region 1', slug='test-region-1')
|
||||
self.region2 = Region.objects.create(name='Test Region 2', slug='test-region-2')
|
||||
@@ -241,13 +236,11 @@ class SiteTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(Site.objects.count(), 2)
|
||||
|
||||
|
||||
class RackGroupTest(HttpStatusMixin, APITestCase):
|
||||
class RackGroupTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(RackGroupTest, self).setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
||||
@@ -343,13 +336,11 @@ class RackGroupTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(RackGroup.objects.count(), 2)
|
||||
|
||||
|
||||
class RackRoleTest(HttpStatusMixin, APITestCase):
|
||||
class RackRoleTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(RackRoleTest, self).setUp()
|
||||
|
||||
self.rackrole1 = RackRole.objects.create(name='Test Rack Role 1', slug='test-rack-role-1', color='ff0000')
|
||||
self.rackrole2 = RackRole.objects.create(name='Test Rack Role 2', slug='test-rack-role-2', color='00ff00')
|
||||
@@ -443,13 +434,11 @@ class RackRoleTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(RackRole.objects.count(), 2)
|
||||
|
||||
|
||||
class RackTest(HttpStatusMixin, APITestCase):
|
||||
class RackTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(RackTest, self).setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
||||
@@ -569,25 +558,22 @@ class RackTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(Rack.objects.count(), 2)
|
||||
|
||||
|
||||
class RackReservationTest(HttpStatusMixin, APITestCase):
|
||||
class RackReservationTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(RackReservationTest, self).setUp()
|
||||
|
||||
self.user1 = user
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.rack1 = Rack.objects.create(site=self.site1, name='Test Rack 1')
|
||||
self.rackreservation1 = RackReservation.objects.create(
|
||||
rack=self.rack1, units=[1, 2, 3], user=user, description='Reservation #1',
|
||||
rack=self.rack1, units=[1, 2, 3], user=self.user, description='Reservation #1',
|
||||
)
|
||||
self.rackreservation2 = RackReservation.objects.create(
|
||||
rack=self.rack1, units=[4, 5, 6], user=user, description='Reservation #2',
|
||||
rack=self.rack1, units=[4, 5, 6], user=self.user, description='Reservation #2',
|
||||
)
|
||||
self.rackreservation3 = RackReservation.objects.create(
|
||||
rack=self.rack1, units=[7, 8, 9], user=user, description='Reservation #3',
|
||||
rack=self.rack1, units=[7, 8, 9], user=self.user, description='Reservation #3',
|
||||
)
|
||||
|
||||
def test_get_rackreservation(self):
|
||||
@@ -609,7 +595,7 @@ class RackReservationTest(HttpStatusMixin, APITestCase):
|
||||
data = {
|
||||
'rack': self.rack1.pk,
|
||||
'units': [10, 11, 12],
|
||||
'user': self.user1.pk,
|
||||
'user': self.user.pk,
|
||||
'description': 'Fourth reservation',
|
||||
}
|
||||
|
||||
@@ -630,19 +616,19 @@ class RackReservationTest(HttpStatusMixin, APITestCase):
|
||||
{
|
||||
'rack': self.rack1.pk,
|
||||
'units': [10, 11, 12],
|
||||
'user': self.user1.pk,
|
||||
'user': self.user.pk,
|
||||
'description': 'Reservation #4',
|
||||
},
|
||||
{
|
||||
'rack': self.rack1.pk,
|
||||
'units': [13, 14, 15],
|
||||
'user': self.user1.pk,
|
||||
'user': self.user.pk,
|
||||
'description': 'Reservation #5',
|
||||
},
|
||||
{
|
||||
'rack': self.rack1.pk,
|
||||
'units': [16, 17, 18],
|
||||
'user': self.user1.pk,
|
||||
'user': self.user.pk,
|
||||
'description': 'Reservation #6',
|
||||
},
|
||||
]
|
||||
@@ -661,7 +647,7 @@ class RackReservationTest(HttpStatusMixin, APITestCase):
|
||||
data = {
|
||||
'rack': self.rack1.pk,
|
||||
'units': [10, 11, 12],
|
||||
'user': self.user1.pk,
|
||||
'user': self.user.pk,
|
||||
'description': 'Modified reservation',
|
||||
}
|
||||
|
||||
@@ -683,13 +669,11 @@ class RackReservationTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(RackReservation.objects.count(), 2)
|
||||
|
||||
|
||||
class ManufacturerTest(HttpStatusMixin, APITestCase):
|
||||
class ManufacturerTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(ManufacturerTest, self).setUp()
|
||||
|
||||
self.manufacturer1 = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.manufacturer2 = Manufacturer.objects.create(name='Test Manufacturer 2', slug='test-manufacturer-2')
|
||||
@@ -776,13 +760,11 @@ class ManufacturerTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(Manufacturer.objects.count(), 2)
|
||||
|
||||
|
||||
class DeviceTypeTest(HttpStatusMixin, APITestCase):
|
||||
class DeviceTypeTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(DeviceTypeTest, self).setUp()
|
||||
|
||||
self.manufacturer1 = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.manufacturer2 = Manufacturer.objects.create(name='Test Manufacturer 2', slug='test-manufacturer-2')
|
||||
@@ -884,13 +866,11 @@ class DeviceTypeTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(DeviceType.objects.count(), 2)
|
||||
|
||||
|
||||
class ConsolePortTemplateTest(HttpStatusMixin, APITestCase):
|
||||
class ConsolePortTemplateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(ConsolePortTemplateTest, self).setUp()
|
||||
|
||||
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.devicetype = DeviceType.objects.create(
|
||||
@@ -986,13 +966,11 @@ class ConsolePortTemplateTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(ConsolePortTemplate.objects.count(), 2)
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateTest(HttpStatusMixin, APITestCase):
|
||||
class ConsoleServerPortTemplateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(ConsoleServerPortTemplateTest, self).setUp()
|
||||
|
||||
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.devicetype = DeviceType.objects.create(
|
||||
@@ -1088,13 +1066,11 @@ class ConsoleServerPortTemplateTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(ConsoleServerPortTemplate.objects.count(), 2)
|
||||
|
||||
|
||||
class PowerPortTemplateTest(HttpStatusMixin, APITestCase):
|
||||
class PowerPortTemplateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(PowerPortTemplateTest, self).setUp()
|
||||
|
||||
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.devicetype = DeviceType.objects.create(
|
||||
@@ -1190,13 +1166,11 @@ class PowerPortTemplateTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(PowerPortTemplate.objects.count(), 2)
|
||||
|
||||
|
||||
class PowerOutletTemplateTest(HttpStatusMixin, APITestCase):
|
||||
class PowerOutletTemplateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(PowerOutletTemplateTest, self).setUp()
|
||||
|
||||
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.devicetype = DeviceType.objects.create(
|
||||
@@ -1292,13 +1266,11 @@ class PowerOutletTemplateTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(PowerOutletTemplate.objects.count(), 2)
|
||||
|
||||
|
||||
class InterfaceTemplateTest(HttpStatusMixin, APITestCase):
|
||||
class InterfaceTemplateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(InterfaceTemplateTest, self).setUp()
|
||||
|
||||
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.devicetype = DeviceType.objects.create(
|
||||
@@ -1394,13 +1366,11 @@ class InterfaceTemplateTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(InterfaceTemplate.objects.count(), 2)
|
||||
|
||||
|
||||
class DeviceBayTemplateTest(HttpStatusMixin, APITestCase):
|
||||
class DeviceBayTemplateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(DeviceBayTemplateTest, self).setUp()
|
||||
|
||||
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.devicetype = DeviceType.objects.create(
|
||||
@@ -1496,13 +1466,11 @@ class DeviceBayTemplateTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(DeviceBayTemplate.objects.count(), 2)
|
||||
|
||||
|
||||
class DeviceRoleTest(HttpStatusMixin, APITestCase):
|
||||
class DeviceRoleTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(DeviceRoleTest, self).setUp()
|
||||
|
||||
self.devicerole1 = DeviceRole.objects.create(
|
||||
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
||||
@@ -1602,13 +1570,11 @@ class DeviceRoleTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(DeviceRole.objects.count(), 2)
|
||||
|
||||
|
||||
class PlatformTest(HttpStatusMixin, APITestCase):
|
||||
class PlatformTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(PlatformTest, self).setUp()
|
||||
|
||||
self.platform1 = Platform.objects.create(name='Test Platform 1', slug='test-platform-1')
|
||||
self.platform2 = Platform.objects.create(name='Test Platform 2', slug='test-platform-2')
|
||||
@@ -1695,13 +1661,11 @@ class PlatformTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(Platform.objects.count(), 2)
|
||||
|
||||
|
||||
class DeviceTest(HttpStatusMixin, APITestCase):
|
||||
class DeviceTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(DeviceTest, self).setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
||||
@@ -1718,14 +1682,28 @@ class DeviceTest(HttpStatusMixin, APITestCase):
|
||||
self.devicerole2 = DeviceRole.objects.create(
|
||||
name='Test Device Role 2', slug='test-device-role-2', color='00ff00'
|
||||
)
|
||||
cluster_type = ClusterType.objects.create(name='Test Cluster Type 1', slug='test-cluster-type-1')
|
||||
self.cluster1 = Cluster.objects.create(name='Test Cluster 1', type=cluster_type)
|
||||
self.device1 = Device.objects.create(
|
||||
device_type=self.devicetype1, device_role=self.devicerole1, name='Test Device 1', site=self.site1
|
||||
device_type=self.devicetype1,
|
||||
device_role=self.devicerole1,
|
||||
name='Test Device 1',
|
||||
site=self.site1,
|
||||
cluster=self.cluster1
|
||||
)
|
||||
self.device2 = Device.objects.create(
|
||||
device_type=self.devicetype1, device_role=self.devicerole1, name='Test Device 2', site=self.site1
|
||||
device_type=self.devicetype1,
|
||||
device_role=self.devicerole1,
|
||||
name='Test Device 2',
|
||||
site=self.site1,
|
||||
cluster=self.cluster1
|
||||
)
|
||||
self.device3 = Device.objects.create(
|
||||
device_type=self.devicetype1, device_role=self.devicerole1, name='Test Device 3', site=self.site1
|
||||
device_type=self.devicetype1,
|
||||
device_role=self.devicerole1,
|
||||
name='Test Device 3',
|
||||
site=self.site1,
|
||||
cluster=self.cluster1
|
||||
)
|
||||
|
||||
def test_get_device(self):
|
||||
@@ -1734,6 +1712,8 @@ class DeviceTest(HttpStatusMixin, APITestCase):
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['name'], self.device1.name)
|
||||
self.assertEqual(response.data['device_role']['id'], self.devicerole1.pk)
|
||||
self.assertEqual(response.data['cluster']['id'], self.cluster1.pk)
|
||||
|
||||
def test_list_devices(self):
|
||||
|
||||
@@ -1749,6 +1729,7 @@ class DeviceTest(HttpStatusMixin, APITestCase):
|
||||
'device_role': self.devicerole1.pk,
|
||||
'name': 'Test Device 4',
|
||||
'site': self.site1.pk,
|
||||
'cluster': self.cluster1.pk,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:device-list')
|
||||
@@ -1760,7 +1741,8 @@ class DeviceTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(device4.device_type_id, data['device_type'])
|
||||
self.assertEqual(device4.device_role_id, data['device_role'])
|
||||
self.assertEqual(device4.name, data['name'])
|
||||
self.assertEqual(device4.site_id, data['site'])
|
||||
self.assertEqual(device4.site.pk, data['site'])
|
||||
self.assertEqual(device4.cluster.pk, data['cluster'])
|
||||
|
||||
def test_create_device_bulk(self):
|
||||
|
||||
@@ -1796,11 +1778,17 @@ class DeviceTest(HttpStatusMixin, APITestCase):
|
||||
|
||||
def test_update_device(self):
|
||||
|
||||
interface = Interface.objects.create(name='Test Interface 1', device=self.device1)
|
||||
ip4_address = IPAddress.objects.create(address=IPNetwork('192.0.2.1/24'), interface=interface)
|
||||
ip6_address = IPAddress.objects.create(address=IPNetwork('2001:db8::1/64'), interface=interface)
|
||||
|
||||
data = {
|
||||
'device_type': self.devicetype2.pk,
|
||||
'device_role': self.devicerole2.pk,
|
||||
'name': 'Test Device X',
|
||||
'site': self.site2.pk,
|
||||
'primary_ip4': ip4_address.pk,
|
||||
'primary_ip6': ip6_address.pk,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:device-detail', kwargs={'pk': self.device1.pk})
|
||||
@@ -1812,7 +1800,9 @@ class DeviceTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(device1.device_type_id, data['device_type'])
|
||||
self.assertEqual(device1.device_role_id, data['device_role'])
|
||||
self.assertEqual(device1.name, data['name'])
|
||||
self.assertEqual(device1.site_id, data['site'])
|
||||
self.assertEqual(device1.site.pk, data['site'])
|
||||
self.assertEqual(device1.primary_ip4.pk, data['primary_ip4'])
|
||||
self.assertEqual(device1.primary_ip6.pk, data['primary_ip6'])
|
||||
|
||||
def test_delete_device(self):
|
||||
|
||||
@@ -1823,13 +1813,11 @@ class DeviceTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(Device.objects.count(), 2)
|
||||
|
||||
|
||||
class ConsolePortTest(HttpStatusMixin, APITestCase):
|
||||
class ConsolePortTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(ConsolePortTest, self).setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -1930,13 +1918,11 @@ class ConsolePortTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(ConsolePort.objects.count(), 2)
|
||||
|
||||
|
||||
class ConsoleServerPortTest(HttpStatusMixin, APITestCase):
|
||||
class ConsoleServerPortTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(ConsoleServerPortTest, self).setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2033,13 +2019,11 @@ class ConsoleServerPortTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(ConsoleServerPort.objects.count(), 2)
|
||||
|
||||
|
||||
class PowerPortTest(HttpStatusMixin, APITestCase):
|
||||
class PowerPortTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(PowerPortTest, self).setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2140,13 +2124,11 @@ class PowerPortTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(PowerPort.objects.count(), 2)
|
||||
|
||||
|
||||
class PowerOutletTest(HttpStatusMixin, APITestCase):
|
||||
class PowerOutletTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(PowerOutletTest, self).setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2243,13 +2225,11 @@ class PowerOutletTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(PowerOutlet.objects.count(), 2)
|
||||
|
||||
|
||||
class InterfaceTest(HttpStatusMixin, APITestCase):
|
||||
class InterfaceTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(InterfaceTest, self).setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2433,13 +2413,11 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(Interface.objects.count(), 2)
|
||||
|
||||
|
||||
class DeviceBayTest(HttpStatusMixin, APITestCase):
|
||||
class DeviceBayTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(DeviceBayTest, self).setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2548,13 +2526,11 @@ class DeviceBayTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(DeviceBay.objects.count(), 2)
|
||||
|
||||
|
||||
class InventoryItemTest(HttpStatusMixin, APITestCase):
|
||||
class InventoryItemTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(InventoryItemTest, self).setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2666,13 +2642,11 @@ class InventoryItemTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(InventoryItem.objects.count(), 2)
|
||||
|
||||
|
||||
class ConsoleConnectionTest(HttpStatusMixin, APITestCase):
|
||||
class ConsoleConnectionTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(ConsoleConnectionTest, self).setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2709,13 +2683,11 @@ class ConsoleConnectionTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
|
||||
class PowerConnectionTest(HttpStatusMixin, APITestCase):
|
||||
class PowerConnectionTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(PowerConnectionTest, self).setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2752,13 +2724,11 @@ class PowerConnectionTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
|
||||
class InterfaceConnectionTest(HttpStatusMixin, APITestCase):
|
||||
class InterfaceConnectionTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(InterfaceConnectionTest, self).setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2879,13 +2849,11 @@ class InterfaceConnectionTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(InterfaceConnection.objects.count(), 2)
|
||||
|
||||
|
||||
class ConnectedDeviceTest(HttpStatusMixin, APITestCase):
|
||||
class ConnectedDeviceTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(ConnectedDeviceTest, self).setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
||||
@@ -2921,13 +2889,11 @@ class ConnectedDeviceTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(response.data['name'], self.device1.name)
|
||||
|
||||
|
||||
class VirtualChassisTest(HttpStatusMixin, APITestCase):
|
||||
class VirtualChassisTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(VirtualChassisTest, self).setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site', slug='test-site')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer', slug='test-manufacturer')
|
||||
|
||||
@@ -7,8 +7,8 @@ from ipam.views import ServiceCreateView
|
||||
from secrets.views import secret_add
|
||||
from . import views
|
||||
from .models import (
|
||||
Device, DeviceRole, DeviceType, Manufacturer, Platform, Rack, RackGroup, RackReservation, RackRole, Region, Site,
|
||||
VirtualChassis,
|
||||
Device, DeviceRole, DeviceType, Interface, Manufacturer, Platform, Rack, RackGroup, RackReservation, RackRole,
|
||||
Region, Site, VirtualChassis,
|
||||
)
|
||||
|
||||
app_name = 'dcim'
|
||||
@@ -203,6 +203,7 @@ urlpatterns = [
|
||||
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/assign-vlans/$', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
|
||||
url(r'^interfaces/rename/$', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
|
||||
|
||||
# Device bays
|
||||
|
||||
@@ -151,7 +151,6 @@ class RegionBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_region'
|
||||
cls = Region
|
||||
queryset = Region.objects.annotate(site_count=Count('sites'))
|
||||
filter = filters.RegionFilter
|
||||
table = tables.RegionTable
|
||||
@@ -223,7 +222,6 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_site'
|
||||
cls = Site
|
||||
queryset = Site.objects.select_related('region', 'tenant')
|
||||
filter = filters.SiteFilter
|
||||
table = tables.SiteTable
|
||||
@@ -263,7 +261,6 @@ class RackGroupBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rackgroup'
|
||||
cls = RackGroup
|
||||
queryset = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
|
||||
filter = filters.RackGroupFilter
|
||||
table = tables.RackGroupTable
|
||||
@@ -300,7 +297,6 @@ class RackRoleBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rackrole'
|
||||
cls = RackRole
|
||||
queryset = RackRole.objects.annotate(rack_count=Count('racks'))
|
||||
table = tables.RackRoleTable
|
||||
default_return_url = 'dcim:rackrole_list'
|
||||
@@ -415,7 +411,6 @@ class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_rack'
|
||||
cls = Rack
|
||||
queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role')
|
||||
filter = filters.RackFilter
|
||||
table = tables.RackTable
|
||||
@@ -425,7 +420,6 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rack'
|
||||
cls = Rack
|
||||
queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role')
|
||||
filter = filters.RackFilter
|
||||
table = tables.RackTable
|
||||
@@ -473,7 +467,6 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_rackreservation'
|
||||
cls = RackReservation
|
||||
queryset = RackReservation.objects.select_related('rack', 'user')
|
||||
filter = filters.RackReservationFilter
|
||||
table = tables.RackReservationTable
|
||||
@@ -483,7 +476,7 @@ class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rackreservation'
|
||||
cls = RackReservation
|
||||
queryset = RackReservation.objects.select_related('rack', 'user')
|
||||
filter = filters.RackReservationFilter
|
||||
table = tables.RackReservationTable
|
||||
default_return_url = 'dcim:rackreservation_list'
|
||||
@@ -522,7 +515,6 @@ class ManufacturerBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_manufacturer'
|
||||
cls = Manufacturer
|
||||
queryset = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
|
||||
table = tables.ManufacturerTable
|
||||
default_return_url = 'dcim:manufacturer_list'
|
||||
@@ -619,7 +611,6 @@ class DeviceTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_devicetype'
|
||||
cls = DeviceType
|
||||
queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
|
||||
filter = filters.DeviceTypeFilter
|
||||
table = tables.DeviceTypeTable
|
||||
@@ -629,7 +620,6 @@ class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_devicetype'
|
||||
cls = DeviceType
|
||||
queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
|
||||
filter = filters.DeviceTypeFilter
|
||||
table = tables.DeviceTypeTable
|
||||
@@ -652,10 +642,8 @@ class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView
|
||||
|
||||
class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleporttemplate'
|
||||
queryset = ConsolePortTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
cls = ConsolePortTemplate
|
||||
parent_cls = DeviceType
|
||||
table = tables.ConsolePortTemplateTable
|
||||
|
||||
|
||||
@@ -671,8 +659,8 @@ class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCrea
|
||||
|
||||
class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleserverporttemplate'
|
||||
cls = ConsoleServerPortTemplate
|
||||
parent_cls = DeviceType
|
||||
queryset = ConsoleServerPortTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.ConsoleServerPortTemplateTable
|
||||
|
||||
|
||||
@@ -688,8 +676,8 @@ class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
|
||||
class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_powerporttemplate'
|
||||
cls = PowerPortTemplate
|
||||
parent_cls = DeviceType
|
||||
queryset = PowerPortTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.PowerPortTemplateTable
|
||||
|
||||
|
||||
@@ -705,8 +693,8 @@ class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView
|
||||
|
||||
class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_poweroutlettemplate'
|
||||
cls = PowerOutletTemplate
|
||||
parent_cls = DeviceType
|
||||
queryset = PowerOutletTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.PowerOutletTemplateTable
|
||||
|
||||
|
||||
@@ -722,16 +710,16 @@ class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
|
||||
class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_interfacetemplate'
|
||||
cls = InterfaceTemplate
|
||||
parent_cls = DeviceType
|
||||
queryset = InterfaceTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.InterfaceTemplateTable
|
||||
form = forms.InterfaceTemplateBulkEditForm
|
||||
|
||||
|
||||
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_interfacetemplate'
|
||||
cls = InterfaceTemplate
|
||||
parent_cls = DeviceType
|
||||
queryset = InterfaceTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.InterfaceTemplateTable
|
||||
|
||||
|
||||
@@ -747,8 +735,8 @@ class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
|
||||
class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_devicebaytemplate'
|
||||
cls = DeviceBayTemplate
|
||||
parent_cls = DeviceType
|
||||
queryset = DeviceBayTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.DeviceBayTemplateTable
|
||||
|
||||
|
||||
@@ -782,8 +770,7 @@ class DeviceRoleBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_devicerole'
|
||||
cls = DeviceRole
|
||||
queryset = DeviceRole.objects.annotate(device_count=Count('devices'))
|
||||
queryset = DeviceRole.objects.all()
|
||||
table = tables.DeviceRoleTable
|
||||
default_return_url = 'dcim:devicerole_list'
|
||||
|
||||
@@ -818,8 +805,7 @@ class PlatformBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_platform'
|
||||
cls = Platform
|
||||
queryset = Platform.objects.annotate(device_count=Count('devices'))
|
||||
queryset = Platform.objects.all()
|
||||
table = tables.PlatformTable
|
||||
default_return_url = 'dcim:platform_list'
|
||||
|
||||
@@ -1032,7 +1018,6 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_device'
|
||||
cls = Device
|
||||
queryset = Device.objects.select_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
@@ -1042,7 +1027,6 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_device'
|
||||
cls = Device
|
||||
queryset = Device.objects.select_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
@@ -1172,8 +1156,8 @@ class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleport'
|
||||
cls = ConsolePort
|
||||
parent_cls = Device
|
||||
queryset = ConsolePort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.ConsolePortTable
|
||||
|
||||
|
||||
@@ -1326,8 +1310,8 @@ class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnec
|
||||
|
||||
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleserverport'
|
||||
cls = ConsoleServerPort
|
||||
parent_cls = Device
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.ConsoleServerPortTable
|
||||
|
||||
|
||||
@@ -1454,8 +1438,8 @@ class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_powerport'
|
||||
cls = PowerPort
|
||||
parent_cls = Device
|
||||
queryset = PowerPort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.PowerPortTable
|
||||
|
||||
|
||||
@@ -1608,8 +1592,8 @@ class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView)
|
||||
|
||||
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_poweroutlet'
|
||||
cls = PowerOutlet
|
||||
parent_cls = Device
|
||||
queryset = PowerOutlet.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.PowerOutletTable
|
||||
|
||||
|
||||
@@ -1700,8 +1684,8 @@ class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
|
||||
|
||||
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_interface'
|
||||
cls = Interface
|
||||
parent_cls = Device
|
||||
queryset = Interface.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.InterfaceTable
|
||||
form = forms.InterfaceBulkEditForm
|
||||
|
||||
@@ -1714,8 +1698,8 @@ class InterfaceBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
|
||||
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_interface'
|
||||
cls = Interface
|
||||
parent_cls = Device
|
||||
queryset = Interface.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.InterfaceTable
|
||||
|
||||
|
||||
@@ -1821,8 +1805,8 @@ class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
|
||||
class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_devicebay'
|
||||
cls = DeviceBay
|
||||
parent_cls = Device
|
||||
queryset = DeviceBay.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.DeviceBayTable
|
||||
|
||||
|
||||
@@ -2079,7 +2063,6 @@ class InventoryItemBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_inventoryitem'
|
||||
cls = InventoryItem
|
||||
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
|
||||
filter = filters.InventoryItemFilter
|
||||
table = tables.InventoryItemTable
|
||||
@@ -2089,7 +2072,6 @@ class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_inventoryitem'
|
||||
cls = InventoryItem
|
||||
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
|
||||
table = tables.InventoryItemTable
|
||||
template_name = 'dcim/inventoryitem_bulk_delete.html'
|
||||
|
||||
@@ -4,12 +4,9 @@ from django import forms
|
||||
from django.contrib import admin
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from netbox.admin import admin_site
|
||||
from utilities.forms import LaxURLField
|
||||
from .constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
||||
from .models import (
|
||||
ConfigContext, CustomField, CustomFieldChoice, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction,
|
||||
Webhook,
|
||||
)
|
||||
from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction, Webhook
|
||||
|
||||
|
||||
def order_content_types(field):
|
||||
@@ -39,7 +36,7 @@ class WebhookForm(forms.ModelForm):
|
||||
order_content_types(self.fields['obj_type'])
|
||||
|
||||
|
||||
@admin.register(Webhook)
|
||||
@admin.register(Webhook, site=admin_site)
|
||||
class WebhookAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update',
|
||||
@@ -72,7 +69,7 @@ class CustomFieldChoiceAdmin(admin.TabularInline):
|
||||
extra = 5
|
||||
|
||||
|
||||
@admin.register(CustomField)
|
||||
@admin.register(CustomField, site=admin_site)
|
||||
class CustomFieldAdmin(admin.ModelAdmin):
|
||||
inlines = [CustomFieldChoiceAdmin]
|
||||
list_display = ['name', 'models', 'type', 'required', 'filter_logic', 'default', 'weight', 'description']
|
||||
@@ -86,7 +83,7 @@ class CustomFieldAdmin(admin.ModelAdmin):
|
||||
# Graphs
|
||||
#
|
||||
|
||||
@admin.register(Graph)
|
||||
@admin.register(Graph, site=admin_site)
|
||||
class GraphAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'type', 'weight', 'source']
|
||||
|
||||
@@ -109,7 +106,7 @@ class ExportTemplateForm(forms.ModelForm):
|
||||
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
||||
|
||||
|
||||
@admin.register(ExportTemplate)
|
||||
@admin.register(ExportTemplate, site=admin_site)
|
||||
class ExportTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'content_type', 'description', 'mime_type', 'file_extension']
|
||||
form = ExportTemplateForm
|
||||
@@ -119,7 +116,7 @@ class ExportTemplateAdmin(admin.ModelAdmin):
|
||||
# Topology maps
|
||||
#
|
||||
|
||||
@admin.register(TopologyMap)
|
||||
@admin.register(TopologyMap, site=admin_site)
|
||||
class TopologyMapAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'site']
|
||||
prepopulated_fields = {
|
||||
@@ -127,63 +124,11 @@ class TopologyMapAdmin(admin.ModelAdmin):
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Config contexts
|
||||
#
|
||||
|
||||
@admin.register(ConfigContext)
|
||||
class ConfigContextAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'weight']
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
||||
@admin.register(ObjectChange)
|
||||
class ObjectChangeAdmin(admin.ModelAdmin):
|
||||
actions = None
|
||||
fields = ['time', 'changed_object_type', 'display_object', 'action', 'display_user', 'request_id', 'object_data']
|
||||
list_display = ['time', 'changed_object_type', 'display_object', 'display_action', 'display_user', 'request_id']
|
||||
list_filter = ['time', 'action', 'user__username']
|
||||
list_select_related = ['changed_object_type', 'user']
|
||||
readonly_fields = fields
|
||||
search_fields = ['user_name', 'object_repr', 'request_id']
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def display_user(self, obj):
|
||||
if obj.user is not None:
|
||||
return obj.user
|
||||
else:
|
||||
return '{} (deleted)'.format(obj.user_name)
|
||||
display_user.short_description = 'user'
|
||||
|
||||
def display_action(self, obj):
|
||||
icon = {
|
||||
OBJECTCHANGE_ACTION_CREATE: 'addlink',
|
||||
OBJECTCHANGE_ACTION_UPDATE: 'changelink',
|
||||
OBJECTCHANGE_ACTION_DELETE: 'deletelink',
|
||||
}
|
||||
return mark_safe('<span class="{}">{}</span>'.format(icon[obj.action], obj.get_action_display()))
|
||||
display_action.short_description = 'action'
|
||||
|
||||
def display_object(self, obj):
|
||||
if hasattr(obj.changed_object, 'get_absolute_url'):
|
||||
return mark_safe('<a href="{}">{}</a>'.format(obj.changed_object.get_absolute_url(), obj.changed_object))
|
||||
elif obj.changed_object is not None:
|
||||
return obj.changed_object
|
||||
else:
|
||||
return '{} (deleted)'.format(obj.object_repr)
|
||||
display_object.short_description = 'object'
|
||||
|
||||
|
||||
#
|
||||
# User actions
|
||||
#
|
||||
|
||||
@admin.register(UserAction)
|
||||
@admin.register(UserAction, site=admin_site)
|
||||
class UserActionAdmin(admin.ModelAdmin):
|
||||
actions = None
|
||||
list_display = ['user', 'action', 'content_type', 'object_id', '_message']
|
||||
|
||||
@@ -8,15 +8,16 @@ from dcim.api.serializers import (
|
||||
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
|
||||
NestedRegionSerializer, NestedSiteSerializer,
|
||||
)
|
||||
from dcim.models import Device, Rack, Site
|
||||
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
|
||||
from extras.models import (
|
||||
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, UserAction,
|
||||
)
|
||||
from extras.constants import *
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from tenancy.api.serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from users.api.serializers import NestedUserSerializer
|
||||
from utilities.api import (
|
||||
ChoiceFieldSerializer, ContentTypeFieldSerializer, get_serializer_for_model, ValidatedModelSerializer,
|
||||
ChoiceField, ContentTypeField, get_serializer_for_model, SerializedPKRelatedField, ValidatedModelSerializer,
|
||||
)
|
||||
|
||||
|
||||
@@ -25,7 +26,7 @@ from utilities.api import (
|
||||
#
|
||||
|
||||
class GraphSerializer(ValidatedModelSerializer):
|
||||
type = ChoiceFieldSerializer(choices=GRAPH_TYPE_CHOICES)
|
||||
type = ChoiceField(choices=GRAPH_TYPE_CHOICES)
|
||||
|
||||
class Meta:
|
||||
model = Graph
|
||||
@@ -35,7 +36,7 @@ class GraphSerializer(ValidatedModelSerializer):
|
||||
class RenderedGraphSerializer(serializers.ModelSerializer):
|
||||
embed_url = serializers.SerializerMethodField()
|
||||
embed_link = serializers.SerializerMethodField()
|
||||
type = ChoiceFieldSerializer(choices=GRAPH_TYPE_CHOICES)
|
||||
type = ChoiceField(choices=GRAPH_TYPE_CHOICES)
|
||||
|
||||
class Meta:
|
||||
model = Graph
|
||||
@@ -88,7 +89,7 @@ class TagSerializer(ValidatedModelSerializer):
|
||||
#
|
||||
|
||||
class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
content_type = ContentTypeFieldSerializer()
|
||||
content_type = ContentTypeField()
|
||||
parent = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@@ -132,17 +133,48 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
#
|
||||
|
||||
class ConfigContextSerializer(ValidatedModelSerializer):
|
||||
regions = NestedRegionSerializer(required=False, many=True)
|
||||
sites = NestedSiteSerializer(required=False, many=True)
|
||||
roles = NestedDeviceRoleSerializer(required=False, many=True)
|
||||
platforms = NestedPlatformSerializer(required=False, many=True)
|
||||
tenants = NestedTenantSerializer(required=False, many=True)
|
||||
regions = SerializedPKRelatedField(
|
||||
queryset=Region.objects.all(),
|
||||
serializer=NestedRegionSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
sites = SerializedPKRelatedField(
|
||||
queryset=Site.objects.all(),
|
||||
serializer=NestedSiteSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
roles = SerializedPKRelatedField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
serializer=NestedDeviceRoleSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
platforms = SerializedPKRelatedField(
|
||||
queryset=Platform.objects.all(),
|
||||
serializer=NestedPlatformSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
tenant_groups = SerializedPKRelatedField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
serializer=NestedTenantGroupSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
tenants = SerializedPKRelatedField(
|
||||
queryset=Tenant.objects.all(),
|
||||
serializer=NestedTenantSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = [
|
||||
'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenants',
|
||||
'data',
|
||||
'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms',
|
||||
'tenant_groups', 'tenants', 'data',
|
||||
]
|
||||
|
||||
|
||||
@@ -187,7 +219,7 @@ class ReportDetailSerializer(ReportSerializer):
|
||||
|
||||
class ObjectChangeSerializer(serializers.ModelSerializer):
|
||||
user = NestedUserSerializer(read_only=True)
|
||||
content_type = ContentTypeFieldSerializer(read_only=True)
|
||||
content_type = ContentTypeField(read_only=True)
|
||||
changed_object = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@@ -216,7 +248,7 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
|
||||
|
||||
class UserActionSerializer(serializers.ModelSerializer):
|
||||
user = NestedUserSerializer()
|
||||
action = ChoiceFieldSerializer(choices=ACTION_CHOICES)
|
||||
action = ChoiceField(choices=ACTION_CHOICES)
|
||||
|
||||
class Meta:
|
||||
model = UserAction
|
||||
|
||||
@@ -138,8 +138,11 @@ class ImageAttachmentViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class ConfigContextViewSet(ModelViewSet):
|
||||
queryset = ConfigContext.objects.prefetch_related('regions', 'sites', 'roles', 'platforms', 'tenants')
|
||||
queryset = ConfigContext.objects.prefetch_related(
|
||||
'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
|
||||
)
|
||||
serializer_class = serializers.ConfigContextSerializer
|
||||
filter_class = filters.ConfigContextFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -237,7 +240,7 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
||||
|
||||
class RecentActivityViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
List all UserActions to provide a log of recent activity.
|
||||
DEPRECATED: List all UserActions to provide a log of recent activity.
|
||||
"""
|
||||
queryset = UserAction.objects.all()
|
||||
serializer_class = serializers.UserActionSerializer
|
||||
|
||||
@@ -6,9 +6,10 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from taggit.models import Tag
|
||||
|
||||
from dcim.models import Site
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
|
||||
from .models import CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction
|
||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction
|
||||
|
||||
|
||||
class CustomFieldFilter(django_filters.Filter):
|
||||
@@ -124,6 +125,92 @@ class TopologyMapFilter(django_filters.FilterSet):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class ConfigContextFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='regions',
|
||||
queryset=Region.objects.all(),
|
||||
label='Region',
|
||||
)
|
||||
region = django_filters.ModelMultipleChoiceFilter(
|
||||
name='regions__slug',
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='sites',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='sites__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='roles',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label='Role',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='roles__slug',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='platforms',
|
||||
queryset=Platform.objects.all(),
|
||||
label='Platform',
|
||||
)
|
||||
platform = django_filters.ModelMultipleChoiceFilter(
|
||||
name='platforms__slug',
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Platform (slug)',
|
||||
)
|
||||
tenant_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant_groups',
|
||||
queryset=TenantGroup.objects.all(),
|
||||
label='Tenant group',
|
||||
)
|
||||
tenant_group = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant_groups__slug',
|
||||
queryset=TenantGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant group (slug)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenants',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenants__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = ['name', 'is_active']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(data__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ObjectChangeFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
|
||||
@@ -10,8 +10,12 @@ from mptt.forms import TreeNodeMultipleChoiceField
|
||||
from taggit.forms import TagField
|
||||
from taggit.models import Tag
|
||||
|
||||
from dcim.models import Region
|
||||
from utilities.forms import add_blank_choice, BootstrapMixin, BulkEditForm, LaxURLField, JSONField, SlugField
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, BulkEditForm, FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField,
|
||||
JSONField, SlugField,
|
||||
)
|
||||
from .constants import (
|
||||
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
||||
OBJECTCHANGE_ACTION_CHOICES,
|
||||
@@ -218,10 +222,42 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = [
|
||||
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenants', 'data',
|
||||
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups',
|
||||
'tenants', 'data',
|
||||
]
|
||||
|
||||
|
||||
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
region = FilterTreeNodeMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug'
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug'
|
||||
)
|
||||
role = FilterChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug'
|
||||
)
|
||||
platform = FilterChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug'
|
||||
)
|
||||
tenant_group = FilterChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
to_field_name='slug'
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug'
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Image attachments
|
||||
#
|
||||
|
||||
@@ -8,59 +8,82 @@ import uuid
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import curry
|
||||
|
||||
from .constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
||||
from extras.webhooks import enqueue_webhooks
|
||||
from .constants import (
|
||||
OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE,
|
||||
)
|
||||
from .models import ObjectChange
|
||||
|
||||
|
||||
_thread_locals = threading.local()
|
||||
|
||||
|
||||
def mark_object_changed(instance, **kwargs):
|
||||
def cache_changed_object(instance, **kwargs):
|
||||
|
||||
action = OBJECTCHANGE_ACTION_CREATE if kwargs['created'] else OBJECTCHANGE_ACTION_UPDATE
|
||||
|
||||
# Cache the object for further processing was the response has completed.
|
||||
_thread_locals.changed_objects.append(
|
||||
(instance, action)
|
||||
)
|
||||
|
||||
|
||||
def _record_object_deleted(request, instance, **kwargs):
|
||||
|
||||
# Record that the object was deleted.
|
||||
if hasattr(instance, 'log_change'):
|
||||
instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
|
||||
|
||||
enqueue_webhooks(instance, OBJECTCHANGE_ACTION_DELETE)
|
||||
|
||||
|
||||
class ObjectChangeMiddleware(object):
|
||||
"""
|
||||
Mark an object as having been created, saved, or updated. At the end of the request, this change will be recorded.
|
||||
We have to wait until the *end* of the request to the serialize the object, because related fields like tags and
|
||||
custom fields have not yet been updated when the post_save signal is emitted.
|
||||
This middleware performs two functions in response to an object being created, updated, or deleted:
|
||||
|
||||
1. Create an ObjectChange to reflect the modification to the object in the changelog.
|
||||
2. Enqueue any relevant webhooks.
|
||||
|
||||
The post_save and pre_delete signals are employed to catch object modifications, however changes are recorded a bit
|
||||
differently for each. Objects being saved are cached into thread-local storage for action *after* the response has
|
||||
completed. This ensures that serialization of the object is performed only after any related objects (e.g. tags)
|
||||
have been created. Conversely, deletions are acted upon immediately, so that the serialized representation of the
|
||||
object is recorded before it (and any related objects) are actually deleted from the database.
|
||||
"""
|
||||
if not hasattr(instance, 'log_change'):
|
||||
return
|
||||
|
||||
# Determine what action is being performed. The post_save signal sends a `created` boolean, whereas post_delete
|
||||
# does not.
|
||||
if 'created' in kwargs:
|
||||
action = OBJECTCHANGE_ACTION_CREATE if kwargs['created'] else OBJECTCHANGE_ACTION_UPDATE
|
||||
else:
|
||||
action = OBJECTCHANGE_ACTION_DELETE
|
||||
|
||||
_thread_locals.changed_objects.append((instance, action))
|
||||
|
||||
|
||||
class ChangeLoggingMiddleware(object):
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
|
||||
# Initialize the list of changed objects
|
||||
# Initialize an empty list to cache objects being saved.
|
||||
_thread_locals.changed_objects = []
|
||||
|
||||
# Assign a random unique ID to the request. This will be used to associate multiple object changes made during
|
||||
# the same request.
|
||||
request.id = uuid.uuid4()
|
||||
|
||||
# Connect mark_object_changed to the post_save and post_delete receivers
|
||||
post_save.connect(mark_object_changed, dispatch_uid='record_object_saved')
|
||||
post_delete.connect(mark_object_changed, dispatch_uid='record_object_deleted')
|
||||
# Signals don't include the request context, so we're currying it into the pre_delete function ahead of time.
|
||||
record_object_deleted = curry(_record_object_deleted, request)
|
||||
|
||||
# Connect our receivers to the post_save and pre_delete signals.
|
||||
post_save.connect(cache_changed_object, dispatch_uid='record_object_saved')
|
||||
post_delete.connect(record_object_deleted, dispatch_uid='record_object_deleted')
|
||||
|
||||
# Process the request
|
||||
response = self.get_response(request)
|
||||
|
||||
# Record object changes
|
||||
# Create records for any cached objects that were created/updated.
|
||||
for obj, action in _thread_locals.changed_objects:
|
||||
if obj.pk:
|
||||
|
||||
# Record the change
|
||||
if hasattr(obj, 'log_change'):
|
||||
obj.log_change(request.user, request.id, action)
|
||||
|
||||
# Enqueue webhooks
|
||||
enqueue_webhooks(obj, action)
|
||||
|
||||
# Housekeeping: 1% chance of clearing out expired ObjectChanges
|
||||
if _thread_locals.changed_objects and settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1:
|
||||
cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.14 on 2018-07-31 02:19
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import connection, migrations, models
|
||||
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):
|
||||
"""
|
||||
Verify that PostgreSQL is version 9.4 or higher.
|
||||
"""
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT VERSION()")
|
||||
row = cursor.fetchone()
|
||||
pg_version = re.match(r'^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1)
|
||||
if StrictVersion(pg_version) < StrictVersion('9.4.0'):
|
||||
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
|
||||
|
||||
# Skip if the database is missing (e.g. for CI testing) or misconfigured.
|
||||
except OperationalError:
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('extras', '0001_initial'), ('extras', '0002_custom_fields'), ('extras', '0003_exporttemplate_add_description'), ('extras', '0004_topologymap_change_comma_to_semicolon'), ('extras', '0005_useraction_add_bulk_create'), ('extras', '0006_add_imageattachments'), ('extras', '0007_unicode_literals'), ('extras', '0008_reports'), ('extras', '0009_topologymap_type'), ('extras', '0010_customfield_filter_logic')]
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('dcim', '0002_auto_20160622_1821'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ExportTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('template_code', models.TextField()),
|
||||
('mime_type', models.CharField(blank=True, max_length=15)),
|
||||
('file_extension', models.CharField(blank=True, max_length=15)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['content_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Graph',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('type', models.PositiveSmallIntegerField(choices=[(100, 'Interface'), (200, 'Provider'), (300, 'Site')])),
|
||||
('weight', models.PositiveSmallIntegerField(default=1000)),
|
||||
('name', models.CharField(max_length=100, verbose_name='Name')),
|
||||
('source', models.CharField(max_length=500, verbose_name='Source URL')),
|
||||
('link', models.URLField(blank=True, verbose_name='Link URL')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['type', 'weight', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TopologyMap',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('device_patterns', models.TextField(help_text='Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. Devices will be rendered in the order they are defined.')),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='topology_maps', to='dcim.Site')),
|
||||
('type', models.PositiveSmallIntegerField(choices=[(1, 'Network'), (2, 'Console'), (3, 'Power')], default=1)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserAction',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('time', models.DateTimeField(auto_now_add=True)),
|
||||
('object_id', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('action', models.PositiveSmallIntegerField(choices=[(1, 'created'), (7, 'bulk created'), (2, 'imported'), (3, 'modified'), (4, 'bulk edited'), (5, 'deleted'), (6, 'bulk deleted')])),
|
||||
('message', models.TextField(blank=True)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='actions', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-time'],
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='exporttemplate',
|
||||
unique_together=set([('content_type', 'name')]),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomField',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('type', models.PositiveSmallIntegerField(choices=[(100, 'Text'), (200, 'Integer'), (300, 'Boolean (true/false)'), (400, 'Date'), (500, 'URL'), (600, 'Selection')], default=100)),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('label', models.CharField(blank=True, help_text="Name of the field as displayed to users (if not provided, the field's name will be used)", max_length=50)),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('required', models.BooleanField(default=False, help_text='If true, this field is required when creating new objects or editing an existing object.')),
|
||||
('default', models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans.', max_length=100)),
|
||||
('weight', models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form.')),
|
||||
('obj_type', models.ManyToManyField(help_text='The object(s) to which this field applies.', related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)')),
|
||||
('filter_logic', models.PositiveSmallIntegerField(choices=[(0, 'Disabled'), (1, 'Loose'), (2, 'Exact')], default=1, help_text='Loose matches any instance of a given string; exact matches the entire field.')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['weight', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomFieldChoice',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.CharField(max_length=100)),
|
||||
('weight', models.PositiveSmallIntegerField(default=100, help_text='Higher weights appear lower in the list')),
|
||||
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['field', 'weight', 'value'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomFieldValue',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('obj_id', models.PositiveIntegerField()),
|
||||
('serialized_value', models.CharField(max_length=255)),
|
||||
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='values', to='extras.CustomField')),
|
||||
('obj_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['obj_type', 'obj_id'],
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='customfieldvalue',
|
||||
unique_together=set([('field', 'obj_type', 'obj_id')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='customfieldchoice',
|
||||
unique_together=set([('field', 'value')]),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ImageAttachment',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('image', models.ImageField(height_field='image_height', upload_to=extras.models.image_upload, width_field='image_width')),
|
||||
('image_height', models.PositiveSmallIntegerField()),
|
||||
('image_width', models.PositiveSmallIntegerField()),
|
||||
('name', models.CharField(blank=True, max_length=50)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=verify_postgresql_version,
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReportResult',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('report', models.CharField(max_length=255, unique=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('failed', models.BooleanField()),
|
||||
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['report'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,14 +1,15 @@
|
||||
# Generated by Django 2.0.6 on 2018-06-29 13:34
|
||||
# Generated by Django 2.0.7 on 2018-07-27 19:44
|
||||
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0005_change_logging'),
|
||||
('dcim', '0060_change_logging'),
|
||||
('dcim', '0061_platform_napalm_args'),
|
||||
('extras', '0013_objectchange'),
|
||||
]
|
||||
|
||||
@@ -19,13 +20,14 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('weight', models.PositiveSmallIntegerField(default=1000)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||
('platforms', models.ManyToManyField(blank=True, related_name='_configcontext_platforms_+', to='dcim.Platform')),
|
||||
('regions', models.ManyToManyField(blank=True, related_name='_configcontext_regions_+', to='dcim.Region')),
|
||||
('roles', models.ManyToManyField(blank=True, related_name='_configcontext_roles_+', to='dcim.DeviceRole')),
|
||||
('sites', models.ManyToManyField(blank=True, related_name='_configcontext_sites_+', to='dcim.Site')),
|
||||
('tenant_groups', models.ManyToManyField(blank=True, related_name='_configcontext_tenant_groups_+', to='tenancy.TenantGroup')),
|
||||
('tenants', models.ManyToManyField(blank=True, related_name='_configcontext_tenants_+', to='tenancy.Tenant')),
|
||||
],
|
||||
options={
|
||||
@@ -35,11 +37,16 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='obj_type',
|
||||
field=models.ManyToManyField(help_text='The object(s) to which this field applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'tenant', 'cluster', 'virtualmachine')}, related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'),
|
||||
field=models.ManyToManyField(help_text='The object(s) to which this field applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine')}, related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='exporttemplate',
|
||||
name='content_type',
|
||||
field=models.ForeignKey(limit_choices_to={'model__in': ['provider', 'circuit', 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', 'consoleport', 'powerport', 'interfaceconnection', 'virtualchassis', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='webhook',
|
||||
name='obj_type',
|
||||
field=models.ManyToManyField(help_text='The object(s) to which this Webhook applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'rackgroup', 'device', 'interface', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vlangroup', 'vrf', 'service', 'tenant', 'tenantgroup', 'cluster', 'clustergroup', 'virtualmachine')}, related_name='webhooks', to='contenttypes.ContentType', verbose_name='Object types'),
|
||||
field=models.ManyToManyField(help_text='The object(s) to which this Webhook applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'virtualchassis', 'consoleport', 'consoleserverport', 'powerport', 'poweroutlet', 'interface', 'devicebay', 'inventoryitem', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine')}, related_name='webhooks', to='contenttypes.ContentType', verbose_name='Object types'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -105,7 +105,10 @@ class Webhook(models.Model):
|
||||
# Custom fields
|
||||
#
|
||||
|
||||
class CustomFieldModel(object):
|
||||
class CustomFieldModel(models.Model):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def cf(self):
|
||||
"""
|
||||
@@ -674,6 +677,11 @@ class ConfigContext(models.Model):
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
tenant_groups = models.ManyToManyField(
|
||||
to='tenancy.TenantGroup',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
tenants = models.ManyToManyField(
|
||||
to='tenancy.Tenant',
|
||||
related_name='+',
|
||||
@@ -805,7 +813,6 @@ class ObjectChange(models.Model):
|
||||
editable=False
|
||||
)
|
||||
|
||||
serializer = 'extras.api.serializers.ObjectChangeSerializer'
|
||||
csv_headers = [
|
||||
'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id',
|
||||
'related_object_type', 'related_object_id', 'object_repr', 'object_data',
|
||||
@@ -896,10 +903,11 @@ class UserActionManager(models.Manager):
|
||||
self.log_bulk_action(user, content_type, ACTION_BULK_DELETE, message)
|
||||
|
||||
|
||||
# TODO: Remove UserAction, which has been replaced by ObjectChange.
|
||||
@python_2_unicode_compatible
|
||||
class UserAction(models.Model):
|
||||
"""
|
||||
A record of an action (add, edit, or delete) performed on an object by a User.
|
||||
DEPRECATED: A record of an action (add, edit, or delete) performed on an object by a User.
|
||||
"""
|
||||
time = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
|
||||
@@ -13,11 +13,22 @@ class ConfigContextQuerySet(QuerySet):
|
||||
# `device_role` for Device; `role` for VirtualMachine
|
||||
role = getattr(obj, 'device_role', None) or obj.role
|
||||
|
||||
# Get the group of the assigned tenant, if any
|
||||
tenant_group = obj.tenant.group if obj.tenant else None
|
||||
|
||||
# Match against the directly assigned region as well as any parent regions.
|
||||
region = getattr(obj.site, 'region', None)
|
||||
if region:
|
||||
regions = region.get_ancestors(include_self=True)
|
||||
else:
|
||||
regions = []
|
||||
|
||||
return self.filter(
|
||||
Q(regions=getattr(obj.site, 'region', None)) | Q(regions=None),
|
||||
Q(regions__in=regions) | Q(regions=None),
|
||||
Q(sites=obj.site) | Q(sites=None),
|
||||
Q(roles=role) | Q(roles=None),
|
||||
Q(tenants=obj.tenant) | Q(tenants=None),
|
||||
Q(platforms=obj.platform) | Q(platforms=None),
|
||||
Q(tenant_groups=tenant_group) | Q(tenant_groups=None),
|
||||
Q(tenants=obj.tenant) | Q(tenants=None),
|
||||
is_active=True,
|
||||
).order_by('weight', 'name')
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
import importlib
|
||||
import inspect
|
||||
import pkgutil
|
||||
from collections import OrderedDict
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
@@ -16,19 +17,36 @@ def is_report(obj):
|
||||
"""
|
||||
Returns True if the given object is a Report.
|
||||
"""
|
||||
if obj in Report.__subclasses__():
|
||||
return True
|
||||
return False
|
||||
return obj in Report.__subclasses__()
|
||||
|
||||
|
||||
def get_report(module_name, report_name):
|
||||
"""
|
||||
Return a specific report from within a module.
|
||||
"""
|
||||
module = importlib.import_module('reports.{}'.format(module_name))
|
||||
file_path = '{}/{}.py'.format(settings.REPORTS_ROOT, module_name)
|
||||
|
||||
# Python 3.5+
|
||||
if sys.version_info >= (3, 5):
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
# Python 2.7
|
||||
else:
|
||||
import imp
|
||||
try:
|
||||
module = imp.load_source(module_name, file_path)
|
||||
except IOError:
|
||||
return None
|
||||
|
||||
report = getattr(module, report_name, None)
|
||||
if report is None:
|
||||
return None
|
||||
|
||||
return report()
|
||||
|
||||
|
||||
@@ -46,8 +64,8 @@ def get_reports():
|
||||
|
||||
# Iterate through all modules within the reports path. These are the user-created files in which reports are
|
||||
# defined.
|
||||
for importer, module_name, is_pkg in pkgutil.walk_packages([settings.REPORTS_ROOT]):
|
||||
module = importlib.import_module('reports.{}'.format(module_name))
|
||||
for importer, module_name, _ in pkgutil.iter_modules([settings.REPORTS_ROOT]):
|
||||
module = importer.find_module(module_name).load_module(module_name)
|
||||
report_list = [cls() for _, cls in inspect.getmembers(module, is_report)]
|
||||
module_list.append((module_name, report_list))
|
||||
|
||||
@@ -104,7 +122,7 @@ class Report(object):
|
||||
|
||||
@property
|
||||
def module(self):
|
||||
return self.__module__.rsplit('.', 1)[1]
|
||||
return self.__module__
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -72,15 +72,10 @@ class ConfigContextTable(BaseTable):
|
||||
is_active = BooleanColumn(
|
||||
verbose_name='Active'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=CONFIGCONTEXT_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConfigContext
|
||||
fields = ('pk', 'name', 'weight', 'is_active', 'description', 'actions')
|
||||
fields = ('pk', 'name', 'weight', 'is_active', 'description')
|
||||
|
||||
|
||||
class ObjectChangeTable(BaseTable):
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
from taggit.models import Tag
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Region, Site
|
||||
from extras.constants import GRAPH_TYPE_SITE
|
||||
from extras.models import ConfigContext, Graph, ExportTemplate
|
||||
from users.models import Token
|
||||
from utilities.testing import HttpStatusMixin
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.testing import APITestCase
|
||||
|
||||
|
||||
class GraphTest(HttpStatusMixin, APITestCase):
|
||||
class GraphTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(GraphTest, self).setUp()
|
||||
|
||||
self.graph1 = Graph.objects.create(
|
||||
type=GRAPH_TYPE_SITE, name='Test Graph 1', source='http://example.com/graphs.py?site={{ obj.name }}&foo=1'
|
||||
@@ -120,13 +116,11 @@ class GraphTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(Graph.objects.count(), 2)
|
||||
|
||||
|
||||
class ExportTemplateTest(HttpStatusMixin, APITestCase):
|
||||
class ExportTemplateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(ExportTemplateTest, self).setUp()
|
||||
|
||||
self.content_type = ContentType.objects.get_for_model(Device)
|
||||
self.exporttemplate1 = ExportTemplate.objects.create(
|
||||
@@ -229,13 +223,11 @@ class ExportTemplateTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(ExportTemplate.objects.count(), 2)
|
||||
|
||||
|
||||
class TagTest(HttpStatusMixin, APITestCase):
|
||||
class TagTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(TagTest, self).setUp()
|
||||
|
||||
self.tag1 = Tag.objects.create(name='Test Tag 1', slug='test-tag-1')
|
||||
self.tag2 = Tag.objects.create(name='Test Tag 2', slug='test-tag-2')
|
||||
@@ -322,13 +314,11 @@ class TagTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(Tag.objects.count(), 2)
|
||||
|
||||
|
||||
class ConfigContextTest(HttpStatusMixin, APITestCase):
|
||||
class ConfigContextTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(ConfigContextTest, self).setUp()
|
||||
|
||||
self.configcontext1 = ConfigContext.objects.create(
|
||||
name='Test Config Context 1',
|
||||
@@ -363,9 +353,28 @@ class ConfigContextTest(HttpStatusMixin, APITestCase):
|
||||
|
||||
def test_create_configcontext(self):
|
||||
|
||||
region1 = Region.objects.create(name='Test Region 1', slug='test-region-1')
|
||||
region2 = Region.objects.create(name='Test Region 2', slug='test-region-2')
|
||||
site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
||||
role1 = DeviceRole.objects.create(name='Test Role 1', slug='test-role-1')
|
||||
role2 = DeviceRole.objects.create(name='Test Role 2', slug='test-role-2')
|
||||
platform1 = Platform.objects.create(name='Test Platform 1', slug='test-platform-1')
|
||||
platform2 = Platform.objects.create(name='Test Platform 2', slug='test-platform-2')
|
||||
tenantgroup1 = TenantGroup.objects.create(name='Test Tenant Group 1', slug='test-tenant-group-1')
|
||||
tenantgroup2 = TenantGroup.objects.create(name='Test Tenant Group 2', slug='test-tenant-group-2')
|
||||
tenant1 = Tenant.objects.create(name='Test Tenant 1', slug='test-tenant-1')
|
||||
tenant2 = Tenant.objects.create(name='Test Tenant 2', slug='test-tenant-2')
|
||||
|
||||
data = {
|
||||
'name': 'Test Config Context 4',
|
||||
'weight': 1000,
|
||||
'regions': [region1.pk, region2.pk],
|
||||
'sites': [site1.pk, site2.pk],
|
||||
'roles': [role1.pk, role2.pk],
|
||||
'platforms': [platform1.pk, platform2.pk],
|
||||
'tenant_groups': [tenantgroup1.pk, tenantgroup2.pk],
|
||||
'tenants': [tenant1.pk, tenant2.pk],
|
||||
'data': {'foo': 'XXX'}
|
||||
}
|
||||
|
||||
@@ -376,6 +385,18 @@ class ConfigContextTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(ConfigContext.objects.count(), 4)
|
||||
configcontext4 = ConfigContext.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(configcontext4.name, data['name'])
|
||||
self.assertEqual(region1.pk, data['regions'][0])
|
||||
self.assertEqual(region2.pk, data['regions'][1])
|
||||
self.assertEqual(site1.pk, data['sites'][0])
|
||||
self.assertEqual(site2.pk, data['sites'][1])
|
||||
self.assertEqual(role1.pk, data['roles'][0])
|
||||
self.assertEqual(role2.pk, data['roles'][1])
|
||||
self.assertEqual(platform1.pk, data['platforms'][0])
|
||||
self.assertEqual(platform2.pk, data['platforms'][1])
|
||||
self.assertEqual(tenantgroup1.pk, data['tenant_groups'][0])
|
||||
self.assertEqual(tenantgroup2.pk, data['tenant_groups'][1])
|
||||
self.assertEqual(tenant1.pk, data['tenants'][0])
|
||||
self.assertEqual(tenant2.pk, data['tenants'][1])
|
||||
self.assertEqual(configcontext4.data, data['data'])
|
||||
|
||||
def test_create_configcontext_bulk(self):
|
||||
@@ -406,9 +427,13 @@ class ConfigContextTest(HttpStatusMixin, APITestCase):
|
||||
|
||||
def test_update_configcontext(self):
|
||||
|
||||
region1 = Region.objects.create(name='Test Region 1', slug='test-region-1')
|
||||
region2 = Region.objects.create(name='Test Region 2', slug='test-region-2')
|
||||
|
||||
data = {
|
||||
'name': 'Test Config Context X',
|
||||
'weight': 999,
|
||||
'regions': [region1.pk, region2.pk],
|
||||
'data': {'foo': 'XXX'}
|
||||
}
|
||||
|
||||
@@ -420,6 +445,7 @@ class ConfigContextTest(HttpStatusMixin, APITestCase):
|
||||
configcontext1 = ConfigContext.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(configcontext1.name, data['name'])
|
||||
self.assertEqual(configcontext1.weight, data['weight'])
|
||||
self.assertEqual(sorted([r.pk for r in configcontext1.regions.all()]), sorted(data['regions']))
|
||||
self.assertEqual(configcontext1.data, data['data'])
|
||||
|
||||
def test_delete_configcontext(self):
|
||||
|
||||
@@ -2,18 +2,15 @@ from __future__ import unicode_literals
|
||||
|
||||
from datetime import date
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from dcim.models import Site
|
||||
from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL
|
||||
from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
|
||||
from users.models import Token
|
||||
from utilities.testing import HttpStatusMixin
|
||||
from utilities.testing import APITestCase
|
||||
|
||||
|
||||
class CustomFieldTest(TestCase):
|
||||
@@ -102,13 +99,11 @@ class CustomFieldTest(TestCase):
|
||||
cf.delete()
|
||||
|
||||
|
||||
class CustomFieldAPITest(HttpStatusMixin, APITestCase):
|
||||
class CustomFieldAPITest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(CustomFieldAPITest, self).setUp()
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Site)
|
||||
|
||||
|
||||
55
netbox/extras/tests/test_tags.py
Normal file
55
netbox/extras/tests/test_tags.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from dcim.models import Site
|
||||
from utilities.testing import APITestCase
|
||||
|
||||
|
||||
class TaggedItemTest(APITestCase):
|
||||
"""
|
||||
Test the application of Tags to and item (a Site, for example) upon creation (POST) and modification (PATCH).
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(TaggedItemTest, self).setUp()
|
||||
|
||||
def test_create_tagged_item(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site',
|
||||
'slug': 'test-site',
|
||||
'tags': ['Foo', 'Bar', 'Baz']
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(sorted(response.data['tags']), sorted(data['tags']))
|
||||
site = Site.objects.get(pk=response.data['id'])
|
||||
tags = [tag.name for tag in site.tags.all()]
|
||||
self.assertEqual(sorted(tags), sorted(data['tags']))
|
||||
|
||||
def test_update_tagged_item(self):
|
||||
|
||||
site = Site.objects.create(
|
||||
name='Test Site',
|
||||
slug='test-site',
|
||||
tags=['Foo', 'Bar', 'Baz']
|
||||
)
|
||||
|
||||
data = {
|
||||
'tags': ['Foo', 'Bar', 'New Tag']
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': site.pk})
|
||||
response = self.client.patch(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(sorted(response.data['tags']), sorted(data['tags']))
|
||||
site = Site.objects.get(pk=response.data['id'])
|
||||
tags = [tag.name for tag in site.tags.all()]
|
||||
self.assertEqual(sorted(tags), sorted(data['tags']))
|
||||
@@ -14,7 +14,7 @@ from taggit.models import Tag
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView
|
||||
from . import filters
|
||||
from .forms import ConfigContextForm, ImageAttachmentForm, ObjectChangeFilterForm, TagForm
|
||||
from .forms import ConfigContextForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm, TagForm
|
||||
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult
|
||||
from .reports import get_report, get_reports
|
||||
from .tables import ConfigContextTable, ObjectChangeTable, TagTable
|
||||
@@ -45,7 +45,6 @@ class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuittype'
|
||||
cls = Tag
|
||||
queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name')
|
||||
table = TagTable
|
||||
default_return_url = 'extras:tag_list'
|
||||
@@ -57,6 +56,8 @@ class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
|
||||
class ConfigContextListView(ObjectListView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
filter = filters.ConfigContextFilter
|
||||
filter_form = ConfigContextFilterForm
|
||||
table = ConfigContextTable
|
||||
template_name = 'extras/configcontext_list.html'
|
||||
|
||||
@@ -92,7 +93,6 @@ class ConfigContextDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class ConfigContextBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'extras.delete_cconfigcontext'
|
||||
cls = ConfigContext
|
||||
queryset = ConfigContext.objects.all()
|
||||
table = ConfigContextTable
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
|
||||
@@ -1,119 +1,51 @@
|
||||
import time
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch import Signal
|
||||
|
||||
from extras.models import Webhook
|
||||
from utilities.utils import dynamic_import
|
||||
from extras.constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
||||
from utilities.api import get_serializer_for_model
|
||||
from .constants import WEBHOOK_MODELS
|
||||
|
||||
|
||||
def enqueue_webhooks(webhooks, model_class, data, event, signal_received_timestamp):
|
||||
def enqueue_webhooks(instance, action):
|
||||
"""
|
||||
Serialize data and enqueue webhooks
|
||||
Find Webhook(s) assigned to this instance + action and enqueue them
|
||||
to be processed
|
||||
"""
|
||||
serializer_context = {
|
||||
'request': None,
|
||||
}
|
||||
if not settings.WEBHOOKS_ENABLED or instance._meta.model_name not in WEBHOOK_MODELS:
|
||||
return
|
||||
|
||||
if isinstance(data, list):
|
||||
serializer_property = data[0].serializer
|
||||
serializer_cls = dynamic_import(serializer_property)
|
||||
serialized_data = serializer_cls(data, context=serializer_context, many=True)
|
||||
else:
|
||||
serializer_property = data.serializer
|
||||
serializer_cls = dynamic_import(serializer_property)
|
||||
serialized_data = serializer_cls(data, context=serializer_context)
|
||||
# Retrieve any applicable Webhooks
|
||||
action_flag = {
|
||||
OBJECTCHANGE_ACTION_CREATE: 'type_create',
|
||||
OBJECTCHANGE_ACTION_UPDATE: 'type_update',
|
||||
OBJECTCHANGE_ACTION_DELETE: 'type_delete',
|
||||
}[action]
|
||||
obj_type = ContentType.objects.get_for_model(instance.__class__)
|
||||
webhooks = Webhook.objects.filter(obj_type=obj_type, enabled=True, **{action_flag: True})
|
||||
|
||||
from django_rq import get_queue
|
||||
webhook_queue = get_queue('default')
|
||||
if webhooks.exists():
|
||||
# Get the Model's API serializer class and serialize the object
|
||||
serializer_class = get_serializer_for_model(instance.__class__)
|
||||
serializer_context = {
|
||||
'request': None,
|
||||
}
|
||||
serializer = serializer_class(instance, context=serializer_context)
|
||||
|
||||
for webhook in webhooks:
|
||||
webhook_queue.enqueue("extras.webhooks_worker.process_webhook",
|
||||
webhook,
|
||||
serialized_data.data,
|
||||
model_class,
|
||||
event,
|
||||
signal_received_timestamp)
|
||||
# We must only import django_rq if the Webhooks feature is enabled.
|
||||
# Only if we have gotten to ths point, is the feature enabled
|
||||
from django_rq import get_queue
|
||||
webhook_queue = get_queue('default')
|
||||
|
||||
|
||||
def post_save_receiver(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Receives post_save signals from registered models. If the webhook
|
||||
backend is enabled, queue any webhooks that apply to the event.
|
||||
"""
|
||||
if settings.WEBHOOKS_ENABLED:
|
||||
signal_received_timestamp = time.time()
|
||||
# look for any webhooks that match this event
|
||||
updated = not created
|
||||
obj_type = ContentType.objects.get_for_model(sender)
|
||||
webhooks = Webhook.objects.filter(
|
||||
Q(enabled=True) &
|
||||
(
|
||||
Q(type_create=created) |
|
||||
Q(type_update=updated)
|
||||
) &
|
||||
Q(obj_type=obj_type)
|
||||
)
|
||||
event = 'created' if created else 'updated'
|
||||
if webhooks:
|
||||
enqueue_webhooks(webhooks, sender, instance, event, signal_received_timestamp)
|
||||
|
||||
|
||||
def post_delete_receiver(sender, instance, **kwargs):
|
||||
"""
|
||||
Receives post_delete signals from registered models. If the webhook
|
||||
backend is enabled, queue any webhooks that apply to the event.
|
||||
"""
|
||||
if settings.WEBHOOKS_ENABLED:
|
||||
signal_received_timestamp = time.time()
|
||||
obj_type = ContentType.objects.get_for_model(sender)
|
||||
# look for any webhooks that match this event
|
||||
webhooks = Webhook.objects.filter(enabled=True, type_delete=True, obj_type=obj_type)
|
||||
if webhooks:
|
||||
enqueue_webhooks(webhooks, sender, instance, 'deleted', signal_received_timestamp)
|
||||
|
||||
|
||||
def bulk_operation_receiver(sender, **kwargs):
|
||||
"""
|
||||
Receives bulk_operation_signal signals from registered models. If the webhook
|
||||
backend is enabled, queue any webhooks that apply to the event.
|
||||
"""
|
||||
if settings.WEBHOOKS_ENABLED:
|
||||
signal_received_timestamp = time.time()
|
||||
event = kwargs['event']
|
||||
obj_type = ContentType.objects.get_for_model(sender)
|
||||
# look for any webhooks that match this event
|
||||
if event == 'created':
|
||||
webhooks = Webhook.objects.filter(enabled=True, type_create=True, obj_type=obj_type)
|
||||
elif event == 'updated':
|
||||
webhooks = Webhook.objects.filter(enabled=True, type_update=True, obj_type=obj_type)
|
||||
elif event == 'deleted':
|
||||
webhooks = Webhook.objects.filter(enabled=True, type_delete=True, obj_type=obj_type)
|
||||
else:
|
||||
webhooks = None
|
||||
|
||||
if webhooks:
|
||||
enqueue_webhooks(webhooks, sender, list(kwargs['instances']), event, signal_received_timestamp)
|
||||
|
||||
|
||||
# the bulk operation signal is used to overcome signals not being sent for bulk model changes
|
||||
bulk_operation_signal = Signal(providing_args=["instances", "event"])
|
||||
bulk_operation_signal.connect(bulk_operation_receiver)
|
||||
|
||||
|
||||
def register_signals(senders):
|
||||
"""
|
||||
Take a list of senders (Models) and register them to the post_save
|
||||
and post_delete signal receivers.
|
||||
"""
|
||||
if settings.WEBHOOKS_ENABLED:
|
||||
# only register signals if the backend is enabled
|
||||
# this reduces load by not firing signals if the
|
||||
# webhook backend feature is disabled
|
||||
|
||||
for sender in senders:
|
||||
post_save.connect(post_save_receiver, sender=sender)
|
||||
post_delete.connect(post_delete_receiver, sender=sender)
|
||||
# enqueue the webhooks:
|
||||
for webhook in webhooks:
|
||||
webhook_queue.enqueue(
|
||||
"extras.webhooks_worker.process_webhook",
|
||||
webhook,
|
||||
serializer.data,
|
||||
instance.__class__,
|
||||
action,
|
||||
str(datetime.datetime.now())
|
||||
)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
import requests
|
||||
from django_rq import job
|
||||
import json
|
||||
|
||||
from extras.constants import WEBHOOK_CT_JSON, WEBHOOK_CT_X_WWW_FORM_ENCODED
|
||||
from django_rq import job
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
|
||||
from extras.constants import WEBHOOK_CT_JSON, WEBHOOK_CT_X_WWW_FORM_ENCODED, OBJECTCHANGE_ACTION_CHOICES
|
||||
|
||||
|
||||
@job('default')
|
||||
@@ -13,9 +15,9 @@ def process_webhook(webhook, data, model_class, event, timestamp):
|
||||
Make a POST request to the defined Webhook
|
||||
"""
|
||||
payload = {
|
||||
'event': event,
|
||||
'event': dict(OBJECTCHANGE_ACTION_CHOICES)[event].lower(),
|
||||
'timestamp': timestamp,
|
||||
'model': model_class.__name__,
|
||||
'model': model_class._meta.model_name,
|
||||
'data': data
|
||||
}
|
||||
headers = {
|
||||
@@ -28,15 +30,19 @@ def process_webhook(webhook, data, model_class, event, timestamp):
|
||||
}
|
||||
|
||||
if webhook.http_content_type == WEBHOOK_CT_JSON:
|
||||
params.update({'json': payload})
|
||||
params.update({'data': json.dumps(payload, cls=JSONEncoder)})
|
||||
elif webhook.http_content_type == WEBHOOK_CT_X_WWW_FORM_ENCODED:
|
||||
params.update({'data': payload})
|
||||
|
||||
prepared_request = requests.Request(**params).prepare()
|
||||
|
||||
if webhook.secret != '':
|
||||
# sign the request with the secret
|
||||
hmac_prep = hmac.new(bytearray(webhook.secret, 'utf8'), prepared_request.body, digestmod=hashlib.sha512)
|
||||
# Sign the request with a hash of the secret key and its content.
|
||||
hmac_prep = hmac.new(
|
||||
key=webhook.secret.encode('utf8'),
|
||||
msg=prepared_request.body.encode('utf8'),
|
||||
digestmod=hashlib.sha512
|
||||
)
|
||||
prepared_request.headers['X-Hook-Signature'] = hmac_prep.hexdigest()
|
||||
|
||||
with requests.Session() as session:
|
||||
|
||||
@@ -5,7 +5,7 @@ from collections import OrderedDict
|
||||
from rest_framework import serializers
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from taggit.models import Tag
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
|
||||
from dcim.models import Interface
|
||||
@@ -16,7 +16,7 @@ from ipam.constants import (
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from utilities.api import (
|
||||
ChoiceFieldSerializer, SerializedPKRelatedField, TagField, ValidatedModelSerializer, WritableNestedSerializer,
|
||||
ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer,
|
||||
)
|
||||
from virtualization.api.serializers import NestedVirtualMachineSerializer
|
||||
|
||||
@@ -25,9 +25,9 @@ from virtualization.api.serializers import NestedVirtualMachineSerializer
|
||||
# VRFs
|
||||
#
|
||||
|
||||
class VRFSerializer(CustomFieldModelSerializer):
|
||||
class VRFSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
@@ -87,9 +87,9 @@ class NestedRIRSerializer(WritableNestedSerializer):
|
||||
# Aggregates
|
||||
#
|
||||
|
||||
class AggregateSerializer(CustomFieldModelSerializer):
|
||||
class AggregateSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
rir = NestedRIRSerializer()
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
@@ -147,13 +147,13 @@ class NestedVLANGroupSerializer(WritableNestedSerializer):
|
||||
# VLANs
|
||||
#
|
||||
|
||||
class VLANSerializer(CustomFieldModelSerializer):
|
||||
class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
group = NestedVLANGroupSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceFieldSerializer(choices=VLAN_STATUS_CHOICES, required=False)
|
||||
status = ChoiceField(choices=VLAN_STATUS_CHOICES, required=False)
|
||||
role = NestedRoleSerializer(required=False, allow_null=True)
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
@@ -190,14 +190,14 @@ class NestedVLANSerializer(WritableNestedSerializer):
|
||||
# Prefixes
|
||||
#
|
||||
|
||||
class PrefixSerializer(CustomFieldModelSerializer):
|
||||
class PrefixSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
status = ChoiceFieldSerializer(choices=PREFIX_STATUS_CHOICES, required=False)
|
||||
status = ChoiceField(choices=PREFIX_STATUS_CHOICES, required=False)
|
||||
role = NestedRoleSerializer(required=False, allow_null=True)
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
@@ -234,10 +234,10 @@ class AvailablePrefixSerializer(serializers.Serializer):
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
class IPAddressInterfaceSerializer(serializers.ModelSerializer):
|
||||
class IPAddressInterfaceSerializer(WritableNestedSerializer):
|
||||
url = serializers.SerializerMethodField() # We're imitating a HyperlinkedIdentityField here
|
||||
device = NestedDeviceSerializer()
|
||||
virtual_machine = NestedVirtualMachineSerializer()
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
virtual_machine = NestedVirtualMachineSerializer(read_only=True)
|
||||
|
||||
class Meta(InterfaceSerializer.Meta):
|
||||
model = Interface
|
||||
@@ -254,13 +254,13 @@ class IPAddressInterfaceSerializer(serializers.ModelSerializer):
|
||||
return reverse(url_name, kwargs={'pk': obj.pk}, request=self.context['request'])
|
||||
|
||||
|
||||
class IPAddressSerializer(CustomFieldModelSerializer):
|
||||
class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES, required=False)
|
||||
role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES, required=False)
|
||||
status = ChoiceField(choices=IPADDRESS_STATUS_CHOICES, required=False)
|
||||
role = ChoiceField(choices=IPADDRESS_ROLE_CHOICES, required=False, allow_null=True)
|
||||
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
|
||||
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
@@ -304,7 +304,7 @@ class AvailableIPSerializer(serializers.Serializer):
|
||||
class ServiceSerializer(CustomFieldModelSerializer):
|
||||
device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
|
||||
protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES)
|
||||
protocol = ChoiceField(choices=IP_PROTOCOL_CHOICES)
|
||||
ipaddresses = SerializedPKRelatedField(
|
||||
queryset=IPAddress.objects.all(),
|
||||
serializer=NestedIPAddressSerializer,
|
||||
|
||||
@@ -140,10 +140,11 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
||||
available_prefixes.remove(allocated_prefix)
|
||||
|
||||
# Initialize the serializer with a list or a single object depending on what was requested
|
||||
context = {'request': request}
|
||||
if isinstance(request.data, list):
|
||||
serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True)
|
||||
serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True, context=context)
|
||||
else:
|
||||
serializer = serializers.PrefixSerializer(data=requested_prefixes[0])
|
||||
serializer = serializers.PrefixSerializer(data=requested_prefixes[0], context=context)
|
||||
|
||||
# Create the new Prefix(es)
|
||||
if serializer.is_valid():
|
||||
@@ -193,15 +194,17 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
||||
|
||||
# Assign addresses from the list of available IPs and copy VRF assignment from the parent prefix
|
||||
available_ips = iter(available_ips)
|
||||
prefix_length = prefix.prefix.prefixlen
|
||||
for requested_ip in requested_ips:
|
||||
requested_ip['address'] = next(available_ips)
|
||||
requested_ip['address'] = '{}/{}'.format(next(available_ips), prefix_length)
|
||||
requested_ip['vrf'] = prefix.vrf.pk if prefix.vrf else None
|
||||
|
||||
# Initialize the serializer with a list or a single object depending on what was requested
|
||||
context = {'request': request}
|
||||
if isinstance(request.data, list):
|
||||
serializer = serializers.IPAddressSerializer(data=requested_ips, many=True)
|
||||
serializer = serializers.IPAddressSerializer(data=requested_ips, many=True, context=context)
|
||||
else:
|
||||
serializer = serializers.IPAddressSerializer(data=requested_ips[0])
|
||||
serializer = serializers.IPAddressSerializer(data=requested_ips[0], context=context)
|
||||
|
||||
# Create the new IP address(es)
|
||||
if serializer.is_valid():
|
||||
|
||||
@@ -6,10 +6,3 @@ from django.apps import AppConfig
|
||||
class IPAMConfig(AppConfig):
|
||||
name = "ipam"
|
||||
verbose_name = "IPAM"
|
||||
|
||||
def ready(self):
|
||||
|
||||
# register webhook signals
|
||||
from extras.webhooks import register_signals
|
||||
from .models import Aggregate, Prefix, IPAddress, VLAN, VRF, VLANGroup, Service
|
||||
register_signals([Aggregate, Prefix, IPAddress, VLAN, VRF, VLANGroup, Service])
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.14 on 2018-07-31 02:12
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import ipam.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('ipam', '0002_vrf_add_enforce_unique'), ('ipam', '0003_ipam_add_vlangroups'), ('ipam', '0004_ipam_vlangroup_uniqueness'), ('ipam', '0005_auto_20160725_1842'), ('ipam', '0006_vrf_vlan_add_tenant'), ('ipam', '0007_prefix_ipaddress_add_tenant'), ('ipam', '0008_prefix_change_order'), ('ipam', '0009_ipaddress_add_status'), ('ipam', '0010_ipaddress_help_texts'), ('ipam', '0011_rir_add_is_private'), ('ipam', '0012_services'), ('ipam', '0013_prefix_add_is_pool'), ('ipam', '0014_ipaddress_status_add_deprecated'), ('ipam', '0015_global_vlans'), ('ipam', '0016_unicode_literals'), ('ipam', '0017_ipaddress_roles'), ('ipam', '0018_remove_service_uniqueness_constraint')]
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0010_devicebay_installed_device_set_null'),
|
||||
('dcim', '0022_color_names_to_rgb'),
|
||||
('tenancy', '0001_initial'),
|
||||
('ipam', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='vrf',
|
||||
name='enforce_unique',
|
||||
field=models.BooleanField(default=True, help_text='Prevent duplicate prefixes/IP addresses within this VRF', verbose_name='Enforce unique space'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VLANGroup',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('slug', models.SlugField()),
|
||||
('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlan_groups', to='dcim.Site')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['site', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='vlan',
|
||||
name='group',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='ipam.VLANGroup'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='vlangroup',
|
||||
unique_together=set([('site', 'slug'), ('site', 'name')]),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='vlan',
|
||||
options={'ordering': ['site', 'group', 'vid'], 'verbose_name': 'VLAN', 'verbose_name_plural': 'VLANs'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='vlangroup',
|
||||
options={'ordering': ['site', 'name'], 'verbose_name': 'VLAN group', 'verbose_name_plural': 'VLAN groups'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='vlan',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='vlan',
|
||||
unique_together=set([('group', 'vid'), ('group', 'name')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vlan',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='vlan',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='vrf',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vrfs', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ipaddress',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='prefix',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='prefix',
|
||||
options={'ordering': ['vrf', 'family', 'prefix'], 'verbose_name_plural': 'prefixes'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ipaddress',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated'), (5, 'DHCP')], default=1, help_text='The operational status of this IP', verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='address',
|
||||
field=ipam.fields.IPAddressField(help_text=b'IPv4 or IPv6 address (with mask)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='nat_inside',
|
||||
field=models.OneToOneField(blank=True, help_text=b'The IP for which this address is the "outside" IP', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nat_outside', to='ipam.IPAddress', verbose_name=b'NAT (Inside)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rir',
|
||||
name='is_private',
|
||||
field=models.BooleanField(default=False, help_text='IP space managed by this RIR is considered private', verbose_name='Private'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Service',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateField(auto_now_add=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('protocol', models.PositiveSmallIntegerField(choices=[(6, 'TCP'), (17, 'UDP')])),
|
||||
('port', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)], verbose_name='Port number')),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='dcim.Device', verbose_name='device')),
|
||||
('ipaddresses', models.ManyToManyField(blank=True, related_name='services', to='ipam.IPAddress', verbose_name='IP addresses')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'protocol', 'port'],
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='service',
|
||||
unique_together=set([('device', 'protocol', 'port')]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='prefix',
|
||||
name='is_pool',
|
||||
field=models.BooleanField(default=False, help_text='All IP addresses within this prefix are considered usable', verbose_name='Is a pool'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='prefix',
|
||||
field=ipam.fields.IPNetworkField(help_text=b'IPv4 or IPv6 network with mask'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='role',
|
||||
field=models.ForeignKey(blank=True, help_text=b'The primary function of this prefix', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='prefixes', to='ipam.Role'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vlan',
|
||||
name='site',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='dcim.Site'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='aggregate',
|
||||
name='family',
|
||||
field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='aggregate',
|
||||
name='rir',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='aggregates', to='ipam.RIR', verbose_name='RIR'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='address',
|
||||
field=ipam.fields.IPAddressField(help_text='IPv4 or IPv6 address (with mask)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='family',
|
||||
field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')], editable=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='nat_inside',
|
||||
field=models.OneToOneField(blank=True, help_text='The IP for which this address is the "outside" IP', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nat_outside', to='ipam.IPAddress', verbose_name='NAT (Inside)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='vrf',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='ipam.VRF', verbose_name='VRF'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='family',
|
||||
field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')], editable=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='prefix',
|
||||
field=ipam.fields.IPNetworkField(help_text='IPv4 or IPv6 network with mask'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='role',
|
||||
field=models.ForeignKey(blank=True, help_text='The primary function of this prefix', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='prefixes', to='ipam.Role'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(0, 'Container'), (1, 'Active'), (2, 'Reserved'), (3, 'Deprecated')], default=1, help_text='Operational status of this prefix', verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='vlan',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VLAN', verbose_name='VLAN'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='vrf',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VRF', verbose_name='VRF'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vlan',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated')], default=1, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vlan',
|
||||
name='vid',
|
||||
field=models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)], verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vrf',
|
||||
name='rd',
|
||||
field=models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ipaddress',
|
||||
name='role',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[(10, 'Loopback'), (20, 'Secondary'), (30, 'Anycast'), (40, 'VIP'), (41, 'VRRP'), (42, 'HSRP'), (43, 'GLBP')], help_text='The functional role of this IP', null=True, verbose_name='Role'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='service',
|
||||
unique_together=set([]),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.14 on 2018-07-31 02:14
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('ipam', '0019_virtualization'), ('ipam', '0020_ipaddress_add_role_carp')]
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0018_remove_service_uniqueness_constraint'),
|
||||
('virtualization', '0001_virtualization'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='service',
|
||||
options={'ordering': ['protocol', 'port']},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='service',
|
||||
name='virtual_machine',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='services', to='virtualization.VirtualMachine'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='service',
|
||||
name='device',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='services', to='dcim.Device', verbose_name='device'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='role',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[(10, 'Loopback'), (20, 'Secondary'), (30, 'Anycast'), (40, 'VIP'), (41, 'VRRP'), (42, 'HSRP'), (43, 'GLBP'), (44, 'CARP')], help_text='The functional role of this IP', null=True, verbose_name='Role'),
|
||||
),
|
||||
]
|
||||
@@ -59,7 +59,6 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
serializer = 'ipam.api.serializers.VRFSerializer'
|
||||
csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||
|
||||
class Meta:
|
||||
@@ -108,7 +107,6 @@ class RIR(ChangeLoggedModel):
|
||||
help_text='IP space managed by this RIR is considered private'
|
||||
)
|
||||
|
||||
serializer = 'ipam.api.serializers.RIRSerializer'
|
||||
csv_headers = ['name', 'slug', 'is_private']
|
||||
|
||||
class Meta:
|
||||
@@ -162,7 +160,6 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
serializer = 'ipam.api.serializers.AggregateSerializer'
|
||||
csv_headers = ['prefix', 'rir', 'date_added', 'description']
|
||||
|
||||
class Meta:
|
||||
@@ -243,7 +240,6 @@ class Role(ChangeLoggedModel):
|
||||
default=1000
|
||||
)
|
||||
|
||||
serializer = 'ipam.api.serializers.RoleSerializer'
|
||||
csv_headers = ['name', 'slug', 'weight']
|
||||
|
||||
class Meta:
|
||||
@@ -336,7 +332,6 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
|
||||
objects = PrefixQuerySet.as_manager()
|
||||
tags = TaggableManager()
|
||||
|
||||
serializer = 'ipam.api.serializers.PrefixSerializer'
|
||||
csv_headers = [
|
||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
|
||||
]
|
||||
@@ -577,7 +572,6 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
objects = IPAddressManager()
|
||||
tags = TaggableManager()
|
||||
|
||||
serializer = 'ipam.api.serializers.IPAddressSerializer'
|
||||
csv_headers = [
|
||||
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary',
|
||||
'description',
|
||||
@@ -677,7 +671,6 @@ class VLANGroup(ChangeLoggedModel):
|
||||
null=True
|
||||
)
|
||||
|
||||
serializer = 'ipam.api.serializers.VLANGroupSerializer'
|
||||
csv_headers = ['name', 'slug', 'site']
|
||||
|
||||
class Meta:
|
||||
@@ -693,7 +686,7 @@ class VLANGroup(ChangeLoggedModel):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
|
||||
return reverse('ipam:vlangroup_vlans', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@@ -775,7 +768,6 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
serializer = 'ipam.api.serializers.VLANSerializer'
|
||||
csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||
|
||||
class Meta:
|
||||
@@ -879,7 +871,6 @@ class Service(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
serializer = 'ipam.api.serializers.ServiceSerializer'
|
||||
csv_headers = ['device', 'virtual_machine', 'name', 'protocol', 'description']
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -116,6 +116,16 @@ STATUS_LABEL = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
VLAN_LINK = """
|
||||
{% if record.pk %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.vid }}</a>
|
||||
{% elif perms.ipam.add_vlan %}
|
||||
<a href="{% url 'ipam:vlan_add' %}?vid={{ record.vid }}&group={{ vlan_group.pk }}{% if vlan_group.site %}&site={{ vlan_group.site.pk }}{% endif %}" class="btn btn-xs btn-success">{{ record.available }} VLAN{{ record.available|pluralize }} available</a>
|
||||
{% else %}
|
||||
{{ record.available }} VLAN{{ record.available|pluralize }} available
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
VLAN_PREFIXES = """
|
||||
{% for prefix in record.prefixes.all %}
|
||||
<a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a>{% if not forloop.last %}<br />{% endif %}
|
||||
@@ -203,17 +213,35 @@ class RIRTable(BaseTable):
|
||||
|
||||
|
||||
class RIRDetailTable(RIRTable):
|
||||
stats_total = tables.Column(accessor='stats.total', verbose_name='Total',
|
||||
footer=lambda table: sum(r.stats['total'] for r in table.data))
|
||||
stats_active = tables.Column(accessor='stats.active', verbose_name='Active',
|
||||
footer=lambda table: sum(r.stats['active'] for r in table.data))
|
||||
stats_reserved = tables.Column(accessor='stats.reserved', verbose_name='Reserved',
|
||||
footer=lambda table: sum(r.stats['reserved'] for r in table.data))
|
||||
stats_deprecated = tables.Column(accessor='stats.deprecated', verbose_name='Deprecated',
|
||||
footer=lambda table: sum(r.stats['deprecated'] for r in table.data))
|
||||
stats_available = tables.Column(accessor='stats.available', verbose_name='Available',
|
||||
footer=lambda table: sum(r.stats['available'] for r in table.data))
|
||||
utilization = tables.TemplateColumn(template_code=RIR_UTILIZATION, verbose_name='Utilization')
|
||||
stats_total = tables.Column(
|
||||
accessor='stats.total',
|
||||
verbose_name='Total',
|
||||
footer=lambda table: sum(r.stats['total'] for r in table.data)
|
||||
)
|
||||
stats_active = tables.Column(
|
||||
accessor='stats.active',
|
||||
verbose_name='Active',
|
||||
footer=lambda table: sum(r.stats['active'] for r in table.data)
|
||||
)
|
||||
stats_reserved = tables.Column(
|
||||
accessor='stats.reserved',
|
||||
verbose_name='Reserved',
|
||||
footer=lambda table: sum(r.stats['reserved'] for r in table.data)
|
||||
)
|
||||
stats_deprecated = tables.Column(
|
||||
accessor='stats.deprecated',
|
||||
verbose_name='Deprecated',
|
||||
footer=lambda table: sum(r.stats['deprecated'] for r in table.data)
|
||||
)
|
||||
stats_available = tables.Column(
|
||||
accessor='stats.available',
|
||||
verbose_name='Available',
|
||||
footer=lambda table: sum(r.stats['available'] for r in table.data)
|
||||
)
|
||||
utilization = tables.TemplateColumn(
|
||||
template_code=RIR_UTILIZATION,
|
||||
verbose_name='Utilization'
|
||||
)
|
||||
|
||||
class Meta(RIRTable.Meta):
|
||||
fields = (
|
||||
@@ -380,9 +408,9 @@ class VLANGroupTable(BaseTable):
|
||||
|
||||
class VLANTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
|
||||
vid = tables.TemplateColumn(VLAN_LINK, verbose_name='ID')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
group = tables.LinkColumn('ipam:vlangroup_vlans', args=[Accessor('group.pk')], verbose_name='Group')
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
role = tables.TemplateColumn(VLAN_ROLE_LINK)
|
||||
@@ -390,6 +418,9 @@ class VLANTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VLAN
|
||||
fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description')
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not isinstance(record, VLAN) else '',
|
||||
}
|
||||
|
||||
|
||||
class VLANDetailTable(VLANTable):
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
from netaddr import IPNetwork
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from ipam.constants import IP_PROTOCOL_TCP, IP_PROTOCOL_UDP
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from users.models import Token
|
||||
from utilities.testing import HttpStatusMixin
|
||||
from utilities.testing import APITestCase
|
||||
|
||||
|
||||
class VRFTest(HttpStatusMixin, APITestCase):
|
||||
class VRFTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(VRFTest, self).setUp()
|
||||
|
||||
self.vrf1 = VRF.objects.create(name='Test VRF 1', rd='65000:1')
|
||||
self.vrf2 = VRF.objects.create(name='Test VRF 2', rd='65000:2')
|
||||
@@ -106,13 +101,11 @@ class VRFTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(VRF.objects.count(), 2)
|
||||
|
||||
|
||||
class RIRTest(HttpStatusMixin, APITestCase):
|
||||
class RIRTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(RIRTest, self).setUp()
|
||||
|
||||
self.rir1 = RIR.objects.create(name='Test RIR 1', slug='test-rir-1')
|
||||
self.rir2 = RIR.objects.create(name='Test RIR 2', slug='test-rir-2')
|
||||
@@ -199,13 +192,11 @@ class RIRTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(RIR.objects.count(), 2)
|
||||
|
||||
|
||||
class AggregateTest(HttpStatusMixin, APITestCase):
|
||||
class AggregateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(AggregateTest, self).setUp()
|
||||
|
||||
self.rir1 = RIR.objects.create(name='Test RIR 1', slug='test-rir-1')
|
||||
self.rir2 = RIR.objects.create(name='Test RIR 2', slug='test-rir-2')
|
||||
@@ -294,13 +285,11 @@ class AggregateTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(Aggregate.objects.count(), 2)
|
||||
|
||||
|
||||
class RoleTest(HttpStatusMixin, APITestCase):
|
||||
class RoleTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(RoleTest, self).setUp()
|
||||
|
||||
self.role1 = Role.objects.create(name='Test Role 1', slug='test-role-1')
|
||||
self.role2 = Role.objects.create(name='Test Role 2', slug='test-role-2')
|
||||
@@ -387,13 +376,11 @@ class RoleTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(Role.objects.count(), 2)
|
||||
|
||||
|
||||
class PrefixTest(HttpStatusMixin, APITestCase):
|
||||
class PrefixTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(PrefixTest, self).setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.vrf1 = VRF.objects.create(name='Test VRF 1', rd='65000:1')
|
||||
@@ -507,7 +494,8 @@ class PrefixTest(HttpStatusMixin, APITestCase):
|
||||
|
||||
def test_create_single_available_prefix(self):
|
||||
|
||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), is_pool=True)
|
||||
vrf = VRF.objects.create(name='Test VRF 1', rd='1234')
|
||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), vrf=vrf, is_pool=True)
|
||||
url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk})
|
||||
|
||||
# Create four available prefixes with individual requests
|
||||
@@ -525,6 +513,7 @@ class PrefixTest(HttpStatusMixin, APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data['prefix'], prefixes_to_be_created[i])
|
||||
self.assertEqual(response.data['vrf']['id'], vrf.pk)
|
||||
self.assertEqual(response.data['description'], data['description'])
|
||||
|
||||
# Try to create one more prefix
|
||||
@@ -575,7 +564,8 @@ class PrefixTest(HttpStatusMixin, APITestCase):
|
||||
|
||||
def test_create_single_available_ip(self):
|
||||
|
||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/30'), is_pool=True)
|
||||
vrf = VRF.objects.create(name='Test VRF 1', rd='1234')
|
||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/30'), vrf=vrf, is_pool=True)
|
||||
url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
|
||||
|
||||
# Create all four available IPs with individual requests
|
||||
@@ -585,6 +575,7 @@ class PrefixTest(HttpStatusMixin, APITestCase):
|
||||
}
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data['vrf']['id'], vrf.pk)
|
||||
self.assertEqual(response.data['description'], data['description'])
|
||||
|
||||
# Try to create one more IP
|
||||
@@ -614,13 +605,11 @@ class PrefixTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(len(response.data), 8)
|
||||
|
||||
|
||||
class IPAddressTest(HttpStatusMixin, APITestCase):
|
||||
class IPAddressTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(IPAddressTest, self).setUp()
|
||||
|
||||
self.vrf1 = VRF.objects.create(name='Test VRF 1', rd='65000:1')
|
||||
self.ipaddress1 = IPAddress.objects.create(address=IPNetwork('192.168.0.1/24'))
|
||||
@@ -705,13 +694,11 @@ class IPAddressTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(IPAddress.objects.count(), 2)
|
||||
|
||||
|
||||
class VLANGroupTest(HttpStatusMixin, APITestCase):
|
||||
class VLANGroupTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(VLANGroupTest, self).setUp()
|
||||
|
||||
self.vlangroup1 = VLANGroup.objects.create(name='Test VLAN Group 1', slug='test-vlan-group-1')
|
||||
self.vlangroup2 = VLANGroup.objects.create(name='Test VLAN Group 2', slug='test-vlan-group-2')
|
||||
@@ -798,13 +785,11 @@ class VLANGroupTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(VLANGroup.objects.count(), 2)
|
||||
|
||||
|
||||
class VLANTest(HttpStatusMixin, APITestCase):
|
||||
class VLANTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(VLANTest, self).setUp()
|
||||
|
||||
self.vlan1 = VLAN.objects.create(vid=1, name='Test VLAN 1')
|
||||
self.vlan2 = VLAN.objects.create(vid=2, name='Test VLAN 2')
|
||||
@@ -891,13 +876,11 @@ class VLANTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(VLAN.objects.count(), 2)
|
||||
|
||||
|
||||
class ServiceTest(HttpStatusMixin, APITestCase):
|
||||
class ServiceTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
super(ServiceTest, self).setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
|
||||
@@ -80,6 +80,7 @@ urlpatterns = [
|
||||
url(r'^vlan-groups/import/$', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
|
||||
url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
|
||||
url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
|
||||
url(r'^vlan-groups/(?P<pk>\d+)/vlans/$', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'),
|
||||
url(r'^vlan-groups/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),
|
||||
|
||||
# VLANs
|
||||
|
||||
@@ -83,6 +83,34 @@ def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False):
|
||||
return output
|
||||
|
||||
|
||||
def add_available_vlans(vlan_group, vlans):
|
||||
"""
|
||||
Create fake records for all gaps between used VLANs
|
||||
"""
|
||||
MIN_VLAN = 1
|
||||
MAX_VLAN = 4094
|
||||
|
||||
if not vlans:
|
||||
return [{'vid': MIN_VLAN, 'available': MAX_VLAN - MIN_VLAN + 1}]
|
||||
|
||||
prev_vid = MAX_VLAN
|
||||
new_vlans = []
|
||||
for vlan in vlans:
|
||||
if vlan.vid - prev_vid > 1:
|
||||
new_vlans.append({'vid': prev_vid + 1, 'available': vlan.vid - prev_vid - 1})
|
||||
prev_vid = vlan.vid
|
||||
|
||||
if vlans[0].vid > MIN_VLAN:
|
||||
new_vlans.append({'vid': MIN_VLAN, 'available': vlans[0].vid - MIN_VLAN})
|
||||
if prev_vid < MAX_VLAN:
|
||||
new_vlans.append({'vid': prev_vid + 1, 'available': MAX_VLAN - prev_vid})
|
||||
|
||||
vlans = list(vlans) + new_vlans
|
||||
vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid'])
|
||||
|
||||
return vlans
|
||||
|
||||
|
||||
#
|
||||
# VRFs
|
||||
#
|
||||
@@ -138,7 +166,6 @@ class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_vrf'
|
||||
cls = VRF
|
||||
queryset = VRF.objects.select_related('tenant')
|
||||
filter = filters.VRFFilter
|
||||
table = tables.VRFTable
|
||||
@@ -148,7 +175,6 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_vrf'
|
||||
cls = VRF
|
||||
queryset = VRF.objects.select_related('tenant')
|
||||
filter = filters.VRFFilter
|
||||
table = tables.VRFTable
|
||||
@@ -191,9 +217,15 @@ class RIRListView(ObjectListView):
|
||||
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))
|
||||
|
||||
# Find all consumed space for each prefix status (we ignore containers for this purpose).
|
||||
active_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_ACTIVE)])
|
||||
reserved_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_RESERVED)])
|
||||
deprecated_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_DEPRECATED)])
|
||||
active_prefixes = netaddr.cidr_merge(
|
||||
[p.prefix for p in queryset.filter(status=PREFIX_STATUS_ACTIVE)]
|
||||
)
|
||||
reserved_prefixes = netaddr.cidr_merge(
|
||||
[p.prefix for p in queryset.filter(status=PREFIX_STATUS_RESERVED)]
|
||||
)
|
||||
deprecated_prefixes = netaddr.cidr_merge(
|
||||
[p.prefix for p in queryset.filter(status=PREFIX_STATUS_DEPRECATED)]
|
||||
)
|
||||
|
||||
# Find all available prefixes by subtracting each of the existing prefix sets from the aggregate prefix.
|
||||
available_prefixes = (
|
||||
@@ -204,11 +236,11 @@ class RIRListView(ObjectListView):
|
||||
)
|
||||
|
||||
# Add the size of each metric to the RIR total.
|
||||
stats['total'] += aggregate.prefix.size / denominator
|
||||
stats['active'] += netaddr.IPSet(active_prefixes).size / denominator
|
||||
stats['reserved'] += netaddr.IPSet(reserved_prefixes).size / denominator
|
||||
stats['deprecated'] += netaddr.IPSet(deprecated_prefixes).size / denominator
|
||||
stats['available'] += available_prefixes.size / denominator
|
||||
stats['total'] += int(aggregate.prefix.size / denominator)
|
||||
stats['active'] += int(netaddr.IPSet(active_prefixes).size / denominator)
|
||||
stats['reserved'] += int(netaddr.IPSet(reserved_prefixes).size / denominator)
|
||||
stats['deprecated'] += int(netaddr.IPSet(deprecated_prefixes).size / denominator)
|
||||
stats['available'] += int(available_prefixes.size / denominator)
|
||||
|
||||
# Calculate the percentage of total space for each prefix status.
|
||||
total = float(stats['total'])
|
||||
@@ -228,20 +260,6 @@ class RIRListView(ObjectListView):
|
||||
|
||||
return rirs
|
||||
|
||||
def extra_context(self):
|
||||
|
||||
totals = {
|
||||
'total': sum([rir.stats['total'] for rir in self.queryset]),
|
||||
'active': sum([rir.stats['active'] for rir in self.queryset]),
|
||||
'reserved': sum([rir.stats['reserved'] for rir in self.queryset]),
|
||||
'deprecated': sum([rir.stats['deprecated'] for rir in self.queryset]),
|
||||
'available': sum([rir.stats['available'] for rir in self.queryset]),
|
||||
}
|
||||
|
||||
return {
|
||||
'totals': totals,
|
||||
}
|
||||
|
||||
|
||||
class RIRCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.add_rir'
|
||||
@@ -263,7 +281,6 @@ class RIRBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_rir'
|
||||
cls = RIR
|
||||
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
|
||||
filter = filters.RIRFilter
|
||||
table = tables.RIRTable
|
||||
@@ -367,7 +384,6 @@ class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_aggregate'
|
||||
cls = Aggregate
|
||||
queryset = Aggregate.objects.select_related('rir')
|
||||
filter = filters.AggregateFilter
|
||||
table = tables.AggregateTable
|
||||
@@ -377,7 +393,6 @@ class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_aggregate'
|
||||
cls = Aggregate
|
||||
queryset = Aggregate.objects.select_related('rir')
|
||||
filter = filters.AggregateFilter
|
||||
table = tables.AggregateTable
|
||||
@@ -414,7 +429,7 @@ class RoleBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_role'
|
||||
cls = Role
|
||||
queryset = Role.objects.all()
|
||||
table = tables.RoleTable
|
||||
default_return_url = 'ipam:role_list'
|
||||
|
||||
@@ -588,7 +603,6 @@ class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_prefix'
|
||||
cls = Prefix
|
||||
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||
filter = filters.PrefixFilter
|
||||
table = tables.PrefixTable
|
||||
@@ -598,7 +612,6 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_prefix'
|
||||
cls = Prefix
|
||||
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||
filter = filters.PrefixFilter
|
||||
table = tables.PrefixTable
|
||||
@@ -761,7 +774,6 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_ipaddress'
|
||||
cls = IPAddress
|
||||
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
|
||||
filter = filters.IPAddressFilter
|
||||
table = tables.IPAddressTable
|
||||
@@ -771,7 +783,6 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_ipaddress'
|
||||
cls = IPAddress
|
||||
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
|
||||
filter = filters.IPAddressFilter
|
||||
table = tables.IPAddressTable
|
||||
@@ -810,13 +821,47 @@ class VLANGroupBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_vlangroup'
|
||||
cls = VLANGroup
|
||||
queryset = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
|
||||
filter = filters.VLANGroupFilter
|
||||
table = tables.VLANGroupTable
|
||||
default_return_url = 'ipam:vlangroup_list'
|
||||
|
||||
|
||||
class VLANGroupVLANsView(View):
|
||||
def get(self, request, pk):
|
||||
|
||||
vlan_group = get_object_or_404(VLANGroup.objects.all(), pk=pk)
|
||||
|
||||
vlans = VLAN.objects.filter(group_id=pk)
|
||||
vlans = add_available_vlans(vlan_group, vlans)
|
||||
|
||||
vlan_table = tables.VLANDetailTable(vlans)
|
||||
if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'):
|
||||
vlan_table.columns.show('pk')
|
||||
vlan_table.columns.hide('site')
|
||||
vlan_table.columns.hide('group')
|
||||
|
||||
paginate = {
|
||||
'klass': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(vlan_table)
|
||||
|
||||
# Compile permissions list for rendering the object table
|
||||
permissions = {
|
||||
'add': request.user.has_perm('ipam.add_vlan'),
|
||||
'change': request.user.has_perm('ipam.change_vlan'),
|
||||
'delete': request.user.has_perm('ipam.delete_vlan'),
|
||||
}
|
||||
|
||||
return render(request, 'ipam/vlangroup_vlans.html', {
|
||||
'vlan_group': vlan_group,
|
||||
'first_available_vlan': vlan_group.get_next_available_vid(),
|
||||
'vlan_table': vlan_table,
|
||||
'permissions': permissions,
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
# VLANs
|
||||
#
|
||||
@@ -895,7 +940,6 @@ class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_vlan'
|
||||
cls = VLAN
|
||||
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
|
||||
filter = filters.VLANFilter
|
||||
table = tables.VLANTable
|
||||
@@ -905,7 +949,6 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_vlan'
|
||||
cls = VLAN
|
||||
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
|
||||
filter = filters.VLANFilter
|
||||
table = tables.VLANTable
|
||||
@@ -960,8 +1003,7 @@ class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class ServiceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_service'
|
||||
cls = Service
|
||||
queryset = Service.objects.all()
|
||||
queryset = Service.objects.select_related('device', 'virtual_machine')
|
||||
filter = filters.ServiceFilter
|
||||
table = tables.ServiceTable
|
||||
form = forms.ServiceBulkEditForm
|
||||
@@ -970,8 +1012,7 @@ class ServiceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
class ServiceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_service'
|
||||
cls = Service
|
||||
queryset = Service.objects.all()
|
||||
queryset = Service.objects.select_related('device', 'virtual_machine')
|
||||
filter = filters.ServiceFilter
|
||||
table = tables.ServiceTable
|
||||
default_return_url = 'ipam:service_list'
|
||||
|
||||
30
netbox/netbox/admin.py
Normal file
30
netbox/netbox/admin.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.admin import AdminSite
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.auth.admin import GroupAdmin, UserAdmin
|
||||
from taggit.admin import TagAdmin
|
||||
from taggit.models import Tag
|
||||
|
||||
|
||||
class NetBoxAdminSite(AdminSite):
|
||||
"""
|
||||
Custom admin site
|
||||
"""
|
||||
site_header = 'NetBox Administration'
|
||||
site_title = 'NetBox'
|
||||
site_url = '/{}'.format(settings.BASE_PATH)
|
||||
|
||||
|
||||
admin_site = NetBoxAdminSite(name='admin')
|
||||
|
||||
# Register external models
|
||||
admin_site.register(Group, GroupAdmin)
|
||||
admin_site.register(User, UserAdmin)
|
||||
admin_site.register(Tag, TagAdmin)
|
||||
|
||||
# Modify the template to include an RQ link if django_rq is installed (see RQ_SHOW_ADMIN_LINK)
|
||||
try:
|
||||
import django_rq
|
||||
admin_site.index_template = 'django_rq/index.html'
|
||||
except ImportError:
|
||||
pass
|
||||
@@ -13,6 +13,7 @@ OBJ_TYPE_CHOICES = (
|
||||
('DCIM', (
|
||||
('site', 'Sites'),
|
||||
('rack', 'Racks'),
|
||||
('rackgroup', 'Rack Groups'),
|
||||
('devicetype', 'Device types'),
|
||||
('device', 'Devices'),
|
||||
('virtualchassis', 'Virtual Chassis'),
|
||||
|
||||
@@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
VERSION = '2.4-beta1'
|
||||
VERSION = '2.4.4'
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
@@ -144,6 +144,7 @@ INSTALLED_APPS = [
|
||||
'mptt',
|
||||
'rest_framework',
|
||||
'taggit',
|
||||
'taggit_serializer',
|
||||
'timezone_field',
|
||||
'circuits',
|
||||
'dcim',
|
||||
@@ -175,7 +176,7 @@ MIDDLEWARE = (
|
||||
'utilities.middleware.ExceptionHandlingMiddleware',
|
||||
'utilities.middleware.LoginRequiredMiddleware',
|
||||
'utilities.middleware.APIVersionMiddleware',
|
||||
'extras.middleware.ChangeLoggingMiddleware',
|
||||
'extras.middleware.ObjectChangeMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'netbox.urls'
|
||||
@@ -271,7 +272,6 @@ RQ_QUEUES = {
|
||||
'DEFAULT_TIMEOUT': REDIS_DEFAULT_TIMEOUT,
|
||||
}
|
||||
}
|
||||
RQ_SHOW_ADMIN_LINK = True
|
||||
|
||||
# drf_yasg settings for Swagger
|
||||
SWAGGER_SETTINGS = {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user