mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-29 16:47:46 -06:00
Compare commits
193 Commits
v3.2-beta2
...
v3.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cd9bcd3f5 | ||
|
|
fdc018d809 | ||
|
|
fa5cf665ce | ||
|
|
d6df6b444f | ||
|
|
78836389f0 | ||
|
|
fa4b88a504 | ||
|
|
1a374a1669 | ||
|
|
01ba1b8c03 | ||
|
|
f09a5aacae | ||
|
|
95d084d36d | ||
|
|
d35cd18745 | ||
|
|
4e493d7836 | ||
|
|
68b8cca540 | ||
|
|
c216405a81 | ||
|
|
aa2ec3b9c9 | ||
|
|
f13a00b2dd | ||
|
|
8781d03aa7 | ||
|
|
1266a2f753 | ||
|
|
23d2cf1718 | ||
|
|
d11031c694 | ||
|
|
916e976297 | ||
|
|
27a9313396 | ||
|
|
517d0158b6 | ||
|
|
a9e05aec7c | ||
|
|
9b3e43cb21 | ||
|
|
23fddf74b6 | ||
|
|
9b8de19fe6 | ||
|
|
1d8b8aad3b | ||
|
|
84c30580aa | ||
|
|
a5f25726cd | ||
|
|
7a6e047519 | ||
|
|
1e65ef0c1a | ||
|
|
2269bf0167 | ||
|
|
5526f8e3dc | ||
|
|
2781b8535c | ||
|
|
c3d9910e08 | ||
|
|
b9f6a5625f | ||
|
|
f4e78b0ea6 | ||
|
|
d93e944c07 | ||
|
|
6760533a10 | ||
|
|
85e65edb7d | ||
|
|
523390cd8e | ||
|
|
ea197eff5f | ||
|
|
d4938b7699 | ||
|
|
6ee6227b67 | ||
|
|
6f0ae8a5b8 | ||
|
|
c1b7f09530 | ||
|
|
9addde00e3 | ||
|
|
10f8a94399 | ||
|
|
631de20a8d | ||
|
|
66464fd807 | ||
|
|
0f5fe746e0 | ||
|
|
a7fc8621a8 | ||
|
|
8b92bc6c4a | ||
|
|
830b56ac9e | ||
|
|
fcfdbfc2b5 | ||
|
|
e575279738 | ||
|
|
796f7258cc | ||
|
|
ea88b040ec | ||
|
|
69b4d0d44b | ||
|
|
36d6ae33d1 | ||
|
|
8126087b3e | ||
|
|
780459d2bf | ||
|
|
e2b6d69596 | ||
|
|
7fff1e6fe5 | ||
|
|
b576ce72a1 | ||
|
|
99a01207bc | ||
|
|
6d6457ad18 | ||
|
|
35f3a42e7f | ||
|
|
a84ae88214 | ||
|
|
4fae42de51 | ||
|
|
1d55c04c21 | ||
|
|
7a54658710 | ||
|
|
bddc35bbc7 | ||
|
|
cdacd2a951 | ||
|
|
58e4d08bb0 | ||
|
|
3ff4fd814e | ||
|
|
91e8f57afb | ||
|
|
e3d0628a06 | ||
|
|
9fca9ca7ec | ||
|
|
2d09a40663 | ||
|
|
1eaf55c555 | ||
|
|
db535e6453 | ||
|
|
dadec9d3cb | ||
|
|
ff780177d0 | ||
|
|
b7e2ea1ca5 | ||
|
|
bf50134d94 | ||
|
|
227bac7c60 | ||
|
|
340ff82487 | ||
|
|
894665b067 | ||
|
|
48b7294ff1 | ||
|
|
cde8ff282d | ||
|
|
0b44a595e2 | ||
|
|
37781bd208 | ||
|
|
e0344e9251 | ||
|
|
a1808a54a4 | ||
|
|
1cef513f6c | ||
|
|
22908a12e9 | ||
|
|
57759aa4a3 | ||
|
|
d50148fab7 | ||
|
|
271c2ea3e3 | ||
|
|
20a6f6ac79 | ||
|
|
8924d5fa05 | ||
|
|
26637d934b | ||
|
|
dde4495e20 | ||
|
|
1278429518 | ||
|
|
421f5a03aa | ||
|
|
a433d5d59d | ||
|
|
934493bf5f | ||
|
|
58f97bc0e7 | ||
|
|
a5820e27a6 | ||
|
|
d312fe7c2b | ||
|
|
124fc73386 | ||
|
|
c78e7c14d3 | ||
|
|
30a6dc2f64 | ||
|
|
6ceb78fd4c | ||
|
|
e09ab79a1a | ||
|
|
b6587c00ce | ||
|
|
df2f6d4a7d | ||
|
|
3b69f07b86 | ||
|
|
fdc0036872 | ||
|
|
fab90ecbe5 | ||
|
|
f72d160249 | ||
|
|
b65404a8a9 | ||
|
|
42446cb87b | ||
|
|
f45e64c756 | ||
|
|
ae46cd33b6 | ||
|
|
61eb22f4f7 | ||
|
|
7c14b8d97b | ||
|
|
8d682041a4 | ||
|
|
75dae5fbe8 | ||
|
|
dc92e19f76 | ||
|
|
ce87e0dfa4 | ||
|
|
41efad4056 | ||
|
|
5f89226cd7 | ||
|
|
197dfca5b2 | ||
|
|
e6980626d8 | ||
|
|
ca44a654a5 | ||
|
|
48dc76a694 | ||
|
|
0c5eab680b | ||
|
|
3dc671395e | ||
|
|
ba1e6e91b9 | ||
|
|
22980cea7b | ||
|
|
ed0c19807a | ||
|
|
15005209d1 | ||
|
|
f64987d0c4 | ||
|
|
0da04232f3 | ||
|
|
8d53b46e82 | ||
|
|
9a0bb14e76 | ||
|
|
900825a2af | ||
|
|
52de50aa64 | ||
|
|
1541060091 | ||
|
|
50bc0caccf | ||
|
|
8f5b14ec84 | ||
|
|
da37db1ea9 | ||
|
|
5abde866f1 | ||
|
|
32eed72d2b | ||
|
|
585b5a221d | ||
|
|
db52fe475a | ||
|
|
57ad730f74 | ||
|
|
c5db99f383 | ||
|
|
fd6d3205d0 | ||
|
|
9548cf32ff | ||
|
|
bdbfff911b | ||
|
|
a143eca57d | ||
|
|
2101f714cc | ||
|
|
3edff89a4d | ||
|
|
f2079de2cb | ||
|
|
93527d892b | ||
|
|
b91218308b | ||
|
|
6170138124 | ||
|
|
1add5accf2 | ||
|
|
faba6c9bdc | ||
|
|
4eb7cd06b4 | ||
|
|
245cff887c | ||
|
|
59aba52b03 | ||
|
|
6d05a4117a | ||
|
|
49e5268d48 | ||
|
|
76445bd19c | ||
|
|
5a3e99626d | ||
|
|
ffc29d14a8 | ||
|
|
342f1d31be | ||
|
|
b779bbfc9d | ||
|
|
ef6576bdd6 | ||
|
|
27dab262de | ||
|
|
412c1df15a | ||
|
|
73af3ba095 | ||
|
|
21b7564976 | ||
|
|
bf22b820bf | ||
|
|
8cd24b1a67 | ||
|
|
a3f172fc77 | ||
|
|
36d6dd1ca9 | ||
|
|
538984c6d2 |
4
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.1.9
|
||||
placeholder: v3.2.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -22,9 +22,9 @@ body:
|
||||
label: Python version
|
||||
description: What version of Python are you currently running?
|
||||
options:
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.1.9
|
||||
placeholder: v3.2.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pycodestyle coverage
|
||||
pip install pycodestyle coverage tblib
|
||||
|
||||
- name: Build documentation
|
||||
run: mkdocs build
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
||||
</div>
|
||||
|
||||
:loudspeaker: The **[2022 NetBox community survey](https://forms.gle/KR8YbR8GiJ9EYXM28)** is now open! We collect this feedback and demographic data from NetBox users around the world to help shape the project's long-term development goals. Please take a few minutes to share your responses!
|
||||
|
||||

|
||||
|
||||
NetBox is an infrastructure resource modeling (IRM) tool designed to empower
|
||||
|
||||
@@ -68,7 +68,8 @@ gunicorn
|
||||
|
||||
# Platform-agnostic template rendering engine
|
||||
# https://github.com/pallets/jinja
|
||||
Jinja2
|
||||
# Pin to v3.0 for mkdocstrings
|
||||
Jinja2<3.1
|
||||
|
||||
# Simple markup language for rendering HTML
|
||||
# https://github.com/Python-Markdown/markdown
|
||||
@@ -84,10 +85,10 @@ mkdocs-material
|
||||
|
||||
# Introspection for embedded code
|
||||
# https://github.com/mkdocstrings/mkdocstrings
|
||||
mkdocstrings
|
||||
mkdocstrings<=0.17.0
|
||||
|
||||
# Library for manipulating IP prefixes and addresses
|
||||
# https://github.com/drkjam/netaddr
|
||||
# https://github.com/netaddr/netaddr
|
||||
netaddr
|
||||
|
||||
# Fork of PIL (Python Imaging Library) for image processing
|
||||
|
||||
@@ -4,6 +4,7 @@ NetBox includes a `housekeeping` management command that should be run nightly.
|
||||
|
||||
* Clearing expired authentication sessions from the database
|
||||
* Deleting changelog records older than the configured [retention time](../configuration/dynamic-settings.md#changelog_retention)
|
||||
* Deleting job result records older than the configured [retention time](../configuration/dynamic-settings.md#jobresult_retention)
|
||||
|
||||
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
|
||||
|
||||
|
||||
@@ -43,6 +43,18 @@ changes in the database indefinitely.
|
||||
|
||||
---
|
||||
|
||||
## JOBRESULT_RETENTION
|
||||
|
||||
Default: 90
|
||||
|
||||
The number of days to retain job results (scripts and reports). Set this to `0` to retain
|
||||
job results in the database indefinitely.
|
||||
|
||||
!!! warning
|
||||
If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
|
||||
|
||||
---
|
||||
|
||||
## CUSTOM_VALIDATORS
|
||||
|
||||
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:
|
||||
|
||||
@@ -207,6 +207,7 @@ The following model fields support configurable choices:
|
||||
* `dcim.PowerFeed.status`
|
||||
* `dcim.Rack.status`
|
||||
* `dcim.Site.status`
|
||||
* `extras.JournalEntry.kind`
|
||||
* `ipam.IPAddress.status`
|
||||
* `ipam.IPRange.status`
|
||||
* `ipam.Prefix.status`
|
||||
|
||||
@@ -43,7 +43,7 @@ A mapping of permissions to assign a new user account when created using remote
|
||||
|
||||
Default: `False`
|
||||
|
||||
NetBox can be configured to support remote user authentication by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.) (`REMOTE_AUTH_DEFAULT_GROUPS` will not function if `REMOTE_AUTH_ENABLED` is enabled)
|
||||
NetBox can be configured to support remote user authentication by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.) (`REMOTE_AUTH_DEFAULT_GROUPS` will not function if `REMOTE_AUTH_ENABLED` is disabled)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
---
|
||||
|
||||
{!models/ipam/fhrpgroup.md!}
|
||||
{!models/ipam/fhrpgroupassignment.md!}
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -89,6 +89,12 @@ The checkbox to commit database changes when executing a script is checked by de
|
||||
commit_default = False
|
||||
```
|
||||
|
||||
### `job_timeout`
|
||||
|
||||
Set the maximum allowed runtime for the script. If not set, `RQ_DEFAULT_TIMEOUT` will be used.
|
||||
|
||||
!!! info "This feature was introduced in v3.2.1"
|
||||
|
||||
## Accessing Request Data
|
||||
|
||||
Details of the current HTTP request (the one being made to execute the script) are available as the instance attribute `self.request`. This can be used to infer, for example, the user executing the script and the client IP address:
|
||||
|
||||
@@ -85,6 +85,20 @@ As you can see, reports are completely customizable. Validation logic can be as
|
||||
!!! warning
|
||||
Reports should never alter data: If you find yourself using the `create()`, `save()`, `update()`, or `delete()` methods on objects within reports, stop and re-evaluate what you're trying to accomplish. Note that there are no safeguards against the accidental alteration or destruction of data.
|
||||
|
||||
## Report Attributes
|
||||
|
||||
### `description`
|
||||
|
||||
A human-friendly description of what your report does.
|
||||
|
||||
### `job_timeout`
|
||||
|
||||
Set the maximum allowed runtime for the report. If not set, `RQ_DEFAULT_TIMEOUT` will be used.
|
||||
|
||||
!!! info "This feature was introduced in v3.2.1"
|
||||
|
||||
## Logging
|
||||
|
||||
The following methods are available to log results within a report:
|
||||
|
||||
* log(message)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{style="height: 100px; margin-bottom: 3em"}
|
||||
|
||||
:loudspeaker: The **[2022 NetBox community survey](https://forms.gle/KR8YbR8GiJ9EYXM28)** is now open! We collect this feedback and demographic data from NetBox users around the world to help shape the project's long-term development goals. Please take a few minutes to share your responses!
|
||||
|
||||
# What is NetBox?
|
||||
|
||||
NetBox is an infrastructure resource modeling (IRM) application designed to empower network automation. 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. NetBox is made available as open source under the Apache 2 license. It encompasses the following aspects of network management:
|
||||
|
||||
@@ -6,7 +6,7 @@ Prior to upgrading your NetBox instance, be sure to carefully review all [releas
|
||||
|
||||
## Update Dependencies to Required Versions
|
||||
|
||||
NetBox v3.0 and later requires the following:
|
||||
NetBox v3.0 and later require the following:
|
||||
|
||||
| Dependency | Minimum Version |
|
||||
|------------|-----------------|
|
||||
@@ -67,6 +67,11 @@ sudo git checkout master
|
||||
sudo git pull origin master
|
||||
```
|
||||
|
||||
!!! info "Checking out an older release"
|
||||
If you need to upgrade to an older version rather than the current stable release, you can check out any valid [git tag](https://github.com/netbox-community/netbox/tags), each of which represents a release. For example, to checkout the code for NetBox v2.11.11, do:
|
||||
|
||||
sudo git checkout v2.11.11
|
||||
|
||||
## Run the Upgrade Script
|
||||
|
||||
Once the new code is in place, verify that any optional Python packages required by your deployment (e.g. `napalm` or `django-auth-ldap`) are listed in `local_requirements.txt`. Then, run the upgrade script:
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
A virtual chassis represents a set of devices which share a common control plane. A common example of this is a stack of switches which are connected and configured to operate as a single device. A virtual chassis must be assigned a name and may be assigned a domain.
|
||||
|
||||
Each device in the virtual chassis is referred to as a VC member, and assigned a position and (optionally) a priority. VC member devices commonly reside within the same rack, though this is not a requirement. One of the devices may be designated as the VC master: This device will typically be assigned a name, services, and other attributes related to managing the VC.
|
||||
Each device in the virtual chassis is referred to as a VC member, and assigned a position and (optionally) a priority. VC member devices commonly reside within the same rack, though this is not a requirement. One of the devices may be designated as the VC master: This device will typically be assigned a name, services, virtual interfaces, and other attributes related to managing the VC.
|
||||
If a VC master is defined, interfaces from all VC members are displayed when navigating to its device interfaces view. This does not include other members interfaces declared as management-only.
|
||||
|
||||
!!! note
|
||||
It's important to recognize the distinction between a virtual chassis and a chassis-based device. A virtual chassis is **not** suitable for modeling a chassis-based switch with removable line cards (such as the Juniper EX9208), as its line cards are _not_ physically autonomous devices.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a Network Monitoring System (NMS).
|
||||
|
||||
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the Netbox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `obj`, and custom fields through `obj.cf`.
|
||||
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the NetBox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `obj`, and custom fields through `obj.cf`.
|
||||
|
||||
For example, you might define a link like this:
|
||||
|
||||
@@ -33,6 +33,10 @@ The following context data is available within the template when rendering a cus
|
||||
| `user` | The current user (if authenticated) |
|
||||
| `perms` | The [permissions](https://docs.djangoproject.com/en/stable/topics/auth/default/#permissions) assigned to the user |
|
||||
|
||||
While most of the context variables listed above will have consistent attributes, the object will be an instance of the specific object being viewed when the link is rendered. Different models have different fields and properties, so you may need to some research to determine the attributes available for use within your template for a specific object type.
|
||||
|
||||
Checking the REST API representation of an object is generally a convenient way to determine what attributes are available. You can also reference the NetBox source code directly for a comprehensive list.
|
||||
|
||||
## Conditional Rendering
|
||||
|
||||
Only links which render with non-empty text are included on the page. You can employ conditional Jinja2 logic to control the conditions under which a link gets rendered.
|
||||
|
||||
@@ -8,9 +8,3 @@ A first-hop redundancy protocol (FHRP) enables multiple physical interfaces to p
|
||||
* Gateway Load Balancing Protocol (GLBP)
|
||||
|
||||
NetBox models these redundancy groups by protocol and group ID. Each group may optionally be assigned an authentication type and key. (Note that the authentication key is stored as a plaintext value in NetBox.) Each group may be assigned or more virtual IPv4 and/or IPv6 addresses.
|
||||
|
||||
## FHRP Group Assignments
|
||||
|
||||
Member device and VM interfaces can be assigned to FHRP groups, along with a numeric priority value. For instance, three interfaces, each belonging to a different router, may each be assigned to the same FHRP group to serve a common virtual IP address. Each of these assignments would typically receive a different priority.
|
||||
|
||||
Interfaces are assigned to FHRP groups under the interface detail view.
|
||||
|
||||
5
docs/models/ipam/fhrpgroupassignment.md
Normal file
5
docs/models/ipam/fhrpgroupassignment.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# FHRP Group Assignments
|
||||
|
||||
Member device and VM interfaces can be assigned to FHRP groups, along with a numeric priority value. For instance, three interfaces, each belonging to a different router, may each be assigned to the same FHRP group to serve a common virtual IP address. Each of these assignments would typically receive a different priority.
|
||||
|
||||
Interfaces are assigned to FHRP groups under the interface detail view.
|
||||
@@ -3,7 +3,7 @@
|
||||
A token is a unique identifier mapped to a NetBox user account. Each user may have one or more tokens which he or she can use for authentication when making REST API requests. To create a token, navigate to the API tokens page under your user profile.
|
||||
|
||||
!!! note
|
||||
The creation and modification of API tokens can be restricted per user by an administrator. If you don't see an option to create an API token, ask an administrator to grant you access.
|
||||
All users can create and manage REST API tokens under the user control panel in the UI. The ability to view, add, change, or delete tokens via the REST API itself is controlled by the relevant model permissions, assigned to users and/or groups in the admin UI. These permissions should be used with great care to avoid accidentally permitting a user to create tokens for other user accounts.
|
||||
|
||||
Each token contains a 160-bit key represented as 40 hexadecimal characters. When creating a token, you'll typically leave the key field blank so that a random key will be automatically generated. However, NetBox allows you to specify a key in case you need to restore a previously deleted token to operation.
|
||||
|
||||
|
||||
@@ -4,17 +4,145 @@
|
||||
|
||||
NetBox provides several base form classes for use by plugins.
|
||||
|
||||
* `NetBoxModelForm`
|
||||
* `NetBoxModelCSVForm`
|
||||
* `NetBoxModelBulkEditForm`
|
||||
* `NetBoxModelFilterSetForm`
|
||||
| Form Class | Purpose |
|
||||
|---------------------------|--------------------------------------|
|
||||
| `NetBoxModelForm` | Create/edit individual objects |
|
||||
| `NetBoxModelCSVForm` | Bulk import objects from CSV data |
|
||||
| `NetBoxModelBulkEditForm` | Edit multiple objects simultaneously |
|
||||
| `NetBoxModelFilterSetForm` | Filter objects within a list view |
|
||||
|
||||
<!-- TODO: Include forms reference -->
|
||||
### `NetBoxModelForm`
|
||||
|
||||
In addition to the [form fields provided by Django](https://docs.djangoproject.com/en/stable/ref/forms/fields/), NetBox provides several field classes for use within forms to handle specific types of data. These can be imported from `utilities.forms.fields` and are documented below.
|
||||
This is the base form for creating and editing NetBox models. It extends Django's ModelForm to add support for tags and custom fields.
|
||||
|
||||
| Attribute | Description |
|
||||
|-------------|-------------------------------------------------------------|
|
||||
| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) |
|
||||
|
||||
**Example**
|
||||
|
||||
```python
|
||||
from dcim.models import Site
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField
|
||||
from .models import MyModel
|
||||
|
||||
class MyModelForm(NetBoxModelForm):
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
fieldsets = (
|
||||
('Model Stuff', ('name', 'status', 'site', 'tags')),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = MyModel
|
||||
fields = ('name', 'status', 'site', 'comments', 'tags')
|
||||
```
|
||||
|
||||
!!! tip "Comment fields"
|
||||
If your form has a `comments` field, there's no need to list it; this will always appear last on the page.
|
||||
|
||||
### `NetBoxModelCSVForm`
|
||||
|
||||
This form facilitates the bulk import of new objects from CSV data. As with model forms, you'll need to declare a `Meta` subclass specifying the associated `model` and `fields`. NetBox also provides several form fields suitable for import various types of CSV data, listed below.
|
||||
|
||||
**Example**
|
||||
|
||||
```python
|
||||
from dcim.models import Site
|
||||
from netbox.forms import NetBoxModelCSVForm
|
||||
from utilities.forms import CSVModelChoiceField
|
||||
from .models import MyModel
|
||||
|
||||
class MyModelCSVForm(NetBoxModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned site'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = MyModel
|
||||
fields = ('name', 'status', 'site', 'comments')
|
||||
```
|
||||
|
||||
### `NetBoxModelBulkEditForm`
|
||||
|
||||
This form facilitates editing multiple objects in bulk. Unlike a model form, this form does not have a child `Meta` class, and must explicitly define each field. All fields in a bulk edit form are generally declared with `required=False`.
|
||||
|
||||
| Attribute | Description |
|
||||
|-------------------|---------------------------------------------------------------------------------------------|
|
||||
| `model` | The model of object being edited |
|
||||
| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) |
|
||||
| `nullable_fields` | A tuple of fields which can be nullified (set to empty) using the bulk edit form (optional) |
|
||||
|
||||
**Example**
|
||||
|
||||
```python
|
||||
from django import forms
|
||||
from dcim.models import Site
|
||||
from netbox.forms import NetBoxModelCSVForm
|
||||
from utilities.forms import CommentField, DynamicModelChoiceField
|
||||
from .models import MyModel, MyModelStatusChoices
|
||||
|
||||
class MyModelEditForm(NetBoxModelCSVForm):
|
||||
name = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
choices=MyModelStatusChoices,
|
||||
required=False
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
model = MyModel
|
||||
fieldsets = (
|
||||
('Model Stuff', ('name', 'status', 'site')),
|
||||
)
|
||||
nullable_fields = ('site', 'comments')
|
||||
```
|
||||
|
||||
### `NetBoxModelFilterSetForm`
|
||||
|
||||
This form class is used to render a form expressly for filtering a list of objects. Its fields should correspond to filters defined on the model's filter set.
|
||||
|
||||
| Attribute | Description |
|
||||
|-------------------|-------------------------------------------------------------|
|
||||
| `model` | The model of object being edited |
|
||||
| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) |
|
||||
|
||||
**Example**
|
||||
|
||||
```python
|
||||
from dcim.models import Site
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from utilities.forms import DynamicModelMultipleChoiceField, MultipleChoiceField
|
||||
from .models import MyModel, MyModelStatusChoices
|
||||
|
||||
class MyModelFilterForm(NetBoxModelFilterSetForm):
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False
|
||||
)
|
||||
status = MultipleChoiceField(
|
||||
choices=MyModelStatusChoices,
|
||||
required=False
|
||||
)
|
||||
|
||||
model = MyModel
|
||||
```
|
||||
|
||||
## General Purpose Fields
|
||||
|
||||
In addition to the [form fields provided by Django](https://docs.djangoproject.com/en/stable/ref/forms/fields/), NetBox provides several field classes for use within forms to handle specific types of data. These can be imported from `utilities.forms.fields` and are documented below.
|
||||
|
||||
::: utilities.forms.ColorField
|
||||
selection:
|
||||
members: false
|
||||
@@ -35,6 +163,16 @@ In addition to the [form fields provided by Django](https://docs.djangoproject.c
|
||||
selection:
|
||||
members: false
|
||||
|
||||
## Choice Fields
|
||||
|
||||
::: utilities.forms.ChoiceField
|
||||
selection:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.MultipleChoiceField
|
||||
selection:
|
||||
members: false
|
||||
|
||||
## Dynamic Object Fields
|
||||
|
||||
::: utilities.forms.DynamicModelChoiceField
|
||||
|
||||
@@ -9,10 +9,11 @@ A plugin can extend NetBox's GraphQL API by registering its own schema class. By
|
||||
```python
|
||||
# graphql.py
|
||||
import graphene
|
||||
from netbox.graphql.types import NetBoxObjectType
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from . import filtersets, models
|
||||
|
||||
class MyModelType(graphene.ObjectType):
|
||||
class MyModelType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.MyModel
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Plugins Development
|
||||
|
||||
!!! tip "Help Improve the NetBox Plugins Framework!"
|
||||
We're looking for volunteers to help improve NetBox's plugins framework. If you have experience developing plugins, we'd love to hear from you! You can find more information about this initiative [here](https://github.com/netbox-community/netbox/discussions/8338).
|
||||
!!! tip "Plugins Development Tutorial"
|
||||
Just getting started with plugins? Check out our [**NetBox Plugin Tutorial**](https://github.com/netbox-community/netbox-plugin-tutorial) on GitHub! This in-depth guide will walk you through the process of creating an entire plugin from scratch. It even includes a companion [demo plugin repo](https://github.com/netbox-community/netbox-plugin-demo) to ensure you can jump in at any step along the way. This will get you up and running with plugins in no time!
|
||||
|
||||
NetBox can be extended to support additional data models and functionality through the use of plugins. A plugin is essentially a self-contained [Django app](https://docs.djangoproject.com/en/stable/) which gets installed alongside NetBox to provide custom functionality. Multiple plugins can be installed in a single NetBox instance, and each plugin can be enabled and configured independently.
|
||||
|
||||
@@ -29,14 +29,20 @@ Although the specific structure of a plugin is largely left to the discretion of
|
||||
project-name/
|
||||
- plugin_name/
|
||||
- api/
|
||||
- __init__.py
|
||||
- serializers.py
|
||||
- urls.py
|
||||
- views.py
|
||||
- migrations/
|
||||
- __init__.py
|
||||
- 0001_initial.py
|
||||
- ...
|
||||
- templates/
|
||||
- plugin_name/
|
||||
- *.html
|
||||
- __init__.py
|
||||
- filtersets.py
|
||||
- graphql.py
|
||||
- models.py
|
||||
- middleware.py
|
||||
- navigation.py
|
||||
|
||||
@@ -161,13 +161,16 @@ class StatusChoices(ChoiceSet):
|
||||
STATUS_BAR = 'bar'
|
||||
STATUS_BAZ = 'baz'
|
||||
|
||||
CHOICES = (
|
||||
CHOICES = [
|
||||
(STATUS_FOO, 'Foo', 'red'),
|
||||
(STATUS_BAR, 'Bar', 'green'),
|
||||
(STATUS_BAZ, 'Baz', 'blue'),
|
||||
)
|
||||
]
|
||||
```
|
||||
|
||||
!!! warning
|
||||
For dynamic configuration to work properly, `CHOICES` must be a mutable list, rather than a tuple.
|
||||
|
||||
```python
|
||||
# models.py
|
||||
from django.db import models
|
||||
|
||||
@@ -4,24 +4,72 @@ Plugins can declare custom endpoints on NetBox's REST API to retrieve or manipul
|
||||
|
||||
Generally speaking, there aren't many NetBox-specific components to implementing REST API functionality in a plugin. NetBox employs the [Django REST Framework](https://www.django-rest-framework.org/) (DRF) for its REST API, and plugin authors will find that they can largely replicate the same patterns found in NetBox's implementation. Some brief examples are included here for reference.
|
||||
|
||||
## Code Layout
|
||||
|
||||
The recommended approach is to separate API serializers, views, and URLs into separate modules under the `api/` directory to keep things tidy, particularly for larger projects. The file at `api/__init__.py` can import the relevant components from each submodule to allow import all API components directly from elsewhere. However, this is merely a convention and not strictly required.
|
||||
|
||||
```no-highlight
|
||||
project-name/
|
||||
- plugin_name/
|
||||
- api/
|
||||
- __init__.py
|
||||
- serializers.py
|
||||
- urls.py
|
||||
- views.py
|
||||
...
|
||||
```
|
||||
|
||||
## Serializers
|
||||
|
||||
### Model Serializers
|
||||
|
||||
Serializers are responsible for converting Python objects to JSON data suitable for conveying to consumers, and vice versa. NetBox provides the `NetBoxModelSerializer` class for use by plugins to handle the assignment of tags and custom field data. (These features can also be included ad hoc via the `CustomFieldModelSerializer` and `TaggableModelSerializer` classes.)
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class.
|
||||
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class. It is generally advisable to include a `url` attribute on each serializer. This will render the direct link to access the object being rendered.
|
||||
|
||||
```python
|
||||
# api/serializers.py
|
||||
from rest_framework import serializers
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from my_plugin.models import MyModel
|
||||
|
||||
class MyModelSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='plugins-api:myplugin-api:mymodel-detail'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = MyModel
|
||||
fields = ('id', 'foo', 'bar')
|
||||
fields = ('id', 'foo', 'bar', 'baz')
|
||||
```
|
||||
|
||||
### Nested Serializers
|
||||
|
||||
There are two cases where it is generally desirable to show only a minimal representation of an object:
|
||||
|
||||
1. When displaying an object related to the one being viewed (for example, the region to which a site is assigned)
|
||||
2. Listing many objects using "brief" mode
|
||||
|
||||
To accommodate these, it is recommended to create nested serializers accompanying the "full" serializer for each model. NetBox provides the `WritableNestedSerializer` class for just this purpose. This class accepts a primary key value on write, but displays an object representation for read requests. It also includes a read-only `display` attribute which conveys the string representation of the object.
|
||||
|
||||
#### Example
|
||||
|
||||
```python
|
||||
# api/serializers.py
|
||||
from rest_framework import serializers
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from my_plugin.models import MyModel
|
||||
|
||||
class NestedMyModelSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='plugins-api:myplugin-api:mymodel-detail'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = MyModel
|
||||
fields = ('id', 'display', 'foo')
|
||||
```
|
||||
|
||||
## Viewsets
|
||||
@@ -55,10 +103,10 @@ Routers should be exposed in `api/urls.py`. This file **must** define a variable
|
||||
|
||||
```python
|
||||
# api/urls.py
|
||||
from rest_framework import routers
|
||||
from netbox.api.routers import NetBoxRouter
|
||||
from .views import MyModelViewSet
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router = NetBoxRouter()
|
||||
router.register('my-model', MyModelViewSet)
|
||||
urlpatterns = router.urls
|
||||
```
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
Templates are used to render HTML content generated from a set of context data. NetBox provides a set of built-in templates suitable for use in plugin views. Plugin authors can extend these templates to minimize the work needed to create custom templates while ensuring that the content they produce matches NetBox's layout and style. These templates are all written in the [Django Template Language (DTL)](https://docs.djangoproject.com/en/stable/ref/templates/language/).
|
||||
|
||||
## Template File Structure
|
||||
|
||||
Plugin templates should live in the `templates/<plugin-name>/` path within the plugin root. For example if your plugin's name is `my_plugin` and you create a template named `foo.html`, it should be saved to `templates/my_plugin/foo.html`. (You can of course use subdirectories below this point as well.) This ensures that Django's template engine can locate the template for rendering.
|
||||
|
||||
## Standard Blocks
|
||||
|
||||
The following template blocks are available on all templates.
|
||||
@@ -226,6 +230,8 @@ The following custom template filters are available in NetBox.
|
||||
|
||||
::: utilities.templatetags.builtins.filters.content_type_id
|
||||
|
||||
::: utilities.templatetags.builtins.filters.linkify
|
||||
|
||||
::: utilities.templatetags.builtins.filters.meta
|
||||
|
||||
::: utilities.templatetags.builtins.filters.placeholder
|
||||
|
||||
@@ -10,7 +10,7 @@ Minor releases are published in April, August, and December of each calendar yea
|
||||
|
||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||
|
||||
#### [Version 3.2](./version-3.2.md) (Pending Release)
|
||||
#### [Version 3.2](./version-3.2.md) (April 2022)
|
||||
|
||||
* Plugins Framework Extensions ([#8333](https://github.com/netbox-community/netbox/issues/8333))
|
||||
* Modules & Module Types ([#7844](https://github.com/netbox-community/netbox/issues/7844))
|
||||
|
||||
@@ -1,6 +1,50 @@
|
||||
# NetBox v3.1
|
||||
|
||||
## v3.1.10 (FUTURE)
|
||||
## v3.1.11 (2022-04-05)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#8163](https://github.com/netbox-community/netbox/issues/8163) - Show bridge interface members under interface view
|
||||
* [#8365](https://github.com/netbox-community/netbox/issues/8365) - Enable filtering child devices by parent device ID
|
||||
* [#8785](https://github.com/netbox-community/netbox/issues/8785) - Permit wildcard values in IP address DNS names
|
||||
* [#8790](https://github.com/netbox-community/netbox/issues/8790) - Include site and prefixes columns in VLAN group VLANs table
|
||||
* [#8830](https://github.com/netbox-community/netbox/issues/8830) - Add Checkpoint ClusterXL protocol for FHRP groups
|
||||
* [#8974](https://github.com/netbox-community/netbox/issues/8974) - Use monospace font for text areas in config revision form
|
||||
* [#9012](https://github.com/netbox-community/netbox/issues/9012) - Linkify circuits count in providers list
|
||||
* [#9036](https://github.com/netbox-community/netbox/issues/9036) - Add bulk edit capability for site contact fields
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#8866](https://github.com/netbox-community/netbox/issues/8866) - Prevent exception when searching for a rack position with no rack specified under device edit view
|
||||
* [#9009](https://github.com/netbox-community/netbox/issues/9009) - Fix device count for racks in global search results
|
||||
|
||||
---
|
||||
|
||||
## v3.1.10 (2022-03-25)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#8232](https://github.com/netbox-community/netbox/issues/8232) - Use a different color for 100% utilization bars
|
||||
* [#8457](https://github.com/netbox-community/netbox/issues/8457) - Enable adding non-racked devices from site & location views
|
||||
* [#8553](https://github.com/netbox-community/netbox/issues/8553) - Add missing object types to global search form
|
||||
* [#8575](https://github.com/netbox-community/netbox/issues/8575) - Add rack columns to cables list
|
||||
* [#8645](https://github.com/netbox-community/netbox/issues/8645) - Enable filtering objects by assigned contacts & contact roles
|
||||
* [#8926](https://github.com/netbox-community/netbox/issues/8926) - Add device type, role columns to device bay table
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#8696](https://github.com/netbox-community/netbox/issues/8696) - Fix help link under FHRP group assigment creation view
|
||||
* [#8813](https://github.com/netbox-community/netbox/issues/8813) - Retain global search bar query after submitting
|
||||
* [#8820](https://github.com/netbox-community/netbox/issues/8820) - Fix navbar background color in dark mode
|
||||
* [#8850](https://github.com/netbox-community/netbox/issues/8850) - Show airflow field on device REST API serializer when config context data is included
|
||||
* [#8905](https://github.com/netbox-community/netbox/issues/8905) - Disable ordering by assigned tags to prevent erroneous results
|
||||
* [#8919](https://github.com/netbox-community/netbox/issues/8919) - Fix filtering of VLAN groups by site under prefix edit form
|
||||
* [#8924](https://github.com/netbox-community/netbox/issues/8924) - Improve load time of custom script list
|
||||
* [#8932](https://github.com/netbox-community/netbox/issues/8932) - Fix error when setting null value for interface `rf_role` via REST API
|
||||
* [#8935](https://github.com/netbox-community/netbox/issues/8935) - Correct ordering of next/previous racks to use naturalized names
|
||||
* [#8947](https://github.com/netbox-community/netbox/issues/8947) - Retain filter parameters when handling an export template exception
|
||||
* [#8951](https://github.com/netbox-community/netbox/issues/8951) - Allow changing device type & platform to different manufacturer simultaneously
|
||||
* [#8952](https://github.com/netbox-community/netbox/issues/8952) - Device images in rear rack elevations should be hyperlinked
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,10 +1,46 @@
|
||||
# NetBox v3.2
|
||||
|
||||
## v3.2.0 (FUTURE)
|
||||
## v3.2.1 (2022-04-14)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#5479](https://github.com/netbox-community/netbox/issues/5479) - Allow custom job timeouts for scripts & reports
|
||||
* [#8543](https://github.com/netbox-community/netbox/issues/8543) - Improve filtering for wireless LAN VLAN selection
|
||||
* [#8920](https://github.com/netbox-community/netbox/issues/8920) - Limit number of non-racked devices displayed
|
||||
* [#8956](https://github.com/netbox-community/netbox/issues/8956) - Retain old script/report results for configured lifetime
|
||||
* [#8973](https://github.com/netbox-community/netbox/issues/8973) - Display VLAN group count under site view
|
||||
* [#9081](https://github.com/netbox-community/netbox/issues/9081) - Add `fhrpgroup_id` filter for IP addresses
|
||||
* [#9099](https://github.com/netbox-community/netbox/issues/9099) - Enable display of installed module serial & asset tag in module bays list
|
||||
* [#9110](https://github.com/netbox-community/netbox/issues/9110) - Add Neutrik proprietary power connectors
|
||||
* [#9123](https://github.com/netbox-community/netbox/issues/9123) - Improve appearance of SSO login providers
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#8931](https://github.com/netbox-community/netbox/issues/8931) - Copy assigned tenant when cloning a location
|
||||
* [#9055](https://github.com/netbox-community/netbox/issues/9055) - Restore ability to move inventory item to other device
|
||||
* [#9057](https://github.com/netbox-community/netbox/issues/9057) - Fix missing instance counts for module types
|
||||
* [#9061](https://github.com/netbox-community/netbox/issues/9061) - Fix general search for device components
|
||||
* [#9065](https://github.com/netbox-community/netbox/issues/9065) - Min/max VID should not be required when filtering VLAN groups
|
||||
* [#9079](https://github.com/netbox-community/netbox/issues/9079) - Fail validation when an inventory item is assigned as its own parent
|
||||
* [#9096](https://github.com/netbox-community/netbox/issues/9096) - Remove duplicate filter tag when filtering by "none"
|
||||
* [#9100](https://github.com/netbox-community/netbox/issues/9100) - Include position field in module type YAML export
|
||||
* [#9116](https://github.com/netbox-community/netbox/issues/9116) - `assigned_to_interface` filter for IP addresses should not match FHRP group assignments
|
||||
* [#9118](https://github.com/netbox-community/netbox/issues/9118) - Fix validation error when importing VM child interfaces
|
||||
* [#9128](https://github.com/netbox-community/netbox/issues/9128) - Resolve component labels per module bay position when installing modules
|
||||
|
||||
---
|
||||
|
||||
## v3.2.0 (2022-04-05)
|
||||
|
||||
!!! warning "Python 3.8 or Later Required"
|
||||
NetBox v3.2 requires Python 3.8 or later.
|
||||
|
||||
!!! warning "Deletion of Legacy Data"
|
||||
This release includes a database migration that will remove the `asn`, `contact_name`, `contact_phone`, and `contact_email` fields from the site model. (These fields have been superseded by the ASN and contact models introduced in NetBox v3.1.) To protect against the accidental destruction of data, the upgrade process **will fail** if any sites still have data in any of these fields. To bypass this safeguard, set the `NETBOX_DELETE_LEGACY_DATA` environment variable when running the upgrade script, which will permit the destruction of legacy data.
|
||||
|
||||
!!! tip "Migration Scripts"
|
||||
A set of [migration scripts](https://github.com/netbox-community/migration-scripts) is available to assist with the migration of legacy site data.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Automatic redirection of legacy slug-based URL paths has been removed. URL-based slugs were changed to use numeric IDs in v2.11.0.
|
||||
@@ -142,26 +178,23 @@ Where it is desired to limit the range of available VLANs within a group, users
|
||||
* [#8296](https://github.com/netbox-community/netbox/issues/8296) - Allow disabling custom links
|
||||
* [#8307](https://github.com/netbox-community/netbox/issues/8307) - Add `data_type` indicator to REST API serializer for custom fields
|
||||
* [#8463](https://github.com/netbox-community/netbox/issues/8463) - Change the `created` field on all change-logged models from date to datetime
|
||||
* [#8496](https://github.com/netbox-community/netbox/issues/8496) - Enable assigning multiple ASNs to a provider
|
||||
* [#8572](https://github.com/netbox-community/netbox/issues/8572) - Add a `pre_run()` method for reports
|
||||
* [#8593](https://github.com/netbox-community/netbox/issues/8593) - Add a `link` field for contacts
|
||||
* [#8649](https://github.com/netbox-community/netbox/issues/8649) - Enable customization of configuration module using `NETBOX_CONFIGURATION` environment variable
|
||||
* [#9006](https://github.com/netbox-community/netbox/issues/9006) - Enable custom fields, custom links, and tags for journal entries
|
||||
|
||||
### Bug Fixes (From Beta1)
|
||||
### Bug Fixes (From Beta2)
|
||||
|
||||
* [#8655](https://github.com/netbox-community/netbox/issues/8655) - Fix AttributeError when viewing cabled interfaces
|
||||
* [#8656](https://github.com/netbox-community/netbox/issues/8656) - Fix migration error when upgrading from a v2.11 database
|
||||
* [#8659](https://github.com/netbox-community/netbox/issues/8659) - Fix display of multi-object custom fields after deleting related object
|
||||
* [#8661](https://github.com/netbox-community/netbox/issues/8661) - Fix ValueError exception when trying to connect a cable
|
||||
* [#8670](https://github.com/netbox-community/netbox/issues/8670) - Fix filtering device components by installed module
|
||||
* [#8671](https://github.com/netbox-community/netbox/issues/8671) - Fix AttributeError when viewing console/power/interface connection lists
|
||||
* [#8682](https://github.com/netbox-community/netbox/issues/8682) - Limit available VLANs by group min/max VIDs
|
||||
* [#8683](https://github.com/netbox-community/netbox/issues/8683) - Fix `ZoneInfoNotFoundError` exception under Python 3.9+
|
||||
* [#8761](https://github.com/netbox-community/netbox/issues/8761) - Correct view name resolution under journal entry views
|
||||
* [#8763](https://github.com/netbox-community/netbox/issues/8763) - Fix inventory item component assignment
|
||||
* [#8764](https://github.com/netbox-community/netbox/issues/8764) - Correct view name resolution for dynamic form fields
|
||||
* [#8791](https://github.com/netbox-community/netbox/issues/8791) - Fix display of form validation failures during device component creation
|
||||
* [#8792](https://github.com/netbox-community/netbox/issues/8792) - Fix creation of circuit terminations via UI
|
||||
* [#8810](https://github.com/netbox-community/netbox/issues/8810) - Enable filtering modules by type
|
||||
* [#8815](https://github.com/netbox-community/netbox/issues/8815) - Fix display of custom object fields in table columns
|
||||
* [#8658](https://github.com/netbox-community/netbox/issues/8658) - Fix display of assigned components under inventory item lists
|
||||
* [#8838](https://github.com/netbox-community/netbox/issues/8838) - Fix FieldError exception during global search
|
||||
* [#8845](https://github.com/netbox-community/netbox/issues/8845) - Correct default ASN formatting in table
|
||||
* [#8869](https://github.com/netbox-community/netbox/issues/8869) - Fix NoReverseMatch exception when displaying tag w/assignments
|
||||
* [#8872](https://github.com/netbox-community/netbox/issues/8872) - Enable filtering by custom object fields
|
||||
* [#8970](https://github.com/netbox-community/netbox/issues/8970) - Permit nested inventory item templates on device types
|
||||
* [#8976](https://github.com/netbox-community/netbox/issues/8976) - Add missing `object_type` field on CustomField REST API serializer
|
||||
* [#8978](https://github.com/netbox-community/netbox/issues/8978) - Fix instantiation of front ports when provisioning a module
|
||||
* [#9007](https://github.com/netbox-community/netbox/issues/9007) - Fix FieldError exception when instantiating a device type with nested inventory items
|
||||
|
||||
### Other Changes
|
||||
|
||||
@@ -184,6 +217,8 @@ Where it is desired to limit the range of available VLANs within a group, users
|
||||
* `/api/dcim/module-types/`
|
||||
* `/api/ipam/service-templates/`
|
||||
* `/api/ipam/vlan-groups/<id>/available-vlans/`
|
||||
* circuits.Provider
|
||||
* Added `asns` field
|
||||
* circuits.ProviderNetwork
|
||||
* Added `service_id` field
|
||||
* dcim.ConsolePort
|
||||
@@ -211,8 +246,14 @@ Where it is desired to limit the range of available VLANs within a group, users
|
||||
* Added `data_type` and `object_type` fields
|
||||
* extras.CustomLink
|
||||
* Added `enabled` field
|
||||
* extras.JournalEntry
|
||||
* Added `custom_fields` and `tags` fields
|
||||
* ipam.ASN
|
||||
* Added `provider_count` field
|
||||
* ipam.VLANGroup
|
||||
* Added the `/availables-vlans/` endpoint
|
||||
* Added the `min_vid` and `max_vid` fields
|
||||
* Added `min_vid` and `max_vid` fields
|
||||
* tenancy.Contact
|
||||
* Added `link` field
|
||||
* virtualization.VMInterface
|
||||
* Added `vrf` field
|
||||
|
||||
@@ -4,7 +4,9 @@ from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
||||
from dcim.api.serializers import LinkTerminationSerializer
|
||||
from netbox.api import ChoiceField
|
||||
from ipam.models import ASN
|
||||
from ipam.api.nested_serializers import NestedASNSerializer
|
||||
from netbox.api import ChoiceField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from .nested_serializers import *
|
||||
@@ -16,13 +18,21 @@ from .nested_serializers import *
|
||||
|
||||
class ProviderSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
asns = SerializedPKRelatedField(
|
||||
queryset=ASN.objects.all(),
|
||||
serializer=NestedASNSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
|
||||
# Related object counts
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
'comments', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from netbox.api import OrderedDefaultRouter
|
||||
from netbox.api import NetBoxRouter
|
||||
from . import views
|
||||
|
||||
|
||||
router = OrderedDefaultRouter()
|
||||
router = NetBoxRouter()
|
||||
router.APIRootView = views.CircuitsRootView
|
||||
|
||||
# Providers
|
||||
|
||||
@@ -21,7 +21,7 @@ class CircuitsRootView(APIRootView):
|
||||
#
|
||||
|
||||
class ProviderViewSet(NetBoxModelViewSet):
|
||||
queryset = Provider.objects.prefetch_related('tags').annotate(
|
||||
queryset = Provider.objects.prefetch_related('asns', 'tags').annotate(
|
||||
circuit_count=count_related(Circuit, 'provider')
|
||||
)
|
||||
serializer_class = serializers.ProviderSerializer
|
||||
|
||||
@@ -3,8 +3,9 @@ from django.db.models import Q
|
||||
|
||||
from dcim.filtersets import CableTerminationFilterSet
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from ipam.models import ASN
|
||||
from netbox.filtersets import ChangeLoggedModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet
|
||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
from .choices import *
|
||||
from .models import *
|
||||
@@ -18,11 +19,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region',
|
||||
@@ -60,6 +57,11 @@ class ProviderFilterSet(NetBoxModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
asn_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='asns',
|
||||
queryset=ASN.objects.all(),
|
||||
label='ASN (ID)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
@@ -78,10 +80,6 @@ class ProviderFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
|
||||
class ProviderNetworkFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider (ID)',
|
||||
@@ -115,11 +113,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider (ID)',
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import add_blank_choice, CommentField, DynamicModelChoiceField, SmallTextarea, StaticSelect
|
||||
from utilities.forms import (
|
||||
add_blank_choice, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea,
|
||||
StaticSelect,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'CircuitBulkEditForm',
|
||||
@@ -17,7 +22,12 @@ __all__ = (
|
||||
class ProviderBulkEditForm(NetBoxModelBulkEditForm):
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
label='ASN'
|
||||
label='ASN (legacy)'
|
||||
)
|
||||
asns = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
label=_('ASNs'),
|
||||
required=False
|
||||
)
|
||||
account = forms.CharField(
|
||||
max_length=30,
|
||||
@@ -45,10 +55,10 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
model = Provider
|
||||
fieldsets = (
|
||||
(None, ('asn', 'account', 'portal_url', 'noc_contact', 'admin_contact')),
|
||||
(None, ('asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
'asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ from django.utils.translation import gettext as _
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.forms import TenancyFilterForm
|
||||
from utilities.forms import DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField
|
||||
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
|
||||
from utilities.forms import DynamicModelMultipleChoiceField, MultipleChoiceField, TagFilterField
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilterForm',
|
||||
@@ -16,12 +17,13 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderFilterForm(NetBoxModelFilterSetForm):
|
||||
class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Provider
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||
('ASN', ('asn',)),
|
||||
('Contacts', ('contact', 'contact_role')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -44,7 +46,12 @@ class ProviderFilterForm(NetBoxModelFilterSetForm):
|
||||
)
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
label=_('ASN')
|
||||
label=_('ASN (legacy)')
|
||||
)
|
||||
asn_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
required=False,
|
||||
label=_('ASNs')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -72,7 +79,7 @@ class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CircuitFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Circuit
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
@@ -80,6 +87,7 @@ class CircuitFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
('Attributes', ('type_id', 'status', 'commit_rate')),
|
||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
('Contacts', ('contact', 'contact_role')),
|
||||
)
|
||||
type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
@@ -99,10 +107,9 @@ class CircuitFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
},
|
||||
label=_('Provider network')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
status = MultipleChoiceField(
|
||||
choices=CircuitStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from extras.models import Tag
|
||||
from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms import (
|
||||
@@ -21,21 +22,22 @@ __all__ = (
|
||||
|
||||
class ProviderForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
asns = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
label=_('ASNs'),
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
('Provider', ('name', 'slug', 'asn', 'tags')),
|
||||
('Provider', ('name', 'slug', 'asn', 'asns', 'tags')),
|
||||
('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
|
||||
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'asns', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'noc_contact': SmallTextarea(
|
||||
@@ -59,10 +61,6 @@ class ProviderNetworkForm(NetBoxModelForm):
|
||||
queryset=Provider.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Provider Network', ('provider', 'name', 'service_id', 'description', 'tags')),
|
||||
@@ -77,10 +75,6 @@ class ProviderNetworkForm(NetBoxModelForm):
|
||||
|
||||
class CircuitTypeForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
@@ -97,10 +91,6 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
queryset=CircuitType.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Circuit', ('provider', 'cid', 'type', 'status', 'install_date', 'commit_rate', 'description', 'tags')),
|
||||
|
||||
@@ -5,6 +5,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0004_rename_cable_peer'),
|
||||
('dcim', '0145_site_remove_deprecated_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
19
netbox/circuits/migrations/0035_provider_asns.py
Normal file
19
netbox/circuits/migrations/0035_provider_asns.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.0.3 on 2022-03-30 20:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0057_created_datetimefield'),
|
||||
('circuits', '0034_created_datetimefield'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='provider',
|
||||
name='asns',
|
||||
field=models.ManyToManyField(blank=True, related_name='providers', to='ipam.asn'),
|
||||
),
|
||||
]
|
||||
@@ -30,6 +30,11 @@ class Provider(NetBoxModel):
|
||||
verbose_name='ASN',
|
||||
help_text='32-bit autonomous system number'
|
||||
)
|
||||
asns = models.ManyToManyField(
|
||||
to='ipam.ASN',
|
||||
related_name='providers',
|
||||
blank=True
|
||||
)
|
||||
account = models.CharField(
|
||||
max_length=30,
|
||||
blank=True,
|
||||
|
||||
@@ -59,6 +59,9 @@ class CircuitTable(NetBoxTable):
|
||||
)
|
||||
commit_rate = CommitRateColumn()
|
||||
comments = columns.MarkdownColumn()
|
||||
contacts = tables.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:circuit_list'
|
||||
)
|
||||
@@ -67,7 +70,7 @@ class CircuitTable(NetBoxTable):
|
||||
model = Circuit
|
||||
fields = (
|
||||
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
|
||||
'commit_rate', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
|
||||
|
||||
@@ -14,11 +14,26 @@ class ProviderTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
circuit_count = tables.Column(
|
||||
asns = tables.ManyToManyColumn(
|
||||
linkify_item=True,
|
||||
verbose_name='ASNs'
|
||||
)
|
||||
asn_count = columns.LinkedCountColumn(
|
||||
accessor=tables.A('asns__count'),
|
||||
viewname='ipam:asn_list',
|
||||
url_params={'provider_id': 'pk'},
|
||||
verbose_name='ASN Count'
|
||||
)
|
||||
circuit_count = columns.LinkedCountColumn(
|
||||
accessor=Accessor('count_circuits'),
|
||||
viewname='circuits:circuit_list',
|
||||
url_params={'provider_id': 'pk'},
|
||||
verbose_name='Circuits'
|
||||
)
|
||||
comments = columns.MarkdownColumn()
|
||||
contacts = tables.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:provider_list'
|
||||
)
|
||||
@@ -26,8 +41,8 @@ class ProviderTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Provider
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count',
|
||||
'comments', 'tags', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'asn_count',
|
||||
'circuit_count', 'comments', 'contacts', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.urls import reverse
|
||||
from circuits.choices import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from ipam.models import ASN, RIR
|
||||
from utilities.testing import APITestCase, APIViewTestCases
|
||||
|
||||
|
||||
@@ -18,20 +19,6 @@ class AppTest(APITestCase):
|
||||
class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Provider
|
||||
brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Provider 4',
|
||||
'slug': 'provider-4',
|
||||
},
|
||||
{
|
||||
'name': 'Provider 5',
|
||||
'slug': 'provider-5',
|
||||
},
|
||||
{
|
||||
'name': 'Provider 6',
|
||||
'slug': 'provider-6',
|
||||
},
|
||||
]
|
||||
bulk_update_data = {
|
||||
'asn': 1234,
|
||||
}
|
||||
@@ -39,6 +26,12 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
rir = RIR.objects.create(name='RFC 6996', is_private=True)
|
||||
asns = [
|
||||
ASN(asn=65000 + i, rir=rir) for i in range(8)
|
||||
]
|
||||
ASN.objects.bulk_create(asns)
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
@@ -46,6 +39,24 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Provider 4',
|
||||
'slug': 'provider-4',
|
||||
'asns': [asns[0].pk, asns[1].pk],
|
||||
},
|
||||
{
|
||||
'name': 'Provider 5',
|
||||
'slug': 'provider-5',
|
||||
'asns': [asns[2].pk, asns[3].pk],
|
||||
},
|
||||
{
|
||||
'name': 'Provider 6',
|
||||
'slug': 'provider-6',
|
||||
'asns': [asns[4].pk, asns[5].pk],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitType
|
||||
|
||||
@@ -4,6 +4,7 @@ from circuits.choices import *
|
||||
from circuits.filtersets import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Cable, Region, Site, SiteGroup
|
||||
from ipam.models import ASN, RIR
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.testing import ChangeLoggedFilterSetTests
|
||||
|
||||
@@ -15,6 +16,14 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
rir = RIR.objects.create(name='RFC 6996', is_private=True)
|
||||
asns = (
|
||||
ASN(asn=64512, rir=rir),
|
||||
ASN(asn=64513, rir=rir),
|
||||
ASN(asn=64514, rir=rir),
|
||||
)
|
||||
ASN.objects.bulk_create(asns)
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1', asn=65001, account='1234'),
|
||||
Provider(name='Provider 2', slug='provider-2', asn=65002, account='2345'),
|
||||
@@ -23,6 +32,9 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Provider(name='Provider 5', slug='provider-5', asn=65005, account='5678'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
providers[0].asns.set([asns[0]])
|
||||
providers[1].asns.set([asns[1]])
|
||||
providers[2].asns.set([asns[2]])
|
||||
|
||||
regions = (
|
||||
Region(name='Test Region 1', slug='test-region-1'),
|
||||
@@ -70,10 +82,15 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'slug': ['provider-1', 'provider-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_asn(self):
|
||||
def test_asn(self): # Legacy field
|
||||
params = {'asn': ['65001', '65002']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_asn_id(self): # ASN object assignment
|
||||
asns = ASN.objects.all()[:2]
|
||||
params = {'asn_id': [asns[0].pk, asns[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_account(self):
|
||||
params = {'account': ['1234', '2345']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.urls import reverse
|
||||
from circuits.choices import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Cable, Interface, Site
|
||||
from ipam.models import ASN, RIR
|
||||
from utilities.testing import ViewTestCases, create_tags, create_test_device
|
||||
|
||||
|
||||
@@ -15,11 +16,21 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
Provider.objects.bulk_create([
|
||||
rir = RIR.objects.create(name='RFC 6996', is_private=True)
|
||||
asns = [
|
||||
ASN(asn=65000 + i, rir=rir) for i in range(8)
|
||||
]
|
||||
ASN.objects.bulk_create(asns)
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1', asn=65001),
|
||||
Provider(name='Provider 2', slug='provider-2', asn=65002),
|
||||
Provider(name='Provider 3', slug='provider-3', asn=65003),
|
||||
])
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
providers[0].asns.set([asns[0], asns[1]])
|
||||
providers[1].asns.set([asns[2], asns[3]])
|
||||
providers[2].asns.set([asns[4], asns[5]])
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
@@ -27,6 +38,7 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'name': 'Provider X',
|
||||
'slug': 'provider-x',
|
||||
'asn': 65123,
|
||||
'asns': [asns[6].pk, asns[7].pk],
|
||||
'account': '1234',
|
||||
'portal_url': 'http://example.com/portal',
|
||||
'noc_contact': 'noc@example.com',
|
||||
|
||||
@@ -42,7 +42,7 @@ class ProviderView(generic.ObjectView):
|
||||
|
||||
class ProviderEditView(generic.ObjectEditView):
|
||||
queryset = Provider.objects.all()
|
||||
model_form = forms.ProviderForm
|
||||
form = forms.ProviderForm
|
||||
|
||||
|
||||
class ProviderDeleteView(generic.ObjectDeleteView):
|
||||
@@ -103,7 +103,7 @@ class ProviderNetworkView(generic.ObjectView):
|
||||
|
||||
class ProviderNetworkEditView(generic.ObjectEditView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
model_form = forms.ProviderNetworkForm
|
||||
form = forms.ProviderNetworkForm
|
||||
|
||||
|
||||
class ProviderNetworkDeleteView(generic.ObjectDeleteView):
|
||||
@@ -157,7 +157,7 @@ class CircuitTypeView(generic.ObjectView):
|
||||
|
||||
class CircuitTypeEditView(generic.ObjectEditView):
|
||||
queryset = CircuitType.objects.all()
|
||||
model_form = forms.CircuitTypeForm
|
||||
form = forms.CircuitTypeForm
|
||||
|
||||
|
||||
class CircuitTypeDeleteView(generic.ObjectDeleteView):
|
||||
@@ -205,7 +205,7 @@ class CircuitView(generic.ObjectView):
|
||||
|
||||
class CircuitEditView(generic.ObjectEditView):
|
||||
queryset = Circuit.objects.all()
|
||||
model_form = forms.CircuitForm
|
||||
form = forms.CircuitForm
|
||||
|
||||
|
||||
class CircuitDeleteView(generic.ObjectDeleteView):
|
||||
@@ -315,7 +315,7 @@ class CircuitSwapTerminations(generic.ObjectEditView):
|
||||
|
||||
class CircuitTerminationEditView(generic.ObjectEditView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
model_form = forms.CircuitTerminationForm
|
||||
form = forms.CircuitTerminationForm
|
||||
template_name = 'circuits/circuittermination_edit.html'
|
||||
|
||||
|
||||
|
||||
@@ -134,10 +134,10 @@ class SiteSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asns',
|
||||
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count',
|
||||
'rack_count', 'virtualmachine_count', 'vlan_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone',
|
||||
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count',
|
||||
'virtualmachine_count', 'vlan_count',
|
||||
]
|
||||
|
||||
|
||||
@@ -576,9 +576,9 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
class Meta(DeviceSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
|
||||
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data',
|
||||
'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
|
||||
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments',
|
||||
'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@@ -720,9 +720,9 @@ class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn
|
||||
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||
duplex = ChoiceField(choices=InterfaceDuplexChoices, allow_blank=True, required=False)
|
||||
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
|
||||
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True)
|
||||
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True)
|
||||
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from netbox.api import OrderedDefaultRouter
|
||||
from netbox.api import NetBoxRouter
|
||||
from . import views
|
||||
|
||||
|
||||
router = OrderedDefaultRouter()
|
||||
router = NetBoxRouter()
|
||||
router.APIRootView = views.DCIMRootView
|
||||
|
||||
# Sites
|
||||
|
||||
@@ -345,6 +345,10 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
TYPE_DC = 'dc-terminal'
|
||||
# Proprietary
|
||||
TYPE_SAF_D_GRID = 'saf-d-grid'
|
||||
TYPE_NEUTRIK_POWERCON_20A = 'neutrik-powercon-20'
|
||||
TYPE_NEUTRIK_POWERCON_32A = 'neutrik-powercon-32'
|
||||
TYPE_NEUTRIK_POWERCON_TRUE1 = 'neutrik-powercon-true1'
|
||||
TYPE_NEUTRIK_POWERCON_TRUE1_TOP = 'neutrik-powercon-true1-top'
|
||||
# Other
|
||||
TYPE_HARDWIRED = 'hardwired'
|
||||
|
||||
@@ -456,6 +460,10 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
)),
|
||||
('Proprietary', (
|
||||
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
|
||||
(TYPE_NEUTRIK_POWERCON_20A, 'Neutrik powerCON (20A)'),
|
||||
(TYPE_NEUTRIK_POWERCON_32A, 'Neutrik powerCON (32A)'),
|
||||
(TYPE_NEUTRIK_POWERCON_TRUE1, 'Neutrik powerCON TRUE1'),
|
||||
(TYPE_NEUTRIK_POWERCON_TRUE1_TOP, 'Neutrik powerCON TRUE1 TOP'),
|
||||
)),
|
||||
('Other', (
|
||||
(TYPE_HARDWIRED, 'Hardwired'),
|
||||
@@ -561,6 +569,10 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
# Proprietary
|
||||
TYPE_HDOT_CX = 'hdot-cx'
|
||||
TYPE_SAF_D_GRID = 'saf-d-grid'
|
||||
TYPE_NEUTRIK_POWERCON_20A = 'neutrik-powercon-20a'
|
||||
TYPE_NEUTRIK_POWERCON_32A = 'neutrik-powercon-32a'
|
||||
TYPE_NEUTRIK_POWERCON_TRUE1 = 'neutrik-powercon-true1'
|
||||
TYPE_NEUTRIK_POWERCON_TRUE1_TOP = 'neutrik-powercon-true1-top'
|
||||
# Other
|
||||
TYPE_HARDWIRED = 'hardwired'
|
||||
|
||||
@@ -665,6 +677,10 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
('Proprietary', (
|
||||
(TYPE_HDOT_CX, 'HDOT Cx'),
|
||||
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
|
||||
(TYPE_NEUTRIK_POWERCON_20A, 'Neutrik powerCON (20A)'),
|
||||
(TYPE_NEUTRIK_POWERCON_32A, 'Neutrik powerCON (32A)'),
|
||||
(TYPE_NEUTRIK_POWERCON_TRUE1, 'Neutrik powerCON TRUE1'),
|
||||
(TYPE_NEUTRIK_POWERCON_TRUE1_TOP, 'Neutrik powerCON TRUE1 TOP'),
|
||||
)),
|
||||
('Other', (
|
||||
(TYPE_HARDWIRED, 'Hardwired'),
|
||||
|
||||
@@ -6,8 +6,8 @@ from ipam.models import ASN, VRF
|
||||
from netbox.filtersets import (
|
||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
||||
)
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
||||
from tenancy.models import *
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.filters import (
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
|
||||
@@ -67,7 +67,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class RegionFilterSet(OrganizationalModelFilterSet):
|
||||
class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
label='Parent region (ID)',
|
||||
@@ -84,7 +84,7 @@ class RegionFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class SiteGroupFilterSet(OrganizationalModelFilterSet):
|
||||
class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Parent site group (ID)',
|
||||
@@ -101,11 +101,7 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=SiteStatusChoices,
|
||||
null_value=None
|
||||
@@ -164,14 +160,13 @@ class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
try:
|
||||
qs_filter |= Q(asn=int(value.strip()))
|
||||
qs_filter |= Q(asns__asn=int(value.strip()))
|
||||
except ValueError:
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class LocationFilterSet(TenancyFilterSet, OrganizationalModelFilterSet):
|
||||
class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region',
|
||||
@@ -242,11 +237,7 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ['id', 'name', 'slug', 'color', 'description']
|
||||
|
||||
|
||||
class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region',
|
||||
@@ -340,10 +331,6 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
|
||||
|
||||
class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack (ID)',
|
||||
@@ -398,7 +385,7 @@ class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ManufacturerFilterSet(OrganizationalModelFilterSet):
|
||||
class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
@@ -406,10 +393,6 @@ class ManufacturerFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
|
||||
class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer (ID)',
|
||||
@@ -498,10 +481,6 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
|
||||
class ModuleTypeFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer (ID)',
|
||||
@@ -745,11 +724,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
|
||||
|
||||
|
||||
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device_type__manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@@ -776,6 +751,11 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, LocalConfigContext
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
parent_device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent_bay__device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Parent Device (ID)',
|
||||
)
|
||||
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Platform.objects.all(),
|
||||
label='Platform (ID)',
|
||||
@@ -957,10 +937,6 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, LocalConfigContext
|
||||
|
||||
|
||||
class ModuleFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='module_type__manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@@ -1119,8 +1095,8 @@ class PathEndpointFilterSet(django_filters.FilterSet):
|
||||
|
||||
|
||||
class ConsolePortFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
NetBoxModelFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet
|
||||
):
|
||||
@@ -1135,8 +1111,8 @@ class ConsolePortFilterSet(
|
||||
|
||||
|
||||
class ConsoleServerPortFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
NetBoxModelFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet
|
||||
):
|
||||
@@ -1151,8 +1127,8 @@ class ConsoleServerPortFilterSet(
|
||||
|
||||
|
||||
class PowerPortFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
NetBoxModelFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet
|
||||
):
|
||||
@@ -1167,8 +1143,8 @@ class PowerPortFilterSet(
|
||||
|
||||
|
||||
class PowerOutletFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
NetBoxModelFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet
|
||||
):
|
||||
@@ -1187,15 +1163,11 @@ class PowerOutletFilterSet(
|
||||
|
||||
|
||||
class InterfaceFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
NetBoxModelFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet
|
||||
):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
# Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis
|
||||
# members
|
||||
device = MultiValueCharFilter(
|
||||
@@ -1319,8 +1291,8 @@ class InterfaceFilterSet(
|
||||
|
||||
|
||||
class FrontPortFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
NetBoxModelFilterSet,
|
||||
CableTerminationFilterSet
|
||||
):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
@@ -1334,8 +1306,8 @@ class FrontPortFilterSet(
|
||||
|
||||
|
||||
class RearPortFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
ModularDeviceComponentFilterSet,
|
||||
NetBoxModelFilterSet,
|
||||
CableTerminationFilterSet
|
||||
):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
@@ -1348,25 +1320,21 @@ class RearPortFilterSet(
|
||||
fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description']
|
||||
|
||||
|
||||
class ModuleBayFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
|
||||
class ModuleBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ModuleBay
|
||||
fields = ['id', 'name', 'label', 'description']
|
||||
|
||||
|
||||
class DeviceBayFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
|
||||
class DeviceBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'name', 'label', 'description']
|
||||
|
||||
|
||||
class InventoryItemFilterSet(NetBoxModelFilterSet, DeviceComponentFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
label='Parent inventory item (ID)',
|
||||
@@ -1422,10 +1390,6 @@ class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
|
||||
class VirtualChassisFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
master_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
label='Master (ID)',
|
||||
@@ -1501,10 +1465,6 @@ class VirtualChassisFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
|
||||
class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
termination_a_type = ContentTypeFilter()
|
||||
termination_a_id = MultiValueNumberFilter()
|
||||
termination_b_type = ContentTypeFilter()
|
||||
@@ -1559,11 +1519,7 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
return queryset
|
||||
|
||||
|
||||
class PowerPanelFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region',
|
||||
@@ -1621,10 +1577,6 @@ class PowerPanelFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
|
||||
class PowerFeedFilterSet(NetBoxModelFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='power_panel__site__region',
|
||||
|
||||
@@ -115,6 +115,18 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
|
||||
label=_('ASNs'),
|
||||
required=False
|
||||
)
|
||||
contact_name = forms.CharField(
|
||||
max_length=50,
|
||||
required=False
|
||||
)
|
||||
contact_phone = forms.CharField(
|
||||
max_length=20,
|
||||
required=False
|
||||
)
|
||||
contact_email = forms.EmailField(
|
||||
required=False,
|
||||
label='Contact E-mail'
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
@@ -912,9 +924,33 @@ class InventoryItemTemplateBulkEditForm(BulkEditForm):
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ComponentBulkEditForm(NetBoxModelBulkEditForm):
|
||||
device = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
disabled=True,
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
module = forms.ModelChoiceField(
|
||||
queryset=Module.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit module queryset to Modules which belong to the parent Device
|
||||
if 'device' in self.initial:
|
||||
device = Device.objects.filter(pk=self.initial['device']).first()
|
||||
self.fields['module'].queryset = Module.objects.filter(device=device)
|
||||
else:
|
||||
self.fields['module'].choices = ()
|
||||
self.fields['module'].widget.attrs['disabled'] = True
|
||||
|
||||
|
||||
class ConsolePortBulkEditForm(
|
||||
form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description']),
|
||||
NetBoxModelBulkEditForm
|
||||
ComponentBulkEditForm
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
required=False,
|
||||
@@ -923,14 +959,14 @@ class ConsolePortBulkEditForm(
|
||||
|
||||
model = ConsolePort
|
||||
fieldsets = (
|
||||
(None, ('type', 'label', 'speed', 'description', 'mark_connected')),
|
||||
(None, ('module', 'type', 'label', 'speed', 'description', 'mark_connected')),
|
||||
)
|
||||
nullable_fields = ('label', 'description')
|
||||
nullable_fields = ('module', 'label', 'description')
|
||||
|
||||
|
||||
class ConsoleServerPortBulkEditForm(
|
||||
form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description']),
|
||||
NetBoxModelBulkEditForm
|
||||
ComponentBulkEditForm
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
required=False,
|
||||
@@ -939,14 +975,14 @@ class ConsoleServerPortBulkEditForm(
|
||||
|
||||
model = ConsoleServerPort
|
||||
fieldsets = (
|
||||
(None, ('type', 'label', 'speed', 'description', 'mark_connected')),
|
||||
(None, ('module', 'type', 'label', 'speed', 'description', 'mark_connected')),
|
||||
)
|
||||
nullable_fields = ('label', 'description')
|
||||
nullable_fields = ('module', 'label', 'description')
|
||||
|
||||
|
||||
class PowerPortBulkEditForm(
|
||||
form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description']),
|
||||
NetBoxModelBulkEditForm
|
||||
ComponentBulkEditForm
|
||||
):
|
||||
mark_connected = forms.NullBooleanField(
|
||||
required=False,
|
||||
@@ -955,22 +991,16 @@ class PowerPortBulkEditForm(
|
||||
|
||||
model = PowerPort
|
||||
fieldsets = (
|
||||
(None, ('type', 'label', 'description', 'mark_connected')),
|
||||
(None, ('module', 'type', 'label', 'description', 'mark_connected')),
|
||||
('Power', ('maximum_draw', 'allocated_draw')),
|
||||
)
|
||||
nullable_fields = ('label', 'description')
|
||||
nullable_fields = ('module', 'label', 'description')
|
||||
|
||||
|
||||
class PowerOutletBulkEditForm(
|
||||
form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description']),
|
||||
NetBoxModelBulkEditForm
|
||||
ComponentBulkEditForm
|
||||
):
|
||||
device = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
disabled=True,
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
mark_connected = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect
|
||||
@@ -978,10 +1008,10 @@ class PowerOutletBulkEditForm(
|
||||
|
||||
model = PowerOutlet
|
||||
fieldsets = (
|
||||
(None, ('type', 'label', 'description', 'mark_connected')),
|
||||
(None, ('module', 'type', 'label', 'description', 'mark_connected')),
|
||||
('Power', ('feed_leg', 'power_port')),
|
||||
)
|
||||
nullable_fields = ('label', 'type', 'feed_leg', 'power_port', 'description')
|
||||
nullable_fields = ('module', 'label', 'type', 'feed_leg', 'power_port', 'description')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -1001,14 +1031,8 @@ class InterfaceBulkEditForm(
|
||||
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
|
||||
'tx_power',
|
||||
]),
|
||||
NetBoxModelBulkEditForm
|
||||
ComponentBulkEditForm
|
||||
):
|
||||
device = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
disabled=True,
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect
|
||||
@@ -1059,7 +1083,7 @@ class InterfaceBulkEditForm(
|
||||
|
||||
model = Interface
|
||||
fieldsets = (
|
||||
(None, ('type', 'label', 'speed', 'duplex', 'description')),
|
||||
(None, ('module', 'type', 'label', 'speed', 'duplex', 'description')),
|
||||
('Addressing', ('vrf', 'mac_address', 'wwn')),
|
||||
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
|
||||
('Related Interfaces', ('parent', 'bridge', 'lag')),
|
||||
@@ -1067,8 +1091,9 @@ class InterfaceBulkEditForm(
|
||||
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description', 'mode',
|
||||
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'vrf',
|
||||
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description',
|
||||
'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans',
|
||||
'vrf',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -1133,24 +1158,24 @@ class InterfaceBulkEditForm(
|
||||
|
||||
class FrontPortBulkEditForm(
|
||||
form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
NetBoxModelBulkEditForm
|
||||
ComponentBulkEditForm
|
||||
):
|
||||
model = FrontPort
|
||||
fieldsets = (
|
||||
(None, ('type', 'label', 'color', 'description', 'mark_connected')),
|
||||
(None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')),
|
||||
)
|
||||
nullable_fields = ('label', 'description')
|
||||
nullable_fields = ('module', 'label', 'description')
|
||||
|
||||
|
||||
class RearPortBulkEditForm(
|
||||
form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
NetBoxModelBulkEditForm
|
||||
ComponentBulkEditForm
|
||||
):
|
||||
model = RearPort
|
||||
fieldsets = (
|
||||
(None, ('type', 'label', 'color', 'description', 'mark_connected')),
|
||||
(None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')),
|
||||
)
|
||||
nullable_fields = ('label', 'description')
|
||||
nullable_fields = ('module', 'label', 'description')
|
||||
|
||||
|
||||
class ModuleBayBulkEditForm(
|
||||
@@ -1179,6 +1204,10 @@ class InventoryItemBulkEditForm(
|
||||
form_from_model(InventoryItem, ['label', 'role', 'manufacturer', 'part_id', 'description']),
|
||||
NetBoxModelBulkEditForm
|
||||
):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=InventoryItemRole.objects.all(),
|
||||
required=False
|
||||
@@ -1190,7 +1219,7 @@ class InventoryItemBulkEditForm(
|
||||
|
||||
model = InventoryItem
|
||||
fieldsets = (
|
||||
(None, ('label', 'role', 'manufacturer', 'part_id', 'description')),
|
||||
(None, ('device', 'label', 'role', 'manufacturer', 'part_id', 'description')),
|
||||
)
|
||||
nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
|
||||
|
||||
|
||||
@@ -651,11 +651,11 @@ class InterfaceCSVForm(NetBoxModelCSVForm):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
# Limit interface choices for parent, bridge and lag to device only
|
||||
params = {}
|
||||
if data.get('device'):
|
||||
params[f"device__{self.fields['device'].to_field_name}"] = data.get('device')
|
||||
if params:
|
||||
# Limit choices for parent, bridge, and LAG interfaces to the assigned device
|
||||
if device := data.get('device'):
|
||||
params = {
|
||||
f"device__{self.fields['device'].to_field_name}": device
|
||||
}
|
||||
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
|
||||
self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
|
||||
self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
|
||||
|
||||
@@ -70,10 +70,6 @@ class ConnectCableToDeviceForm(TenancyForm, NetBoxModelForm):
|
||||
'rack_id': '$termination_b_rack',
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
@@ -212,10 +208,6 @@ class ConnectCableToCircuitTerminationForm(TenancyForm, NetBoxModelForm):
|
||||
'circuit_id': '$termination_b_circuit'
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta(ConnectCableToDeviceForm.Meta):
|
||||
fields = [
|
||||
@@ -274,10 +266,6 @@ class ConnectCableToPowerFeedForm(TenancyForm, NetBoxModelForm):
|
||||
'power_panel_id': '$termination_b_powerpanel'
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta(ConnectCableToDeviceForm.Meta):
|
||||
fields = [
|
||||
|
||||
@@ -8,10 +8,10 @@ from dcim.models import *
|
||||
from extras.forms import LocalConfigContextFilterForm
|
||||
from ipam.models import ASN, VRF
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.forms import TenancyFilterForm
|
||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||
from utilities.forms import (
|
||||
APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect,
|
||||
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, SelectSpeedWidget,
|
||||
APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, MultipleChoiceField,
|
||||
StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, SelectSpeedWidget,
|
||||
)
|
||||
from wireless.choices import *
|
||||
|
||||
@@ -104,8 +104,12 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
||||
)
|
||||
|
||||
|
||||
class RegionFilterForm(NetBoxModelFilterSetForm):
|
||||
class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Region
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag', 'parent_id')),
|
||||
('Contacts', ('contact', 'contact_role'))
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -114,8 +118,12 @@ class RegionFilterForm(NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SiteGroupFilterForm(NetBoxModelFilterSetForm):
|
||||
class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = SiteGroup
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag', 'parent_id')),
|
||||
('Contacts', ('contact', 'contact_role'))
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
@@ -124,17 +132,17 @@ class SiteGroupFilterForm(NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SiteFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Site
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
('Contacts', ('contact', 'contact_role')),
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
status = MultipleChoiceField(
|
||||
choices=SiteStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple(),
|
||||
required=False
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -154,12 +162,13 @@ class SiteFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class LocationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Location
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Parent', ('region_id', 'site_group_id', 'site_id', 'parent_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
('Contacts', ('contact', 'contact_role')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -197,7 +206,7 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class RackFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Rack
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
@@ -205,6 +214,7 @@ class RackFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
('Function', ('status', 'role_id')),
|
||||
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
('Contacts', ('contact', 'contact_role')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -228,20 +238,17 @@ class RackFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
},
|
||||
label=_('Location')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
status = MultipleChoiceField(
|
||||
choices=RackStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
type = MultipleChoiceField(
|
||||
choices=RackTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
width = forms.MultipleChoiceField(
|
||||
width = MultipleChoiceField(
|
||||
choices=RackWidthChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=RackRole.objects.all(),
|
||||
@@ -308,8 +315,12 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ManufacturerFilterForm(NetBoxModelFilterSetForm):
|
||||
class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Manufacturer
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Contacts', ('contact', 'contact_role'))
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
@@ -331,15 +342,13 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
part_number = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
subdevice_role = forms.MultipleChoiceField(
|
||||
subdevice_role = MultipleChoiceField(
|
||||
choices=add_blank_choice(SubdeviceRoleChoices),
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
airflow = forms.MultipleChoiceField(
|
||||
airflow = MultipleChoiceField(
|
||||
choices=add_blank_choice(DeviceAirflowChoices),
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
console_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
@@ -465,7 +474,12 @@ class PlatformFilterForm(NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
class DeviceFilterForm(
|
||||
LocalConfigContextFilterForm,
|
||||
TenancyFilterForm,
|
||||
ContactModelFilterForm,
|
||||
NetBoxModelFilterSetForm
|
||||
):
|
||||
model = Device
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
@@ -473,6 +487,7 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
|
||||
('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
|
||||
('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
('Contacts', ('contact', 'contact_role')),
|
||||
('Components', (
|
||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
|
||||
)),
|
||||
@@ -540,15 +555,13 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
|
||||
null_option='None',
|
||||
label=_('Platform')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
status = MultipleChoiceField(
|
||||
choices=DeviceStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
airflow = forms.MultipleChoiceField(
|
||||
airflow = MultipleChoiceField(
|
||||
choices=add_blank_choice(DeviceAirflowChoices),
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
serial = forms.CharField(
|
||||
required=False
|
||||
@@ -718,15 +731,13 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
},
|
||||
label=_('Device')
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
type = MultipleChoiceField(
|
||||
choices=add_blank_choice(CableTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
status = MultipleChoiceField(
|
||||
required=False,
|
||||
choices=add_blank_choice(LinkStatusChoices),
|
||||
widget=StaticSelect()
|
||||
choices=add_blank_choice(LinkStatusChoices)
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
@@ -741,11 +752,12 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PowerPanelFilterForm(NetBoxModelFilterSetForm):
|
||||
class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = PowerPanel
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id'))
|
||||
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
||||
('Contacts', ('contact', 'contact_role')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -821,10 +833,9 @@ class PowerFeedFilterForm(NetBoxModelFilterSetForm):
|
||||
},
|
||||
label=_('Rack')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
status = MultipleChoiceField(
|
||||
choices=PowerFeedStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerFeedTypeChoices),
|
||||
@@ -864,15 +875,13 @@ class ConsolePortFilterForm(DeviceComponentFilterForm):
|
||||
('Attributes', ('name', 'label', 'type', 'speed')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
type = MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
speed = forms.MultipleChoiceField(
|
||||
speed = MultipleChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -884,15 +893,13 @@ class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
|
||||
('Attributes', ('name', 'label', 'type', 'speed')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
type = MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
speed = forms.MultipleChoiceField(
|
||||
speed = MultipleChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -904,10 +911,9 @@ class PowerPortFilterForm(DeviceComponentFilterForm):
|
||||
('Attributes', ('name', 'label', 'type')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
type = MultipleChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -919,10 +925,9 @@ class PowerOutletFilterForm(DeviceComponentFilterForm):
|
||||
('Attributes', ('name', 'label', 'type')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
type = MultipleChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -936,26 +941,22 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
kind = forms.MultipleChoiceField(
|
||||
kind = MultipleChoiceField(
|
||||
choices=InterfaceKindChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
type = MultipleChoiceField(
|
||||
choices=InterfaceTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
speed = forms.IntegerField(
|
||||
required=False,
|
||||
label='Select Speed',
|
||||
widget=SelectSpeedWidget(attrs={'readonly': None})
|
||||
)
|
||||
duplex = forms.MultipleChoiceField(
|
||||
duplex = MultipleChoiceField(
|
||||
choices=InterfaceDuplexChoices,
|
||||
required=False,
|
||||
label='Select Duplex',
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
required=False,
|
||||
@@ -977,16 +978,14 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
required=False,
|
||||
label='WWN'
|
||||
)
|
||||
rf_role = forms.MultipleChoiceField(
|
||||
rf_role = MultipleChoiceField(
|
||||
choices=WirelessRoleChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple(),
|
||||
label='Wireless role'
|
||||
)
|
||||
rf_channel = forms.MultipleChoiceField(
|
||||
rf_channel = MultipleChoiceField(
|
||||
choices=WirelessChannelChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple(),
|
||||
label='Wireless channel'
|
||||
)
|
||||
rf_channel_frequency = forms.IntegerField(
|
||||
@@ -1018,10 +1017,9 @@ class FrontPortFilterForm(DeviceComponentFilterForm):
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
model = FrontPort
|
||||
type = forms.MultipleChoiceField(
|
||||
type = MultipleChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
@@ -1036,10 +1034,9 @@ class RearPortFilterForm(DeviceComponentFilterForm):
|
||||
('Attributes', ('name', 'label', 'type', 'color')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
type = MultipleChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
|
||||
@@ -7,7 +7,6 @@ from timezone_field import TimeZoneFormField
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.models import Tag
|
||||
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
@@ -78,10 +77,6 @@ class RegionForm(NetBoxModelForm):
|
||||
required=False
|
||||
)
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
@@ -96,10 +91,6 @@ class SiteGroupForm(NetBoxModelForm):
|
||||
required=False
|
||||
)
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
@@ -129,10 +120,6 @@ class SiteForm(TenancyForm, NetBoxModelForm):
|
||||
widget=StaticSelect()
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Site', (
|
||||
@@ -204,10 +191,6 @@ class LocationForm(TenancyForm, NetBoxModelForm):
|
||||
}
|
||||
)
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Location', (
|
||||
@@ -225,10 +208,6 @@ class LocationForm(TenancyForm, NetBoxModelForm):
|
||||
|
||||
class RackRoleForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
@@ -271,10 +250,6 @@ class RackForm(TenancyForm, NetBoxModelForm):
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
@@ -344,10 +319,6 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
|
||||
),
|
||||
widget=StaticSelect()
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Reservation', ('region', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
|
||||
@@ -364,10 +335,6 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
|
||||
|
||||
class ManufacturerForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
@@ -384,10 +351,6 @@ class DeviceTypeForm(NetBoxModelForm):
|
||||
slug_source='model'
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Device Type', (
|
||||
@@ -421,10 +384,6 @@ class ModuleTypeForm(NetBoxModelForm):
|
||||
queryset=Manufacturer.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
@@ -435,10 +394,6 @@ class ModuleTypeForm(NetBoxModelForm):
|
||||
|
||||
class DeviceRoleForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
@@ -455,10 +410,6 @@ class PlatformForm(NetBoxModelForm):
|
||||
slug = SlugField(
|
||||
max_length=64
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
@@ -564,10 +515,6 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
||||
required=False,
|
||||
label=''
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
@@ -626,11 +573,6 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
||||
# can be flipped from one face to another.
|
||||
self.fields['position'].widget.add_query_param('exclude', self.instance.pk)
|
||||
|
||||
# Limit platform by manufacturer
|
||||
self.fields['platform'].queryset = Platform.objects.filter(
|
||||
Q(manufacturer__isnull=True) | Q(manufacturer=self.instance.device_type.manufacturer)
|
||||
)
|
||||
|
||||
# Disable rack assignment if this is a child device installed in a parent device
|
||||
if self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
|
||||
self.fields['site'].disabled = True
|
||||
@@ -679,10 +621,6 @@ class ModuleForm(NetBoxModelForm):
|
||||
}
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
replicate_components = forms.BooleanField(
|
||||
required=False,
|
||||
initial=True,
|
||||
@@ -713,10 +651,6 @@ class ModuleForm(NetBoxModelForm):
|
||||
|
||||
|
||||
class CableForm(TenancyForm, NetBoxModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
@@ -764,10 +698,6 @@ class PowerPanelForm(NetBoxModelForm):
|
||||
'site_id': '$site'
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Power Panel', ('region', 'site_group', 'site', 'location', 'name', 'tags')),
|
||||
@@ -820,10 +750,6 @@ class PowerFeedForm(NetBoxModelForm):
|
||||
}
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Power Panel', ('region', 'site', 'power_panel')),
|
||||
@@ -854,10 +780,6 @@ class VirtualChassisForm(NetBoxModelForm):
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
@@ -1102,10 +1024,10 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
queryset=InventoryItemTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device'
|
||||
'devicetype_id': '$device_type'
|
||||
}
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
@@ -1127,11 +1049,6 @@ class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
widget=forms.HiddenInput
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Inventory Item', ('device_type', 'parent', 'name', 'label', 'role', 'description')),
|
||||
('Hardware', ('manufacturer', 'part_id')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemTemplate
|
||||
fields = [
|
||||
@@ -1155,10 +1072,6 @@ class ConsolePortForm(NetBoxModelForm):
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
@@ -1180,10 +1093,6 @@ class ConsoleServerPortForm(NetBoxModelForm):
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
@@ -1205,10 +1114,6 @@ class PowerPortForm(NetBoxModelForm):
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
@@ -1238,10 +1143,6 @@ class PowerOutletForm(NetBoxModelForm):
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
@@ -1330,10 +1231,6 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
|
||||
required=False,
|
||||
label='VRF'
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Interface', ('device', 'module', 'name', 'type', 'speed', 'duplex', 'label', 'description', 'tags')),
|
||||
@@ -1387,10 +1284,6 @@ class FrontPortForm(NetBoxModelForm):
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
@@ -1412,10 +1305,6 @@ class RearPortForm(NetBoxModelForm):
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
@@ -1429,10 +1318,6 @@ class RearPortForm(NetBoxModelForm):
|
||||
|
||||
|
||||
class ModuleBayForm(NetBoxModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ModuleBay
|
||||
@@ -1445,10 +1330,6 @@ class ModuleBayForm(NetBoxModelForm):
|
||||
|
||||
|
||||
class DeviceBayForm(NetBoxModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
@@ -1481,6 +1362,9 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
||||
|
||||
|
||||
class InventoryItemForm(NetBoxModelForm):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all()
|
||||
)
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
required=False,
|
||||
@@ -1506,10 +1390,6 @@ class InventoryItemForm(NetBoxModelForm):
|
||||
required=False,
|
||||
widget=forms.HiddenInput
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Inventory Item', ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
|
||||
@@ -1522,9 +1402,6 @@ class InventoryItemForm(NetBoxModelForm):
|
||||
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||
'description', 'component_type', 'component_id', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
@@ -1533,10 +1410,6 @@ class InventoryItemForm(NetBoxModelForm):
|
||||
|
||||
class InventoryItemRoleForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemRole
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.models import *
|
||||
from extras.models import Tag
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from utilities.forms import (
|
||||
BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
|
||||
@@ -12,6 +11,7 @@ __all__ = (
|
||||
'DeviceComponentCreateForm',
|
||||
'FrontPortCreateForm',
|
||||
'FrontPortTemplateCreateForm',
|
||||
'InventoryItemCreateForm',
|
||||
'ModularComponentTemplateCreateForm',
|
||||
'ModuleBayCreateForm',
|
||||
'ModuleBayTemplateCreateForm',
|
||||
@@ -199,6 +199,11 @@ class ModuleBayCreateForm(DeviceComponentCreateForm):
|
||||
field_order = ('device', 'name_pattern', 'label_pattern', 'position_pattern')
|
||||
|
||||
|
||||
class InventoryItemCreateForm(ComponentCreateForm):
|
||||
# Device is assigned by the model form
|
||||
field_order = ('name_pattern', 'label_pattern')
|
||||
|
||||
|
||||
class VirtualChassisCreateForm(NetBoxModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -243,10 +248,6 @@ class VirtualChassisCreateForm(NetBoxModelForm):
|
||||
required=False,
|
||||
help_text='Position of the first member device. Increases by one for each additional member.'
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
|
||||
@@ -1,4 +1,32 @@
|
||||
import os
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.utils import DataError
|
||||
|
||||
|
||||
def check_legacy_data(apps, schema_editor):
|
||||
"""
|
||||
Abort the migration if any legacy site fields still contain data.
|
||||
"""
|
||||
Site = apps.get_model('dcim', 'Site')
|
||||
|
||||
site_count = Site.objects.exclude(asn__isnull=True).count()
|
||||
if site_count and 'NETBOX_DELETE_LEGACY_DATA' not in os.environ:
|
||||
raise DataError(
|
||||
f"Unable to proceed with deleting asn field from Site model: Found {site_count} sites with "
|
||||
f"legacy ASN data. Please ensure all legacy site ASN data has been migrated to ASN objects "
|
||||
f"before proceeding. Or, set the NETBOX_DELETE_LEGACY_DATA environment variable to bypass "
|
||||
f"this safeguard and delete all legacy site ASN data."
|
||||
)
|
||||
|
||||
site_count = Site.objects.exclude(contact_name='', contact_phone='', contact_email='').count()
|
||||
if site_count and 'NETBOX_DELETE_LEGACY_DATA' not in os.environ:
|
||||
raise DataError(
|
||||
f"Unable to proceed with deleting contact fields from Site model: Found {site_count} sites "
|
||||
f"with legacy contact data. Please ensure all legacy site contact data has been migrated to "
|
||||
f"contact objects before proceeding. Or, set the NETBOX_DELETE_LEGACY_DATA environment "
|
||||
f"variable to bypass this safeguard and delete all legacy site contact data."
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -8,6 +36,10 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=check_legacy_data,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='site',
|
||||
name='asn',
|
||||
|
||||
@@ -124,6 +124,11 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
||||
return self.name.replace('{module}', module.module_bay.position)
|
||||
return self.name
|
||||
|
||||
def resolve_label(self, module):
|
||||
if module:
|
||||
return self.label.replace('{module}', module.module_bay.position)
|
||||
return self.label
|
||||
|
||||
|
||||
class ConsolePortTemplate(ModularComponentTemplateModel):
|
||||
"""
|
||||
@@ -147,7 +152,7 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.label,
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
**kwargs
|
||||
)
|
||||
@@ -175,7 +180,7 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.label,
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
**kwargs
|
||||
)
|
||||
@@ -215,7 +220,7 @@ class PowerPortTemplate(ModularComponentTemplateModel):
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.label,
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
maximum_draw=self.maximum_draw,
|
||||
allocated_draw=self.allocated_draw,
|
||||
@@ -280,12 +285,13 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
if self.power_port:
|
||||
power_port = PowerPort.objects.get(name=self.power_port.name, **kwargs)
|
||||
power_port_name = self.power_port.resolve_name(kwargs.get('module'))
|
||||
power_port = PowerPort.objects.get(name=power_port_name, **kwargs)
|
||||
else:
|
||||
power_port = None
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.label,
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
power_port=power_port,
|
||||
feed_leg=self.feed_leg,
|
||||
@@ -325,7 +331,7 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.label,
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
mgmt_only=self.mgmt_only,
|
||||
**kwargs
|
||||
@@ -390,12 +396,13 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
if self.rear_port:
|
||||
rear_port = RearPort.objects.get(name=self.rear_port.name, **kwargs)
|
||||
rear_port_name = self.rear_port.resolve_name(kwargs.get('module'))
|
||||
rear_port = RearPort.objects.get(name=rear_port_name, **kwargs)
|
||||
else:
|
||||
rear_port = None
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.label,
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
color=self.color,
|
||||
rear_port=rear_port,
|
||||
@@ -435,7 +442,7 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.label,
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
color=self.color,
|
||||
positions=self.positions,
|
||||
@@ -549,7 +556,7 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
|
||||
unique_together = ('device_type', 'parent', 'name')
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
parent = InventoryItemTemplate.objects.get(name=self.parent.name, **kwargs) if self.parent else None
|
||||
parent = InventoryItem.objects.get(name=self.parent.name, **kwargs) if self.parent else None
|
||||
if self.component:
|
||||
model = self.component.component_model
|
||||
component = model.objects.get(name=self.component.name, **kwargs)
|
||||
|
||||
@@ -784,6 +784,10 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
|
||||
def is_lag(self):
|
||||
return self.type == InterfaceTypeChoices.TYPE_LAG
|
||||
|
||||
@property
|
||||
def is_bridge(self):
|
||||
return self.type == InterfaceTypeChoices.TYPE_BRIDGE
|
||||
|
||||
@property
|
||||
def link(self):
|
||||
return self.cable or self.wireless_link
|
||||
@@ -1066,3 +1070,12 @@ class InventoryItem(MPTTModel, ComponentModel):
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:inventoryitem', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# An InventoryItem cannot be its own parent
|
||||
if self.pk and self.parent_id == self.pk:
|
||||
raise ValidationError({
|
||||
"parent": "Cannot assign self as parent."
|
||||
})
|
||||
|
||||
@@ -257,6 +257,7 @@ class DeviceType(NetBoxModel):
|
||||
{
|
||||
'name': c.name,
|
||||
'label': c.label,
|
||||
'position': c.position,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.modulebaytemplates.all()
|
||||
@@ -374,6 +375,11 @@ class ModuleType(NetBoxModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
||||
clone_fields = ('manufacturer',)
|
||||
|
||||
class Meta:
|
||||
@@ -873,8 +879,8 @@ class Device(NetBoxModel, ConfigContextModel):
|
||||
if hasattr(self, 'device_type') and self.platform:
|
||||
if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer:
|
||||
raise ValidationError({
|
||||
'platform': "The assigned platform is limited to {} device types, but this device's type belongs "
|
||||
"to {}.".format(self.platform.manufacturer, self.device_type.manufacturer)
|
||||
'platform': f"The assigned platform is limited to {self.platform.manufacturer} device types, but "
|
||||
f"this device's type belongs to {self.device_type.manufacturer}."
|
||||
})
|
||||
|
||||
# A Device can only be assigned to a Cluster in the same Site (or no Site)
|
||||
@@ -1049,7 +1055,7 @@ class Module(NetBoxModel, ConfigContextModel):
|
||||
ordering = ('module_bay',)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.module_type)
|
||||
return f'{self.module_bay.name}: {self.module_type} ({self.pk})'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:module', args=[self.pk])
|
||||
|
||||
@@ -409,7 +409,7 @@ class Rack(NetBoxModel):
|
||||
available_units.remove(u)
|
||||
|
||||
occupied_unit_count = self.u_height - len(available_units)
|
||||
percentage = int(float(occupied_unit_count) / self.u_height * 100)
|
||||
percentage = float(occupied_unit_count) / self.u_height * 100
|
||||
|
||||
return percentage
|
||||
|
||||
|
||||
@@ -367,7 +367,7 @@ class Location(NestedGroupModel):
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
||||
clone_fields = ['site', 'parent', 'description']
|
||||
clone_fields = ['site', 'parent', 'tenant', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
|
||||
@@ -146,10 +146,10 @@ class RackElevationSVG:
|
||||
class_='device-image'
|
||||
)
|
||||
image.fit(scale='slice')
|
||||
drawing.add(image)
|
||||
drawing.add(drawing.text(get_device_name(device), insert=text, stroke='black',
|
||||
stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label'))
|
||||
drawing.add(drawing.text(get_device_name(device), insert=text, fill='white', class_='device-image-label'))
|
||||
link.add(image)
|
||||
link.add(drawing.text(get_device_name(device), insert=text, stroke='black',
|
||||
stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label'))
|
||||
link.add(drawing.text(get_device_name(device), insert=text, fill='white', class_='device-image-label'))
|
||||
|
||||
@staticmethod
|
||||
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
|
||||
|
||||
@@ -22,6 +22,12 @@ class CableTable(NetBoxTable):
|
||||
orderable=False,
|
||||
verbose_name='Side A'
|
||||
)
|
||||
rack_a = tables.Column(
|
||||
accessor=Accessor('termination_a__device__rack'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Rack A'
|
||||
)
|
||||
termination_a = tables.Column(
|
||||
accessor=Accessor('termination_a'),
|
||||
orderable=False,
|
||||
@@ -34,6 +40,12 @@ class CableTable(NetBoxTable):
|
||||
orderable=False,
|
||||
verbose_name='Side B'
|
||||
)
|
||||
rack_b = tables.Column(
|
||||
accessor=Accessor('termination_b__device__rack'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Rack B'
|
||||
)
|
||||
termination_b = tables.Column(
|
||||
accessor=Accessor('termination_b'),
|
||||
orderable=False,
|
||||
@@ -54,7 +66,7 @@ class CableTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Cable
|
||||
fields = (
|
||||
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
||||
'pk', 'id', 'label', 'termination_a_parent', 'rack_a', 'termination_a', 'termination_b_parent', 'rack_b', 'termination_b',
|
||||
'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
|
||||
@@ -190,6 +190,9 @@ class DeviceTable(NetBoxTable):
|
||||
verbose_name='VC Priority'
|
||||
)
|
||||
comments = columns.MarkdownColumn()
|
||||
contacts = tables.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:device_list'
|
||||
)
|
||||
@@ -199,8 +202,8 @@ class DeviceTable(NetBoxTable):
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
|
||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
|
||||
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'created',
|
||||
'last_updated',
|
||||
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
|
||||
@@ -676,6 +679,15 @@ class DeviceBayTable(DeviceComponentTable):
|
||||
'args': [Accessor('device_id')],
|
||||
}
|
||||
)
|
||||
device_role = columns.ColoredLabelColumn(
|
||||
accessor=Accessor('installed_device__device_role'),
|
||||
verbose_name='Role'
|
||||
)
|
||||
device_type = tables.Column(
|
||||
accessor=Accessor('installed_device__device_type'),
|
||||
linkify=True,
|
||||
verbose_name='Type'
|
||||
)
|
||||
status = tables.TemplateColumn(
|
||||
template_code=DEVICEBAY_STATUS,
|
||||
order_by=Accessor('installed_device__status')
|
||||
@@ -690,7 +702,7 @@ class DeviceBayTable(DeviceComponentTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = DeviceBay
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags',
|
||||
'pk', 'id', 'name', 'device', 'label', 'status', 'device_role', 'device_type', 'installed_device', 'description', 'tags',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
|
||||
@@ -727,13 +739,22 @@ class ModuleBayTable(DeviceComponentTable):
|
||||
linkify=True,
|
||||
verbose_name='Installed module'
|
||||
)
|
||||
module_serial = tables.Column(
|
||||
accessor=tables.A('installed_module__serial')
|
||||
)
|
||||
module_asset_tag = tables.Column(
|
||||
accessor=tables.A('installed_module__asset_tag')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:modulebay_list'
|
||||
)
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ModuleBay
|
||||
fields = ('pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'description', 'tags')
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
|
||||
'description', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'installed_module', 'description')
|
||||
|
||||
|
||||
@@ -744,7 +765,10 @@ class DeviceModuleBayTable(ModuleBayTable):
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ModuleBay
|
||||
fields = ('pk', 'id', 'name', 'label', 'position', 'installed_module', 'description', 'tags', 'actions')
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
|
||||
'description', 'tags', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'label', 'installed_module', 'description')
|
||||
|
||||
|
||||
@@ -760,7 +784,6 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
linkify=True
|
||||
)
|
||||
component = tables.Column(
|
||||
accessor=Accessor('component'),
|
||||
orderable=False,
|
||||
linkify=True
|
||||
)
|
||||
@@ -788,7 +811,6 @@ class DeviceInventoryItemTable(InventoryItemTable):
|
||||
order_by=Accessor('_name'),
|
||||
attrs={'td': {'class': 'text-nowrap'}}
|
||||
)
|
||||
actions = columns.ActionsColumn()
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = InventoryItem
|
||||
|
||||
@@ -41,6 +41,9 @@ class ManufacturerTable(NetBoxTable):
|
||||
verbose_name='Platforms'
|
||||
)
|
||||
slug = tables.Column()
|
||||
contacts = tables.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:manufacturer_list'
|
||||
)
|
||||
@@ -49,7 +52,7 @@ class ManufacturerTable(NetBoxTable):
|
||||
model = Manufacturer
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
|
||||
'actions', 'created', 'last_updated',
|
||||
'contacts', 'actions', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
|
||||
@@ -110,7 +113,7 @@ class ComponentTemplateTable(NetBoxTable):
|
||||
|
||||
class ConsolePortTemplateTable(ComponentTemplateTable):
|
||||
actions = columns.ActionsColumn(
|
||||
sequence=('edit', 'delete'),
|
||||
actions=('edit', 'delete'),
|
||||
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||
)
|
||||
|
||||
@@ -122,7 +125,7 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
||||
actions = columns.ActionsColumn(
|
||||
sequence=('edit', 'delete'),
|
||||
actions=('edit', 'delete'),
|
||||
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||
)
|
||||
|
||||
@@ -134,7 +137,7 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class PowerPortTemplateTable(ComponentTemplateTable):
|
||||
actions = columns.ActionsColumn(
|
||||
sequence=('edit', 'delete'),
|
||||
actions=('edit', 'delete'),
|
||||
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||
)
|
||||
|
||||
@@ -146,7 +149,7 @@ class PowerPortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class PowerOutletTemplateTable(ComponentTemplateTable):
|
||||
actions = columns.ActionsColumn(
|
||||
sequence=('edit', 'delete'),
|
||||
actions=('edit', 'delete'),
|
||||
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||
)
|
||||
|
||||
@@ -161,7 +164,7 @@ class InterfaceTemplateTable(ComponentTemplateTable):
|
||||
verbose_name='Management Only'
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
sequence=('edit', 'delete'),
|
||||
actions=('edit', 'delete'),
|
||||
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||
)
|
||||
|
||||
@@ -177,7 +180,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
||||
)
|
||||
color = columns.ColorColumn()
|
||||
actions = columns.ActionsColumn(
|
||||
sequence=('edit', 'delete'),
|
||||
actions=('edit', 'delete'),
|
||||
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||
)
|
||||
|
||||
@@ -190,7 +193,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
||||
class RearPortTemplateTable(ComponentTemplateTable):
|
||||
color = columns.ColorColumn()
|
||||
actions = columns.ActionsColumn(
|
||||
sequence=('edit', 'delete'),
|
||||
actions=('edit', 'delete'),
|
||||
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||
)
|
||||
|
||||
@@ -202,7 +205,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class ModuleBayTemplateTable(ComponentTemplateTable):
|
||||
actions = columns.ActionsColumn(
|
||||
sequence=('edit', 'delete')
|
||||
actions=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
@@ -213,7 +216,7 @@ class ModuleBayTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class DeviceBayTemplateTable(ComponentTemplateTable):
|
||||
actions = columns.ActionsColumn(
|
||||
sequence=('edit', 'delete')
|
||||
actions=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
@@ -224,7 +227,7 @@ class DeviceBayTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class InventoryItemTemplateTable(ComponentTemplateTable):
|
||||
actions = columns.ActionsColumn(
|
||||
sequence=('edit', 'delete')
|
||||
actions=('edit', 'delete')
|
||||
)
|
||||
role = tables.Column(
|
||||
linkify=True
|
||||
@@ -238,5 +241,7 @@ class InventoryItemTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = InventoryItemTemplate
|
||||
fields = ('pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'component', 'description', 'actions')
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'parent', 'role', 'manufacturer', 'part_id', 'component', 'description', 'actions',
|
||||
)
|
||||
empty_text = "None"
|
||||
|
||||
@@ -26,13 +26,16 @@ class PowerPanelTable(NetBoxTable):
|
||||
url_params={'power_panel_id': 'pk'},
|
||||
verbose_name='Feeds'
|
||||
)
|
||||
contacts = tables.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:powerpanel_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = PowerPanel
|
||||
fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'tags', 'created', 'last_updated',)
|
||||
fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated',)
|
||||
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
|
||||
|
||||
|
||||
|
||||
@@ -69,6 +69,9 @@ class RackTable(NetBoxTable):
|
||||
orderable=False,
|
||||
verbose_name='Power'
|
||||
)
|
||||
contacts = tables.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:rack_list'
|
||||
)
|
||||
@@ -86,7 +89,7 @@ class RackTable(NetBoxTable):
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag',
|
||||
'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization',
|
||||
'get_power_utilization', 'tags', 'created', 'last_updated',
|
||||
'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
|
||||
|
||||
@@ -26,6 +26,9 @@ class RegionTable(NetBoxTable):
|
||||
url_params={'region_id': 'pk'},
|
||||
verbose_name='Sites'
|
||||
)
|
||||
contacts = tables.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:region_list'
|
||||
)
|
||||
@@ -33,7 +36,8 @@ class RegionTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Region
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'created', 'last_updated', 'actions',
|
||||
'pk', 'id', 'name', 'slug', 'site_count', 'description', 'contacts', 'tags', 'created', 'last_updated',
|
||||
'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'site_count', 'description')
|
||||
|
||||
@@ -51,6 +55,9 @@ class SiteGroupTable(NetBoxTable):
|
||||
url_params={'group_id': 'pk'},
|
||||
verbose_name='Sites'
|
||||
)
|
||||
contacts = tables.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:sitegroup_list'
|
||||
)
|
||||
@@ -58,7 +65,8 @@ class SiteGroupTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SiteGroup
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'created', 'last_updated', 'actions',
|
||||
'pk', 'id', 'name', 'slug', 'site_count', 'description', 'contacts', 'tags', 'created', 'last_updated',
|
||||
'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'site_count', 'description')
|
||||
|
||||
@@ -78,18 +86,21 @@ class SiteTable(NetBoxTable):
|
||||
group = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
asns = tables.ManyToManyColumn(
|
||||
linkify_item=True,
|
||||
verbose_name='ASNs'
|
||||
)
|
||||
asn_count = columns.LinkedCountColumn(
|
||||
accessor=tables.A('asns__count'),
|
||||
viewname='ipam:asn_list',
|
||||
url_params={'site_id': 'pk'},
|
||||
verbose_name='ASN Count'
|
||||
)
|
||||
asns = tables.ManyToManyColumn(
|
||||
linkify_item=True,
|
||||
verbose_name='ASNs'
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
comments = columns.MarkdownColumn()
|
||||
contacts = tables.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:site_list'
|
||||
)
|
||||
@@ -99,7 +110,7 @@ class SiteTable(NetBoxTable):
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count',
|
||||
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments',
|
||||
'tags', 'created', 'last_updated', 'actions',
|
||||
'contacts', 'tags', 'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')
|
||||
|
||||
@@ -126,6 +137,9 @@ class LocationTable(NetBoxTable):
|
||||
url_params={'location_id': 'pk'},
|
||||
verbose_name='Devices'
|
||||
)
|
||||
contacts = tables.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:location_list'
|
||||
)
|
||||
@@ -136,7 +150,7 @@ class LocationTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Location
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags',
|
||||
'actions', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'contacts',
|
||||
'tags', 'actions', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description')
|
||||
|
||||
@@ -14,7 +14,7 @@ from django.views.generic import View
|
||||
|
||||
from circuits.models import Circuit
|
||||
from extras.views import ObjectConfigContextView
|
||||
from ipam.models import ASN, IPAddress, Prefix, Service, VLAN
|
||||
from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup
|
||||
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
|
||||
from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm
|
||||
@@ -177,7 +177,7 @@ class RegionView(generic.ObjectView):
|
||||
|
||||
class RegionEditView(generic.ObjectEditView):
|
||||
queryset = Region.objects.all()
|
||||
model_form = forms.RegionForm
|
||||
form = forms.RegionForm
|
||||
|
||||
|
||||
class RegionDeleteView(generic.ObjectDeleteView):
|
||||
@@ -262,7 +262,7 @@ class SiteGroupView(generic.ObjectView):
|
||||
|
||||
class SiteGroupEditView(generic.ObjectEditView):
|
||||
queryset = SiteGroup.objects.all()
|
||||
model_form = forms.SiteGroupForm
|
||||
form = forms.SiteGroupForm
|
||||
|
||||
|
||||
class SiteGroupDeleteView(generic.ObjectDeleteView):
|
||||
@@ -320,6 +320,10 @@ class SiteView(generic.ObjectView):
|
||||
'rack_count': Rack.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'device_count': Device.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'prefix_count': Prefix.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'vlangroup_count': VLANGroup.objects.restrict(request.user, 'view').filter(
|
||||
scope_type=ContentType.objects.get_for_model(Site),
|
||||
scope_id=instance.pk
|
||||
).count(),
|
||||
'vlan_count': VLAN.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).count(),
|
||||
'vm_count': VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=instance).count(),
|
||||
@@ -339,6 +343,12 @@ class SiteView(generic.ObjectView):
|
||||
cumulative=True
|
||||
).restrict(request.user, 'view').filter(site=instance)
|
||||
|
||||
nonracked_devices = Device.objects.filter(
|
||||
site=instance,
|
||||
position__isnull=True,
|
||||
parent_bay__isnull=True
|
||||
).prefetch_related('device_type__manufacturer')
|
||||
|
||||
asns = ASN.objects.restrict(request.user, 'view').filter(sites=instance)
|
||||
asn_count = asns.count()
|
||||
|
||||
@@ -348,12 +358,14 @@ class SiteView(generic.ObjectView):
|
||||
'stats': stats,
|
||||
'locations': locations,
|
||||
'asns': asns,
|
||||
'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
|
||||
'total_nonracked_devices_count': nonracked_devices.count(),
|
||||
}
|
||||
|
||||
|
||||
class SiteEditView(generic.ObjectEditView):
|
||||
queryset = Site.objects.all()
|
||||
model_form = forms.SiteForm
|
||||
form = forms.SiteForm
|
||||
|
||||
|
||||
class SiteDeleteView(generic.ObjectDeleteView):
|
||||
@@ -426,16 +438,24 @@ class LocationView(generic.ObjectView):
|
||||
child_locations_table = tables.LocationTable(child_locations)
|
||||
child_locations_table.configure(request)
|
||||
|
||||
nonracked_devices = Device.objects.filter(
|
||||
location=instance,
|
||||
position__isnull=True,
|
||||
parent_bay__isnull=True
|
||||
).prefetch_related('device_type__manufacturer')
|
||||
|
||||
return {
|
||||
'rack_count': rack_count,
|
||||
'device_count': device_count,
|
||||
'child_locations_table': child_locations_table,
|
||||
'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
|
||||
'total_nonracked_devices_count': nonracked_devices.count(),
|
||||
}
|
||||
|
||||
|
||||
class LocationEditView(generic.ObjectEditView):
|
||||
queryset = Location.objects.all()
|
||||
model_form = forms.LocationForm
|
||||
form = forms.LocationForm
|
||||
|
||||
|
||||
class LocationDeleteView(generic.ObjectDeleteView):
|
||||
@@ -504,7 +524,7 @@ class RackRoleView(generic.ObjectView):
|
||||
|
||||
class RackRoleEditView(generic.ObjectEditView):
|
||||
queryset = RackRole.objects.all()
|
||||
model_form = forms.RackRoleForm
|
||||
form = forms.RackRoleForm
|
||||
|
||||
|
||||
class RackRoleDeleteView(generic.ObjectDeleteView):
|
||||
@@ -607,8 +627,8 @@ class RackView(generic.ObjectView):
|
||||
peer_racks = peer_racks.filter(location=instance.location)
|
||||
else:
|
||||
peer_racks = peer_racks.filter(location__isnull=True)
|
||||
next_rack = peer_racks.filter(name__gt=instance.name).order_by('name').first()
|
||||
prev_rack = peer_racks.filter(name__lt=instance.name).order_by('-name').first()
|
||||
next_rack = peer_racks.filter(_name__gt=instance._name).first()
|
||||
prev_rack = peer_racks.filter(_name__lt=instance._name).reverse().first()
|
||||
|
||||
reservations = RackReservation.objects.restrict(request.user, 'view').filter(rack=instance)
|
||||
power_feeds = PowerFeed.objects.restrict(request.user, 'view').filter(rack=instance).prefetch_related(
|
||||
@@ -629,7 +649,7 @@ class RackView(generic.ObjectView):
|
||||
|
||||
class RackEditView(generic.ObjectEditView):
|
||||
queryset = Rack.objects.all()
|
||||
model_form = forms.RackForm
|
||||
form = forms.RackForm
|
||||
template_name = 'dcim/rack_edit.html'
|
||||
|
||||
|
||||
@@ -673,7 +693,7 @@ class RackReservationView(generic.ObjectView):
|
||||
|
||||
class RackReservationEditView(generic.ObjectEditView):
|
||||
queryset = RackReservation.objects.all()
|
||||
model_form = forms.RackReservationForm
|
||||
form = forms.RackReservationForm
|
||||
|
||||
def alter_object(self, obj, request, args, kwargs):
|
||||
if not obj.pk:
|
||||
@@ -759,7 +779,7 @@ class ManufacturerView(generic.ObjectView):
|
||||
|
||||
class ManufacturerEditView(generic.ObjectEditView):
|
||||
queryset = Manufacturer.objects.all()
|
||||
model_form = forms.ManufacturerForm
|
||||
form = forms.ManufacturerForm
|
||||
|
||||
|
||||
class ManufacturerDeleteView(generic.ObjectDeleteView):
|
||||
@@ -884,7 +904,7 @@ class DeviceTypeInventoryItemsView(DeviceTypeComponentsView):
|
||||
|
||||
class DeviceTypeEditView(generic.ObjectEditView):
|
||||
queryset = DeviceType.objects.all()
|
||||
model_form = forms.DeviceTypeForm
|
||||
form = forms.DeviceTypeForm
|
||||
|
||||
|
||||
class DeviceTypeDeleteView(generic.ObjectDeleteView):
|
||||
@@ -948,7 +968,7 @@ class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ModuleTypeListView(generic.ObjectListView):
|
||||
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||
# instance_count=count_related(Module, 'module_type')
|
||||
instance_count=count_related(Module, 'module_type')
|
||||
)
|
||||
filterset = filtersets.ModuleTypeFilterSet
|
||||
filterset_form = forms.ModuleTypeFilterForm
|
||||
@@ -1017,7 +1037,7 @@ class ModuleTypeRearPortsView(ModuleTypeComponentsView):
|
||||
|
||||
class ModuleTypeEditView(generic.ObjectEditView):
|
||||
queryset = ModuleType.objects.all()
|
||||
model_form = forms.ModuleTypeForm
|
||||
form = forms.ModuleTypeForm
|
||||
|
||||
|
||||
class ModuleTypeDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1054,7 +1074,7 @@ class ModuleTypeImportView(generic.ObjectImportView):
|
||||
|
||||
class ModuleTypeBulkEditView(generic.BulkEditView):
|
||||
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||
# instance_count=count_related(Module, 'module_type')
|
||||
instance_count=count_related(Module, 'module_type')
|
||||
)
|
||||
filterset = filtersets.ModuleTypeFilterSet
|
||||
table = tables.ModuleTypeTable
|
||||
@@ -1063,7 +1083,7 @@ class ModuleTypeBulkEditView(generic.BulkEditView):
|
||||
|
||||
class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||
# instance_count=count_related(Module, 'module_type')
|
||||
instance_count=count_related(Module, 'module_type')
|
||||
)
|
||||
filterset = filtersets.ModuleTypeFilterSet
|
||||
table = tables.ModuleTypeTable
|
||||
@@ -1082,7 +1102,7 @@ class ConsolePortTemplateCreateView(generic.ComponentCreateView):
|
||||
|
||||
class ConsolePortTemplateEditView(generic.ObjectEditView):
|
||||
queryset = ConsolePortTemplate.objects.all()
|
||||
model_form = forms.ConsolePortTemplateForm
|
||||
form = forms.ConsolePortTemplateForm
|
||||
|
||||
|
||||
class ConsolePortTemplateDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1117,7 +1137,7 @@ class ConsoleServerPortTemplateCreateView(generic.ComponentCreateView):
|
||||
|
||||
class ConsoleServerPortTemplateEditView(generic.ObjectEditView):
|
||||
queryset = ConsoleServerPortTemplate.objects.all()
|
||||
model_form = forms.ConsoleServerPortTemplateForm
|
||||
form = forms.ConsoleServerPortTemplateForm
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1152,7 +1172,7 @@ class PowerPortTemplateCreateView(generic.ComponentCreateView):
|
||||
|
||||
class PowerPortTemplateEditView(generic.ObjectEditView):
|
||||
queryset = PowerPortTemplate.objects.all()
|
||||
model_form = forms.PowerPortTemplateForm
|
||||
form = forms.PowerPortTemplateForm
|
||||
|
||||
|
||||
class PowerPortTemplateDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1187,7 +1207,7 @@ class PowerOutletTemplateCreateView(generic.ComponentCreateView):
|
||||
|
||||
class PowerOutletTemplateEditView(generic.ObjectEditView):
|
||||
queryset = PowerOutletTemplate.objects.all()
|
||||
model_form = forms.PowerOutletTemplateForm
|
||||
form = forms.PowerOutletTemplateForm
|
||||
|
||||
|
||||
class PowerOutletTemplateDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1222,7 +1242,7 @@ class InterfaceTemplateCreateView(generic.ComponentCreateView):
|
||||
|
||||
class InterfaceTemplateEditView(generic.ObjectEditView):
|
||||
queryset = InterfaceTemplate.objects.all()
|
||||
model_form = forms.InterfaceTemplateForm
|
||||
form = forms.InterfaceTemplateForm
|
||||
|
||||
|
||||
class InterfaceTemplateDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1265,7 +1285,7 @@ class FrontPortTemplateCreateView(generic.ComponentCreateView):
|
||||
|
||||
class FrontPortTemplateEditView(generic.ObjectEditView):
|
||||
queryset = FrontPortTemplate.objects.all()
|
||||
model_form = forms.FrontPortTemplateForm
|
||||
form = forms.FrontPortTemplateForm
|
||||
|
||||
|
||||
class FrontPortTemplateDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1300,7 +1320,7 @@ class RearPortTemplateCreateView(generic.ComponentCreateView):
|
||||
|
||||
class RearPortTemplateEditView(generic.ObjectEditView):
|
||||
queryset = RearPortTemplate.objects.all()
|
||||
model_form = forms.RearPortTemplateForm
|
||||
form = forms.RearPortTemplateForm
|
||||
|
||||
|
||||
class RearPortTemplateDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1336,7 +1356,7 @@ class ModuleBayTemplateCreateView(generic.ComponentCreateView):
|
||||
|
||||
class ModuleBayTemplateEditView(generic.ObjectEditView):
|
||||
queryset = ModuleBayTemplate.objects.all()
|
||||
model_form = forms.ModuleBayTemplateForm
|
||||
form = forms.ModuleBayTemplateForm
|
||||
|
||||
|
||||
class ModuleBayTemplateDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1371,7 +1391,7 @@ class DeviceBayTemplateCreateView(generic.ComponentCreateView):
|
||||
|
||||
class DeviceBayTemplateEditView(generic.ObjectEditView):
|
||||
queryset = DeviceBayTemplate.objects.all()
|
||||
model_form = forms.DeviceBayTemplateForm
|
||||
form = forms.DeviceBayTemplateForm
|
||||
|
||||
|
||||
class DeviceBayTemplateDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1417,7 +1437,7 @@ class InventoryItemTemplateCreateView(generic.ComponentCreateView):
|
||||
|
||||
class InventoryItemTemplateEditView(generic.ObjectEditView):
|
||||
queryset = InventoryItemTemplate.objects.all()
|
||||
model_form = forms.InventoryItemTemplateForm
|
||||
form = forms.InventoryItemTemplateForm
|
||||
|
||||
|
||||
class InventoryItemTemplateDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1472,7 +1492,7 @@ class DeviceRoleView(generic.ObjectView):
|
||||
|
||||
class DeviceRoleEditView(generic.ObjectEditView):
|
||||
queryset = DeviceRole.objects.all()
|
||||
model_form = forms.DeviceRoleForm
|
||||
form = forms.DeviceRoleForm
|
||||
|
||||
|
||||
class DeviceRoleDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1535,7 +1555,7 @@ class PlatformView(generic.ObjectView):
|
||||
|
||||
class PlatformEditView(generic.ObjectEditView):
|
||||
queryset = Platform.objects.all()
|
||||
model_form = forms.PlatformForm
|
||||
form = forms.PlatformForm
|
||||
|
||||
|
||||
class PlatformDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1718,7 +1738,7 @@ class DeviceConfigContextView(ObjectConfigContextView):
|
||||
|
||||
class DeviceEditView(generic.ObjectEditView):
|
||||
queryset = Device.objects.all()
|
||||
model_form = forms.DeviceForm
|
||||
form = forms.DeviceForm
|
||||
template_name = 'dcim/device_edit.html'
|
||||
|
||||
|
||||
@@ -1781,7 +1801,7 @@ class ModuleView(generic.ObjectView):
|
||||
|
||||
class ModuleEditView(generic.ObjectEditView):
|
||||
queryset = Module.objects.all()
|
||||
model_form = forms.ModuleForm
|
||||
form = forms.ModuleForm
|
||||
|
||||
|
||||
class ModuleDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1831,7 +1851,7 @@ class ConsolePortCreateView(generic.ComponentCreateView):
|
||||
|
||||
class ConsolePortEditView(generic.ObjectEditView):
|
||||
queryset = ConsolePort.objects.all()
|
||||
model_form = forms.ConsolePortForm
|
||||
form = forms.ConsolePortForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
@@ -1890,7 +1910,7 @@ class ConsoleServerPortCreateView(generic.ComponentCreateView):
|
||||
|
||||
class ConsoleServerPortEditView(generic.ObjectEditView):
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
model_form = forms.ConsoleServerPortForm
|
||||
form = forms.ConsoleServerPortForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
@@ -1949,7 +1969,7 @@ class PowerPortCreateView(generic.ComponentCreateView):
|
||||
|
||||
class PowerPortEditView(generic.ObjectEditView):
|
||||
queryset = PowerPort.objects.all()
|
||||
model_form = forms.PowerPortForm
|
||||
form = forms.PowerPortForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
@@ -2008,7 +2028,7 @@ class PowerOutletCreateView(generic.ComponentCreateView):
|
||||
|
||||
class PowerOutletEditView(generic.ObjectEditView):
|
||||
queryset = PowerOutlet.objects.all()
|
||||
model_form = forms.PowerOutletForm
|
||||
form = forms.PowerOutletForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
@@ -2065,6 +2085,14 @@ class InterfaceView(generic.ObjectView):
|
||||
orderable=False
|
||||
)
|
||||
|
||||
# Get bridge interfaces
|
||||
bridge_interfaces = Interface.objects.restrict(request.user, 'view').filter(bridge=instance)
|
||||
bridge_interfaces_tables = tables.InterfaceTable(
|
||||
bridge_interfaces,
|
||||
exclude=('device', 'parent'),
|
||||
orderable=False
|
||||
)
|
||||
|
||||
# Get child interfaces
|
||||
child_interfaces = Interface.objects.restrict(request.user, 'view').filter(parent=instance)
|
||||
child_interfaces_tables = tables.InterfaceTable(
|
||||
@@ -2089,6 +2117,7 @@ class InterfaceView(generic.ObjectView):
|
||||
|
||||
return {
|
||||
'ipaddress_table': ipaddress_table,
|
||||
'bridge_interfaces_table': bridge_interfaces_tables,
|
||||
'child_interfaces_table': child_interfaces_tables,
|
||||
'vlan_table': vlan_table,
|
||||
}
|
||||
@@ -2130,7 +2159,7 @@ class InterfaceCreateView(generic.ComponentCreateView):
|
||||
|
||||
class InterfaceEditView(generic.ObjectEditView):
|
||||
queryset = Interface.objects.all()
|
||||
model_form = forms.InterfaceForm
|
||||
form = forms.InterfaceForm
|
||||
template_name = 'dcim/interface_edit.html'
|
||||
|
||||
|
||||
@@ -2197,7 +2226,7 @@ class FrontPortCreateView(generic.ComponentCreateView):
|
||||
|
||||
class FrontPortEditView(generic.ObjectEditView):
|
||||
queryset = FrontPort.objects.all()
|
||||
model_form = forms.FrontPortForm
|
||||
form = forms.FrontPortForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
@@ -2256,7 +2285,7 @@ class RearPortCreateView(generic.ComponentCreateView):
|
||||
|
||||
class RearPortEditView(generic.ObjectEditView):
|
||||
queryset = RearPort.objects.all()
|
||||
model_form = forms.RearPortForm
|
||||
form = forms.RearPortForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
@@ -2316,7 +2345,7 @@ class ModuleBayCreateView(generic.ComponentCreateView):
|
||||
|
||||
class ModuleBayEditView(generic.ObjectEditView):
|
||||
queryset = ModuleBay.objects.all()
|
||||
model_form = forms.ModuleBayForm
|
||||
form = forms.ModuleBayForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
@@ -2371,7 +2400,7 @@ class DeviceBayCreateView(generic.ComponentCreateView):
|
||||
|
||||
class DeviceBayEditView(generic.ObjectEditView):
|
||||
queryset = DeviceBay.objects.all()
|
||||
model_form = forms.DeviceBayForm
|
||||
form = forms.DeviceBayForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
@@ -2487,12 +2516,12 @@ class InventoryItemView(generic.ObjectView):
|
||||
|
||||
class InventoryItemEditView(generic.ObjectEditView):
|
||||
queryset = InventoryItem.objects.all()
|
||||
model_form = forms.InventoryItemForm
|
||||
form = forms.InventoryItemForm
|
||||
|
||||
|
||||
class InventoryItemCreateView(generic.ComponentCreateView):
|
||||
queryset = InventoryItem.objects.all()
|
||||
form = forms.DeviceComponentCreateForm
|
||||
form = forms.InventoryItemCreateForm
|
||||
model_form = forms.InventoryItemForm
|
||||
template_name = 'dcim/inventoryitem_create.html'
|
||||
|
||||
@@ -2559,7 +2588,7 @@ class InventoryItemRoleView(generic.ObjectView):
|
||||
|
||||
class InventoryItemRoleEditView(generic.ObjectEditView):
|
||||
queryset = InventoryItemRole.objects.all()
|
||||
model_form = forms.InventoryItemRoleForm
|
||||
form = forms.InventoryItemRoleForm
|
||||
|
||||
|
||||
class InventoryItemRoleDeleteView(generic.ObjectDeleteView):
|
||||
@@ -2779,8 +2808,8 @@ class CableCreateView(generic.ObjectEditView):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
|
||||
# Set the model_form class based on the type of component being connected
|
||||
self.model_form = {
|
||||
# Set the form class based on the type of component being connected
|
||||
self.form = {
|
||||
'console-port': forms.ConnectCableToConsolePortForm,
|
||||
'console-server-port': forms.ConnectCableToConsoleServerPortForm,
|
||||
'power-port': forms.ConnectCableToPowerPortForm,
|
||||
@@ -2828,7 +2857,7 @@ class CableCreateView(generic.ObjectEditView):
|
||||
if 'termination_b_rack' not in initial_data:
|
||||
initial_data['termination_b_rack'] = getattr(obj.termination_a.parent_object, 'rack', None)
|
||||
|
||||
form = self.model_form(instance=obj, initial=initial_data)
|
||||
form = self.form(instance=obj, initial=initial_data)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'obj': obj,
|
||||
@@ -2841,7 +2870,7 @@ class CableCreateView(generic.ObjectEditView):
|
||||
|
||||
class CableEditView(generic.ObjectEditView):
|
||||
queryset = Cable.objects.all()
|
||||
model_form = forms.CableForm
|
||||
form = forms.CableForm
|
||||
template_name = 'dcim/cable_edit.html'
|
||||
|
||||
|
||||
@@ -2940,7 +2969,7 @@ class VirtualChassisView(generic.ObjectView):
|
||||
|
||||
class VirtualChassisCreateView(generic.ObjectEditView):
|
||||
queryset = VirtualChassis.objects.all()
|
||||
model_form = forms.VirtualChassisCreateForm
|
||||
form = forms.VirtualChassisCreateForm
|
||||
template_name = 'dcim/virtualchassis_add.html'
|
||||
|
||||
|
||||
@@ -3177,7 +3206,7 @@ class PowerPanelView(generic.ObjectView):
|
||||
|
||||
class PowerPanelEditView(generic.ObjectEditView):
|
||||
queryset = PowerPanel.objects.all()
|
||||
model_form = forms.PowerPanelForm
|
||||
form = forms.PowerPanelForm
|
||||
|
||||
|
||||
class PowerPanelDeleteView(generic.ObjectDeleteView):
|
||||
@@ -3224,7 +3253,7 @@ class PowerFeedView(generic.ObjectView):
|
||||
|
||||
class PowerFeedEditView(generic.ObjectEditView):
|
||||
queryset = PowerFeed.objects.all()
|
||||
model_form = forms.PowerFeedForm
|
||||
form = forms.PowerFeedForm
|
||||
|
||||
|
||||
class PowerFeedDeleteView(generic.ObjectDeleteView):
|
||||
|
||||
@@ -23,21 +23,24 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
|
||||
}),
|
||||
('Banners', {
|
||||
'fields': ('BANNER_LOGIN', 'BANNER_TOP', 'BANNER_BOTTOM'),
|
||||
'classes': ('monospace',),
|
||||
}),
|
||||
('Pagination', {
|
||||
'fields': ('PAGINATE_COUNT', 'MAX_PAGE_SIZE'),
|
||||
}),
|
||||
('Validation', {
|
||||
'fields': ('CUSTOM_VALIDATORS',),
|
||||
'classes': ('monospace',),
|
||||
}),
|
||||
('NAPALM', {
|
||||
'fields': ('NAPALM_USERNAME', 'NAPALM_PASSWORD', 'NAPALM_TIMEOUT', 'NAPALM_ARGS'),
|
||||
'classes': ('monospace',),
|
||||
}),
|
||||
('User Preferences', {
|
||||
'fields': ('DEFAULT_USER_PREFERENCES',),
|
||||
}),
|
||||
('Miscellaneous', {
|
||||
'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'MAPS_URL'),
|
||||
'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOBRESULT_RETENTION', 'MAPS_URL'),
|
||||
}),
|
||||
('Config Revision', {
|
||||
'fields': ('comment',),
|
||||
|
||||
@@ -14,7 +14,7 @@ from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||
from netbox.api.exceptions import SerializerNotFound
|
||||
from netbox.api.serializers import BaseModelSerializer, ValidatedModelSerializer
|
||||
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
@@ -78,15 +78,19 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
||||
many=True
|
||||
)
|
||||
type = ChoiceField(choices=CustomFieldTypeChoices)
|
||||
object_type = ContentTypeField(
|
||||
queryset=ContentType.objects.all(),
|
||||
required=False
|
||||
)
|
||||
filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
|
||||
data_type = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = [
|
||||
'id', 'url', 'display', 'content_types', 'type', 'data_type', 'name', 'label', 'description', 'required',
|
||||
'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
|
||||
'choices', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'description',
|
||||
'required', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum',
|
||||
'validation_regex', 'choices', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def get_data_type(self, obj):
|
||||
@@ -196,7 +200,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
# Journal entries
|
||||
#
|
||||
|
||||
class JournalEntrySerializer(ValidatedModelSerializer):
|
||||
class JournalEntrySerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
|
||||
assigned_object_type = ContentTypeField(
|
||||
queryset=ContentType.objects.all()
|
||||
@@ -217,7 +221,7 @@ class JournalEntrySerializer(ValidatedModelSerializer):
|
||||
model = JournalEntry
|
||||
fields = [
|
||||
'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
|
||||
'created_by', 'kind', 'comments',
|
||||
'created_by', 'kind', 'comments', 'tags', 'custom_fields',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from netbox.api import OrderedDefaultRouter
|
||||
from netbox.api import NetBoxRouter
|
||||
from . import views
|
||||
|
||||
|
||||
router = OrderedDefaultRouter()
|
||||
router = NetBoxRouter()
|
||||
router.APIRootView = views.ExtrasRootView
|
||||
|
||||
# Webhooks
|
||||
|
||||
@@ -179,7 +179,7 @@ class ReportViewSet(ViewSet):
|
||||
for r in JobResult.objects.filter(
|
||||
obj_type=report_content_type,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).defer('data')
|
||||
).order_by('name', '-created').distinct('name').defer('data')
|
||||
}
|
||||
|
||||
# Iterate through all available Reports.
|
||||
@@ -236,7 +236,8 @@ class ReportViewSet(ViewSet):
|
||||
run_report,
|
||||
report.full_name,
|
||||
report_content_type,
|
||||
request.user
|
||||
request.user,
|
||||
job_timeout=report.job_timeout
|
||||
)
|
||||
report.result = job_result
|
||||
|
||||
@@ -270,7 +271,7 @@ class ScriptViewSet(ViewSet):
|
||||
for r in JobResult.objects.filter(
|
||||
obj_type=script_content_type,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).defer('data').order_by('created')
|
||||
).order_by('name', '-created').distinct('name').defer('data')
|
||||
}
|
||||
|
||||
flat_list = []
|
||||
@@ -320,7 +321,8 @@ class ScriptViewSet(ViewSet):
|
||||
request.user,
|
||||
data=data,
|
||||
request=copy_safe_request(request),
|
||||
commit=commit
|
||||
commit=commit,
|
||||
job_timeout=script.job_timeout,
|
||||
)
|
||||
script.result = job_result
|
||||
serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
|
||||
|
||||
@@ -83,18 +83,19 @@ class ObjectChangeActionChoices(ChoiceSet):
|
||||
#
|
||||
|
||||
class JournalEntryKindChoices(ChoiceSet):
|
||||
key = 'JournalEntry.kind'
|
||||
|
||||
KIND_INFO = 'info'
|
||||
KIND_SUCCESS = 'success'
|
||||
KIND_WARNING = 'warning'
|
||||
KIND_DANGER = 'danger'
|
||||
|
||||
CHOICES = (
|
||||
CHOICES = [
|
||||
(KIND_INFO, 'Info', 'cyan'),
|
||||
(KIND_SUCCESS, 'Success', 'green'),
|
||||
(KIND_WARNING, 'Warning', 'yellow'),
|
||||
(KIND_DANGER, 'Danger', 'red'),
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
|
||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet
|
||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
@@ -134,11 +134,7 @@ class ImageAttachmentFilterSet(BaseFilterSet):
|
||||
return queryset.filter(name__icontains=value)
|
||||
|
||||
|
||||
class JournalEntryFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class JournalEntryFilterSet(NetBoxModelFilterSet):
|
||||
created = django_filters.DateTimeFromToRangeFilter()
|
||||
assigned_object_type = ContentTypeFilter()
|
||||
created_by_id = django_filters.ModelMultipleChoiceFilter(
|
||||
|
||||
@@ -7,10 +7,12 @@ from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGrou
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.forms.base import NetBoxModelFilterSetForm
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, APISelectMultiple, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DateTimePicker,
|
||||
DynamicModelMultipleChoiceField, FilterForm, StaticSelect, StaticSelectMultiple, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
add_blank_choice, APISelectMultiple, BOOLEAN_WITH_BLANK_CHOICES, ContentTypeChoiceField,
|
||||
ContentTypeMultipleChoiceField, DateTimePicker, DynamicModelMultipleChoiceField, FilterForm, MultipleChoiceField,
|
||||
StaticSelect, TagFilterField,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
||||
@@ -37,10 +39,9 @@ class CustomFieldFilterForm(FilterForm):
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
required=False
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
type = MultipleChoiceField(
|
||||
choices=CustomFieldTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple(),
|
||||
label=_('Field type')
|
||||
)
|
||||
weight = forms.IntegerField(
|
||||
@@ -117,10 +118,9 @@ class WebhookFilterForm(FilterForm):
|
||||
limit_choices_to=FeatureQuery('webhooks'),
|
||||
required=False
|
||||
)
|
||||
http_method = forms.MultipleChoiceField(
|
||||
http_method = MultipleChoiceField(
|
||||
choices=WebhookHttpMethodChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple(),
|
||||
label=_('HTTP method')
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
@@ -239,10 +239,10 @@ class LocalConfigContextFilterForm(forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class JournalEntryFilterForm(FilterForm):
|
||||
class JournalEntryFilterForm(NetBoxModelFilterSetForm):
|
||||
model = JournalEntry
|
||||
fieldsets = (
|
||||
(None, ('q',)),
|
||||
(None, ('q', 'tag')),
|
||||
('Creation', ('created_before', 'created_after', 'created_by_id')),
|
||||
('Attributes', ('assigned_object_type_id', 'kind'))
|
||||
)
|
||||
@@ -277,6 +277,7 @@ class JournalEntryFilterForm(FilterForm):
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ObjectChangeFilterForm(FilterForm):
|
||||
|
||||
@@ -5,6 +5,7 @@ from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGrou
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField,
|
||||
@@ -32,6 +33,8 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
||||
)
|
||||
object_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
# TODO: Come up with a canonical way to register suitable models
|
||||
limit_choices_to=FeatureQuery('webhooks'),
|
||||
required=False,
|
||||
help_text="Type of the related object (for object/multi-object fields only)"
|
||||
)
|
||||
@@ -46,6 +49,10 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = '__all__'
|
||||
help_texts = {
|
||||
'type': "The type of data stored in this field. For object/multi-object fields, select the related object "
|
||||
"type below."
|
||||
}
|
||||
widgets = {
|
||||
'type': StaticSelect(),
|
||||
'filter_logic': StaticSelect(),
|
||||
@@ -213,18 +220,17 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
||||
]
|
||||
|
||||
|
||||
class JournalEntryForm(BootstrapMixin, forms.ModelForm):
|
||||
comments = CommentField()
|
||||
|
||||
class JournalEntryForm(NetBoxModelForm):
|
||||
kind = forms.ChoiceField(
|
||||
choices=add_blank_choice(JournalEntryKindChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = JournalEntry
|
||||
fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'comments']
|
||||
fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'tags', 'comments']
|
||||
widgets = {
|
||||
'assigned_object_type': forms.HiddenInput,
|
||||
'assigned_object_id': forms.HiddenInput,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from extras import filtersets, models
|
||||
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
|
||||
from netbox.graphql.types import BaseObjectType, ObjectType
|
||||
|
||||
__all__ = (
|
||||
@@ -54,7 +55,7 @@ class ImageAttachmentType(BaseObjectType):
|
||||
filterset_class = filtersets.ImageAttachmentFilterSet
|
||||
|
||||
|
||||
class JournalEntryType(ObjectType):
|
||||
class JournalEntryType(CustomFieldsMixin, TagsMixin, ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.JournalEntry
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.db import DEFAULT_DB_ALIAS
|
||||
from django.utils import timezone
|
||||
from packaging import version
|
||||
|
||||
from extras.models import JobResult
|
||||
from extras.models import ObjectChange
|
||||
from netbox.config import Config
|
||||
|
||||
@@ -63,6 +64,33 @@ class Command(BaseCommand):
|
||||
f"\tSkipping: No retention period specified (CHANGELOG_RETENTION = {config.CHANGELOG_RETENTION})"
|
||||
)
|
||||
|
||||
# Delete expired JobResults
|
||||
if options['verbosity']:
|
||||
self.stdout.write("[*] Checking for expired jobresult records")
|
||||
if config.JOBRESULT_RETENTION:
|
||||
cutoff = timezone.now() - timedelta(days=config.JOBRESULT_RETENTION)
|
||||
if options['verbosity'] >= 2:
|
||||
self.stdout.write(f"\tRetention period: {config.JOBRESULT_RETENTION} days")
|
||||
self.stdout.write(f"\tCut-off time: {cutoff}")
|
||||
expired_records = JobResult.objects.filter(created__lt=cutoff).count()
|
||||
if expired_records:
|
||||
if options['verbosity']:
|
||||
self.stdout.write(
|
||||
f"\tDeleting {expired_records} expired records... ",
|
||||
self.style.WARNING,
|
||||
ending=""
|
||||
)
|
||||
self.stdout.flush()
|
||||
JobResult.objects.filter(created__lt=cutoff)._raw_delete(using=DEFAULT_DB_ALIAS)
|
||||
if options['verbosity']:
|
||||
self.stdout.write("Done.", self.style.SUCCESS)
|
||||
elif options['verbosity']:
|
||||
self.stdout.write("\tNo expired records found.", self.style.SUCCESS)
|
||||
elif options['verbosity']:
|
||||
self.stdout.write(
|
||||
f"\tSkipping: No retention period specified (JOBRESULT_RETENTION = {config.JOBRESULT_RETENTION})"
|
||||
)
|
||||
|
||||
# Check for new releases (if enabled)
|
||||
if options['verbosity']:
|
||||
self.stdout.write("[*] Checking for latest release")
|
||||
|
||||
@@ -35,7 +35,8 @@ class Command(BaseCommand):
|
||||
run_report,
|
||||
report.full_name,
|
||||
report_content_type,
|
||||
None
|
||||
None,
|
||||
job_timeout=report.job_timeout
|
||||
)
|
||||
|
||||
# Wait on the job to finish
|
||||
|
||||
@@ -113,13 +113,6 @@ class Command(BaseCommand):
|
||||
|
||||
script_content_type = ContentType.objects.get(app_label='extras', model='script')
|
||||
|
||||
# Delete any previous terminal state results
|
||||
JobResult.objects.filter(
|
||||
obj_type=script_content_type,
|
||||
name=script.full_name,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).delete()
|
||||
|
||||
# Create the job result
|
||||
job_result = JobResult.objects.create(
|
||||
name=script.full_name,
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0145_site_remove_deprecated_fields'),
|
||||
('virtualization', '0026_vminterface_bridge'),
|
||||
('extras', '0067_customfield_min_max_values'),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import django.core.serializers.json
|
||||
from django.db import migrations, models
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0072_created_datetimefield'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='journalentry',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='journalentry',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||
),
|
||||
]
|
||||
@@ -430,6 +430,15 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||
filter_class = filters.MultiValueCharFilter
|
||||
kwargs['lookup_expr'] = 'has_key'
|
||||
|
||||
# Object
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
||||
filter_class = filters.MultiValueNumberFilter
|
||||
|
||||
# Multi-object
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
||||
filter_class = filters.MultiValueNumberFilter
|
||||
kwargs['lookup_expr'] = 'contains'
|
||||
|
||||
# Unsupported custom field type
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -13,13 +13,16 @@ from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.formats import date_format
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
import django_rq
|
||||
|
||||
from extras.choices import *
|
||||
from extras.constants import *
|
||||
from extras.conditions import ConditionSet
|
||||
from extras.utils import FeatureQuery, image_upload
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import ExportTemplatesMixin, JobResultsMixin, WebhooksMixin
|
||||
from netbox.models.features import (
|
||||
CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, JobResultsMixin, TagsMixin, WebhooksMixin,
|
||||
)
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import render_jinja2
|
||||
|
||||
@@ -419,7 +422,7 @@ class ImageAttachment(WebhooksMixin, ChangeLoggedModel):
|
||||
return objectchange
|
||||
|
||||
|
||||
class JournalEntry(WebhooksMixin, ChangeLoggedModel):
|
||||
class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, WebhooksMixin, ChangeLoggedModel):
|
||||
"""
|
||||
A historical remark concerning an object; collectively, these form an object's journal. The journal is used to
|
||||
preserve historical context around an object, and complements NetBox's built-in change logging. For example, you
|
||||
@@ -548,7 +551,8 @@ class JobResult(models.Model):
|
||||
job_id=uuid.uuid4()
|
||||
)
|
||||
|
||||
func.delay(*args, job_id=str(job_result.job_id), job_result=job_result, **kwargs)
|
||||
queue = django_rq.get_queue("default")
|
||||
queue.enqueue(func, job_id=str(job_result.job_id), job_result=job_result, **kwargs)
|
||||
|
||||
return job_result
|
||||
|
||||
|
||||
@@ -84,15 +84,6 @@ def run_report(job_result, *args, **kwargs):
|
||||
job_result.save()
|
||||
logging.error(f"Error during execution of report {job_result.name}")
|
||||
|
||||
# Delete any previous terminal state results
|
||||
JobResult.objects.filter(
|
||||
obj_type=job_result.obj_type,
|
||||
name=job_result.name,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).exclude(
|
||||
pk=job_result.pk
|
||||
).delete()
|
||||
|
||||
|
||||
class Report(object):
|
||||
"""
|
||||
@@ -119,6 +110,7 @@ class Report(object):
|
||||
}
|
||||
"""
|
||||
description = None
|
||||
job_timeout = None
|
||||
|
||||
def __init__(self):
|
||||
|
||||
|
||||
@@ -259,6 +259,10 @@ class BaseScript:
|
||||
Base model for custom scripts. User classes should inherit from this model if they want to extend Script
|
||||
functionality for use in other subclasses.
|
||||
"""
|
||||
|
||||
# Prevent django from instantiating the class on all accesses
|
||||
do_not_call_in_templates = True
|
||||
|
||||
class Meta:
|
||||
pass
|
||||
|
||||
@@ -280,7 +284,7 @@ class BaseScript:
|
||||
|
||||
@classproperty
|
||||
def name(self):
|
||||
return getattr(self.Meta, 'name', self.__class__.__name__)
|
||||
return getattr(self.Meta, 'name', self.__name__)
|
||||
|
||||
@classproperty
|
||||
def full_name(self):
|
||||
@@ -294,6 +298,10 @@ class BaseScript:
|
||||
def module(cls):
|
||||
return cls.__module__
|
||||
|
||||
@classproperty
|
||||
def job_timeout(self):
|
||||
return getattr(self.Meta, 'job_timeout', None)
|
||||
|
||||
@classmethod
|
||||
def _get_vars(cls):
|
||||
vars = {}
|
||||
@@ -410,7 +418,6 @@ def is_variable(obj):
|
||||
return isinstance(obj, ScriptVariable)
|
||||
|
||||
|
||||
@job('default')
|
||||
def run_script(data, request, commit=True, *args, **kwargs):
|
||||
"""
|
||||
A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It
|
||||
@@ -474,15 +481,6 @@ def run_script(data, request, commit=True, *args, **kwargs):
|
||||
else:
|
||||
_run_script()
|
||||
|
||||
# Delete any previous terminal state results
|
||||
JobResult.objects.filter(
|
||||
obj_type=job_result.obj_type,
|
||||
name=job_result.name,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).exclude(
|
||||
pk=job_result.pk
|
||||
).delete()
|
||||
|
||||
|
||||
def get_scripts(use_names=False):
|
||||
"""
|
||||
@@ -490,7 +488,7 @@ def get_scripts(use_names=False):
|
||||
defined name in place of the actual module name.
|
||||
"""
|
||||
scripts = OrderedDict()
|
||||
# Iterate through all modules within the reports path. These are the user-created files in which reports are
|
||||
# Iterate through all modules within the scripts path. These are the user-created files in which reports are
|
||||
# defined.
|
||||
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
|
||||
# Remove cached module to ensure consistency with filesystem
|
||||
|
||||
@@ -12,7 +12,6 @@ __all__ = (
|
||||
'ExportTemplateTable',
|
||||
'JournalEntryTable',
|
||||
'ObjectChangeTable',
|
||||
'ObjectJournalTable',
|
||||
'TaggedItemTable',
|
||||
'TagTable',
|
||||
'WebhookTable',
|
||||
@@ -147,6 +146,9 @@ class TaggedItemTable(NetBoxTable):
|
||||
orderable=False,
|
||||
verbose_name='Object'
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
actions=()
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = TaggedItem
|
||||
@@ -195,7 +197,9 @@ class ObjectChangeTable(NetBoxTable):
|
||||
template_code=OBJECTCHANGE_REQUEST_ID,
|
||||
verbose_name='Request ID'
|
||||
)
|
||||
actions = columns.ActionsColumn(sequence=())
|
||||
actions = columns.ActionsColumn(
|
||||
actions=()
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = ObjectChange
|
||||
@@ -205,25 +209,11 @@ class ObjectChangeTable(NetBoxTable):
|
||||
)
|
||||
|
||||
|
||||
class ObjectJournalTable(NetBoxTable):
|
||||
"""
|
||||
Used for displaying a set of JournalEntries within the context of a single object.
|
||||
"""
|
||||
class JournalEntryTable(NetBoxTable):
|
||||
created = tables.DateTimeColumn(
|
||||
linkify=True,
|
||||
format=settings.SHORT_DATETIME_FORMAT
|
||||
)
|
||||
kind = columns.ChoiceFieldColumn()
|
||||
comments = tables.TemplateColumn(
|
||||
template_code='{{ value|markdown|truncatewords_html:50 }}'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = JournalEntry
|
||||
fields = ('id', 'created', 'created_by', 'kind', 'comments', 'actions')
|
||||
|
||||
|
||||
class JournalEntryTable(ObjectJournalTable):
|
||||
assigned_object_type = columns.ContentTypeColumn(
|
||||
verbose_name='Object type'
|
||||
)
|
||||
@@ -232,13 +222,22 @@ class JournalEntryTable(ObjectJournalTable):
|
||||
orderable=False,
|
||||
verbose_name='Object'
|
||||
)
|
||||
kind = columns.ChoiceFieldColumn()
|
||||
comments = columns.MarkdownColumn()
|
||||
comments_short = tables.TemplateColumn(
|
||||
accessor=tables.A('comments'),
|
||||
template_code='{{ value|markdown|truncatewords_html:50 }}',
|
||||
verbose_name='Comments (Short)'
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='extras:journalentry_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = JournalEntry
|
||||
fields = (
|
||||
'pk', 'id', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments',
|
||||
'actions',
|
||||
'comments_short', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments'
|
||||
|
||||
@@ -5,7 +5,7 @@ from rest_framework import status
|
||||
|
||||
from dcim.filtersets import SiteFilterSet
|
||||
from dcim.forms import SiteCSVForm
|
||||
from dcim.models import Site, Rack
|
||||
from dcim.models import Manufacturer, Rack, Site
|
||||
from extras.choices import *
|
||||
from extras.models import CustomField
|
||||
from ipam.models import VLAN
|
||||
@@ -1022,6 +1022,13 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
def setUpTestData(cls):
|
||||
obj_type = ContentType.objects.get_for_model(Site)
|
||||
|
||||
manufacturers = Manufacturer.objects.bulk_create((
|
||||
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
|
||||
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
|
||||
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
|
||||
Manufacturer(name='Manufacturer 4', slug='manufacturer-4'),
|
||||
))
|
||||
|
||||
# Integer filtering
|
||||
cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER)
|
||||
cf.save()
|
||||
@@ -1071,6 +1078,24 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Object filtering
|
||||
cf = CustomField(
|
||||
name='cf10',
|
||||
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
||||
object_type=ContentType.objects.get_for_model(Manufacturer)
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Multi-object filtering
|
||||
cf = CustomField(
|
||||
name='cf11',
|
||||
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
||||
object_type=ContentType.objects.get_for_model(Manufacturer)
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
Site.objects.bulk_create([
|
||||
Site(name='Site 1', slug='site-1', custom_field_data={
|
||||
'cf1': 100,
|
||||
@@ -1082,6 +1107,8 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
'cf7': 'http://a.example.com',
|
||||
'cf8': 'Foo',
|
||||
'cf9': ['A', 'X'],
|
||||
'cf10': manufacturers[0].pk,
|
||||
'cf11': [manufacturers[0].pk, manufacturers[3].pk],
|
||||
}),
|
||||
Site(name='Site 2', slug='site-2', custom_field_data={
|
||||
'cf1': 200,
|
||||
@@ -1093,6 +1120,8 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
'cf7': 'http://b.example.com',
|
||||
'cf8': 'Bar',
|
||||
'cf9': ['B', 'X'],
|
||||
'cf10': manufacturers[1].pk,
|
||||
'cf11': [manufacturers[1].pk, manufacturers[3].pk],
|
||||
}),
|
||||
Site(name='Site 3', slug='site-3', custom_field_data={
|
||||
'cf1': 300,
|
||||
@@ -1104,6 +1133,8 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
'cf7': 'http://c.example.com',
|
||||
'cf8': 'Baz',
|
||||
'cf9': ['C', 'X'],
|
||||
'cf10': manufacturers[2].pk,
|
||||
'cf11': [manufacturers[2].pk, manufacturers[3].pk],
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -1163,3 +1194,12 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
def test_filter_multiselect(self):
|
||||
self.assertEqual(self.filterset({'cf_cf9': ['A', 'B']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf9': ['X']}, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_filter_object(self):
|
||||
manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
|
||||
self.assertEqual(self.filterset({'cf_cf10': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_filter_multiobject(self):
|
||||
manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
|
||||
self.assertEqual(self.filterset({'cf_cf11': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf11': [manufacturer_ids[3]]}, self.queryset).qs.count(), 3)
|
||||
|
||||
@@ -37,7 +37,7 @@ class CustomFieldView(generic.ObjectView):
|
||||
|
||||
class CustomFieldEditView(generic.ObjectEditView):
|
||||
queryset = CustomField.objects.all()
|
||||
model_form = forms.CustomFieldForm
|
||||
form = forms.CustomFieldForm
|
||||
|
||||
|
||||
class CustomFieldDeleteView(generic.ObjectDeleteView):
|
||||
@@ -80,7 +80,7 @@ class CustomLinkView(generic.ObjectView):
|
||||
|
||||
class CustomLinkEditView(generic.ObjectEditView):
|
||||
queryset = CustomLink.objects.all()
|
||||
model_form = forms.CustomLinkForm
|
||||
form = forms.CustomLinkForm
|
||||
|
||||
|
||||
class CustomLinkDeleteView(generic.ObjectDeleteView):
|
||||
@@ -123,7 +123,7 @@ class ExportTemplateView(generic.ObjectView):
|
||||
|
||||
class ExportTemplateEditView(generic.ObjectEditView):
|
||||
queryset = ExportTemplate.objects.all()
|
||||
model_form = forms.ExportTemplateForm
|
||||
form = forms.ExportTemplateForm
|
||||
|
||||
|
||||
class ExportTemplateDeleteView(generic.ObjectDeleteView):
|
||||
@@ -166,7 +166,7 @@ class WebhookView(generic.ObjectView):
|
||||
|
||||
class WebhookEditView(generic.ObjectEditView):
|
||||
queryset = Webhook.objects.all()
|
||||
model_form = forms.WebhookForm
|
||||
form = forms.WebhookForm
|
||||
|
||||
|
||||
class WebhookDeleteView(generic.ObjectDeleteView):
|
||||
@@ -232,7 +232,7 @@ class TagView(generic.ObjectView):
|
||||
|
||||
class TagEditView(generic.ObjectEditView):
|
||||
queryset = Tag.objects.all()
|
||||
model_form = forms.TagForm
|
||||
form = forms.TagForm
|
||||
|
||||
|
||||
class TagDeleteView(generic.ObjectDeleteView):
|
||||
@@ -310,7 +310,7 @@ class ConfigContextView(generic.ObjectView):
|
||||
|
||||
class ConfigContextEditView(generic.ObjectEditView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
model_form = forms.ConfigContextForm
|
||||
form = forms.ConfigContextForm
|
||||
template_name = 'extras/configcontext_edit.html'
|
||||
|
||||
|
||||
@@ -428,7 +428,7 @@ class ObjectChangeView(generic.ObjectView):
|
||||
|
||||
class ImageAttachmentEditView(generic.ObjectEditView):
|
||||
queryset = ImageAttachment.objects.all()
|
||||
model_form = forms.ImageAttachmentForm
|
||||
form = forms.ImageAttachmentForm
|
||||
template_name = 'extras/imageattachment_edit.html'
|
||||
|
||||
def alter_object(self, instance, request, args, kwargs):
|
||||
@@ -467,7 +467,7 @@ class JournalEntryView(generic.ObjectView):
|
||||
|
||||
class JournalEntryEditView(generic.ObjectEditView):
|
||||
queryset = JournalEntry.objects.all()
|
||||
model_form = forms.JournalEntryForm
|
||||
form = forms.JournalEntryForm
|
||||
|
||||
def alter_object(self, obj, request, args, kwargs):
|
||||
if not obj.pk:
|
||||
@@ -524,7 +524,7 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
|
||||
for r in JobResult.objects.filter(
|
||||
obj_type=report_content_type,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).defer('data')
|
||||
).order_by('name', '-created').distinct('name').defer('data')
|
||||
}
|
||||
|
||||
ret = []
|
||||
@@ -588,7 +588,8 @@ class ReportView(ContentTypePermissionRequiredMixin, View):
|
||||
run_report,
|
||||
report.full_name,
|
||||
report_content_type,
|
||||
request.user
|
||||
request.user,
|
||||
job_timeout=report.job_timeout
|
||||
)
|
||||
|
||||
return redirect('extras:report_result', job_result_pk=job_result.pk)
|
||||
@@ -655,7 +656,7 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View):
|
||||
for r in JobResult.objects.filter(
|
||||
obj_type=script_content_type,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).defer('data')
|
||||
).order_by('name', '-created').distinct('name').defer('data')
|
||||
}
|
||||
|
||||
for _scripts in scripts.values():
|
||||
@@ -708,6 +709,7 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
|
||||
commit = form.cleaned_data.pop('_commit')
|
||||
|
||||
script_content_type = ContentType.objects.get(app_label='extras', model='script')
|
||||
|
||||
job_result = JobResult.enqueue_job(
|
||||
run_script,
|
||||
script.full_name,
|
||||
@@ -715,7 +717,8 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
|
||||
request.user,
|
||||
data=form.cleaned_data,
|
||||
request=copy_safe_request(request),
|
||||
commit=commit
|
||||
commit=commit,
|
||||
job_timeout=script.job_timeout,
|
||||
)
|
||||
|
||||
return redirect('extras:script_result', job_result_pk=job_result.pk)
|
||||
|
||||
@@ -24,12 +24,13 @@ class ASNSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
provider_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ASN
|
||||
fields = [
|
||||
'id', 'url', 'display', 'asn', 'site_count', 'rir', 'tenant', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display', 'asn', 'rir', 'tenant', 'description', 'site_count', 'provider_count', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from django.urls import path
|
||||
|
||||
from netbox.api import OrderedDefaultRouter
|
||||
from netbox.api import NetBoxRouter
|
||||
from ipam.models import IPRange, Prefix
|
||||
from . import views
|
||||
|
||||
|
||||
router = OrderedDefaultRouter()
|
||||
router = NetBoxRouter()
|
||||
router.APIRootView = views.IPAMRootView
|
||||
|
||||
# ASNs
|
||||
|
||||
@@ -8,6 +8,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.routers import APIRootView
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from circuits.models import Provider
|
||||
from dcim.models import Site
|
||||
from ipam import filtersets
|
||||
from ipam.models import *
|
||||
@@ -32,7 +33,10 @@ class IPAMRootView(APIRootView):
|
||||
#
|
||||
|
||||
class ASNViewSet(NetBoxModelViewSet):
|
||||
queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate(site_count=count_related(Site, 'asns'))
|
||||
queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate(
|
||||
site_count=count_related(Site, 'asns'),
|
||||
provider_count=count_related(Provider, 'asns')
|
||||
)
|
||||
serializer_class = serializers.ASNSerializer
|
||||
filterset_class = filtersets.ASNFilterSet
|
||||
|
||||
|
||||
@@ -106,14 +106,22 @@ class FHRPGroupProtocolChoices(ChoiceSet):
|
||||
PROTOCOL_HSRP = 'hsrp'
|
||||
PROTOCOL_GLBP = 'glbp'
|
||||
PROTOCOL_CARP = 'carp'
|
||||
PROTOCOL_CLUSTERXL = 'clusterxl'
|
||||
PROTOCOL_OTHER = 'other'
|
||||
|
||||
CHOICES = (
|
||||
(PROTOCOL_VRRP2, 'VRRPv2'),
|
||||
(PROTOCOL_VRRP3, 'VRRPv3'),
|
||||
(PROTOCOL_HSRP, 'HSRP'),
|
||||
(PROTOCOL_GLBP, 'GLBP'),
|
||||
(PROTOCOL_CARP, 'CARP'),
|
||||
('Standard', (
|
||||
(PROTOCOL_VRRP2, 'VRRPv2'),
|
||||
(PROTOCOL_VRRP3, 'VRRPv3'),
|
||||
(PROTOCOL_CARP, 'CARP'),
|
||||
)),
|
||||
('CheckPoint', (
|
||||
(PROTOCOL_CLUSTERXL, 'ClusterXL'),
|
||||
)),
|
||||
('Cisco', (
|
||||
(PROTOCOL_HSRP, 'HSRP'),
|
||||
(PROTOCOL_GLBP, 'GLBP'),
|
||||
)),
|
||||
(PROTOCOL_OTHER, 'Other'),
|
||||
)
|
||||
|
||||
|
||||
@@ -36,10 +36,6 @@ __all__ = (
|
||||
|
||||
|
||||
class VRFFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
import_target_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='import_targets',
|
||||
queryset=RouteTarget.objects.all(),
|
||||
@@ -78,10 +74,6 @@ class VRFFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
|
||||
|
||||
class RouteTargetFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
importing_vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='importing_vrfs',
|
||||
queryset=VRF.objects.all(),
|
||||
@@ -126,10 +118,6 @@ class RIRFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
|
||||
class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
family = django_filters.NumberFilter(
|
||||
field_name='prefix',
|
||||
lookup_expr='family'
|
||||
@@ -213,10 +201,6 @@ class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
|
||||
|
||||
class RoleFilterSet(OrganizationalModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
@@ -224,10 +208,6 @@ class RoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
|
||||
class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
family = django_filters.NumberFilter(
|
||||
field_name='prefix',
|
||||
lookup_expr='family'
|
||||
@@ -329,7 +309,7 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
)
|
||||
vlan_vid = django_filters.NumberFilter(
|
||||
field_name='vlan__vid',
|
||||
label='VLAN number (1-4095)',
|
||||
label='VLAN number (1-4094)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Role.objects.all(),
|
||||
@@ -414,10 +394,6 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
|
||||
|
||||
class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
family = django_filters.NumberFilter(
|
||||
field_name='start_address',
|
||||
lookup_expr='family'
|
||||
@@ -480,10 +456,6 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
|
||||
|
||||
class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
family = django_filters.NumberFilter(
|
||||
field_name='address',
|
||||
lookup_expr='family'
|
||||
@@ -563,6 +535,11 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
queryset=VMInterface.objects.all(),
|
||||
label='VM interface (ID)',
|
||||
)
|
||||
fhrpgroup_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='fhrpgroup',
|
||||
queryset=FHRPGroup.objects.all(),
|
||||
label='FHRP group (ID)',
|
||||
)
|
||||
assigned_to_interface = django_filters.BooleanFilter(
|
||||
method='_assigned_to_interface',
|
||||
label='Is assigned to an interface',
|
||||
@@ -641,14 +618,20 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
)
|
||||
|
||||
def _assigned_to_interface(self, queryset, name, value):
|
||||
return queryset.exclude(assigned_object_id__isnull=value)
|
||||
content_types = ContentType.objects.get_for_models(Interface, VMInterface).values()
|
||||
if value:
|
||||
return queryset.filter(
|
||||
assigned_object_type__in=content_types,
|
||||
assigned_object_id__isnull=False
|
||||
)
|
||||
else:
|
||||
return queryset.exclude(
|
||||
assigned_object_type__in=content_types,
|
||||
assigned_object_id__isnull=False
|
||||
)
|
||||
|
||||
|
||||
class FHRPGroupFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
protocol = django_filters.MultipleChoiceFilter(
|
||||
choices=FHRPGroupProtocolChoices
|
||||
)
|
||||
@@ -705,10 +688,6 @@ class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
|
||||
|
||||
|
||||
class VLANGroupFilterSet(OrganizationalModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
scope_type = ContentTypeFilter()
|
||||
region = django_filters.NumberFilter(
|
||||
method='filter_scope'
|
||||
@@ -753,10 +732,6 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
|
||||
class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region',
|
||||
@@ -848,10 +823,6 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
|
||||
|
||||
class ServiceTemplateFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
port = NumericArrayFilter(
|
||||
field_name='ports',
|
||||
lookup_expr='contains'
|
||||
@@ -869,10 +840,6 @@ class ServiceTemplateFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
|
||||
class ServiceFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
|
||||
@@ -388,7 +388,7 @@ class VLANCSVForm(NetBoxModelCSVForm):
|
||||
model = VLAN
|
||||
fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description')
|
||||
help_texts = {
|
||||
'vid': 'Numeric VLAN ID (1-4095)',
|
||||
'vid': 'Numeric VLAN ID (1-4094)',
|
||||
'name': 'VLAN name',
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.forms import TenancyFilterForm
|
||||
from utilities.forms import (
|
||||
add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect, StaticSelectMultiple,
|
||||
add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField, MultipleChoiceField, StaticSelect,
|
||||
TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
|
||||
@@ -164,11 +164,10 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
label=_('Address family'),
|
||||
widget=StaticSelect()
|
||||
)
|
||||
mask_length = forms.MultipleChoiceField(
|
||||
mask_length = MultipleChoiceField(
|
||||
required=False,
|
||||
choices=PREFIX_MASK_LENGTH_CHOICES,
|
||||
label=_('Mask length'),
|
||||
widget=StaticSelectMultiple()
|
||||
label=_('Mask length')
|
||||
)
|
||||
vrf_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
@@ -181,10 +180,9 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
required=False,
|
||||
label=_('Present in VRF')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
status = MultipleChoiceField(
|
||||
choices=PrefixStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -247,10 +245,9 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
label=_('Assigned VRF'),
|
||||
null_option='Global'
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
status = MultipleChoiceField(
|
||||
choices=PrefixStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
@@ -301,15 +298,13 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
required=False,
|
||||
label=_('Present in VRF')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
status = MultipleChoiceField(
|
||||
choices=IPAddressStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
role = forms.MultipleChoiceField(
|
||||
role = MultipleChoiceField(
|
||||
choices=IPAddressRoleChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
assigned_to_interface = forms.NullBooleanField(
|
||||
required=False,
|
||||
@@ -328,20 +323,18 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
|
||||
('Attributes', ('protocol', 'group_id')),
|
||||
('Authentication', ('auth_type', 'auth_key')),
|
||||
)
|
||||
protocol = forms.MultipleChoiceField(
|
||||
protocol = MultipleChoiceField(
|
||||
choices=FHRPGroupProtocolChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
group_id = forms.IntegerField(
|
||||
min_value=0,
|
||||
required=False,
|
||||
label='Group ID'
|
||||
)
|
||||
auth_type = forms.MultipleChoiceField(
|
||||
auth_type = MultipleChoiceField(
|
||||
choices=FHRPGroupAuthTypeChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple(),
|
||||
label='Authentication type'
|
||||
)
|
||||
auth_key = forms.CharField(
|
||||
@@ -384,12 +377,16 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
|
||||
label=_('Rack')
|
||||
)
|
||||
min_vid = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=VLAN_VID_MIN,
|
||||
max_value=VLAN_VID_MAX,
|
||||
label='Minimum VID'
|
||||
)
|
||||
max_vid = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=VLAN_VID_MIN,
|
||||
max_value=VLAN_VID_MAX,
|
||||
label='Maximum VID'
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -430,10 +427,9 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
},
|
||||
label=_('VLAN group')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
status = MultipleChoiceField(
|
||||
choices=VLANStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
required=False
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
@@ -457,7 +453,7 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
|
||||
protocol = forms.ChoiceField(
|
||||
choices=add_blank_choice(ServiceProtocolChoices),
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
widget=StaticSelect()
|
||||
)
|
||||
port = forms.IntegerField(
|
||||
required=False,
|
||||
|
||||
@@ -48,10 +48,6 @@ class VRFForm(TenancyForm, NetBoxModelForm):
|
||||
queryset=RouteTarget.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('VRF', ('name', 'rd', 'enforce_unique', 'description', 'tags')),
|
||||
@@ -74,11 +70,6 @@ class VRFForm(TenancyForm, NetBoxModelForm):
|
||||
|
||||
|
||||
class RouteTargetForm(TenancyForm, NetBoxModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Route Target', ('name', 'description', 'tags')),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
@@ -93,10 +84,6 @@ class RouteTargetForm(TenancyForm, NetBoxModelForm):
|
||||
|
||||
class RIRForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
@@ -110,10 +97,6 @@ class AggregateForm(TenancyForm, NetBoxModelForm):
|
||||
queryset=RIR.objects.all(),
|
||||
label='RIR'
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Aggregate', ('prefix', 'rir', 'date_added', 'description', 'tags')),
|
||||
@@ -144,10 +127,6 @@ class ASNForm(TenancyForm, NetBoxModelForm):
|
||||
label='Sites',
|
||||
required=False
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('ASN', ('asn', 'rir', 'sites', 'description', 'tags')),
|
||||
@@ -181,10 +160,6 @@ class ASNForm(TenancyForm, NetBoxModelForm):
|
||||
|
||||
class RoleForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
@@ -228,7 +203,7 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
|
||||
label='VLAN group',
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
'site': '$site'
|
||||
},
|
||||
initial_params={
|
||||
'vlans': '$vlan'
|
||||
@@ -247,10 +222,6 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
|
||||
queryset=Role.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')),
|
||||
@@ -279,10 +250,6 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
|
||||
queryset=Role.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('IP Range', ('vrf', 'start_address', 'end_address', 'role', 'status', 'description', 'tags')),
|
||||
@@ -414,10 +381,6 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||
required=False,
|
||||
label='Make this the primary IP for the device/VM'
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
@@ -518,10 +481,6 @@ class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm):
|
||||
required=False,
|
||||
label='VRF'
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
@@ -547,10 +506,6 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
||||
|
||||
|
||||
class FHRPGroupForm(NetBoxModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
# Optionally create a new IPAddress along with the FHRPGroup
|
||||
ip_vrf = DynamicModelChoiceField(
|
||||
@@ -701,10 +656,6 @@ class VLANGroupForm(NetBoxModelForm):
|
||||
}
|
||||
)
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('VLAN Group', ('name', 'slug', 'description', 'tags')),
|
||||
@@ -802,10 +753,6 @@ class VLANForm(TenancyForm, NetBoxModelForm):
|
||||
queryset=Role.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
@@ -833,10 +780,6 @@ class ServiceTemplateForm(NetBoxModelForm):
|
||||
),
|
||||
help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen."
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ServiceTemplate
|
||||
@@ -871,10 +814,6 @@ class ServiceForm(NetBoxModelForm):
|
||||
'virtual_machine_id': '$virtual_machine',
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Service
|
||||
|
||||
@@ -50,7 +50,7 @@ class Migration(migrations.Migration):
|
||||
('status', models.CharField(default='active', max_length=50)),
|
||||
('role', models.CharField(blank=True, max_length=50)),
|
||||
('assigned_object_id', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('dns_name', models.CharField(blank=True, max_length=255, validators=[django.core.validators.RegexValidator(code='invalid', message='Only alphanumeric characters, hyphens, periods, and underscores are allowed in DNS names', regex='^[0-9A-Za-z._-]+$')])),
|
||||
('dns_name', models.CharField(blank=True, max_length=255, validators=[django.core.validators.RegexValidator(code='invalid', message='Only alphanumeric characters, asterisks, hyphens, periods, and underscores are allowed in DNS names', regex='^([0-9A-Za-z_-]+|\\*)(\\.[0-9A-Za-z_-]+)*\\.?$')])),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
],
|
||||
options={
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0145_site_remove_deprecated_fields'),
|
||||
('ipam', '0053_asn_model'),
|
||||
]
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Ensure that VRFs are imported before IPs/prefixes so dumpdata & loaddata work correctly
|
||||
from .fhrp import *
|
||||
from .vrfs import *
|
||||
from .ip import *
|
||||
from .services import *
|
||||
from .vlans import *
|
||||
from .vrfs import *
|
||||
|
||||
__all__ = (
|
||||
'ASN',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user