mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-15 11:42:52 -06:00
Merge branch 'develop' into feature
This commit is contained in:
commit
ca10a7834a
31
SECURITY.md
Normal file
31
SECURITY.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## No Warranty
|
||||||
|
|
||||||
|
Per the terms of the Apache 2 license, NetBox is offered "as is" and without any guarantee or warranty pertaining to its operation. While every reasonable effort is made by its maintainers to ensure the product remains free of security vulnerabilities, users are ultimately responsible for conducting their own evaluations of each software release.
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
Administrators are encouraged to adhere to industry best practices concerning the secure operation of software, such as:
|
||||||
|
|
||||||
|
* Do not expose your NetBox installation to the public Internet
|
||||||
|
* Do not permit multiple users to share an account
|
||||||
|
* Enforce minimum password complexity requirements for local accounts
|
||||||
|
* Prohibit access to your database from clients other than the NetBox application
|
||||||
|
* Keep your deployment updated to the most recent stable release
|
||||||
|
|
||||||
|
## Reporting a Suspected Vulnerability
|
||||||
|
|
||||||
|
If you believe you've uncovered a security vulnerability and wish to report it confidentially, you may do so via email. Please note that any reported vulnerabilities **MUST** meet all the following conditions:
|
||||||
|
|
||||||
|
* Affects the most recent stable release of NetBox, or a current beta release
|
||||||
|
* Affects a NetBox instance installed and configured per the official documentation
|
||||||
|
* Is reproducible following a prescribed set of instructions
|
||||||
|
|
||||||
|
Please note that we **DO NOT** accept reports generated by automated tooling which merely suggest that a file or file(s) _may_ be vulnerable under certain conditions, as these are most often innocuous.
|
||||||
|
|
||||||
|
If you believe that you've found a vulnerability which meets all of these conditions, please email a brief description of the suspected bug and instructions for reproduction to **security@netbox.dev**. For any security concerns regarding NetBox deployed via Docker, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project.
|
||||||
|
|
||||||
|
### Bug Bounties
|
||||||
|
|
||||||
|
As NetBox is provided as free open source software, we do not offer any monetary compensation for vulnerability or bug reports, however your contributions are greatly appreciated.
|
@ -2,6 +2,25 @@
|
|||||||
|
|
||||||
## v3.2.3 (FUTURE)
|
## v3.2.3 (FUTURE)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#8894](https://github.com/netbox-community/netbox/issues/8894) - Include full names when listing users
|
||||||
|
* [#8998](https://github.com/netbox-community/netbox/issues/8998) - Enable filtering racks & reservations by site group
|
||||||
|
* [#9122](https://github.com/netbox-community/netbox/issues/9122) - Introduce `clearcache` management command & clear cache during upgrade
|
||||||
|
* [#9260](https://github.com/netbox-community/netbox/issues/9260) - Apply user preferences to tables under object detail views
|
||||||
|
* [#9278](https://github.com/netbox-community/netbox/issues/9278) - Linkify device types count under manufacturers list
|
||||||
|
* [#9280](https://github.com/netbox-community/netbox/issues/9280) - Allow adopting existing components when installing a module
|
||||||
|
* [#9314](https://github.com/netbox-community/netbox/issues/9314) - Add device and VM filters for FHRP group assignments
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#9190](https://github.com/netbox-community/netbox/issues/9190) - Prevent exception when attempting to instantiate module components which already exist on the parent device
|
||||||
|
* [#9267](https://github.com/netbox-community/netbox/issues/9267) - Remove invalid entry in IP address role choices
|
||||||
|
* [#9306](https://github.com/netbox-community/netbox/issues/9306) - Include VC master interfaces when selecting a LAG/bridge for a VC member interface
|
||||||
|
* [#9311](https://github.com/netbox-community/netbox/issues/9311) - Permit creating contact assignment without a priority via the REST API
|
||||||
|
* [#9313](https://github.com/netbox-community/netbox/issues/9313) - Remove HTML code from CSV output of many-to-many relationships
|
||||||
|
* [#9330](https://github.com/netbox-community/netbox/issues/9330) - Add missing `module_type` field to REST API serializers for modular device component templates
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v3.2.2 (2022-04-28)
|
## v3.2.2 (2022-04-28)
|
||||||
|
@ -59,7 +59,7 @@ class CircuitTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
commit_rate = CommitRateColumn()
|
commit_rate = CommitRateColumn()
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
contacts = tables.ManyToManyColumn(
|
contacts = columns.ManyToManyColumn(
|
||||||
linkify_item=True
|
linkify_item=True
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
|
@ -14,7 +14,7 @@ class ProviderTable(NetBoxTable):
|
|||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
asns = tables.ManyToManyColumn(
|
asns = columns.ManyToManyColumn(
|
||||||
linkify_item=True,
|
linkify_item=True,
|
||||||
verbose_name='ASNs'
|
verbose_name='ASNs'
|
||||||
)
|
)
|
||||||
@ -31,7 +31,7 @@ class ProviderTable(NetBoxTable):
|
|||||||
verbose_name='Circuits'
|
verbose_name='Circuits'
|
||||||
)
|
)
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
contacts = tables.ManyToManyColumn(
|
contacts = columns.ManyToManyColumn(
|
||||||
linkify_item=True
|
linkify_item=True
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
|
@ -32,7 +32,7 @@ class ProviderView(generic.ObjectView):
|
|||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'type', 'tenant', 'terminations__site'
|
'type', 'tenant', 'terminations__site'
|
||||||
)
|
)
|
||||||
circuits_table = tables.CircuitTable(circuits, exclude=('provider',))
|
circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('provider',))
|
||||||
circuits_table.configure(request)
|
circuits_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -93,7 +93,7 @@ class ProviderNetworkView(generic.ObjectView):
|
|||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'type', 'tenant', 'terminations__site'
|
'type', 'tenant', 'terminations__site'
|
||||||
)
|
)
|
||||||
circuits_table = tables.CircuitTable(circuits)
|
circuits_table = tables.CircuitTable(circuits, user=request.user)
|
||||||
circuits_table.configure(request)
|
circuits_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -147,7 +147,7 @@ class CircuitTypeView(generic.ObjectView):
|
|||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance)
|
circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance)
|
||||||
circuits_table = tables.CircuitTable(circuits, exclude=('type',))
|
circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('type',))
|
||||||
circuits_table.configure(request)
|
circuits_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -315,7 +315,16 @@ class ModuleTypeSerializer(NetBoxModelSerializer):
|
|||||||
|
|
||||||
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
module_type = NestedModuleTypeSerializer(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
@ -325,13 +334,23 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePortTemplate
|
model = ConsolePortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'description', 'created', 'last_updated',
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created',
|
||||||
|
'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail')
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
module_type = NestedModuleTypeSerializer(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
@ -341,13 +360,23 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPortTemplate
|
model = ConsoleServerPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'description', 'created', 'last_updated',
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created',
|
||||||
|
'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
module_type = NestedModuleTypeSerializer(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=PowerPortTypeChoices,
|
choices=PowerPortTypeChoices,
|
||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
@ -357,14 +386,23 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPortTemplate
|
model = PowerPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw',
|
||||||
'description', 'created', 'last_updated',
|
'allocated_draw', 'description', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail')
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
module_type = NestedModuleTypeSerializer(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=PowerOutletTypeChoices,
|
choices=PowerOutletTypeChoices,
|
||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
@ -383,48 +421,75 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutletTemplate
|
model = PowerOutletTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg',
|
||||||
'created', 'last_updated',
|
'description', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail')
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
module_type = NestedModuleTypeSerializer(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceTemplate
|
model = InterfaceTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'created',
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description',
|
||||||
'last_updated',
|
'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class RearPortTemplateSerializer(ValidatedModelSerializer):
|
class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
module_type = NestedModuleTypeSerializer(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RearPortTemplate
|
model = RearPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'color', 'positions', 'description',
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
|
||||||
'created', 'last_updated',
|
'description', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
module_type = NestedModuleTypeSerializer(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
rear_port = NestedRearPortTemplateSerializer()
|
rear_port = NestedRearPortTemplateSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port',
|
||||||
'description', 'created', 'last_updated',
|
'rear_port_position', 'description', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,6 +62,8 @@ POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
|
|||||||
# Device components
|
# Device components
|
||||||
#
|
#
|
||||||
|
|
||||||
|
MODULE_TOKEN = '{module}'
|
||||||
|
|
||||||
MODULAR_COMPONENT_TEMPLATE_MODELS = Q(
|
MODULAR_COMPONENT_TEMPLATE_MODELS = Q(
|
||||||
app_label='dcim',
|
app_label='dcim',
|
||||||
model__in=(
|
model__in=(
|
||||||
|
@ -346,6 +346,32 @@ class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
field_name='rack__site__region',
|
||||||
|
lookup_expr='in',
|
||||||
|
label='Region (ID)',
|
||||||
|
)
|
||||||
|
region = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
field_name='rack__site__region',
|
||||||
|
lookup_expr='in',
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Region (slug)',
|
||||||
|
)
|
||||||
|
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=SiteGroup.objects.all(),
|
||||||
|
field_name='rack__site__group',
|
||||||
|
lookup_expr='in',
|
||||||
|
label='Site group (ID)',
|
||||||
|
)
|
||||||
|
site_group = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=SiteGroup.objects.all(),
|
||||||
|
field_name='rack__site__group',
|
||||||
|
lookup_expr='in',
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Site group (slug)',
|
||||||
|
)
|
||||||
location_id = TreeNodeMultipleChoiceFilter(
|
location_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Location.objects.all(),
|
queryset=Location.objects.all(),
|
||||||
field_name='rack__location',
|
field_name='rack__location',
|
||||||
|
@ -210,7 +210,7 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
|||||||
model = Rack
|
model = Rack
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Location', ('region_id', 'site_id', 'location_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
||||||
('Function', ('status', 'role_id')),
|
('Function', ('status', 'role_id')),
|
||||||
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
|
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
@ -229,6 +229,11 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
|||||||
},
|
},
|
||||||
label=_('Site')
|
label=_('Site')
|
||||||
)
|
)
|
||||||
|
site_group_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=SiteGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Site group')
|
||||||
|
)
|
||||||
location_id = DynamicModelMultipleChoiceField(
|
location_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Location.objects.all(),
|
queryset=Location.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -282,7 +287,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('User', ('user_id',)),
|
('User', ('user_id',)),
|
||||||
('Rack', ('region_id', 'site_id', 'location_id')),
|
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
@ -298,6 +303,11 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
},
|
},
|
||||||
label=_('Site')
|
label=_('Site')
|
||||||
)
|
)
|
||||||
|
site_group_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=SiteGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Site group')
|
||||||
|
)
|
||||||
location_id = DynamicModelMultipleChoiceField(
|
location_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Location.objects.prefetch_related('site'),
|
queryset=Location.objects.prefetch_related('site'),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -633,12 +633,18 @@ class ModuleForm(NetBoxModelForm):
|
|||||||
help_text="Automatically populate components associated with this module type"
|
help_text="Automatically populate components associated with this module type"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
adopt_components = forms.BooleanField(
|
||||||
|
required=False,
|
||||||
|
initial=False,
|
||||||
|
help_text="Adopt already existing components"
|
||||||
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Module', (
|
('Module', (
|
||||||
'device', 'module_bay', 'manufacturer', 'module_type', 'tags',
|
'device', 'module_bay', 'manufacturer', 'module_type', 'tags',
|
||||||
)),
|
)),
|
||||||
('Hardware', (
|
('Hardware', (
|
||||||
'serial', 'asset_tag', 'replicate_components',
|
'serial', 'asset_tag', 'replicate_components', 'adopt_components',
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -646,7 +652,7 @@ class ModuleForm(NetBoxModelForm):
|
|||||||
model = Module
|
model = Module
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'tags',
|
'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'tags',
|
||||||
'replicate_components', 'comments',
|
'replicate_components', 'adopt_components', 'comments',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -655,6 +661,8 @@ class ModuleForm(NetBoxModelForm):
|
|||||||
if self.instance.pk:
|
if self.instance.pk:
|
||||||
self.fields['replicate_components'].initial = False
|
self.fields['replicate_components'].initial = False
|
||||||
self.fields['replicate_components'].disabled = True
|
self.fields['replicate_components'].disabled = True
|
||||||
|
self.fields['adopt_components'].initial = False
|
||||||
|
self.fields['adopt_components'].disabled = True
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
@ -662,8 +670,62 @@ class ModuleForm(NetBoxModelForm):
|
|||||||
if self.instance.pk or not self.cleaned_data['replicate_components']:
|
if self.instance.pk or not self.cleaned_data['replicate_components']:
|
||||||
self.instance._disable_replication = True
|
self.instance._disable_replication = True
|
||||||
|
|
||||||
|
if self.cleaned_data['adopt_components']:
|
||||||
|
self.instance._adopt_components = True
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
replicate_components = self.cleaned_data.get("replicate_components")
|
||||||
|
adopt_components = self.cleaned_data.get("adopt_components")
|
||||||
|
device = self.cleaned_data['device']
|
||||||
|
module_type = self.cleaned_data['module_type']
|
||||||
|
module_bay = self.cleaned_data['module_bay']
|
||||||
|
|
||||||
|
# Bail out if we are not installing a new module or if we are not replicating components
|
||||||
|
if self.instance.pk or not replicate_components:
|
||||||
|
return
|
||||||
|
|
||||||
|
for templates, component_attribute in [
|
||||||
|
("consoleporttemplates", "consoleports"),
|
||||||
|
("consoleserverporttemplates", "consoleserverports"),
|
||||||
|
("interfacetemplates", "interfaces"),
|
||||||
|
("powerporttemplates", "powerports"),
|
||||||
|
("poweroutlettemplates", "poweroutlets"),
|
||||||
|
("rearporttemplates", "rearports"),
|
||||||
|
("frontporttemplates", "frontports")
|
||||||
|
]:
|
||||||
|
# Prefetch installed components
|
||||||
|
installed_components = {
|
||||||
|
component.name: component for component in getattr(device, component_attribute).all()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the templates for the module type.
|
||||||
|
for template in getattr(module_type, templates).all():
|
||||||
|
# Installing modules with placeholders require that the bay has a position value
|
||||||
|
if MODULE_TOKEN in template.name and not module_bay.position:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
"Cannot install module with placeholder values in a module bay with no position defined"
|
||||||
|
)
|
||||||
|
|
||||||
|
resolved_name = template.name.replace(MODULE_TOKEN, module_bay.position)
|
||||||
|
existing_item = installed_components.get(resolved_name)
|
||||||
|
|
||||||
|
# It is not possible to adopt components already belonging to a module
|
||||||
|
if adopt_components and existing_item and existing_item.module:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
f"Cannot adopt {template.component_model.__name__} '{resolved_name}' as it already belongs "
|
||||||
|
f"to a module"
|
||||||
|
)
|
||||||
|
|
||||||
|
# If we are not adopting components we error if the component exists
|
||||||
|
if not adopt_components and resolved_name in installed_components:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
f"{template.component_model.__name__} - {resolved_name} already exists"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CableForm(TenancyForm, NetBoxModelForm):
|
class CableForm(TenancyForm, NetBoxModelForm):
|
||||||
|
|
||||||
@ -1284,6 +1346,16 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
|
|||||||
'rf_channel_width': "Populated by selected channel (if set)",
|
'rf_channel_width': "Populated by selected channel (if set)",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Restrict LAG/bridge interface assignment by device/VC
|
||||||
|
device_id = self.data['device'] if self.is_bound else self.initial.get('device')
|
||||||
|
device = Device.objects.filter(pk=device_id).first()
|
||||||
|
if device and device.virtual_chassis and device.virtual_chassis.master:
|
||||||
|
self.fields['lag'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
|
||||||
|
self.fields['bridge'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
|
||||||
|
|
||||||
|
|
||||||
class FrontPortForm(NetBoxModelForm):
|
class FrontPortForm(NetBoxModelForm):
|
||||||
module = DynamicModelChoiceField(
|
module = DynamicModelChoiceField(
|
||||||
|
@ -121,12 +121,12 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
|||||||
|
|
||||||
def resolve_name(self, module):
|
def resolve_name(self, module):
|
||||||
if module:
|
if module:
|
||||||
return self.name.replace('{module}', module.module_bay.position)
|
return self.name.replace(MODULE_TOKEN, module.module_bay.position)
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def resolve_label(self, module):
|
def resolve_label(self, module):
|
||||||
if module:
|
if module:
|
||||||
return self.label.replace('{module}', module.module_bay.position)
|
return self.label.replace(MODULE_TOKEN, module.module_bay.position)
|
||||||
return self.label
|
return self.label
|
||||||
|
|
||||||
|
|
||||||
|
@ -543,7 +543,8 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
|
|||||||
)
|
)
|
||||||
speed = models.PositiveIntegerField(
|
speed = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True,
|
||||||
|
verbose_name='Speed (Kbps)'
|
||||||
)
|
)
|
||||||
duplex = models.CharField(
|
duplex = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
|
@ -1065,30 +1065,52 @@ class Module(NetBoxModel, ConfigContextModel):
|
|||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
# If this is a new Module and component replication has not been disabled, instantiate all its
|
adopt_components = getattr(self, '_adopt_components', False)
|
||||||
# related components per the ModuleType definition
|
disable_replication = getattr(self, '_disable_replication', False)
|
||||||
if is_new and not getattr(self, '_disable_replication', False):
|
|
||||||
ConsolePort.objects.bulk_create(
|
# We skip adding components if the module is being edited or
|
||||||
[x.instantiate(device=self.device, module=self) for x in self.module_type.consoleporttemplates.all()]
|
# both replication and component adoption is disabled
|
||||||
)
|
if not is_new or (disable_replication and not adopt_components):
|
||||||
ConsoleServerPort.objects.bulk_create(
|
return
|
||||||
[x.instantiate(device=self.device, module=self) for x in self.module_type.consoleserverporttemplates.all()]
|
|
||||||
)
|
# Iterate all component types
|
||||||
PowerPort.objects.bulk_create(
|
for templates, component_attribute, component_model in [
|
||||||
[x.instantiate(device=self.device, module=self) for x in self.module_type.powerporttemplates.all()]
|
("consoleporttemplates", "consoleports", ConsolePort),
|
||||||
)
|
("consoleserverporttemplates", "consoleserverports", ConsoleServerPort),
|
||||||
PowerOutlet.objects.bulk_create(
|
("interfacetemplates", "interfaces", Interface),
|
||||||
[x.instantiate(device=self.device, module=self) for x in self.module_type.poweroutlettemplates.all()]
|
("powerporttemplates", "powerports", PowerPort),
|
||||||
)
|
("poweroutlettemplates", "poweroutlets", PowerOutlet),
|
||||||
Interface.objects.bulk_create(
|
("rearporttemplates", "rearports", RearPort),
|
||||||
[x.instantiate(device=self.device, module=self) for x in self.module_type.interfacetemplates.all()]
|
("frontporttemplates", "frontports", FrontPort)
|
||||||
)
|
]:
|
||||||
RearPort.objects.bulk_create(
|
create_instances = []
|
||||||
[x.instantiate(device=self.device, module=self) for x in self.module_type.rearporttemplates.all()]
|
update_instances = []
|
||||||
)
|
|
||||||
FrontPort.objects.bulk_create(
|
# Prefetch installed components
|
||||||
[x.instantiate(device=self.device, module=self) for x in self.module_type.frontporttemplates.all()]
|
installed_components = {
|
||||||
)
|
component.name: component for component in getattr(self.device, component_attribute).filter(module__isnull=True)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the template for the module type.
|
||||||
|
for template in getattr(self.module_type, templates).all():
|
||||||
|
template_instance = template.instantiate(device=self.device, module=self)
|
||||||
|
|
||||||
|
if adopt_components:
|
||||||
|
existing_item = installed_components.get(template_instance.name)
|
||||||
|
|
||||||
|
# Check if there's a component with the same name already
|
||||||
|
if existing_item:
|
||||||
|
# Assign it to the module
|
||||||
|
existing_item.module = self
|
||||||
|
update_instances.append(existing_item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Only create new components if replication is enabled
|
||||||
|
if not disable_replication:
|
||||||
|
create_instances.append(template_instance)
|
||||||
|
|
||||||
|
component_model.objects.bulk_create(create_instances)
|
||||||
|
component_model.objects.bulk_update(update_instances, ['module'])
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -190,7 +190,7 @@ class DeviceTable(NetBoxTable):
|
|||||||
verbose_name='VC Priority'
|
verbose_name='VC Priority'
|
||||||
)
|
)
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
contacts = tables.ManyToManyColumn(
|
contacts = columns.ManyToManyColumn(
|
||||||
linkify_item=True
|
linkify_item=True
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
|
@ -31,7 +31,9 @@ class ManufacturerTable(NetBoxTable):
|
|||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
devicetype_count = tables.Column(
|
devicetype_count = columns.LinkedCountColumn(
|
||||||
|
viewname='dcim:devicetype_list',
|
||||||
|
url_params={'manufacturer_id': 'pk'},
|
||||||
verbose_name='Device Types'
|
verbose_name='Device Types'
|
||||||
)
|
)
|
||||||
inventoryitem_count = tables.Column(
|
inventoryitem_count = tables.Column(
|
||||||
@ -41,7 +43,7 @@ class ManufacturerTable(NetBoxTable):
|
|||||||
verbose_name='Platforms'
|
verbose_name='Platforms'
|
||||||
)
|
)
|
||||||
slug = tables.Column()
|
slug = tables.Column()
|
||||||
contacts = tables.ManyToManyColumn(
|
contacts = columns.ManyToManyColumn(
|
||||||
linkify_item=True
|
linkify_item=True
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
|
@ -26,7 +26,7 @@ class PowerPanelTable(NetBoxTable):
|
|||||||
url_params={'power_panel_id': 'pk'},
|
url_params={'power_panel_id': 'pk'},
|
||||||
verbose_name='Feeds'
|
verbose_name='Feeds'
|
||||||
)
|
)
|
||||||
contacts = tables.ManyToManyColumn(
|
contacts = columns.ManyToManyColumn(
|
||||||
linkify_item=True
|
linkify_item=True
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
|
@ -69,7 +69,7 @@ class RackTable(NetBoxTable):
|
|||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Power'
|
verbose_name='Power'
|
||||||
)
|
)
|
||||||
contacts = tables.ManyToManyColumn(
|
contacts = columns.ManyToManyColumn(
|
||||||
linkify_item=True
|
linkify_item=True
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
|
@ -26,7 +26,7 @@ class RegionTable(NetBoxTable):
|
|||||||
url_params={'region_id': 'pk'},
|
url_params={'region_id': 'pk'},
|
||||||
verbose_name='Sites'
|
verbose_name='Sites'
|
||||||
)
|
)
|
||||||
contacts = tables.ManyToManyColumn(
|
contacts = columns.ManyToManyColumn(
|
||||||
linkify_item=True
|
linkify_item=True
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
@ -55,7 +55,7 @@ class SiteGroupTable(NetBoxTable):
|
|||||||
url_params={'group_id': 'pk'},
|
url_params={'group_id': 'pk'},
|
||||||
verbose_name='Sites'
|
verbose_name='Sites'
|
||||||
)
|
)
|
||||||
contacts = tables.ManyToManyColumn(
|
contacts = columns.ManyToManyColumn(
|
||||||
linkify_item=True
|
linkify_item=True
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
@ -86,7 +86,7 @@ class SiteTable(NetBoxTable):
|
|||||||
group = tables.Column(
|
group = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
asns = tables.ManyToManyColumn(
|
asns = columns.ManyToManyColumn(
|
||||||
linkify_item=True,
|
linkify_item=True,
|
||||||
verbose_name='ASNs'
|
verbose_name='ASNs'
|
||||||
)
|
)
|
||||||
@ -98,7 +98,7 @@ class SiteTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
contacts = tables.ManyToManyColumn(
|
contacts = columns.ManyToManyColumn(
|
||||||
linkify_item=True
|
linkify_item=True
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
@ -137,7 +137,7 @@ class LocationTable(NetBoxTable):
|
|||||||
url_params={'location_id': 'pk'},
|
url_params={'location_id': 'pk'},
|
||||||
verbose_name='Devices'
|
verbose_name='Devices'
|
||||||
)
|
)
|
||||||
contacts = tables.ManyToManyColumn(
|
contacts = columns.ManyToManyColumn(
|
||||||
linkify_item=True
|
linkify_item=True
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
|
@ -523,6 +523,9 @@ class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
devicetype = DeviceType.objects.create(
|
devicetype = DeviceType.objects.create(
|
||||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||||
)
|
)
|
||||||
|
moduletype = ModuleType.objects.create(
|
||||||
|
manufacturer=manufacturer, model='Module Type 1'
|
||||||
|
)
|
||||||
|
|
||||||
console_port_templates = (
|
console_port_templates = (
|
||||||
ConsolePortTemplate(device_type=devicetype, name='Console Port Template 1'),
|
ConsolePortTemplate(device_type=devicetype, name='Console Port Template 1'),
|
||||||
@ -541,9 +544,13 @@ class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'name': 'Console Port Template 5',
|
'name': 'Console Port Template 5',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device_type': devicetype.pk,
|
'module_type': moduletype.pk,
|
||||||
'name': 'Console Port Template 6',
|
'name': 'Console Port Template 6',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'module_type': moduletype.pk,
|
||||||
|
'name': 'Console Port Template 7',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -560,6 +567,9 @@ class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
devicetype = DeviceType.objects.create(
|
devicetype = DeviceType.objects.create(
|
||||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||||
)
|
)
|
||||||
|
moduletype = ModuleType.objects.create(
|
||||||
|
manufacturer=manufacturer, model='Module Type 1'
|
||||||
|
)
|
||||||
|
|
||||||
console_server_port_templates = (
|
console_server_port_templates = (
|
||||||
ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 1'),
|
ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 1'),
|
||||||
@ -578,9 +588,13 @@ class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'name': 'Console Server Port Template 5',
|
'name': 'Console Server Port Template 5',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device_type': devicetype.pk,
|
'module_type': moduletype.pk,
|
||||||
'name': 'Console Server Port Template 6',
|
'name': 'Console Server Port Template 6',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'module_type': moduletype.pk,
|
||||||
|
'name': 'Console Server Port Template 7',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -597,6 +611,9 @@ class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
devicetype = DeviceType.objects.create(
|
devicetype = DeviceType.objects.create(
|
||||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||||
)
|
)
|
||||||
|
moduletype = ModuleType.objects.create(
|
||||||
|
manufacturer=manufacturer, model='Module Type 1'
|
||||||
|
)
|
||||||
|
|
||||||
power_port_templates = (
|
power_port_templates = (
|
||||||
PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
|
PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
|
||||||
@ -615,9 +632,13 @@ class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'name': 'Power Port Template 5',
|
'name': 'Power Port Template 5',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device_type': devicetype.pk,
|
'module_type': moduletype.pk,
|
||||||
'name': 'Power Port Template 6',
|
'name': 'Power Port Template 6',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'module_type': moduletype.pk,
|
||||||
|
'name': 'Power Port Template 7',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -634,6 +655,9 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
devicetype = DeviceType.objects.create(
|
devicetype = DeviceType.objects.create(
|
||||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||||
)
|
)
|
||||||
|
moduletype = ModuleType.objects.create(
|
||||||
|
manufacturer=manufacturer, model='Module Type 1'
|
||||||
|
)
|
||||||
|
|
||||||
power_port_templates = (
|
power_port_templates = (
|
||||||
PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
|
PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
|
||||||
@ -664,6 +688,14 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'name': 'Power Outlet Template 6',
|
'name': 'Power Outlet Template 6',
|
||||||
'power_port': None,
|
'power_port': None,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'module_type': moduletype.pk,
|
||||||
|
'name': 'Power Outlet Template 7',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'module_type': moduletype.pk,
|
||||||
|
'name': 'Power Outlet Template 8',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -680,6 +712,9 @@ class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
devicetype = DeviceType.objects.create(
|
devicetype = DeviceType.objects.create(
|
||||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||||
)
|
)
|
||||||
|
moduletype = ModuleType.objects.create(
|
||||||
|
manufacturer=manufacturer, model='Module Type 1'
|
||||||
|
)
|
||||||
|
|
||||||
interface_templates = (
|
interface_templates = (
|
||||||
InterfaceTemplate(device_type=devicetype, name='Interface Template 1', type='1000base-t'),
|
InterfaceTemplate(device_type=devicetype, name='Interface Template 1', type='1000base-t'),
|
||||||
@ -700,10 +735,15 @@ class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'type': '1000base-t',
|
'type': '1000base-t',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device_type': devicetype.pk,
|
'module_type': moduletype.pk,
|
||||||
'name': 'Interface Template 6',
|
'name': 'Interface Template 6',
|
||||||
'type': '1000base-t',
|
'type': '1000base-t',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'module_type': moduletype.pk,
|
||||||
|
'name': 'Interface Template 7',
|
||||||
|
'type': '1000base-t',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -720,14 +760,19 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
devicetype = DeviceType.objects.create(
|
devicetype = DeviceType.objects.create(
|
||||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||||
)
|
)
|
||||||
|
moduletype = ModuleType.objects.create(
|
||||||
|
manufacturer=manufacturer, model='Module Type 1'
|
||||||
|
)
|
||||||
|
|
||||||
rear_port_templates = (
|
rear_port_templates = (
|
||||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1', type=PortTypeChoices.TYPE_8P8C),
|
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1', type=PortTypeChoices.TYPE_8P8C),
|
||||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2', type=PortTypeChoices.TYPE_8P8C),
|
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2', type=PortTypeChoices.TYPE_8P8C),
|
||||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 3', type=PortTypeChoices.TYPE_8P8C),
|
RearPortTemplate(device_type=devicetype, name='Rear Port Template 3', type=PortTypeChoices.TYPE_8P8C),
|
||||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 4', type=PortTypeChoices.TYPE_8P8C),
|
RearPortTemplate(device_type=devicetype, name='Rear Port Template 4', type=PortTypeChoices.TYPE_8P8C),
|
||||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 5', type=PortTypeChoices.TYPE_8P8C),
|
RearPortTemplate(module_type=moduletype, name='Rear Port Template 5', type=PortTypeChoices.TYPE_8P8C),
|
||||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 6', type=PortTypeChoices.TYPE_8P8C),
|
RearPortTemplate(module_type=moduletype, name='Rear Port Template 6', type=PortTypeChoices.TYPE_8P8C),
|
||||||
|
RearPortTemplate(module_type=moduletype, name='Rear Port Template 7', type=PortTypeChoices.TYPE_8P8C),
|
||||||
|
RearPortTemplate(module_type=moduletype, name='Rear Port Template 8', type=PortTypeChoices.TYPE_8P8C),
|
||||||
)
|
)
|
||||||
RearPortTemplate.objects.bulk_create(rear_port_templates)
|
RearPortTemplate.objects.bulk_create(rear_port_templates)
|
||||||
|
|
||||||
@ -745,15 +790,28 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
rear_port=rear_port_templates[1]
|
rear_port=rear_port_templates[1]
|
||||||
),
|
),
|
||||||
FrontPortTemplate(
|
FrontPortTemplate(
|
||||||
device_type=devicetype,
|
module_type=moduletype,
|
||||||
name='Front Port Template 3',
|
name='Front Port Template 5',
|
||||||
type=PortTypeChoices.TYPE_8P8C,
|
type=PortTypeChoices.TYPE_8P8C,
|
||||||
rear_port=rear_port_templates[2]
|
rear_port=rear_port_templates[4]
|
||||||
|
),
|
||||||
|
FrontPortTemplate(
|
||||||
|
module_type=moduletype,
|
||||||
|
name='Front Port Template 6',
|
||||||
|
type=PortTypeChoices.TYPE_8P8C,
|
||||||
|
rear_port=rear_port_templates[5]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
FrontPortTemplate.objects.bulk_create(front_port_templates)
|
FrontPortTemplate.objects.bulk_create(front_port_templates)
|
||||||
|
|
||||||
cls.create_data = [
|
cls.create_data = [
|
||||||
|
{
|
||||||
|
'device_type': devicetype.pk,
|
||||||
|
'name': 'Front Port Template 3',
|
||||||
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
|
'rear_port': rear_port_templates[2].pk,
|
||||||
|
'rear_port_position': 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'device_type': devicetype.pk,
|
'device_type': devicetype.pk,
|
||||||
'name': 'Front Port Template 4',
|
'name': 'Front Port Template 4',
|
||||||
@ -762,17 +820,17 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'rear_port_position': 1,
|
'rear_port_position': 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device_type': devicetype.pk,
|
'module_type': moduletype.pk,
|
||||||
'name': 'Front Port Template 5',
|
'name': 'Front Port Template 7',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
'rear_port': rear_port_templates[4].pk,
|
'rear_port': rear_port_templates[6].pk,
|
||||||
'rear_port_position': 1,
|
'rear_port_position': 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device_type': devicetype.pk,
|
'module_type': moduletype.pk,
|
||||||
'name': 'Front Port Template 6',
|
'name': 'Front Port Template 8',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
'rear_port': rear_port_templates[5].pk,
|
'rear_port': rear_port_templates[7].pk,
|
||||||
'rear_port_position': 1,
|
'rear_port_position': 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -791,6 +849,9 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
devicetype = DeviceType.objects.create(
|
devicetype = DeviceType.objects.create(
|
||||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||||
)
|
)
|
||||||
|
moduletype = ModuleType.objects.create(
|
||||||
|
manufacturer=manufacturer, model='Module Type 1'
|
||||||
|
)
|
||||||
|
|
||||||
rear_port_templates = (
|
rear_port_templates = (
|
||||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1', type=PortTypeChoices.TYPE_8P8C),
|
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1', type=PortTypeChoices.TYPE_8P8C),
|
||||||
@ -811,10 +872,15 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device_type': devicetype.pk,
|
'module_type': moduletype.pk,
|
||||||
'name': 'Rear Port Template 6',
|
'name': 'Rear Port Template 6',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'module_type': moduletype.pk,
|
||||||
|
'name': 'Rear Port Template 7',
|
||||||
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -521,10 +521,26 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
regions = (
|
||||||
|
Region(name='Region 1', slug='region-1'),
|
||||||
|
Region(name='Region 2', slug='region-2'),
|
||||||
|
Region(name='Region 3', slug='region-3'),
|
||||||
|
)
|
||||||
|
for region in regions:
|
||||||
|
region.save()
|
||||||
|
|
||||||
|
groups = (
|
||||||
|
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||||
|
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||||
|
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||||
|
)
|
||||||
|
for group in groups:
|
||||||
|
group.save()
|
||||||
|
|
||||||
sites = (
|
sites = (
|
||||||
Site(name='Site 1', slug='site-1'),
|
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
|
||||||
Site(name='Site 2', slug='site-2'),
|
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
|
||||||
Site(name='Site 3', slug='site-3'),
|
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||||
)
|
)
|
||||||
Site.objects.bulk_create(sites)
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
@ -572,6 +588,20 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
RackReservation.objects.bulk_create(reservations)
|
RackReservation.objects.bulk_create(reservations)
|
||||||
|
|
||||||
|
def test_region(self):
|
||||||
|
regions = Region.objects.all()[:2]
|
||||||
|
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_site_group(self):
|
||||||
|
site_groups = SiteGroup.objects.all()[:2]
|
||||||
|
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_site(self):
|
def test_site(self):
|
||||||
sites = Site.objects.all()[:2]
|
sites = Site.objects.all()[:2]
|
||||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||||
|
@ -1869,6 +1869,44 @@ class ModuleTestCase(
|
|||||||
self.assertHttpStatus(self.client.post(**request), 302)
|
self.assertHttpStatus(self.client.post(**request), 302)
|
||||||
self.assertEqual(Interface.objects.filter(device=device).count(), 5)
|
self.assertEqual(Interface.objects.filter(device=device).count(), 5)
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_module_component_adoption(self):
|
||||||
|
self.add_permissions('dcim.add_module')
|
||||||
|
|
||||||
|
interface_name = "Interface-1"
|
||||||
|
|
||||||
|
# Add an interface to the ModuleType
|
||||||
|
module_type = ModuleType.objects.first()
|
||||||
|
InterfaceTemplate(module_type=module_type, name=interface_name).save()
|
||||||
|
|
||||||
|
form_data = self.form_data.copy()
|
||||||
|
device = Device.objects.get(pk=form_data['device'])
|
||||||
|
|
||||||
|
# Create an interface to be adopted
|
||||||
|
interface = Interface(device=device, name=interface_name, type=InterfaceTypeChoices.TYPE_10GE_FIXED)
|
||||||
|
interface.save()
|
||||||
|
|
||||||
|
# Ensure that interface is created with no module
|
||||||
|
self.assertIsNone(interface.module)
|
||||||
|
|
||||||
|
# Create a module with adopted components
|
||||||
|
form_data['module_bay'] = ModuleBay.objects.filter(device=device).first()
|
||||||
|
form_data['module_type'] = module_type
|
||||||
|
form_data['replicate_components'] = False
|
||||||
|
form_data['adopt_components'] = True
|
||||||
|
request = {
|
||||||
|
'path': self._get_url('add'),
|
||||||
|
'data': post_data(form_data),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertHttpStatus(self.client.post(**request), 302)
|
||||||
|
|
||||||
|
# Re-retrieve interface to get new module id
|
||||||
|
interface.refresh_from_db()
|
||||||
|
|
||||||
|
# Check that the Interface now has a module
|
||||||
|
self.assertIsNotNone(interface.module)
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
|
@ -166,7 +166,7 @@ class RegionView(generic.ObjectView):
|
|||||||
sites = Site.objects.restrict(request.user, 'view').filter(
|
sites = Site.objects.restrict(request.user, 'view').filter(
|
||||||
region=instance
|
region=instance
|
||||||
)
|
)
|
||||||
sites_table = tables.SiteTable(sites, exclude=('region',))
|
sites_table = tables.SiteTable(sites, user=request.user, exclude=('region',))
|
||||||
sites_table.configure(request)
|
sites_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -251,7 +251,7 @@ class SiteGroupView(generic.ObjectView):
|
|||||||
sites = Site.objects.restrict(request.user, 'view').filter(
|
sites = Site.objects.restrict(request.user, 'view').filter(
|
||||||
group=instance
|
group=instance
|
||||||
)
|
)
|
||||||
sites_table = tables.SiteTable(sites, exclude=('group',))
|
sites_table = tables.SiteTable(sites, user=request.user, exclude=('group',))
|
||||||
sites_table.configure(request)
|
sites_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -435,7 +435,7 @@ class LocationView(generic.ObjectView):
|
|||||||
'rack_count',
|
'rack_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
).filter(pk__in=location_ids).exclude(pk=instance.pk)
|
).filter(pk__in=location_ids).exclude(pk=instance.pk)
|
||||||
child_locations_table = tables.LocationTable(child_locations)
|
child_locations_table = tables.LocationTable(child_locations, user=request.user)
|
||||||
child_locations_table.configure(request)
|
child_locations_table.configure(request)
|
||||||
|
|
||||||
nonracked_devices = Device.objects.filter(
|
nonracked_devices = Device.objects.filter(
|
||||||
@ -514,7 +514,9 @@ class RackRoleView(generic.ObjectView):
|
|||||||
role=instance
|
role=instance
|
||||||
)
|
)
|
||||||
|
|
||||||
racks_table = tables.RackTable(racks, exclude=('role', 'get_utilization', 'get_power_utilization'))
|
racks_table = tables.RackTable(racks, user=request.user, exclude=(
|
||||||
|
'role', 'get_utilization', 'get_power_utilization',
|
||||||
|
))
|
||||||
racks_table.configure(request)
|
racks_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -767,7 +769,7 @@ class ManufacturerView(generic.ObjectView):
|
|||||||
manufacturer=instance
|
manufacturer=instance
|
||||||
)
|
)
|
||||||
|
|
||||||
devicetypes_table = tables.DeviceTypeTable(device_types, exclude=('manufacturer',))
|
devicetypes_table = tables.DeviceTypeTable(device_types, user=request.user, exclude=('manufacturer',))
|
||||||
devicetypes_table.configure(request)
|
devicetypes_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -1480,7 +1482,7 @@ class DeviceRoleView(generic.ObjectView):
|
|||||||
devices = Device.objects.restrict(request.user, 'view').filter(
|
devices = Device.objects.restrict(request.user, 'view').filter(
|
||||||
device_role=instance
|
device_role=instance
|
||||||
)
|
)
|
||||||
devices_table = tables.DeviceTable(devices, exclude=('device_role',))
|
devices_table = tables.DeviceTable(devices, user=request.user, exclude=('device_role',))
|
||||||
devices_table.configure(request)
|
devices_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -1544,7 +1546,7 @@ class PlatformView(generic.ObjectView):
|
|||||||
devices = Device.objects.restrict(request.user, 'view').filter(
|
devices = Device.objects.restrict(request.user, 'view').filter(
|
||||||
platform=instance
|
platform=instance
|
||||||
)
|
)
|
||||||
devices_table = tables.DeviceTable(devices, exclude=('platform',))
|
devices_table = tables.DeviceTable(devices, user=request.user, exclude=('platform',))
|
||||||
devices_table.configure(request)
|
devices_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
11
netbox/extras/management/commands/clearcache.py
Normal file
11
netbox/extras/management/commands/clearcache.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""Command to clear the entire cache."""
|
||||||
|
help = 'Clears the cache.'
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
cache.clear()
|
||||||
|
self.stdout.write('Cache has been cleared.', ending="\n")
|
@ -91,7 +91,7 @@ class IPAddressRoleChoices(ChoiceSet):
|
|||||||
(ROLE_VRRP, 'VRRP', 'green'),
|
(ROLE_VRRP, 'VRRP', 'green'),
|
||||||
(ROLE_HSRP, 'HSRP', 'green'),
|
(ROLE_HSRP, 'HSRP', 'green'),
|
||||||
(ROLE_GLBP, 'GLBP', 'green'),
|
(ROLE_GLBP, 'GLBP', 'green'),
|
||||||
(ROLE_CARP, 'CARP'), 'green',
|
(ROLE_CARP, 'CARP', 'green'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -681,11 +681,53 @@ class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
queryset=FHRPGroup.objects.all(),
|
queryset=FHRPGroup.objects.all(),
|
||||||
label='Group (ID)',
|
label='Group (ID)',
|
||||||
)
|
)
|
||||||
|
device = MultiValueCharFilter(
|
||||||
|
method='filter_device',
|
||||||
|
field_name='name',
|
||||||
|
label='Device (name)',
|
||||||
|
)
|
||||||
|
device_id = MultiValueNumberFilter(
|
||||||
|
method='filter_device',
|
||||||
|
field_name='pk',
|
||||||
|
label='Device (ID)',
|
||||||
|
)
|
||||||
|
virtual_machine = MultiValueCharFilter(
|
||||||
|
method='filter_virtual_machine',
|
||||||
|
field_name='name',
|
||||||
|
label='Virtual machine (name)',
|
||||||
|
)
|
||||||
|
virtual_machine_id = MultiValueNumberFilter(
|
||||||
|
method='filter_virtual_machine',
|
||||||
|
field_name='pk',
|
||||||
|
label='Virtual machine (ID)',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FHRPGroupAssignment
|
model = FHRPGroupAssignment
|
||||||
fields = ['id', 'group_id', 'interface_type', 'interface_id', 'priority']
|
fields = ['id', 'group_id', 'interface_type', 'interface_id', 'priority']
|
||||||
|
|
||||||
|
def filter_device(self, queryset, name, value):
|
||||||
|
devices = Device.objects.filter(**{f'{name}__in': value})
|
||||||
|
if not devices.exists():
|
||||||
|
return queryset.none()
|
||||||
|
interface_ids = []
|
||||||
|
for device in devices:
|
||||||
|
interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
|
||||||
|
return queryset.filter(
|
||||||
|
Q(interface_type=ContentType.objects.get_for_model(Interface), interface_id__in=interface_ids)
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_virtual_machine(self, queryset, name, value):
|
||||||
|
virtual_machines = VirtualMachine.objects.filter(**{f'{name}__in': value})
|
||||||
|
if not virtual_machines.exists():
|
||||||
|
return queryset.none()
|
||||||
|
interface_ids = []
|
||||||
|
for vm in virtual_machines:
|
||||||
|
interface_ids.extend(vm.interfaces.values_list('id', flat=True))
|
||||||
|
return queryset.filter(
|
||||||
|
Q(interface_type=ContentType.objects.get_for_model(VMInterface), interface_id__in=interface_ids)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupFilterSet(OrganizationalModelFilterSet):
|
class VLANGroupFilterSet(OrganizationalModelFilterSet):
|
||||||
scope_type = ContentTypeFilter()
|
scope_type = ContentTypeFilter()
|
||||||
|
@ -118,7 +118,7 @@ class ASNTable(NetBoxTable):
|
|||||||
url_params={'asn_id': 'pk'},
|
url_params={'asn_id': 'pk'},
|
||||||
verbose_name='Provider Count'
|
verbose_name='Provider Count'
|
||||||
)
|
)
|
||||||
sites = tables.ManyToManyColumn(
|
sites = columns.ManyToManyColumn(
|
||||||
linkify_item=True,
|
linkify_item=True,
|
||||||
verbose_name='Sites'
|
verbose_name='Sites'
|
||||||
)
|
)
|
||||||
|
@ -1024,6 +1024,20 @@ class FHRPGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'priority': [10, 20]}
|
params = {'priority': [10, 20]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_device(self):
|
||||||
|
device = Device.objects.first()
|
||||||
|
params = {'device': [device.name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
params = {'device_id': [device.pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
|
||||||
|
def test_virtual_machine(self):
|
||||||
|
vm = VirtualMachine.objects.first()
|
||||||
|
params = {'virtual_machine': [vm.name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
params = {'virtual_machine_id': [vm.pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = VLANGroup.objects.all()
|
queryset = VLANGroup.objects.all()
|
||||||
|
@ -161,7 +161,7 @@ class RIRView(generic.ObjectView):
|
|||||||
aggregates = Aggregate.objects.restrict(request.user, 'view').filter(rir=instance).annotate(
|
aggregates = Aggregate.objects.restrict(request.user, 'view').filter(rir=instance).annotate(
|
||||||
child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
|
child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
|
||||||
)
|
)
|
||||||
aggregates_table = tables.AggregateTable(aggregates, exclude=('rir', 'utilization'))
|
aggregates_table = tables.AggregateTable(aggregates, user=request.user, exclude=('rir', 'utilization'))
|
||||||
aggregates_table.configure(request)
|
aggregates_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -221,12 +221,12 @@ class ASNView(generic.ObjectView):
|
|||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
# Gather assigned Sites
|
# Gather assigned Sites
|
||||||
sites = instance.sites.restrict(request.user, 'view')
|
sites = instance.sites.restrict(request.user, 'view')
|
||||||
sites_table = SiteTable(sites)
|
sites_table = SiteTable(sites, user=request.user)
|
||||||
sites_table.configure(request)
|
sites_table.configure(request)
|
||||||
|
|
||||||
# Gather assigned Providers
|
# Gather assigned Providers
|
||||||
providers = instance.providers.restrict(request.user, 'view')
|
providers = instance.providers.restrict(request.user, 'view')
|
||||||
providers_table = ProviderTable(providers)
|
providers_table = ProviderTable(providers, user=request.user)
|
||||||
providers_table.configure(request)
|
providers_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -366,7 +366,7 @@ class RoleView(generic.ObjectView):
|
|||||||
role=instance
|
role=instance
|
||||||
)
|
)
|
||||||
|
|
||||||
prefixes_table = tables.PrefixTable(prefixes, exclude=('role', 'utilization'))
|
prefixes_table = tables.PrefixTable(prefixes, user=request.user, exclude=('role', 'utilization'))
|
||||||
prefixes_table.configure(request)
|
prefixes_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -805,7 +805,7 @@ class VLANGroupView(generic.ObjectView):
|
|||||||
vlans_count = vlans.count()
|
vlans_count = vlans.count()
|
||||||
vlans = add_available_vlans(vlans, vlan_group=instance)
|
vlans = add_available_vlans(vlans, vlan_group=instance)
|
||||||
|
|
||||||
vlans_table = tables.VLANTable(vlans, exclude=('group',))
|
vlans_table = tables.VLANTable(vlans, user=request.user, exclude=('group',))
|
||||||
if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'):
|
if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'):
|
||||||
vlans_table.columns.show('pk')
|
vlans_table.columns.show('pk')
|
||||||
vlans_table.configure(request)
|
vlans_table.configure(request)
|
||||||
|
@ -6,7 +6,7 @@ from django.conf import settings
|
|||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.db.models import DateField, DateTimeField
|
from django.db.models import DateField, DateTimeField
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import reverse
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django_tables2.columns import library
|
from django_tables2.columns import library
|
||||||
@ -27,6 +27,7 @@ __all__ = (
|
|||||||
'CustomLinkColumn',
|
'CustomLinkColumn',
|
||||||
'LinkedCountColumn',
|
'LinkedCountColumn',
|
||||||
'MarkdownColumn',
|
'MarkdownColumn',
|
||||||
|
'ManyToManyColumn',
|
||||||
'MPTTColumn',
|
'MPTTColumn',
|
||||||
'TagColumn',
|
'TagColumn',
|
||||||
'TemplateColumn',
|
'TemplateColumn',
|
||||||
@ -35,6 +36,10 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Django-tables2 overrides
|
||||||
|
#
|
||||||
|
|
||||||
@library.register
|
@library.register
|
||||||
class DateColumn(tables.DateColumn):
|
class DateColumn(tables.DateColumn):
|
||||||
"""
|
"""
|
||||||
@ -42,7 +47,6 @@ class DateColumn(tables.DateColumn):
|
|||||||
tables and null when exporting data. It is registered in the tables library to use this class instead of the
|
tables and null when exporting data. It is registered in the tables library to use this class instead of the
|
||||||
default, making this behavior consistent in all fields of type DateField.
|
default, making this behavior consistent in all fields of type DateField.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def value(self, value):
|
def value(self, value):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@ -59,7 +63,6 @@ class DateTimeColumn(tables.DateTimeColumn):
|
|||||||
tables and null when exporting data. It is registered in the tables library to use this class instead of the
|
tables and null when exporting data. It is registered in the tables library to use this class instead of the
|
||||||
default, making this behavior consistent in all fields of type DateTimeField.
|
default, making this behavior consistent in all fields of type DateTimeField.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def value(self, value):
|
def value(self, value):
|
||||||
if value:
|
if value:
|
||||||
return date_format(value, format="SHORT_DATETIME_FORMAT")
|
return date_format(value, format="SHORT_DATETIME_FORMAT")
|
||||||
@ -71,6 +74,39 @@ class DateTimeColumn(tables.DateTimeColumn):
|
|||||||
return cls(**kwargs)
|
return cls(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ManyToManyColumn(tables.ManyToManyColumn):
|
||||||
|
"""
|
||||||
|
Overrides django-tables2's stock ManyToManyColumn to ensure that value() returns only plaintext data.
|
||||||
|
"""
|
||||||
|
def value(self, value):
|
||||||
|
items = [self.transform(item) for item in self.filter(value)]
|
||||||
|
return self.separator.join(items)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateColumn(tables.TemplateColumn):
|
||||||
|
"""
|
||||||
|
Overrides django-tables2's stock TemplateColumn class to render a placeholder symbol if the returned value
|
||||||
|
is an empty string.
|
||||||
|
"""
|
||||||
|
PLACEHOLDER = mark_safe('—')
|
||||||
|
|
||||||
|
def render(self, *args, **kwargs):
|
||||||
|
ret = super().render(*args, **kwargs)
|
||||||
|
if not ret.strip():
|
||||||
|
return self.PLACEHOLDER
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def value(self, **kwargs):
|
||||||
|
ret = super().value(**kwargs)
|
||||||
|
if ret == self.PLACEHOLDER:
|
||||||
|
return ''
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Custom columns
|
||||||
|
#
|
||||||
|
|
||||||
class ToggleColumn(tables.CheckBoxColumn):
|
class ToggleColumn(tables.CheckBoxColumn):
|
||||||
"""
|
"""
|
||||||
Extend CheckBoxColumn to add a "toggle all" checkbox in the column header.
|
Extend CheckBoxColumn to add a "toggle all" checkbox in the column header.
|
||||||
@ -112,26 +148,6 @@ class BooleanColumn(tables.Column):
|
|||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
class TemplateColumn(tables.TemplateColumn):
|
|
||||||
"""
|
|
||||||
Overrides django-tables2's stock TemplateColumn class to render a placeholder symbol if the returned value
|
|
||||||
is an empty string.
|
|
||||||
"""
|
|
||||||
PLACEHOLDER = mark_safe('—')
|
|
||||||
|
|
||||||
def render(self, *args, **kwargs):
|
|
||||||
ret = super().render(*args, **kwargs)
|
|
||||||
if not ret.strip():
|
|
||||||
return self.PLACEHOLDER
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def value(self, **kwargs):
|
|
||||||
ret = super().value(**kwargs)
|
|
||||||
if ret == self.PLACEHOLDER:
|
|
||||||
return ''
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ActionsItem:
|
class ActionsItem:
|
||||||
title: str
|
title: str
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
<div class="float-end">
|
<div class="float-end">
|
||||||
<span class="text-muted">{{ context.weight }}</span>
|
<span class="text-muted">{{ context.weight }}</span>
|
||||||
</div>
|
</div>
|
||||||
<strong>{{ context|linkify:"name" }}"></strong>
|
<strong>{{ context|linkify:"name" }}</strong>
|
||||||
{% if context.description %}
|
{% if context.description %}
|
||||||
<br /><small>{{ context.description }}</small>
|
<br /><small>{{ context.description }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -97,7 +97,7 @@ class ContactAssignmentSerializer(NetBoxModelSerializer):
|
|||||||
object = serializers.SerializerMethodField(read_only=True)
|
object = serializers.SerializerMethodField(read_only=True)
|
||||||
contact = NestedContactSerializer()
|
contact = NestedContactSerializer()
|
||||||
role = NestedContactRoleSerializer(required=False, allow_null=True)
|
role = NestedContactRoleSerializer(required=False, allow_null=True)
|
||||||
priority = ChoiceField(choices=ContactPriorityChoices, required=False)
|
priority = ChoiceField(choices=ContactPriorityChoices, allow_blank=True, required=False, default='')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContactAssignment
|
model = ContactAssignment
|
||||||
|
@ -38,7 +38,7 @@ class TenantTable(NetBoxTable):
|
|||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
contacts = tables.ManyToManyColumn(
|
contacts = columns.ManyToManyColumn(
|
||||||
linkify_item=True
|
linkify_item=True
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
|
@ -35,7 +35,7 @@ class TenantGroupView(generic.ObjectView):
|
|||||||
tenants = Tenant.objects.restrict(request.user, 'view').filter(
|
tenants = Tenant.objects.restrict(request.user, 'view').filter(
|
||||||
group=instance
|
group=instance
|
||||||
)
|
)
|
||||||
tenants_table = tables.TenantTable(tenants, exclude=('group',))
|
tenants_table = tables.TenantTable(tenants, user=request.user, exclude=('group',))
|
||||||
tenants_table.configure(request)
|
tenants_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -184,7 +184,7 @@ class ContactGroupView(generic.ObjectView):
|
|||||||
contacts = Contact.objects.restrict(request.user, 'view').filter(
|
contacts = Contact.objects.restrict(request.user, 'view').filter(
|
||||||
group=instance
|
group=instance
|
||||||
)
|
)
|
||||||
contacts_table = tables.ContactTable(contacts, exclude=('group',))
|
contacts_table = tables.ContactTable(contacts, user=request.user, exclude=('group',))
|
||||||
contacts_table.configure(request)
|
contacts_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -250,7 +250,7 @@ class ContactRoleView(generic.ObjectView):
|
|||||||
contact_assignments = ContactAssignment.objects.restrict(request.user, 'view').filter(
|
contact_assignments = ContactAssignment.objects.restrict(request.user, 'view').filter(
|
||||||
role=instance
|
role=instance
|
||||||
)
|
)
|
||||||
contacts_table = tables.ContactAssignmentTable(contact_assignments)
|
contacts_table = tables.ContactAssignmentTable(contact_assignments, user=request.user)
|
||||||
contacts_table.columns.hide('role')
|
contacts_table.columns.hide('role')
|
||||||
contacts_table.configure(request)
|
contacts_table.configure(request)
|
||||||
|
|
||||||
@ -307,7 +307,7 @@ class ContactView(generic.ObjectView):
|
|||||||
contact_assignments = ContactAssignment.objects.restrict(request.user, 'view').filter(
|
contact_assignments = ContactAssignment.objects.restrict(request.user, 'view').filter(
|
||||||
contact=instance
|
contact=instance
|
||||||
)
|
)
|
||||||
assignments_table = tables.ContactAssignmentTable(contact_assignments)
|
assignments_table = tables.ContactAssignmentTable(contact_assignments, user=request.user)
|
||||||
assignments_table.columns.hide('contact')
|
assignments_table.columns.hide('contact')
|
||||||
assignments_table.configure(request)
|
assignments_table.configure(request)
|
||||||
|
|
||||||
|
@ -28,6 +28,11 @@ class NestedUserSerializer(WritableNestedSerializer):
|
|||||||
model = User
|
model = User
|
||||||
fields = ['id', 'url', 'display', 'username']
|
fields = ['id', 'url', 'display', 'username']
|
||||||
|
|
||||||
|
def get_display(self, obj):
|
||||||
|
if full_name := obj.get_full_name():
|
||||||
|
return f"{obj.username} ({full_name})"
|
||||||
|
return obj.username
|
||||||
|
|
||||||
|
|
||||||
class NestedTokenSerializer(WritableNestedSerializer):
|
class NestedTokenSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='users-api:token-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='users-api:token-detail')
|
||||||
|
@ -45,6 +45,11 @@ class UserSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
def get_display(self, obj):
|
||||||
|
if full_name := obj.get_full_name():
|
||||||
|
return f"{obj.username} ({full_name})"
|
||||||
|
return obj.username
|
||||||
|
|
||||||
|
|
||||||
class GroupSerializer(ValidatedModelSerializer):
|
class GroupSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='users-api:group-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='users-api:group-detail')
|
||||||
|
@ -40,7 +40,7 @@ class ClusterGroupTable(NetBoxTable):
|
|||||||
url_params={'group_id': 'pk'},
|
url_params={'group_id': 'pk'},
|
||||||
verbose_name='Clusters'
|
verbose_name='Clusters'
|
||||||
)
|
)
|
||||||
contacts = tables.ManyToManyColumn(
|
contacts = columns.ManyToManyColumn(
|
||||||
linkify_item=True
|
linkify_item=True
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
@ -83,7 +83,7 @@ class ClusterTable(NetBoxTable):
|
|||||||
verbose_name='VMs'
|
verbose_name='VMs'
|
||||||
)
|
)
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
contacts = tables.ManyToManyColumn(
|
contacts = columns.ManyToManyColumn(
|
||||||
linkify_item=True
|
linkify_item=True
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
|
@ -78,7 +78,7 @@ class VMInterfaceTable(BaseInterfaceTable):
|
|||||||
vrf = tables.Column(
|
vrf = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
contacts = tables.ManyToManyColumn(
|
contacts = columns.ManyToManyColumn(
|
||||||
linkify_item=True
|
linkify_item=True
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
|
@ -39,7 +39,7 @@ class ClusterTypeView(generic.ObjectView):
|
|||||||
device_count=count_related(Device, 'cluster'),
|
device_count=count_related(Device, 'cluster'),
|
||||||
vm_count=count_related(VirtualMachine, 'cluster')
|
vm_count=count_related(VirtualMachine, 'cluster')
|
||||||
)
|
)
|
||||||
clusters_table = tables.ClusterTable(clusters, exclude=('type',))
|
clusters_table = tables.ClusterTable(clusters, user=request.user, exclude=('type',))
|
||||||
clusters_table.configure(request)
|
clusters_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -101,7 +101,7 @@ class ClusterGroupView(generic.ObjectView):
|
|||||||
device_count=count_related(Device, 'cluster'),
|
device_count=count_related(Device, 'cluster'),
|
||||||
vm_count=count_related(VirtualMachine, 'cluster')
|
vm_count=count_related(VirtualMachine, 'cluster')
|
||||||
)
|
)
|
||||||
clusters_table = tables.ClusterTable(clusters, exclude=('group',))
|
clusters_table = tables.ClusterTable(clusters, user=request.user, exclude=('group',))
|
||||||
clusters_table.configure(request)
|
clusters_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -29,7 +29,7 @@ class WirelessLANGroupView(generic.ObjectView):
|
|||||||
wirelesslans = WirelessLAN.objects.restrict(request.user, 'view').filter(
|
wirelesslans = WirelessLAN.objects.restrict(request.user, 'view').filter(
|
||||||
group=instance
|
group=instance
|
||||||
)
|
)
|
||||||
wirelesslans_table = tables.WirelessLANTable(wirelesslans, exclude=('group',))
|
wirelesslans_table = tables.WirelessLANTable(wirelesslans, user=request.user, exclude=('group',))
|
||||||
wirelesslans_table.configure(request)
|
wirelesslans_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -97,7 +97,7 @@ class WirelessLANView(generic.ObjectView):
|
|||||||
attached_interfaces = Interface.objects.restrict(request.user, 'view').filter(
|
attached_interfaces = Interface.objects.restrict(request.user, 'view').filter(
|
||||||
wireless_lans=instance
|
wireless_lans=instance
|
||||||
)
|
)
|
||||||
interfaces_table = tables.WirelessLANInterfacesTable(attached_interfaces)
|
interfaces_table = tables.WirelessLANInterfacesTable(attached_interfaces, user=request.user)
|
||||||
interfaces_table.configure(request)
|
interfaces_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -108,6 +108,11 @@ COMMAND="python3 netbox/manage.py clearsessions"
|
|||||||
echo "Removing expired user sessions ($COMMAND)..."
|
echo "Removing expired user sessions ($COMMAND)..."
|
||||||
eval $COMMAND || exit 1
|
eval $COMMAND || exit 1
|
||||||
|
|
||||||
|
# Clear the cache
|
||||||
|
COMMAND="python3 netbox/manage.py clearcache"
|
||||||
|
echo "Clearing the cache ($COMMAND)..."
|
||||||
|
eval $COMMAND || exit 1
|
||||||
|
|
||||||
if [ -v WARN_MISSING_VENV ]; then
|
if [ -v WARN_MISSING_VENV ]; then
|
||||||
echo "--------------------------------------------------------------------"
|
echo "--------------------------------------------------------------------"
|
||||||
echo "WARNING: No existing virtual environment was detected. A new one has"
|
echo "WARNING: No existing virtual environment was detected. A new one has"
|
||||||
|
Loading…
Reference in New Issue
Block a user