Merge branch 'develop' into feature

This commit is contained in:
jeremystretch 2021-11-03 10:29:02 -04:00
commit 2c2e37e9f0
36 changed files with 502 additions and 176 deletions

View File

@ -17,7 +17,7 @@ body:
What version of NetBox are you currently running? (If you don't have access to the most What version of NetBox are you currently running? (If you don't have access to the most
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/) recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
before opening a bug report to see if your issue has already been addressed.) before opening a bug report to see if your issue has already been addressed.)
placeholder: v3.0.8 placeholder: v3.0.9
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -14,7 +14,7 @@ body:
attributes: attributes:
label: NetBox version label: NetBox version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v3.0.8 placeholder: v3.0.9
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -5,4 +5,4 @@
# Example Power Topology # Example Power Topology
![Power distribution model](/media/power_distribution.png) ![Power distribution model](../media/power_distribution.png)

View File

@ -240,7 +240,7 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a
!!! note !!! note
To run a custom script, a user must be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in the admin UI as shown below. To run a custom script, a user must be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in the admin UI as shown below.
![Adding the run action to a permission](/media/admin_ui_run_permission.png) ![Adding the run action to a permission](../media/admin_ui_run_permission.png)
### Via the Web UI ### Via the Web UI
@ -259,6 +259,22 @@ http://netbox/api/extras/scripts/example.MyReport/ \
--data '{"data": {"foo": "somevalue", "bar": 123}, "commit": true}' --data '{"data": {"foo": "somevalue", "bar": 123}, "commit": true}'
``` ```
### Via the CLI
Scripts can be run on the CLI by invoking the management command:
```
python3 manage.py runscript [--commit] [--loglevel {debug,info,warning,error,critical}] [--data "<data>"] <module>.<script>
```
The required ``<module>.<script>`` argument is the script to run where ``<module>`` is the name of the python file in the ``scripts`` directory without the ``.py`` extension and ``<script>`` is the name of the script class in the ``<module>`` to run.
The optional ``--data "<data>"`` argument is the data to send to the script
The optional ``--loglevel`` argument is the desired logging level to output to the console.
The optional ``--commit`` argument will commit any changes in the script to the database.
## Example ## Example
Below is an example script that creates new objects for a planned site. The user is prompted for three variables: Below is an example script that creates new objects for a planned site. The user is prompted for three variables:

View File

@ -25,7 +25,7 @@ A cable may be traced from either of its endpoints by clicking the "trace" butto
In the example below, three individual cables comprise a path between devices A and D: In the example below, three individual cables comprise a path between devices A and D:
![Cable path](/media/models/dcim_cable_trace.png) ![Cable path](../media/models/dcim_cable_trace.png)
Traced from Interface 1 on Device A, NetBox will show the following path: Traced from Interface 1 on Device A, NetBox will show the following path:

View File

@ -1,6 +1,29 @@
# NetBox v3.0 # NetBox v3.0
## v3.0.9 (FUTURE) ## v3.0.10 (FUTURE)
---
## v3.0.9 (2021-11-03)
### Enhancements
* [#6529](https://github.com/netbox-community/netbox/issues/6529) - Introduce the `runscript` management command
* [#6930](https://github.com/netbox-community/netbox/issues/6930) - Add an optional "ID" column to all tables
* [#7668](https://github.com/netbox-community/netbox/issues/7668) - Add "view elevations" button to location view
### Bug Fixes
* [#7599](https://github.com/netbox-community/netbox/issues/7599) - Improve color mode preference handling
* [#7601](https://github.com/netbox-community/netbox/issues/7601) - Correct devices count for locations within global search results
* [#7612](https://github.com/netbox-community/netbox/issues/7612) - Strip HTML from custom field descriptions
* [#7628](https://github.com/netbox-community/netbox/issues/7628) - Fix `load_yaml` method for custom scripts
* [#7643](https://github.com/netbox-community/netbox/issues/7643) - Fix circuit assignment when creating multiple terminations simultaneously
* [#7644](https://github.com/netbox-community/netbox/issues/7644) - Prevent inadvertent deletion of prior change records when deleting objects (#7333 revisited)
* [#7647](https://github.com/netbox-community/netbox/issues/7647) - Require interface assignment when designating IP address as primary for device/VM during CSV import
* [#7664](https://github.com/netbox-community/netbox/issues/7664) - Preserve initial form data when bulk edit validation fails
* [#7717](https://github.com/netbox-community/netbox/issues/7717) - Restore missing tags column on IP range table
* [#7721](https://github.com/netbox-community/netbox/issues/7721) - Retain pagination preference when `MAX_PAGE_SIZE` is zero
--- ---

View File

@ -11,6 +11,7 @@ def update_circuit(instance, **kwargs):
When a CircuitTermination has been modified, update its parent Circuit. When a CircuitTermination has been modified, update its parent Circuit.
""" """
termination_name = f'termination_{instance.term_side.lower()}' termination_name = f'termination_{instance.term_side.lower()}'
instance.circuit.refresh_from_db()
setattr(instance.circuit, termination_name, instance) setattr(instance.circuit, termination_name, instance)
instance.circuit.save() instance.circuit.save()

View File

@ -44,8 +44,8 @@ class ProviderTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Provider model = Provider
fields = ( fields = (
'pk', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count', 'comments', 'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count',
'tags', 'comments', 'tags',
) )
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count') default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
@ -69,7 +69,7 @@ class ProviderNetworkTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ProviderNetwork model = ProviderNetwork
fields = ('pk', 'name', 'provider', 'description', 'comments', 'tags') fields = ('pk', 'id', 'name', 'provider', 'description', 'comments', 'tags')
default_columns = ('pk', 'name', 'provider', 'description') default_columns = ('pk', 'name', 'provider', 'description')
@ -92,7 +92,7 @@ class CircuitTypeTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = CircuitType model = CircuitType
fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions') fields = ('pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions')
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions') default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
@ -104,7 +104,7 @@ class CircuitTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
cid = tables.Column( cid = tables.Column(
linkify=True, linkify=True,
verbose_name='ID' verbose_name='Circuit ID'
) )
provider = tables.Column( provider = tables.Column(
linkify=True linkify=True
@ -127,7 +127,7 @@ class CircuitTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Circuit model = Circuit
fields = ( fields = (
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date', 'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
'commit_rate', 'description', 'comments', 'tags', 'commit_rate', 'description', 'comments', 'tags',
) )
default_columns = ( default_columns = (

View File

@ -43,6 +43,7 @@ class ConsoleConnectionTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ConsolePort model = ConsolePort
fields = ('device', 'name', 'console_server', 'console_server_port', 'reachable') fields = ('device', 'name', 'console_server', 'console_server_port', 'reachable')
exclude = ('id', )
class PowerConnectionTable(BaseTable): class PowerConnectionTable(BaseTable):
@ -73,6 +74,7 @@ class PowerConnectionTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerPort model = PowerPort
fields = ('device', 'name', 'pdu', 'outlet', 'reachable') fields = ('device', 'name', 'pdu', 'outlet', 'reachable')
exclude = ('id', )
class InterfaceConnectionTable(BaseTable): class InterfaceConnectionTable(BaseTable):
@ -106,3 +108,4 @@ class InterfaceConnectionTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Interface model = Interface
fields = ('device_a', 'interface_a', 'device_b', 'interface_b', 'reachable') fields = ('device_a', 'interface_a', 'device_b', 'interface_b', 'reachable')
exclude = ('id', )

View File

@ -17,10 +17,6 @@ __all__ = (
class CableTable(BaseTable): class CableTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
id = tables.Column(
linkify=True,
verbose_name='ID'
)
termination_a_parent = tables.TemplateColumn( termination_a_parent = tables.TemplateColumn(
template_code=CABLE_TERMINATION_PARENT, template_code=CABLE_TERMINATION_PARENT,
accessor=Accessor('termination_a'), accessor=Accessor('termination_a'),

View File

@ -1,6 +1,5 @@
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from django.conf import settings
from dcim.models import ( from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform,
@ -15,6 +14,7 @@ from .template_code import *
__all__ = ( __all__ = (
'BaseInterfaceTable', 'BaseInterfaceTable',
'CableTerminationTable',
'ConsolePortTable', 'ConsolePortTable',
'ConsoleServerPortTable', 'ConsoleServerPortTable',
'DeviceBayTable', 'DeviceBayTable',
@ -88,7 +88,8 @@ class DeviceRoleTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceRole model = DeviceRole
fields = ( fields = (
'pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags', 'actions', 'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags',
'actions',
) )
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions') default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions')
@ -120,7 +121,7 @@ class PlatformTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Platform model = Platform
fields = ( fields = (
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args', 'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
'description', 'tags', 'actions', 'description', 'tags', 'actions',
) )
default_columns = ( default_columns = (
@ -193,8 +194,8 @@ class DeviceTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Device model = Device
fields = ( fields = (
'pk', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial', 'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'airflow', 'primary_ip', 'primary_ip4', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags',
) )
default_columns = ( default_columns = (
@ -224,7 +225,7 @@ class DeviceImportTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Device model = Device
fields = ('name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type') fields = ('id', 'name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
empty_text = False empty_text = False
@ -287,7 +288,7 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsolePort model = ConsolePort
fields = ( fields = (
'pk', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color', 'pk', 'id', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'link_peer', 'connection', 'tags', 'link_peer', 'connection', 'tags',
) )
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description') default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
@ -308,7 +309,7 @@ class DeviceConsolePortTable(ConsolePortTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsolePort model = ConsolePort
fields = ( fields = (
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color', 'pk', 'id', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'link_peer', 'connection', 'tags', 'actions' 'link_peer', 'connection', 'tags', 'actions'
) )
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions') default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
@ -331,8 +332,8 @@ class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsoleServerPort model = ConsoleServerPort
fields = ( fields = (
'pk', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color', 'pk', 'id', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable',
'link_peer', 'connection', 'tags', 'cable_color', 'link_peer', 'connection', 'tags',
) )
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description') default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
@ -353,7 +354,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsoleServerPort model = ConsoleServerPort
fields = ( fields = (
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color', 'pk', 'id', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'link_peer', 'connection', 'tags', 'actions', 'link_peer', 'connection', 'tags', 'actions',
) )
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions') default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
@ -376,8 +377,8 @@ class PowerPortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerPort model = PowerPort
fields = ( fields = (
'pk', 'name', 'device', 'label', 'type', 'description', 'mark_connected', 'maximum_draw', 'allocated_draw', 'pk', 'id', 'name', 'device', 'label', 'type', 'description', 'mark_connected', 'maximum_draw',
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'tags',
) )
default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description') default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
@ -398,8 +399,8 @@ class DevicePowerPortTable(PowerPortTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerPort model = PowerPort
fields = ( fields = (
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable', 'pk', 'id', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected',
'cable_color', 'link_peer', 'connection', 'tags', 'actions', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection', 'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection',
@ -427,8 +428,8 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerOutlet model = PowerOutlet
fields = ( fields = (
'pk', 'name', 'device', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected', 'cable', 'pk', 'id', 'name', 'device', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected',
'cable_color', 'link_peer', 'connection', 'tags', 'cable', 'cable_color', 'link_peer', 'connection', 'tags',
) )
default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description') default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description')
@ -448,7 +449,7 @@ class DevicePowerOutletTable(PowerOutletTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerOutlet model = PowerOutlet
fields = ( fields = (
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable', 'pk', 'id', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable',
'cable_color', 'link_peer', 'connection', 'tags', 'actions', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
) )
default_columns = ( default_columns = (
@ -497,7 +498,7 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = Interface model = Interface
fields = ( fields = (
'pk', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'pk', 'id', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn',
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description',
'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection',
'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
@ -532,7 +533,7 @@ class DeviceInterfaceTable(InterfaceTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = Interface model = Interface
fields = ( fields = (
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag', 'mgmt_only', 'mtu', 'mode', 'pk', 'id', 'name', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag', 'mgmt_only', 'mtu', 'mode',
'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', 'description', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', 'description',
'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection',
'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'actions', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'actions',
@ -570,7 +571,7 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = FrontPort model = FrontPort
fields = ( fields = (
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'pk', 'id', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags',
) )
default_columns = ( default_columns = (
@ -594,7 +595,7 @@ class DeviceFrontPortTable(FrontPortTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = FrontPort model = FrontPort
fields = ( fields = (
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected', 'cable', 'pk', 'id', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected', 'cable',
'cable_color', 'link_peer', 'tags', 'actions', 'cable_color', 'link_peer', 'tags', 'actions',
) )
default_columns = ( default_columns = (
@ -621,7 +622,7 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = RearPort model = RearPort
fields = ( fields = (
'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable', 'pk', 'id', 'name', 'device', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable',
'cable_color', 'link_peer', 'tags', 'cable_color', 'link_peer', 'tags',
) )
default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description') default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description')
@ -643,7 +644,7 @@ class DeviceRearPortTable(RearPortTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = RearPort model = RearPort
fields = ( fields = (
'pk', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable', 'cable_color', 'pk', 'id', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable', 'cable_color',
'link_peer', 'tags', 'actions', 'link_peer', 'tags', 'actions',
) )
default_columns = ( default_columns = (
@ -673,7 +674,7 @@ class DeviceBayTable(DeviceComponentTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = DeviceBay model = DeviceBay
fields = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags') fields = ('pk', 'id', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags')
default_columns = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description') default_columns = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description')
@ -693,7 +694,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = DeviceBay model = DeviceBay
fields = ( fields = (
'pk', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions', 'pk', 'id', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'status', 'installed_device', 'description', 'actions', 'pk', 'name', 'label', 'status', 'installed_device', 'description', 'actions',
@ -719,7 +720,7 @@ class InventoryItemTable(DeviceComponentTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = InventoryItem model = InventoryItem
fields = ( fields = (
'pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'pk', 'id', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
'discovered', 'tags', 'discovered', 'tags',
) )
default_columns = ('pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag') default_columns = ('pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
@ -740,7 +741,7 @@ class DeviceInventoryItemTable(InventoryItemTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = InventoryItem model = InventoryItem
fields = ( fields = (
'pk', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered', 'pk', 'id', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered',
'tags', 'actions', 'tags', 'actions',
) )
default_columns = ( default_columns = (
@ -772,5 +773,5 @@ class VirtualChassisTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VirtualChassis model = VirtualChassis
fields = ('pk', 'name', 'domain', 'master', 'member_count', 'tags') fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags')
default_columns = ('pk', 'name', 'domain', 'master', 'member_count') default_columns = ('pk', 'name', 'domain', 'master', 'member_count')

View File

@ -49,9 +49,12 @@ class ManufacturerTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Manufacturer model = Manufacturer
fields = ( fields = (
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'tags', 'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
'actions', 'actions',
) )
default_columns = (
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',
)
# #
@ -80,7 +83,7 @@ class DeviceTypeTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceType model = DeviceType
fields = ( fields = (
'pk', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
'airflow', 'comments', 'instance_count', 'tags', 'airflow', 'comments', 'instance_count', 'tags',
) )
default_columns = ( default_columns = (
@ -94,10 +97,16 @@ class DeviceTypeTable(BaseTable):
class ComponentTemplateTable(BaseTable): class ComponentTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
id = tables.Column(
verbose_name='ID'
)
name = tables.Column( name = tables.Column(
order_by=('_name',) order_by=('_name',)
) )
class Meta(BaseTable.Meta):
exclude = ('id', )
class ConsolePortTemplateTable(ComponentTemplateTable): class ConsolePortTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn( actions = ButtonsColumn(
@ -106,7 +115,7 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
return_url_extra='%23tab_consoleports' return_url_extra='%23tab_consoleports'
) )
class Meta(BaseTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ('pk', 'name', 'label', 'type', 'description', 'actions') fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -119,7 +128,7 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
return_url_extra='%23tab_consoleserverports' return_url_extra='%23tab_consoleserverports'
) )
class Meta(BaseTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ('pk', 'name', 'label', 'type', 'description', 'actions') fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -132,7 +141,7 @@ class PowerPortTemplateTable(ComponentTemplateTable):
return_url_extra='%23tab_powerports' return_url_extra='%23tab_powerports'
) )
class Meta(BaseTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = PowerPortTemplate model = PowerPortTemplate
fields = ('pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'actions') fields = ('pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -145,7 +154,7 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
return_url_extra='%23tab_poweroutlets' return_url_extra='%23tab_poweroutlets'
) )
class Meta(BaseTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = PowerOutletTemplate model = PowerOutletTemplate
fields = ('pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'actions') fields = ('pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -161,7 +170,7 @@ class InterfaceTemplateTable(ComponentTemplateTable):
return_url_extra='%23tab_interfaces' return_url_extra='%23tab_interfaces'
) )
class Meta(BaseTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = InterfaceTemplate model = InterfaceTemplate
fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'actions') fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -178,7 +187,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
return_url_extra='%23tab_frontports' return_url_extra='%23tab_frontports'
) )
class Meta(BaseTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = FrontPortTemplate model = FrontPortTemplate
fields = ('pk', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'actions') fields = ('pk', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -192,7 +201,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
return_url_extra='%23tab_rearports' return_url_extra='%23tab_rearports'
) )
class Meta(BaseTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = RearPortTemplate model = RearPortTemplate
fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'description', 'actions') fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -205,7 +214,7 @@ class DeviceBayTemplateTable(ComponentTemplateTable):
return_url_extra='%23tab_devicebays' return_url_extra='%23tab_devicebays'
) )
class Meta(BaseTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = DeviceBayTemplate model = DeviceBayTemplate
fields = ('pk', 'name', 'label', 'description', 'actions') fields = ('pk', 'name', 'label', 'description', 'actions')
empty_text = "None" empty_text = "None"

View File

@ -33,7 +33,7 @@ class PowerPanelTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerPanel model = PowerPanel
fields = ('pk', 'name', 'site', 'location', 'powerfeed_count', 'tags') fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'tags')
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count') default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
@ -70,7 +70,7 @@ class PowerFeedTable(CableTerminationTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerFeed model = PowerFeed
fields = ( fields = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'available_power', 'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'available_power',
'comments', 'tags', 'comments', 'tags',
) )

View File

@ -31,7 +31,7 @@ class RackRoleTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RackRole model = RackRole
fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions') fields = ('pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions')
default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions') default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions')
@ -79,7 +79,7 @@ class RackTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Rack model = Rack
fields = ( fields = (
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type', 'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
'width', 'u_height', 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'tags', 'width', 'u_height', 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'tags',
) )
default_columns = ( default_columns = (
@ -118,7 +118,7 @@ class RackReservationTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RackReservation model = RackReservation
fields = ( fields = (
'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags', 'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags',
'actions', 'actions',
) )
default_columns = ( default_columns = (

View File

@ -36,7 +36,7 @@ class RegionTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Region model = Region
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'tags', 'actions') fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'site_count', 'description', 'actions') default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
@ -61,7 +61,7 @@ class SiteGroupTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = SiteGroup model = SiteGroup
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'tags', 'actions') fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'site_count', 'description', 'actions') default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
@ -90,7 +90,7 @@ class SiteTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Site model = Site
fields = ( fields = (
'pk', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'time_zone', 'description', 'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'time_zone', 'description',
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
'contact_email', 'comments', 'tags', 'contact_email', 'comments', 'tags',
) )
@ -131,6 +131,7 @@ class LocationTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Location model = Location
fields = ( fields = (
'pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags', 'actions', 'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags',
'actions',
) )
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions') default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions')

View File

@ -0,0 +1,158 @@
import json
import logging
import sys
import traceback
import uuid
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from extras.api.serializers import ScriptOutputSerializer
from extras.choices import JobResultStatusChoices
from extras.context_managers import change_logging
from extras.models import JobResult
from extras.scripts import get_script
from utilities.exceptions import AbortTransaction
from utilities.utils import NetBoxFakeRequest
class Command(BaseCommand):
help = "Run a script in Netbox"
def add_arguments(self, parser):
parser.add_argument(
'--loglevel',
help="Logging Level (default: info)",
dest='loglevel',
default='info',
choices=['debug', 'info', 'warning', 'error', 'critical'])
parser.add_argument('--commit', help="Commit this script to database", action='store_true')
parser.add_argument('--user', help="User script is running as")
parser.add_argument('--data', help="Data as a string encapsulated JSON blob")
parser.add_argument('script', help="Script to run")
def handle(self, *args, **options):
def _run_script():
"""
Core script execution task. We capture this within a subfunction to allow for conditionally wrapping it with
the change_logging context manager (which is bypassed if commit == False).
"""
try:
with transaction.atomic():
script.output = script.run(data=data, commit=commit)
job_result.set_status(JobResultStatusChoices.STATUS_COMPLETED)
if not commit:
raise AbortTransaction()
except AbortTransaction:
script.log_info("Database changes have been reverted automatically.")
except Exception as e:
stacktrace = traceback.format_exc()
script.log_failure(
f"An exception occurred: `{type(e).__name__}: {e}`\n```\n{stacktrace}\n```"
)
script.log_info("Database changes have been reverted due to error.")
logger.error(f"Exception raised during script execution: {e}")
job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
finally:
job_result.data = ScriptOutputSerializer(script).data
job_result.save()
logger.info(f"Script completed in {job_result.duration}")
# Params
script = options['script']
loglevel = options['loglevel']
commit = options['commit']
try:
data = json.loads(options['data'])
except TypeError:
data = {}
module, name = script.split('.', 1)
# Take user from command line if provided and exists, other
if options['user']:
try:
user = User.objects.get(username=options['user'])
except User.DoesNotExist:
user = User.objects.filter(is_superuser=True).order_by('pk')[0]
else:
user = User.objects.filter(is_superuser=True).order_by('pk')[0]
# Setup logging to Stdout
formatter = logging.Formatter(f'[%(asctime)s][%(levelname)s] - %(message)s')
stdouthandler = logging.StreamHandler(sys.stdout)
stdouthandler.setLevel(logging.DEBUG)
stdouthandler.setFormatter(formatter)
logger = logging.getLogger(f"netbox.scripts.{module}.{name}")
logger.addHandler(stdouthandler)
try:
logger.setLevel({
'critical': logging.CRITICAL,
'debug': logging.DEBUG,
'error': logging.ERROR,
'fatal': logging.FATAL,
'info': logging.INFO,
'warning': logging.WARNING,
}[loglevel])
except KeyError:
raise CommandError(f"Invalid log level: {loglevel}")
# Get the script
script = get_script(module, name)()
# Parse the parameters
form = script.as_form(data, None)
script_content_type = ContentType.objects.get(app_label='extras', model='script')
# Delete any previous terminal state results
JobResult.objects.filter(
obj_type=script_content_type,
name=script.full_name,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).delete()
# Create the job result
job_result = JobResult.objects.create(
name=script.full_name,
obj_type=script_content_type,
user=User.objects.filter(is_superuser=True).order_by('pk')[0],
job_id=uuid.uuid4()
)
request = NetBoxFakeRequest({
'META': {},
'POST': data,
'GET': {},
'FILES': {},
'user': user,
'path': '',
'id': job_result.job_id
})
if form.is_valid():
job_result.status = JobResultStatusChoices.STATUS_RUNNING
job_result.save()
logger.info(f"Running script (commit={commit})")
script.request = request
# Execute the script. If commit is True, wrap it with the change_logging context manager to ensure we process
# change logging, webhooks, etc.
with change_logging(request):
_run_script()
else:
logger.error('Data is not valid:')
for field, errors in form.errors.get_json_data().items():
for error in errors:
logger.error(f'\t{field}: {error.get("message")}')
job_result.status = JobResultStatusChoices.STATUS_ERRORED
job_result.save()

View File

@ -8,6 +8,7 @@ from django.contrib.postgres.fields import ArrayField
from django.core.validators import RegexValidator, ValidationError from django.core.validators import RegexValidator, ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from extras.choices import * from extras.choices import *
@ -306,7 +307,7 @@ class CustomField(ChangeLoggedModel):
field.model = self field.model = self
field.label = str(self) field.label = str(self)
if self.description: if self.description:
field.help_text = self.description field.help_text = escape(self.description)
return field return field

View File

@ -4,7 +4,6 @@ import logging
import os import os
import pkgutil import pkgutil
import traceback import traceback
import warnings
from collections import OrderedDict from collections import OrderedDict
import yaml import yaml
@ -345,9 +344,14 @@ class BaseScript:
""" """
Return data from a YAML file Return data from a YAML file
""" """
try:
from yaml import CLoader as Loader
except ImportError:
from yaml import Loader
file_path = os.path.join(settings.SCRIPTS_ROOT, filename) file_path = os.path.join(settings.SCRIPTS_ROOT, filename)
with open(file_path, 'r') as datafile: with open(file_path, 'r') as datafile:
data = yaml.load(datafile) data = yaml.load(datafile, Loader=Loader)
return data return data

View File

@ -57,8 +57,8 @@ class CustomFieldTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = CustomField model = CustomField
fields = ( fields = (
'pk', 'name', 'content_types', 'label', 'type', 'required', 'weight', 'default', 'description', 'pk', 'id', 'name', 'content_types', 'label', 'type', 'required', 'weight', 'default',
'filter_logic', 'choices', 'description', 'filter_logic', 'choices',
) )
default_columns = ('pk', 'name', 'content_types', 'label', 'type', 'required', 'description') default_columns = ('pk', 'name', 'content_types', 'label', 'type', 'required', 'description')
@ -78,7 +78,8 @@ class CustomLinkTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = CustomLink model = CustomLink
fields = ( fields = (
'pk', 'name', 'content_type', 'link_text', 'link_url', 'weight', 'group_name', 'button_class', 'new_window', 'pk', 'id', 'name', 'content_type', 'link_text', 'link_url', 'weight', 'group_name',
'button_class', 'new_window',
) )
default_columns = ('pk', 'name', 'content_type', 'group_name', 'button_class', 'new_window') default_columns = ('pk', 'name', 'content_type', 'group_name', 'button_class', 'new_window')
@ -98,7 +99,7 @@ class ExportTemplateTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ExportTemplate model = ExportTemplate
fields = ( fields = (
'pk', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', 'pk', 'id', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
) )
default_columns = ( default_columns = (
'pk', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', 'pk', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
@ -132,7 +133,7 @@ class WebhookTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Webhook model = Webhook
fields = ( fields = (
'pk', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method', 'pk', 'id', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method',
'payload_url', 'secret', 'ssl_validation', 'ca_file_path', 'payload_url', 'secret', 'ssl_validation', 'ca_file_path',
) )
default_columns = ( default_columns = (
@ -155,10 +156,16 @@ class TagTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Tag model = Tag
fields = ('pk', 'name', 'items', 'slug', 'color', 'description', 'actions') fields = ('pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'actions')
default_columns = ('pk', 'name', 'items', 'slug', 'color', 'description', 'actions')
class TaggedItemTable(BaseTable): class TaggedItemTable(BaseTable):
id = tables.Column(
verbose_name='ID',
linkify=lambda record: record.content_object.get_absolute_url(),
accessor='content_object__id'
)
content_type = ContentTypeColumn( content_type = ContentTypeColumn(
verbose_name='Type' verbose_name='Type'
) )
@ -170,7 +177,7 @@ class TaggedItemTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = TaggedItem model = TaggedItem
fields = ('content_type', 'content_object') fields = ('id', 'content_type', 'content_object')
class ConfigContextTable(BaseTable): class ConfigContextTable(BaseTable):
@ -185,8 +192,8 @@ class ConfigContextTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ConfigContext model = ConfigContext
fields = ( fields = (
'pk', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles', 'platforms', 'pk', 'id', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles',
'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants',
) )
default_columns = ('pk', 'name', 'weight', 'is_active', 'description') default_columns = ('pk', 'name', 'weight', 'is_active', 'description')
@ -211,7 +218,7 @@ class ObjectChangeTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ObjectChange model = ObjectChange
fields = ('time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'request_id') fields = ('id', 'time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'request_id')
class ObjectJournalTable(BaseTable): class ObjectJournalTable(BaseTable):
@ -232,7 +239,7 @@ class ObjectJournalTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = JournalEntry model = JournalEntry
fields = ('created', 'created_by', 'kind', 'comments', 'actions') fields = ('id', 'created', 'created_by', 'kind', 'comments', 'actions')
class JournalEntryTable(ObjectJournalTable): class JournalEntryTable(ObjectJournalTable):
@ -250,5 +257,10 @@ class JournalEntryTable(ObjectJournalTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = JournalEntry model = JournalEntry
fields = ( fields = (
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments', 'actions' 'pk', 'id', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind',
'comments', 'actions'
)
default_columns = (
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind',
'comments', 'actions'
) )

View File

@ -1,3 +1,5 @@
import tempfile
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase from django.test import TestCase
from netaddr import IPAddress, IPNetwork from netaddr import IPAddress, IPNetwork
@ -11,6 +13,50 @@ CHOICES = (
('0000ff', 'Blue') ('0000ff', 'Blue')
) )
YAML_DATA = """
Foo: 123
Bar: 456
Baz:
- A
- B
- C
"""
JSON_DATA = """
{
"Foo": 123,
"Bar": 456,
"Baz": ["A", "B", "C"]
}
"""
class ScriptTest(TestCase):
def test_load_yaml(self):
datafile = tempfile.NamedTemporaryFile()
datafile.write(bytes(YAML_DATA, 'UTF-8'))
datafile.seek(0)
data = Script().load_yaml(datafile.name)
self.assertEqual(data, {
'Foo': 123,
'Bar': 456,
'Baz': ['A', 'B', 'C'],
})
def test_load_json(self):
datafile = tempfile.NamedTemporaryFile()
datafile.write(bytes(JSON_DATA, 'UTF-8'))
datafile.seek(0)
data = Script().load_json(datafile.name)
self.assertEqual(data, {
'Foo': 123,
'Bar': 456,
'Baz': ['A', 'B', 'C'],
})
class ScriptVariablesTest(TestCase): class ScriptVariablesTest(TestCase):

View File

@ -258,11 +258,18 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
device = self.cleaned_data.get('device') device = self.cleaned_data.get('device')
virtual_machine = self.cleaned_data.get('virtual_machine') virtual_machine = self.cleaned_data.get('virtual_machine')
interface = self.cleaned_data.get('interface')
is_primary = self.cleaned_data.get('is_primary') is_primary = self.cleaned_data.get('is_primary')
# Validate is_primary # Validate is_primary
if is_primary and not device and not virtual_machine: if is_primary and not device and not virtual_machine:
raise forms.ValidationError("No device or virtual machine specified; cannot set as primary IP") raise forms.ValidationError({
"is_primary": "No device or virtual machine specified; cannot set as primary IP"
})
if is_primary and not interface:
raise forms.ValidationError({
"is_primary": "No interface specified; cannot set as primary IP"
})
def save(self, *args, **kwargs): def save(self, *args, **kwargs):

View File

@ -92,7 +92,7 @@ class RIRTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RIR model = RIR
fields = ('pk', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'actions') fields = ('pk', 'id', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions') default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions')
@ -124,7 +124,7 @@ class AggregateTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Aggregate model = Aggregate
fields = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description', 'tags') fields = ('pk', 'id', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description', 'tags')
default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description') default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description')
@ -154,7 +154,7 @@ class RoleTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Role model = Role
fields = ('pk', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'tags', 'actions') fields = ('pk', 'id', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'tags', 'actions')
default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'actions') default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'actions')
@ -236,7 +236,7 @@ class PrefixTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Prefix model = Prefix
fields = ( fields = (
'pk', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role',
'is_pool', 'mark_utilized', 'description', 'tags', 'is_pool', 'mark_utilized', 'description', 'tags',
) )
default_columns = ( default_columns = (
@ -270,12 +270,15 @@ class IPRangeTable(BaseTable):
accessor='utilization', accessor='utilization',
orderable=False orderable=False
) )
tags = TagColumn(
url_name='ipam:iprange_list'
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = IPRange model = IPRange
fields = ( fields = (
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description', 'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
'utilization', 'utilization', 'tags',
) )
default_columns = ( default_columns = (
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description', 'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
@ -332,7 +335,7 @@ class IPAddressTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = IPAddress model = IPAddress
fields = ( fields = (
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'assigned', 'dns_name', 'description', 'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'assigned', 'dns_name', 'description',
'tags', 'tags',
) )
default_columns = ( default_columns = (
@ -356,6 +359,7 @@ class IPAddressAssignTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = IPAddress model = IPAddress
fields = ('address', 'dns_name', 'vrf', 'status', 'role', 'tenant', 'assigned_object', 'description') fields = ('address', 'dns_name', 'vrf', 'status', 'role', 'tenant', 'assigned_object', 'description')
exclude = ('id', )
orderable = False orderable = False
@ -380,3 +384,4 @@ class AssignedIPAddressesTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = IPAddress model = IPAddress
fields = ('address', 'vrf', 'status', 'role', 'tenant', 'description') fields = ('address', 'vrf', 'status', 'role', 'tenant', 'description')
exclude = ('id', )

View File

@ -31,5 +31,5 @@ class ServiceTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Service model = Service
fields = ('pk', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'tags') fields = ('pk', 'id', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'tags')
default_columns = ('pk', 'name', 'parent', 'protocol', 'ports', 'description') default_columns = ('pk', 'name', 'parent', 'protocol', 'ports', 'description')

View File

@ -84,7 +84,7 @@ class VLANGroupTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VLANGroup model = VLANGroup
fields = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'tags', 'actions') fields = ('pk', 'id', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions') default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions')
@ -122,7 +122,7 @@ class VLANTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VLAN model = VLAN
fields = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags') fields = ('pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags')
default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description') default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')
row_attrs = { row_attrs = {
'class': lambda record: 'success' if not isinstance(record, VLAN) else '', 'class': lambda record: 'success' if not isinstance(record, VLAN) else '',
@ -152,6 +152,7 @@ class VLANDevicesTable(VLANMembersTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Interface model = Interface
fields = ('device', 'name', 'tagged', 'actions') fields = ('device', 'name', 'tagged', 'actions')
exclude = ('id', )
class VLANVirtualMachinesTable(VLANMembersTable): class VLANVirtualMachinesTable(VLANMembersTable):
@ -163,6 +164,7 @@ class VLANVirtualMachinesTable(VLANMembersTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VMInterface model = VMInterface
fields = ('virtual_machine', 'name', 'tagged', 'actions') fields = ('virtual_machine', 'name', 'tagged', 'actions')
exclude = ('id', )
class InterfaceVLANTable(BaseTable): class InterfaceVLANTable(BaseTable):
@ -190,6 +192,7 @@ class InterfaceVLANTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VLAN model = VLAN
fields = ('vid', 'tagged', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description') fields = ('vid', 'tagged', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description')
exclude = ('id', )
def __init__(self, interface, *args, **kwargs): def __init__(self, interface, *args, **kwargs):
self.interface = interface self.interface = interface

View File

@ -47,7 +47,7 @@ class VRFTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VRF model = VRF
fields = ( fields = (
'pk', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'tags', 'pk', 'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'tags',
) )
default_columns = ('pk', 'name', 'rd', 'tenant', 'description') default_columns = ('pk', 'name', 'rd', 'tenant', 'description')
@ -68,5 +68,5 @@ class RouteTargetTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RouteTarget model = RouteTarget
fields = ('pk', 'name', 'tenant', 'description', 'tags') fields = ('pk', 'id', 'name', 'tenant', 'description', 'tags')
default_columns = ('pk', 'name', 'tenant', 'description') default_columns = ('pk', 'name', 'tenant', 'description')

View File

@ -69,7 +69,13 @@ SEARCH_TYPES = OrderedDict((
}), }),
('location', { ('location', {
'queryset': Location.objects.add_related_count( 'queryset': Location.objects.add_related_count(
Location.objects.all(), Location.objects.add_related_count(
Location.objects.all(),
Device,
'location',
'device_count',
cumulative=True
),
Rack, Rack,
'location', 'location',
'rack_count', 'rack_count',

View File

@ -40,11 +40,6 @@ class ChangeLoggingMixin(models.Model):
blank=True, blank=True,
null=True null=True
) )
object_changes = GenericRelation(
to='extras.ObjectChange',
content_type_field='changed_object_type',
object_id_field='changed_object_id'
)
class Meta: class Meta:
abstract = True abstract = True

View File

@ -777,8 +777,21 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
else: else:
pk_list = request.POST.getlist('pk') pk_list = request.POST.getlist('pk')
# Include the PK list as initial data for the form
initial_data = {'pk': pk_list}
# Check for other contextual data needed for the form. We avoid passing all of request.GET because the
# filter values will conflict with the bulk edit form fields.
# TODO: Find a better way to accomplish this
if 'device' in request.GET:
initial_data['device'] = request.GET.get('device')
elif 'device_type' in request.GET:
initial_data['device_type'] = request.GET.get('device_type')
elif 'virtual_machine' in request.GET:
initial_data['virtual_machine'] = request.GET.get('virtual_machine')
if '_apply' in request.POST: if '_apply' in request.POST:
form = self.form(model, request.POST) form = self.form(model, request.POST, initial=initial_data)
restrict_form_fields(form, request.user) restrict_form_fields(form, request.user)
if form.is_valid(): if form.is_valid():
@ -867,18 +880,6 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
logger.debug("Form validation failed") logger.debug("Form validation failed")
else: else:
# Include the PK list as initial data for the form
initial_data = {'pk': pk_list}
# Check for other contextual data needed for the form. We avoid passing all of request.GET because the
# filter values will conflict with the bulk edit form fields.
# TODO: Find a better way to accomplish this
if 'device' in request.GET:
initial_data['device'] = request.GET.get('device')
elif 'device_type' in request.GET:
initial_data['device_type'] = request.GET.get('device_type')
elif 'virtual_machine' in request.GET:
initial_data['virtual_machine'] = request.GET.get('virtual_machine')
form = self.form(model, initial=initial_data) form = self.form(model, initial=initial_data)
restrict_form_fields(form, request.user) restrict_form_fields(form, request.user)

View File

@ -27,55 +27,78 @@
<title>{% block title %}Home{% endblock %} | NetBox</title> <title>{% block title %}Home{% endblock %} | NetBox</title>
<script type="text/javascript"> <script type="text/javascript">
/** /**
* Set the color mode on the `<html/>` element and in local storage. * Set the color mode on the `<html/>` element and in local storage.
*/ *
function setMode(mode) { * @param mode {"dark" | "light"} NetBox Color Mode.
document.documentElement.setAttribute("data-netbox-color-mode", mode); * @param inferred {boolean} Value is inferred from browser/system preference.
localStorage.setItem("netbox-color-mode", mode); */
} function setMode(mode, inferred) {
/** document.documentElement.setAttribute("data-netbox-color-mode", mode);
* Determine the best initial color mode to use prior to rendering. localStorage.setItem("netbox-color-mode", mode);
*/ localStorage.setItem("netbox-color-mode-inferred", inferred);
(function () { }
try { /**
// Browser prefers dark color scheme. * Determine the best initial color mode to use prior to rendering.
var preferDark = window.matchMedia("(prefers-color-scheme: dark)").matches; */
// Browser prefers light color scheme. (function () {
var preferLight = window.matchMedia("(prefers-color-scheme: light)").matches; try {
// Client NetBox color-mode override. // Browser prefers dark color scheme.
var clientMode = localStorage.getItem("netbox-color-mode"); var preferDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
// NetBox server-rendered value. // Browser prefers light color scheme.
var serverMode = document.documentElement.getAttribute("data-netbox-color-mode"); var preferLight = window.matchMedia("(prefers-color-scheme: light)").matches;
// Client NetBox color-mode override.
if (clientMode === null && (serverMode === "light" || serverMode === "dark")) { var clientMode = localStorage.getItem("netbox-color-mode");
// If the client mode is not set but the server mode is, use the server mode. // NetBox server-rendered value.
return setMode(serverMode); var serverMode = document.documentElement.getAttribute("data-netbox-color-mode");
} // Color mode is inferred from browser/system preference and not deterministically set by
if (clientMode !== null && clientMode !== serverMode) { // the client or server.
// If the client mode is set and is different than the server mode, use the client mode var inferred = JSON.parse(localStorage.getItem("netbox-color-mode-inferred"));
// over the server mode, as it should be more recent.
return setMode(clientMode); if (inferred === true && (serverMode === "light" || serverMode === "dark")) {
} // The color mode was previously inferred from browser/system preference, but
if (clientMode === serverMode) { // the server now has a value, so we should use the server's value.
// If the client and server modes match, use that value. return setMode(serverMode, false);
return setMode(clientMode); }
} if (clientMode === null && (serverMode === "light" || serverMode === "dark")) {
if (preferDark && serverMode === "unset") { // If the client mode is not set but the server mode is, use the server mode.
// If the server mode is not set but the browser prefers dark mode, use dark mode. return setMode(serverMode, false);
return setMode("dark"); }
} if (clientMode !== null && serverMode === "unset") {
if (preferLight && serverMode === "unset") { // The color mode has been set, deterministically or otherwise, and the server
// If the server mode is not set but the browser prefers light mode, use light mode. // has no preference or has not been set. Use the client mode, but allow it to
return setMode("light"); /// be overridden by the server if/when a server value exists.
} return setMode(clientMode, true);
} catch (error) { }
// In the event of an error, log it to the console and set the mode to light mode. if (
console.error(error); clientMode !== null &&
} (serverMode === "light" || serverMode === "dark") &&
return setMode("light"); clientMode !== serverMode
})(); ) {
// If the client mode is set and is different than the server mode (which is also set),
// use the client mode over the server mode, as it should be more recent.
return setMode(clientMode, false);
}
if (clientMode === serverMode) {
// If the client and server modes match, use that value.
return setMode(clientMode, false);
}
if (preferDark && serverMode === "unset") {
// If the server mode is not set but the browser prefers dark mode, use dark mode, but
// allow it to be overridden by an explicit preference.
return setMode("dark", true);
}
if (preferLight && serverMode === "unset") {
// If the server mode is not set but the browser prefers light mode, use light mode,
// but allow it to be overridden by an explicit preference.
return setMode("light", true);
}
} catch (error) {
// In the event of an error, log it to the console and set the mode to light mode.
console.error(error);
}
return setMode("light", true);
})();
</script> </script>
{# Static resources #} {# Static resources #}

View File

@ -56,6 +56,13 @@
<tr> <tr>
<th scope="row">Racks</th> <th scope="row">Racks</th>
<td> <td>
{% if rack_count %}
<div class="float-end noprint">
<a href="{% url 'dcim:rack_elevation_list' %}?location_id={{ object.pk }}" class="btn btn-sm btn-primary" title="View elevations">
<i class="mdi mdi-server"></i>
</a>
</div>
{% endif %}
<a href="{% url 'dcim:rack_list' %}?location_id={{ object.pk }}">{{ rack_count }}</a> <a href="{% url 'dcim:rack_list' %}?location_id={{ object.pk }}">{{ rack_count }}</a>
</td> </td>
</tr> </tr>

View File

@ -10,7 +10,7 @@
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
{% for field, value in custom_fields.items %} {% for field, value in custom_fields.items %}
<tr> <tr>
<td><span title="{{ field.description }}">{{ field }}</span></td> <td><span title="{{ field.description|escape }}">{{ field }}</span></td>
<td> <td>
{% if field.type == 'longtext' and value %} {% if field.type == 'longtext' and value %}
{{ value|render_markdown }} {{ value|render_markdown }}

View File

@ -62,7 +62,7 @@ class TenantGroupTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = TenantGroup model = TenantGroup
fields = ('pk', 'name', 'tenant_count', 'description', 'slug', 'tags', 'actions') fields = ('pk', 'id', 'name', 'tenant_count', 'description', 'slug', 'tags', 'actions')
default_columns = ('pk', 'name', 'tenant_count', 'description', 'actions') default_columns = ('pk', 'name', 'tenant_count', 'description', 'actions')
@ -81,7 +81,7 @@ class TenantTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Tenant model = Tenant
fields = ('pk', 'name', 'slug', 'group', 'description', 'comments', 'tags') fields = ('pk', 'id', 'name', 'slug', 'group', 'description', 'comments', 'tags')
default_columns = ('pk', 'name', 'group', 'description') default_columns = ('pk', 'name', 'group', 'description')

View File

@ -68,17 +68,22 @@ def get_paginate_count(request):
""" """
config = get_config() config = get_config()
def _max_allowed(page_size):
if config.MAX_PAGE_SIZE:
return min(page_size, config.MAX_PAGE_SIZE)
return page_size
if 'per_page' in request.GET: if 'per_page' in request.GET:
try: try:
per_page = int(request.GET.get('per_page')) per_page = int(request.GET.get('per_page'))
if request.user.is_authenticated: if request.user.is_authenticated:
request.user.config.set('pagination.per_page', per_page, commit=True) request.user.config.set('pagination.per_page', per_page, commit=True)
return min(per_page, config.MAX_PAGE_SIZE) return _max_allowed(per_page)
except ValueError: except ValueError:
pass pass
if request.user.is_authenticated: if request.user.is_authenticated:
per_page = request.user.config.get('pagination.per_page', config.PAGINATE_COUNT) per_page = request.user.config.get('pagination.per_page', config.PAGINATE_COUNT)
return min(per_page, config.MAX_PAGE_SIZE) return _max_allowed(per_page)
return min(config.PAGINATE_COUNT, config.MAX_PAGE_SIZE) return _max_allowed(config.PAGINATE_COUNT)

View File

@ -23,6 +23,10 @@ class BaseTable(tables.Table):
:param user: Personalize table display for the given user (optional). Has no effect if AnonymousUser is passed. :param user: Personalize table display for the given user (optional). Has no effect if AnonymousUser is passed.
""" """
id = tables.Column(
linkify=True,
verbose_name='ID'
)
class Meta: class Meta:
attrs = { attrs = {

View File

@ -1,5 +1,4 @@
import django_tables2 as tables import django_tables2 as tables
from django.conf import settings
from dcim.tables.devices import BaseInterfaceTable from dcim.tables.devices import BaseInterfaceTable
from tenancy.tables import TenantColumn from tenancy.tables import TenantColumn
from utilities.tables import ( from utilities.tables import (
@ -45,7 +44,7 @@ class ClusterTypeTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ClusterType model = ClusterType
fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'tags', 'actions') fields = ('pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions') default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions')
@ -68,7 +67,7 @@ class ClusterGroupTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ClusterGroup model = ClusterGroup
fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'tags', 'actions') fields = ('pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions') default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions')
@ -104,7 +103,7 @@ class ClusterTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Cluster model = Cluster
fields = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'device_count', 'vm_count', 'tags') fields = ('pk', 'id', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'device_count', 'vm_count', 'tags')
default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count') default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count')
@ -144,7 +143,7 @@ class VirtualMachineTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VirtualMachine model = VirtualMachine
fields = ( fields = (
'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'primary_ip4', 'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'primary_ip4',
'primary_ip6', 'primary_ip', 'comments', 'tags', 'primary_ip6', 'primary_ip', 'comments', 'tags',
) )
default_columns = ( default_columns = (
@ -171,7 +170,7 @@ class VMInterfaceTable(BaseInterfaceTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VMInterface model = VMInterface
fields = ( fields = (
'pk', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags', 'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
) )
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description') default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
@ -193,7 +192,7 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VMInterface model = VMInterface
fields = ( fields = (
'pk', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags', 'pk', 'id', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags',
'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'actions', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'actions',
) )
default_columns = ( default_columns = (

View File

@ -1,4 +1,4 @@
Django==3.2.8 Django==3.2.9
django-cors-headers==3.10.0 django-cors-headers==3.10.0
django-debug-toolbar==3.2.2 django-debug-toolbar==3.2.2
django-filter==21.1 django-filter==21.1
@ -18,7 +18,7 @@ gunicorn==20.1.0
Jinja2==3.0.2 Jinja2==3.0.2
Markdown==3.3.4 Markdown==3.3.4
markdown-include==0.6.0 markdown-include==0.6.0
mkdocs-material==7.3.4 mkdocs-material==7.3.6
netaddr==0.8.0 netaddr==0.8.0
Pillow==8.4.0 Pillow==8.4.0
psycopg2-binary==2.9.1 psycopg2-binary==2.9.1
@ -26,7 +26,7 @@ PyYAML==6.0
social-auth-app-django==5.0.0 social-auth-app-django==5.0.0
social-auth-core==4.1.0 social-auth-core==4.1.0
svgwrite==1.4.1 svgwrite==1.4.1
tablib==3.0.0 tablib==3.1.0
# Workaround for #7401 # Workaround for #7401
jsonschema==3.2.0 jsonschema==3.2.0