mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
commit
b72793a85a
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.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: v3.2.5
|
placeholder: v3.2.6
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
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: v3.2.5
|
placeholder: v3.2.6
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
6
NOTICE
6
NOTICE
@ -1 +1,7 @@
|
|||||||
Copyrighted and licensed under Apache License 2.0 by DigitalOcean, LLC.
|
Copyrighted and licensed under Apache License 2.0 by DigitalOcean, LLC.
|
||||||
|
|
||||||
|
This project contains code developed expressly for NetBox, and its reuse in
|
||||||
|
other projects may introduce issues affecting performance, data integrity,
|
||||||
|
and security.
|
||||||
|
|
||||||
|
For more information, please see https://github.com/netbox-community/netbox.
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
# HTML sanitizer
|
||||||
|
# https://github.com/mozilla/bleach
|
||||||
|
bleach
|
||||||
|
|
||||||
# The Python web framework on which NetBox is built
|
# The Python web framework on which NetBox is built
|
||||||
# https://github.com/django/django
|
# https://github.com/django/django
|
||||||
Django
|
Django
|
||||||
@ -126,7 +130,3 @@ tablib
|
|||||||
# Timezone data (required by django-timezone-field on Python 3.9+)
|
# Timezone data (required by django-timezone-field on Python 3.9+)
|
||||||
# https://github.com/python/tzdata
|
# https://github.com/python/tzdata
|
||||||
tzdata
|
tzdata
|
||||||
|
|
||||||
# HTML sanitizer
|
|
||||||
# https://github.com/mozilla/bleach
|
|
||||||
bleach
|
|
@ -43,18 +43,6 @@ changes in the database indefinitely.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## JOBRESULT_RETENTION
|
|
||||||
|
|
||||||
Default: 90
|
|
||||||
|
|
||||||
The number of days to retain job results (scripts and reports). Set this to `0` to retain
|
|
||||||
job results in the database indefinitely.
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CUSTOM_VALIDATORS
|
## CUSTOM_VALIDATORS
|
||||||
|
|
||||||
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:
|
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:
|
||||||
@ -110,6 +98,18 @@ Setting this to False will disable the GraphQL API.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## JOBRESULT_RETENTION
|
||||||
|
|
||||||
|
Default: 90
|
||||||
|
|
||||||
|
The number of days to retain job results (scripts and reports). Set this to `0` to retain
|
||||||
|
job results in the database indefinitely.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## MAINTENANCE_MODE
|
## MAINTENANCE_MODE
|
||||||
|
|
||||||
Default: False
|
Default: False
|
||||||
@ -185,6 +185,30 @@ The default maximum number of objects to display per page within each list of ob
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## POWERFEED_DEFAULT_AMPERAGE
|
||||||
|
|
||||||
|
Default: 15
|
||||||
|
|
||||||
|
The default value for the `amperage` field when creating new power feeds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## POWERFEED_DEFAULT_MAX_UTILIZATION
|
||||||
|
|
||||||
|
Default: 80
|
||||||
|
|
||||||
|
The default value (percentage) for the `max_utilization` field when creating new power feeds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## POWERFEED_DEFAULT_VOLTAGE
|
||||||
|
|
||||||
|
Default: 120
|
||||||
|
|
||||||
|
The default value for the `voltage` field when creating new power feeds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## PREFER_IPV4
|
## PREFER_IPV4
|
||||||
|
|
||||||
Default: False
|
Default: False
|
||||||
|
@ -10,7 +10,7 @@ Within the database, custom fields are stored as JSON data directly alongside ea
|
|||||||
|
|
||||||
Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field:
|
Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field:
|
||||||
|
|
||||||
* Text: Free-form text (up to 255 characters)
|
* Text: Free-form text (intended for single-line use)
|
||||||
* Long text: Free-form of any length; supports Markdown rendering
|
* Long text: Free-form of any length; supports Markdown rendering
|
||||||
* Integer: A whole number (positive or negative)
|
* Integer: A whole number (positive or negative)
|
||||||
* Boolean: True or false
|
* Boolean: True or false
|
||||||
|
@ -1,5 +1,28 @@
|
|||||||
# NetBox v3.2
|
# NetBox v3.2
|
||||||
|
|
||||||
|
## v3.2.6 (2022-07-11)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#7702](https://github.com/netbox-community/netbox/issues/7702) - Enable dynamic configuration for default powerfeed attributes
|
||||||
|
* [#9396](https://github.com/netbox-community/netbox/issues/9396) - Allow filtering modules by bay ID
|
||||||
|
* [#9403](https://github.com/netbox-community/netbox/issues/9403) - Enable modifying virtual chassis properties when creating/editing a device
|
||||||
|
* [#9540](https://github.com/netbox-community/netbox/issues/9540) - Add filters for assigned device & VM to IP addresses list
|
||||||
|
* [#9686](https://github.com/netbox-community/netbox/issues/9686) - Add tenant group column for all object tables with tenant assignments
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#8854](https://github.com/netbox-community/netbox/issues/8854) - Fix `REMOTE_AUTH_DEFAULT_GROUPS` for social-auth backends
|
||||||
|
* [#9575](https://github.com/netbox-community/netbox/issues/9575) - Fix AttributeError exception for FHRP group with an IP address assigned
|
||||||
|
* [#9597](https://github.com/netbox-community/netbox/issues/9597) - Include `installed_module` in module bay REST API serializer
|
||||||
|
* [#9632](https://github.com/netbox-community/netbox/issues/9632) - Automatically focus on search box when expanding dropdowns
|
||||||
|
* [#9657](https://github.com/netbox-community/netbox/issues/9657) - Fix filtering for custom fields and webhooks in the UI
|
||||||
|
* [#9682](https://github.com/netbox-community/netbox/issues/9682) - Fix bulk assignment of ASNs to sites
|
||||||
|
* [#9687](https://github.com/netbox-community/netbox/issues/9687) - Don't restrict custom text field lengths when entering via UI form
|
||||||
|
* [#9704](https://github.com/netbox-community/netbox/issues/9704) - Include `last_updated` field on JournalEntry REST API serializer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v3.2.5 (2022-06-20)
|
## v3.2.5 (2022-06-20)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
@ -25,7 +48,7 @@
|
|||||||
* [#9484](https://github.com/netbox-community/netbox/issues/9484) - Include services listening on "all IPs" under IP address view
|
* [#9484](https://github.com/netbox-community/netbox/issues/9484) - Include services listening on "all IPs" under IP address view
|
||||||
* [#9486](https://github.com/netbox-community/netbox/issues/9486) - Fix redirect URL when adding device components from the module view
|
* [#9486](https://github.com/netbox-community/netbox/issues/9486) - Fix redirect URL when adding device components from the module view
|
||||||
* [#9495](https://github.com/netbox-community/netbox/issues/9495) - Correct link to contacts in contact groups table column
|
* [#9495](https://github.com/netbox-community/netbox/issues/9495) - Correct link to contacts in contact groups table column
|
||||||
* [#9503](https://github.com/netbox-community/netbox/issues/9503) - Hyperlinks in ack elevation SVGs must always use absolute URLs
|
* [#9503](https://github.com/netbox-community/netbox/issues/9503) - Hyperlinks in rack elevation SVGs must always use absolute URLs
|
||||||
* [#9512](https://github.com/netbox-community/netbox/issues/9512) - Fix duplicate site results when searching by ASN
|
* [#9512](https://github.com/netbox-community/netbox/issues/9512) - Fix duplicate site results when searching by ASN
|
||||||
* [#9524](https://github.com/netbox-community/netbox/issues/9524) - Correct order of VLAN fields under VM interface creation form
|
* [#9524](https://github.com/netbox-community/netbox/issues/9524) - Correct order of VLAN fields under VM interface creation form
|
||||||
* [#9537](https://github.com/netbox-community/netbox/issues/9537) - Ensure consistent use of placeholder tag throughout UI
|
* [#9537](https://github.com/netbox-community/netbox/issues/9537) - Ensure consistent use of placeholder tag throughout UI
|
||||||
|
@ -2,7 +2,7 @@ import django_tables2 as tables
|
|||||||
|
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenancyColumnsMixin
|
||||||
from .columns import CommitRateColumn
|
from .columns import CommitRateColumn
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -39,7 +39,7 @@ class CircuitTypeTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
|
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
|
||||||
|
|
||||||
|
|
||||||
class CircuitTable(NetBoxTable):
|
class CircuitTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
cid = tables.Column(
|
cid = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='Circuit ID'
|
verbose_name='Circuit ID'
|
||||||
@ -48,7 +48,6 @@ class CircuitTable(NetBoxTable):
|
|||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
status = columns.ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
tenant = TenantColumn()
|
|
||||||
termination_a = tables.TemplateColumn(
|
termination_a = tables.TemplateColumn(
|
||||||
template_code=CIRCUITTERMINATION_LINK,
|
template_code=CIRCUITTERMINATION_LINK,
|
||||||
verbose_name='Side A'
|
verbose_name='Side A'
|
||||||
@ -69,7 +68,7 @@ class CircuitTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
|
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'tenant_group', 'termination_a', 'termination_z', 'install_date',
|
||||||
'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
|
'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
|
@ -30,7 +30,7 @@ class ProviderView(generic.ObjectView):
|
|||||||
circuits = Circuit.objects.restrict(request.user, 'view').filter(
|
circuits = Circuit.objects.restrict(request.user, 'view').filter(
|
||||||
provider=instance
|
provider=instance
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'type', 'tenant', 'terminations__site'
|
'type', 'tenant', 'tenant__group', 'terminations__site'
|
||||||
)
|
)
|
||||||
circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('provider',))
|
circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('provider',))
|
||||||
circuits_table.configure(request)
|
circuits_table.configure(request)
|
||||||
@ -91,7 +91,7 @@ class ProviderNetworkView(generic.ObjectView):
|
|||||||
Q(termination_a__provider_network=instance.pk) |
|
Q(termination_a__provider_network=instance.pk) |
|
||||||
Q(termination_z__provider_network=instance.pk)
|
Q(termination_z__provider_network=instance.pk)
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'type', 'tenant', 'terminations__site'
|
'type', 'tenant', 'tenant__group', 'terminations__site'
|
||||||
)
|
)
|
||||||
circuits_table = tables.CircuitTable(circuits, user=request.user)
|
circuits_table = tables.CircuitTable(circuits, user=request.user)
|
||||||
circuits_table.configure(request)
|
circuits_table.configure(request)
|
||||||
@ -192,7 +192,7 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
|||||||
|
|
||||||
class CircuitListView(generic.ObjectListView):
|
class CircuitListView(generic.ObjectListView):
|
||||||
queryset = Circuit.objects.prefetch_related(
|
queryset = Circuit.objects.prefetch_related(
|
||||||
'provider', 'type', 'tenant', 'termination_a', 'termination_z'
|
'provider', 'type', 'tenant', 'tenant__group', 'termination_a', 'termination_z'
|
||||||
)
|
)
|
||||||
filterset = filtersets.CircuitFilterSet
|
filterset = filtersets.CircuitFilterSet
|
||||||
filterset_form = forms.CircuitFilterForm
|
filterset_form = forms.CircuitFilterForm
|
||||||
|
@ -5,6 +5,7 @@ from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'ComponentNestedModuleSerializer',
|
'ComponentNestedModuleSerializer',
|
||||||
|
'ModuleBayNestedModuleSerializer',
|
||||||
'NestedCableSerializer',
|
'NestedCableSerializer',
|
||||||
'NestedConsolePortSerializer',
|
'NestedConsolePortSerializer',
|
||||||
'NestedConsolePortTemplateSerializer',
|
'NestedConsolePortTemplateSerializer',
|
||||||
@ -281,6 +282,14 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name']
|
fields = ['id', 'url', 'display', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Module
|
||||||
|
fields = ['id', 'url', 'display', 'serial']
|
||||||
|
|
||||||
|
|
||||||
class ComponentNestedModuleSerializer(WritableNestedSerializer):
|
class ComponentNestedModuleSerializer(WritableNestedSerializer):
|
||||||
"""
|
"""
|
||||||
Used by device component serializers.
|
Used by device component serializers.
|
||||||
|
@ -886,12 +886,12 @@ class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
|
|||||||
class ModuleBaySerializer(NetBoxModelSerializer):
|
class ModuleBaySerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
# installed_module = NestedModuleSerializer(required=False, allow_null=True)
|
installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleBay
|
model = ModuleBay
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device', 'name', 'label', 'position', 'description', 'tags', 'custom_fields',
|
'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags', 'custom_fields',
|
||||||
'created', 'last_updated',
|
'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -611,7 +611,7 @@ class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleBayViewSet(NetBoxModelViewSet):
|
class ModuleBayViewSet(NetBoxModelViewSet):
|
||||||
queryset = ModuleBay.objects.prefetch_related('tags')
|
queryset = ModuleBay.objects.prefetch_related('tags', 'installed_module')
|
||||||
serializer_class = serializers.ModuleBaySerializer
|
serializer_class = serializers.ModuleBaySerializer
|
||||||
filterset_class = filtersets.ModuleBayFilterSet
|
filterset_class = filtersets.ModuleBayFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
brief_prefetch_fields = ['device']
|
||||||
|
@ -49,15 +49,6 @@ WIRELESS_IFACE_TYPES = [
|
|||||||
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Power feeds
|
|
||||||
#
|
|
||||||
|
|
||||||
POWERFEED_VOLTAGE_DEFAULT = 120
|
|
||||||
POWERFEED_AMPERAGE_DEFAULT = 20
|
|
||||||
POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device components
|
# Device components
|
||||||
#
|
#
|
||||||
|
@ -992,6 +992,12 @@ class ModuleFilterSet(NetBoxModelFilterSet):
|
|||||||
to_field_name='model',
|
to_field_name='model',
|
||||||
label='Module type (model)',
|
label='Module type (model)',
|
||||||
)
|
)
|
||||||
|
module_bay_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='module_bay',
|
||||||
|
queryset=ModuleBay.objects.all(),
|
||||||
|
to_field_name='id',
|
||||||
|
label='Module Bay (ID)'
|
||||||
|
)
|
||||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
|
@ -521,13 +521,28 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=''
|
label=''
|
||||||
)
|
)
|
||||||
|
virtual_chassis = DynamicModelChoiceField(
|
||||||
|
queryset=VirtualChassis.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
vc_position = forms.IntegerField(
|
||||||
|
required=False,
|
||||||
|
label='Position',
|
||||||
|
help_text="The position in the virtual chassis this device is identified by"
|
||||||
|
)
|
||||||
|
vc_priority = forms.IntegerField(
|
||||||
|
required=False,
|
||||||
|
label='Priority',
|
||||||
|
help_text="The priority of the device in the virtual chassis"
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
|
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
|
||||||
'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6',
|
'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6',
|
||||||
'cluster_group', 'cluster', 'tenant_group', 'tenant', 'comments', 'tags', 'local_context_data'
|
'cluster_group', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority',
|
||||||
|
'comments', 'tags', 'local_context_data'
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'device_role': "The function this device serves",
|
'device_role': "The function this device serves",
|
||||||
|
@ -386,9 +386,9 @@ class Migration(migrations.Migration):
|
|||||||
('type', models.CharField(default='primary', max_length=50)),
|
('type', models.CharField(default='primary', max_length=50)),
|
||||||
('supply', models.CharField(default='ac', max_length=50)),
|
('supply', models.CharField(default='ac', max_length=50)),
|
||||||
('phase', models.CharField(default='single-phase', max_length=50)),
|
('phase', models.CharField(default='single-phase', max_length=50)),
|
||||||
('voltage', models.SmallIntegerField(default=120, validators=[utilities.validators.ExclusionValidator([0])])),
|
('voltage', models.SmallIntegerField(validators=[utilities.validators.ExclusionValidator([0])])),
|
||||||
('amperage', models.PositiveSmallIntegerField(default=20, validators=[django.core.validators.MinValueValidator(1)])),
|
('amperage', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1)])),
|
||||||
('max_utilization', models.PositiveSmallIntegerField(default=80, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])),
|
('max_utilization', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])),
|
||||||
('available_power', models.PositiveIntegerField(default=0, editable=False)),
|
('available_power', models.PositiveIntegerField(default=0, editable=False)),
|
||||||
('comments', models.TextField(blank=True)),
|
('comments', models.TextField(blank=True)),
|
||||||
],
|
],
|
||||||
|
@ -95,8 +95,7 @@ class ModularComponentModel(ComponentModel):
|
|||||||
inventory_items = GenericRelation(
|
inventory_items = GenericRelation(
|
||||||
to='dcim.InventoryItem',
|
to='dcim.InventoryItem',
|
||||||
content_type_field='component_type',
|
content_type_field='component_type',
|
||||||
object_id_field='component_id',
|
object_id_field='component_id'
|
||||||
related_name='%(class)ss',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -6,6 +6,7 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
|
from netbox.config import ConfigItem
|
||||||
from netbox.models import NetBoxModel
|
from netbox.models import NetBoxModel
|
||||||
from utilities.validators import ExclusionValidator
|
from utilities.validators import ExclusionValidator
|
||||||
from .device_components import LinkTermination, PathEndpoint
|
from .device_components import LinkTermination, PathEndpoint
|
||||||
@ -105,16 +106,16 @@ class PowerFeed(NetBoxModel, PathEndpoint, LinkTermination):
|
|||||||
default=PowerFeedPhaseChoices.PHASE_SINGLE
|
default=PowerFeedPhaseChoices.PHASE_SINGLE
|
||||||
)
|
)
|
||||||
voltage = models.SmallIntegerField(
|
voltage = models.SmallIntegerField(
|
||||||
default=POWERFEED_VOLTAGE_DEFAULT,
|
default=ConfigItem('POWERFEED_DEFAULT_VOLTAGE'),
|
||||||
validators=[ExclusionValidator([0])]
|
validators=[ExclusionValidator([0])]
|
||||||
)
|
)
|
||||||
amperage = models.PositiveSmallIntegerField(
|
amperage = models.PositiveSmallIntegerField(
|
||||||
validators=[MinValueValidator(1)],
|
validators=[MinValueValidator(1)],
|
||||||
default=POWERFEED_AMPERAGE_DEFAULT
|
default=ConfigItem('POWERFEED_DEFAULT_AMPERAGE')
|
||||||
)
|
)
|
||||||
max_utilization = models.PositiveSmallIntegerField(
|
max_utilization = models.PositiveSmallIntegerField(
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(100)],
|
validators=[MinValueValidator(1), MaxValueValidator(100)],
|
||||||
default=POWERFEED_MAX_UTILIZATION_DEFAULT,
|
default=ConfigItem('POWERFEED_DEFAULT_MAX_UTILIZATION'),
|
||||||
help_text="Maximum permissible draw (percentage)"
|
help_text="Maximum permissible draw (percentage)"
|
||||||
)
|
)
|
||||||
available_power = models.PositiveIntegerField(
|
available_power = models.PositiveIntegerField(
|
||||||
|
@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
|
|||||||
|
|
||||||
from dcim.models import Cable
|
from dcim.models import Cable
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenancyColumnsMixin
|
||||||
from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
|
from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -15,7 +15,7 @@ __all__ = (
|
|||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
|
|
||||||
class CableTable(NetBoxTable):
|
class CableTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
termination_a_parent = tables.TemplateColumn(
|
termination_a_parent = tables.TemplateColumn(
|
||||||
template_code=CABLE_TERMINATION_PARENT,
|
template_code=CABLE_TERMINATION_PARENT,
|
||||||
accessor=Accessor('termination_a'),
|
accessor=Accessor('termination_a'),
|
||||||
@ -53,7 +53,6 @@ class CableTable(NetBoxTable):
|
|||||||
verbose_name='Termination B'
|
verbose_name='Termination B'
|
||||||
)
|
)
|
||||||
status = columns.ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
tenant = TenantColumn()
|
|
||||||
length = columns.TemplateColumn(
|
length = columns.TemplateColumn(
|
||||||
template_code=CABLE_LENGTH,
|
template_code=CABLE_LENGTH,
|
||||||
order_by=('_abs_length', 'length_unit')
|
order_by=('_abs_length', 'length_unit')
|
||||||
@ -67,7 +66,7 @@ class CableTable(NetBoxTable):
|
|||||||
model = Cable
|
model = Cable
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'label', 'termination_a_parent', 'rack_a', 'termination_a', 'termination_b_parent', 'rack_b', 'termination_b',
|
'pk', 'id', 'label', 'termination_a_parent', 'rack_a', 'termination_a', 'termination_b_parent', 'rack_b', 'termination_b',
|
||||||
'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated',
|
'status', 'type', 'tenant', 'tenant_group', 'color', 'length', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
||||||
|
@ -6,7 +6,7 @@ from dcim.models import (
|
|||||||
InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
|
InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
|
||||||
)
|
)
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenancyColumnsMixin
|
||||||
from .template_code import *
|
from .template_code import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -137,13 +137,12 @@ class PlatformTable(NetBoxTable):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceTable(NetBoxTable):
|
class DeviceTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
order_by=('_name',),
|
order_by=('_name',),
|
||||||
template_code=DEVICE_LINK
|
template_code=DEVICE_LINK
|
||||||
)
|
)
|
||||||
status = columns.ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
tenant = TenantColumn()
|
|
||||||
site = tables.Column(
|
site = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -200,7 +199,7 @@ class DeviceTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Device
|
model = Device
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
|
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
|
||||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
|
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
|
||||||
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags',
|
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags',
|
||||||
'created', 'last_updated',
|
'created', 'last_updated',
|
||||||
@ -211,12 +210,11 @@ class DeviceTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceImportTable(NetBoxTable):
|
class DeviceImportTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
template_code=DEVICE_LINK
|
template_code=DEVICE_LINK
|
||||||
)
|
)
|
||||||
status = columns.ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
tenant = TenantColumn()
|
|
||||||
site = tables.Column(
|
site = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -232,7 +230,7 @@ class DeviceImportTable(NetBoxTable):
|
|||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Device
|
model = Device
|
||||||
fields = ('id', 'name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
|
fields = ('id', 'name', 'status', 'tenant', 'tenant_group', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||||
empty_text = False
|
empty_text = False
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
|
|||||||
|
|
||||||
from dcim.models import Rack, RackReservation, RackRole
|
from dcim.models import Rack, RackReservation, RackRole
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenancyColumnsMixin
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'RackTable',
|
'RackTable',
|
||||||
@ -37,7 +37,7 @@ class RackRoleTable(NetBoxTable):
|
|||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackTable(NetBoxTable):
|
class RackTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
order_by=('_name',),
|
order_by=('_name',),
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -48,7 +48,6 @@ class RackTable(NetBoxTable):
|
|||||||
site = tables.Column(
|
site = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
|
||||||
status = columns.ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
role = columns.ColoredLabelColumn()
|
role = columns.ColoredLabelColumn()
|
||||||
u_height = tables.TemplateColumn(
|
u_height = tables.TemplateColumn(
|
||||||
@ -87,7 +86,7 @@ class RackTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag',
|
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial', 'asset_tag',
|
||||||
'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization',
|
'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization',
|
||||||
'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated',
|
'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
@ -101,7 +100,7 @@ class RackTable(NetBoxTable):
|
|||||||
# Rack reservations
|
# Rack reservations
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackReservationTable(NetBoxTable):
|
class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
reservation = tables.Column(
|
reservation = tables.Column(
|
||||||
accessor='pk',
|
accessor='pk',
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -110,7 +109,6 @@ class RackReservationTable(NetBoxTable):
|
|||||||
accessor=Accessor('rack__site'),
|
accessor=Accessor('rack__site'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
|
||||||
rack = tables.Column(
|
rack = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -125,7 +123,7 @@ class RackReservationTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags',
|
'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags',
|
||||||
'actions', 'created', 'last_updated',
|
'actions', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')
|
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')
|
||||||
|
@ -2,7 +2,7 @@ import django_tables2 as tables
|
|||||||
|
|
||||||
from dcim.models import Location, Region, Site, SiteGroup
|
from dcim.models import Location, Region, Site, SiteGroup
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenancyColumnsMixin
|
||||||
from .template_code import LOCATION_BUTTONS
|
from .template_code import LOCATION_BUTTONS
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -75,7 +75,7 @@ class SiteGroupTable(NetBoxTable):
|
|||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteTable(NetBoxTable):
|
class SiteTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -96,7 +96,6 @@ class SiteTable(NetBoxTable):
|
|||||||
url_params={'site_id': 'pk'},
|
url_params={'site_id': 'pk'},
|
||||||
verbose_name='ASN Count'
|
verbose_name='ASN Count'
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
contacts = columns.ManyToManyColumn(
|
contacts = columns.ManyToManyColumn(
|
||||||
linkify_item=True
|
linkify_item=True
|
||||||
@ -108,7 +107,7 @@ class SiteTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Site
|
model = Site
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count',
|
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'tenant_group', 'asns', 'asn_count',
|
||||||
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments',
|
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments',
|
||||||
'contacts', 'tags', 'created', 'last_updated', 'actions',
|
'contacts', 'tags', 'created', 'last_updated', 'actions',
|
||||||
)
|
)
|
||||||
@ -119,14 +118,13 @@ class SiteTable(NetBoxTable):
|
|||||||
# Locations
|
# Locations
|
||||||
#
|
#
|
||||||
|
|
||||||
class LocationTable(NetBoxTable):
|
class LocationTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
name = columns.MPTTColumn(
|
name = columns.MPTTColumn(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
site = tables.Column(
|
site = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
|
||||||
rack_count = columns.LinkedCountColumn(
|
rack_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:rack_list',
|
viewname='dcim:rack_list',
|
||||||
url_params={'location_id': 'pk'},
|
url_params={'location_id': 'pk'},
|
||||||
@ -150,7 +148,7 @@ class LocationTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Location
|
model = Location
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'contacts',
|
'pk', 'id', 'name', 'site', 'tenant', 'tenant_group', 'rack_count', 'device_count', 'description', 'slug', 'contacts',
|
||||||
'tags', 'actions', 'created', 'last_updated',
|
'tags', 'actions', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description')
|
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description')
|
||||||
|
@ -1849,6 +1849,11 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'module_type': [module_types[0].model, module_types[1].model]}
|
params = {'module_type': [module_types[0].model, module_types[1].model]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
|
|
||||||
|
def test_module_bay(self):
|
||||||
|
module_bays = ModuleBay.objects.all()[:2]
|
||||||
|
params = {'module_bay_id': [module_bays[0].pk, module_bays[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_device(self):
|
def test_device(self):
|
||||||
device_types = Device.objects.all()[:2]
|
device_types = Device.objects.all()[:2]
|
||||||
params = {'device_id': [device_types[0].pk, device_types[1].pk]}
|
params = {'device_id': [device_types[0].pk, device_types[1].pk]}
|
||||||
|
@ -561,7 +561,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
|||||||
|
|
||||||
class RackListView(generic.ObjectListView):
|
class RackListView(generic.ObjectListView):
|
||||||
queryset = Rack.objects.prefetch_related(
|
queryset = Rack.objects.prefetch_related(
|
||||||
'site', 'location', 'tenant', 'role', 'devices__device_type'
|
'site', 'location', 'tenant', 'tenant_group', 'role', 'devices__device_type'
|
||||||
).annotate(
|
).annotate(
|
||||||
device_count=count_related(Device, 'rack')
|
device_count=count_related(Device, 'rack')
|
||||||
)
|
)
|
||||||
|
@ -15,6 +15,9 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
|
|||||||
('Rack Elevations', {
|
('Rack Elevations', {
|
||||||
'fields': ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH'),
|
'fields': ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH'),
|
||||||
}),
|
}),
|
||||||
|
('Power', {
|
||||||
|
'fields': ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')
|
||||||
|
}),
|
||||||
('IPAM', {
|
('IPAM', {
|
||||||
'fields': ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4'),
|
'fields': ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4'),
|
||||||
}),
|
}),
|
||||||
|
@ -221,7 +221,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
|
|||||||
model = JournalEntry
|
model = JournalEntry
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
|
'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
|
||||||
'created_by', 'kind', 'comments', 'tags', 'custom_fields',
|
'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
@ -32,6 +32,9 @@ class WebhookFilterSet(BaseFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
|
content_type_id = MultiValueNumberFilter(
|
||||||
|
field_name='content_types__id'
|
||||||
|
)
|
||||||
content_types = ContentTypeFilter()
|
content_types = ContentTypeFilter()
|
||||||
http_method = django_filters.MultipleChoiceFilter(
|
http_method = django_filters.MultipleChoiceFilter(
|
||||||
choices=WebhookHttpMethodChoices
|
choices=WebhookHttpMethodChoices
|
||||||
@ -40,8 +43,8 @@ class WebhookFilterSet(BaseFilterSet):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Webhook
|
model = Webhook
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled',
|
'id', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled', 'http_method',
|
||||||
'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
|
'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
|
||||||
]
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
@ -58,11 +61,17 @@ class CustomFieldFilterSet(BaseFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
|
type = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=CustomFieldTypeChoices
|
||||||
|
)
|
||||||
|
content_type_id = MultiValueNumberFilter(
|
||||||
|
field_name='content_types__id'
|
||||||
|
)
|
||||||
content_types = ContentTypeFilter()
|
content_types = ContentTypeFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight', 'description']
|
fields = ['id', 'name', 'required', 'filter_logic', 'weight', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -32,12 +32,13 @@ __all__ = (
|
|||||||
class CustomFieldFilterForm(FilterForm):
|
class CustomFieldFilterForm(FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q',)),
|
(None, ('q',)),
|
||||||
('Attributes', ('type', 'content_types', 'weight', 'required')),
|
('Attributes', ('type', 'content_type_id', 'weight', 'required')),
|
||||||
)
|
)
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
content_type_id = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
limit_choices_to=FeatureQuery('custom_fields'),
|
limit_choices_to=FeatureQuery('custom_fields'),
|
||||||
required=False
|
required=False,
|
||||||
|
label='Object type'
|
||||||
)
|
)
|
||||||
type = MultipleChoiceField(
|
type = MultipleChoiceField(
|
||||||
choices=CustomFieldTypeChoices,
|
choices=CustomFieldTypeChoices,
|
||||||
@ -110,13 +111,14 @@ class ExportTemplateFilterForm(FilterForm):
|
|||||||
class WebhookFilterForm(FilterForm):
|
class WebhookFilterForm(FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q',)),
|
(None, ('q',)),
|
||||||
('Attributes', ('content_types', 'http_method', 'enabled')),
|
('Attributes', ('content_type_id', 'http_method', 'enabled')),
|
||||||
('Events', ('type_create', 'type_update', 'type_delete')),
|
('Events', ('type_create', 'type_update', 'type_delete')),
|
||||||
)
|
)
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
content_type_id = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
limit_choices_to=FeatureQuery('webhooks'),
|
limit_choices_to=FeatureQuery('webhooks'),
|
||||||
required=False
|
required=False,
|
||||||
|
label='Object type'
|
||||||
)
|
)
|
||||||
http_method = MultipleChoiceField(
|
http_method = MultipleChoiceField(
|
||||||
choices=WebhookHttpMethodChoices,
|
choices=WebhookHttpMethodChoices,
|
||||||
|
@ -365,13 +365,8 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
|||||||
|
|
||||||
# Text
|
# Text
|
||||||
else:
|
else:
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_LONGTEXT:
|
widget = forms.Textarea if self.type == CustomFieldTypeChoices.TYPE_LONGTEXT else None
|
||||||
max_length = None
|
field = forms.CharField(required=required, initial=initial, widget=widget)
|
||||||
widget = forms.Textarea
|
|
||||||
else:
|
|
||||||
max_length = 255
|
|
||||||
widget = None
|
|
||||||
field = forms.CharField(max_length=max_length, required=required, initial=initial, widget=widget)
|
|
||||||
if self.validation_regex:
|
if self.validation_regex:
|
||||||
field.validators = [
|
field.validators = [
|
||||||
RegexValidator(
|
RegexValidator(
|
||||||
|
@ -7,7 +7,9 @@ from django.test import TestCase
|
|||||||
|
|
||||||
from circuits.models import Provider
|
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 (
|
||||||
|
CustomFieldTypeChoices, CustomFieldFilterLogicChoices, JournalEntryKindChoices, ObjectChangeActionChoices,
|
||||||
|
)
|
||||||
from extras.filtersets import *
|
from extras.filtersets import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress
|
||||||
@ -16,6 +18,65 @@ from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests, cr
|
|||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldTestCase(TestCase, BaseFilterSetTests):
|
||||||
|
queryset = CustomField.objects.all()
|
||||||
|
filterset = CustomFieldFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
|
||||||
|
|
||||||
|
custom_fields = (
|
||||||
|
CustomField(
|
||||||
|
name='Custom Field 1',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||||
|
required=True,
|
||||||
|
weight=100,
|
||||||
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
|
||||||
|
),
|
||||||
|
CustomField(
|
||||||
|
name='Custom Field 2',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_INTEGER,
|
||||||
|
required=False,
|
||||||
|
weight=200,
|
||||||
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
|
||||||
|
),
|
||||||
|
CustomField(
|
||||||
|
name='Custom Field 3',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_BOOLEAN,
|
||||||
|
required=False,
|
||||||
|
weight=300,
|
||||||
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
|
||||||
|
),
|
||||||
|
)
|
||||||
|
CustomField.objects.bulk_create(custom_fields)
|
||||||
|
custom_fields[0].content_types.add(content_types[0])
|
||||||
|
custom_fields[1].content_types.add(content_types[1])
|
||||||
|
custom_fields[2].content_types.add(content_types[2])
|
||||||
|
|
||||||
|
def test_name(self):
|
||||||
|
params = {'name': ['Custom Field 1', 'Custom Field 2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_content_types(self):
|
||||||
|
params = {'content_types': 'dcim.site'}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_required(self):
|
||||||
|
params = {'required': True}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_weight(self):
|
||||||
|
params = {'weight': [100, 200]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_filter_logic(self):
|
||||||
|
params = {'filter_logic': CustomFieldFilterLogicChoices.FILTER_LOOSE}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
class WebhookTestCase(TestCase, BaseFilterSetTests):
|
class WebhookTestCase(TestCase, BaseFilterSetTests):
|
||||||
queryset = Webhook.objects.all()
|
queryset = Webhook.objects.all()
|
||||||
filterset = WebhookFilterSet
|
filterset = WebhookFilterSet
|
||||||
@ -62,6 +123,8 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
|
|||||||
def test_content_types(self):
|
def test_content_types(self):
|
||||||
params = {'content_types': 'dcim.site'}
|
params = {'content_types': 'dcim.site'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
def test_type_create(self):
|
def test_type_create(self):
|
||||||
params = {'type_create': True}
|
params = {'type_create': True}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from dcim.models import Location, Rack, Region, Site, SiteGroup
|
from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
|
||||||
|
from virtualization.models import VirtualMachine
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import *
|
from ipam.constants import *
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
@ -265,6 +266,7 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
|
('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
|
||||||
('VRF', ('vrf_id', 'present_in_vrf_id')),
|
('VRF', ('vrf_id', 'present_in_vrf_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
|
('Device/VM', ('device_id', 'virtual_machine_id')),
|
||||||
)
|
)
|
||||||
parent = forms.CharField(
|
parent = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
@ -298,6 +300,16 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('Present in VRF')
|
label=_('Present in VRF')
|
||||||
)
|
)
|
||||||
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Assigned Device'),
|
||||||
|
)
|
||||||
|
virtual_machine_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=VirtualMachine.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Assigned VM'),
|
||||||
|
)
|
||||||
status = MultipleChoiceField(
|
status = MultipleChoiceField(
|
||||||
choices=IPAddressStatusChoices,
|
choices=IPAddressStatusChoices,
|
||||||
required=False
|
required=False
|
||||||
|
@ -4,7 +4,7 @@ from django_tables2.utils import Accessor
|
|||||||
|
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenancyColumnsMixin, TenantColumn
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateTable',
|
'AggregateTable',
|
||||||
@ -99,7 +99,7 @@ class RIRTable(NetBoxTable):
|
|||||||
# ASNs
|
# ASNs
|
||||||
#
|
#
|
||||||
|
|
||||||
class ASNTable(NetBoxTable):
|
class ASNTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
asn = tables.Column(
|
asn = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -122,7 +122,6 @@ class ASNTable(NetBoxTable):
|
|||||||
linkify_item=True,
|
linkify_item=True,
|
||||||
verbose_name='Sites'
|
verbose_name='Sites'
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='ipam:asn_list'
|
url_name='ipam:asn_list'
|
||||||
)
|
)
|
||||||
@ -130,7 +129,7 @@ class ASNTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = ASN
|
model = ASN
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'description', 'sites', 'tags',
|
'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description', 'sites', 'tags',
|
||||||
'created', 'last_updated', 'actions',
|
'created', 'last_updated', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant')
|
default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant')
|
||||||
@ -140,12 +139,11 @@ class ASNTable(NetBoxTable):
|
|||||||
# Aggregates
|
# Aggregates
|
||||||
#
|
#
|
||||||
|
|
||||||
class AggregateTable(NetBoxTable):
|
class AggregateTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
prefix = tables.Column(
|
prefix = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='Aggregate'
|
verbose_name='Aggregate'
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
|
||||||
date_added = tables.DateColumn(
|
date_added = tables.DateColumn(
|
||||||
format="Y-m-d",
|
format="Y-m-d",
|
||||||
verbose_name='Added'
|
verbose_name='Added'
|
||||||
@ -164,7 +162,7 @@ class AggregateTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description', 'tags',
|
'pk', 'id', 'prefix', 'rir', 'tenant', 'tenant_group', 'child_count', 'utilization', 'date_added', 'description', 'tags',
|
||||||
'created', 'last_updated',
|
'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description')
|
default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description')
|
||||||
@ -225,7 +223,7 @@ class PrefixUtilizationColumn(columns.UtilizationColumn):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class PrefixTable(NetBoxTable):
|
class PrefixTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
prefix = columns.TemplateColumn(
|
prefix = columns.TemplateColumn(
|
||||||
template_code=PREFIX_LINK,
|
template_code=PREFIX_LINK,
|
||||||
export_raw=True,
|
export_raw=True,
|
||||||
@ -256,7 +254,6 @@ class PrefixTable(NetBoxTable):
|
|||||||
template_code=VRF_LINK,
|
template_code=VRF_LINK,
|
||||||
verbose_name='VRF'
|
verbose_name='VRF'
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
|
||||||
site = tables.Column(
|
site = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -289,7 +286,7 @@ class PrefixTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site',
|
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group', 'site',
|
||||||
'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', 'created', 'last_updated',
|
'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
@ -303,7 +300,7 @@ class PrefixTable(NetBoxTable):
|
|||||||
#
|
#
|
||||||
# IP ranges
|
# IP ranges
|
||||||
#
|
#
|
||||||
class IPRangeTable(NetBoxTable):
|
class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
start_address = tables.Column(
|
start_address = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -317,7 +314,6 @@ class IPRangeTable(NetBoxTable):
|
|||||||
role = tables.Column(
|
role = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
|
||||||
utilization = columns.UtilizationColumn(
|
utilization = columns.UtilizationColumn(
|
||||||
accessor='utilization',
|
accessor='utilization',
|
||||||
orderable=False
|
orderable=False
|
||||||
@ -329,7 +325,7 @@ class IPRangeTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = IPRange
|
model = IPRange
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
|
'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'description',
|
||||||
'utilization', 'tags', 'created', 'last_updated',
|
'utilization', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
@ -344,7 +340,7 @@ class IPRangeTable(NetBoxTable):
|
|||||||
# IPAddresses
|
# IPAddresses
|
||||||
#
|
#
|
||||||
|
|
||||||
class IPAddressTable(NetBoxTable):
|
class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
address = tables.TemplateColumn(
|
address = tables.TemplateColumn(
|
||||||
template_code=IPADDRESS_LINK,
|
template_code=IPADDRESS_LINK,
|
||||||
verbose_name='IP Address'
|
verbose_name='IP Address'
|
||||||
@ -357,7 +353,6 @@ class IPAddressTable(NetBoxTable):
|
|||||||
default=AVAILABLE_LABEL
|
default=AVAILABLE_LABEL
|
||||||
)
|
)
|
||||||
role = columns.ChoiceFieldColumn()
|
role = columns.ChoiceFieldColumn()
|
||||||
tenant = TenantColumn()
|
|
||||||
assigned_object = tables.Column(
|
assigned_object = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
orderable=False,
|
orderable=False,
|
||||||
@ -386,7 +381,7 @@ class IPAddressTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'assigned', 'dns_name', 'description',
|
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'assigned', 'dns_name', 'description',
|
||||||
'tags', 'created', 'last_updated',
|
'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
|
@ -5,7 +5,7 @@ from django_tables2.utils import Accessor
|
|||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenancyColumnsMixin, TenantColumn
|
||||||
from virtualization.models import VMInterface
|
from virtualization.models import VMInterface
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -90,7 +90,7 @@ class VLANGroupTable(NetBoxTable):
|
|||||||
# VLANs
|
# VLANs
|
||||||
#
|
#
|
||||||
|
|
||||||
class VLANTable(NetBoxTable):
|
class VLANTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
vid = tables.TemplateColumn(
|
vid = tables.TemplateColumn(
|
||||||
template_code=VLAN_LINK,
|
template_code=VLAN_LINK,
|
||||||
verbose_name='VID'
|
verbose_name='VID'
|
||||||
@ -104,7 +104,6 @@ class VLANTable(NetBoxTable):
|
|||||||
group = tables.Column(
|
group = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
|
||||||
status = columns.ChoiceFieldColumn(
|
status = columns.ChoiceFieldColumn(
|
||||||
default=AVAILABLE_LABEL
|
default=AVAILABLE_LABEL
|
||||||
)
|
)
|
||||||
@ -123,7 +122,7 @@ class VLANTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags',
|
'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role', 'description', 'tags',
|
||||||
'created', 'last_updated',
|
'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')
|
default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')
|
||||||
|
@ -2,7 +2,7 @@ import django_tables2 as tables
|
|||||||
|
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenancyColumnsMixin
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'RouteTargetTable',
|
'RouteTargetTable',
|
||||||
@ -20,14 +20,13 @@ VRF_TARGETS = """
|
|||||||
# VRFs
|
# VRFs
|
||||||
#
|
#
|
||||||
|
|
||||||
class VRFTable(NetBoxTable):
|
class VRFTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
rd = tables.Column(
|
rd = tables.Column(
|
||||||
verbose_name='RD'
|
verbose_name='RD'
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
|
||||||
enforce_unique = columns.BooleanColumn(
|
enforce_unique = columns.BooleanColumn(
|
||||||
verbose_name='Unique'
|
verbose_name='Unique'
|
||||||
)
|
)
|
||||||
@ -46,7 +45,7 @@ class VRFTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = VRF
|
model = VRF
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets',
|
'pk', 'id', 'name', 'rd', 'tenant', 'tenant_group', 'enforce_unique', 'description', 'import_targets', 'export_targets',
|
||||||
'tags', 'created', 'last_updated',
|
'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'rd', 'tenant', 'description')
|
default_columns = ('pk', 'name', 'rd', 'tenant', 'description')
|
||||||
@ -56,16 +55,15 @@ class VRFTable(NetBoxTable):
|
|||||||
# Route targets
|
# Route targets
|
||||||
#
|
#
|
||||||
|
|
||||||
class RouteTargetTable(NetBoxTable):
|
class RouteTargetTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='ipam:vrf_list'
|
url_name='ipam:vrf_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = RouteTarget
|
model = RouteTarget
|
||||||
fields = ('pk', 'id', 'name', 'tenant', 'description', 'tags', 'created', 'last_updated',)
|
fields = ('pk', 'id', 'name', 'tenant', 'tenant_group', 'description', 'tags', 'created', 'last_updated',)
|
||||||
default_columns = ('pk', 'name', 'tenant', 'description')
|
default_columns = ('pk', 'name', 'tenant', 'description')
|
||||||
|
@ -298,7 +298,7 @@ class AggregatePrefixesView(generic.ObjectChildrenView):
|
|||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
return Prefix.objects.restrict(request.user, 'view').filter(
|
return Prefix.objects.restrict(request.user, 'view').filter(
|
||||||
prefix__net_contained_or_equal=str(parent.prefix)
|
prefix__net_contained_or_equal=str(parent.prefix)
|
||||||
).prefetch_related('site', 'role', 'tenant', 'vlan')
|
).prefetch_related('site', 'role', 'tenant', 'tenant__group', 'vlan')
|
||||||
|
|
||||||
def prep_table_data(self, request, queryset, parent):
|
def prep_table_data(self, request, queryset, parent):
|
||||||
# Determine whether to show assigned prefixes, available prefixes, or both
|
# Determine whether to show assigned prefixes, available prefixes, or both
|
||||||
@ -470,7 +470,7 @@ class PrefixPrefixesView(generic.ObjectChildrenView):
|
|||||||
|
|
||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
|
return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
|
||||||
'site', 'vrf', 'vlan', 'role', 'tenant',
|
'site', 'vrf', 'vlan', 'role', 'tenant', 'tenant__group'
|
||||||
)
|
)
|
||||||
|
|
||||||
def prep_table_data(self, request, queryset, parent):
|
def prep_table_data(self, request, queryset, parent):
|
||||||
@ -499,7 +499,7 @@ class PrefixIPRangesView(generic.ObjectChildrenView):
|
|||||||
|
|
||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
return parent.get_child_ranges().restrict(request.user, 'view').prefetch_related(
|
return parent.get_child_ranges().restrict(request.user, 'view').prefetch_related(
|
||||||
'vrf', 'role', 'tenant',
|
'vrf', 'role', 'tenant', 'tenant__group',
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
@ -587,7 +587,7 @@ class IPRangeIPAddressesView(generic.ObjectChildrenView):
|
|||||||
|
|
||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related(
|
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related(
|
||||||
'vrf', 'role', 'tenant',
|
'vrf', 'role', 'tenant', 'tenant__group',
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
@ -680,13 +680,16 @@ class IPAddressView(generic.ObjectView):
|
|||||||
service_filter = Q(ipaddresses=instance)
|
service_filter = Q(ipaddresses=instance)
|
||||||
|
|
||||||
# Find services listening on all IPs on the assigned device/vm
|
# Find services listening on all IPs on the assigned device/vm
|
||||||
if instance.assigned_object and instance.assigned_object.parent_object:
|
try:
|
||||||
parent_object = instance.assigned_object.parent_object
|
if instance.assigned_object and instance.assigned_object.parent_object:
|
||||||
|
parent_object = instance.assigned_object.parent_object
|
||||||
|
|
||||||
if isinstance(parent_object, VirtualMachine):
|
if isinstance(parent_object, VirtualMachine):
|
||||||
service_filter |= (Q(virtual_machine=parent_object) & Q(ipaddresses=None))
|
service_filter |= (Q(virtual_machine=parent_object) & Q(ipaddresses=None))
|
||||||
elif isinstance(parent_object, Device):
|
elif isinstance(parent_object, Device):
|
||||||
service_filter |= (Q(device=parent_object) & Q(ipaddresses=None))
|
service_filter |= (Q(device=parent_object) & Q(ipaddresses=None))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
services = Service.objects.restrict(request.user, 'view').filter(service_filter)
|
services = Service.objects.restrict(request.user, 'view').filter(service_filter)
|
||||||
|
|
||||||
|
@ -348,3 +348,26 @@ class LDAPBackend:
|
|||||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
# Custom Social Auth Pipeline Handlers
|
||||||
|
def user_default_groups_handler(backend, user, response, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Custom pipeline handler which adds remote auth users to the default group specified in the
|
||||||
|
configuration file.
|
||||||
|
"""
|
||||||
|
logger = logging.getLogger('netbox.auth.user_default_groups_handler')
|
||||||
|
if settings.REMOTE_AUTH_DEFAULT_GROUPS:
|
||||||
|
# Assign default groups to the user
|
||||||
|
group_list = []
|
||||||
|
for name in settings.REMOTE_AUTH_DEFAULT_GROUPS:
|
||||||
|
try:
|
||||||
|
group_list.append(Group.objects.get(name=name))
|
||||||
|
except Group.DoesNotExist:
|
||||||
|
logging.error(
|
||||||
|
f"Could not assign group {name} to remotely-authenticated user {user}: Group not found")
|
||||||
|
if group_list:
|
||||||
|
user.groups.add(*group_list)
|
||||||
|
else:
|
||||||
|
user.groups.clear()
|
||||||
|
logger.debug(f"Stripping user {user} from Groups")
|
||||||
|
@ -82,6 +82,31 @@ PARAMS = (
|
|||||||
field=forms.IntegerField
|
field=forms.IntegerField
|
||||||
),
|
),
|
||||||
|
|
||||||
|
# Power
|
||||||
|
ConfigParam(
|
||||||
|
name='POWERFEED_DEFAULT_VOLTAGE',
|
||||||
|
label='Powerfeed voltage',
|
||||||
|
default=120,
|
||||||
|
description="Default voltage for powerfeeds",
|
||||||
|
field=forms.IntegerField
|
||||||
|
),
|
||||||
|
|
||||||
|
ConfigParam(
|
||||||
|
name='POWERFEED_DEFAULT_AMPERAGE',
|
||||||
|
label='Powerfeed amperage',
|
||||||
|
default=15,
|
||||||
|
description="Default amperage for powerfeeds",
|
||||||
|
field=forms.IntegerField
|
||||||
|
),
|
||||||
|
|
||||||
|
ConfigParam(
|
||||||
|
name='POWERFEED_DEFAULT_MAX_UTILIZATION',
|
||||||
|
label='Powerfeed max utilization',
|
||||||
|
default=80,
|
||||||
|
description="Default max utilization for powerfeeds",
|
||||||
|
field=forms.IntegerField
|
||||||
|
),
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
ConfigParam(
|
ConfigParam(
|
||||||
name='ALLOWED_URL_SCHEMES',
|
name='ALLOWED_URL_SCHEMES',
|
||||||
|
@ -34,7 +34,7 @@ CIRCUIT_TYPES = OrderedDict(
|
|||||||
}),
|
}),
|
||||||
('circuit', {
|
('circuit', {
|
||||||
'queryset': Circuit.objects.prefetch_related(
|
'queryset': Circuit.objects.prefetch_related(
|
||||||
'type', 'provider', 'tenant', 'terminations__site'
|
'type', 'provider', 'tenant', 'tenant__group', 'terminations__site'
|
||||||
),
|
),
|
||||||
'filterset': circuits.filtersets.CircuitFilterSet,
|
'filterset': circuits.filtersets.CircuitFilterSet,
|
||||||
'table': circuits.tables.CircuitTable,
|
'table': circuits.tables.CircuitTable,
|
||||||
@ -53,13 +53,13 @@ CIRCUIT_TYPES = OrderedDict(
|
|||||||
DCIM_TYPES = OrderedDict(
|
DCIM_TYPES = OrderedDict(
|
||||||
(
|
(
|
||||||
('site', {
|
('site', {
|
||||||
'queryset': Site.objects.prefetch_related('region', 'tenant'),
|
'queryset': Site.objects.prefetch_related('region', 'tenant', 'tenant__group'),
|
||||||
'filterset': dcim.filtersets.SiteFilterSet,
|
'filterset': dcim.filtersets.SiteFilterSet,
|
||||||
'table': dcim.tables.SiteTable,
|
'table': dcim.tables.SiteTable,
|
||||||
'url': 'dcim:site_list',
|
'url': 'dcim:site_list',
|
||||||
}),
|
}),
|
||||||
('rack', {
|
('rack', {
|
||||||
'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'role').annotate(
|
'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
|
||||||
device_count=count_related(Device, 'rack')
|
device_count=count_related(Device, 'rack')
|
||||||
),
|
),
|
||||||
'filterset': dcim.filtersets.RackFilterSet,
|
'filterset': dcim.filtersets.RackFilterSet,
|
||||||
@ -100,7 +100,7 @@ DCIM_TYPES = OrderedDict(
|
|||||||
}),
|
}),
|
||||||
('device', {
|
('device', {
|
||||||
'queryset': Device.objects.prefetch_related(
|
'queryset': Device.objects.prefetch_related(
|
||||||
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6',
|
'device_type__manufacturer', 'device_role', 'tenant', 'tenant__group', 'site', 'rack', 'primary_ip4', 'primary_ip6',
|
||||||
),
|
),
|
||||||
'filterset': dcim.filtersets.DeviceFilterSet,
|
'filterset': dcim.filtersets.DeviceFilterSet,
|
||||||
'table': dcim.tables.DeviceTable,
|
'table': dcim.tables.DeviceTable,
|
||||||
@ -148,7 +148,7 @@ DCIM_TYPES = OrderedDict(
|
|||||||
IPAM_TYPES = OrderedDict(
|
IPAM_TYPES = OrderedDict(
|
||||||
(
|
(
|
||||||
('vrf', {
|
('vrf', {
|
||||||
'queryset': VRF.objects.prefetch_related('tenant'),
|
'queryset': VRF.objects.prefetch_related('tenant', 'tenant__group'),
|
||||||
'filterset': ipam.filtersets.VRFFilterSet,
|
'filterset': ipam.filtersets.VRFFilterSet,
|
||||||
'table': ipam.tables.VRFTable,
|
'table': ipam.tables.VRFTable,
|
||||||
'url': 'ipam:vrf_list',
|
'url': 'ipam:vrf_list',
|
||||||
@ -160,25 +160,25 @@ IPAM_TYPES = OrderedDict(
|
|||||||
'url': 'ipam:aggregate_list',
|
'url': 'ipam:aggregate_list',
|
||||||
}),
|
}),
|
||||||
('prefix', {
|
('prefix', {
|
||||||
'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
|
'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'),
|
||||||
'filterset': ipam.filtersets.PrefixFilterSet,
|
'filterset': ipam.filtersets.PrefixFilterSet,
|
||||||
'table': ipam.tables.PrefixTable,
|
'table': ipam.tables.PrefixTable,
|
||||||
'url': 'ipam:prefix_list',
|
'url': 'ipam:prefix_list',
|
||||||
}),
|
}),
|
||||||
('ipaddress', {
|
('ipaddress', {
|
||||||
'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'),
|
'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group'),
|
||||||
'filterset': ipam.filtersets.IPAddressFilterSet,
|
'filterset': ipam.filtersets.IPAddressFilterSet,
|
||||||
'table': ipam.tables.IPAddressTable,
|
'table': ipam.tables.IPAddressTable,
|
||||||
'url': 'ipam:ipaddress_list',
|
'url': 'ipam:ipaddress_list',
|
||||||
}),
|
}),
|
||||||
('vlan', {
|
('vlan', {
|
||||||
'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role'),
|
'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role'),
|
||||||
'filterset': ipam.filtersets.VLANFilterSet,
|
'filterset': ipam.filtersets.VLANFilterSet,
|
||||||
'table': ipam.tables.VLANTable,
|
'table': ipam.tables.VLANTable,
|
||||||
'url': 'ipam:vlan_list',
|
'url': 'ipam:vlan_list',
|
||||||
}),
|
}),
|
||||||
('asn', {
|
('asn', {
|
||||||
'queryset': ASN.objects.prefetch_related('rir', 'tenant'),
|
'queryset': ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group'),
|
||||||
'filterset': ipam.filtersets.ASNFilterSet,
|
'filterset': ipam.filtersets.ASNFilterSet,
|
||||||
'table': ipam.tables.ASNTable,
|
'table': ipam.tables.ASNTable,
|
||||||
'url': 'ipam:asn_list',
|
'url': 'ipam:asn_list',
|
||||||
@ -223,7 +223,7 @@ VIRTUALIZATION_TYPES = OrderedDict(
|
|||||||
}),
|
}),
|
||||||
('virtualmachine', {
|
('virtualmachine', {
|
||||||
'queryset': VirtualMachine.objects.prefetch_related(
|
'queryset': VirtualMachine.objects.prefetch_related(
|
||||||
'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
|
'cluster', 'tenant', 'tenant__group', 'platform', 'primary_ip4', 'primary_ip6',
|
||||||
),
|
),
|
||||||
'filterset': virtualization.filtersets.VirtualMachineFilterSet,
|
'filterset': virtualization.filtersets.VirtualMachineFilterSet,
|
||||||
'table': virtualization.tables.VirtualMachineTable,
|
'table': virtualization.tables.VirtualMachineTable,
|
||||||
|
@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.2.5'
|
VERSION = '3.2.6'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@ -483,6 +483,19 @@ for param in dir(configuration):
|
|||||||
|
|
||||||
SOCIAL_AUTH_JSONFIELD_ENABLED = True
|
SOCIAL_AUTH_JSONFIELD_ENABLED = True
|
||||||
|
|
||||||
|
SOCIAL_AUTH_PIPELINE = (
|
||||||
|
'social_core.pipeline.social_auth.social_details',
|
||||||
|
'social_core.pipeline.social_auth.social_uid',
|
||||||
|
'social_core.pipeline.social_auth.social_user',
|
||||||
|
'social_core.pipeline.user.get_username',
|
||||||
|
'social_core.pipeline.social_auth.associate_by_email',
|
||||||
|
'social_core.pipeline.user.create_user',
|
||||||
|
'social_core.pipeline.social_auth.associate_user',
|
||||||
|
'netbox.authentication.user_default_groups_handler',
|
||||||
|
'social_core.pipeline.social_auth.load_extra_data',
|
||||||
|
'social_core.pipeline.user.user_details',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Django Prometheus
|
# Django Prometheus
|
||||||
|
@ -8,6 +8,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.core.exceptions import FieldDoesNotExist, ValidationError
|
from django.core.exceptions import FieldDoesNotExist, ValidationError
|
||||||
from django.db import transaction, IntegrityError
|
from django.db import transaction, IntegrityError
|
||||||
from django.db.models import ManyToManyField, ProtectedError
|
from django.db.models import ManyToManyField, ProtectedError
|
||||||
|
from django.db.models.fields.reverse_related import ManyToManyRel
|
||||||
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
|
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
@ -484,7 +485,7 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
setattr(obj, name, None if model_field.null else '')
|
setattr(obj, name, None if model_field.null else '')
|
||||||
|
|
||||||
# ManyToManyFields
|
# ManyToManyFields
|
||||||
elif isinstance(model_field, ManyToManyField):
|
elif isinstance(model_field, (ManyToManyField, ManyToManyRel)):
|
||||||
if form.cleaned_data[name]:
|
if form.cleaned_data[name]:
|
||||||
getattr(obj, name).set(form.cleaned_data[name])
|
getattr(obj, name).set(form.cleaned_data[name])
|
||||||
# Normal fields
|
# Normal fields
|
||||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -411,6 +411,7 @@ export class APISelect {
|
|||||||
} finally {
|
} finally {
|
||||||
this.setOptionStyles();
|
this.setOptionStyles();
|
||||||
this.enable();
|
this.enable();
|
||||||
|
this.slim.slim.search.input.focus();
|
||||||
this.base.dispatchEvent(this.loadEvent);
|
this.base.dispatchEvent(this.loadEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,13 +46,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Rack</th>
|
<th scope="row">Rack</th>
|
||||||
<td>
|
<td>{{ object.rack|linkify|placeholder }}</td>
|
||||||
{% if object.rack %}
|
|
||||||
<a href="{% url 'dcim:rack' pk=object.rack.pk %}">{{ object.rack }}</a>
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Position</th>
|
<th scope="row">Position</th>
|
||||||
@ -161,9 +155,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Role</th>
|
<th scope="row">Role</th>
|
||||||
<td>
|
<td>{{ object.device_role|linkify }}</td>
|
||||||
<a href="{% url 'dcim:device_list' %}?role={{ object.device_role.slug }}">{{ object.device_role }}</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Platform</th>
|
<th scope="row">Platform</th>
|
||||||
@ -173,7 +165,7 @@
|
|||||||
<th scope="row">Primary IPv4</th>
|
<th scope="row">Primary IPv4</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.primary_ip4 %}
|
{% if object.primary_ip4 %}
|
||||||
<a href="{% url 'ipam:ipaddress' pk=object.primary_ip4.pk %}">{{ object.primary_ip4.address.ip }}</a>
|
<a href="{{ object.primary_ip4.get_absolute_url }}">{{ object.primary_ip4.address.ip }}</a>
|
||||||
{% if object.primary_ip4.nat_inside %}
|
{% if object.primary_ip4.nat_inside %}
|
||||||
(NAT for {{ object.primary_ip4.nat_inside.address.ip|linkify }})
|
(NAT for {{ object.primary_ip4.nat_inside.address.ip|linkify }})
|
||||||
{% elif object.primary_ip4.nat_outside %}
|
{% elif object.primary_ip4.nat_outside %}
|
||||||
@ -188,7 +180,7 @@
|
|||||||
<th scope="row">Primary IPv6</th>
|
<th scope="row">Primary IPv6</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.primary_ip6 %}
|
{% if object.primary_ip6 %}
|
||||||
<a href="{% url 'ipam:ipaddress' pk=object.primary_ip6.pk %}">{{ object.primary_ip6.address.ip }}</a>
|
<a href="{{ object.primary_ip6.get_absolute_url }}">{{ object.primary_ip6.address.ip }}</a>
|
||||||
{% if object.primary_ip6.nat_inside %}
|
{% if object.primary_ip6.nat_inside %}
|
||||||
(NAT for {{ object.primary_ip6.nat_inside.address.ip|linkify }})
|
(NAT for {{ object.primary_ip6.nat_inside.address.ip|linkify }})
|
||||||
{% elif object.primary_ip6.nat_outside %}
|
{% elif object.primary_ip6.nat_outside %}
|
||||||
|
@ -86,6 +86,15 @@
|
|||||||
{% render_field form.tenant %}
|
{% render_field form.tenant %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">Virtual Chassis</h5>
|
||||||
|
</div>
|
||||||
|
{% render_field form.virtual_chassis %}
|
||||||
|
{% render_field form.vc_position %}
|
||||||
|
{% render_field form.vc_priority %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if form.custom_fields %}
|
{% if form.custom_fields %}
|
||||||
<div class="field-group my-5">
|
<div class="field-group my-5">
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
|
@ -5,25 +5,29 @@
|
|||||||
{% render_errors form %}
|
{% render_errors form %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if perms.extras.add_journalentry %}
|
|
||||||
<form action="{% url 'extras:journalentry_add' %}" method="post" enctype="multipart/form-data">
|
|
||||||
<div class="container">
|
|
||||||
<div class="field-group">
|
|
||||||
<h4>New Journal Entry</h4>
|
|
||||||
{% csrf_token %}
|
|
||||||
{% render_form form %}
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-12 text-end my-3">
|
|
||||||
<a href="{{ object.get_absolute_url }}" class="btn btn-outline-danger">Cancel</a>
|
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body table-responsive">
|
<div class="card-body table-responsive">
|
||||||
{% render_table table 'inc/table.html' %}
|
{% render_table table 'inc/table.html' %}
|
||||||
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if perms.extras.add_journalentry %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body table-responsive">
|
||||||
|
<h4 class="card-header">New Journal Entry</h4>
|
||||||
|
<form action="{% url 'extras:journalentry_add' %}" method="post" enctype="multipart/form-data">
|
||||||
|
<div class="container">
|
||||||
|
<div class="field-group">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% render_form form %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-12 text-end my-3">
|
||||||
|
<a href="{{ object.get_absolute_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -2,6 +2,8 @@ import django_tables2 as tables
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'TenantColumn',
|
'TenantColumn',
|
||||||
|
'TenantGroupColumn',
|
||||||
|
'TenancyColumnsMixin',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -24,3 +26,32 @@ class TenantColumn(tables.TemplateColumn):
|
|||||||
|
|
||||||
def value(self, value):
|
def value(self, value):
|
||||||
return str(value) if value else None
|
return str(value) if value else None
|
||||||
|
|
||||||
|
|
||||||
|
class TenantGroupColumn(tables.TemplateColumn):
|
||||||
|
"""
|
||||||
|
Include the tenant group description.
|
||||||
|
"""
|
||||||
|
template_code = """
|
||||||
|
{% if record.tenant and record.tenant.group %}
|
||||||
|
<a href="{{ record.tenant.group.get_absolute_url }}" title="{{ record.tenant.group.description }}">{{ record.tenant.group }}</a>
|
||||||
|
{% elif record.vrf.tenant and record.vrf.tenant.group %}
|
||||||
|
<a href="{{ record.vrf.tenant.group.get_absolute_url }}" title="{{ record.vrf.tenant.group.description }}">{{ record.vrf.tenant.group }}</a>*
|
||||||
|
{% else %}
|
||||||
|
—
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, accessor=tables.A('tenant__group'), *args, **kwargs):
|
||||||
|
if 'verbose_name' not in kwargs:
|
||||||
|
kwargs['verbose_name'] = 'Tenant Group'
|
||||||
|
|
||||||
|
super().__init__(template_code=self.template_code, accessor=accessor, *args, **kwargs)
|
||||||
|
|
||||||
|
def value(self, value):
|
||||||
|
return str(value) if value else None
|
||||||
|
|
||||||
|
|
||||||
|
class TenancyColumnsMixin(tables.Table):
|
||||||
|
tenant_group = TenantGroupColumn()
|
||||||
|
tenant = TenantColumn()
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from timezone_field import TimeZoneField
|
from timezone_field import TimeZoneField
|
||||||
|
|
||||||
|
from netbox.config import ConfigItem
|
||||||
|
|
||||||
|
|
||||||
SKIP_FIELDS = (
|
SKIP_FIELDS = (
|
||||||
TimeZoneField,
|
TimeZoneField,
|
||||||
@ -26,4 +28,9 @@ def custom_deconstruct(field):
|
|||||||
for attr in EXEMPT_ATTRS:
|
for attr in EXEMPT_ATTRS:
|
||||||
kwargs.pop(attr, None)
|
kwargs.pop(attr, None)
|
||||||
|
|
||||||
|
# Ignore any field defaults which reference a ConfigItem
|
||||||
|
kwargs = {
|
||||||
|
k: v for k, v in kwargs.items() if not isinstance(v, ConfigItem)
|
||||||
|
}
|
||||||
|
|
||||||
return name, path, args, kwargs
|
return name, path, args, kwargs
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
|
from tenancy.tables import TenancyColumnsMixin
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -56,7 +57,7 @@ class ClusterGroupTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'name', 'cluster_count', 'description')
|
default_columns = ('pk', 'name', 'cluster_count', 'description')
|
||||||
|
|
||||||
|
|
||||||
class ClusterTable(NetBoxTable):
|
class ClusterTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -66,9 +67,6 @@ class ClusterTable(NetBoxTable):
|
|||||||
group = tables.Column(
|
group = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
tenant = tables.Column(
|
|
||||||
linkify=True
|
|
||||||
)
|
|
||||||
site = tables.Column(
|
site = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -93,7 +91,7 @@ class ClusterTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Cluster
|
model = Cluster
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'device_count', 'vm_count', 'contacts',
|
'pk', 'id', 'name', 'type', 'group', 'tenant', 'tenant_group', 'site', 'comments', 'device_count', 'vm_count', 'contacts',
|
||||||
'tags', 'created', 'last_updated',
|
'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count')
|
default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count')
|
||||||
|
@ -2,7 +2,7 @@ import django_tables2 as tables
|
|||||||
|
|
||||||
from dcim.tables.devices import BaseInterfaceTable
|
from dcim.tables.devices import BaseInterfaceTable
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenancyColumnsMixin
|
||||||
from virtualization.models import VirtualMachine, VMInterface
|
from virtualization.models import VirtualMachine, VMInterface
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -24,7 +24,7 @@ VMINTERFACE_BUTTONS = """
|
|||||||
# Virtual machines
|
# Virtual machines
|
||||||
#
|
#
|
||||||
|
|
||||||
class VirtualMachineTable(NetBoxTable):
|
class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
order_by=('_name',),
|
order_by=('_name',),
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -34,7 +34,6 @@ class VirtualMachineTable(NetBoxTable):
|
|||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
role = columns.ColoredLabelColumn()
|
role = columns.ColoredLabelColumn()
|
||||||
tenant = TenantColumn()
|
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
primary_ip4 = tables.Column(
|
primary_ip4 = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
@ -56,7 +55,7 @@ class VirtualMachineTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
|
'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'tenant_group', 'platform', 'vcpus', 'memory', 'disk',
|
||||||
'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated',
|
'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
bleach==5.0.0
|
bleach==5.0.1
|
||||||
Django==4.0.5
|
Django==4.0.6
|
||||||
django-cors-headers==3.13.0
|
django-cors-headers==3.13.0
|
||||||
django-debug-toolbar==3.4.0
|
django-debug-toolbar==3.5.0
|
||||||
django-filter==22.1
|
django-filter==22.1
|
||||||
django-graphiql-debug-toolbar==0.2.0
|
django-graphiql-debug-toolbar==0.2.0
|
||||||
django-mptt==0.13.4
|
django-mptt==0.13.4
|
||||||
@ -19,13 +19,13 @@ gunicorn==20.1.0
|
|||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
Markdown==3.3.7
|
Markdown==3.3.7
|
||||||
markdown-include==0.6.0
|
markdown-include==0.6.0
|
||||||
mkdocs-material==8.3.6
|
mkdocs-material==8.3.9
|
||||||
mkdocstrings[python-legacy]==0.19.0
|
mkdocstrings[python-legacy]==0.19.0
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==9.1.1
|
Pillow==9.2.0
|
||||||
psycopg2-binary==2.9.3
|
psycopg2-binary==2.9.3
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
sentry-sdk==1.5.12
|
sentry-sdk==1.7.0
|
||||||
social-auth-app-django==5.0.0
|
social-auth-app-django==5.0.0
|
||||||
social-auth-core==4.3.0
|
social-auth-core==4.3.0
|
||||||
svgwrite==1.4.2
|
svgwrite==1.4.2
|
||||||
|
Loading…
Reference in New Issue
Block a user