mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -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
|
||||
description: To what section of the documentation does this change primarily pertain?
|
||||
options:
|
||||
- Installation instructions
|
||||
- Configuration parameters
|
||||
- Functionality/features
|
||||
- REST API
|
||||
- Administration/development
|
||||
- Features
|
||||
- Installation/upgrade
|
||||
- Getting started
|
||||
- Configuration
|
||||
- Customization
|
||||
- Integrations/API
|
||||
- Plugins
|
||||
- Administration
|
||||
- Development
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
@ -46,7 +46,7 @@ Next, create a file in the same directory as `configuration.py` (typically `/opt
|
||||
### General Server Configuration
|
||||
|
||||
!!! 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
|
||||
import ldap
|
||||
@ -67,6 +67,16 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
|
||||
# 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_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.
|
||||
|
@ -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 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
|
||||
|
||||
@ -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}}"}
|
||||
```
|
||||
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
|
||||
|
||||
|
@ -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.
|
||||
|
||||
::: utilities.forms.ColorField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.CommentField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.JSONField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.MACAddressField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.SlugField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
## Choice Fields
|
||||
|
||||
::: utilities.forms.ChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.MultipleChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
## Dynamic Object Fields
|
||||
|
||||
::: utilities.forms.DynamicModelChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.DynamicModelMultipleChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
## Content Type Fields
|
||||
|
||||
::: utilities.forms.ContentTypeChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.ContentTypeMultipleChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
## CSV Import Fields
|
||||
|
||||
::: utilities.forms.CSVChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.CSVMultipleChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.CSVModelChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.CSVContentTypeField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.CSVMultipleContentTypeField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
@ -32,11 +32,11 @@ schema = MyQuery
|
||||
NetBox provides two object type classes for use by plugins.
|
||||
|
||||
::: netbox.graphql.types.BaseObjectType
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.graphql.types.NetBoxObjectType
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
## 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.graphql.fields.ObjectField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.graphql.fields.ObjectListField
|
||||
selection:
|
||||
options:
|
||||
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 |
|
||||
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
|
||||
| `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`) |
|
||||
| `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`) |
|
||||
|
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`.
|
||||
|
||||
::: netbox.tables.BooleanColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.ChoiceFieldColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.ColorColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.ColoredLabelColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.ContentTypeColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.ContentTypesColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.MarkdownColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.TagColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.TemplateColumn
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- __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.ObjectView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- get_object
|
||||
- get_template_name
|
||||
|
||||
::: netbox.views.generic.ObjectEditView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- get_object
|
||||
- alter_object
|
||||
|
||||
::: netbox.views.generic.ObjectDeleteView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- get_object
|
||||
|
||||
::: netbox.views.generic.ObjectChildrenView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- get_children
|
||||
- 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.ObjectListView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- get_table
|
||||
- export_table
|
||||
- export_template
|
||||
|
||||
::: netbox.views.generic.BulkImportView
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.views.generic.BulkEditView
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.views.generic.BulkDeleteView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- 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.
|
||||
|
||||
::: netbox.views.generic.ObjectChangeLogView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- get_form
|
||||
|
||||
::: netbox.views.generic.ObjectJournalView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- get_form
|
||||
|
||||
|
@ -2,6 +2,21 @@
|
||||
|
||||
## 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)
|
||||
|
@ -17,15 +17,19 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
|
||||
|
||||
### 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
|
||||
* [#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
|
||||
* [#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
|
||||
* [#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
|
||||
* [#10595](https://github.com/netbox-community/netbox/issues/10595) - Add GraphQL relationships for additional generic foreign key fields
|
||||
|
||||
### 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
|
||||
* [#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
|
||||
@ -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
|
||||
* [#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
|
||||
* [#10699](https://github.com/netbox-community/netbox/issues/10699) - Remove custom `import_object()` function
|
||||
|
||||
### 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 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.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
|
||||
- django.setup()
|
||||
rendering:
|
||||
options:
|
||||
heading_level: 3
|
||||
members_order: source
|
||||
show_root_heading: true
|
||||
@ -132,6 +132,7 @@ nav:
|
||||
- GraphQL API: 'plugins/development/graphql-api.md'
|
||||
- Background Tasks: 'plugins/development/background-tasks.md'
|
||||
- Exceptions: 'plugins/development/exceptions.md'
|
||||
- Search: 'plugins/development/search.md'
|
||||
- Administration:
|
||||
- Authentication:
|
||||
- Overview: 'administration/authentication/overview.md'
|
||||
|
@ -6,4 +6,4 @@ class CircuitsConfig(AppConfig):
|
||||
verbose_name = "Circuits"
|
||||
|
||||
def ready(self):
|
||||
import circuits.signals
|
||||
from . import signals, search
|
||||
|
@ -64,6 +64,12 @@ class ProviderNetworkForm(NetBoxModelForm):
|
||||
class CircuitTypeForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Circuit Type', (
|
||||
'name', 'slug', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
|
@ -1,6 +1,8 @@
|
||||
import graphene
|
||||
|
||||
from circuits import filtersets, models
|
||||
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
|
||||
|
||||
__all__ = (
|
||||
@ -20,8 +22,7 @@ class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, Ob
|
||||
filterset_class = filtersets.CircuitTerminationFilterSet
|
||||
|
||||
|
||||
class CircuitType(NetBoxObjectType):
|
||||
|
||||
class CircuitType(NetBoxObjectType, ContactsMixin):
|
||||
class Meta:
|
||||
model = models.Circuit
|
||||
fields = '__all__'
|
||||
@ -36,7 +37,7 @@ class CircuitTypeType(OrganizationalObjectType):
|
||||
filterset_class = filtersets.CircuitTypeFilterSet
|
||||
|
||||
|
||||
class ProviderType(NetBoxObjectType):
|
||||
class ProviderType(NetBoxObjectType, ContactsMixin):
|
||||
|
||||
class Meta:
|
||||
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
|
||||
|
||||
from circuits.models import *
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from tenancy.tables import TenancyColumnsMixin
|
||||
|
||||
from .columns import CommitRateColumn
|
||||
|
||||
__all__ = (
|
||||
@ -39,7 +40,7 @@ class CircuitTypeTable(NetBoxTable):
|
||||
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
|
||||
|
||||
|
||||
class CircuitTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
cid = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Circuit ID'
|
||||
@ -58,9 +59,6 @@ class CircuitTable(TenancyColumnsMixin, NetBoxTable):
|
||||
)
|
||||
commit_rate = CommitRateColumn()
|
||||
comments = columns.MarkdownColumn()
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:circuit_list'
|
||||
)
|
||||
|
@ -1,7 +1,8 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from circuits.models import *
|
||||
from django_tables2.utils import Accessor
|
||||
from tenancy.tables import ContactsColumnMixin
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
|
||||
__all__ = (
|
||||
@ -10,7 +11,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderTable(NetBoxTable):
|
||||
class ProviderTable(ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@ -31,9 +32,6 @@ class ProviderTable(NetBoxTable):
|
||||
verbose_name='Circuits'
|
||||
)
|
||||
comments = columns.MarkdownColumn()
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:provider_list'
|
||||
)
|
||||
|
@ -8,7 +8,7 @@ class DCIMConfig(AppConfig):
|
||||
verbose_name = "DCIM"
|
||||
|
||||
def ready(self):
|
||||
import dcim.signals
|
||||
from . import signals, search
|
||||
from .models import CableTermination
|
||||
|
||||
# Register denormalized fields
|
||||
|
@ -108,7 +108,7 @@ def get_cable_form(a_type, b_type):
|
||||
label='Power Feed',
|
||||
disabled_indicator='_occupied',
|
||||
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()
|
||||
|
||||
fieldsets = (
|
||||
('Region', (
|
||||
'parent', 'name', 'slug', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = (
|
||||
@ -92,6 +98,12 @@ class SiteGroupForm(NetBoxModelForm):
|
||||
)
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Site Group', (
|
||||
'parent', 'name', 'slug', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
fields = (
|
||||
@ -213,6 +225,12 @@ class LocationForm(TenancyForm, NetBoxModelForm):
|
||||
class RackRoleForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Rack Role', (
|
||||
'name', 'slug', 'color', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = [
|
||||
@ -341,6 +359,12 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
|
||||
class ManufacturerForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Manufacturer', (
|
||||
'name', 'slug', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = [
|
||||
@ -413,6 +437,12 @@ class ModuleTypeForm(NetBoxModelForm):
|
||||
class DeviceRoleForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Device Role', (
|
||||
'name', 'slug', 'color', 'vm_role', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = [
|
||||
@ -429,6 +459,13 @@ class PlatformForm(NetBoxModelForm):
|
||||
max_length=64
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Platform', (
|
||||
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
|
||||
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = [
|
||||
@ -1584,6 +1621,12 @@ class InventoryItemForm(DeviceComponentForm):
|
||||
class InventoryItemRoleForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Inventory Item Role', (
|
||||
'name', 'slug', 'color', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemRole
|
||||
fields = [
|
||||
|
@ -2,24 +2,38 @@ import graphene
|
||||
from circuits.graphql.types import CircuitTerminationType
|
||||
from circuits.models import CircuitTermination
|
||||
from dcim.graphql.types import (
|
||||
ConsolePortTemplateType,
|
||||
ConsolePortType,
|
||||
ConsoleServerPortTemplateType,
|
||||
ConsoleServerPortType,
|
||||
FrontPortTemplateType,
|
||||
FrontPortType,
|
||||
InterfaceTemplateType,
|
||||
InterfaceType,
|
||||
PowerFeedType,
|
||||
PowerOutletTemplateType,
|
||||
PowerOutletType,
|
||||
PowerPortTemplateType,
|
||||
PowerPortType,
|
||||
RearPortTemplateType,
|
||||
RearPortType,
|
||||
)
|
||||
from dcim.models import (
|
||||
ConsolePort,
|
||||
ConsolePortTemplate,
|
||||
ConsoleServerPort,
|
||||
ConsoleServerPortTemplate,
|
||||
FrontPort,
|
||||
FrontPortTemplate,
|
||||
Interface,
|
||||
InterfaceTemplate,
|
||||
PowerFeed,
|
||||
PowerOutlet,
|
||||
PowerOutletTemplate,
|
||||
PowerPort,
|
||||
PowerPortTemplate,
|
||||
RearPort,
|
||||
RearPortTemplate,
|
||||
)
|
||||
|
||||
|
||||
@ -57,3 +71,99 @@ class LinkPeerType(graphene.Union):
|
||||
return PowerPortType
|
||||
if type(instance) == RearPort:
|
||||
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 extras.graphql.mixins import (
|
||||
ChangelogMixin, ConfigContextMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin,
|
||||
ChangelogMixin, ConfigContextMixin, ContactsMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin,
|
||||
)
|
||||
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
|
||||
from netbox.graphql.scalars import BigInt
|
||||
@ -87,6 +87,8 @@ class ComponentTemplateObjectType(
|
||||
#
|
||||
|
||||
class CableType(NetBoxObjectType):
|
||||
a_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType')
|
||||
b_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType')
|
||||
|
||||
class Meta:
|
||||
model = models.Cable
|
||||
@ -99,12 +101,19 @@ class CableType(NetBoxObjectType):
|
||||
def resolve_length_unit(self, info):
|
||||
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):
|
||||
termination = graphene.Field('dcim.graphql.gfk_mixins.CableTerminationTerminationType')
|
||||
|
||||
class Meta:
|
||||
model = models.CableTermination
|
||||
fields = '__all__'
|
||||
exclude = ('termination_type', 'termination_id')
|
||||
filterset_class = filtersets.CableTerminationFilterSet
|
||||
|
||||
|
||||
@ -152,7 +161,7 @@ class ConsoleServerPortTemplateType(ComponentTemplateObjectType):
|
||||
return self.type or None
|
||||
|
||||
|
||||
class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, NetBoxObjectType):
|
||||
class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Device
|
||||
@ -183,10 +192,11 @@ class DeviceBayTemplateType(ComponentTemplateObjectType):
|
||||
|
||||
|
||||
class InventoryItemTemplateType(ComponentTemplateObjectType):
|
||||
component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemTemplateComponentType')
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItemTemplate
|
||||
fields = '__all__'
|
||||
exclude = ('component_type', 'component_id')
|
||||
filterset_class = filtersets.InventoryItemTemplateFilterSet
|
||||
|
||||
|
||||
@ -269,10 +279,11 @@ class InterfaceTemplateType(ComponentTemplateObjectType):
|
||||
|
||||
|
||||
class InventoryItemType(ComponentObjectType):
|
||||
component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemComponentType')
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItem
|
||||
fields = '__all__'
|
||||
exclude = ('component_type', 'component_id')
|
||||
filterset_class = filtersets.InventoryItemFilterSet
|
||||
|
||||
|
||||
@ -284,7 +295,7 @@ class InventoryItemRoleType(OrganizationalObjectType):
|
||||
filterset_class = filtersets.InventoryItemRoleFilterSet
|
||||
|
||||
|
||||
class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectType):
|
||||
class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Location
|
||||
@ -292,7 +303,7 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectT
|
||||
filterset_class = filtersets.LocationFilterSet
|
||||
|
||||
|
||||
class ManufacturerType(OrganizationalObjectType):
|
||||
class ManufacturerType(OrganizationalObjectType, ContactsMixin):
|
||||
|
||||
class Meta:
|
||||
model = models.Manufacturer
|
||||
@ -379,7 +390,7 @@ class PowerOutletTemplateType(ComponentTemplateObjectType):
|
||||
return self.type or None
|
||||
|
||||
|
||||
class PowerPanelType(NetBoxObjectType):
|
||||
class PowerPanelType(NetBoxObjectType, ContactsMixin):
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPanel
|
||||
@ -409,7 +420,7 @@ class PowerPortTemplateType(ComponentTemplateObjectType):
|
||||
return self.type or None
|
||||
|
||||
|
||||
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
|
||||
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Rack
|
||||
@ -458,7 +469,7 @@ class RearPortTemplateType(ComponentTemplateObjectType):
|
||||
filterset_class = filtersets.RearPortTemplateFilterSet
|
||||
|
||||
|
||||
class RegionType(VLANGroupsMixin, OrganizationalObjectType):
|
||||
class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Region
|
||||
@ -466,7 +477,7 @@ class RegionType(VLANGroupsMixin, OrganizationalObjectType):
|
||||
filterset_class = filtersets.RegionFilterSet
|
||||
|
||||
|
||||
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
|
||||
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
|
||||
asn = graphene.Field(BigInt)
|
||||
|
||||
class Meta:
|
||||
@ -475,7 +486,7 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
|
||||
filterset_class = filtersets.SiteFilterSet
|
||||
|
||||
|
||||
class SiteGroupType(VLANGroupsMixin, OrganizationalObjectType):
|
||||
class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
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
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem,
|
||||
InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
|
||||
ConsolePort,
|
||||
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 tenancy.tables import TenancyColumnsMixin
|
||||
|
||||
from .template_code import *
|
||||
|
||||
__all__ = (
|
||||
@ -137,7 +151,7 @@ class PlatformTable(NetBoxTable):
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.TemplateColumn(
|
||||
order_by=('_name',),
|
||||
template_code=DEVICE_LINK
|
||||
@ -201,9 +215,6 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
|
||||
verbose_name='VC Priority'
|
||||
)
|
||||
comments = columns.MarkdownColumn()
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:device_list'
|
||||
)
|
||||
|
@ -1,10 +1,21 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from dcim.models import (
|
||||
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, DeviceType, FrontPortTemplate, InterfaceTemplate,
|
||||
InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
||||
ConsolePortTemplate,
|
||||
ConsoleServerPortTemplate,
|
||||
DeviceBayTemplate,
|
||||
DeviceType,
|
||||
FrontPortTemplate,
|
||||
InterfaceTemplate,
|
||||
InventoryItemTemplate,
|
||||
Manufacturer,
|
||||
ModuleBayTemplate,
|
||||
PowerOutletTemplate,
|
||||
PowerPortTemplate,
|
||||
RearPortTemplate,
|
||||
)
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from tenancy.tables import ContactsColumnMixin
|
||||
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, DEVICE_WEIGHT
|
||||
|
||||
__all__ = (
|
||||
@ -27,7 +38,7 @@ __all__ = (
|
||||
# Manufacturers
|
||||
#
|
||||
|
||||
class ManufacturerTable(NetBoxTable):
|
||||
class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@ -43,9 +54,6 @@ class ManufacturerTable(NetBoxTable):
|
||||
verbose_name='Platforms'
|
||||
)
|
||||
slug = tables.Column()
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:manufacturer_list'
|
||||
)
|
||||
|
@ -1,7 +1,9 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from dcim.models import PowerFeed, PowerPanel
|
||||
from tenancy.tables import ContactsColumnMixin
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
|
||||
from .devices import CableTerminationTable
|
||||
|
||||
__all__ = (
|
||||
@ -14,7 +16,7 @@ __all__ = (
|
||||
# Power panels
|
||||
#
|
||||
|
||||
class PowerPanelTable(NetBoxTable):
|
||||
class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@ -29,9 +31,6 @@ class PowerPanelTable(NetBoxTable):
|
||||
url_params={'power_panel_id': 'pk'},
|
||||
verbose_name='Feeds'
|
||||
)
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:powerpanel_list'
|
||||
)
|
||||
|
@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import Rack, RackReservation, RackRole
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from tenancy.tables import TenancyColumnsMixin
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
from .template_code import DEVICE_WEIGHT
|
||||
|
||||
__all__ = (
|
||||
@ -38,7 +38,7 @@ class RackRoleTable(NetBoxTable):
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
order_by=('_name',),
|
||||
linkify=True
|
||||
@ -69,9 +69,6 @@ class RackTable(TenancyColumnsMixin, NetBoxTable):
|
||||
orderable=False,
|
||||
verbose_name='Power'
|
||||
)
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:rack_list'
|
||||
)
|
||||
|
@ -1,8 +1,9 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from dcim.models import Location, Region, Site, SiteGroup
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from tenancy.tables import TenancyColumnsMixin
|
||||
|
||||
from .template_code import LOCATION_BUTTONS
|
||||
|
||||
__all__ = (
|
||||
@ -17,7 +18,7 @@ __all__ = (
|
||||
# Regions
|
||||
#
|
||||
|
||||
class RegionTable(NetBoxTable):
|
||||
class RegionTable(ContactsColumnMixin, NetBoxTable):
|
||||
name = columns.MPTTColumn(
|
||||
linkify=True
|
||||
)
|
||||
@ -26,9 +27,6 @@ class RegionTable(NetBoxTable):
|
||||
url_params={'region_id': 'pk'},
|
||||
verbose_name='Sites'
|
||||
)
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:region_list'
|
||||
)
|
||||
@ -46,7 +44,7 @@ class RegionTable(NetBoxTable):
|
||||
# Site groups
|
||||
#
|
||||
|
||||
class SiteGroupTable(NetBoxTable):
|
||||
class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
|
||||
name = columns.MPTTColumn(
|
||||
linkify=True
|
||||
)
|
||||
@ -55,9 +53,6 @@ class SiteGroupTable(NetBoxTable):
|
||||
url_params={'group_id': 'pk'},
|
||||
verbose_name='Sites'
|
||||
)
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:sitegroup_list'
|
||||
)
|
||||
@ -75,7 +70,7 @@ class SiteGroupTable(NetBoxTable):
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@ -97,9 +92,6 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable):
|
||||
verbose_name='ASN Count'
|
||||
)
|
||||
comments = columns.MarkdownColumn()
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:site_list'
|
||||
)
|
||||
@ -118,7 +110,7 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable):
|
||||
# Locations
|
||||
#
|
||||
|
||||
class LocationTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
name = columns.MPTTColumn(
|
||||
linkify=True
|
||||
)
|
||||
@ -136,9 +128,6 @@ class LocationTable(TenancyColumnsMixin, NetBoxTable):
|
||||
url_params={'location_id': 'pk'},
|
||||
verbose_name='Devices'
|
||||
)
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:location_list'
|
||||
)
|
||||
|
@ -951,7 +951,8 @@ class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
|
||||
tab = ViewTab(
|
||||
label=_('Console Ports'),
|
||||
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(
|
||||
label=_('Console Server Ports'),
|
||||
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(
|
||||
label=_('Power Ports'),
|
||||
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(
|
||||
label=_('Power Outlets'),
|
||||
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(
|
||||
label=_('Interfaces'),
|
||||
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(
|
||||
label=_('Front Ports'),
|
||||
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(
|
||||
label=_('Rear Ports'),
|
||||
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(
|
||||
label=_('Module Bays'),
|
||||
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(
|
||||
label=_('Device Bays'),
|
||||
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(
|
||||
label=_('Inventory Items'),
|
||||
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(
|
||||
label=_('Console Ports'),
|
||||
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(
|
||||
label=_('Console Server Ports'),
|
||||
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(
|
||||
label=_('Power Ports'),
|
||||
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(
|
||||
label=_('Power Outlets'),
|
||||
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(
|
||||
label=_('Interfaces'),
|
||||
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(
|
||||
label=_('Front Ports'),
|
||||
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(
|
||||
label=_('Rear Ports'),
|
||||
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
|
||||
table = tables.DeviceConsolePortTable
|
||||
filterset = filtersets.ConsolePortFilterSet
|
||||
template_name = 'dcim/device/consoleports.html'
|
||||
template_name = 'dcim/device/consoleports.html',
|
||||
tab = ViewTab(
|
||||
label=_('Console Ports'),
|
||||
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(
|
||||
label=_('Console Server Ports'),
|
||||
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(
|
||||
label=_('Power Ports'),
|
||||
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(
|
||||
label=_('Power Outlets'),
|
||||
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(
|
||||
label=_('Interfaces'),
|
||||
badge=lambda obj: obj.interfaces.count(),
|
||||
permission='dcim.view_interface'
|
||||
permission='dcim.view_interface',
|
||||
hide_if_empty=True
|
||||
)
|
||||
|
||||
def get_children(self, request, parent):
|
||||
@ -1920,7 +1942,8 @@ class DeviceFrontPortsView(DeviceComponentsView):
|
||||
tab = ViewTab(
|
||||
label=_('Front Ports'),
|
||||
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(
|
||||
label=_('Rear Ports'),
|
||||
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(
|
||||
label=_('Module Bays'),
|
||||
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(
|
||||
label=_('Device Bays'),
|
||||
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(
|
||||
label=_('Inventory Items'),
|
||||
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"
|
||||
|
||||
def ready(self):
|
||||
import extras.lookups
|
||||
import extras.signals
|
||||
from . import lookups, search, signals
|
||||
|
@ -59,3 +59,10 @@ class TagsMixin:
|
||||
|
||||
def resolve_tags(self, info):
|
||||
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:
|
||||
model = models.CustomField
|
||||
fields = '__all__'
|
||||
exclude = ('content_types', )
|
||||
filterset_class = filtersets.CustomFieldFilterSet
|
||||
|
||||
|
||||
@ -83,5 +83,5 @@ class WebhookType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Webhook
|
||||
fields = '__all__'
|
||||
exclude = ('content_types', )
|
||||
filterset_class = filtersets.WebhookFilterSet
|
||||
|
@ -5,10 +5,11 @@ from packaging import version
|
||||
from django.apps import AppConfig
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
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 netbox.navigation import MenuGroup
|
||||
from netbox.search import register_search
|
||||
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
|
||||
# integrated components.
|
||||
search_indexes = 'search.indexes'
|
||||
graphql_schema = 'graphql.schema'
|
||||
menu = 'navigation.menu'
|
||||
menu_items = 'navigation.menu_items'
|
||||
@ -69,26 +71,46 @@ class PluginConfig(AppConfig):
|
||||
def ready(self):
|
||||
plugin_name = self.name.rsplit('.', 1)[-1]
|
||||
|
||||
# Register template content (if defined)
|
||||
template_extensions = import_object(f"{self.__module__}.{self.template_extensions}")
|
||||
if template_extensions is not None:
|
||||
register_template_extensions(template_extensions)
|
||||
# Register search extensions (if defined)
|
||||
try:
|
||||
search_indexes = import_string(f"{self.__module__}.{self.search_indexes}")
|
||||
for idx in search_indexes:
|
||||
register_search()(idx)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Register navigation menu or menu items (if defined)
|
||||
if menu := import_object(f"{self.__module__}.{self.menu}"):
|
||||
# Register template content (if defined)
|
||||
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)
|
||||
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)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Register GraphQL schema (if defined)
|
||||
graphql_schema = import_object(f"{self.__module__}.{self.graphql_schema}")
|
||||
if graphql_schema is not None:
|
||||
try:
|
||||
graphql_schema = import_string(f"{self.__module__}.{self.graphql_schema}")
|
||||
register_graphql_schema(graphql_schema)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Register user preferences (if defined)
|
||||
user_preferences = import_object(f"{self.__module__}.{self.user_preferences}")
|
||||
if user_preferences is not None:
|
||||
try:
|
||||
user_preferences = import_string(f"{self.__module__}.{self.user_preferences}")
|
||||
register_user_preferences(plugin_name, user_preferences)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def validate(cls, user_config, netbox_version):
|
||||
|
@ -3,8 +3,7 @@ from django.conf import settings
|
||||
from django.conf.urls import include
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.urls import path
|
||||
|
||||
from extras.plugins.utils import import_object
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from . import views
|
||||
|
||||
@ -25,15 +24,19 @@ for plugin_path in settings.PLUGINS:
|
||||
base_url = getattr(app, 'base_url') or app.label
|
||||
|
||||
# Check if the plugin specifies any base URLs
|
||||
urlpatterns = import_object(f"{plugin_path}.urls.urlpatterns")
|
||||
if urlpatterns is not None:
|
||||
try:
|
||||
urlpatterns = import_string(f"{plugin_path}.urls.urlpatterns")
|
||||
plugin_patterns.append(
|
||||
path(f"{base_url}/", include((urlpatterns, app.label)))
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Check if the plugin specifies any API URLs
|
||||
urlpatterns = import_object(f"{plugin_path}.api.urls.urlpatterns")
|
||||
if urlpatterns is not None:
|
||||
try:
|
||||
urlpatterns = import_string(f"{plugin_path}.api.urls.urlpatterns")
|
||||
plugin_api_patterns.append(
|
||||
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
|
||||
}
|
||||
registry['denormalized_fields'] = collections.defaultdict(list)
|
||||
registry['search'] = 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"
|
||||
|
||||
def ready(self):
|
||||
import ipam.signals
|
||||
from . import signals, search
|
||||
|
@ -88,6 +88,12 @@ class RouteTargetForm(TenancyForm, NetBoxModelForm):
|
||||
class RIRForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('RIR', (
|
||||
'name', 'slug', 'is_private', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
fields = [
|
||||
@ -164,6 +170,12 @@ class ASNForm(TenancyForm, NetBoxModelForm):
|
||||
class RoleForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Role', (
|
||||
'name', 'slug', 'weight', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
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."
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Service Template', (
|
||||
'name', 'protocol', 'ports', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ServiceTemplate
|
||||
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
|
||||
|
||||
from graphene_django import DjangoObjectType
|
||||
from extras.graphql.mixins import ContactsMixin
|
||||
from ipam import filtersets, models
|
||||
from netbox.graphql.scalars import BigInt
|
||||
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
|
||||
@ -54,18 +56,20 @@ class FHRPGroupType(NetBoxObjectType):
|
||||
|
||||
|
||||
class FHRPGroupAssignmentType(BaseObjectType):
|
||||
interface = graphene.Field('ipam.graphql.gfk_mixins.FHRPGroupInterfaceType')
|
||||
|
||||
class Meta:
|
||||
model = models.FHRPGroupAssignment
|
||||
fields = '__all__'
|
||||
exclude = ('interface_type', 'interface_id')
|
||||
filterset_class = filtersets.FHRPGroupAssignmentFilterSet
|
||||
|
||||
|
||||
class IPAddressType(NetBoxObjectType):
|
||||
assigned_object = graphene.Field('ipam.graphql.gfk_mixins.IPAddressAssignmentType')
|
||||
|
||||
class Meta:
|
||||
model = models.IPAddress
|
||||
fields = '__all__'
|
||||
exclude = ('assigned_object_type', 'assigned_object_id')
|
||||
filterset_class = filtersets.IPAddressFilterSet
|
||||
|
||||
def resolve_role(self, info):
|
||||
@ -140,10 +144,11 @@ class VLANType(NetBoxObjectType):
|
||||
|
||||
|
||||
class VLANGroupType(OrganizationalObjectType):
|
||||
scope = graphene.Field('ipam.graphql.gfk_mixins.VLANGroupScopeType')
|
||||
|
||||
class Meta:
|
||||
model = models.VLANGroup
|
||||
fields = '__all__'
|
||||
exclude = ('scope_type', 'scope_id')
|
||||
filterset_class = filtersets.VLANGroupFilterSet
|
||||
|
||||
|
||||
@ -155,7 +160,7 @@ class VRFType(NetBoxObjectType):
|
||||
filterset_class = filtersets.VRFFilterSet
|
||||
|
||||
|
||||
class L2VPNType(NetBoxObjectType):
|
||||
class L2VPNType(ContactsMixin, NetBoxObjectType):
|
||||
class Meta:
|
||||
model = models.L2VPN
|
||||
fields = '__all__'
|
||||
@ -163,7 +168,9 @@ class L2VPNType(NetBoxObjectType):
|
||||
|
||||
|
||||
class L2VPNTerminationType(NetBoxObjectType):
|
||||
assigned_object = graphene.Field('ipam.graphql.gfk_mixins.L2VPNAssignmentType')
|
||||
|
||||
class Meta:
|
||||
model = models.L2VPNTermination
|
||||
fields = '__all__'
|
||||
exclude = ('assigned_object_type', 'assigned_object_id')
|
||||
filtersets_class = filtersets.L2VPNTerminationFilterSet
|
||||
|
@ -92,6 +92,8 @@ class Service(ServiceBase, NetBoxModel):
|
||||
verbose_name='IP addresses'
|
||||
)
|
||||
|
||||
clone_fields = ['protocol', 'ports', 'description', 'device', 'virtual_machine', 'ipaddresses', ]
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
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
|
||||
|
||||
|
||||
|
@ -1,31 +1,15 @@
|
||||
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 .base import *
|
||||
|
||||
|
||||
def build_search_choices():
|
||||
result = list()
|
||||
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)))
|
||||
def build_options(choices):
|
||||
options = [{"label": choices[0][1], "items": []}]
|
||||
|
||||
return tuple(result)
|
||||
|
||||
|
||||
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:]:
|
||||
for label, choices in choices[1:]:
|
||||
items = []
|
||||
|
||||
for value, choice_label in choices:
|
||||
@ -36,10 +20,19 @@ def build_options():
|
||||
|
||||
|
||||
class SearchForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(
|
||||
label='Search'
|
||||
)
|
||||
obj_type = forms.ChoiceField(
|
||||
choices=OBJ_TYPE_CHOICES, required=False, label='Type'
|
||||
)
|
||||
options = build_options()
|
||||
q = forms.CharField(label='Search')
|
||||
options = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["obj_type"] = forms.ChoiceField(
|
||||
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
|
||||
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 dcim.fields import MACAddressField, WWNField
|
||||
from ipam.fields import IPAddressField, IPNetworkField
|
||||
from .fields import ObjectListField
|
||||
|
||||
|
||||
@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):
|
||||
# TODO: Update to use get_django_field_description under django_graphene v3.0
|
||||
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
|
||||
|
||||
# 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
|
||||
@ -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('/')
|
||||
RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
|
||||
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_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
|
||||
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
|
||||
SOCIAL_AUTH_JSONFIELD_ENABLED = True
|
||||
|
||||
SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = 'netbox.users.utils.clean_username'
|
||||
|
||||
#
|
||||
# Django Prometheus
|
||||
@ -648,7 +644,6 @@ RQ_QUEUES = {
|
||||
#
|
||||
|
||||
for plugin_name in PLUGINS:
|
||||
|
||||
# Import plugin module
|
||||
try:
|
||||
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 netbox.constants import SEARCH_MAX_RESULTS
|
||||
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 virtualization.models import Cluster, VirtualMachine
|
||||
from wireless.models import WirelessLAN, WirelessLink
|
||||
@ -153,31 +153,14 @@ class SearchView(View):
|
||||
results = []
|
||||
|
||||
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 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']}")
|
||||
|
||||
for obj_type in SEARCH_TYPES.keys():
|
||||
|
||||
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')}"
|
||||
})
|
||||
results = default_search_engine.search(request, form.cleaned_data['q'])
|
||||
|
||||
return render(request, 'search.html', {
|
||||
'form': form,
|
||||
|
@ -31,8 +31,7 @@
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars-experimental": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"no-unused-vars": "off",
|
||||
"no-inner-declarations": "off",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^5.9.55",
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"@mdi/font": "^7.0.96",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"bootstrap": "~5.0.2",
|
||||
"clipboard": "^2.0.8",
|
||||
"color2k": "^1.2.4",
|
||||
"dayjs": "^1.10.4",
|
||||
"clipboard": "^2.0.11",
|
||||
"color2k": "^2.0.0",
|
||||
"dayjs": "^1.11.5",
|
||||
"flatpickr": "4.6.13",
|
||||
"htmx.org": "^1.6.1",
|
||||
"just-debounce-it": "^1.4.0",
|
||||
"htmx.org": "^1.8.0",
|
||||
"just-debounce-it": "^3.1.1",
|
||||
"masonry-layout": "^4.2.2",
|
||||
"query-string": "^6.14.1",
|
||||
"sass": "^1.32.8",
|
||||
"simplebar": "^5.3.4",
|
||||
"slim-select": "^1.27.0"
|
||||
"query-string": "^7.1.1",
|
||||
"sass": "^1.55.0",
|
||||
"simplebar": "^5.3.9",
|
||||
"slim-select": "^1.27.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bootstrap": "^5.0.12",
|
||||
"@types/cookie": "^0.4.0",
|
||||
"@types/masonry-layout": "^4.2.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.3",
|
||||
"@typescript-eslint/parser": "^4.29.3",
|
||||
"esbuild": "^0.12.24",
|
||||
"esbuild-sass-plugin": "^1.5.2",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-import-resolver-typescript": "^2.4.0",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-prettier": "^3.4.1",
|
||||
"prettier": "^2.3.2",
|
||||
"typescript": "~4.3.5"
|
||||
"@types/bootstrap": "^5.0.17",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/masonry-layout": "^4.2.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.39.0",
|
||||
"@typescript-eslint/parser": "^5.39.0",
|
||||
"esbuild": "^0.13.15",
|
||||
"esbuild-sass-plugin": "^2.3.3",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "~4.8.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"eslint-import-resolver-typescript/**/path-parse": "^1.0.7",
|
||||
"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"
|
||||
"@types/bootstrap/**/@popperjs/core": "^2.11.6"
|
||||
}
|
||||
}
|
||||
}
|
@ -37,14 +37,12 @@ function initDocument(): void {
|
||||
}
|
||||
|
||||
function initWindow(): void {
|
||||
|
||||
const documentForms = document.forms
|
||||
for (var documentForm of documentForms) {
|
||||
const documentForms = document.forms;
|
||||
for (const documentForm of documentForms) {
|
||||
if (documentForm.method.toUpperCase() == 'GET') {
|
||||
// @ts-ignore: Our version of typescript seems to be too old for FormDataEvent
|
||||
documentForm.addEventListener('formdata', function(event: FormDataEvent) {
|
||||
let formData: FormData = event.formData;
|
||||
for (let [name, value] of Array.from(formData.entries())) {
|
||||
documentForm.addEventListener('formdata', function (event: FormDataEvent) {
|
||||
const formData: FormData = event.formData;
|
||||
for (const [name, value] of Array.from(formData.entries())) {
|
||||
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 {
|
||||
color: $form-select-color;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -60,23 +60,17 @@
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<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/image_attachments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</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="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
|
@ -77,10 +77,10 @@
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<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>
|
||||
<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>
|
||||
</ul>
|
||||
</span>
|
||||
|
@ -105,16 +105,16 @@
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</ul>
|
||||
</span>
|
||||
|
@ -3,3 +3,6 @@ from django.apps import AppConfig
|
||||
|
||||
class TenancyConfig(AppConfig):
|
||||
name = 'tenancy'
|
||||
|
||||
def ready(self):
|
||||
from . import search
|
||||
|
@ -27,6 +27,12 @@ class TenantGroupForm(NetBoxModelForm):
|
||||
)
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Tenant Group', (
|
||||
'parent', 'name', 'slug', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = TenantGroup
|
||||
fields = [
|
||||
@ -64,6 +70,12 @@ class ContactGroupForm(NetBoxModelForm):
|
||||
)
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Contact Group', (
|
||||
'parent', 'name', 'slug', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ContactGroup
|
||||
fields = ('parent', 'name', 'slug', 'description', 'tags')
|
||||
@ -72,6 +84,12 @@ class ContactGroupForm(NetBoxModelForm):
|
||||
class ContactRoleForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Contact Role', (
|
||||
'name', 'slug', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ContactRole
|
||||
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
|
||||
|
||||
from netbox.tables import columns
|
||||
|
||||
__all__ = (
|
||||
'ContactsColumnMixin',
|
||||
'TenantColumn',
|
||||
'TenantGroupColumn',
|
||||
'TenancyColumnsMixin',
|
||||
@ -55,3 +58,10 @@ class TenantGroupColumn(tables.TemplateColumn):
|
||||
class TenancyColumnsMixin(tables.Table):
|
||||
tenant_group = TenantGroupColumn()
|
||||
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
|
||||
from tenancy.models import *
|
||||
from tenancy.tables import ContactsColumnMixin
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from tenancy.models import *
|
||||
|
||||
__all__ = (
|
||||
'TenantGroupTable',
|
||||
@ -30,7 +31,7 @@ class TenantGroupTable(NetBoxTable):
|
||||
default_columns = ('pk', 'name', 'tenant_count', 'description')
|
||||
|
||||
|
||||
class TenantTable(NetBoxTable):
|
||||
class TenantTable(ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@ -38,9 +39,6 @@ class TenantTable(NetBoxTable):
|
||||
linkify=True
|
||||
)
|
||||
comments = columns.MarkdownColumn()
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
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 netbox.forms import SearchForm
|
||||
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
from netbox.forms import SearchForm
|
||||
|
||||
register = template.Library()
|
||||
search_form = SearchForm()
|
||||
|
||||
|
||||
@register.inclusion_tag("search/searchbar.html")
|
||||
def search_options(request) -> Dict:
|
||||
"""Provide search options to template."""
|
||||
|
||||
# Provide search options to template.
|
||||
return {
|
||||
'options': search_form.options,
|
||||
'options': search_form.get_options(),
|
||||
'request': request,
|
||||
}
|
||||
|
@ -450,6 +450,9 @@ class APIViewTestCases:
|
||||
if type(field) is GQLDynamic:
|
||||
# Dynamic fields must specify a subselection
|
||||
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):
|
||||
# Union types dont' have an id or consistent values
|
||||
continue
|
||||
|
@ -140,19 +140,22 @@ class ViewTab:
|
||||
|
||||
Args:
|
||||
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
|
||||
argument representing the object being viewed.
|
||||
badge: A static value or callable to display alongside the label (optional). If a callable is used, it must
|
||||
accept a single argument representing the object being viewed.
|
||||
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.badge = badge
|
||||
self.permission = permission
|
||||
self.hide_if_empty = hide_if_empty
|
||||
|
||||
def render(self, instance):
|
||||
"""Return the attributes needed to render a tab in HTML."""
|
||||
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 {
|
||||
'label': self.label,
|
||||
|
@ -3,3 +3,6 @@ from django.apps import AppConfig
|
||||
|
||||
class VirtualizationConfig(AppConfig):
|
||||
name = 'virtualization'
|
||||
|
||||
def ready(self):
|
||||
from . import search
|
||||
|
@ -28,6 +28,12 @@ __all__ = (
|
||||
class ClusterTypeForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Cluster Type', (
|
||||
'name', 'slug', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ClusterType
|
||||
fields = (
|
||||
@ -38,6 +44,12 @@ class ClusterTypeForm(NetBoxModelForm):
|
||||
class ClusterGroupForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Cluster Group', (
|
||||
'name', 'slug', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ClusterGroup
|
||||
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
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from tenancy.tables import TenancyColumnsMixin
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
||||
__all__ = (
|
||||
'ClusterTable',
|
||||
@ -32,7 +32,7 @@ class ClusterTypeTable(NetBoxTable):
|
||||
default_columns = ('pk', 'name', 'cluster_count', 'description')
|
||||
|
||||
|
||||
class ClusterGroupTable(NetBoxTable):
|
||||
class ClusterGroupTable(ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@ -41,9 +41,6 @@ class ClusterGroupTable(NetBoxTable):
|
||||
url_params={'group_id': 'pk'},
|
||||
verbose_name='Clusters'
|
||||
)
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='virtualization:clustergroup_list'
|
||||
)
|
||||
@ -57,7 +54,7 @@ class ClusterGroupTable(NetBoxTable):
|
||||
default_columns = ('pk', 'name', 'cluster_count', 'description')
|
||||
|
||||
|
||||
class ClusterTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class ClusterTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@ -81,9 +78,6 @@ class ClusterTable(TenancyColumnsMixin, NetBoxTable):
|
||||
verbose_name='VMs'
|
||||
)
|
||||
comments = columns.MarkdownColumn()
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='virtualization:cluster_list'
|
||||
)
|
||||
|
@ -1,10 +1,10 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from dcim.tables.devices import BaseInterfaceTable
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from tenancy.tables import TenancyColumnsMixin
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
|
||||
__all__ = (
|
||||
'VirtualMachineTable',
|
||||
'VirtualMachineVMInterfaceTable',
|
||||
@ -37,7 +37,7 @@ VMINTERFACE_BUTTONS = """
|
||||
# Virtual machines
|
||||
#
|
||||
|
||||
class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
order_by=('_name',),
|
||||
linkify=True
|
||||
@ -67,9 +67,6 @@ class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable):
|
||||
order_by=('primary_ip4', 'primary_ip6'),
|
||||
verbose_name='IP Address'
|
||||
)
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='virtualization:virtualmachine_list'
|
||||
)
|
||||
|
@ -5,4 +5,4 @@ class WirelessConfig(AppConfig):
|
||||
name = 'wireless'
|
||||
|
||||
def ready(self):
|
||||
import wireless.signals
|
||||
from . import signals, search
|
||||
|
@ -19,6 +19,12 @@ class WirelessLANGroupForm(NetBoxModelForm):
|
||||
)
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Wireless LAN Group', (
|
||||
'parent', 'name', 'slug', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = WirelessLANGroup
|
||||
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
|
||||
sentry-sdk==1.9.10
|
||||
social-auth-app-django==5.0.0
|
||||
social-auth-core==4.3.0
|
||||
social-auth-core[openidconnect]==4.3.0
|
||||
svgwrite==1.4.3
|
||||
tablib==3.2.1
|
||||
tzdata==2022.4
|
||||
|
||||
# Workaround for #7401
|
||||
jsonschema==3.2.0
|
||||
|
||||
# Temporary fix for #10712
|
||||
swagger-spec-validator==2.7.6
|
||||
|
Loading…
Reference in New Issue
Block a user