1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge branch 'develop' into feature

This commit is contained in:
jeremystretch
2021-11-03 10:29:02 -04:00
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
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.)
placeholder: v3.0.8
placeholder: v3.0.9
validations:
required: true
- type: dropdown

View File

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

View File

@ -5,4 +5,4 @@
# 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
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
@ -259,6 +259,22 @@ http://netbox/api/extras/scripts/example.MyReport/ \
--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
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:
![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:

View File

@ -1,6 +1,29 @@
# 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.
"""
termination_name = f'termination_{instance.term_side.lower()}'
instance.circuit.refresh_from_db()
setattr(instance.circuit, termination_name, instance)
instance.circuit.save()

View File

@ -44,8 +44,8 @@ class ProviderTable(BaseTable):
class Meta(BaseTable.Meta):
model = Provider
fields = (
'pk', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count', 'comments',
'tags',
'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count',
'comments', 'tags',
)
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
@ -69,7 +69,7 @@ class ProviderNetworkTable(BaseTable):
class Meta(BaseTable.Meta):
model = ProviderNetwork
fields = ('pk', 'name', 'provider', 'description', 'comments', 'tags')
fields = ('pk', 'id', 'name', 'provider', 'description', 'comments', 'tags')
default_columns = ('pk', 'name', 'provider', 'description')
@ -92,7 +92,7 @@ class CircuitTypeTable(BaseTable):
class Meta(BaseTable.Meta):
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')
@ -104,7 +104,7 @@ class CircuitTable(BaseTable):
pk = ToggleColumn()
cid = tables.Column(
linkify=True,
verbose_name='ID'
verbose_name='Circuit ID'
)
provider = tables.Column(
linkify=True
@ -127,7 +127,7 @@ class CircuitTable(BaseTable):
class Meta(BaseTable.Meta):
model = Circuit
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',
)
default_columns = (

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ class PowerPanelTable(BaseTable):
class Meta(BaseTable.Meta):
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')
@ -70,7 +70,7 @@ class PowerFeedTable(CableTerminationTable):
class Meta(BaseTable.Meta):
model = PowerFeed
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',
'comments', 'tags',
)

View File

@ -31,7 +31,7 @@ class RackRoleTable(BaseTable):
class Meta(BaseTable.Meta):
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')
@ -79,7 +79,7 @@ class RackTable(BaseTable):
class Meta(BaseTable.Meta):
model = Rack
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',
)
default_columns = (
@ -118,7 +118,7 @@ class RackReservationTable(BaseTable):
class Meta(BaseTable.Meta):
model = RackReservation
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',
)
default_columns = (

View File

@ -36,7 +36,7 @@ class RegionTable(BaseTable):
class Meta(BaseTable.Meta):
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')
@ -61,7 +61,7 @@ class SiteGroupTable(BaseTable):
class Meta(BaseTable.Meta):
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')
@ -90,7 +90,7 @@ class SiteTable(BaseTable):
class Meta(BaseTable.Meta):
model = Site
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',
'contact_email', 'comments', 'tags',
)
@ -131,6 +131,7 @@ class LocationTable(BaseTable):
class Meta(BaseTable.Meta):
model = Location
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')

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.db import models
from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import mark_safe
from extras.choices import *
@ -306,7 +307,7 @@ class CustomField(ChangeLoggedModel):
field.model = self
field.label = str(self)
if self.description:
field.help_text = self.description
field.help_text = escape(self.description)
return field

View File

@ -4,7 +4,6 @@ import logging
import os
import pkgutil
import traceback
import warnings
from collections import OrderedDict
import yaml
@ -345,9 +344,14 @@ class BaseScript:
"""
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)
with open(file_path, 'r') as datafile:
data = yaml.load(datafile)
data = yaml.load(datafile, Loader=Loader)
return data

View File

@ -57,8 +57,8 @@ class CustomFieldTable(BaseTable):
class Meta(BaseTable.Meta):
model = CustomField
fields = (
'pk', 'name', 'content_types', 'label', 'type', 'required', 'weight', 'default', 'description',
'filter_logic', 'choices',
'pk', 'id', 'name', 'content_types', 'label', 'type', 'required', 'weight', 'default',
'description', 'filter_logic', 'choices',
)
default_columns = ('pk', 'name', 'content_types', 'label', 'type', 'required', 'description')
@ -78,7 +78,8 @@ class CustomLinkTable(BaseTable):
class Meta(BaseTable.Meta):
model = CustomLink
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')
@ -98,7 +99,7 @@ class ExportTemplateTable(BaseTable):
class Meta(BaseTable.Meta):
model = ExportTemplate
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 = (
'pk', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
@ -132,7 +133,7 @@ class WebhookTable(BaseTable):
class Meta(BaseTable.Meta):
model = Webhook
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',
)
default_columns = (
@ -155,10 +156,16 @@ class TagTable(BaseTable):
class Meta(BaseTable.Meta):
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):
id = tables.Column(
verbose_name='ID',
linkify=lambda record: record.content_object.get_absolute_url(),
accessor='content_object__id'
)
content_type = ContentTypeColumn(
verbose_name='Type'
)
@ -170,7 +177,7 @@ class TaggedItemTable(BaseTable):
class Meta(BaseTable.Meta):
model = TaggedItem
fields = ('content_type', 'content_object')
fields = ('id', 'content_type', 'content_object')
class ConfigContextTable(BaseTable):
@ -185,8 +192,8 @@ class ConfigContextTable(BaseTable):
class Meta(BaseTable.Meta):
model = ConfigContext
fields = (
'pk', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles', 'platforms',
'cluster_groups', 'clusters', 'tenant_groups', 'tenants',
'pk', 'id', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles',
'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants',
)
default_columns = ('pk', 'name', 'weight', 'is_active', 'description')
@ -211,7 +218,7 @@ class ObjectChangeTable(BaseTable):
class Meta(BaseTable.Meta):
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):
@ -232,7 +239,7 @@ class ObjectJournalTable(BaseTable):
class Meta(BaseTable.Meta):
model = JournalEntry
fields = ('created', 'created_by', 'kind', 'comments', 'actions')
fields = ('id', 'created', 'created_by', 'kind', 'comments', 'actions')
class JournalEntryTable(ObjectJournalTable):
@ -250,5 +257,10 @@ class JournalEntryTable(ObjectJournalTable):
class Meta(BaseTable.Meta):
model = JournalEntry
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.test import TestCase
from netaddr import IPAddress, IPNetwork
@ -11,6 +13,50 @@ CHOICES = (
('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):

View File

@ -258,11 +258,18 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
device = self.cleaned_data.get('device')
virtual_machine = self.cleaned_data.get('virtual_machine')
interface = self.cleaned_data.get('interface')
is_primary = self.cleaned_data.get('is_primary')
# Validate is_primary
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):

View File

@ -92,7 +92,7 @@ class RIRTable(BaseTable):
class Meta(BaseTable.Meta):
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')
@ -124,7 +124,7 @@ class AggregateTable(BaseTable):
class Meta(BaseTable.Meta):
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')
@ -154,7 +154,7 @@ class RoleTable(BaseTable):
class Meta(BaseTable.Meta):
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')
@ -236,7 +236,7 @@ class PrefixTable(BaseTable):
class Meta(BaseTable.Meta):
model = Prefix
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',
)
default_columns = (
@ -270,12 +270,15 @@ class IPRangeTable(BaseTable):
accessor='utilization',
orderable=False
)
tags = TagColumn(
url_name='ipam:iprange_list'
)
class Meta(BaseTable.Meta):
model = IPRange
fields = (
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
'utilization',
'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
'utilization', 'tags',
)
default_columns = (
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
@ -332,7 +335,7 @@ class IPAddressTable(BaseTable):
class Meta(BaseTable.Meta):
model = IPAddress
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',
)
default_columns = (
@ -356,6 +359,7 @@ class IPAddressAssignTable(BaseTable):
class Meta(BaseTable.Meta):
model = IPAddress
fields = ('address', 'dns_name', 'vrf', 'status', 'role', 'tenant', 'assigned_object', 'description')
exclude = ('id', )
orderable = False
@ -380,3 +384,4 @@ class AssignedIPAddressesTable(BaseTable):
class Meta(BaseTable.Meta):
model = IPAddress
fields = ('address', 'vrf', 'status', 'role', 'tenant', 'description')
exclude = ('id', )

View File

@ -31,5 +31,5 @@ class ServiceTable(BaseTable):
class Meta(BaseTable.Meta):
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')

View File

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

View File

@ -47,7 +47,7 @@ class VRFTable(BaseTable):
class Meta(BaseTable.Meta):
model = VRF
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')
@ -68,5 +68,5 @@ class RouteTargetTable(BaseTable):
class Meta(BaseTable.Meta):
model = RouteTarget
fields = ('pk', 'name', 'tenant', 'description', 'tags')
fields = ('pk', 'id', 'name', 'tenant', 'description', 'tags')
default_columns = ('pk', 'name', 'tenant', 'description')

View File

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

View File

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

View File

@ -777,8 +777,21 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
else:
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:
form = self.form(model, request.POST)
form = self.form(model, request.POST, initial=initial_data)
restrict_form_fields(form, request.user)
if form.is_valid():
@ -867,18 +880,6 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
logger.debug("Form validation failed")
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)
restrict_form_fields(form, request.user)

View File

@ -29,10 +29,14 @@
<script type="text/javascript">
/**
* Set the color mode on the `<html/>` element and in local storage.
*
* @param mode {"dark" | "light"} NetBox Color Mode.
* @param inferred {boolean} Value is inferred from browser/system preference.
*/
function setMode(mode) {
function setMode(mode, inferred) {
document.documentElement.setAttribute("data-netbox-color-mode", mode);
localStorage.setItem("netbox-color-mode", mode);
localStorage.setItem("netbox-color-mode-inferred", inferred);
}
/**
* Determine the best initial color mode to use prior to rendering.
@ -47,35 +51,54 @@
var clientMode = localStorage.getItem("netbox-color-mode");
// NetBox server-rendered value.
var serverMode = document.documentElement.getAttribute("data-netbox-color-mode");
// Color mode is inferred from browser/system preference and not deterministically set by
// the client or server.
var inferred = JSON.parse(localStorage.getItem("netbox-color-mode-inferred"));
if (inferred === true && (serverMode === "light" || serverMode === "dark")) {
// The color mode was previously inferred from browser/system preference, but
// the server now has a value, so we should use the server's value.
return setMode(serverMode, false);
}
if (clientMode === null && (serverMode === "light" || serverMode === "dark")) {
// If the client mode is not set but the server mode is, use the server mode.
return setMode(serverMode);
return setMode(serverMode, false);
}
if (clientMode !== null && clientMode !== serverMode) {
// If the client mode is set and is different than the server mode, use the client mode
// over the server mode, as it should be more recent.
return setMode(clientMode);
if (clientMode !== null && serverMode === "unset") {
// The color mode has been set, deterministically or otherwise, and the server
// has no preference or has not been set. Use the client mode, but allow it to
/// be overridden by the server if/when a server value exists.
return setMode(clientMode, true);
}
if (
clientMode !== null &&
(serverMode === "light" || serverMode === "dark") &&
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);
return setMode(clientMode, false);
}
if (preferDark && serverMode === "unset") {
// If the server mode is not set but the browser prefers dark mode, use dark mode.
return setMode("dark");
// 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.
return setMode("light");
// 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");
return setMode("light", true);
})();
</script>
{# Static resources #}

View File

@ -56,6 +56,13 @@
<tr>
<th scope="row">Racks</th>
<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>
</td>
</tr>

View File

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

View File

@ -62,7 +62,7 @@ class TenantGroupTable(BaseTable):
class Meta(BaseTable.Meta):
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')
@ -81,7 +81,7 @@ class TenantTable(BaseTable):
class Meta(BaseTable.Meta):
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')

View File

@ -68,17 +68,22 @@ def get_paginate_count(request):
"""
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:
try:
per_page = int(request.GET.get('per_page'))
if request.user.is_authenticated:
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:
pass
if request.user.is_authenticated:
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.
"""
id = tables.Column(
linkify=True,
verbose_name='ID'
)
class Meta:
attrs = {

View File

@ -1,5 +1,4 @@
import django_tables2 as tables
from django.conf import settings
from dcim.tables.devices import BaseInterfaceTable
from tenancy.tables import TenantColumn
from utilities.tables import (
@ -45,7 +44,7 @@ class ClusterTypeTable(BaseTable):
class Meta(BaseTable.Meta):
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')
@ -68,7 +67,7 @@ class ClusterGroupTable(BaseTable):
class Meta(BaseTable.Meta):
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')
@ -104,7 +103,7 @@ class ClusterTable(BaseTable):
class Meta(BaseTable.Meta):
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')
@ -144,7 +143,7 @@ class VirtualMachineTable(BaseTable):
class Meta(BaseTable.Meta):
model = VirtualMachine
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',
)
default_columns = (
@ -171,7 +170,7 @@ class VMInterfaceTable(BaseInterfaceTable):
class Meta(BaseTable.Meta):
model = VMInterface
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',
)
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
@ -193,7 +192,7 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable):
class Meta(BaseTable.Meta):
model = VMInterface
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',
)
default_columns = (

View File

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