mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
commit
6dcb2de28f
@ -143,17 +143,28 @@ AUTH_LDAP_CACHE_TIMEOUT = 3600
|
|||||||
|
|
||||||
`systemctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`.
|
`systemctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`.
|
||||||
|
|
||||||
For troubleshooting LDAP user/group queries, add the following lines to the start of `ldap_config.py` after `import ldap`.
|
For troubleshooting LDAP user/group queries, add or merge the following [logging](/configuration/optional-settings.md#logging) configuration to `configuration.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import logging, logging.handlers
|
LOGGING = {
|
||||||
logfile = "/opt/netbox/logs/django-ldap-debug.log"
|
'version': 1,
|
||||||
my_logger = logging.getLogger('django_auth_ldap')
|
'disable_existing_loggers': False,
|
||||||
my_logger.setLevel(logging.DEBUG)
|
'handlers': {
|
||||||
handler = logging.handlers.RotatingFileHandler(
|
'netbox_auth_log': {
|
||||||
logfile, maxBytes=1024 * 500, backupCount=5
|
'level': 'DEBUG',
|
||||||
)
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
my_logger.addHandler(handler)
|
'filename': '/opt/netbox/logs/django-ldap-debug.log',
|
||||||
|
'maxBytes': 1024 * 500,
|
||||||
|
'backupCount': 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'django_auth_ldap': {
|
||||||
|
'handlers': ['netbox_auth_log'],
|
||||||
|
'level': 'DEBUG',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Ensure the file and path specified in logfile exist and are writable and executable by the application service account. Restart the netbox service and attempt to log into the site to trigger log entries to this file.
|
Ensure the file and path specified in logfile exist and are writable and executable by the application service account. Restart the netbox service and attempt to log into the site to trigger log entries to this file.
|
||||||
|
@ -38,14 +38,14 @@ You can use the command `systemctl status netbox` to verify that the WSGI servic
|
|||||||
# systemctl status netbox.service
|
# systemctl status netbox.service
|
||||||
● netbox.service - NetBox WSGI Service
|
● netbox.service - NetBox WSGI Service
|
||||||
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
|
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
|
||||||
Active: active (running) since Thu 2019-12-12 19:23:40 UTC; 25s ago
|
Active: active (running) since Sat 2020-10-24 19:23:40 UTC; 25s ago
|
||||||
Docs: https://netbox.readthedocs.io/en/stable/
|
Docs: https://netbox.readthedocs.io/en/stable/
|
||||||
Main PID: 11993 (gunicorn)
|
Main PID: 11993 (gunicorn)
|
||||||
Tasks: 6 (limit: 2362)
|
Tasks: 6 (limit: 2362)
|
||||||
CGroup: /system.slice/netbox.service
|
CGroup: /system.slice/netbox.service
|
||||||
├─11993 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
|
├─11993 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
|
||||||
├─12015 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
|
├─12015 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
|
||||||
├─12016 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
|
├─12016 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -28,44 +28,44 @@ Download and extract the latest version:
|
|||||||
# wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz
|
# wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz
|
||||||
# tar -xzf vX.Y.Z.tar.gz -C /opt
|
# tar -xzf vX.Y.Z.tar.gz -C /opt
|
||||||
# cd /opt/
|
# cd /opt/
|
||||||
# ln -sfn netbox-X.Y.Z/ netbox
|
# ln -sfn /opt/netbox-X.Y.Z/ /opt/netbox
|
||||||
```
|
```
|
||||||
|
|
||||||
Copy the 'configuration.py' you created when first installing to the new version:
|
Copy the 'configuration.py' you created when first installing to the new version:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cp netbox-X.Y.Z/netbox/netbox/configuration.py netbox/netbox/netbox/configuration.py
|
# cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/configuration.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Copy your local requirements file if used:
|
Copy your local requirements file if used:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cp netbox-X.Y.Z/local_requirements.txt netbox/local_requirements.txt
|
# cp /opt/netbox-X.Y.Z/local_requirements.txt /opt/netbox/local_requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Also copy the LDAP configuration if using LDAP:
|
Also copy the LDAP configuration if using LDAP:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cp netbox-X.Y.Z/netbox/netbox/ldap_config.py netbox/netbox/netbox/ldap_config.py
|
# cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ldap_config.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.)
|
Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.)
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cp -pr netbox-X.Y.Z/netbox/media/ netbox/netbox/
|
# cp -pr /opt/netbox-X.Y.Z/netbox/media/ /opt/netbox/netbox/
|
||||||
```
|
```
|
||||||
|
|
||||||
Also make sure to copy over any custom scripts and reports that you've made. Note that if these are stored outside the project root, you will not need to copy them. (Check the `SCRIPTS_ROOT` and `REPORTS_ROOT` parameters in the configuration file above if you're unsure.)
|
Also make sure to copy over any custom scripts and reports that you've made. Note that if these are stored outside the project root, you will not need to copy them. (Check the `SCRIPTS_ROOT` and `REPORTS_ROOT` parameters in the configuration file above if you're unsure.)
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cp -r /opt/netbox-X.Y.Z/netbox/scripts /opt/netbox/netbox/scripts/
|
# cp -r /opt/netbox-X.Y.Z/netbox/scripts /opt/netbox/netbox/
|
||||||
# cp -r /opt/netbox-X.Y.Z/netbox/reports /opt/netbox/netbox/reports/
|
# cp -r /opt/netbox-X.Y.Z/netbox/reports /opt/netbox/netbox/
|
||||||
```
|
```
|
||||||
|
|
||||||
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
|
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cp netbox-X.Y.Z/gunicorn.py netbox/gunicorn.py
|
# cp /opt/netbox-X.Y.Z/gunicorn.py /opt/netbox/gunicorn.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option B: Clone the Git Repository
|
### Option B: Clone the Git Repository
|
||||||
|
@ -201,26 +201,37 @@ class RandomAnimalView(View):
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
This view retrieves a random animal from the database and and passes it as a context variable when rendering a template named `animal.html`, which doesn't exist yet. To create this template, first create a directory named `templates/netbox_animal_sounds/` within the plugin source directory. (We use the plugin's name as a subdirectory to guard against naming collisions with other plugins.) Then, create `animal.html`:
|
This view retrieves a random animal from the database and and passes it as a context variable when rendering a template named `animal.html`, which doesn't exist yet. To create this template, first create a directory named `templates/netbox_animal_sounds/` within the plugin source directory. (We use the plugin's name as a subdirectory to guard against naming collisions with other plugins.) Then, create a template named `animal.html` as described below.
|
||||||
|
|
||||||
|
### Extending the Base Template
|
||||||
|
|
||||||
|
NetBox provides a base template to ensure a consistent user experience, which plugins can extend with their own content. This template includes four content blocks:
|
||||||
|
|
||||||
|
* `title` - The page title
|
||||||
|
* `header` - The upper portion of the page
|
||||||
|
* `content` - The main page body
|
||||||
|
* `javascript` - A section at the end of the page for including Javascript code
|
||||||
|
|
||||||
|
For more information on how template blocks work, consult the [Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#block).
|
||||||
|
|
||||||
```jinja2
|
```jinja2
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% with config=settings.PLUGINS_CONFIG.netbox_animal_sounds %}
|
{% with config=settings.PLUGINS_CONFIG.netbox_animal_sounds %}
|
||||||
<h2 class="text-center" style="margin-top: 200px">
|
<h2 class="text-center" style="margin-top: 200px">
|
||||||
{% if animal %}
|
{% if animal %}
|
||||||
The {{ animal.name|lower }} says
|
The {{ animal.name|lower }} says
|
||||||
{% if config.loud %}
|
{% if config.loud %}
|
||||||
{{ animal.sound|upper }}!
|
{{ animal.sound|upper }}!
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ animal.sound }}
|
{{ animal.sound }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
No animals have been created yet!
|
No animals have been created yet!
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -64,6 +64,15 @@ PLUGINS_CONFIG = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Run Database Migrations
|
||||||
|
|
||||||
|
If the plugin introduces new database models, run the provided schema migrations:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
(venv) $ cd /opt/netbox/netbox/
|
||||||
|
(venv) $ python3 manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
### Collect Static Files
|
### Collect Static Files
|
||||||
|
|
||||||
Plugins may package static files to be served directly by the HTTP front end. Ensure that these are copied to the static root directory with the `collectstatic` management command:
|
Plugins may package static files to be served directly by the HTTP front end. Ensure that these are copied to the static root directory with the `collectstatic` management command:
|
||||||
|
@ -1,5 +1,22 @@
|
|||||||
# NetBox v2.9
|
# NetBox v2.9
|
||||||
|
|
||||||
|
## v2.9.8 (2020-10-30)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#4559](https://github.com/netbox-community/netbox/issues/4559) - Improve device/VM context data rendering performance
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#3672](https://github.com/netbox-community/netbox/issues/3672) - Fix a caching issue causing incorrect related object counts in API responses
|
||||||
|
* [#5113](https://github.com/netbox-community/netbox/issues/5113) - Fix incorrect caching of permission object assignments to user groups in the admin panel
|
||||||
|
* [#5243](https://github.com/netbox-community/netbox/issues/5243) - Redirect user to appropriate tab after modifying device components
|
||||||
|
* [#5273](https://github.com/netbox-community/netbox/issues/5273) - Fix exception when validating a new permission with no models selected
|
||||||
|
* [#5282](https://github.com/netbox-community/netbox/issues/5282) - Fix high CPU load when LDAP authentication is enabled
|
||||||
|
* [#5285](https://github.com/netbox-community/netbox/issues/5285) - Plugins no longer need to define `app_name` for API URLs to be included in the root view
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.9.7 (2020-10-12)
|
## v2.9.7 (2020-10-12)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
@ -24,7 +24,7 @@ from dcim.models import (
|
|||||||
VirtualChassis,
|
VirtualChassis,
|
||||||
)
|
)
|
||||||
from extras.api.serializers import RenderedGraphSerializer
|
from extras.api.serializers import RenderedGraphSerializer
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
|
||||||
from extras.models import Graph
|
from extras.models import Graph
|
||||||
from ipam.models import Prefix, VLAN
|
from ipam.models import Prefix, VLAN
|
||||||
from utilities.api import (
|
from utilities.api import (
|
||||||
@ -336,7 +336,7 @@ class PlatformViewSet(ModelViewSet):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceViewSet(CustomFieldModelViewSet):
|
class DeviceViewSet(CustomFieldModelViewSet, ConfigContextQuerySetMixin):
|
||||||
queryset = Device.objects.prefetch_related(
|
queryset = Device.objects.prefetch_related(
|
||||||
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
|
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
|
||||||
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
|
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
|
||||||
|
@ -15,6 +15,7 @@ from taggit.managers import TaggableManager
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem
|
from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem
|
||||||
|
from extras.querysets import ConfigContextModelQuerySet
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
@ -594,7 +595,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
)
|
)
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = ConfigContextModelQuerySet.as_manager()
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||||
|
@ -432,7 +432,8 @@ class ComponentTemplateTable(BaseTable):
|
|||||||
class ConsolePortTemplateTable(ComponentTemplateTable):
|
class ConsolePortTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=ConsolePortTemplate,
|
model=ConsolePortTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_consoleports'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@ -444,7 +445,8 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
|
|||||||
class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=ConsoleServerPortTemplate,
|
model=ConsoleServerPortTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_consoleserverports'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@ -456,7 +458,8 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
|||||||
class PowerPortTemplateTable(ComponentTemplateTable):
|
class PowerPortTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=PowerPortTemplate,
|
model=PowerPortTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_powerports'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@ -468,7 +471,8 @@ class PowerPortTemplateTable(ComponentTemplateTable):
|
|||||||
class PowerOutletTemplateTable(ComponentTemplateTable):
|
class PowerOutletTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=PowerOutletTemplate,
|
model=PowerOutletTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_poweroutlets'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@ -483,7 +487,8 @@ class InterfaceTemplateTable(ComponentTemplateTable):
|
|||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=InterfaceTemplate,
|
model=InterfaceTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_interfaces'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@ -498,7 +503,8 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
|||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=FrontPortTemplate,
|
model=FrontPortTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_frontports'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@ -510,7 +516,8 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
|||||||
class RearPortTemplateTable(ComponentTemplateTable):
|
class RearPortTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=RearPortTemplate,
|
model=RearPortTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_rearports'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@ -522,7 +529,8 @@ class RearPortTemplateTable(ComponentTemplateTable):
|
|||||||
class DeviceBayTemplateTable(ComponentTemplateTable):
|
class DeviceBayTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=DeviceBayTemplate,
|
model=DeviceBayTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_devicebays'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
|
@ -1163,7 +1163,7 @@ class DeviceConfigView(ObjectView):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceConfigContextView(ObjectConfigContextView):
|
class DeviceConfigContextView(ObjectConfigContextView):
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.annotate_config_context_data()
|
||||||
base_template = 'dcim/device.html'
|
base_template = 'dcim/device.html'
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,6 +26,29 @@ from utilities.utils import copy_safe_request
|
|||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigContextQuerySetMixin:
|
||||||
|
"""
|
||||||
|
Used by views that work with config context models (device and virtual machine).
|
||||||
|
Provides a get_queryset() method which deals with adding the config context
|
||||||
|
data annotation or not.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""
|
||||||
|
Build the proper queryset based on the request context
|
||||||
|
|
||||||
|
If the `brief` query param equates to True or the `exclude` query param
|
||||||
|
includes `config_context` as a value, return the base queryset.
|
||||||
|
|
||||||
|
Else, return the queryset annotated with config context data
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.get_serializer_context()['request']
|
||||||
|
if request.query_params.get('brief') or 'config_context' in request.query_params.get('exclude', []):
|
||||||
|
return self.queryset
|
||||||
|
return self.queryset.annotate_config_context_data()
|
||||||
|
|
||||||
|
|
||||||
class ExtrasRootView(APIRootView):
|
class ExtrasRootView(APIRootView):
|
||||||
"""
|
"""
|
||||||
Extras API root view
|
Extras API root view
|
||||||
|
@ -542,8 +542,16 @@ class ConfigContextModel(models.Model):
|
|||||||
|
|
||||||
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
|
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
|
||||||
data = OrderedDict()
|
data = OrderedDict()
|
||||||
for context in ConfigContext.objects.get_for_object(self):
|
|
||||||
data = deepmerge(data, context.data)
|
if not hasattr(self, 'config_context_data'):
|
||||||
|
# The annotation is not available, so we fall back to manually querying for the config context objects
|
||||||
|
config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True)
|
||||||
|
else:
|
||||||
|
# The attribute may exist, but the annotated value could be None if there is no config context data
|
||||||
|
config_context_data = self.config_context_data or []
|
||||||
|
|
||||||
|
for context in config_context_data:
|
||||||
|
data = deepmerge(data, context)
|
||||||
|
|
||||||
# If the object has local config context data defined, merge it last
|
# If the object has local config context data defined, merge it last
|
||||||
if self.local_context_data:
|
if self.local_context_data:
|
||||||
|
@ -10,8 +10,6 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from extras.plugins.utils import import_object
|
|
||||||
|
|
||||||
|
|
||||||
class InstalledPluginsAdminView(View):
|
class InstalledPluginsAdminView(View):
|
||||||
"""
|
"""
|
||||||
@ -62,11 +60,7 @@ class PluginsAPIRootView(APIView):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_plugin_entry(plugin, app_config, request, format):
|
def _get_plugin_entry(plugin, app_config, request, format):
|
||||||
# Check if the plugin specifies any API URLs
|
# Check if the plugin specifies any API URLs
|
||||||
api_app_name = import_object(f"{plugin}.api.urls.app_name")
|
api_app_name = f'{app_config.name}-api'
|
||||||
if api_app_name is None:
|
|
||||||
# Plugin does not expose an API
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
entry = (getattr(app_config, 'base_url', app_config.label), reverse(
|
entry = (getattr(app_config, 'base_url', app_config.label), reverse(
|
||||||
f"plugins-api:{api_app_name}:api-root",
|
f"plugins-api:{api_app_name}:api-root",
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.db.models import Q, QuerySet
|
from django.db.models import OuterRef, Subquery, Q
|
||||||
|
|
||||||
|
from utilities.query_functions import EmptyGroupByJSONBAgg, OrderableJSONBAgg
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
|
|
||||||
|
|
||||||
@ -23,9 +24,12 @@ class CustomFieldQueryset:
|
|||||||
|
|
||||||
class ConfigContextQuerySet(RestrictedQuerySet):
|
class ConfigContextQuerySet(RestrictedQuerySet):
|
||||||
|
|
||||||
def get_for_object(self, obj):
|
def get_for_object(self, obj, aggregate_data=False):
|
||||||
"""
|
"""
|
||||||
Return all applicable ConfigContexts for a given object. Only active ConfigContexts will be included.
|
Return all applicable ConfigContexts for a given object. Only active ConfigContexts will be included.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
aggregate_data: If True, use the JSONBAgg aggregate function to return only the list of JSON data objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# `device_role` for Device; `role` for VirtualMachine
|
# `device_role` for Device; `role` for VirtualMachine
|
||||||
@ -45,7 +49,7 @@ class ConfigContextQuerySet(RestrictedQuerySet):
|
|||||||
else:
|
else:
|
||||||
regions = []
|
regions = []
|
||||||
|
|
||||||
return self.filter(
|
queryset = self.filter(
|
||||||
Q(regions__in=regions) | Q(regions=None),
|
Q(regions__in=regions) | Q(regions=None),
|
||||||
Q(sites=obj.site) | Q(sites=None),
|
Q(sites=obj.site) | Q(sites=None),
|
||||||
Q(roles=role) | Q(roles=None),
|
Q(roles=role) | Q(roles=None),
|
||||||
@ -57,3 +61,72 @@ class ConfigContextQuerySet(RestrictedQuerySet):
|
|||||||
Q(tags__slug__in=obj.tags.slugs()) | Q(tags=None),
|
Q(tags__slug__in=obj.tags.slugs()) | Q(tags=None),
|
||||||
is_active=True,
|
is_active=True,
|
||||||
).order_by('weight', 'name')
|
).order_by('weight', 'name')
|
||||||
|
|
||||||
|
if aggregate_data:
|
||||||
|
return queryset.aggregate(
|
||||||
|
config_context_data=OrderableJSONBAgg('data', ordering=['weight', 'name'])
|
||||||
|
)['config_context_data']
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigContextModelQuerySet(RestrictedQuerySet):
|
||||||
|
"""
|
||||||
|
QuerySet manager used by models which support ConfigContext (device and virtual machine).
|
||||||
|
|
||||||
|
Includes a method which appends an annotation of aggregated config context JSON data objects. This is
|
||||||
|
implemented as a subquery which performs all the joins necessary to filter relevant config context objects.
|
||||||
|
This offers a substantial performance gain over ConfigContextQuerySet.get_for_object() when dealing with
|
||||||
|
multiple objects.
|
||||||
|
|
||||||
|
This allows the annotation to be entirely optional.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def annotate_config_context_data(self):
|
||||||
|
"""
|
||||||
|
Attach the subquery annotation to the base queryset
|
||||||
|
"""
|
||||||
|
from extras.models import ConfigContext
|
||||||
|
return self.annotate(
|
||||||
|
config_context_data=Subquery(
|
||||||
|
ConfigContext.objects.filter(
|
||||||
|
self._get_config_context_filters()
|
||||||
|
).annotate(
|
||||||
|
_data=EmptyGroupByJSONBAgg('data', ordering=['weight', 'name'])
|
||||||
|
).values("_data")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_config_context_filters(self):
|
||||||
|
# Construct the set of Q objects for the specific object types
|
||||||
|
base_query = Q(
|
||||||
|
Q(platforms=OuterRef('platform')) | Q(platforms=None),
|
||||||
|
Q(tenant_groups=OuterRef('tenant__group')) | Q(tenant_groups=None),
|
||||||
|
Q(tenants=OuterRef('tenant')) | Q(tenants=None),
|
||||||
|
Q(tags=OuterRef('tags')) | Q(tags=None),
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.model._meta.model_name == 'device':
|
||||||
|
base_query.add((Q(roles=OuterRef('device_role')) | Q(roles=None)), Q.AND)
|
||||||
|
base_query.add((Q(sites=OuterRef('site')) | Q(sites=None)), Q.AND)
|
||||||
|
region_field = 'site__region'
|
||||||
|
|
||||||
|
elif self.model._meta.model_name == 'virtualmachine':
|
||||||
|
base_query.add((Q(roles=OuterRef('role')) | Q(roles=None)), Q.AND)
|
||||||
|
base_query.add((Q(cluster_groups=OuterRef('cluster__group')) | Q(cluster_groups=None)), Q.AND)
|
||||||
|
base_query.add((Q(clusters=OuterRef('cluster')) | Q(clusters=None)), Q.AND)
|
||||||
|
base_query.add((Q(sites=OuterRef('cluster__site')) | Q(sites=None)), Q.AND)
|
||||||
|
region_field = 'cluster__site__region'
|
||||||
|
|
||||||
|
base_query.add(
|
||||||
|
(Q(
|
||||||
|
regions__tree_id=OuterRef(f'{region_field}__tree_id'),
|
||||||
|
regions__level__lte=OuterRef(f'{region_field}__level'),
|
||||||
|
regions__lft__lte=OuterRef(f'{region_field}__lft'),
|
||||||
|
regions__rght__gte=OuterRef(f'{region_field}__rght'),
|
||||||
|
) | Q(regions=None)),
|
||||||
|
Q.AND
|
||||||
|
)
|
||||||
|
|
||||||
|
return base_query
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Site, Region
|
||||||
from extras.choices import TemplateLanguageChoices
|
from extras.choices import TemplateLanguageChoices
|
||||||
from extras.models import Graph, Tag
|
from extras.models import ConfigContext, Graph, Tag
|
||||||
|
from tenancy.models import Tenant, TenantGroup
|
||||||
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||||
|
|
||||||
|
|
||||||
class GraphTest(TestCase):
|
class GraphTest(TestCase):
|
||||||
@ -53,3 +55,276 @@ class TagTest(TestCase):
|
|||||||
tag.save()
|
tag.save()
|
||||||
|
|
||||||
self.assertEqual(tag.slug, 'testing-unicode-台灣')
|
self.assertEqual(tag.slug, 'testing-unicode-台灣')
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigContextTest(TestCase):
|
||||||
|
"""
|
||||||
|
These test cases deal with the weighting, ordering, and deep merge logic of config context data.
|
||||||
|
|
||||||
|
It also ensures the various config context querysets are consistent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
|
||||||
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
|
self.devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
||||||
|
self.devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||||
|
self.region = Region.objects.create(name="Region")
|
||||||
|
self.site = Site.objects.create(name='Site-1', slug='site-1', region=self.region)
|
||||||
|
self.platform = Platform.objects.create(name="Platform")
|
||||||
|
self.tenantgroup = TenantGroup.objects.create(name="Tenant Group")
|
||||||
|
self.tenant = Tenant.objects.create(name="Tenant", group=self.tenantgroup)
|
||||||
|
self.tag = Tag.objects.create(name="Tag", slug="tag")
|
||||||
|
|
||||||
|
self.device = Device.objects.create(
|
||||||
|
name='Device 1',
|
||||||
|
device_type=self.devicetype,
|
||||||
|
device_role=self.devicerole,
|
||||||
|
site=self.site
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_higher_weight_wins(self):
|
||||||
|
|
||||||
|
context1 = ConfigContext(
|
||||||
|
name="context 1",
|
||||||
|
weight=101,
|
||||||
|
data={
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 777
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context2 = ConfigContext(
|
||||||
|
name="context 2",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 789
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ConfigContext.objects.bulk_create([context1, context2])
|
||||||
|
|
||||||
|
expected_data = {
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 777
|
||||||
|
}
|
||||||
|
self.assertEqual(self.device.get_config_context(), expected_data)
|
||||||
|
|
||||||
|
def test_name_ordering_after_weight(self):
|
||||||
|
|
||||||
|
context1 = ConfigContext(
|
||||||
|
name="context 1",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 777
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context2 = ConfigContext(
|
||||||
|
name="context 2",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 789
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ConfigContext.objects.bulk_create([context1, context2])
|
||||||
|
|
||||||
|
expected_data = {
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 789
|
||||||
|
}
|
||||||
|
self.assertEqual(self.device.get_config_context(), expected_data)
|
||||||
|
|
||||||
|
def test_annotation_same_as_get_for_object(self):
|
||||||
|
"""
|
||||||
|
This test incorperates features from all of the above tests cases to ensure
|
||||||
|
the annotate_config_context_data() and get_for_object() queryset methods are the same.
|
||||||
|
"""
|
||||||
|
context1 = ConfigContext(
|
||||||
|
name="context 1",
|
||||||
|
weight=101,
|
||||||
|
data={
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 777
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context2 = ConfigContext(
|
||||||
|
name="context 2",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 789
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context3 = ConfigContext(
|
||||||
|
name="context 3",
|
||||||
|
weight=99,
|
||||||
|
data={
|
||||||
|
"d": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context4 = ConfigContext(
|
||||||
|
name="context 4",
|
||||||
|
weight=99,
|
||||||
|
data={
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ConfigContext.objects.bulk_create([context1, context2, context3, context4])
|
||||||
|
|
||||||
|
annotated_queryset = Device.objects.filter(name=self.device.name).annotate_config_context_data()
|
||||||
|
self.assertEqual(self.device.get_config_context(), annotated_queryset[0].get_config_context())
|
||||||
|
|
||||||
|
def test_annotation_same_as_get_for_object_device_relations(self):
|
||||||
|
|
||||||
|
site_context = ConfigContext.objects.create(
|
||||||
|
name="site",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"site": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
site_context.sites.add(self.site)
|
||||||
|
region_context = ConfigContext.objects.create(
|
||||||
|
name="region",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"region": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
region_context.regions.add(self.region)
|
||||||
|
platform_context = ConfigContext.objects.create(
|
||||||
|
name="platform",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"platform": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
platform_context.platforms.add(self.platform)
|
||||||
|
tenant_group_context = ConfigContext.objects.create(
|
||||||
|
name="tenant group",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"tenant_group": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tenant_group_context.tenant_groups.add(self.tenantgroup)
|
||||||
|
tenant_context = ConfigContext.objects.create(
|
||||||
|
name="tenant",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"tenant": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tenant_context.tenants.add(self.tenant)
|
||||||
|
tag_context = ConfigContext.objects.create(
|
||||||
|
name="tag",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"tag": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tag_context.tags.add(self.tag)
|
||||||
|
|
||||||
|
device = Device.objects.create(
|
||||||
|
name="Device 2",
|
||||||
|
site=self.site,
|
||||||
|
tenant=self.tenant,
|
||||||
|
platform=self.platform,
|
||||||
|
device_role=self.devicerole,
|
||||||
|
device_type=self.devicetype
|
||||||
|
)
|
||||||
|
device.tags.add(self.tag)
|
||||||
|
|
||||||
|
annotated_queryset = Device.objects.filter(name=device.name).annotate_config_context_data()
|
||||||
|
self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context())
|
||||||
|
|
||||||
|
def test_annotation_same_as_get_for_object_virtualmachine_relations(self):
|
||||||
|
|
||||||
|
site_context = ConfigContext.objects.create(
|
||||||
|
name="site",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"site": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
site_context.sites.add(self.site)
|
||||||
|
region_context = ConfigContext.objects.create(
|
||||||
|
name="region",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"region": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
region_context.regions.add(self.region)
|
||||||
|
platform_context = ConfigContext.objects.create(
|
||||||
|
name="platform",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"platform": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
platform_context.platforms.add(self.platform)
|
||||||
|
tenant_group_context = ConfigContext.objects.create(
|
||||||
|
name="tenant group",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"tenant_group": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tenant_group_context.tenant_groups.add(self.tenantgroup)
|
||||||
|
tenant_context = ConfigContext.objects.create(
|
||||||
|
name="tenant",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"tenant": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tenant_context.tenants.add(self.tenant)
|
||||||
|
tag_context = ConfigContext.objects.create(
|
||||||
|
name="tag",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"tag": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tag_context.tags.add(self.tag)
|
||||||
|
cluster_group = ClusterGroup.objects.create(name="Cluster Group")
|
||||||
|
cluster_group_context = ConfigContext.objects.create(
|
||||||
|
name="cluster group",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"cluster_group": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cluster_group_context.cluster_groups.add(cluster_group)
|
||||||
|
cluster_type = ClusterType.objects.create(name="Cluster Type 1")
|
||||||
|
cluster = Cluster.objects.create(name="Cluster", group=cluster_group, type=cluster_type)
|
||||||
|
cluster_context = ConfigContext.objects.create(
|
||||||
|
name="cluster",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"cluster": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cluster_context.clusters.add(cluster)
|
||||||
|
|
||||||
|
virtual_machine = VirtualMachine.objects.create(
|
||||||
|
name="VM 1",
|
||||||
|
cluster=cluster,
|
||||||
|
tenant=self.tenant,
|
||||||
|
platform=self.platform,
|
||||||
|
role=self.devicerole
|
||||||
|
)
|
||||||
|
virtual_machine.tags.add(self.tag)
|
||||||
|
|
||||||
|
annotated_queryset = VirtualMachine.objects.filter(name=virtual_machine.name).annotate_config_context_data()
|
||||||
|
self.assertEqual(virtual_machine.get_config_context(), annotated_queryset[0].get_config_context())
|
||||||
|
@ -172,9 +172,4 @@ class LDAPBackend:
|
|||||||
if getattr(ldap_config, 'LDAP_IGNORE_CERT_ERRORS', False):
|
if getattr(ldap_config, 'LDAP_IGNORE_CERT_ERRORS', False):
|
||||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
||||||
|
|
||||||
# Enable logging for django_auth_ldap
|
|
||||||
ldap_logger = logging.getLogger('django_auth_ldap')
|
|
||||||
ldap_logger.addHandler(logging.StreamHandler())
|
|
||||||
ldap_logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '2.9.7'
|
VERSION = '2.9.8'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
|
@ -48,28 +48,28 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{% if perms.dcim.add_consoleport %}
|
{% if perms.dcim.add_consoleport %}
|
||||||
<li><a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Console Ports</a></li>
|
<li><a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_consoleports">Console Ports</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_consoleserverport %}
|
{% if perms.dcim.add_consoleserverport %}
|
||||||
<li><a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Console Server Ports</a></li>
|
<li><a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_consoleserverports">Console Server Ports</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_powerport %}
|
{% if perms.dcim.add_powerport %}
|
||||||
<li><a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Power Ports</a></li>
|
<li><a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_powerports">Power Ports</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_poweroutlet %}
|
{% if perms.dcim.add_poweroutlet %}
|
||||||
<li><a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Power Outlets</a></li>
|
<li><a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_poweroutlets">Power Outlets</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_interface %}
|
{% if perms.dcim.add_interface %}
|
||||||
<li><a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Interfaces</a></li>
|
<li><a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_interfaces">Interfaces</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_frontport %}
|
{% if perms.dcim.add_frontport %}
|
||||||
<li><a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Front Ports</a></li>
|
<li><a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_frontports">Front Ports</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_rearport %}
|
{% if perms.dcim.add_rearport %}
|
||||||
<li><a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Rear Ports</a></li>
|
<li><a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_rearports">Rear Ports</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_devicebay %}
|
{% if perms.dcim.add_devicebay %}
|
||||||
<li><a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Device Bays</a></li>
|
<li><a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_devicebays">Device Bays</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_inventoryitem %}
|
{% if perms.dcim.add_inventoryitem %}
|
||||||
<li><a href="{% url 'dcim:inventoryitem_add' %}?device={{ device.pk }}&return_url={% url 'dcim:device_inventory' pk=device.pk %}">Inventory Items</a></li>
|
<li><a href="{% url 'dcim:inventoryitem_add' %}?device={{ device.pk }}&return_url={% url 'dcim:device_inventory' pk=device.pk %}">Inventory Items</a></li>
|
||||||
@ -537,26 +537,26 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if interfaces and perms.dcim.change_interface %}
|
{% if interfaces and perms.dcim.change_interface %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_interfaces" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_interfaces" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if interfaces and perms.dcim.change_interface %}
|
{% if interfaces and perms.dcim.change_interface %}
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}%23tab_interfaces" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if interfaces and perms.dcim.delete_interface %}
|
{% if interfaces and perms.dcim.delete_interface %}
|
||||||
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_interfaces" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_interface %}
|
{% if perms.dcim.add_interface %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_interfaces" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -597,24 +597,24 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if frontports and perms.dcim.change_frontport %}
|
{% if frontports and perms.dcim.change_frontport %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_frontports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_frontports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}%23tab_frontports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if frontports and perms.dcim.delete_frontport %}
|
{% if frontports and perms.dcim.delete_frontport %}
|
||||||
<button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_frontports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_frontport %}
|
{% if perms.dcim.add_frontport %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_frontports" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add front ports
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add front ports
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -654,24 +654,24 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if rearports and perms.dcim.change_rearport %}
|
{% if rearports and perms.dcim.change_rearport %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_rearports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_rearports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}%23tab_rearports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if rearports and perms.dcim.delete_rearport %}
|
{% if rearports and perms.dcim.delete_rearport %}
|
||||||
<button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_rearports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_rearport %}
|
{% if perms.dcim.add_rearport %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_rearports" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add rear ports
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add rear ports
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -708,24 +708,24 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if consoleports and perms.dcim.change_consoleport %}
|
{% if consoleports and perms.dcim.change_consoleport %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_consoleports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_consoleports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}%23tab_consoleports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if consoleports and perms.dcim.delete_consoleport %}
|
{% if consoleports and perms.dcim.delete_consoleport %}
|
||||||
<button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_consoleports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_consoleport %}
|
{% if perms.dcim.add_consoleport %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
|
<a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_consoleports" class="btn btn-xs btn-primary">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -763,24 +763,24 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if consoleserverports and perms.dcim.change_consoleport %}
|
{% if consoleserverports and perms.dcim.change_consoleport %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_consoleserverports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_consoleserverports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}%23tab_consoleserverports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if consoleserverports and perms.dcim.delete_consoleserverport %}
|
{% if consoleserverports and perms.dcim.delete_consoleserverport %}
|
||||||
<button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_consoleserverports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_consoleserverport %}
|
{% if perms.dcim.add_consoleserverport %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_consoleserverports" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -818,24 +818,24 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if powerports and perms.dcim.change_powerport %}
|
{% if powerports and perms.dcim.change_powerport %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_powerports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_powerports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}%23tab_powerports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if powerports and perms.dcim.delete_powerport %}
|
{% if powerports and perms.dcim.delete_powerport %}
|
||||||
<button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_powerports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_powerport %}
|
{% if perms.dcim.add_powerport %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
|
<a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_powerports" class="btn btn-xs btn-primary">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -874,24 +874,24 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if poweroutlets and perms.dcim.change_powerport %}
|
{% if poweroutlets and perms.dcim.change_powerport %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_poweroutlets" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_poweroutlets" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}%23tab_poweroutlets" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if poweroutlets and perms.dcim.delete_poweroutlet %}
|
{% if poweroutlets and perms.dcim.delete_poweroutlet %}
|
||||||
<button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_poweroutlets" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_poweroutlet %}
|
{% if perms.dcim.add_poweroutlet %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_poweroutlets" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -933,18 +933,18 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if devicebays and perms.dcim.change_devicebay %}
|
{% if devicebays and perms.dcim.change_devicebay %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_devicebays" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if devicebays and perms.dcim.delete_devicebay %}
|
{% if devicebays and perms.dcim.delete_devicebay %}
|
||||||
<button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_devicebays" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_devicebay %}
|
{% if perms.dcim.add_devicebay %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_devicebays" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -963,6 +963,16 @@
|
|||||||
|
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
// Redirect user to appropriate components tab if specified
|
||||||
|
var hash = document.location.hash;
|
||||||
|
var prefix = "tab_";
|
||||||
|
if (hash) {
|
||||||
|
$('.nav-tabs a[href="'+hash.replace(prefix,"")+'"]').tab('show');
|
||||||
|
}
|
||||||
|
$('.nav-tabs a').on('shown.bs.tab', function (e) {
|
||||||
|
window.location.hash = e.target.hash.replace("#", "#" + prefix);
|
||||||
|
});
|
||||||
|
|
||||||
function toggleConnection(elem) {
|
function toggleConnection(elem) {
|
||||||
var url = netbox_api_path + "dcim/cables/" + elem.attr('data') + "/";
|
var url = netbox_api_path + "dcim/cables/" + elem.attr('data') + "/";
|
||||||
if (elem.hasClass('connected')) {
|
if (elem.hasClass('connected')) {
|
||||||
|
@ -24,14 +24,30 @@
|
|||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{% if perms.dcim.add_consoleporttemplate %}<li><a href="{% url 'dcim:consoleporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Console Ports</a></li>{% endif %}
|
{% if perms.dcim.add_consoleporttemplate %}
|
||||||
{% if perms.dcim.add_consoleserverporttemplate %}<li><a href="{% url 'dcim:consoleserverporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Console Server Ports</a></li>{% endif %}
|
<li><a href="{% url 'dcim:consoleporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_consoleports">Console Ports</a></li>
|
||||||
{% if perms.dcim.add_powerporttemplate %}<li><a href="{% url 'dcim:powerporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Power Ports</a></li>{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_poweroutlettemplate %}<li><a href="{% url 'dcim:poweroutlettemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Power Outlets</a></li>{% endif %}
|
{% if perms.dcim.add_consoleserverporttemplate %}
|
||||||
{% if perms.dcim.add_interfacetemplate %}<li><a href="{% url 'dcim:interfacetemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Interfaces</a></li>{% endif %}
|
<li><a href="{% url 'dcim:consoleserverporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_consoleserverports">Console Server Ports</a></li>
|
||||||
{% if perms.dcim.add_frontporttemplate %}<li><a href="{% url 'dcim:frontporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Front Ports</a></li>{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_rearporttemplate %}<li><a href="{% url 'dcim:rearporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Rear Ports</a></li>{% endif %}
|
{% if perms.dcim.add_powerporttemplate %}
|
||||||
{% if perms.dcim.add_devicebaytemplate %}<li><a href="{% url 'dcim:devicebaytemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Device Bays</a></li>{% endif %}
|
<li><a href="{% url 'dcim:powerporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_powerports">Power Ports</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_poweroutlettemplate %}
|
||||||
|
<li><a href="{% url 'dcim:poweroutlettemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_poweroutlets">Power Outlets</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_interfacetemplate %}
|
||||||
|
<li><a href="{% url 'dcim:interfacetemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_interfaces">Interfaces</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_frontporttemplate %}
|
||||||
|
<li><a href="{% url 'dcim:frontporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_frontports">Front Ports</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_rearporttemplate %}
|
||||||
|
<li><a href="{% url 'dcim:rearporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_rearports">Rear Ports</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_devicebaytemplate %}
|
||||||
|
<li><a href="{% url 'dcim:devicebaytemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_devicebays">Device Bays</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -217,3 +233,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascript %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Redirect user to appropriate components tab if specified
|
||||||
|
var hash = document.location.hash;
|
||||||
|
var prefix = "tab_";
|
||||||
|
if (hash) {
|
||||||
|
$('.nav-tabs a[href="'+hash.replace(prefix,"")+'"]').tab('show');
|
||||||
|
}
|
||||||
|
$('.nav-tabs a').on('shown.bs.tab', function (e) {
|
||||||
|
window.location.hash = e.target.hash.replace("#", "#" + prefix);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
@ -22,7 +22,7 @@ class ObjectPermissionInline(admin.TabularInline):
|
|||||||
verbose_name_plural = 'Permissions'
|
verbose_name_plural = 'Permissions'
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return super().get_queryset(request).prefetch_related('objectpermission__object_types')
|
return super().get_queryset(request).prefetch_related('objectpermission__object_types').nocache()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def object_types(instance):
|
def object_types(instance):
|
||||||
@ -185,7 +185,7 @@ class ObjectPermissionForm(forms.ModelForm):
|
|||||||
|
|
||||||
# Validate the specified model constraints by attempting to execute a query. We don't care whether the query
|
# Validate the specified model constraints by attempting to execute a query. We don't care whether the query
|
||||||
# returns anything; we just want to make sure the specified constraints are valid.
|
# returns anything; we just want to make sure the specified constraints are valid.
|
||||||
if constraints:
|
if object_types and constraints:
|
||||||
# Normalize the constraints to a list of dicts
|
# Normalize the constraints to a list of dicts
|
||||||
if type(constraints) is not list:
|
if type(constraints) is not list:
|
||||||
constraints = [constraints]
|
constraints = [constraints]
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from django.contrib.postgres.aggregates import JSONBAgg
|
||||||
|
from django.contrib.postgres.aggregates.mixins import OrderableAggMixin
|
||||||
from django.db.models import F, Func
|
from django.db.models import F, Func
|
||||||
|
|
||||||
|
|
||||||
@ -7,3 +9,21 @@ class CollateAsChar(Func):
|
|||||||
"""
|
"""
|
||||||
function = 'C'
|
function = 'C'
|
||||||
template = '(%(expressions)s) COLLATE "%(function)s"'
|
template = '(%(expressions)s) COLLATE "%(function)s"'
|
||||||
|
|
||||||
|
|
||||||
|
class OrderableJSONBAgg(OrderableAggMixin, JSONBAgg):
|
||||||
|
"""
|
||||||
|
TODO in Django 3.2 ordering is supported natively on JSONBAgg so this is no longer needed.
|
||||||
|
"""
|
||||||
|
template = '%(function)s(%(distinct)s%(expressions)s %(ordering)s)'
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyGroupByJSONBAgg(OrderableJSONBAgg):
|
||||||
|
"""
|
||||||
|
JSONBAgg is a builtin aggregation function which means it includes the use of a GROUP BY clause.
|
||||||
|
When used as an annotation for collecting config context data objects, the GROUP BY is
|
||||||
|
incorrect. This subclass overrides the Django ORM aggregation control to remove the GROUP BY.
|
||||||
|
|
||||||
|
TODO in Django 3.2 ordering is supported natively on JSONBAgg so we only need to inherit from JSONBAgg.
|
||||||
|
"""
|
||||||
|
contains_aggregate = False
|
||||||
|
@ -130,6 +130,7 @@ class ButtonsColumn(tables.TemplateColumn):
|
|||||||
|
|
||||||
:param model: Model class to use for calculating URL view names
|
:param model: Model class to use for calculating URL view names
|
||||||
:param prepend_content: Additional template content to render in the column (optional)
|
:param prepend_content: Additional template content to render in the column (optional)
|
||||||
|
:param return_url_extra: String to append to the return URL (e.g. for specifying a tab) (optional)
|
||||||
"""
|
"""
|
||||||
buttons = ('changelog', 'edit', 'delete')
|
buttons = ('changelog', 'edit', 'delete')
|
||||||
attrs = {'td': {'class': 'text-right text-nowrap noprint'}}
|
attrs = {'td': {'class': 'text-right text-nowrap noprint'}}
|
||||||
@ -141,18 +142,19 @@ class ButtonsColumn(tables.TemplateColumn):
|
|||||||
</a>
|
</a>
|
||||||
{{% endif %}}
|
{{% endif %}}
|
||||||
{{% if "edit" in buttons and perms.{app_label}.change_{model_name} %}}
|
{{% if "edit" in buttons and perms.{app_label}.change_{model_name} %}}
|
||||||
<a href="{{% url '{app_label}:{model_name}_edit' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-warning" title="Edit">
|
<a href="{{% url '{app_label}:{model_name}_edit' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}{{{{ return_url_extra }}}}" class="btn btn-xs btn-warning" title="Edit">
|
||||||
<i class="fa fa-pencil"></i>
|
<i class="fa fa-pencil"></i>
|
||||||
</a>
|
</a>
|
||||||
{{% endif %}}
|
{{% endif %}}
|
||||||
{{% if "delete" in buttons and perms.{app_label}.delete_{model_name} %}}
|
{{% if "delete" in buttons and perms.{app_label}.delete_{model_name} %}}
|
||||||
<a href="{{% url '{app_label}:{model_name}_delete' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-danger" title="Delete">
|
<a href="{{% url '{app_label}:{model_name}_delete' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}{{{{ return_url_extra }}}}" class="btn btn-xs btn-danger" title="Delete">
|
||||||
<i class="fa fa-trash"></i>
|
<i class="fa fa-trash"></i>
|
||||||
</a>
|
</a>
|
||||||
{{% endif %}}
|
{{% endif %}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, model, *args, pk_field='pk', buttons=None, prepend_template=None, **kwargs):
|
def __init__(self, model, *args, pk_field='pk', buttons=None, prepend_template=None, return_url_extra='',
|
||||||
|
**kwargs):
|
||||||
if prepend_template:
|
if prepend_template:
|
||||||
prepend_template = prepend_template.replace('{', '{{')
|
prepend_template = prepend_template.replace('{', '{{')
|
||||||
prepend_template = prepend_template.replace('}', '}}')
|
prepend_template = prepend_template.replace('}', '}}')
|
||||||
@ -169,6 +171,7 @@ class ButtonsColumn(tables.TemplateColumn):
|
|||||||
|
|
||||||
self.extra_context.update({
|
self.extra_context.update({
|
||||||
'buttons': buttons or self.buttons,
|
'buttons': buttons or self.buttons,
|
||||||
|
'return_url_extra': return_url_extra,
|
||||||
})
|
})
|
||||||
|
|
||||||
def header(self):
|
def header(self):
|
||||||
|
@ -6,7 +6,7 @@ from rest_framework.routers import APIRootView
|
|||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from extras.api.serializers import RenderedGraphSerializer
|
from extras.api.serializers import RenderedGraphSerializer
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
|
||||||
from extras.models import Graph
|
from extras.models import Graph
|
||||||
from utilities.api import ModelViewSet
|
from utilities.api import ModelViewSet
|
||||||
from utilities.utils import get_subquery
|
from utilities.utils import get_subquery
|
||||||
@ -58,7 +58,7 @@ class ClusterViewSet(CustomFieldModelViewSet):
|
|||||||
# Virtual machines
|
# Virtual machines
|
||||||
#
|
#
|
||||||
|
|
||||||
class VirtualMachineViewSet(CustomFieldModelViewSet):
|
class VirtualMachineViewSet(CustomFieldModelViewSet, ConfigContextQuerySetMixin):
|
||||||
queryset = VirtualMachine.objects.prefetch_related(
|
queryset = VirtualMachine.objects.prefetch_related(
|
||||||
'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
|
'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
|
||||||
)
|
)
|
||||||
|
@ -8,6 +8,7 @@ from taggit.managers import TaggableManager
|
|||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from dcim.models import BaseInterface, Device
|
from dcim.models import BaseInterface, Device
|
||||||
from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
|
from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
|
||||||
|
from extras.querysets import ConfigContextModelQuerySet
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from utilities.fields import NaturalOrderingField
|
from utilities.fields import NaturalOrderingField
|
||||||
from utilities.ordering import naturalize_interface
|
from utilities.ordering import naturalize_interface
|
||||||
@ -282,7 +283,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
)
|
)
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = ConfigContextModelQuerySet.as_manager()
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
||||||
|
@ -261,7 +261,7 @@ class VirtualMachineView(ObjectView):
|
|||||||
|
|
||||||
|
|
||||||
class VirtualMachineConfigContextView(ObjectConfigContextView):
|
class VirtualMachineConfigContextView(ObjectConfigContextView):
|
||||||
queryset = VirtualMachine.objects.all()
|
queryset = VirtualMachine.objects.annotate_config_context_data()
|
||||||
base_template = 'virtualization/virtualmachine.html'
|
base_template = 'virtualization/virtualmachine.html'
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
Django==3.1
|
Django==3.1
|
||||||
django-cacheops==5.0.1
|
django-cacheops==5.1.0
|
||||||
django-cors-headers==3.4.0
|
django-cors-headers==3.4.0
|
||||||
django-debug-toolbar==2.2
|
django-debug-toolbar==2.2
|
||||||
django-filter==2.3.0
|
django-filter==2.3.0
|
||||||
|
Loading…
Reference in New Issue
Block a user