mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 17:38:37 -06:00
Merge branch 'feature' into 8366-job-scheduling
This commit is contained in:
commit
893925436d
14
.github/ISSUE_TEMPLATE/documentation_change.yaml
vendored
14
.github/ISSUE_TEMPLATE/documentation_change.yaml
vendored
@ -19,11 +19,15 @@ body:
|
|||||||
label: Area
|
label: Area
|
||||||
description: To what section of the documentation does this change primarily pertain?
|
description: To what section of the documentation does this change primarily pertain?
|
||||||
options:
|
options:
|
||||||
- Installation instructions
|
- Features
|
||||||
- Configuration parameters
|
- Installation/upgrade
|
||||||
- Functionality/features
|
- Getting started
|
||||||
- REST API
|
- Configuration
|
||||||
- Administration/development
|
- Customization
|
||||||
|
- Integrations/API
|
||||||
|
- Plugins
|
||||||
|
- Administration
|
||||||
|
- Development
|
||||||
- Other
|
- Other
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
@ -46,7 +46,7 @@ Next, create a file in the same directory as `configuration.py` (typically `/opt
|
|||||||
### General Server Configuration
|
### General Server Configuration
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
When using Windows Server 2012 you may need to specify a port on `AUTH_LDAP_SERVER_URI`. Use `3269` for secure, or `3268` for non-secure.
|
When using Active Directory you may need to specify a port on `AUTH_LDAP_SERVER_URI` to authenticate users from all domains in the forest. Use `3269` for secure, or `3268` for non-secure access to the GC (Global Catalog).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import ldap
|
import ldap
|
||||||
@ -67,6 +67,16 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
|
|||||||
# Note that this is a NetBox-specific setting which sets:
|
# Note that this is a NetBox-specific setting which sets:
|
||||||
# ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
# ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
||||||
LDAP_IGNORE_CERT_ERRORS = True
|
LDAP_IGNORE_CERT_ERRORS = True
|
||||||
|
|
||||||
|
# Include this setting if you want to validate the LDAP server certificates against a CA certificate directory on your server
|
||||||
|
# Note that this is a NetBox-specific setting which sets:
|
||||||
|
# ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, LDAP_CA_CERT_DIR)
|
||||||
|
LDAP_CA_CERT_DIR = '/etc/ssl/certs'
|
||||||
|
|
||||||
|
# Include this setting if you want to validate the LDAP server certificates against your own CA.
|
||||||
|
# Note that this is a NetBox-specific setting which sets:
|
||||||
|
# ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, LDAP_CA_CERT_FILE)
|
||||||
|
LDAP_CA_CERT_FILE = '/path/to/example-CA.crt'
|
||||||
```
|
```
|
||||||
|
|
||||||
STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme.
|
STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme.
|
||||||
|
@ -47,7 +47,7 @@ NetBox provides both a singular and plural query field for each object type:
|
|||||||
|
|
||||||
For example, query `device(id:123)` to fetch a specific device (identified by its unique ID), and query `device_list` (with an optional set of filters) to fetch all devices.
|
For example, query `device(id:123)` to fetch a specific device (identified by its unique ID), and query `device_list` (with an optional set of filters) to fetch all devices.
|
||||||
|
|
||||||
For more detail on constructing GraphQL queries, see the [Graphene documentation](https://docs.graphene-python.org/en/latest/).
|
For more detail on constructing GraphQL queries, see the [Graphene documentation](https://docs.graphene-python.org/en/latest/) as well as the [GraphQL queries documentation](https://graphql.org/learn/queries/).
|
||||||
|
|
||||||
## Filtering
|
## Filtering
|
||||||
|
|
||||||
@ -56,6 +56,47 @@ The GraphQL API employs the same filtering logic as the UI and REST API. Filters
|
|||||||
```
|
```
|
||||||
{"query": "query {site_list(region:\"north-carolina\", status:\"active\") {name}}"}
|
{"query": "query {site_list(region:\"north-carolina\", status:\"active\") {name}}"}
|
||||||
```
|
```
|
||||||
|
In addition, filtering can be done on list of related objects as shown in the following query:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
device_list {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
interfaces(enabled: true) {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multiple Return Types
|
||||||
|
|
||||||
|
Certain queries can return multiple types of objects, for example cable terminations can return circuit terminations, console ports and many others. These can be queried using [inline fragments](https://graphql.org/learn/schema/#union-types) as shown below:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
cable_list {
|
||||||
|
id
|
||||||
|
a_terminations {
|
||||||
|
... on CircuitTerminationType {
|
||||||
|
id
|
||||||
|
class_type
|
||||||
|
}
|
||||||
|
... on ConsolePortType {
|
||||||
|
id
|
||||||
|
class_type
|
||||||
|
}
|
||||||
|
... on ConsoleServerPortType {
|
||||||
|
id
|
||||||
|
class_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
The field "class_type" is an easy way to distinguish what type of object it is when viewing the returned data, or when filtering. It contains the class name, for example "CircuitTermination" or "ConsoleServerPort".
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
|
@ -144,73 +144,73 @@ class MyModelFilterForm(NetBoxModelFilterSetForm):
|
|||||||
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.
|
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
|
::: utilities.forms.ColorField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: utilities.forms.CommentField
|
::: utilities.forms.CommentField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: utilities.forms.JSONField
|
::: utilities.forms.JSONField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: utilities.forms.MACAddressField
|
::: utilities.forms.MACAddressField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: utilities.forms.SlugField
|
::: utilities.forms.SlugField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
## Choice Fields
|
## Choice Fields
|
||||||
|
|
||||||
::: utilities.forms.ChoiceField
|
::: utilities.forms.ChoiceField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: utilities.forms.MultipleChoiceField
|
::: utilities.forms.MultipleChoiceField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
## Dynamic Object Fields
|
## Dynamic Object Fields
|
||||||
|
|
||||||
::: utilities.forms.DynamicModelChoiceField
|
::: utilities.forms.DynamicModelChoiceField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: utilities.forms.DynamicModelMultipleChoiceField
|
::: utilities.forms.DynamicModelMultipleChoiceField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
## Content Type Fields
|
## Content Type Fields
|
||||||
|
|
||||||
::: utilities.forms.ContentTypeChoiceField
|
::: utilities.forms.ContentTypeChoiceField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: utilities.forms.ContentTypeMultipleChoiceField
|
::: utilities.forms.ContentTypeMultipleChoiceField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
## CSV Import Fields
|
## CSV Import Fields
|
||||||
|
|
||||||
::: utilities.forms.CSVChoiceField
|
::: utilities.forms.CSVChoiceField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: utilities.forms.CSVMultipleChoiceField
|
::: utilities.forms.CSVMultipleChoiceField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: utilities.forms.CSVModelChoiceField
|
::: utilities.forms.CSVModelChoiceField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: utilities.forms.CSVContentTypeField
|
::: utilities.forms.CSVContentTypeField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: utilities.forms.CSVMultipleContentTypeField
|
::: utilities.forms.CSVMultipleContentTypeField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
@ -32,11 +32,11 @@ schema = MyQuery
|
|||||||
NetBox provides two object type classes for use by plugins.
|
NetBox provides two object type classes for use by plugins.
|
||||||
|
|
||||||
::: netbox.graphql.types.BaseObjectType
|
::: netbox.graphql.types.BaseObjectType
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: netbox.graphql.types.NetBoxObjectType
|
::: netbox.graphql.types.NetBoxObjectType
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
## GraphQL Fields
|
## GraphQL Fields
|
||||||
@ -44,9 +44,9 @@ NetBox provides two object type classes for use by plugins.
|
|||||||
NetBox provides two field classes for use by plugins.
|
NetBox provides two field classes for use by plugins.
|
||||||
|
|
||||||
::: netbox.graphql.fields.ObjectField
|
::: netbox.graphql.fields.ObjectField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: netbox.graphql.fields.ObjectListField
|
::: netbox.graphql.fields.ObjectListField
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
@ -108,6 +108,7 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
|
|||||||
| `max_version` | Maximum version of NetBox with which the plugin is compatible |
|
| `max_version` | Maximum version of NetBox with which the plugin is compatible |
|
||||||
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
|
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
|
||||||
| `queues` | A list of custom background task queues to create |
|
| `queues` | A list of custom background task queues to create |
|
||||||
|
| `search_extensions` | The dotted path to the list of search index classes (default: `search.indexes`) |
|
||||||
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
|
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
|
||||||
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
|
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
|
||||||
| `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) |
|
| `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) |
|
||||||
|
29
docs/plugins/development/search.md
Normal file
29
docs/plugins/development/search.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Search
|
||||||
|
|
||||||
|
Plugins can define and register their own models to extend NetBox's core search functionality. Typically, a plugin will include a file named `search.py`, which holds all search indexes for its models (see the example below).
|
||||||
|
|
||||||
|
```python
|
||||||
|
# search.py
|
||||||
|
from netbox.search import SearchMixin
|
||||||
|
from .filters import MyModelFilterSet
|
||||||
|
from .tables import MyModelTable
|
||||||
|
from .models import MyModel
|
||||||
|
|
||||||
|
class MyModelIndex(SearchMixin):
|
||||||
|
model = MyModel
|
||||||
|
queryset = MyModel.objects.all()
|
||||||
|
filterset = MyModelFilterSet
|
||||||
|
table = MyModelTable
|
||||||
|
url = 'plugins:myplugin:mymodel_list'
|
||||||
|
```
|
||||||
|
|
||||||
|
To register one or more indexes with NetBox, define a list named `indexes` at the end of this file:
|
||||||
|
|
||||||
|
```python
|
||||||
|
indexes = [MyModelIndex]
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
The path to the list of search indexes can be modified by setting `search_indexes` in the PluginConfig instance.
|
||||||
|
|
||||||
|
::: netbox.search.SearchIndex
|
@ -52,38 +52,38 @@ This will automatically apply any user-specific preferences for the table. (If u
|
|||||||
The table column classes listed below are supported for use in plugins. These classes can be imported from `netbox.tables.columns`.
|
The table column classes listed below are supported for use in plugins. These classes can be imported from `netbox.tables.columns`.
|
||||||
|
|
||||||
::: netbox.tables.BooleanColumn
|
::: netbox.tables.BooleanColumn
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: netbox.tables.ChoiceFieldColumn
|
::: netbox.tables.ChoiceFieldColumn
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: netbox.tables.ColorColumn
|
::: netbox.tables.ColorColumn
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: netbox.tables.ColoredLabelColumn
|
::: netbox.tables.ColoredLabelColumn
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: netbox.tables.ContentTypeColumn
|
::: netbox.tables.ContentTypeColumn
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: netbox.tables.ContentTypesColumn
|
::: netbox.tables.ContentTypesColumn
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: netbox.tables.MarkdownColumn
|
::: netbox.tables.MarkdownColumn
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: netbox.tables.TagColumn
|
::: netbox.tables.TagColumn
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: netbox.tables.TemplateColumn
|
::: netbox.tables.TemplateColumn
|
||||||
selection:
|
options:
|
||||||
members:
|
members:
|
||||||
- __init__
|
- __init__
|
||||||
|
@ -84,24 +84,24 @@ Below are the class definitions for NetBox's object views. These views handle CR
|
|||||||
::: netbox.views.generic.base.BaseObjectView
|
::: netbox.views.generic.base.BaseObjectView
|
||||||
|
|
||||||
::: netbox.views.generic.ObjectView
|
::: netbox.views.generic.ObjectView
|
||||||
selection:
|
options:
|
||||||
members:
|
members:
|
||||||
- get_object
|
- get_object
|
||||||
- get_template_name
|
- get_template_name
|
||||||
|
|
||||||
::: netbox.views.generic.ObjectEditView
|
::: netbox.views.generic.ObjectEditView
|
||||||
selection:
|
options:
|
||||||
members:
|
members:
|
||||||
- get_object
|
- get_object
|
||||||
- alter_object
|
- alter_object
|
||||||
|
|
||||||
::: netbox.views.generic.ObjectDeleteView
|
::: netbox.views.generic.ObjectDeleteView
|
||||||
selection:
|
options:
|
||||||
members:
|
members:
|
||||||
- get_object
|
- get_object
|
||||||
|
|
||||||
::: netbox.views.generic.ObjectChildrenView
|
::: netbox.views.generic.ObjectChildrenView
|
||||||
selection:
|
options:
|
||||||
members:
|
members:
|
||||||
- get_children
|
- get_children
|
||||||
- prep_table_data
|
- prep_table_data
|
||||||
@ -113,22 +113,22 @@ Below are the class definitions for NetBox's multi-object views. These views han
|
|||||||
::: netbox.views.generic.base.BaseMultiObjectView
|
::: netbox.views.generic.base.BaseMultiObjectView
|
||||||
|
|
||||||
::: netbox.views.generic.ObjectListView
|
::: netbox.views.generic.ObjectListView
|
||||||
selection:
|
options:
|
||||||
members:
|
members:
|
||||||
- get_table
|
- get_table
|
||||||
- export_table
|
- export_table
|
||||||
- export_template
|
- export_template
|
||||||
|
|
||||||
::: netbox.views.generic.BulkImportView
|
::: netbox.views.generic.BulkImportView
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: netbox.views.generic.BulkEditView
|
::: netbox.views.generic.BulkEditView
|
||||||
selection:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: netbox.views.generic.BulkDeleteView
|
::: netbox.views.generic.BulkDeleteView
|
||||||
selection:
|
options:
|
||||||
members:
|
members:
|
||||||
- get_form
|
- get_form
|
||||||
|
|
||||||
@ -137,12 +137,12 @@ Below are the class definitions for NetBox's multi-object views. These views han
|
|||||||
These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path.
|
These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path.
|
||||||
|
|
||||||
::: netbox.views.generic.ObjectChangeLogView
|
::: netbox.views.generic.ObjectChangeLogView
|
||||||
selection:
|
options:
|
||||||
members:
|
members:
|
||||||
- get_form
|
- get_form
|
||||||
|
|
||||||
::: netbox.views.generic.ObjectJournalView
|
::: netbox.views.generic.ObjectJournalView
|
||||||
selection:
|
options:
|
||||||
members:
|
members:
|
||||||
- get_form
|
- get_form
|
||||||
|
|
||||||
|
@ -2,6 +2,21 @@
|
|||||||
|
|
||||||
## v3.3.6 (FUTURE)
|
## v3.3.6 (FUTURE)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#9722](https://github.com/netbox-community/netbox/issues/9722) - Add LDAP configuration parameters to specify certificates
|
||||||
|
* [#10685](https://github.com/netbox-community/netbox/issues/10685) - Position A/Z termination cards above the fold under circuit view
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#9669](https://github.com/netbox-community/netbox/issues/9669) - Strip colons from usernames when using remote authentication
|
||||||
|
* [#10575](https://github.com/netbox-community/netbox/issues/10575) - Include OIDC dependencies for python-social-auth
|
||||||
|
* [#10584](https://github.com/netbox-community/netbox/issues/10584) - Fix service clone link
|
||||||
|
* [#10643](https://github.com/netbox-community/netbox/issues/10643) - Ensure consistent display of custom fields for all model forms
|
||||||
|
* [#10646](https://github.com/netbox-community/netbox/issues/10646) - Fix filtering of power feed by power panel when connecting a cable
|
||||||
|
* [#10655](https://github.com/netbox-community/netbox/issues/10655) - Correct display of assigned contacts in object tables
|
||||||
|
* [#10712](https://github.com/netbox-community/netbox/issues/10712) - Fix ModuleNotFoundError exception when generating API schema under Python 3.9+
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v3.3.5 (2022-10-05)
|
## v3.3.5 (2022-10-05)
|
||||||
|
@ -17,15 +17,19 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
|
|||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
|
* [#8245](https://github.com/netbox-community/netbox/issues/8245) - Enable GraphQL filtering of related objects
|
||||||
* [#9249](https://github.com/netbox-community/netbox/issues/9249) - Device and virtual machine names are no longer case-sensitive
|
* [#9249](https://github.com/netbox-community/netbox/issues/9249) - Device and virtual machine names are no longer case-sensitive
|
||||||
* [#9478](https://github.com/netbox-community/netbox/issues/9478) - Add `link_peers` field to GraphQL types for cabled objects
|
* [#9478](https://github.com/netbox-community/netbox/issues/9478) - Add `link_peers` field to GraphQL types for cabled objects
|
||||||
* [#9654](https://github.com/netbox-community/netbox/issues/9654) - Add `weight` field to racks, device types, and module types
|
* [#9654](https://github.com/netbox-community/netbox/issues/9654) - Add `weight` field to racks, device types, and module types
|
||||||
|
* [#9817](https://github.com/netbox-community/netbox/issues/9817) - Add `assigned_object` field to GraphQL type for IP addresses and L2VPN terminations
|
||||||
* [#9892](https://github.com/netbox-community/netbox/issues/9892) - Add optional `name` field for FHRP groups
|
* [#9892](https://github.com/netbox-community/netbox/issues/9892) - Add optional `name` field for FHRP groups
|
||||||
* [#10348](https://github.com/netbox-community/netbox/issues/10348) - Add decimal custom field type
|
* [#10348](https://github.com/netbox-community/netbox/issues/10348) - Add decimal custom field type
|
||||||
* [#10556](https://github.com/netbox-community/netbox/issues/10556) - Include a `display` field in all GraphQL object types
|
* [#10556](https://github.com/netbox-community/netbox/issues/10556) - Include a `display` field in all GraphQL object types
|
||||||
|
* [#10595](https://github.com/netbox-community/netbox/issues/10595) - Add GraphQL relationships for additional generic foreign key fields
|
||||||
|
|
||||||
### Plugins API
|
### Plugins API
|
||||||
|
|
||||||
|
* [#8927](https://github.com/netbox-community/netbox/issues/8927) - Enable inclusion of plugin models in global search via `SearchIndex`
|
||||||
* [#9071](https://github.com/netbox-community/netbox/issues/9071) - Introduce `PluginMenu` for top-level plugin navigation menus
|
* [#9071](https://github.com/netbox-community/netbox/issues/9071) - Introduce `PluginMenu` for top-level plugin navigation menus
|
||||||
* [#9072](https://github.com/netbox-community/netbox/issues/9072) - Enable registration of tabbed plugin views for core NetBox models
|
* [#9072](https://github.com/netbox-community/netbox/issues/9072) - Enable registration of tabbed plugin views for core NetBox models
|
||||||
* [#9880](https://github.com/netbox-community/netbox/issues/9880) - Introduce `django_apps` plugin configuration parameter
|
* [#9880](https://github.com/netbox-community/netbox/issues/9880) - Introduce `django_apps` plugin configuration parameter
|
||||||
@ -36,6 +40,7 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
|
|||||||
* [#9045](https://github.com/netbox-community/netbox/issues/9045) - Remove legacy ASN field from provider model
|
* [#9045](https://github.com/netbox-community/netbox/issues/9045) - Remove legacy ASN field from provider model
|
||||||
* [#9046](https://github.com/netbox-community/netbox/issues/9046) - Remove legacy contact fields from provider model
|
* [#9046](https://github.com/netbox-community/netbox/issues/9046) - Remove legacy contact fields from provider model
|
||||||
* [#10358](https://github.com/netbox-community/netbox/issues/10358) - Raise minimum required PostgreSQL version from 10 to 11
|
* [#10358](https://github.com/netbox-community/netbox/issues/10358) - Raise minimum required PostgreSQL version from 10 to 11
|
||||||
|
* [#10699](https://github.com/netbox-community/netbox/issues/10699) - Remove custom `import_object()` function
|
||||||
|
|
||||||
### REST API Changes
|
### REST API Changes
|
||||||
|
|
||||||
@ -54,3 +59,20 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
|
|||||||
|
|
||||||
* All object types now include a `display` field
|
* All object types now include a `display` field
|
||||||
* All cabled object types now include a `link_peers` field
|
* All cabled object types now include a `link_peers` field
|
||||||
|
* Add a `contacts` relationship for all relevant models
|
||||||
|
* dcim.Cable
|
||||||
|
* Add A/B terminations fields
|
||||||
|
* dcim.CableTermination
|
||||||
|
* Add `termination` field
|
||||||
|
* dcim.InventoryItem
|
||||||
|
* Add `component` field
|
||||||
|
* dcim.InventoryItemTemplate
|
||||||
|
* Add `component` field
|
||||||
|
* ipam.FHRPGroupAssignment
|
||||||
|
* Add `interface` field
|
||||||
|
* ipam.IPAddress
|
||||||
|
* Add `assigned_object` field
|
||||||
|
* ipam.L2VPNTermination
|
||||||
|
* Add `assigned_object` field
|
||||||
|
* ipam.VLANGroupType
|
||||||
|
* Add `scope` field
|
||||||
|
@ -30,7 +30,7 @@ plugins:
|
|||||||
- os.chdir('netbox/')
|
- os.chdir('netbox/')
|
||||||
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
|
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
|
||||||
- django.setup()
|
- django.setup()
|
||||||
rendering:
|
options:
|
||||||
heading_level: 3
|
heading_level: 3
|
||||||
members_order: source
|
members_order: source
|
||||||
show_root_heading: true
|
show_root_heading: true
|
||||||
@ -132,6 +132,7 @@ nav:
|
|||||||
- GraphQL API: 'plugins/development/graphql-api.md'
|
- GraphQL API: 'plugins/development/graphql-api.md'
|
||||||
- Background Tasks: 'plugins/development/background-tasks.md'
|
- Background Tasks: 'plugins/development/background-tasks.md'
|
||||||
- Exceptions: 'plugins/development/exceptions.md'
|
- Exceptions: 'plugins/development/exceptions.md'
|
||||||
|
- Search: 'plugins/development/search.md'
|
||||||
- Administration:
|
- Administration:
|
||||||
- Authentication:
|
- Authentication:
|
||||||
- Overview: 'administration/authentication/overview.md'
|
- Overview: 'administration/authentication/overview.md'
|
||||||
|
@ -6,4 +6,4 @@ class CircuitsConfig(AppConfig):
|
|||||||
verbose_name = "Circuits"
|
verbose_name = "Circuits"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import circuits.signals
|
from . import signals, search
|
||||||
|
@ -64,6 +64,12 @@ class ProviderNetworkForm(NetBoxModelForm):
|
|||||||
class CircuitTypeForm(NetBoxModelForm):
|
class CircuitTypeForm(NetBoxModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Circuit Type', (
|
||||||
|
'name', 'slug', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
import graphene
|
||||||
|
|
||||||
from circuits import filtersets, models
|
from circuits import filtersets, models
|
||||||
from dcim.graphql.mixins import CabledObjectMixin
|
from dcim.graphql.mixins import CabledObjectMixin
|
||||||
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
|
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin, ContactsMixin
|
||||||
from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
|
from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -20,8 +22,7 @@ class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, Ob
|
|||||||
filterset_class = filtersets.CircuitTerminationFilterSet
|
filterset_class = filtersets.CircuitTerminationFilterSet
|
||||||
|
|
||||||
|
|
||||||
class CircuitType(NetBoxObjectType):
|
class CircuitType(NetBoxObjectType, ContactsMixin):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Circuit
|
model = models.Circuit
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
@ -36,7 +37,7 @@ class CircuitTypeType(OrganizationalObjectType):
|
|||||||
filterset_class = filtersets.CircuitTypeFilterSet
|
filterset_class = filtersets.CircuitTypeFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ProviderType(NetBoxObjectType):
|
class ProviderType(NetBoxObjectType, ContactsMixin):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Provider
|
model = models.Provider
|
||||||
|
34
netbox/circuits/search.py
Normal file
34
netbox/circuits/search.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import circuits.filtersets
|
||||||
|
import circuits.tables
|
||||||
|
from circuits.models import Circuit, Provider, ProviderNetwork
|
||||||
|
from netbox.search import SearchIndex, register_search
|
||||||
|
from utilities.utils import count_related
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class ProviderIndex(SearchIndex):
|
||||||
|
model = Provider
|
||||||
|
queryset = Provider.objects.annotate(count_circuits=count_related(Circuit, 'provider'))
|
||||||
|
filterset = circuits.filtersets.ProviderFilterSet
|
||||||
|
table = circuits.tables.ProviderTable
|
||||||
|
url = 'circuits:provider_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class CircuitIndex(SearchIndex):
|
||||||
|
model = Circuit
|
||||||
|
queryset = Circuit.objects.prefetch_related(
|
||||||
|
'type', 'provider', 'tenant', 'tenant__group', 'terminations__site'
|
||||||
|
)
|
||||||
|
filterset = circuits.filtersets.CircuitFilterSet
|
||||||
|
table = circuits.tables.CircuitTable
|
||||||
|
url = 'circuits:circuit_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class ProviderNetworkIndex(SearchIndex):
|
||||||
|
model = ProviderNetwork
|
||||||
|
queryset = ProviderNetwork.objects.prefetch_related('provider')
|
||||||
|
filterset = circuits.filtersets.ProviderNetworkFilterSet
|
||||||
|
table = circuits.tables.ProviderNetworkTable
|
||||||
|
url = 'circuits:providernetwork_list'
|
@ -1,8 +1,9 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
|
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||||
|
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenancyColumnsMixin
|
|
||||||
from .columns import CommitRateColumn
|
from .columns import CommitRateColumn
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -39,7 +40,7 @@ class CircuitTypeTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
|
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
|
||||||
|
|
||||||
|
|
||||||
class CircuitTable(TenancyColumnsMixin, NetBoxTable):
|
class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||||
cid = tables.Column(
|
cid = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='Circuit ID'
|
verbose_name='Circuit ID'
|
||||||
@ -58,9 +59,6 @@ class CircuitTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
)
|
)
|
||||||
commit_rate = CommitRateColumn()
|
commit_rate = CommitRateColumn()
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='circuits:circuit_list'
|
url_name='circuits:circuit_list'
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
|
||||||
|
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
|
from django_tables2.utils import Accessor
|
||||||
|
from tenancy.tables import ContactsColumnMixin
|
||||||
|
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -10,7 +11,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProviderTable(NetBoxTable):
|
class ProviderTable(ContactsColumnMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -31,9 +32,6 @@ class ProviderTable(NetBoxTable):
|
|||||||
verbose_name='Circuits'
|
verbose_name='Circuits'
|
||||||
)
|
)
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='circuits:provider_list'
|
url_name='circuits:provider_list'
|
||||||
)
|
)
|
||||||
|
@ -8,7 +8,7 @@ class DCIMConfig(AppConfig):
|
|||||||
verbose_name = "DCIM"
|
verbose_name = "DCIM"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import dcim.signals
|
from . import signals, search
|
||||||
from .models import CableTermination
|
from .models import CableTermination
|
||||||
|
|
||||||
# Register denormalized fields
|
# Register denormalized fields
|
||||||
|
@ -108,7 +108,7 @@ def get_cable_form(a_type, b_type):
|
|||||||
label='Power Feed',
|
label='Power Feed',
|
||||||
disabled_indicator='_occupied',
|
disabled_indicator='_occupied',
|
||||||
query_params={
|
query_params={
|
||||||
'powerpanel_id': f'$termination_{cable_end}_powerpanel',
|
'power_panel_id': f'$termination_{cable_end}_powerpanel',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -78,6 +78,12 @@ class RegionForm(NetBoxModelForm):
|
|||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Region', (
|
||||||
|
'parent', 'name', 'slug', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Region
|
model = Region
|
||||||
fields = (
|
fields = (
|
||||||
@ -92,6 +98,12 @@ class SiteGroupForm(NetBoxModelForm):
|
|||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Site Group', (
|
||||||
|
'parent', 'name', 'slug', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
fields = (
|
fields = (
|
||||||
@ -213,6 +225,12 @@ class LocationForm(TenancyForm, NetBoxModelForm):
|
|||||||
class RackRoleForm(NetBoxModelForm):
|
class RackRoleForm(NetBoxModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Rack Role', (
|
||||||
|
'name', 'slug', 'color', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fields = [
|
fields = [
|
||||||
@ -341,6 +359,12 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
|
|||||||
class ManufacturerForm(NetBoxModelForm):
|
class ManufacturerForm(NetBoxModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Manufacturer', (
|
||||||
|
'name', 'slug', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fields = [
|
fields = [
|
||||||
@ -413,6 +437,12 @@ class ModuleTypeForm(NetBoxModelForm):
|
|||||||
class DeviceRoleForm(NetBoxModelForm):
|
class DeviceRoleForm(NetBoxModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Device Role', (
|
||||||
|
'name', 'slug', 'color', 'vm_role', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = [
|
fields = [
|
||||||
@ -429,6 +459,13 @@ class PlatformForm(NetBoxModelForm):
|
|||||||
max_length=64
|
max_length=64
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Platform', (
|
||||||
|
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
|
||||||
|
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = [
|
fields = [
|
||||||
@ -1584,6 +1621,12 @@ class InventoryItemForm(DeviceComponentForm):
|
|||||||
class InventoryItemRoleForm(NetBoxModelForm):
|
class InventoryItemRoleForm(NetBoxModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Inventory Item Role', (
|
||||||
|
'name', 'slug', 'color', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItemRole
|
model = InventoryItemRole
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -2,24 +2,38 @@ import graphene
|
|||||||
from circuits.graphql.types import CircuitTerminationType
|
from circuits.graphql.types import CircuitTerminationType
|
||||||
from circuits.models import CircuitTermination
|
from circuits.models import CircuitTermination
|
||||||
from dcim.graphql.types import (
|
from dcim.graphql.types import (
|
||||||
|
ConsolePortTemplateType,
|
||||||
ConsolePortType,
|
ConsolePortType,
|
||||||
|
ConsoleServerPortTemplateType,
|
||||||
ConsoleServerPortType,
|
ConsoleServerPortType,
|
||||||
|
FrontPortTemplateType,
|
||||||
FrontPortType,
|
FrontPortType,
|
||||||
|
InterfaceTemplateType,
|
||||||
InterfaceType,
|
InterfaceType,
|
||||||
PowerFeedType,
|
PowerFeedType,
|
||||||
|
PowerOutletTemplateType,
|
||||||
PowerOutletType,
|
PowerOutletType,
|
||||||
|
PowerPortTemplateType,
|
||||||
PowerPortType,
|
PowerPortType,
|
||||||
|
RearPortTemplateType,
|
||||||
RearPortType,
|
RearPortType,
|
||||||
)
|
)
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort,
|
ConsolePort,
|
||||||
|
ConsolePortTemplate,
|
||||||
ConsoleServerPort,
|
ConsoleServerPort,
|
||||||
|
ConsoleServerPortTemplate,
|
||||||
FrontPort,
|
FrontPort,
|
||||||
|
FrontPortTemplate,
|
||||||
Interface,
|
Interface,
|
||||||
|
InterfaceTemplate,
|
||||||
PowerFeed,
|
PowerFeed,
|
||||||
PowerOutlet,
|
PowerOutlet,
|
||||||
|
PowerOutletTemplate,
|
||||||
PowerPort,
|
PowerPort,
|
||||||
|
PowerPortTemplate,
|
||||||
RearPort,
|
RearPort,
|
||||||
|
RearPortTemplate,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -57,3 +71,99 @@ class LinkPeerType(graphene.Union):
|
|||||||
return PowerPortType
|
return PowerPortType
|
||||||
if type(instance) == RearPort:
|
if type(instance) == RearPort:
|
||||||
return RearPortType
|
return RearPortType
|
||||||
|
|
||||||
|
|
||||||
|
class CableTerminationTerminationType(graphene.Union):
|
||||||
|
class Meta:
|
||||||
|
types = (
|
||||||
|
CircuitTerminationType,
|
||||||
|
ConsolePortType,
|
||||||
|
ConsoleServerPortType,
|
||||||
|
FrontPortType,
|
||||||
|
InterfaceType,
|
||||||
|
PowerFeedType,
|
||||||
|
PowerOutletType,
|
||||||
|
PowerPortType,
|
||||||
|
RearPortType,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resolve_type(cls, instance, info):
|
||||||
|
if type(instance) == CircuitTermination:
|
||||||
|
return CircuitTerminationType
|
||||||
|
if type(instance) == ConsolePortType:
|
||||||
|
return ConsolePortType
|
||||||
|
if type(instance) == ConsoleServerPort:
|
||||||
|
return ConsoleServerPortType
|
||||||
|
if type(instance) == FrontPort:
|
||||||
|
return FrontPortType
|
||||||
|
if type(instance) == Interface:
|
||||||
|
return InterfaceType
|
||||||
|
if type(instance) == PowerFeed:
|
||||||
|
return PowerFeedType
|
||||||
|
if type(instance) == PowerOutlet:
|
||||||
|
return PowerOutletType
|
||||||
|
if type(instance) == PowerPort:
|
||||||
|
return PowerPortType
|
||||||
|
if type(instance) == RearPort:
|
||||||
|
return RearPortType
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemTemplateComponentType(graphene.Union):
|
||||||
|
class Meta:
|
||||||
|
types = (
|
||||||
|
ConsolePortTemplateType,
|
||||||
|
ConsoleServerPortTemplateType,
|
||||||
|
FrontPortTemplateType,
|
||||||
|
InterfaceTemplateType,
|
||||||
|
PowerOutletTemplateType,
|
||||||
|
PowerPortTemplateType,
|
||||||
|
RearPortTemplateType,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resolve_type(cls, instance, info):
|
||||||
|
if type(instance) == ConsolePortTemplate:
|
||||||
|
return ConsolePortTemplateType
|
||||||
|
if type(instance) == ConsoleServerPortTemplate:
|
||||||
|
return ConsoleServerPortTemplateType
|
||||||
|
if type(instance) == FrontPortTemplate:
|
||||||
|
return FrontPortTemplateType
|
||||||
|
if type(instance) == InterfaceTemplate:
|
||||||
|
return InterfaceTemplateType
|
||||||
|
if type(instance) == PowerOutletTemplate:
|
||||||
|
return PowerOutletTemplateType
|
||||||
|
if type(instance) == PowerPortTemplate:
|
||||||
|
return PowerPortTemplateType
|
||||||
|
if type(instance) == RearPortTemplate:
|
||||||
|
return RearPortTemplateType
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemComponentType(graphene.Union):
|
||||||
|
class Meta:
|
||||||
|
types = (
|
||||||
|
ConsolePortType,
|
||||||
|
ConsoleServerPortType,
|
||||||
|
FrontPortType,
|
||||||
|
InterfaceType,
|
||||||
|
PowerOutletType,
|
||||||
|
PowerPortType,
|
||||||
|
RearPortType,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resolve_type(cls, instance, info):
|
||||||
|
if type(instance) == ConsolePort:
|
||||||
|
return ConsolePortType
|
||||||
|
if type(instance) == ConsoleServerPort:
|
||||||
|
return ConsoleServerPortType
|
||||||
|
if type(instance) == FrontPort:
|
||||||
|
return FrontPortType
|
||||||
|
if type(instance) == Interface:
|
||||||
|
return InterfaceType
|
||||||
|
if type(instance) == PowerOutlet:
|
||||||
|
return PowerOutletType
|
||||||
|
if type(instance) == PowerPort:
|
||||||
|
return PowerPortType
|
||||||
|
if type(instance) == RearPort:
|
||||||
|
return RearPortType
|
||||||
|
@ -2,7 +2,7 @@ import graphene
|
|||||||
|
|
||||||
from dcim import filtersets, models
|
from dcim import filtersets, models
|
||||||
from extras.graphql.mixins import (
|
from extras.graphql.mixins import (
|
||||||
ChangelogMixin, ConfigContextMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin,
|
ChangelogMixin, ConfigContextMixin, ContactsMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin,
|
||||||
)
|
)
|
||||||
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
|
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
|
||||||
from netbox.graphql.scalars import BigInt
|
from netbox.graphql.scalars import BigInt
|
||||||
@ -87,6 +87,8 @@ class ComponentTemplateObjectType(
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CableType(NetBoxObjectType):
|
class CableType(NetBoxObjectType):
|
||||||
|
a_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType')
|
||||||
|
b_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Cable
|
model = models.Cable
|
||||||
@ -99,12 +101,19 @@ class CableType(NetBoxObjectType):
|
|||||||
def resolve_length_unit(self, info):
|
def resolve_length_unit(self, info):
|
||||||
return self.length_unit or None
|
return self.length_unit or None
|
||||||
|
|
||||||
|
def resolve_a_terminations(self, info):
|
||||||
|
return self.a_terminations
|
||||||
|
|
||||||
|
def resolve_b_terminations(self, info):
|
||||||
|
return self.b_terminations
|
||||||
|
|
||||||
|
|
||||||
class CableTerminationType(NetBoxObjectType):
|
class CableTerminationType(NetBoxObjectType):
|
||||||
|
termination = graphene.Field('dcim.graphql.gfk_mixins.CableTerminationTerminationType')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.CableTermination
|
model = models.CableTermination
|
||||||
fields = '__all__'
|
exclude = ('termination_type', 'termination_id')
|
||||||
filterset_class = filtersets.CableTerminationFilterSet
|
filterset_class = filtersets.CableTerminationFilterSet
|
||||||
|
|
||||||
|
|
||||||
@ -152,7 +161,7 @@ class ConsoleServerPortTemplateType(ComponentTemplateObjectType):
|
|||||||
return self.type or None
|
return self.type or None
|
||||||
|
|
||||||
|
|
||||||
class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, NetBoxObjectType):
|
class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Device
|
model = models.Device
|
||||||
@ -183,10 +192,11 @@ class DeviceBayTemplateType(ComponentTemplateObjectType):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryItemTemplateType(ComponentTemplateObjectType):
|
class InventoryItemTemplateType(ComponentTemplateObjectType):
|
||||||
|
component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemTemplateComponentType')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.InventoryItemTemplate
|
model = models.InventoryItemTemplate
|
||||||
fields = '__all__'
|
exclude = ('component_type', 'component_id')
|
||||||
filterset_class = filtersets.InventoryItemTemplateFilterSet
|
filterset_class = filtersets.InventoryItemTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
@ -269,10 +279,11 @@ class InterfaceTemplateType(ComponentTemplateObjectType):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryItemType(ComponentObjectType):
|
class InventoryItemType(ComponentObjectType):
|
||||||
|
component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemComponentType')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.InventoryItem
|
model = models.InventoryItem
|
||||||
fields = '__all__'
|
exclude = ('component_type', 'component_id')
|
||||||
filterset_class = filtersets.InventoryItemFilterSet
|
filterset_class = filtersets.InventoryItemFilterSet
|
||||||
|
|
||||||
|
|
||||||
@ -284,7 +295,7 @@ class InventoryItemRoleType(OrganizationalObjectType):
|
|||||||
filterset_class = filtersets.InventoryItemRoleFilterSet
|
filterset_class = filtersets.InventoryItemRoleFilterSet
|
||||||
|
|
||||||
|
|
||||||
class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectType):
|
class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Location
|
model = models.Location
|
||||||
@ -292,7 +303,7 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectT
|
|||||||
filterset_class = filtersets.LocationFilterSet
|
filterset_class = filtersets.LocationFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ManufacturerType(OrganizationalObjectType):
|
class ManufacturerType(OrganizationalObjectType, ContactsMixin):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Manufacturer
|
model = models.Manufacturer
|
||||||
@ -379,7 +390,7 @@ class PowerOutletTemplateType(ComponentTemplateObjectType):
|
|||||||
return self.type or None
|
return self.type or None
|
||||||
|
|
||||||
|
|
||||||
class PowerPanelType(NetBoxObjectType):
|
class PowerPanelType(NetBoxObjectType, ContactsMixin):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.PowerPanel
|
model = models.PowerPanel
|
||||||
@ -409,7 +420,7 @@ class PowerPortTemplateType(ComponentTemplateObjectType):
|
|||||||
return self.type or None
|
return self.type or None
|
||||||
|
|
||||||
|
|
||||||
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
|
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Rack
|
model = models.Rack
|
||||||
@ -458,7 +469,7 @@ class RearPortTemplateType(ComponentTemplateObjectType):
|
|||||||
filterset_class = filtersets.RearPortTemplateFilterSet
|
filterset_class = filtersets.RearPortTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class RegionType(VLANGroupsMixin, OrganizationalObjectType):
|
class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Region
|
model = models.Region
|
||||||
@ -466,7 +477,7 @@ class RegionType(VLANGroupsMixin, OrganizationalObjectType):
|
|||||||
filterset_class = filtersets.RegionFilterSet
|
filterset_class = filtersets.RegionFilterSet
|
||||||
|
|
||||||
|
|
||||||
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
|
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
|
||||||
asn = graphene.Field(BigInt)
|
asn = graphene.Field(BigInt)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -475,7 +486,7 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
|
|||||||
filterset_class = filtersets.SiteFilterSet
|
filterset_class = filtersets.SiteFilterSet
|
||||||
|
|
||||||
|
|
||||||
class SiteGroupType(VLANGroupsMixin, OrganizationalObjectType):
|
class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.SiteGroup
|
model = models.SiteGroup
|
||||||
|
143
netbox/dcim/search.py
Normal file
143
netbox/dcim/search.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import dcim.filtersets
|
||||||
|
import dcim.tables
|
||||||
|
from dcim.models import (
|
||||||
|
Cable,
|
||||||
|
Device,
|
||||||
|
DeviceType,
|
||||||
|
Location,
|
||||||
|
Module,
|
||||||
|
ModuleType,
|
||||||
|
PowerFeed,
|
||||||
|
Rack,
|
||||||
|
RackReservation,
|
||||||
|
Site,
|
||||||
|
VirtualChassis,
|
||||||
|
)
|
||||||
|
from netbox.search import SearchIndex, register_search
|
||||||
|
from utilities.utils import count_related
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class SiteIndex(SearchIndex):
|
||||||
|
model = Site
|
||||||
|
queryset = Site.objects.prefetch_related('region', 'tenant', 'tenant__group')
|
||||||
|
filterset = dcim.filtersets.SiteFilterSet
|
||||||
|
table = dcim.tables.SiteTable
|
||||||
|
url = 'dcim:site_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class RackIndex(SearchIndex):
|
||||||
|
model = Rack
|
||||||
|
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
|
||||||
|
device_count=count_related(Device, 'rack')
|
||||||
|
)
|
||||||
|
filterset = dcim.filtersets.RackFilterSet
|
||||||
|
table = dcim.tables.RackTable
|
||||||
|
url = 'dcim:rack_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class RackReservationIndex(SearchIndex):
|
||||||
|
model = RackReservation
|
||||||
|
queryset = RackReservation.objects.prefetch_related('rack', 'user')
|
||||||
|
filterset = dcim.filtersets.RackReservationFilterSet
|
||||||
|
table = dcim.tables.RackReservationTable
|
||||||
|
url = 'dcim:rackreservation_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class LocationIndex(SearchIndex):
|
||||||
|
model = Location
|
||||||
|
queryset = Location.objects.add_related_count(
|
||||||
|
Location.objects.add_related_count(Location.objects.all(), Device, 'location', 'device_count', cumulative=True),
|
||||||
|
Rack,
|
||||||
|
'location',
|
||||||
|
'rack_count',
|
||||||
|
cumulative=True,
|
||||||
|
).prefetch_related('site')
|
||||||
|
filterset = dcim.filtersets.LocationFilterSet
|
||||||
|
table = dcim.tables.LocationTable
|
||||||
|
url = 'dcim:location_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class DeviceTypeIndex(SearchIndex):
|
||||||
|
model = DeviceType
|
||||||
|
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||||
|
instance_count=count_related(Device, 'device_type')
|
||||||
|
)
|
||||||
|
filterset = dcim.filtersets.DeviceTypeFilterSet
|
||||||
|
table = dcim.tables.DeviceTypeTable
|
||||||
|
url = 'dcim:devicetype_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class DeviceIndex(SearchIndex):
|
||||||
|
model = Device
|
||||||
|
queryset = Device.objects.prefetch_related(
|
||||||
|
'device_type__manufacturer',
|
||||||
|
'device_role',
|
||||||
|
'tenant',
|
||||||
|
'tenant__group',
|
||||||
|
'site',
|
||||||
|
'rack',
|
||||||
|
'primary_ip4',
|
||||||
|
'primary_ip6',
|
||||||
|
)
|
||||||
|
filterset = dcim.filtersets.DeviceFilterSet
|
||||||
|
table = dcim.tables.DeviceTable
|
||||||
|
url = 'dcim:device_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class ModuleTypeIndex(SearchIndex):
|
||||||
|
model = ModuleType
|
||||||
|
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||||
|
instance_count=count_related(Module, 'module_type')
|
||||||
|
)
|
||||||
|
filterset = dcim.filtersets.ModuleTypeFilterSet
|
||||||
|
table = dcim.tables.ModuleTypeTable
|
||||||
|
url = 'dcim:moduletype_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class ModuleIndex(SearchIndex):
|
||||||
|
model = Module
|
||||||
|
queryset = Module.objects.prefetch_related(
|
||||||
|
'module_type__manufacturer',
|
||||||
|
'device',
|
||||||
|
'module_bay',
|
||||||
|
)
|
||||||
|
filterset = dcim.filtersets.ModuleFilterSet
|
||||||
|
table = dcim.tables.ModuleTable
|
||||||
|
url = 'dcim:module_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class VirtualChassisIndex(SearchIndex):
|
||||||
|
model = VirtualChassis
|
||||||
|
queryset = VirtualChassis.objects.prefetch_related('master').annotate(
|
||||||
|
member_count=count_related(Device, 'virtual_chassis')
|
||||||
|
)
|
||||||
|
filterset = dcim.filtersets.VirtualChassisFilterSet
|
||||||
|
table = dcim.tables.VirtualChassisTable
|
||||||
|
url = 'dcim:virtualchassis_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class CableIndex(SearchIndex):
|
||||||
|
model = Cable
|
||||||
|
queryset = Cable.objects.all()
|
||||||
|
filterset = dcim.filtersets.CableFilterSet
|
||||||
|
table = dcim.tables.CableTable
|
||||||
|
url = 'dcim:cable_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class PowerFeedIndex(SearchIndex):
|
||||||
|
model = PowerFeed
|
||||||
|
queryset = PowerFeed.objects.all()
|
||||||
|
filterset = dcim.filtersets.PowerFeedFilterSet
|
||||||
|
table = dcim.tables.PowerFeedTable
|
||||||
|
url = 'dcim:powerfeed_list'
|
@ -1,12 +1,26 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
|
||||||
|
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem,
|
ConsolePort,
|
||||||
InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
|
ConsoleServerPort,
|
||||||
|
Device,
|
||||||
|
DeviceBay,
|
||||||
|
DeviceRole,
|
||||||
|
FrontPort,
|
||||||
|
Interface,
|
||||||
|
InventoryItem,
|
||||||
|
InventoryItemRole,
|
||||||
|
ModuleBay,
|
||||||
|
Platform,
|
||||||
|
PowerOutlet,
|
||||||
|
PowerPort,
|
||||||
|
RearPort,
|
||||||
|
VirtualChassis,
|
||||||
)
|
)
|
||||||
|
from django_tables2.utils import Accessor
|
||||||
|
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||||
|
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenancyColumnsMixin
|
|
||||||
from .template_code import *
|
from .template_code import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -137,7 +151,7 @@ class PlatformTable(NetBoxTable):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceTable(TenancyColumnsMixin, NetBoxTable):
|
class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
order_by=('_name',),
|
order_by=('_name',),
|
||||||
template_code=DEVICE_LINK
|
template_code=DEVICE_LINK
|
||||||
@ -201,9 +215,6 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
verbose_name='VC Priority'
|
verbose_name='VC Priority'
|
||||||
)
|
)
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:device_list'
|
url_name='dcim:device_list'
|
||||||
)
|
)
|
||||||
|
@ -1,10 +1,21 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, DeviceType, FrontPortTemplate, InterfaceTemplate,
|
ConsolePortTemplate,
|
||||||
InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
ConsoleServerPortTemplate,
|
||||||
|
DeviceBayTemplate,
|
||||||
|
DeviceType,
|
||||||
|
FrontPortTemplate,
|
||||||
|
InterfaceTemplate,
|
||||||
|
InventoryItemTemplate,
|
||||||
|
Manufacturer,
|
||||||
|
ModuleBayTemplate,
|
||||||
|
PowerOutletTemplate,
|
||||||
|
PowerPortTemplate,
|
||||||
|
RearPortTemplate,
|
||||||
)
|
)
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
|
from tenancy.tables import ContactsColumnMixin
|
||||||
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, DEVICE_WEIGHT
|
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, DEVICE_WEIGHT
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -27,7 +38,7 @@ __all__ = (
|
|||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
|
|
||||||
class ManufacturerTable(NetBoxTable):
|
class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -43,9 +54,6 @@ class ManufacturerTable(NetBoxTable):
|
|||||||
verbose_name='Platforms'
|
verbose_name='Platforms'
|
||||||
)
|
)
|
||||||
slug = tables.Column()
|
slug = tables.Column()
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:manufacturer_list'
|
url_name='dcim:manufacturer_list'
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
||||||
from dcim.models import PowerFeed, PowerPanel
|
from dcim.models import PowerFeed, PowerPanel
|
||||||
|
from tenancy.tables import ContactsColumnMixin
|
||||||
|
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
|
|
||||||
from .devices import CableTerminationTable
|
from .devices import CableTerminationTable
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -14,7 +16,7 @@ __all__ = (
|
|||||||
# Power panels
|
# Power panels
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerPanelTable(NetBoxTable):
|
class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -29,9 +31,6 @@ class PowerPanelTable(NetBoxTable):
|
|||||||
url_params={'power_panel_id': 'pk'},
|
url_params={'power_panel_id': 'pk'},
|
||||||
verbose_name='Feeds'
|
verbose_name='Feeds'
|
||||||
)
|
)
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:powerpanel_list'
|
url_name='dcim:powerpanel_list'
|
||||||
)
|
)
|
||||||
|
@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
|
|||||||
|
|
||||||
from dcim.models import Rack, RackReservation, RackRole
|
from dcim.models import Rack, RackReservation, RackRole
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenancyColumnsMixin
|
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||||
from .template_code import DEVICE_WEIGHT
|
from .template_code import DEVICE_WEIGHT
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -38,7 +38,7 @@ class RackRoleTable(NetBoxTable):
|
|||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackTable(TenancyColumnsMixin, NetBoxTable):
|
class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
order_by=('_name',),
|
order_by=('_name',),
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -69,9 +69,6 @@ class RackTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Power'
|
verbose_name='Power'
|
||||||
)
|
)
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:rack_list'
|
url_name='dcim:rack_list'
|
||||||
)
|
)
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
||||||
from dcim.models import Location, Region, Site, SiteGroup
|
from dcim.models import Location, Region, Site, SiteGroup
|
||||||
|
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||||
|
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenancyColumnsMixin
|
|
||||||
from .template_code import LOCATION_BUTTONS
|
from .template_code import LOCATION_BUTTONS
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -17,7 +18,7 @@ __all__ = (
|
|||||||
# Regions
|
# Regions
|
||||||
#
|
#
|
||||||
|
|
||||||
class RegionTable(NetBoxTable):
|
class RegionTable(ContactsColumnMixin, NetBoxTable):
|
||||||
name = columns.MPTTColumn(
|
name = columns.MPTTColumn(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -26,9 +27,6 @@ class RegionTable(NetBoxTable):
|
|||||||
url_params={'region_id': 'pk'},
|
url_params={'region_id': 'pk'},
|
||||||
verbose_name='Sites'
|
verbose_name='Sites'
|
||||||
)
|
)
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:region_list'
|
url_name='dcim:region_list'
|
||||||
)
|
)
|
||||||
@ -46,7 +44,7 @@ class RegionTable(NetBoxTable):
|
|||||||
# Site groups
|
# Site groups
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteGroupTable(NetBoxTable):
|
class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
|
||||||
name = columns.MPTTColumn(
|
name = columns.MPTTColumn(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -55,9 +53,6 @@ class SiteGroupTable(NetBoxTable):
|
|||||||
url_params={'group_id': 'pk'},
|
url_params={'group_id': 'pk'},
|
||||||
verbose_name='Sites'
|
verbose_name='Sites'
|
||||||
)
|
)
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:sitegroup_list'
|
url_name='dcim:sitegroup_list'
|
||||||
)
|
)
|
||||||
@ -75,7 +70,7 @@ class SiteGroupTable(NetBoxTable):
|
|||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteTable(TenancyColumnsMixin, NetBoxTable):
|
class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -97,9 +92,6 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
verbose_name='ASN Count'
|
verbose_name='ASN Count'
|
||||||
)
|
)
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:site_list'
|
url_name='dcim:site_list'
|
||||||
)
|
)
|
||||||
@ -118,7 +110,7 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
# Locations
|
# Locations
|
||||||
#
|
#
|
||||||
|
|
||||||
class LocationTable(TenancyColumnsMixin, NetBoxTable):
|
class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||||
name = columns.MPTTColumn(
|
name = columns.MPTTColumn(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -136,9 +128,6 @@ class LocationTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
url_params={'location_id': 'pk'},
|
url_params={'location_id': 'pk'},
|
||||||
verbose_name='Devices'
|
verbose_name='Devices'
|
||||||
)
|
)
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:location_list'
|
url_name='dcim:location_list'
|
||||||
)
|
)
|
||||||
|
@ -951,7 +951,8 @@ class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Console Ports'),
|
label=_('Console Ports'),
|
||||||
badge=lambda obj: obj.consoleporttemplates.count(),
|
badge=lambda obj: obj.consoleporttemplates.count(),
|
||||||
permission='dcim.view_consoleporttemplate'
|
permission='dcim.view_consoleporttemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -964,7 +965,8 @@ class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Console Server Ports'),
|
label=_('Console Server Ports'),
|
||||||
badge=lambda obj: obj.consoleserverporttemplates.count(),
|
badge=lambda obj: obj.consoleserverporttemplates.count(),
|
||||||
permission='dcim.view_consoleserverporttemplate'
|
permission='dcim.view_consoleserverporttemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -977,7 +979,8 @@ class DeviceTypePowerPortsView(DeviceTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Power Ports'),
|
label=_('Power Ports'),
|
||||||
badge=lambda obj: obj.powerporttemplates.count(),
|
badge=lambda obj: obj.powerporttemplates.count(),
|
||||||
permission='dcim.view_powerporttemplate'
|
permission='dcim.view_powerporttemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -990,7 +993,8 @@ class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Power Outlets'),
|
label=_('Power Outlets'),
|
||||||
badge=lambda obj: obj.poweroutlettemplates.count(),
|
badge=lambda obj: obj.poweroutlettemplates.count(),
|
||||||
permission='dcim.view_poweroutlettemplate'
|
permission='dcim.view_poweroutlettemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1003,7 +1007,8 @@ class DeviceTypeInterfacesView(DeviceTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Interfaces'),
|
label=_('Interfaces'),
|
||||||
badge=lambda obj: obj.interfacetemplates.count(),
|
badge=lambda obj: obj.interfacetemplates.count(),
|
||||||
permission='dcim.view_interfacetemplate'
|
permission='dcim.view_interfacetemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1016,7 +1021,8 @@ class DeviceTypeFrontPortsView(DeviceTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Front Ports'),
|
label=_('Front Ports'),
|
||||||
badge=lambda obj: obj.frontporttemplates.count(),
|
badge=lambda obj: obj.frontporttemplates.count(),
|
||||||
permission='dcim.view_frontporttemplate'
|
permission='dcim.view_frontporttemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1029,7 +1035,8 @@ class DeviceTypeRearPortsView(DeviceTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Rear Ports'),
|
label=_('Rear Ports'),
|
||||||
badge=lambda obj: obj.rearporttemplates.count(),
|
badge=lambda obj: obj.rearporttemplates.count(),
|
||||||
permission='dcim.view_rearporttemplate'
|
permission='dcim.view_rearporttemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1042,7 +1049,8 @@ class DeviceTypeModuleBaysView(DeviceTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Module Bays'),
|
label=_('Module Bays'),
|
||||||
badge=lambda obj: obj.modulebaytemplates.count(),
|
badge=lambda obj: obj.modulebaytemplates.count(),
|
||||||
permission='dcim.view_modulebaytemplate'
|
permission='dcim.view_modulebaytemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1055,7 +1063,8 @@ class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Device Bays'),
|
label=_('Device Bays'),
|
||||||
badge=lambda obj: obj.devicebaytemplates.count(),
|
badge=lambda obj: obj.devicebaytemplates.count(),
|
||||||
permission='dcim.view_devicebaytemplate'
|
permission='dcim.view_devicebaytemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1068,7 +1077,8 @@ class DeviceTypeInventoryItemsView(DeviceTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Inventory Items'),
|
label=_('Inventory Items'),
|
||||||
badge=lambda obj: obj.inventoryitemtemplates.count(),
|
badge=lambda obj: obj.inventoryitemtemplates.count(),
|
||||||
permission='dcim.view_invenotryitemtemplate'
|
permission='dcim.view_invenotryitemtemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1168,7 +1178,8 @@ class ModuleTypeConsolePortsView(ModuleTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Console Ports'),
|
label=_('Console Ports'),
|
||||||
badge=lambda obj: obj.consoleporttemplates.count(),
|
badge=lambda obj: obj.consoleporttemplates.count(),
|
||||||
permission='dcim.view_consoleporttemplate'
|
permission='dcim.view_consoleporttemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1181,7 +1192,8 @@ class ModuleTypeConsoleServerPortsView(ModuleTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Console Server Ports'),
|
label=_('Console Server Ports'),
|
||||||
badge=lambda obj: obj.consoleserverporttemplates.count(),
|
badge=lambda obj: obj.consoleserverporttemplates.count(),
|
||||||
permission='dcim.view_consoleserverporttemplate'
|
permission='dcim.view_consoleserverporttemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1194,7 +1206,8 @@ class ModuleTypePowerPortsView(ModuleTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Power Ports'),
|
label=_('Power Ports'),
|
||||||
badge=lambda obj: obj.powerporttemplates.count(),
|
badge=lambda obj: obj.powerporttemplates.count(),
|
||||||
permission='dcim.view_powerporttemplate'
|
permission='dcim.view_powerporttemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1207,7 +1220,8 @@ class ModuleTypePowerOutletsView(ModuleTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Power Outlets'),
|
label=_('Power Outlets'),
|
||||||
badge=lambda obj: obj.poweroutlettemplates.count(),
|
badge=lambda obj: obj.poweroutlettemplates.count(),
|
||||||
permission='dcim.view_poweroutlettemplate'
|
permission='dcim.view_poweroutlettemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1220,7 +1234,8 @@ class ModuleTypeInterfacesView(ModuleTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Interfaces'),
|
label=_('Interfaces'),
|
||||||
badge=lambda obj: obj.interfacetemplates.count(),
|
badge=lambda obj: obj.interfacetemplates.count(),
|
||||||
permission='dcim.view_interfacetemplate'
|
permission='dcim.view_interfacetemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1233,7 +1248,8 @@ class ModuleTypeFrontPortsView(ModuleTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Front Ports'),
|
label=_('Front Ports'),
|
||||||
badge=lambda obj: obj.frontporttemplates.count(),
|
badge=lambda obj: obj.frontporttemplates.count(),
|
||||||
permission='dcim.view_frontporttemplate'
|
permission='dcim.view_frontporttemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1246,7 +1262,8 @@ class ModuleTypeRearPortsView(ModuleTypeComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Rear Ports'),
|
label=_('Rear Ports'),
|
||||||
badge=lambda obj: obj.rearporttemplates.count(),
|
badge=lambda obj: obj.rearporttemplates.count(),
|
||||||
permission='dcim.view_rearporttemplate'
|
permission='dcim.view_rearporttemplate',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1845,11 +1862,12 @@ class DeviceConsolePortsView(DeviceComponentsView):
|
|||||||
child_model = ConsolePort
|
child_model = ConsolePort
|
||||||
table = tables.DeviceConsolePortTable
|
table = tables.DeviceConsolePortTable
|
||||||
filterset = filtersets.ConsolePortFilterSet
|
filterset = filtersets.ConsolePortFilterSet
|
||||||
template_name = 'dcim/device/consoleports.html'
|
template_name = 'dcim/device/consoleports.html',
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Console Ports'),
|
label=_('Console Ports'),
|
||||||
badge=lambda obj: obj.consoleports.count(),
|
badge=lambda obj: obj.consoleports.count(),
|
||||||
permission='dcim.view_consoleport'
|
permission='dcim.view_consoleport',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1862,7 +1880,8 @@ class DeviceConsoleServerPortsView(DeviceComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Console Server Ports'),
|
label=_('Console Server Ports'),
|
||||||
badge=lambda obj: obj.consoleserverports.count(),
|
badge=lambda obj: obj.consoleserverports.count(),
|
||||||
permission='dcim.view_consoleserverport'
|
permission='dcim.view_consoleserverport',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1875,7 +1894,8 @@ class DevicePowerPortsView(DeviceComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Power Ports'),
|
label=_('Power Ports'),
|
||||||
badge=lambda obj: obj.powerports.count(),
|
badge=lambda obj: obj.powerports.count(),
|
||||||
permission='dcim.view_powerport'
|
permission='dcim.view_powerport',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1888,7 +1908,8 @@ class DevicePowerOutletsView(DeviceComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Power Outlets'),
|
label=_('Power Outlets'),
|
||||||
badge=lambda obj: obj.poweroutlets.count(),
|
badge=lambda obj: obj.poweroutlets.count(),
|
||||||
permission='dcim.view_poweroutlet'
|
permission='dcim.view_poweroutlet',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1901,7 +1922,8 @@ class DeviceInterfacesView(DeviceComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Interfaces'),
|
label=_('Interfaces'),
|
||||||
badge=lambda obj: obj.interfaces.count(),
|
badge=lambda obj: obj.interfaces.count(),
|
||||||
permission='dcim.view_interface'
|
permission='dcim.view_interface',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
@ -1920,7 +1942,8 @@ class DeviceFrontPortsView(DeviceComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Front Ports'),
|
label=_('Front Ports'),
|
||||||
badge=lambda obj: obj.frontports.count(),
|
badge=lambda obj: obj.frontports.count(),
|
||||||
permission='dcim.view_frontport'
|
permission='dcim.view_frontport',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1933,7 +1956,8 @@ class DeviceRearPortsView(DeviceComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Rear Ports'),
|
label=_('Rear Ports'),
|
||||||
badge=lambda obj: obj.rearports.count(),
|
badge=lambda obj: obj.rearports.count(),
|
||||||
permission='dcim.view_rearport'
|
permission='dcim.view_rearport',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1946,7 +1970,8 @@ class DeviceModuleBaysView(DeviceComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Module Bays'),
|
label=_('Module Bays'),
|
||||||
badge=lambda obj: obj.modulebays.count(),
|
badge=lambda obj: obj.modulebays.count(),
|
||||||
permission='dcim.view_modulebay'
|
permission='dcim.view_modulebay',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1959,7 +1984,8 @@ class DeviceDeviceBaysView(DeviceComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Device Bays'),
|
label=_('Device Bays'),
|
||||||
badge=lambda obj: obj.devicebays.count(),
|
badge=lambda obj: obj.devicebays.count(),
|
||||||
permission='dcim.view_devicebay'
|
permission='dcim.view_devicebay',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1972,7 +1998,8 @@ class DeviceInventoryView(DeviceComponentsView):
|
|||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Inventory Items'),
|
label=_('Inventory Items'),
|
||||||
badge=lambda obj: obj.inventoryitems.count(),
|
badge=lambda obj: obj.inventoryitems.count(),
|
||||||
permission='dcim.view_inventoryitem'
|
permission='dcim.view_inventoryitem',
|
||||||
|
hide_if_empty=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,5 +5,4 @@ class ExtrasConfig(AppConfig):
|
|||||||
name = "extras"
|
name = "extras"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import extras.lookups
|
from . import lookups, search, signals
|
||||||
import extras.signals
|
|
||||||
|
@ -59,3 +59,10 @@ class TagsMixin:
|
|||||||
|
|
||||||
def resolve_tags(self, info):
|
def resolve_tags(self, info):
|
||||||
return self.tags.all()
|
return self.tags.all()
|
||||||
|
|
||||||
|
|
||||||
|
class ContactsMixin:
|
||||||
|
contacts = graphene.List('tenancy.graphql.types.ContactAssignmentType')
|
||||||
|
|
||||||
|
def resolve_contacts(self, info):
|
||||||
|
return list(self.contacts.all())
|
||||||
|
@ -27,7 +27,7 @@ class CustomFieldType(ObjectType):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.CustomField
|
model = models.CustomField
|
||||||
fields = '__all__'
|
exclude = ('content_types', )
|
||||||
filterset_class = filtersets.CustomFieldFilterSet
|
filterset_class = filtersets.CustomFieldFilterSet
|
||||||
|
|
||||||
|
|
||||||
@ -83,5 +83,5 @@ class WebhookType(ObjectType):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Webhook
|
model = models.Webhook
|
||||||
fields = '__all__'
|
exclude = ('content_types', )
|
||||||
filterset_class = filtersets.WebhookFilterSet
|
filterset_class = filtersets.WebhookFilterSet
|
||||||
|
@ -5,10 +5,11 @@ from packaging import version
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
|
|
||||||
from extras.plugins.utils import import_object
|
|
||||||
from extras.registry import registry
|
from extras.registry import registry
|
||||||
from netbox.navigation import MenuGroup
|
from netbox.navigation import MenuGroup
|
||||||
|
from netbox.search import register_search
|
||||||
from utilities.choices import ButtonColorChoices
|
from utilities.choices import ButtonColorChoices
|
||||||
|
|
||||||
|
|
||||||
@ -60,6 +61,7 @@ class PluginConfig(AppConfig):
|
|||||||
|
|
||||||
# Default integration paths. Plugin authors can override these to customize the paths to
|
# Default integration paths. Plugin authors can override these to customize the paths to
|
||||||
# integrated components.
|
# integrated components.
|
||||||
|
search_indexes = 'search.indexes'
|
||||||
graphql_schema = 'graphql.schema'
|
graphql_schema = 'graphql.schema'
|
||||||
menu = 'navigation.menu'
|
menu = 'navigation.menu'
|
||||||
menu_items = 'navigation.menu_items'
|
menu_items = 'navigation.menu_items'
|
||||||
@ -69,26 +71,46 @@ class PluginConfig(AppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
plugin_name = self.name.rsplit('.', 1)[-1]
|
plugin_name = self.name.rsplit('.', 1)[-1]
|
||||||
|
|
||||||
# Register template content (if defined)
|
# Register search extensions (if defined)
|
||||||
template_extensions = import_object(f"{self.__module__}.{self.template_extensions}")
|
try:
|
||||||
if template_extensions is not None:
|
search_indexes = import_string(f"{self.__module__}.{self.search_indexes}")
|
||||||
register_template_extensions(template_extensions)
|
for idx in search_indexes:
|
||||||
|
register_search()(idx)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Register navigation menu or menu items (if defined)
|
# Register template content (if defined)
|
||||||
if menu := import_object(f"{self.__module__}.{self.menu}"):
|
try:
|
||||||
|
template_extensions = import_string(f"{self.__module__}.{self.template_extensions}")
|
||||||
|
register_template_extensions(template_extensions)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Register navigation menu and/or menu items (if defined)
|
||||||
|
try:
|
||||||
|
menu = import_string(f"{self.__module__}.{self.menu}")
|
||||||
register_menu(menu)
|
register_menu(menu)
|
||||||
if menu_items := import_object(f"{self.__module__}.{self.menu_items}"):
|
except ImportError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
menu_items = import_string(f"{self.__module__}.{self.menu_items}")
|
||||||
register_menu_items(self.verbose_name, menu_items)
|
register_menu_items(self.verbose_name, menu_items)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Register GraphQL schema (if defined)
|
# Register GraphQL schema (if defined)
|
||||||
graphql_schema = import_object(f"{self.__module__}.{self.graphql_schema}")
|
try:
|
||||||
if graphql_schema is not None:
|
graphql_schema = import_string(f"{self.__module__}.{self.graphql_schema}")
|
||||||
register_graphql_schema(graphql_schema)
|
register_graphql_schema(graphql_schema)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Register user preferences (if defined)
|
# Register user preferences (if defined)
|
||||||
user_preferences = import_object(f"{self.__module__}.{self.user_preferences}")
|
try:
|
||||||
if user_preferences is not None:
|
user_preferences = import_string(f"{self.__module__}.{self.user_preferences}")
|
||||||
register_user_preferences(plugin_name, user_preferences)
|
register_user_preferences(plugin_name, user_preferences)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, user_config, netbox_version):
|
def validate(cls, user_config, netbox_version):
|
||||||
|
@ -3,8 +3,7 @@ from django.conf import settings
|
|||||||
from django.conf.urls import include
|
from django.conf.urls import include
|
||||||
from django.contrib.admin.views.decorators import staff_member_required
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
from extras.plugins.utils import import_object
|
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
@ -25,15 +24,19 @@ for plugin_path in settings.PLUGINS:
|
|||||||
base_url = getattr(app, 'base_url') or app.label
|
base_url = getattr(app, 'base_url') or app.label
|
||||||
|
|
||||||
# Check if the plugin specifies any base URLs
|
# Check if the plugin specifies any base URLs
|
||||||
urlpatterns = import_object(f"{plugin_path}.urls.urlpatterns")
|
try:
|
||||||
if urlpatterns is not None:
|
urlpatterns = import_string(f"{plugin_path}.urls.urlpatterns")
|
||||||
plugin_patterns.append(
|
plugin_patterns.append(
|
||||||
path(f"{base_url}/", include((urlpatterns, app.label)))
|
path(f"{base_url}/", include((urlpatterns, app.label)))
|
||||||
)
|
)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Check if the plugin specifies any API URLs
|
# Check if the plugin specifies any API URLs
|
||||||
urlpatterns = import_object(f"{plugin_path}.api.urls.urlpatterns")
|
try:
|
||||||
if urlpatterns is not None:
|
urlpatterns = import_string(f"{plugin_path}.api.urls.urlpatterns")
|
||||||
plugin_api_patterns.append(
|
plugin_api_patterns.append(
|
||||||
path(f"{base_url}/", include((urlpatterns, f"{app.label}-api")))
|
path(f"{base_url}/", include((urlpatterns, f"{app.label}-api")))
|
||||||
)
|
)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
import importlib.util
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def import_object(module_and_object):
|
|
||||||
"""
|
|
||||||
Import a specific object from a specific module by name, such as "extras.plugins.utils.import_object".
|
|
||||||
|
|
||||||
Returns the imported object, or None if it doesn't exist.
|
|
||||||
"""
|
|
||||||
target_module_name, object_name = module_and_object.rsplit('.', 1)
|
|
||||||
module_hierarchy = target_module_name.split('.')
|
|
||||||
|
|
||||||
# Iterate through the module hierarchy, checking for the existence of each successive submodule.
|
|
||||||
# We have to do this rather than jumping directly to calling find_spec(target_module_name)
|
|
||||||
# because find_spec will raise a ModuleNotFoundError if any parent module of target_module_name does not exist.
|
|
||||||
module_name = ""
|
|
||||||
for module_component in module_hierarchy:
|
|
||||||
module_name = f"{module_name}.{module_component}" if module_name else module_component
|
|
||||||
spec = importlib.util.find_spec(module_name)
|
|
||||||
if spec is None:
|
|
||||||
# No such module
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Okay, target_module_name exists. Load it if not already loaded
|
|
||||||
if target_module_name in sys.modules:
|
|
||||||
module = sys.modules[target_module_name]
|
|
||||||
else:
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
sys.modules[target_module_name] = module
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
|
|
||||||
return getattr(module, object_name, None)
|
|
@ -29,4 +29,5 @@ registry['model_features'] = {
|
|||||||
feature: collections.defaultdict(set) for feature in EXTRAS_FEATURES
|
feature: collections.defaultdict(set) for feature in EXTRAS_FEATURES
|
||||||
}
|
}
|
||||||
registry['denormalized_fields'] = collections.defaultdict(list)
|
registry['denormalized_fields'] = collections.defaultdict(list)
|
||||||
|
registry['search'] = collections.defaultdict(dict)
|
||||||
registry['views'] = collections.defaultdict(dict)
|
registry['views'] = collections.defaultdict(dict)
|
||||||
|
14
netbox/extras/search.py
Normal file
14
netbox/extras/search.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import extras.filtersets
|
||||||
|
import extras.tables
|
||||||
|
from extras.models import JournalEntry
|
||||||
|
from netbox.search import SearchIndex, register_search
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class JournalEntryIndex(SearchIndex):
|
||||||
|
model = JournalEntry
|
||||||
|
queryset = JournalEntry.objects.prefetch_related('assigned_object', 'created_by')
|
||||||
|
filterset = extras.filtersets.JournalEntryFilterSet
|
||||||
|
table = extras.tables.JournalEntryTable
|
||||||
|
url = 'extras:journalentry_list'
|
||||||
|
category = 'Journal'
|
13
netbox/extras/tests/dummy_plugin/search.py
Normal file
13
netbox/extras/tests/dummy_plugin/search.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from netbox.search import SearchIndex
|
||||||
|
from .models import DummyModel
|
||||||
|
|
||||||
|
|
||||||
|
class DummyModelIndex(SearchIndex):
|
||||||
|
model = DummyModel
|
||||||
|
queryset = DummyModel.objects.all()
|
||||||
|
url = 'plugins:dummy_plugin:dummy_models'
|
||||||
|
|
||||||
|
|
||||||
|
indexes = (
|
||||||
|
DummyModelIndex,
|
||||||
|
)
|
@ -6,4 +6,4 @@ class IPAMConfig(AppConfig):
|
|||||||
verbose_name = "IPAM"
|
verbose_name = "IPAM"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import ipam.signals
|
from . import signals, search
|
||||||
|
@ -88,6 +88,12 @@ class RouteTargetForm(TenancyForm, NetBoxModelForm):
|
|||||||
class RIRForm(NetBoxModelForm):
|
class RIRForm(NetBoxModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('RIR', (
|
||||||
|
'name', 'slug', 'is_private', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RIR
|
model = RIR
|
||||||
fields = [
|
fields = [
|
||||||
@ -164,6 +170,12 @@ class ASNForm(TenancyForm, NetBoxModelForm):
|
|||||||
class RoleForm(NetBoxModelForm):
|
class RoleForm(NetBoxModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Role', (
|
||||||
|
'name', 'slug', 'weight', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Role
|
model = Role
|
||||||
fields = [
|
fields = [
|
||||||
@ -784,6 +796,12 @@ class ServiceTemplateForm(NetBoxModelForm):
|
|||||||
help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen."
|
help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Service Template', (
|
||||||
|
'name', 'protocol', 'ports', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ServiceTemplate
|
model = ServiceTemplate
|
||||||
fields = ('name', 'protocol', 'ports', 'description', 'tags')
|
fields = ('name', 'protocol', 'ports', 'description', 'tags')
|
||||||
|
95
netbox/ipam/graphql/gfk_mixins.py
Normal file
95
netbox/ipam/graphql/gfk_mixins.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import graphene
|
||||||
|
from dcim.graphql.types import (
|
||||||
|
InterfaceType,
|
||||||
|
LocationType,
|
||||||
|
RackType,
|
||||||
|
RegionType,
|
||||||
|
SiteGroupType,
|
||||||
|
SiteType,
|
||||||
|
)
|
||||||
|
from dcim.models import Interface, Location, Rack, Region, Site, SiteGroup
|
||||||
|
from ipam.graphql.types import FHRPGroupType, VLANType
|
||||||
|
from ipam.models import VLAN, FHRPGroup
|
||||||
|
from virtualization.graphql.types import ClusterGroupType, ClusterType, VMInterfaceType
|
||||||
|
from virtualization.models import Cluster, ClusterGroup, VMInterface
|
||||||
|
|
||||||
|
|
||||||
|
class IPAddressAssignmentType(graphene.Union):
|
||||||
|
class Meta:
|
||||||
|
types = (
|
||||||
|
InterfaceType,
|
||||||
|
FHRPGroupType,
|
||||||
|
VMInterfaceType,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resolve_type(cls, instance, info):
|
||||||
|
if type(instance) == Interface:
|
||||||
|
return InterfaceType
|
||||||
|
if type(instance) == FHRPGroup:
|
||||||
|
return FHRPGroupType
|
||||||
|
if type(instance) == VMInterface:
|
||||||
|
return VMInterfaceType
|
||||||
|
|
||||||
|
|
||||||
|
class L2VPNAssignmentType(graphene.Union):
|
||||||
|
class Meta:
|
||||||
|
types = (
|
||||||
|
InterfaceType,
|
||||||
|
VLANType,
|
||||||
|
VMInterfaceType,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resolve_type(cls, instance, info):
|
||||||
|
if type(instance) == Interface:
|
||||||
|
return InterfaceType
|
||||||
|
if type(instance) == VLAN:
|
||||||
|
return VLANType
|
||||||
|
if type(instance) == VMInterface:
|
||||||
|
return VMInterfaceType
|
||||||
|
|
||||||
|
|
||||||
|
class FHRPGroupInterfaceType(graphene.Union):
|
||||||
|
class Meta:
|
||||||
|
types = (
|
||||||
|
InterfaceType,
|
||||||
|
VMInterfaceType,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resolve_type(cls, instance, info):
|
||||||
|
if type(instance) == Interface:
|
||||||
|
return InterfaceType
|
||||||
|
if type(instance) == VMInterface:
|
||||||
|
return VMInterfaceType
|
||||||
|
|
||||||
|
|
||||||
|
class VLANGroupScopeType(graphene.Union):
|
||||||
|
class Meta:
|
||||||
|
types = (
|
||||||
|
ClusterType,
|
||||||
|
ClusterGroupType,
|
||||||
|
LocationType,
|
||||||
|
RackType,
|
||||||
|
RegionType,
|
||||||
|
SiteType,
|
||||||
|
SiteGroupType,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resolve_type(cls, instance, info):
|
||||||
|
if type(instance) == Cluster:
|
||||||
|
return ClusterType
|
||||||
|
if type(instance) == ClusterGroup:
|
||||||
|
return ClusterGroupType
|
||||||
|
if type(instance) == Location:
|
||||||
|
return LocationType
|
||||||
|
if type(instance) == Rack:
|
||||||
|
return RackType
|
||||||
|
if type(instance) == Region:
|
||||||
|
return RegionType
|
||||||
|
if type(instance) == Site:
|
||||||
|
return SiteType
|
||||||
|
if type(instance) == SiteGroup:
|
||||||
|
return SiteGroupType
|
@ -1,5 +1,7 @@
|
|||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
from extras.graphql.mixins import ContactsMixin
|
||||||
from ipam import filtersets, models
|
from ipam import filtersets, models
|
||||||
from netbox.graphql.scalars import BigInt
|
from netbox.graphql.scalars import BigInt
|
||||||
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
|
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
|
||||||
@ -54,18 +56,20 @@ class FHRPGroupType(NetBoxObjectType):
|
|||||||
|
|
||||||
|
|
||||||
class FHRPGroupAssignmentType(BaseObjectType):
|
class FHRPGroupAssignmentType(BaseObjectType):
|
||||||
|
interface = graphene.Field('ipam.graphql.gfk_mixins.FHRPGroupInterfaceType')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.FHRPGroupAssignment
|
model = models.FHRPGroupAssignment
|
||||||
fields = '__all__'
|
exclude = ('interface_type', 'interface_id')
|
||||||
filterset_class = filtersets.FHRPGroupAssignmentFilterSet
|
filterset_class = filtersets.FHRPGroupAssignmentFilterSet
|
||||||
|
|
||||||
|
|
||||||
class IPAddressType(NetBoxObjectType):
|
class IPAddressType(NetBoxObjectType):
|
||||||
|
assigned_object = graphene.Field('ipam.graphql.gfk_mixins.IPAddressAssignmentType')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.IPAddress
|
model = models.IPAddress
|
||||||
fields = '__all__'
|
exclude = ('assigned_object_type', 'assigned_object_id')
|
||||||
filterset_class = filtersets.IPAddressFilterSet
|
filterset_class = filtersets.IPAddressFilterSet
|
||||||
|
|
||||||
def resolve_role(self, info):
|
def resolve_role(self, info):
|
||||||
@ -140,10 +144,11 @@ class VLANType(NetBoxObjectType):
|
|||||||
|
|
||||||
|
|
||||||
class VLANGroupType(OrganizationalObjectType):
|
class VLANGroupType(OrganizationalObjectType):
|
||||||
|
scope = graphene.Field('ipam.graphql.gfk_mixins.VLANGroupScopeType')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.VLANGroup
|
model = models.VLANGroup
|
||||||
fields = '__all__'
|
exclude = ('scope_type', 'scope_id')
|
||||||
filterset_class = filtersets.VLANGroupFilterSet
|
filterset_class = filtersets.VLANGroupFilterSet
|
||||||
|
|
||||||
|
|
||||||
@ -155,7 +160,7 @@ class VRFType(NetBoxObjectType):
|
|||||||
filterset_class = filtersets.VRFFilterSet
|
filterset_class = filtersets.VRFFilterSet
|
||||||
|
|
||||||
|
|
||||||
class L2VPNType(NetBoxObjectType):
|
class L2VPNType(ContactsMixin, NetBoxObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.L2VPN
|
model = models.L2VPN
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
@ -163,7 +168,9 @@ class L2VPNType(NetBoxObjectType):
|
|||||||
|
|
||||||
|
|
||||||
class L2VPNTerminationType(NetBoxObjectType):
|
class L2VPNTerminationType(NetBoxObjectType):
|
||||||
|
assigned_object = graphene.Field('ipam.graphql.gfk_mixins.L2VPNAssignmentType')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.L2VPNTermination
|
model = models.L2VPNTermination
|
||||||
fields = '__all__'
|
exclude = ('assigned_object_type', 'assigned_object_id')
|
||||||
filtersets_class = filtersets.L2VPNTerminationFilterSet
|
filtersets_class = filtersets.L2VPNTerminationFilterSet
|
||||||
|
@ -92,6 +92,8 @@ class Service(ServiceBase, NetBoxModel):
|
|||||||
verbose_name='IP addresses'
|
verbose_name='IP addresses'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
clone_fields = ['protocol', 'ports', 'description', 'device', 'virtual_machine', 'ipaddresses', ]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique
|
ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique
|
||||||
|
|
||||||
|
69
netbox/ipam/search.py
Normal file
69
netbox/ipam/search.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import ipam.filtersets
|
||||||
|
import ipam.tables
|
||||||
|
from ipam.models import ASN, VLAN, VRF, Aggregate, IPAddress, Prefix, Service
|
||||||
|
from netbox.search import SearchIndex, register_search
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class VRFIndex(SearchIndex):
|
||||||
|
model = VRF
|
||||||
|
queryset = VRF.objects.prefetch_related('tenant', 'tenant__group')
|
||||||
|
filterset = ipam.filtersets.VRFFilterSet
|
||||||
|
table = ipam.tables.VRFTable
|
||||||
|
url = 'ipam:vrf_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class AggregateIndex(SearchIndex):
|
||||||
|
model = Aggregate
|
||||||
|
queryset = Aggregate.objects.prefetch_related('rir')
|
||||||
|
filterset = ipam.filtersets.AggregateFilterSet
|
||||||
|
table = ipam.tables.AggregateTable
|
||||||
|
url = 'ipam:aggregate_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class PrefixIndex(SearchIndex):
|
||||||
|
model = Prefix
|
||||||
|
queryset = Prefix.objects.prefetch_related(
|
||||||
|
'site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'
|
||||||
|
)
|
||||||
|
filterset = ipam.filtersets.PrefixFilterSet
|
||||||
|
table = ipam.tables.PrefixTable
|
||||||
|
url = 'ipam:prefix_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class IPAddressIndex(SearchIndex):
|
||||||
|
model = IPAddress
|
||||||
|
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group')
|
||||||
|
filterset = ipam.filtersets.IPAddressFilterSet
|
||||||
|
table = ipam.tables.IPAddressTable
|
||||||
|
url = 'ipam:ipaddress_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class VLANIndex(SearchIndex):
|
||||||
|
model = VLAN
|
||||||
|
queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role')
|
||||||
|
filterset = ipam.filtersets.VLANFilterSet
|
||||||
|
table = ipam.tables.VLANTable
|
||||||
|
url = 'ipam:vlan_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class ASNIndex(SearchIndex):
|
||||||
|
model = ASN
|
||||||
|
queryset = ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group')
|
||||||
|
filterset = ipam.filtersets.ASNFilterSet
|
||||||
|
table = ipam.tables.ASNTable
|
||||||
|
url = 'ipam:asn_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class ServiceIndex(SearchIndex):
|
||||||
|
model = Service
|
||||||
|
queryset = Service.objects.prefetch_related('device', 'virtual_machine')
|
||||||
|
filterset = ipam.filtersets.ServiceFilterSet
|
||||||
|
table = ipam.tables.ServiceTable
|
||||||
|
url = 'ipam:service_list'
|
@ -351,6 +351,14 @@ class LDAPBackend:
|
|||||||
if getattr(ldap_config, 'LDAP_IGNORE_CERT_ERRORS', False):
|
if getattr(ldap_config, 'LDAP_IGNORE_CERT_ERRORS', False):
|
||||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
||||||
|
|
||||||
|
# Optionally set CA cert directory
|
||||||
|
if ca_cert_dir := getattr(ldap_config, 'LDAP_CA_CERT_DIR', None):
|
||||||
|
ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, ca_cert_dir)
|
||||||
|
|
||||||
|
# Optionally set CA cert file
|
||||||
|
if ca_cert_file := getattr(ldap_config, 'LDAP_CA_CERT_FILE', None):
|
||||||
|
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, ca_cert_file)
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,31 +1,15 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from netbox.search import SEARCH_TYPE_HIERARCHY
|
from netbox.search.backends import default_search_engine
|
||||||
from utilities.forms import BootstrapMixin
|
from utilities.forms import BootstrapMixin
|
||||||
|
|
||||||
from .base import *
|
from .base import *
|
||||||
|
|
||||||
|
|
||||||
def build_search_choices():
|
def build_options(choices):
|
||||||
result = list()
|
options = [{"label": choices[0][1], "items": []}]
|
||||||
result.append(('', 'All Objects'))
|
|
||||||
for category, items in SEARCH_TYPE_HIERARCHY.items():
|
|
||||||
subcategories = list()
|
|
||||||
for slug, obj in items.items():
|
|
||||||
name = obj['queryset'].model._meta.verbose_name_plural
|
|
||||||
name = name[0].upper() + name[1:]
|
|
||||||
subcategories.append((slug, name))
|
|
||||||
result.append((category, tuple(subcategories)))
|
|
||||||
|
|
||||||
return tuple(result)
|
for label, choices in choices[1:]:
|
||||||
|
|
||||||
|
|
||||||
OBJ_TYPE_CHOICES = build_search_choices()
|
|
||||||
|
|
||||||
|
|
||||||
def build_options():
|
|
||||||
options = [{"label": OBJ_TYPE_CHOICES[0][1], "items": []}]
|
|
||||||
|
|
||||||
for label, choices in OBJ_TYPE_CHOICES[1:]:
|
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
for value, choice_label in choices:
|
for value, choice_label in choices:
|
||||||
@ -36,10 +20,19 @@ def build_options():
|
|||||||
|
|
||||||
|
|
||||||
class SearchForm(BootstrapMixin, forms.Form):
|
class SearchForm(BootstrapMixin, forms.Form):
|
||||||
q = forms.CharField(
|
q = forms.CharField(label='Search')
|
||||||
label='Search'
|
options = None
|
||||||
)
|
|
||||||
obj_type = forms.ChoiceField(
|
def __init__(self, *args, **kwargs):
|
||||||
choices=OBJ_TYPE_CHOICES, required=False, label='Type'
|
super().__init__(*args, **kwargs)
|
||||||
)
|
self.fields["obj_type"] = forms.ChoiceField(
|
||||||
options = build_options()
|
choices=default_search_engine.get_search_choices(),
|
||||||
|
required=False,
|
||||||
|
label='Type'
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_options(self):
|
||||||
|
if not self.options:
|
||||||
|
self.options = build_options(default_search_engine.get_search_choices())
|
||||||
|
|
||||||
|
return self.options
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import graphene
|
import graphene
|
||||||
from graphene_django.converter import convert_django_field
|
from dcim.fields import MACAddressField, WWNField
|
||||||
|
from django.db import models
|
||||||
|
from graphene import Dynamic
|
||||||
|
from graphene_django.converter import convert_django_field, get_django_field_description
|
||||||
|
from graphene_django.fields import DjangoConnectionField
|
||||||
|
from ipam.fields import IPAddressField, IPNetworkField
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from dcim.fields import MACAddressField, WWNField
|
from .fields import ObjectListField
|
||||||
from ipam.fields import IPAddressField, IPNetworkField
|
|
||||||
|
|
||||||
|
|
||||||
@convert_django_field.register(TaggableManager)
|
@convert_django_field.register(TaggableManager)
|
||||||
@ -21,3 +25,45 @@ def convert_field_to_tags_list(field, registry=None):
|
|||||||
def convert_field_to_string(field, registry=None):
|
def convert_field_to_string(field, registry=None):
|
||||||
# TODO: Update to use get_django_field_description under django_graphene v3.0
|
# TODO: Update to use get_django_field_description under django_graphene v3.0
|
||||||
return graphene.String(description=field.help_text, required=not field.null)
|
return graphene.String(description=field.help_text, required=not field.null)
|
||||||
|
|
||||||
|
|
||||||
|
@convert_django_field.register(models.ManyToManyField)
|
||||||
|
@convert_django_field.register(models.ManyToManyRel)
|
||||||
|
@convert_django_field.register(models.ManyToOneRel)
|
||||||
|
def convert_field_to_list_or_connection(field, registry=None):
|
||||||
|
"""
|
||||||
|
From graphene_django.converter.py we need to monkey-patch this to return
|
||||||
|
our ObjectListField with filtering support instead of DjangoListField
|
||||||
|
"""
|
||||||
|
model = field.related_model
|
||||||
|
|
||||||
|
def dynamic_type():
|
||||||
|
_type = registry.get_type_for_model(model)
|
||||||
|
if not _type:
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(field, models.ManyToManyField):
|
||||||
|
description = get_django_field_description(field)
|
||||||
|
else:
|
||||||
|
description = get_django_field_description(field.field)
|
||||||
|
|
||||||
|
# If there is a connection, we should transform the field
|
||||||
|
# into a DjangoConnectionField
|
||||||
|
if _type._meta.connection:
|
||||||
|
# Use a DjangoFilterConnectionField if there are
|
||||||
|
# defined filter_fields or a filterset_class in the
|
||||||
|
# DjangoObjectType Meta
|
||||||
|
if _type._meta.filter_fields or _type._meta.filterset_class:
|
||||||
|
from .filter.fields import DjangoFilterConnectionField
|
||||||
|
|
||||||
|
return DjangoFilterConnectionField(_type, required=True, description=description)
|
||||||
|
|
||||||
|
return DjangoConnectionField(_type, required=True, description=description)
|
||||||
|
|
||||||
|
return ObjectListField(
|
||||||
|
_type,
|
||||||
|
required=True, # A Set is always returned, never None.
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Dynamic(dynamic_type)
|
||||||
|
@ -1,274 +0,0 @@
|
|||||||
import circuits.filtersets
|
|
||||||
import circuits.tables
|
|
||||||
import dcim.filtersets
|
|
||||||
import dcim.tables
|
|
||||||
import extras.filtersets
|
|
||||||
import extras.tables
|
|
||||||
import ipam.filtersets
|
|
||||||
import ipam.tables
|
|
||||||
import tenancy.filtersets
|
|
||||||
import tenancy.tables
|
|
||||||
import virtualization.filtersets
|
|
||||||
import wireless.tables
|
|
||||||
import wireless.filtersets
|
|
||||||
import virtualization.tables
|
|
||||||
from circuits.models import Circuit, ProviderNetwork, Provider
|
|
||||||
from dcim.models import (
|
|
||||||
Cable, Device, DeviceType, Interface, Location, Module, ModuleType, PowerFeed, Rack, RackReservation, Site,
|
|
||||||
VirtualChassis,
|
|
||||||
)
|
|
||||||
from extras.models import JournalEntry
|
|
||||||
from ipam.models import Aggregate, ASN, IPAddress, Prefix, Service, VLAN, VRF
|
|
||||||
from tenancy.models import Contact, Tenant, ContactAssignment
|
|
||||||
from utilities.utils import count_related
|
|
||||||
from wireless.models import WirelessLAN, WirelessLink
|
|
||||||
from virtualization.models import Cluster, VirtualMachine
|
|
||||||
|
|
||||||
CIRCUIT_TYPES = {
|
|
||||||
'provider': {
|
|
||||||
'queryset': Provider.objects.annotate(
|
|
||||||
count_circuits=count_related(Circuit, 'provider')
|
|
||||||
),
|
|
||||||
'filterset': circuits.filtersets.ProviderFilterSet,
|
|
||||||
'table': circuits.tables.ProviderTable,
|
|
||||||
'url': 'circuits:provider_list',
|
|
||||||
},
|
|
||||||
'circuit': {
|
|
||||||
'queryset': Circuit.objects.prefetch_related(
|
|
||||||
'type', 'provider', 'tenant', 'tenant__group', 'terminations__site'
|
|
||||||
),
|
|
||||||
'filterset': circuits.filtersets.CircuitFilterSet,
|
|
||||||
'table': circuits.tables.CircuitTable,
|
|
||||||
'url': 'circuits:circuit_list',
|
|
||||||
},
|
|
||||||
'providernetwork': {
|
|
||||||
'queryset': ProviderNetwork.objects.prefetch_related('provider'),
|
|
||||||
'filterset': circuits.filtersets.ProviderNetworkFilterSet,
|
|
||||||
'table': circuits.tables.ProviderNetworkTable,
|
|
||||||
'url': 'circuits:providernetwork_list',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
DCIM_TYPES = {
|
|
||||||
'site': {
|
|
||||||
'queryset': Site.objects.prefetch_related('region', 'tenant', 'tenant__group'),
|
|
||||||
'filterset': dcim.filtersets.SiteFilterSet,
|
|
||||||
'table': dcim.tables.SiteTable,
|
|
||||||
'url': 'dcim:site_list',
|
|
||||||
},
|
|
||||||
'rack': {
|
|
||||||
'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
|
|
||||||
device_count=count_related(Device, 'rack')
|
|
||||||
),
|
|
||||||
'filterset': dcim.filtersets.RackFilterSet,
|
|
||||||
'table': dcim.tables.RackTable,
|
|
||||||
'url': 'dcim:rack_list',
|
|
||||||
},
|
|
||||||
'rackreservation': {
|
|
||||||
'queryset': RackReservation.objects.prefetch_related('rack', 'user'),
|
|
||||||
'filterset': dcim.filtersets.RackReservationFilterSet,
|
|
||||||
'table': dcim.tables.RackReservationTable,
|
|
||||||
'url': 'dcim:rackreservation_list',
|
|
||||||
},
|
|
||||||
'location': {
|
|
||||||
'queryset': Location.objects.add_related_count(
|
|
||||||
Location.objects.add_related_count(
|
|
||||||
Location.objects.all(),
|
|
||||||
Device,
|
|
||||||
'location',
|
|
||||||
'device_count',
|
|
||||||
cumulative=True
|
|
||||||
),
|
|
||||||
Rack,
|
|
||||||
'location',
|
|
||||||
'rack_count',
|
|
||||||
cumulative=True
|
|
||||||
).prefetch_related('site'),
|
|
||||||
'filterset': dcim.filtersets.LocationFilterSet,
|
|
||||||
'table': dcim.tables.LocationTable,
|
|
||||||
'url': 'dcim:location_list',
|
|
||||||
},
|
|
||||||
'devicetype': {
|
|
||||||
'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
|
|
||||||
instance_count=count_related(Device, 'device_type')
|
|
||||||
),
|
|
||||||
'filterset': dcim.filtersets.DeviceTypeFilterSet,
|
|
||||||
'table': dcim.tables.DeviceTypeTable,
|
|
||||||
'url': 'dcim:devicetype_list',
|
|
||||||
},
|
|
||||||
'device': {
|
|
||||||
'queryset': Device.objects.prefetch_related(
|
|
||||||
'device_type__manufacturer', 'device_role', 'tenant', 'tenant__group', 'site', 'rack', 'primary_ip4',
|
|
||||||
'primary_ip6',
|
|
||||||
),
|
|
||||||
'filterset': dcim.filtersets.DeviceFilterSet,
|
|
||||||
'table': dcim.tables.DeviceTable,
|
|
||||||
'url': 'dcim:device_list',
|
|
||||||
},
|
|
||||||
'moduletype': {
|
|
||||||
'queryset': ModuleType.objects.prefetch_related('manufacturer').annotate(
|
|
||||||
instance_count=count_related(Module, 'module_type')
|
|
||||||
),
|
|
||||||
'filterset': dcim.filtersets.ModuleTypeFilterSet,
|
|
||||||
'table': dcim.tables.ModuleTypeTable,
|
|
||||||
'url': 'dcim:moduletype_list',
|
|
||||||
},
|
|
||||||
'module': {
|
|
||||||
'queryset': Module.objects.prefetch_related(
|
|
||||||
'module_type__manufacturer', 'device', 'module_bay',
|
|
||||||
),
|
|
||||||
'filterset': dcim.filtersets.ModuleFilterSet,
|
|
||||||
'table': dcim.tables.ModuleTable,
|
|
||||||
'url': 'dcim:module_list',
|
|
||||||
},
|
|
||||||
'virtualchassis': {
|
|
||||||
'queryset': VirtualChassis.objects.prefetch_related('master').annotate(
|
|
||||||
member_count=count_related(Device, 'virtual_chassis')
|
|
||||||
),
|
|
||||||
'filterset': dcim.filtersets.VirtualChassisFilterSet,
|
|
||||||
'table': dcim.tables.VirtualChassisTable,
|
|
||||||
'url': 'dcim:virtualchassis_list',
|
|
||||||
},
|
|
||||||
'cable': {
|
|
||||||
'queryset': Cable.objects.all(),
|
|
||||||
'filterset': dcim.filtersets.CableFilterSet,
|
|
||||||
'table': dcim.tables.CableTable,
|
|
||||||
'url': 'dcim:cable_list',
|
|
||||||
},
|
|
||||||
'powerfeed': {
|
|
||||||
'queryset': PowerFeed.objects.all(),
|
|
||||||
'filterset': dcim.filtersets.PowerFeedFilterSet,
|
|
||||||
'table': dcim.tables.PowerFeedTable,
|
|
||||||
'url': 'dcim:powerfeed_list',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
IPAM_TYPES = {
|
|
||||||
'vrf': {
|
|
||||||
'queryset': VRF.objects.prefetch_related('tenant', 'tenant__group'),
|
|
||||||
'filterset': ipam.filtersets.VRFFilterSet,
|
|
||||||
'table': ipam.tables.VRFTable,
|
|
||||||
'url': 'ipam:vrf_list',
|
|
||||||
},
|
|
||||||
'aggregate': {
|
|
||||||
'queryset': Aggregate.objects.prefetch_related('rir'),
|
|
||||||
'filterset': ipam.filtersets.AggregateFilterSet,
|
|
||||||
'table': ipam.tables.AggregateTable,
|
|
||||||
'url': 'ipam:aggregate_list',
|
|
||||||
},
|
|
||||||
'prefix': {
|
|
||||||
'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'),
|
|
||||||
'filterset': ipam.filtersets.PrefixFilterSet,
|
|
||||||
'table': ipam.tables.PrefixTable,
|
|
||||||
'url': 'ipam:prefix_list',
|
|
||||||
},
|
|
||||||
'ipaddress': {
|
|
||||||
'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group'),
|
|
||||||
'filterset': ipam.filtersets.IPAddressFilterSet,
|
|
||||||
'table': ipam.tables.IPAddressTable,
|
|
||||||
'url': 'ipam:ipaddress_list',
|
|
||||||
},
|
|
||||||
'vlan': {
|
|
||||||
'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role'),
|
|
||||||
'filterset': ipam.filtersets.VLANFilterSet,
|
|
||||||
'table': ipam.tables.VLANTable,
|
|
||||||
'url': 'ipam:vlan_list',
|
|
||||||
},
|
|
||||||
'asn': {
|
|
||||||
'queryset': ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group'),
|
|
||||||
'filterset': ipam.filtersets.ASNFilterSet,
|
|
||||||
'table': ipam.tables.ASNTable,
|
|
||||||
'url': 'ipam:asn_list',
|
|
||||||
},
|
|
||||||
'service': {
|
|
||||||
'queryset': Service.objects.prefetch_related('device', 'virtual_machine'),
|
|
||||||
'filterset': ipam.filtersets.ServiceFilterSet,
|
|
||||||
'table': ipam.tables.ServiceTable,
|
|
||||||
'url': 'ipam:service_list',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
TENANCY_TYPES = {
|
|
||||||
'tenant': {
|
|
||||||
'queryset': Tenant.objects.prefetch_related('group'),
|
|
||||||
'filterset': tenancy.filtersets.TenantFilterSet,
|
|
||||||
'table': tenancy.tables.TenantTable,
|
|
||||||
'url': 'tenancy:tenant_list',
|
|
||||||
},
|
|
||||||
'contact': {
|
|
||||||
'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(
|
|
||||||
assignment_count=count_related(ContactAssignment, 'contact')),
|
|
||||||
'filterset': tenancy.filtersets.ContactFilterSet,
|
|
||||||
'table': tenancy.tables.ContactTable,
|
|
||||||
'url': 'tenancy:contact_list',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
VIRTUALIZATION_TYPES = {
|
|
||||||
'cluster': {
|
|
||||||
'queryset': Cluster.objects.prefetch_related('type', 'group').annotate(
|
|
||||||
device_count=count_related(Device, 'cluster'),
|
|
||||||
vm_count=count_related(VirtualMachine, 'cluster')
|
|
||||||
),
|
|
||||||
'filterset': virtualization.filtersets.ClusterFilterSet,
|
|
||||||
'table': virtualization.tables.ClusterTable,
|
|
||||||
'url': 'virtualization:cluster_list',
|
|
||||||
},
|
|
||||||
'virtualmachine': {
|
|
||||||
'queryset': VirtualMachine.objects.prefetch_related(
|
|
||||||
'cluster', 'tenant', 'tenant__group', 'platform', 'primary_ip4', 'primary_ip6',
|
|
||||||
),
|
|
||||||
'filterset': virtualization.filtersets.VirtualMachineFilterSet,
|
|
||||||
'table': virtualization.tables.VirtualMachineTable,
|
|
||||||
'url': 'virtualization:virtualmachine_list',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
WIRELESS_TYPES = {
|
|
||||||
'wirelesslan': {
|
|
||||||
'queryset': WirelessLAN.objects.prefetch_related('group', 'vlan').annotate(
|
|
||||||
interface_count=count_related(Interface, 'wireless_lans')
|
|
||||||
),
|
|
||||||
'filterset': wireless.filtersets.WirelessLANFilterSet,
|
|
||||||
'table': wireless.tables.WirelessLANTable,
|
|
||||||
'url': 'wireless:wirelesslan_list',
|
|
||||||
},
|
|
||||||
'wirelesslink': {
|
|
||||||
'queryset': WirelessLink.objects.prefetch_related('interface_a__device', 'interface_b__device'),
|
|
||||||
'filterset': wireless.filtersets.WirelessLinkFilterSet,
|
|
||||||
'table': wireless.tables.WirelessLinkTable,
|
|
||||||
'url': 'wireless:wirelesslink_list',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
JOURNAL_TYPES = {
|
|
||||||
'journalentry': {
|
|
||||||
'queryset': JournalEntry.objects.prefetch_related('assigned_object', 'created_by'),
|
|
||||||
'filterset': extras.filtersets.JournalEntryFilterSet,
|
|
||||||
'table': extras.tables.JournalEntryTable,
|
|
||||||
'url': 'extras:journalentry_list',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
SEARCH_TYPE_HIERARCHY = {
|
|
||||||
'Circuits': CIRCUIT_TYPES,
|
|
||||||
'DCIM': DCIM_TYPES,
|
|
||||||
'IPAM': IPAM_TYPES,
|
|
||||||
'Tenancy': TENANCY_TYPES,
|
|
||||||
'Virtualization': VIRTUALIZATION_TYPES,
|
|
||||||
'Wireless': WIRELESS_TYPES,
|
|
||||||
'Journal': JOURNAL_TYPES,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build_search_types():
|
|
||||||
result = dict()
|
|
||||||
|
|
||||||
for app_types in SEARCH_TYPE_HIERARCHY.values():
|
|
||||||
for name, items in app_types.items():
|
|
||||||
result[name] = items
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
SEARCH_TYPES = build_search_types()
|
|
33
netbox/netbox/search/__init__.py
Normal file
33
netbox/netbox/search/__init__.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from extras.registry import registry
|
||||||
|
|
||||||
|
|
||||||
|
class SearchIndex:
|
||||||
|
"""
|
||||||
|
Base class for building search indexes.
|
||||||
|
|
||||||
|
Attrs:
|
||||||
|
model: The model class for which this index is used.
|
||||||
|
"""
|
||||||
|
model = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_category(cls):
|
||||||
|
"""
|
||||||
|
Return the title of the search category under which this model is registered.
|
||||||
|
"""
|
||||||
|
if hasattr(cls, 'category'):
|
||||||
|
return cls.category
|
||||||
|
return cls.model._meta.app_config.verbose_name
|
||||||
|
|
||||||
|
|
||||||
|
def register_search():
|
||||||
|
def _wrapper(cls):
|
||||||
|
model = cls.model
|
||||||
|
app_label = model._meta.app_label
|
||||||
|
model_name = model._meta.model_name
|
||||||
|
|
||||||
|
registry['search'][app_label][model_name] = cls
|
||||||
|
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return _wrapper
|
125
netbox/netbox/search/backends.py
Normal file
125
netbox/netbox/search/backends.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from extras.registry import registry
|
||||||
|
from netbox.constants import SEARCH_MAX_RESULTS
|
||||||
|
|
||||||
|
# The cache for the initialized backend.
|
||||||
|
_backends_cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
class SearchEngineError(Exception):
|
||||||
|
"""Something went wrong with a search engine."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SearchBackend:
|
||||||
|
"""A search engine capable of performing multi-table searches."""
|
||||||
|
_search_choice_options = tuple()
|
||||||
|
|
||||||
|
def get_registry(self):
|
||||||
|
r = {}
|
||||||
|
for app_label, models in registry['search'].items():
|
||||||
|
r.update(**models)
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
def get_search_choices(self):
|
||||||
|
"""Return the set of choices for individual object types, organized by category."""
|
||||||
|
if not self._search_choice_options:
|
||||||
|
|
||||||
|
# Organize choices by category
|
||||||
|
categories = defaultdict(dict)
|
||||||
|
for app_label, models in registry['search'].items():
|
||||||
|
for name, cls in models.items():
|
||||||
|
title = cls.model._meta.verbose_name.title()
|
||||||
|
categories[cls.get_category()][name] = title
|
||||||
|
|
||||||
|
# Compile a nested tuple of choices for form rendering
|
||||||
|
results = (
|
||||||
|
('', 'All Objects'),
|
||||||
|
*[(category, choices.items()) for category, choices in categories.items()]
|
||||||
|
)
|
||||||
|
|
||||||
|
self._search_choice_options = results
|
||||||
|
|
||||||
|
return self._search_choice_options
|
||||||
|
|
||||||
|
def search(self, request, value, **kwargs):
|
||||||
|
"""Execute a search query for the given value."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def cache(self, instance):
|
||||||
|
"""Create or update the cached copy of an instance."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class FilterSetSearchBackend(SearchBackend):
|
||||||
|
"""
|
||||||
|
Legacy search backend. Performs a discrete database query for each registered object type, using the FilterSet
|
||||||
|
class specified by the index for each.
|
||||||
|
"""
|
||||||
|
def search(self, request, value, **kwargs):
|
||||||
|
results = []
|
||||||
|
|
||||||
|
search_registry = self.get_registry()
|
||||||
|
for obj_type in search_registry.keys():
|
||||||
|
|
||||||
|
queryset = search_registry[obj_type].queryset
|
||||||
|
url = search_registry[obj_type].url
|
||||||
|
|
||||||
|
# Restrict the queryset for the current user
|
||||||
|
if hasattr(queryset, 'restrict'):
|
||||||
|
queryset = queryset.restrict(request.user, 'view')
|
||||||
|
|
||||||
|
filterset = getattr(search_registry[obj_type], 'filterset', None)
|
||||||
|
if not filterset:
|
||||||
|
# This backend requires a FilterSet class for the model
|
||||||
|
continue
|
||||||
|
|
||||||
|
table = getattr(search_registry[obj_type], 'table', None)
|
||||||
|
if not table:
|
||||||
|
# This backend requires a Table class for the model
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Construct the results table for this object type
|
||||||
|
filtered_queryset = filterset({'q': value}, queryset=queryset).qs
|
||||||
|
table = table(filtered_queryset, orderable=False)
|
||||||
|
table.paginate(per_page=SEARCH_MAX_RESULTS)
|
||||||
|
|
||||||
|
if table.page:
|
||||||
|
results.append({
|
||||||
|
'name': queryset.model._meta.verbose_name_plural,
|
||||||
|
'table': table,
|
||||||
|
'url': f"{reverse(url)}?q={value}"
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def cache(self, instance):
|
||||||
|
# This backend does not utilize a cache
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_backend():
|
||||||
|
"""Initializes and returns the configured search backend."""
|
||||||
|
backend_name = settings.SEARCH_BACKEND
|
||||||
|
|
||||||
|
# Load the backend class
|
||||||
|
backend_module_name, backend_cls_name = backend_name.rsplit('.', 1)
|
||||||
|
backend_module = import_module(backend_module_name)
|
||||||
|
try:
|
||||||
|
backend_cls = getattr(backend_module, backend_cls_name)
|
||||||
|
except AttributeError:
|
||||||
|
raise ImproperlyConfigured(f"Could not find a class named {backend_module_name} in {backend_cls_name}")
|
||||||
|
|
||||||
|
# Initialize and return the backend instance
|
||||||
|
return backend_cls()
|
||||||
|
|
||||||
|
|
||||||
|
default_search_engine = get_backend()
|
||||||
|
search = default_search_engine.search
|
@ -18,11 +18,6 @@ from sentry_sdk.integrations.django import DjangoIntegration
|
|||||||
|
|
||||||
from netbox.config import PARAMS
|
from netbox.config import PARAMS
|
||||||
|
|
||||||
# Monkey patch to fix Django 4.0 support for graphene-django (see
|
|
||||||
# https://github.com/graphql-python/graphene-django/issues/1284)
|
|
||||||
# TODO: Remove this when graphene-django 2.16 becomes available
|
|
||||||
django.utils.encoding.force_text = force_str # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Environment setup
|
# Environment setup
|
||||||
@ -121,6 +116,7 @@ REMOTE_AUTH_GROUP_SEPARATOR = getattr(configuration, 'REMOTE_AUTH_GROUP_SEPARATO
|
|||||||
REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
|
REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
|
||||||
RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
|
RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
|
||||||
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
|
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
|
||||||
|
SEARCH_BACKEND = getattr(configuration, 'SEARCH_BACKEND', 'netbox.search.backends.FilterSetSearchBackend')
|
||||||
SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', DEFAULT_SENTRY_DSN)
|
SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', DEFAULT_SENTRY_DSN)
|
||||||
SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
|
SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
|
||||||
SENTRY_SAMPLE_RATE = getattr(configuration, 'SENTRY_SAMPLE_RATE', 1.0)
|
SENTRY_SAMPLE_RATE = getattr(configuration, 'SENTRY_SAMPLE_RATE', 1.0)
|
||||||
@ -497,7 +493,7 @@ for param in dir(configuration):
|
|||||||
|
|
||||||
# Force usage of PostgreSQL's JSONB field for extra data
|
# Force usage of PostgreSQL's JSONB field for extra data
|
||||||
SOCIAL_AUTH_JSONFIELD_ENABLED = True
|
SOCIAL_AUTH_JSONFIELD_ENABLED = True
|
||||||
|
SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = 'netbox.users.utils.clean_username'
|
||||||
|
|
||||||
#
|
#
|
||||||
# Django Prometheus
|
# Django Prometheus
|
||||||
@ -648,7 +644,6 @@ RQ_QUEUES = {
|
|||||||
#
|
#
|
||||||
|
|
||||||
for plugin_name in PLUGINS:
|
for plugin_name in PLUGINS:
|
||||||
|
|
||||||
# Import plugin module
|
# Import plugin module
|
||||||
try:
|
try:
|
||||||
plugin = importlib.import_module(plugin_name)
|
plugin = importlib.import_module(plugin_name)
|
||||||
|
@ -23,7 +23,7 @@ from extras.tables import ObjectChangeTable
|
|||||||
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
|
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
|
||||||
from netbox.constants import SEARCH_MAX_RESULTS
|
from netbox.constants import SEARCH_MAX_RESULTS
|
||||||
from netbox.forms import SearchForm
|
from netbox.forms import SearchForm
|
||||||
from netbox.search import SEARCH_TYPES
|
from netbox.search.backends import default_search_engine
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from virtualization.models import Cluster, VirtualMachine
|
from virtualization.models import Cluster, VirtualMachine
|
||||||
from wireless.models import WirelessLAN, WirelessLink
|
from wireless.models import WirelessLAN, WirelessLink
|
||||||
@ -153,31 +153,14 @@ class SearchView(View):
|
|||||||
results = []
|
results = []
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
search_registry = default_search_engine.get_registry()
|
||||||
# If an object type has been specified, redirect to the dedicated view for it
|
# If an object type has been specified, redirect to the dedicated view for it
|
||||||
if form.cleaned_data['obj_type']:
|
if form.cleaned_data['obj_type']:
|
||||||
object_type = form.cleaned_data['obj_type']
|
object_type = form.cleaned_data['obj_type']
|
||||||
url = reverse(SEARCH_TYPES[object_type]['url'])
|
url = reverse(search_registry[object_type].url)
|
||||||
return redirect(f"{url}?q={form.cleaned_data['q']}")
|
return redirect(f"{url}?q={form.cleaned_data['q']}")
|
||||||
|
|
||||||
for obj_type in SEARCH_TYPES.keys():
|
results = default_search_engine.search(request, form.cleaned_data['q'])
|
||||||
|
|
||||||
queryset = SEARCH_TYPES[obj_type]['queryset'].restrict(request.user, 'view')
|
|
||||||
filterset = SEARCH_TYPES[obj_type]['filterset']
|
|
||||||
table = SEARCH_TYPES[obj_type]['table']
|
|
||||||
url = SEARCH_TYPES[obj_type]['url']
|
|
||||||
|
|
||||||
# Construct the results table for this object type
|
|
||||||
filtered_queryset = filterset({'q': form.cleaned_data['q']}, queryset=queryset).qs
|
|
||||||
table = table(filtered_queryset, orderable=False)
|
|
||||||
table.paginate(per_page=SEARCH_MAX_RESULTS)
|
|
||||||
|
|
||||||
if table.page:
|
|
||||||
results.append({
|
|
||||||
'name': queryset.model._meta.verbose_name_plural,
|
|
||||||
'table': table,
|
|
||||||
'url': f"{reverse(url)}?q={form.cleaned_data.get('q')}"
|
|
||||||
})
|
|
||||||
|
|
||||||
return render(request, 'search.html', {
|
return render(request, 'search.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
|
@ -31,8 +31,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
"@typescript-eslint/no-unused-vars": "error",
|
||||||
"@typescript-eslint/no-unused-vars-experimental": "error",
|
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"no-inner-declarations": "off",
|
"no-inner-declarations": "off",
|
||||||
"comma-dangle": ["error", "always-multiline"],
|
"comma-dangle": ["error", "always-multiline"],
|
||||||
|
BIN
netbox/project-static/dist/cable_trace.css
vendored
BIN
netbox/project-static/dist/cable_trace.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/config.js
vendored
BIN
netbox/project-static/dist/config.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/config.js.map
vendored
BIN
netbox/project-static/dist/config.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/graphiql.css
vendored
BIN
netbox/project-static/dist/graphiql.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/graphiql.js
vendored
BIN
netbox/project-static/dist/graphiql.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/graphiql.js.map
vendored
BIN
netbox/project-static/dist/graphiql.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js
vendored
BIN
netbox/project-static/dist/lldp.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js.map
vendored
BIN
netbox/project-static/dist/lldp.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/materialdesignicons-webfont-DWVXV5L5.woff
vendored
Normal file
BIN
netbox/project-static/dist/materialdesignicons-webfont-DWVXV5L5.woff
vendored
Normal file
Binary file not shown.
BIN
netbox/project-static/dist/materialdesignicons-webfont-ER2MFQKM.woff2
vendored
Normal file
BIN
netbox/project-static/dist/materialdesignicons-webfont-ER2MFQKM.woff2
vendored
Normal file
Binary file not shown.
BIN
netbox/project-static/dist/materialdesignicons-webfont-UHEFFMSX.eot
vendored
Normal file
BIN
netbox/project-static/dist/materialdesignicons-webfont-UHEFFMSX.eot
vendored
Normal file
Binary file not shown.
BIN
netbox/project-static/dist/materialdesignicons-webfont-WM6M6ZHQ.ttf
vendored
Normal file
BIN
netbox/project-static/dist/materialdesignicons-webfont-WM6M6ZHQ.ttf
vendored
Normal file
Binary file not shown.
BIN
netbox/project-static/dist/netbox-dark.css
vendored
BIN
netbox/project-static/dist/netbox-dark.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-external.css
vendored
BIN
netbox/project-static/dist/netbox-external.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-light.css
vendored
BIN
netbox/project-static/dist/netbox-light.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-print.css
vendored
BIN
netbox/project-static/dist/netbox-print.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/rack_elevation.css
vendored
BIN
netbox/project-static/dist/rack_elevation.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/status.js
vendored
BIN
netbox/project-static/dist/status.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/status.js.map
vendored
BIN
netbox/project-static/dist/status.js.map
vendored
Binary file not shown.
@ -22,43 +22,38 @@
|
|||||||
"validate:formatting:scripts": "prettier -c src/**/*.ts"
|
"validate:formatting:scripts": "prettier -c src/**/*.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/font": "^5.9.55",
|
"@mdi/font": "^7.0.96",
|
||||||
"@popperjs/core": "^2.9.2",
|
"@popperjs/core": "^2.11.6",
|
||||||
"bootstrap": "~5.0.2",
|
"bootstrap": "~5.0.2",
|
||||||
"clipboard": "^2.0.8",
|
"clipboard": "^2.0.11",
|
||||||
"color2k": "^1.2.4",
|
"color2k": "^2.0.0",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.11.5",
|
||||||
"flatpickr": "4.6.13",
|
"flatpickr": "4.6.13",
|
||||||
"htmx.org": "^1.6.1",
|
"htmx.org": "^1.8.0",
|
||||||
"just-debounce-it": "^1.4.0",
|
"just-debounce-it": "^3.1.1",
|
||||||
"masonry-layout": "^4.2.2",
|
"masonry-layout": "^4.2.2",
|
||||||
"query-string": "^6.14.1",
|
"query-string": "^7.1.1",
|
||||||
"sass": "^1.32.8",
|
"sass": "^1.55.0",
|
||||||
"simplebar": "^5.3.4",
|
"simplebar": "^5.3.9",
|
||||||
"slim-select": "^1.27.0"
|
"slim-select": "^1.27.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bootstrap": "^5.0.12",
|
"@types/bootstrap": "^5.0.17",
|
||||||
"@types/cookie": "^0.4.0",
|
"@types/cookie": "^0.5.1",
|
||||||
"@types/masonry-layout": "^4.2.2",
|
"@types/masonry-layout": "^4.2.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.29.3",
|
"@typescript-eslint/eslint-plugin": "^5.39.0",
|
||||||
"@typescript-eslint/parser": "^4.29.3",
|
"@typescript-eslint/parser": "^5.39.0",
|
||||||
"esbuild": "^0.12.24",
|
"esbuild": "^0.13.15",
|
||||||
"esbuild-sass-plugin": "^1.5.2",
|
"esbuild-sass-plugin": "^2.3.3",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^8.24.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-import-resolver-typescript": "^2.4.0",
|
"eslint-import-resolver-typescript": "^3.5.1",
|
||||||
"eslint-plugin-import": "^2.24.2",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-prettier": "^3.4.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.7.1",
|
||||||
"typescript": "~4.3.5"
|
"typescript": "~4.8.4"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"eslint-import-resolver-typescript/**/path-parse": "^1.0.7",
|
"@types/bootstrap/**/@popperjs/core": "^2.11.6"
|
||||||
"slim-select/**/trim-newlines": "^3.0.1",
|
|
||||||
"eslint/glob-parent": "^5.1.2",
|
|
||||||
"esbuild-sass-plugin/**/glob-parent": "^5.1.2",
|
|
||||||
"@typescript-eslint/**/glob-parent": "^5.1.2",
|
|
||||||
"eslint-plugin-import/**/hosted-git-info": "^2.8.9"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -37,14 +37,12 @@ function initDocument(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initWindow(): void {
|
function initWindow(): void {
|
||||||
|
const documentForms = document.forms;
|
||||||
const documentForms = document.forms
|
for (const documentForm of documentForms) {
|
||||||
for (var documentForm of documentForms) {
|
|
||||||
if (documentForm.method.toUpperCase() == 'GET') {
|
if (documentForm.method.toUpperCase() == 'GET') {
|
||||||
// @ts-ignore: Our version of typescript seems to be too old for FormDataEvent
|
documentForm.addEventListener('formdata', function (event: FormDataEvent) {
|
||||||
documentForm.addEventListener('formdata', function(event: FormDataEvent) {
|
const formData: FormData = event.formData;
|
||||||
let formData: FormData = event.formData;
|
for (const [name, value] of Array.from(formData.entries())) {
|
||||||
for (let [name, value] of Array.from(formData.entries())) {
|
|
||||||
if (value === '') formData.delete(name);
|
if (value === '') formData.delete(name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -32,7 +32,7 @@ $spacing-s: $input-padding-x;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@import './node_modules/slim-select/src/slim-select/slimselect';
|
@import '../node_modules/slim-select/src/slim-select/slimselect';
|
||||||
|
|
||||||
.ss-main {
|
.ss-main {
|
||||||
color: $form-select-color;
|
color: $form-select-color;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -60,23 +60,17 @@
|
|||||||
</div>
|
</div>
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/tags.html' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
|
{% include 'inc/panels/comments.html' %}
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/panels/comments.html' %}
|
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
|
||||||
|
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
{% include 'inc/panels/contacts.html' %}
|
||||||
{% include 'inc/panels/image_attachments.html' %}
|
{% include 'inc/panels/image_attachments.html' %}
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
<div class="col col-md-6">
|
|
||||||
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-6">
|
|
||||||
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
{% plugin_full_width_page object %}
|
{% plugin_full_width_page object %}
|
||||||
|
@ -77,10 +77,10 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.poweroutlet&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Outlet</a>
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.poweroutlet&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Power Outlet</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.powerfeed&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Feed</a>
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.powerfeed&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Power Feed</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
|
@ -105,16 +105,16 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Interface</a>
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Interface</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Front Port</a>
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Rear Port</a>
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ object.device.site.pk }}&return_url={{ object.get_absolute_url }}">Circuit Termination</a>
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ object.device.site.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Circuit Termination</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
|
@ -3,3 +3,6 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
class TenancyConfig(AppConfig):
|
class TenancyConfig(AppConfig):
|
||||||
name = 'tenancy'
|
name = 'tenancy'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from . import search
|
||||||
|
@ -27,6 +27,12 @@ class TenantGroupForm(NetBoxModelForm):
|
|||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Tenant Group', (
|
||||||
|
'parent', 'name', 'slug', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TenantGroup
|
model = TenantGroup
|
||||||
fields = [
|
fields = [
|
||||||
@ -64,6 +70,12 @@ class ContactGroupForm(NetBoxModelForm):
|
|||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Contact Group', (
|
||||||
|
'parent', 'name', 'slug', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContactGroup
|
model = ContactGroup
|
||||||
fields = ('parent', 'name', 'slug', 'description', 'tags')
|
fields = ('parent', 'name', 'slug', 'description', 'tags')
|
||||||
@ -72,6 +84,12 @@ class ContactGroupForm(NetBoxModelForm):
|
|||||||
class ContactRoleForm(NetBoxModelForm):
|
class ContactRoleForm(NetBoxModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Contact Role', (
|
||||||
|
'name', 'slug', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContactRole
|
model = ContactRole
|
||||||
fields = ('name', 'slug', 'description', 'tags')
|
fields = ('name', 'slug', 'description', 'tags')
|
||||||
|
25
netbox/tenancy/search.py
Normal file
25
netbox/tenancy/search.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import tenancy.filtersets
|
||||||
|
import tenancy.tables
|
||||||
|
from netbox.search import SearchIndex, register_search
|
||||||
|
from tenancy.models import Contact, ContactAssignment, Tenant
|
||||||
|
from utilities.utils import count_related
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class TenantIndex(SearchIndex):
|
||||||
|
model = Tenant
|
||||||
|
queryset = Tenant.objects.prefetch_related('group')
|
||||||
|
filterset = tenancy.filtersets.TenantFilterSet
|
||||||
|
table = tenancy.tables.TenantTable
|
||||||
|
url = 'tenancy:tenant_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class ContactIndex(SearchIndex):
|
||||||
|
model = Contact
|
||||||
|
queryset = Contact.objects.prefetch_related('group', 'assignments').annotate(
|
||||||
|
assignment_count=count_related(ContactAssignment, 'contact')
|
||||||
|
)
|
||||||
|
filterset = tenancy.filtersets.ContactFilterSet
|
||||||
|
table = tenancy.tables.ContactTable
|
||||||
|
url = 'tenancy:contact_list'
|
@ -1,6 +1,9 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
||||||
|
from netbox.tables import columns
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'ContactsColumnMixin',
|
||||||
'TenantColumn',
|
'TenantColumn',
|
||||||
'TenantGroupColumn',
|
'TenantGroupColumn',
|
||||||
'TenancyColumnsMixin',
|
'TenancyColumnsMixin',
|
||||||
@ -55,3 +58,10 @@ class TenantGroupColumn(tables.TemplateColumn):
|
|||||||
class TenancyColumnsMixin(tables.Table):
|
class TenancyColumnsMixin(tables.Table):
|
||||||
tenant_group = TenantGroupColumn()
|
tenant_group = TenantGroupColumn()
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
|
|
||||||
|
|
||||||
|
class ContactsColumnMixin(tables.Table):
|
||||||
|
contacts = columns.ManyToManyColumn(
|
||||||
|
linkify_item=True,
|
||||||
|
transform=lambda obj: obj.contact.name
|
||||||
|
)
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
from tenancy.models import *
|
||||||
|
from tenancy.tables import ContactsColumnMixin
|
||||||
|
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.models import *
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'TenantGroupTable',
|
'TenantGroupTable',
|
||||||
@ -30,7 +31,7 @@ class TenantGroupTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'name', 'tenant_count', 'description')
|
default_columns = ('pk', 'name', 'tenant_count', 'description')
|
||||||
|
|
||||||
|
|
||||||
class TenantTable(NetBoxTable):
|
class TenantTable(ContactsColumnMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -38,9 +39,6 @@ class TenantTable(NetBoxTable):
|
|||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='tenancy:contact_list'
|
url_name='tenancy:contact_list'
|
||||||
)
|
)
|
||||||
|
9
netbox/users/utils.py
Normal file
9
netbox/users/utils.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from social_core.storage import NO_ASCII_REGEX, NO_SPECIAL_REGEX
|
||||||
|
|
||||||
|
|
||||||
|
def clean_username(value):
|
||||||
|
"""Clean username removing any unsupported character"""
|
||||||
|
value = NO_ASCII_REGEX.sub('', value)
|
||||||
|
value = NO_SPECIAL_REGEX.sub('', value)
|
||||||
|
value = value.replace(':', '')
|
||||||
|
return value
|
@ -1,16 +1,18 @@
|
|||||||
from typing import Dict
|
from typing import Dict
|
||||||
from netbox.forms import SearchForm
|
|
||||||
from django import template
|
from django import template
|
||||||
|
|
||||||
register = template.Library()
|
from netbox.forms import SearchForm
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
search_form = SearchForm()
|
search_form = SearchForm()
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag("search/searchbar.html")
|
@register.inclusion_tag("search/searchbar.html")
|
||||||
def search_options(request) -> Dict:
|
def search_options(request) -> Dict:
|
||||||
"""Provide search options to template."""
|
|
||||||
|
# Provide search options to template.
|
||||||
return {
|
return {
|
||||||
'options': search_form.options,
|
'options': search_form.get_options(),
|
||||||
'request': request,
|
'request': request,
|
||||||
}
|
}
|
||||||
|
@ -450,6 +450,9 @@ class APIViewTestCases:
|
|||||||
if type(field) is GQLDynamic:
|
if type(field) is GQLDynamic:
|
||||||
# Dynamic fields must specify a subselection
|
# Dynamic fields must specify a subselection
|
||||||
fields_string += f'{field_name} {{ id }}\n'
|
fields_string += f'{field_name} {{ id }}\n'
|
||||||
|
elif inspect.isclass(field.type) and issubclass(field.type, GQLUnion):
|
||||||
|
# Union types dont' have an id or consistent values
|
||||||
|
continue
|
||||||
elif type(field.type) is GQLList and inspect.isclass(field.type.of_type) and issubclass(field.type.of_type, GQLUnion):
|
elif type(field.type) is GQLList and inspect.isclass(field.type.of_type) and issubclass(field.type.of_type, GQLUnion):
|
||||||
# Union types dont' have an id or consistent values
|
# Union types dont' have an id or consistent values
|
||||||
continue
|
continue
|
||||||
|
@ -140,19 +140,22 @@ class ViewTab:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
label: Human-friendly text
|
label: Human-friendly text
|
||||||
badge: A static value or callable to display alongside the label (optional). If a callable is used, it must accept a single
|
badge: A static value or callable to display alongside the label (optional). If a callable is used, it must
|
||||||
argument representing the object being viewed.
|
accept a single argument representing the object being viewed.
|
||||||
permission: The permission required to display the tab (optional).
|
permission: The permission required to display the tab (optional).
|
||||||
|
hide_if_empty: If true, the tab will be displayed only if its badge has a meaningful value. (Tabs without a
|
||||||
|
badge are always displayed.)
|
||||||
"""
|
"""
|
||||||
def __init__(self, label, badge=None, permission=None):
|
def __init__(self, label, badge=None, permission=None, hide_if_empty=False):
|
||||||
self.label = label
|
self.label = label
|
||||||
self.badge = badge
|
self.badge = badge
|
||||||
self.permission = permission
|
self.permission = permission
|
||||||
|
self.hide_if_empty = hide_if_empty
|
||||||
|
|
||||||
def render(self, instance):
|
def render(self, instance):
|
||||||
"""Return the attributes needed to render a tab in HTML."""
|
"""Return the attributes needed to render a tab in HTML."""
|
||||||
badge_value = self._get_badge_value(instance)
|
badge_value = self._get_badge_value(instance)
|
||||||
if self.badge and not badge_value:
|
if self.badge and self.hide_if_empty and not badge_value:
|
||||||
return None
|
return None
|
||||||
return {
|
return {
|
||||||
'label': self.label,
|
'label': self.label,
|
||||||
|
@ -3,3 +3,6 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
class VirtualizationConfig(AppConfig):
|
class VirtualizationConfig(AppConfig):
|
||||||
name = 'virtualization'
|
name = 'virtualization'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from . import search
|
||||||
|
@ -28,6 +28,12 @@ __all__ = (
|
|||||||
class ClusterTypeForm(NetBoxModelForm):
|
class ClusterTypeForm(NetBoxModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Cluster Type', (
|
||||||
|
'name', 'slug', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ClusterType
|
model = ClusterType
|
||||||
fields = (
|
fields = (
|
||||||
@ -38,6 +44,12 @@ class ClusterTypeForm(NetBoxModelForm):
|
|||||||
class ClusterGroupForm(NetBoxModelForm):
|
class ClusterGroupForm(NetBoxModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Cluster Group', (
|
||||||
|
'name', 'slug', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ClusterGroup
|
model = ClusterGroup
|
||||||
fields = (
|
fields = (
|
||||||
|
33
netbox/virtualization/search.py
Normal file
33
netbox/virtualization/search.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import virtualization.filtersets
|
||||||
|
import virtualization.tables
|
||||||
|
from dcim.models import Device
|
||||||
|
from netbox.search import SearchIndex, register_search
|
||||||
|
from utilities.utils import count_related
|
||||||
|
from virtualization.models import Cluster, VirtualMachine
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class ClusterIndex(SearchIndex):
|
||||||
|
model = Cluster
|
||||||
|
queryset = Cluster.objects.prefetch_related('type', 'group').annotate(
|
||||||
|
device_count=count_related(Device, 'cluster'), vm_count=count_related(VirtualMachine, 'cluster')
|
||||||
|
)
|
||||||
|
filterset = virtualization.filtersets.ClusterFilterSet
|
||||||
|
table = virtualization.tables.ClusterTable
|
||||||
|
url = 'virtualization:cluster_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class VirtualMachineIndex(SearchIndex):
|
||||||
|
model = VirtualMachine
|
||||||
|
queryset = VirtualMachine.objects.prefetch_related(
|
||||||
|
'cluster',
|
||||||
|
'tenant',
|
||||||
|
'tenant__group',
|
||||||
|
'platform',
|
||||||
|
'primary_ip4',
|
||||||
|
'primary_ip6',
|
||||||
|
)
|
||||||
|
filterset = virtualization.filtersets.VirtualMachineFilterSet
|
||||||
|
table = virtualization.tables.VirtualMachineTable
|
||||||
|
url = 'virtualization:virtualmachine_list'
|
@ -1,8 +1,8 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||||
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
|
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenancyColumnsMixin
|
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ClusterTable',
|
'ClusterTable',
|
||||||
@ -32,7 +32,7 @@ class ClusterTypeTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'name', 'cluster_count', 'description')
|
default_columns = ('pk', 'name', 'cluster_count', 'description')
|
||||||
|
|
||||||
|
|
||||||
class ClusterGroupTable(NetBoxTable):
|
class ClusterGroupTable(ContactsColumnMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -41,9 +41,6 @@ class ClusterGroupTable(NetBoxTable):
|
|||||||
url_params={'group_id': 'pk'},
|
url_params={'group_id': 'pk'},
|
||||||
verbose_name='Clusters'
|
verbose_name='Clusters'
|
||||||
)
|
)
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='virtualization:clustergroup_list'
|
url_name='virtualization:clustergroup_list'
|
||||||
)
|
)
|
||||||
@ -57,7 +54,7 @@ class ClusterGroupTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'name', 'cluster_count', 'description')
|
default_columns = ('pk', 'name', 'cluster_count', 'description')
|
||||||
|
|
||||||
|
|
||||||
class ClusterTable(TenancyColumnsMixin, NetBoxTable):
|
class ClusterTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -81,9 +78,6 @@ class ClusterTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
verbose_name='VMs'
|
verbose_name='VMs'
|
||||||
)
|
)
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='virtualization:cluster_list'
|
url_name='virtualization:cluster_list'
|
||||||
)
|
)
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
||||||
from dcim.tables.devices import BaseInterfaceTable
|
from dcim.tables.devices import BaseInterfaceTable
|
||||||
from netbox.tables import NetBoxTable, columns
|
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||||
from tenancy.tables import TenancyColumnsMixin
|
|
||||||
from virtualization.models import VirtualMachine, VMInterface
|
from virtualization.models import VirtualMachine, VMInterface
|
||||||
|
|
||||||
|
from netbox.tables import NetBoxTable, columns
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'VirtualMachineTable',
|
'VirtualMachineTable',
|
||||||
'VirtualMachineVMInterfaceTable',
|
'VirtualMachineVMInterfaceTable',
|
||||||
@ -37,7 +37,7 @@ VMINTERFACE_BUTTONS = """
|
|||||||
# Virtual machines
|
# Virtual machines
|
||||||
#
|
#
|
||||||
|
|
||||||
class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable):
|
class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
order_by=('_name',),
|
order_by=('_name',),
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -67,9 +67,6 @@ class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
order_by=('primary_ip4', 'primary_ip6'),
|
order_by=('primary_ip4', 'primary_ip6'),
|
||||||
verbose_name='IP Address'
|
verbose_name='IP Address'
|
||||||
)
|
)
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='virtualization:virtualmachine_list'
|
url_name='virtualization:virtualmachine_list'
|
||||||
)
|
)
|
||||||
|
@ -5,4 +5,4 @@ class WirelessConfig(AppConfig):
|
|||||||
name = 'wireless'
|
name = 'wireless'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import wireless.signals
|
from . import signals, search
|
||||||
|
@ -19,6 +19,12 @@ class WirelessLANGroupForm(NetBoxModelForm):
|
|||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Wireless LAN Group', (
|
||||||
|
'parent', 'name', 'slug', 'description', 'tags',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WirelessLANGroup
|
model = WirelessLANGroup
|
||||||
fields = [
|
fields = [
|
||||||
|
26
netbox/wireless/search.py
Normal file
26
netbox/wireless/search.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import wireless.filtersets
|
||||||
|
import wireless.tables
|
||||||
|
from dcim.models import Interface
|
||||||
|
from netbox.search import SearchIndex, register_search
|
||||||
|
from utilities.utils import count_related
|
||||||
|
from wireless.models import WirelessLAN, WirelessLink
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class WirelessLANIndex(SearchIndex):
|
||||||
|
model = WirelessLAN
|
||||||
|
queryset = WirelessLAN.objects.prefetch_related('group', 'vlan').annotate(
|
||||||
|
interface_count=count_related(Interface, 'wireless_lans')
|
||||||
|
)
|
||||||
|
filterset = wireless.filtersets.WirelessLANFilterSet
|
||||||
|
table = wireless.tables.WirelessLANTable
|
||||||
|
url = 'wireless:wirelesslan_list'
|
||||||
|
|
||||||
|
|
||||||
|
@register_search()
|
||||||
|
class WirelessLinkIndex(SearchIndex):
|
||||||
|
model = WirelessLink
|
||||||
|
queryset = WirelessLink.objects.prefetch_related('interface_a__device', 'interface_b__device')
|
||||||
|
filterset = wireless.filtersets.WirelessLinkFilterSet
|
||||||
|
table = wireless.tables.WirelessLinkTable
|
||||||
|
url = 'wireless:wirelesslink_list'
|
@ -27,10 +27,13 @@ psycopg2-binary==2.9.3
|
|||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
sentry-sdk==1.9.10
|
sentry-sdk==1.9.10
|
||||||
social-auth-app-django==5.0.0
|
social-auth-app-django==5.0.0
|
||||||
social-auth-core==4.3.0
|
social-auth-core[openidconnect]==4.3.0
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
tablib==3.2.1
|
tablib==3.2.1
|
||||||
tzdata==2022.4
|
tzdata==2022.4
|
||||||
|
|
||||||
# Workaround for #7401
|
# Workaround for #7401
|
||||||
jsonschema==3.2.0
|
jsonschema==3.2.0
|
||||||
|
|
||||||
|
# Temporary fix for #10712
|
||||||
|
swagger-spec-validator==2.7.6
|
||||||
|
Loading…
Reference in New Issue
Block a user