mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
Merge branch 'develop' into feature
This commit is contained in:
commit
5b4dacf0f5
7
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
7
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -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: v2.11.3
|
placeholder: v2.11.4
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
@ -39,8 +39,9 @@ body:
|
|||||||
reproduce this bug using the current stable release of NetBox. Begin with the
|
reproduce this bug using the current stable release of NetBox. Begin with the
|
||||||
creation of any necessary database objects and call out every operation being
|
creation of any necessary database objects and call out every operation being
|
||||||
performed explicitly. If reporting a bug in the REST API, be sure to reconstruct
|
performed explicitly. If reporting a bug in the REST API, be sure to reconstruct
|
||||||
the raw HTTP request(s) being made: Don't rely on a client library such as
|
the raw HTTP request(s) being made: Don't rely on a client library such as
|
||||||
pynetbox."
|
pynetbox. Additionally, **do not rely on the demo instance** for reproducing
|
||||||
|
suspected bugs, as its data is prone to modification or deletion at any time.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1. Click on "create widget"
|
1. Click on "create widget"
|
||||||
2. Set foo to 12 and bar to G
|
2. Set foo to 12 and bar to G
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -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: v2.11.3
|
placeholder: v2.11.4
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -22,6 +22,8 @@ The complete documentation for NetBox can be found at [Read the Docs](https://ne
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<h4>Thank you to our sponsors!</h4>
|
<h4>Thank you to our sponsors!</h4>
|
||||||
|
|
||||||
|
[](https://try.digitalocean.com/developer-cloud)
|
||||||
|
|
||||||
[](https://ns1.com/)
|
[](https://ns1.com/)
|
||||||
|
|
||||||
[](https://stellar.tech/)
|
[](https://stellar.tech/)
|
||||||
|
@ -6,7 +6,7 @@ If a change is made to any of the objects returned by the query within that time
|
|||||||
|
|
||||||
## Invalidating Cached Data
|
## Invalidating Cached Data
|
||||||
|
|
||||||
Although caching is performed automatically and rarely requires administrative intervention, NetBox provides the `invalidate` management command to force invalidation of cached results. This command can reference a specific object my its type and numeric ID:
|
Although caching is performed automatically and rarely requires administrative intervention, NetBox provides the `invalidate` management command to force invalidation of cached results. This command can reference a specific object by its type and numeric ID:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
$ python netbox/manage.py invalidate dcim.Device.34
|
$ python netbox/manage.py invalidate dcim.Device.34
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Power Feed
|
# Power Feed
|
||||||
|
|
||||||
A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power pot (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks.
|
A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power port (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks.
|
||||||
|
|
||||||
Each power feed is assigned an operational type (primary or redundant) and one of the following statuses:
|
Each power feed is assigned an operational type (primary or redundant) and one of the following statuses:
|
||||||
|
|
||||||
|
@ -1,5 +1,28 @@
|
|||||||
# NetBox v2.11
|
# NetBox v2.11
|
||||||
|
|
||||||
|
## v2.11.4 (2021-05-25)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#5121](https://github.com/netbox-community/netbox/issues/5121) - Add content type filters for tags
|
||||||
|
* [#6358](https://github.com/netbox-community/netbox/issues/6358) - Add search field for VLAN groups
|
||||||
|
* [#6393](https://github.com/netbox-community/netbox/issues/6393) - Add `description` filter for IP addresses
|
||||||
|
* [#6400](https://github.com/netbox-community/netbox/issues/6400) - Add cyan color choice for plugin buttons
|
||||||
|
* [#6422](https://github.com/netbox-community/netbox/issues/6422) - Enable filtering users by group under admin UI
|
||||||
|
* [#6441](https://github.com/netbox-community/netbox/issues/6441) - Improve UI paginator to optimize page object count
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#6376](https://github.com/netbox-community/netbox/issues/6376) - Fix assignment of VLAN groups to clusters, cluster groups via REST API
|
||||||
|
* [#6398](https://github.com/netbox-community/netbox/issues/6398) - Avoid exception when deleting device connected to self via circuit
|
||||||
|
* [#6426](https://github.com/netbox-community/netbox/issues/6426) - Allow assigning virtual chassis member interfaces to LAG on VC master
|
||||||
|
* [#6438](https://github.com/netbox-community/netbox/issues/6438) - Fix missing descriptions and label for device type imports and exports
|
||||||
|
* [#6465](https://github.com/netbox-community/netbox/issues/6465) - Fix typo in installed plugins REST API endpoint
|
||||||
|
* [#6467](https://github.com/netbox-community/netbox/issues/6467) - Fix access to metrics on custom `BASE_PATH` when login is required
|
||||||
|
* [#6468](https://github.com/netbox-community/netbox/issues/6468) - Disable ordering VLAN groups list by scope object
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.11.3 (2021-05-07)
|
## v2.11.3 (2021-05-07)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
@ -20,7 +20,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Provider(PrimaryModel):
|
class Provider(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
||||||
@ -96,7 +96,7 @@ class Provider(PrimaryModel):
|
|||||||
# Provider networks
|
# Provider networks
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class ProviderNetwork(PrimaryModel):
|
class ProviderNetwork(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
This represents a provider network which exists outside of NetBox, the details of which are unknown or
|
This represents a provider network which exists outside of NetBox, the details of which are unknown or
|
||||||
@ -189,7 +189,7 @@ class CircuitType(OrganizationalModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Circuit(PrimaryModel):
|
class Circuit(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
||||||
|
@ -1818,7 +1818,7 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePortTemplate
|
model = ConsolePortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'name', 'label', 'type',
|
'device_type', 'name', 'label', 'type', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -1827,7 +1827,7 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPortTemplate
|
model = ConsoleServerPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'name', 'label', 'type',
|
'device_type', 'name', 'label', 'type', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -1836,7 +1836,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPortTemplate
|
model = PowerPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
|
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -1850,7 +1850,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutletTemplate
|
model = PowerOutletTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg',
|
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -1862,7 +1862,7 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceTemplate
|
model = InterfaceTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'name', 'label', 'type', 'mgmt_only',
|
'device_type', 'name', 'label', 'type', 'mgmt_only', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -1879,7 +1879,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'name', 'type', 'rear_port', 'rear_port_position',
|
'device_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -1891,7 +1891,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = RearPortTemplate
|
model = RearPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'name', 'type', 'positions',
|
'device_type', 'name', 'type', 'positions', 'label', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -1900,7 +1900,7 @@ class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceBayTemplate
|
model = DeviceBayTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'name',
|
'device_type', 'name', 'label', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -3150,9 +3150,13 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
|||||||
|
|
||||||
device = Device.objects.get(pk=self.data['device']) if self.is_bound else self.instance.device
|
device = Device.objects.get(pk=self.data['device']) if self.is_bound else self.instance.device
|
||||||
|
|
||||||
# Restrict parent/LAG interface assignment by device
|
# Restrict parent/LAG interface assignment by device/VC
|
||||||
self.fields['parent'].widget.add_query_param('device_id', device.pk)
|
self.fields['parent'].widget.add_query_param('device_id', device.pk)
|
||||||
self.fields['lag'].widget.add_query_param('device_id', device.pk)
|
if device.virtual_chassis and device.virtual_chassis.master:
|
||||||
|
# Get available LAG interfaces by VirtualChassis master
|
||||||
|
self.fields['lag'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
|
||||||
|
else:
|
||||||
|
self.fields['lag'].widget.add_query_param('device_id', device.pk)
|
||||||
|
|
||||||
# Limit VLAN choices by device
|
# Limit VLAN choices by device
|
||||||
self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)
|
self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)
|
||||||
|
@ -30,7 +30,7 @@ __all__ = (
|
|||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Cable(PrimaryModel):
|
class Cable(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A physical connection between two endpoints.
|
A physical connection between two endpoints.
|
||||||
|
@ -211,7 +211,7 @@ class PathEndpoint(models.Model):
|
|||||||
# Console ports
|
# Console ports
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
|
class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||||
@ -254,7 +254,7 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
|
|||||||
# Console server ports
|
# Console server ports
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
|
class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||||
@ -297,7 +297,7 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
|
|||||||
# Power ports
|
# Power ports
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||||
@ -408,7 +408,7 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
|||||||
# Power outlets
|
# Power outlets
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
|
class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||||
@ -512,7 +512,7 @@ class BaseInterface(models.Model):
|
|||||||
return self.ip_addresses.count()
|
return self.ip_addresses.count()
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
||||||
@ -683,7 +683,7 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
|||||||
# Pass-through ports
|
# Pass-through ports
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class FrontPort(ComponentModel, CableTermination):
|
class FrontPort(ComponentModel, CableTermination):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the front of a Device.
|
A pass-through port on the front of a Device.
|
||||||
@ -748,7 +748,7 @@ class FrontPort(ComponentModel, CableTermination):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class RearPort(ComponentModel, CableTermination):
|
class RearPort(ComponentModel, CableTermination):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the rear of a Device.
|
A pass-through port on the rear of a Device.
|
||||||
@ -801,7 +801,7 @@ class RearPort(ComponentModel, CableTermination):
|
|||||||
# Device bays
|
# Device bays
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class DeviceBay(ComponentModel):
|
class DeviceBay(ComponentModel):
|
||||||
"""
|
"""
|
||||||
An empty space within a Device which can house a child device
|
An empty space within a Device which can house a child device
|
||||||
@ -860,7 +860,7 @@ class DeviceBay(ComponentModel):
|
|||||||
# Inventory items
|
# Inventory items
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class InventoryItem(MPTTModel, ComponentModel):
|
class InventoryItem(MPTTModel, ComponentModel):
|
||||||
"""
|
"""
|
||||||
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
||||||
|
@ -75,7 +75,7 @@ class Manufacturer(OrganizationalModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class DeviceType(PrimaryModel):
|
class DeviceType(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
||||||
@ -183,6 +183,8 @@ class DeviceType(PrimaryModel):
|
|||||||
{
|
{
|
||||||
'name': c.name,
|
'name': c.name,
|
||||||
'type': c.type,
|
'type': c.type,
|
||||||
|
'label': c.label,
|
||||||
|
'description': c.description,
|
||||||
}
|
}
|
||||||
for c in self.consoleporttemplates.all()
|
for c in self.consoleporttemplates.all()
|
||||||
]
|
]
|
||||||
@ -191,6 +193,8 @@ class DeviceType(PrimaryModel):
|
|||||||
{
|
{
|
||||||
'name': c.name,
|
'name': c.name,
|
||||||
'type': c.type,
|
'type': c.type,
|
||||||
|
'label': c.label,
|
||||||
|
'description': c.description,
|
||||||
}
|
}
|
||||||
for c in self.consoleserverporttemplates.all()
|
for c in self.consoleserverporttemplates.all()
|
||||||
]
|
]
|
||||||
@ -201,6 +205,8 @@ class DeviceType(PrimaryModel):
|
|||||||
'type': c.type,
|
'type': c.type,
|
||||||
'maximum_draw': c.maximum_draw,
|
'maximum_draw': c.maximum_draw,
|
||||||
'allocated_draw': c.allocated_draw,
|
'allocated_draw': c.allocated_draw,
|
||||||
|
'label': c.label,
|
||||||
|
'description': c.description,
|
||||||
}
|
}
|
||||||
for c in self.powerporttemplates.all()
|
for c in self.powerporttemplates.all()
|
||||||
]
|
]
|
||||||
@ -211,6 +217,8 @@ class DeviceType(PrimaryModel):
|
|||||||
'type': c.type,
|
'type': c.type,
|
||||||
'power_port': c.power_port.name if c.power_port else None,
|
'power_port': c.power_port.name if c.power_port else None,
|
||||||
'feed_leg': c.feed_leg,
|
'feed_leg': c.feed_leg,
|
||||||
|
'label': c.label,
|
||||||
|
'description': c.description,
|
||||||
}
|
}
|
||||||
for c in self.poweroutlettemplates.all()
|
for c in self.poweroutlettemplates.all()
|
||||||
]
|
]
|
||||||
@ -220,6 +228,8 @@ class DeviceType(PrimaryModel):
|
|||||||
'name': c.name,
|
'name': c.name,
|
||||||
'type': c.type,
|
'type': c.type,
|
||||||
'mgmt_only': c.mgmt_only,
|
'mgmt_only': c.mgmt_only,
|
||||||
|
'label': c.label,
|
||||||
|
'description': c.description,
|
||||||
}
|
}
|
||||||
for c in self.interfacetemplates.all()
|
for c in self.interfacetemplates.all()
|
||||||
]
|
]
|
||||||
@ -230,6 +240,8 @@ class DeviceType(PrimaryModel):
|
|||||||
'type': c.type,
|
'type': c.type,
|
||||||
'rear_port': c.rear_port.name,
|
'rear_port': c.rear_port.name,
|
||||||
'rear_port_position': c.rear_port_position,
|
'rear_port_position': c.rear_port_position,
|
||||||
|
'label': c.label,
|
||||||
|
'description': c.description,
|
||||||
}
|
}
|
||||||
for c in self.frontporttemplates.all()
|
for c in self.frontporttemplates.all()
|
||||||
]
|
]
|
||||||
@ -239,6 +251,8 @@ class DeviceType(PrimaryModel):
|
|||||||
'name': c.name,
|
'name': c.name,
|
||||||
'type': c.type,
|
'type': c.type,
|
||||||
'positions': c.positions,
|
'positions': c.positions,
|
||||||
|
'label': c.label,
|
||||||
|
'description': c.description,
|
||||||
}
|
}
|
||||||
for c in self.rearporttemplates.all()
|
for c in self.rearporttemplates.all()
|
||||||
]
|
]
|
||||||
@ -246,6 +260,8 @@ class DeviceType(PrimaryModel):
|
|||||||
data['device-bays'] = [
|
data['device-bays'] = [
|
||||||
{
|
{
|
||||||
'name': c.name,
|
'name': c.name,
|
||||||
|
'label': c.label,
|
||||||
|
'description': c.description,
|
||||||
}
|
}
|
||||||
for c in self.devicebaytemplates.all()
|
for c in self.devicebaytemplates.all()
|
||||||
]
|
]
|
||||||
@ -448,7 +464,7 @@ class Platform(OrganizationalModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Device(PrimaryModel, ConfigContextModel):
|
class Device(PrimaryModel, ConfigContextModel):
|
||||||
"""
|
"""
|
||||||
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
||||||
@ -891,7 +907,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
|||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class VirtualChassis(PrimaryModel):
|
class VirtualChassis(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A collection of Devices which operate with a shared control plane (e.g. a switch stack).
|
A collection of Devices which operate with a shared control plane (e.g. a switch stack).
|
||||||
|
@ -21,7 +21,7 @@ __all__ = (
|
|||||||
# Power
|
# Power
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class PowerPanel(PrimaryModel):
|
class PowerPanel(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A distribution point for electrical power; e.g. a data center RPP.
|
A distribution point for electrical power; e.g. a data center RPP.
|
||||||
@ -71,7 +71,7 @@ class PowerPanel(PrimaryModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
|
class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
|
||||||
"""
|
"""
|
||||||
An electrical circuit delivered from a PowerPanel.
|
An electrical circuit delivered from a PowerPanel.
|
||||||
|
@ -78,7 +78,7 @@ class RackRole(OrganizationalModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Rack(PrimaryModel):
|
class Rack(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||||
@ -463,7 +463,7 @@ class Rack(PrimaryModel):
|
|||||||
return int(allocated_draw_total / available_power_total * 100)
|
return int(allocated_draw_total / available_power_total * 100)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class RackReservation(PrimaryModel):
|
class RackReservation(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
One or more reserved units within a Rack.
|
One or more reserved units within a Rack.
|
||||||
|
@ -130,7 +130,7 @@ class SiteGroup(NestedGroupModel):
|
|||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Site(PrimaryModel):
|
class Site(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
||||||
|
@ -31,9 +31,10 @@ def rebuild_paths(obj):
|
|||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for cp in cable_paths:
|
for cp in cable_paths:
|
||||||
invalidate_obj(cp.origin)
|
|
||||||
cp.delete()
|
cp.delete()
|
||||||
create_cablepath(cp.origin)
|
if cp.origin:
|
||||||
|
invalidate_obj(cp.origin)
|
||||||
|
create_cablepath(cp.origin)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -7,5 +7,6 @@ EXTRAS_FEATURES = [
|
|||||||
'custom_links',
|
'custom_links',
|
||||||
'export_templates',
|
'export_templates',
|
||||||
'job_results',
|
'job_results',
|
||||||
|
'tags',
|
||||||
'webhooks'
|
'webhooks'
|
||||||
]
|
]
|
||||||
|
@ -6,7 +6,7 @@ from django.db.models import Q
|
|||||||
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
|
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
|
||||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet
|
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.filters import ContentTypeFilter
|
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import *
|
from .models import *
|
||||||
@ -114,6 +114,12 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
|
content_type = MultiValueCharFilter(
|
||||||
|
method='_content_type'
|
||||||
|
)
|
||||||
|
content_type_id = MultiValueNumberFilter(
|
||||||
|
method='_content_type_id'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = Tag
|
||||||
@ -127,6 +133,32 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
Q(slug__icontains=value)
|
Q(slug__icontains=value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _content_type(self, queryset, name, values):
|
||||||
|
ct_filter = Q()
|
||||||
|
|
||||||
|
# Compile list of app_label & model pairings
|
||||||
|
for value in values:
|
||||||
|
try:
|
||||||
|
app_label, model = value.lower().split('.')
|
||||||
|
ct_filter |= Q(
|
||||||
|
app_label=app_label,
|
||||||
|
model=model
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get ContentType instances
|
||||||
|
content_types = ContentType.objects.filter(ct_filter)
|
||||||
|
|
||||||
|
return queryset.filter(extras_taggeditem_items__content_type__in=content_types).distinct()
|
||||||
|
|
||||||
|
def _content_type_id(self, queryset, name, values):
|
||||||
|
|
||||||
|
# Get ContentType instances
|
||||||
|
content_types = ContentType.objects.filter(pk__in=values)
|
||||||
|
|
||||||
|
return queryset.filter(extras_taggeditem_items__content_type__in=content_types).distinct()
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
|
class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
|
@ -8,12 +8,13 @@ from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGrou
|
|||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
|
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
|
||||||
CommentField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
|
CommentField, ContentTypeMultipleChoiceField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField,
|
||||||
BOOLEAN_WITH_BLANK_CHOICES,
|
JSONField, SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import ConfigContext, CustomField, ImageAttachment, JournalEntry, ObjectChange, Tag
|
from .models import ConfigContext, CustomField, ImageAttachment, JournalEntry, ObjectChange, Tag
|
||||||
|
from .utils import FeatureQuery
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -177,6 +178,15 @@ class AddRemoveTagsForm(forms.Form):
|
|||||||
|
|
||||||
class TagFilterForm(BootstrapMixin, forms.Form):
|
class TagFilterForm(BootstrapMixin, forms.Form):
|
||||||
model = Tag
|
model = Tag
|
||||||
|
q = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=_('Search')
|
||||||
|
)
|
||||||
|
content_type_id = ContentTypeMultipleChoiceField(
|
||||||
|
queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
|
||||||
|
required=False,
|
||||||
|
label=_('Tagged object type')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TagBulkEditForm(BootstrapMixin, BulkEditForm):
|
class TagBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
@ -42,7 +42,7 @@ class InstalledPluginsAPIView(APIView):
|
|||||||
'author': plugin_app_config.author,
|
'author': plugin_app_config.author,
|
||||||
'author_email': plugin_app_config.author_email,
|
'author_email': plugin_app_config.author_email,
|
||||||
'description': plugin_app_config.description,
|
'description': plugin_app_config.description,
|
||||||
'verison': plugin_app_config.version
|
'version': plugin_app_config.version
|
||||||
}
|
}
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
|
@ -5,6 +5,7 @@ from django.contrib.auth.models import User
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from circuits.models import Provider
|
||||||
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
|
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
|
||||||
from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
|
from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
|
||||||
from extras.filtersets import *
|
from extras.filtersets import *
|
||||||
@ -537,6 +538,13 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
Tag.objects.bulk_create(tags)
|
Tag.objects.bulk_create(tags)
|
||||||
|
|
||||||
|
# Apply some tags so we can filter by content type
|
||||||
|
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||||
|
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||||
|
|
||||||
|
site.tags.set(tags[0])
|
||||||
|
provider.tags.set(tags[1])
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
params = {'name': ['Tag 1', 'Tag 2']}
|
params = {'name': ['Tag 1', 'Tag 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
@ -549,6 +557,14 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'color': ['ff0000', '00ff00']}
|
params = {'color': ['ff0000', '00ff00']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_content_type(self):
|
||||||
|
params = {'content_type': ['dcim.site', 'circuits.provider']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
site_ct = ContentType.objects.get_for_model(Site).pk
|
||||||
|
provider_ct = ContentType.objects.get_for_model(Provider).pk
|
||||||
|
params = {'content_type_id': [site_ct, provider_ct]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
|
class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
|
||||||
queryset = ObjectChange.objects.all()
|
queryset = ObjectChange.objects.all()
|
||||||
|
@ -7,7 +7,7 @@ from rest_framework.validators import UniqueTogetherValidator
|
|||||||
|
|
||||||
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
|
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
|
||||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
||||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import OrganizationalModelSerializer
|
from netbox.api.serializers import OrganizationalModelSerializer
|
||||||
@ -115,8 +115,7 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
|
|||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
||||||
scope_type = ContentTypeField(
|
scope_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.filter(
|
queryset=ContentType.objects.filter(
|
||||||
app_label='dcim',
|
model__in=VLANGROUP_SCOPE_TYPES
|
||||||
model__in=['region', 'sitegroup', 'site', 'location', 'rack']
|
|
||||||
),
|
),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
@ -468,7 +468,7 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ['id', 'dns_name']
|
fields = ['id', 'dns_name', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -536,6 +536,10 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class VLANGroupFilterSet(OrganizationalModelFilterSet):
|
class VLANGroupFilterSet(OrganizationalModelFilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
scope_type = ContentTypeFilter()
|
scope_type = ContentTypeFilter()
|
||||||
region = django_filters.NumberFilter(
|
region = django_filters.NumberFilter(
|
||||||
method='filter_scope'
|
method='filter_scope'
|
||||||
@ -563,6 +567,15 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet):
|
|||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = ['id', 'name', 'slug', 'description', 'scope_id']
|
fields = ['id', 'name', 'slug', 'description', 'scope_id']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
qs_filter = (
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
|
)
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
def filter_scope(self, queryset, name, value):
|
def filter_scope(self, queryset, name, value):
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
scope_type=ContentType.objects.get(model=name),
|
scope_type=ContentType.objects.get(model=name),
|
||||||
|
@ -1291,6 +1291,10 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
|||||||
['region', 'sitegroup', 'site'],
|
['region', 'sitegroup', 'site'],
|
||||||
['location', 'rack']
|
['location', 'rack']
|
||||||
]
|
]
|
||||||
|
q = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=_('Search')
|
||||||
|
)
|
||||||
region = DynamicModelMultipleChoiceField(
|
region = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -77,7 +77,7 @@ class RIR(OrganizationalModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Aggregate(PrimaryModel):
|
class Aggregate(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
|
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
|
||||||
@ -228,7 +228,7 @@ class Role(OrganizationalModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Prefix(PrimaryModel):
|
class Prefix(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
|
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
|
||||||
@ -489,7 +489,7 @@ class Prefix(PrimaryModel):
|
|||||||
return int(float(child_count) / prefix_size * 100)
|
return int(float(child_count) / prefix_size * 100)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class IPAddress(PrimaryModel):
|
class IPAddress(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
|
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
|
||||||
|
@ -17,7 +17,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Service(PrimaryModel):
|
class Service(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
|
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
|
||||||
|
@ -100,7 +100,7 @@ class VLANGroup(OrganizationalModel):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class VLAN(PrimaryModel):
|
class VLAN(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
|
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
|
||||||
|
@ -13,7 +13,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class VRF(PrimaryModel):
|
class VRF(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
|
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
|
||||||
@ -88,7 +88,7 @@ class VRF(PrimaryModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class RouteTarget(PrimaryModel):
|
class RouteTarget(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364.
|
A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364.
|
||||||
|
@ -449,7 +449,8 @@ class VLANGroupTable(BaseTable):
|
|||||||
name = tables.Column(linkify=True)
|
name = tables.Column(linkify=True)
|
||||||
scope_type = ContentTypeColumn()
|
scope_type = ContentTypeColumn()
|
||||||
scope = tables.Column(
|
scope = tables.Column(
|
||||||
linkify=True
|
linkify=True,
|
||||||
|
orderable=False
|
||||||
)
|
)
|
||||||
vlan_count = LinkedCountColumn(
|
vlan_count = LinkedCountColumn(
|
||||||
viewname='ipam:vlan_list',
|
viewname='ipam:vlan_list',
|
||||||
|
@ -577,12 +577,12 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
ipaddresses = (
|
ipaddresses = (
|
||||||
IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a', description='foobar1'),
|
||||||
IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], assigned_object=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], assigned_object=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||||
IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], assigned_object=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], assigned_object=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||||
IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], assigned_object=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], assigned_object=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||||
IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||||
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a', description='foobar2'),
|
||||||
IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], assigned_object=vminterfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], assigned_object=vminterfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||||
IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], assigned_object=vminterfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], assigned_object=vminterfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||||
IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], assigned_object=vminterfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], assigned_object=vminterfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||||
@ -598,6 +598,10 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'dns_name': ['ipaddress-a', 'ipaddress-b']}
|
params = {'dns_name': ['ipaddress-a', 'ipaddress-b']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_parent(self):
|
def test_parent(self):
|
||||||
params = {'parent': '10.0.0.0/24'}
|
params = {'parent': '10.0.0.0/24'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||||
|
@ -20,17 +20,20 @@ class LoginRequiredMiddleware(object):
|
|||||||
self.get_response = get_response
|
self.get_response = get_response
|
||||||
|
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
|
# Redirect unauthenticated requests (except those exempted) to the login page if LOGIN_REQUIRED is true
|
||||||
if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
|
if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
|
||||||
# Redirect unauthenticated requests to the login page. API requests are exempt from redirection as the API
|
# Determine exempt paths
|
||||||
# performs its own authentication. Also metrics can be read without login.
|
exempt_paths = [
|
||||||
api_path = reverse('api-root')
|
reverse('api-root')
|
||||||
if not request.path_info.startswith((api_path, '/metrics')) and request.path_info != settings.LOGIN_URL:
|
]
|
||||||
return HttpResponseRedirect(
|
if settings.METRICS_ENABLED:
|
||||||
'{}?next={}'.format(
|
exempt_paths.append(reverse('prometheus-django-metrics'))
|
||||||
settings.LOGIN_URL,
|
|
||||||
parse.quote(request.get_full_path_info())
|
# Redirect unauthenticated requests
|
||||||
)
|
if not request.path_info.startswith(tuple(exempt_paths)) and request.path_info != settings.LOGIN_URL:
|
||||||
)
|
login_url = f'{settings.LOGIN_URL}?next={parse.quote(request.get_full_path_info())}'
|
||||||
|
return HttpResponseRedirect(login_url)
|
||||||
|
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ class TenantGroup(NestedGroupModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Tenant(PrimaryModel):
|
class Tenant(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
|
A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
|
||||||
|
@ -89,6 +89,7 @@ class UserAdmin(UserAdmin_):
|
|||||||
('Important dates', {'fields': ('last_login', 'date_joined')}),
|
('Important dates', {'fields': ('last_login', 'date_joined')}),
|
||||||
)
|
)
|
||||||
filter_horizontal = ('groups',)
|
filter_horizontal = ('groups',)
|
||||||
|
list_filter = ('is_active', 'is_staff', 'is_superuser', 'groups__name')
|
||||||
|
|
||||||
def get_inlines(self, request, obj):
|
def get_inlines(self, request, obj):
|
||||||
if obj is not None:
|
if obj is not None:
|
||||||
|
@ -130,22 +130,24 @@ class ColorChoices(ChoiceSet):
|
|||||||
|
|
||||||
class ButtonColorChoices(ChoiceSet):
|
class ButtonColorChoices(ChoiceSet):
|
||||||
"""
|
"""
|
||||||
Map standard button color choices to Bootstrap color classes
|
Map standard button color choices to Bootstrap 3 button classes
|
||||||
"""
|
"""
|
||||||
DEFAULT = 'outline-dark'
|
DEFAULT = 'outline-dark'
|
||||||
BLUE = 'primary'
|
BLUE = 'primary'
|
||||||
GREY = 'secondary'
|
CYAN = 'info'
|
||||||
GREEN = 'success'
|
GREEN = 'success'
|
||||||
RED = 'danger'
|
RED = 'danger'
|
||||||
YELLOW = 'warning'
|
YELLOW = 'warning'
|
||||||
|
GREY = 'secondary'
|
||||||
BLACK = 'dark'
|
BLACK = 'dark'
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(DEFAULT, 'Default'),
|
(DEFAULT, 'Default'),
|
||||||
(BLUE, 'Blue'),
|
(BLUE, 'Blue'),
|
||||||
(GREY, 'Grey'),
|
(CYAN, 'Cyan'),
|
||||||
(GREEN, 'Green'),
|
(GREEN, 'Green'),
|
||||||
(RED, 'Red'),
|
(RED, 'Red'),
|
||||||
(YELLOW, 'Yellow'),
|
(YELLOW, 'Yellow'),
|
||||||
|
(GREY, 'Grey'),
|
||||||
(BLACK, 'Black')
|
(BLACK, 'Black')
|
||||||
)
|
)
|
||||||
|
@ -4,7 +4,9 @@ from django.core.paginator import Paginator, Page
|
|||||||
|
|
||||||
class EnhancedPaginator(Paginator):
|
class EnhancedPaginator(Paginator):
|
||||||
|
|
||||||
def __init__(self, object_list, per_page, **kwargs):
|
def __init__(self, object_list, per_page, orphans=None, **kwargs):
|
||||||
|
|
||||||
|
# Determine the page size
|
||||||
try:
|
try:
|
||||||
per_page = int(per_page)
|
per_page = int(per_page)
|
||||||
if per_page < 1:
|
if per_page < 1:
|
||||||
@ -12,7 +14,13 @@ class EnhancedPaginator(Paginator):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
per_page = settings.PAGINATE_COUNT
|
per_page = settings.PAGINATE_COUNT
|
||||||
|
|
||||||
super().__init__(object_list, per_page, **kwargs)
|
# Set orphans count based on page size
|
||||||
|
if orphans is None and per_page <= 50:
|
||||||
|
orphans = 5
|
||||||
|
elif orphans is None:
|
||||||
|
orphans = 10
|
||||||
|
|
||||||
|
super().__init__(object_list, per_page, orphans=orphans, **kwargs)
|
||||||
|
|
||||||
def _get_page(self, *args, **kwargs):
|
def _get_page(self, *args, **kwargs):
|
||||||
return EnhancedPage(*args, **kwargs)
|
return EnhancedPage(*args, **kwargs)
|
||||||
|
@ -5,7 +5,6 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.core.exceptions import FieldDoesNotExist
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
from django.db.models.fields.related import RelatedField
|
from django.db.models.fields.related import RelatedField
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.html import strip_tags
|
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
from django_tables2.data import TableQuerysetData
|
from django_tables2.data import TableQuerysetData
|
||||||
@ -15,19 +14,6 @@ from extras.models import CustomField
|
|||||||
from .paginator import EnhancedPaginator, get_paginate_count
|
from .paginator import EnhancedPaginator, get_paginate_count
|
||||||
|
|
||||||
|
|
||||||
def stripped_value(self, **kwargs):
|
|
||||||
"""
|
|
||||||
Replaces TemplateColumn's value() method to both strip HTML tags and remove any leading/trailing whitespace.
|
|
||||||
"""
|
|
||||||
html = super(tables.TemplateColumn, self).value(**kwargs)
|
|
||||||
return strip_tags(html).strip() if isinstance(html, str) else html
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: We're monkey-patching TemplateColumn here to strip leading/trailing whitespace. This will no longer
|
|
||||||
# be necessary under django-tables2 v2.3.5+. (See #5926)
|
|
||||||
tables.TemplateColumn.value = stripped_value
|
|
||||||
|
|
||||||
|
|
||||||
class BaseTable(tables.Table):
|
class BaseTable(tables.Table):
|
||||||
"""
|
"""
|
||||||
Default table for object lists
|
Default table for object lists
|
||||||
|
@ -116,7 +116,7 @@ class ClusterGroup(OrganizationalModel):
|
|||||||
# Clusters
|
# Clusters
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Cluster(PrimaryModel):
|
class Cluster(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
|
A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
|
||||||
@ -199,7 +199,7 @@ class Cluster(PrimaryModel):
|
|||||||
# Virtual machines
|
# Virtual machines
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class VirtualMachine(PrimaryModel, ConfigContextModel):
|
class VirtualMachine(PrimaryModel, ConfigContextModel):
|
||||||
"""
|
"""
|
||||||
A virtual machine which runs inside a Cluster.
|
A virtual machine which runs inside a Cluster.
|
||||||
@ -374,7 +374,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
|
|||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class VMInterface(PrimaryModel, BaseInterface):
|
class VMInterface(PrimaryModel, BaseInterface):
|
||||||
virtual_machine = models.ForeignKey(
|
virtual_machine = models.ForeignKey(
|
||||||
to='virtualization.VirtualMachine',
|
to='virtualization.VirtualMachine',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Django==3.2.2
|
Django==3.2.3
|
||||||
django-cacheops==6.0
|
django-cacheops==6.0
|
||||||
django-cors-headers==3.7.0
|
django-cors-headers==3.7.0
|
||||||
django-debug-toolbar==3.2.1
|
django-debug-toolbar==3.2.1
|
||||||
@ -7,13 +7,13 @@ django-mptt==0.12.0
|
|||||||
django-pglocks==1.0.4
|
django-pglocks==1.0.4
|
||||||
django-prometheus==2.1.0
|
django-prometheus==2.1.0
|
||||||
django-rq==2.4.1
|
django-rq==2.4.1
|
||||||
django-tables2==2.3.4
|
django-tables2==2.4.0
|
||||||
django-taggit==1.4.0
|
django-taggit==1.4.0
|
||||||
django-timezone-field==4.1.2
|
django-timezone-field==4.1.2
|
||||||
djangorestframework==3.12.4
|
djangorestframework==3.12.4
|
||||||
drf-yasg[validation]==1.20.0
|
drf-yasg[validation]==1.20.0
|
||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
Jinja2==2.11.3
|
Jinja2==3.0.1
|
||||||
Markdown==3.3.4
|
Markdown==3.3.4
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==8.2.0
|
Pillow==8.2.0
|
||||||
|
Loading…
Reference in New Issue
Block a user