From dbe331afb8171a516acae8a62ac39308786f386c Mon Sep 17 00:00:00 2001 From: Mario Date: Mon, 25 Sep 2023 17:28:32 +0200 Subject: [PATCH] add filters / housekeeping (#384) --- netbox_topology_views/filters.py | 124 +++++++++- netbox_topology_views/forms.py | 400 +++++++++++++++++++------------ 2 files changed, 362 insertions(+), 162 deletions(-) diff --git a/netbox_topology_views/filters.py b/netbox_topology_views/filters.py index 98ca06a..5dd96ad 100644 --- a/netbox_topology_views/filters.py +++ b/netbox_topology_views/filters.py @@ -1,38 +1,59 @@ import django_filters from dcim.choices import DeviceStatusChoices -from dcim.models import Device, DeviceRole, Location, Rack, Region, Site, SiteGroup +from dcim.models import Device, DeviceRole, Location, Rack, Region, Site, SiteGroup, Manufacturer, DeviceType, Platform from django.db.models import Q +from extras.filtersets import LocalConfigContextFilterSet +from extras.models import ConfigTemplate from netbox.filtersets import NetBoxModelFilterSet -from tenancy.filtersets import TenancyFilterSet -from utilities.filters import TreeNodeMultipleChoiceFilter +from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet +from utilities.filters import TreeNodeMultipleChoiceFilter, MultiValueCharFilter, MultiValueMACAddressFilter from netbox_topology_views.models import CoordinateGroup, Coordinate -class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet): +class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet): q = django_filters.CharFilter( method="search", label="Search", ) + manufacturer_id = django_filters.ModelMultipleChoiceFilter( + field_name='device_type__manufacturer', + queryset=Manufacturer.objects.all(), + label="Manufacturer (ID)", + ) + manufacturer = django_filters.ModelMultipleChoiceFilter( + field_name='device_type__manufacturer__slug', + queryset=Manufacturer.objects.all(), + to_field_name='slug', + label="Manufacturer (slug)", + ) + device_type_id = django_filters.ModelMultipleChoiceFilter( + queryset=DeviceType.objects.all(), + label="Device type (ID)", + ) role_id = django_filters.ModelMultipleChoiceFilter( field_name="role_id", queryset=DeviceRole.objects.all(), label="Role (ID)", ) + platform_id = django_filters.ModelMultipleChoiceFilter( + queryset=Platform.objects.all(), + label="Platform (ID)", + ) region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), field_name="site__region", lookup_expr="in", label="Region (ID)", ) - site_id = django_filters.ModelMultipleChoiceFilter( - queryset=Site.objects.all(), - label="Site (ID)", - ) - sitegroup_id = TreeNodeMultipleChoiceFilter( + site_group_id = TreeNodeMultipleChoiceFilter( queryset=SiteGroup.objects.all(), field_name="site__group", lookup_expr="in", label="Site Group (ID)", ) + site_id = django_filters.ModelMultipleChoiceFilter( + queryset=Site.objects.all(), + label="Site (ID)", + ) location_id = TreeNodeMultipleChoiceFilter( queryset=Location.objects.all(), field_name="location", @@ -48,10 +69,57 @@ class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet): choices=DeviceStatusChoices, null_value=None, ) + mac_address = MultiValueMACAddressFilter( + field_name='interfaces__mac_address', + label="MAC address", + ) + serial = MultiValueCharFilter( + lookup_expr='iexact' + ) + console_ports = django_filters.BooleanFilter( + method='_console_ports', + label="Has console ports", + ) + console_server_ports = django_filters.BooleanFilter( + method='_console_server_ports', + label="Has console server ports", + ) + power_ports = django_filters.BooleanFilter( + method='_power_ports', + label="Has power ports", + ) + power_outlets = django_filters.BooleanFilter( + method='_power_outlets', + label="Has power outlets", + ) + interfaces = django_filters.BooleanFilter( + method='_interfaces', + label="Has interfaces", + ) + pass_through_ports = django_filters.BooleanFilter( + method='_pass_through_ports', + label="Has pass-through ports", + ) + config_template_id = django_filters.ModelMultipleChoiceFilter( + queryset=ConfigTemplate.objects.all(), + label="Config template (ID)", + ) + has_primary_ip = django_filters.BooleanFilter( + method='_has_primary_ip', + label="Has a primary IP", + ) + has_oob_ip = django_filters.BooleanFilter( + method='_has_oob_ip', + label="Has an out-of-band IP", + ) + virtual_chassis_member = django_filters.BooleanFilter( + method='_virtual_chassis_member', + label="Is a virtual chassis member", + ) class Meta: model = Device - fields = ["id", "name"] + fields = ["id", "name", "asset_tag", "airflow"] def search(self, queryset, name, value): """Perform the filtered search.""" @@ -60,6 +128,42 @@ class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet): qs_filter = Q(name__icontains=value) return queryset.filter(qs_filter) + def _console_ports(self, queryset, name, value): + return queryset.exclude(consoleports__isnull=value) + + def _console_server_ports(self, queryset, name, value): + return queryset.exclude(consoleserverports__isnull=value) + + def _power_ports(self, queryset, name, value): + return queryset.exclude(powerports__isnull=value) + + def _power_outlets(self, queryset, name, value): + return queryset.exclude(poweroutlets__isnull=value) + + def _interfaces(self, queryset, name, value): + return queryset.exclude(interfaces__isnull=value) + + def _pass_through_ports(self, queryset, name, value): + return queryset.exclude( + frontports__isnull=value, + rearports__isnull=value + ) + + def _has_primary_ip(self, queryset, name, value): + params = Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False) + if value: + return queryset.filter(params) + return queryset.exclude(params) + + def _has_oob_ip(self, queryset, name, value): + params = Q(oob_ip__isnull=False) + if value: + return queryset.filter(params) + return queryset.exclude(params) + + def _virtual_chassis_member(self, queryset, name, value): + return queryset.exclude(virtual_chassis__isnull=value) + class CoordinatesFilterSet(NetBoxModelFilterSet): group = django_filters.ModelMultipleChoiceFilter( queryset = CoordinateGroup.objects.all(), diff --git a/netbox_topology_views/forms.py b/netbox_topology_views/forms.py index 030dab4..711a474 100644 --- a/netbox_topology_views/forms.py +++ b/netbox_topology_views/forms.py @@ -5,14 +5,17 @@ from django.conf import settings from django.utils.translation import gettext as _ -from dcim.models import Device, Site, SiteGroup, Region, DeviceRole, Location, Rack +from dcim.models import Device, Site, SiteGroup, Region, DeviceRole, Location, Rack, Manufacturer, DeviceType, Platform from django import forms -from dcim.choices import DeviceStatusChoices +from dcim.choices import DeviceStatusChoices, DeviceAirflowChoices from tenancy.models import TenantGroup, Tenant -from tenancy.forms import TenancyFilterForm +from extras.models import ConfigTemplate +from extras.forms import LocalConfigContextFilterForm +from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from django.conf import settings from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm, NetBoxModelImportForm +from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice from utilities.forms.fields import ( TagFilterField, DynamicModelMultipleChoiceField @@ -20,146 +23,239 @@ from utilities.forms.fields import ( from netbox_topology_views.models import IndividualOptions, CoordinateGroup, Coordinate -class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): +class DeviceFilterForm( + LocalConfigContextFilterForm, + TenancyFilterForm, + ContactModelFilterForm, + NetBoxModelFilterSetForm +): model = Device fieldsets = ( - ( - None, - ( - "q", - "filter_id", - ), - ), - ( - None, - ( - "group", - "save_coords", - "show_unconnected", - "show_cables", - "show_logical_connections", - "show_single_cable_logical_conns", - "show_neighbors", - "show_circuit", - "show_power", - "show_wireless", - ), - ), - ( - None, - ( - "tenant_group_id", - "tenant_id", - ), - ), - (None, ("region_id", "sitegroup_id", "site_id", "location_id", "rack_id")), - ( - None, - ( - "role_id", - "id", - "status", - ), - ), - (None, ("tag",)), + (None, ('q', 'filter_id', 'tag')), + (_('Options'), ( + 'group', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections', + 'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', 'show_wireless', + )), + (_('Device'), ('device_id',)), + (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), + (_('Operation'), ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')), + (_('Hardware'), ('manufacturer_id', 'device_type_id', 'platform_id')), + (_('Tenant'), ('tenant_group_id', 'tenant_id')), + (_('Contacts'), ('contact', 'contact_role', 'contact_group')), + (_('Components'), ( + 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports', + )), + (_('Miscellaneous'), ( + 'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data', + )), ) group = forms.ModelChoiceField( queryset=CoordinateGroup.objects.all(), required=False, - label=_("Coordinate group"), + label=_('Coordinate group'), + ) + device_id = DynamicModelMultipleChoiceField( + queryset=Device.objects.all(), + required=False, + label=_('Device'), + query_params={ + 'location_id': '$location_id', + 'region_id': '$region_id', + 'site_group_id': '$site_group_id', + 'site_id': '$site_id', + 'role_id': '$role_id', + 'contact': '$contact', + 'contact_role': '$contact_role', + 'contact_group': '$contact_group', + }, ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_("Region") + label=_('Region') ) - role_id = DynamicModelMultipleChoiceField( - queryset=DeviceRole.objects.all(), - required=False, - label=_("Device Role") - ) - id = DynamicModelMultipleChoiceField( - queryset=Device.objects.all(), + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), required=False, - label=_("Device"), - query_params={ - "location_id": "$location_id", - "region_id": "$region_id", - "site_group_id": "$sitegroup_id", - "site_id": "$site_id", - "role_id": "$role_id", - }, + label=_('Site Group'), ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ - "region_id": "$region_id", - "group_id": "$sitegroup_id", + 'region_id': '$region_id', + 'group_id': '$site_group_id', }, - label=_("Site"), - ) - sitegroup_id = DynamicModelMultipleChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label=_("Site Group"), + label=_('Site'), ) location_id = DynamicModelMultipleChoiceField( queryset=Location.objects.all(), required=False, query_params={ - "region_id": "$region_id", - "site_group_id": "$sitegroup_id", - "site_id": "$site_id", + 'region_id': '$region_id', + 'site_group_id': '$site_group_id', + 'site_id': '$site_id', }, - label=_("Location"), + label=_('Location'), ) rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), required=False, query_params={ - "region_id": "$region_id", - "site_group_id": "$sitegroup_id", - "site_id": "$site_id", - "location_id": "$location_id", + 'region_id': '$region_id', + 'site_group_id': '$site_group_id', + 'site_id': '$site_id', + 'location_id': '$location_id', }, - label=_("Rack"), + label=_('Rack'), ) status = forms.MultipleChoiceField( - choices=DeviceStatusChoices, required=False, label=_("Device Status") + choices=DeviceStatusChoices, + required=False, + label=_('Device Status') ) + role_id = DynamicModelMultipleChoiceField( + queryset=DeviceRole.objects.all(), + required=False, + label=_('Role') + ) + airflow = forms.MultipleChoiceField( + label=_('Airflow'), + choices=add_blank_choice(DeviceAirflowChoices), + required=False + ) + serial = forms.CharField( + label=_('Serial'), + required=False + ) + asset_tag = forms.CharField( + label=_('Asset tag'), + required=False + ) + mac_address = forms.CharField( + required=False, + label=_('MAC address') + ) + manufacturer_id = DynamicModelMultipleChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + label=_('Manufacturer') + ) + device_type_id = DynamicModelMultipleChoiceField( + queryset=DeviceType.objects.all(), + required=False, + query_params={ + 'manufacturer_id': '$manufacturer_id' + }, + label=_('Model') + ) + platform_id = DynamicModelMultipleChoiceField( + queryset=Platform.objects.all(), + required=False, + null_option='None', + label=_('Platform') + ) + console_ports = forms.NullBooleanField( + required=False, + label=_('Has console ports'), + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + console_server_ports = forms.NullBooleanField( + required=False, + label=_('Has console server ports'), + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + power_ports = forms.NullBooleanField( + required=False, + label=_('Has power ports'), + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + power_outlets = forms.NullBooleanField( + required=False, + label=_('Has power outlets'), + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + interfaces = forms.NullBooleanField( + required=False, + label=_('Has interfaces'), + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + pass_through_ports = forms.NullBooleanField( + required=False, + label=_('Has pass-through ports'), + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + config_template_id = DynamicModelMultipleChoiceField( + queryset=ConfigTemplate.objects.all(), + required=False, + label=_('Config template') + ) + has_primary_ip = forms.NullBooleanField( + required=False, + label=_('Has a primary IP'), + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + has_oob_ip = forms.NullBooleanField( + required=False, + label='Has an OOB IP', + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + virtual_chassis_member = forms.NullBooleanField( + required=False, + label=_('Virtual chassis member'), + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + tag = TagFilterField(model) # options save_coords = forms.BooleanField( - label=_("Save Coordinates"), + label=_('Save Coordinates'), required=False, - disabled=(not settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"] or settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"]), - initial=(settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"]) + disabled=(not settings.PLUGINS_CONFIG['netbox_topology_views']['allow_coordinates_saving'] or settings.PLUGINS_CONFIG['netbox_topology_views']['always_save_coordinates']), + initial=(settings.PLUGINS_CONFIG['netbox_topology_views']['always_save_coordinates']) ) show_unconnected = forms.BooleanField( - label=_("Show Unconnected"), required=False, initial=False + label=_('Show Unconnected'), required=False, initial=False ) show_cables = forms.BooleanField( - label =_("Show Cables"), required=False, initial=False + label =_('Show Cables'), required=False, initial=False ) show_logical_connections = forms.BooleanField( - label =_("Show Logical Connections"), required=False, initial=False + label =_('Show Logical Connections'), required=False, initial=False ) show_single_cable_logical_conns = forms.BooleanField( - label =_("Show redundant Cable and Logical Connection"), required=False, initial=False + label =_('Show redundant Cable and Logical Connection'), required=False, initial=False ) show_neighbors = forms.BooleanField( - label =_("Show Neighbors"), required=False, initial=False + label =_('Show Neighbors'), required=False, initial=False ) show_circuit = forms.BooleanField( - label=_("Show Circuit Terminations"), required=False, initial=False + label=_('Show Circuit Terminations'), required=False, initial=False ) show_power = forms.BooleanField( - label=_("Show Power Feeds"), required=False, initial=False + label=_('Show Power Feeds'), required=False, initial=False ) show_wireless = forms.BooleanField( - label =_("Show Wireless Links"), required=False, initial=False + label =_('Show Wireless Links'), required=False, initial=False ) class CoordinateGroupsForm(NetBoxModelForm): @@ -220,20 +316,20 @@ class IndividualOptionsForm(NetBoxModelForm): ( None, ( - "user_id", - "ignore_cable_type", - "preselected_device_roles", - "preselected_tags", - "save_coords", - "show_unconnected", - "show_cables", - "show_logical_connections", - "show_single_cable_logical_conns", - "show_neighbors", - "show_circuit", - "show_power", - "show_wireless", - "draw_default_layout", + 'user_id', + 'ignore_cable_type', + 'preselected_device_roles', + 'preselected_tags', + 'save_coords', + 'show_unconnected', + 'show_cables', + 'show_logical_connections', + 'show_single_cable_logical_conns', + 'show_neighbors', + 'show_circuit', + 'show_power', + 'show_wireless', + 'draw_default_layout', ), ), ) @@ -241,105 +337,105 @@ class IndividualOptionsForm(NetBoxModelForm): user_id = forms.CharField(widget=forms.HiddenInput()) ignore_cable_type = forms.MultipleChoiceField( - label=_("Ignore Termination Types"), + label=_('Ignore Termination Types'), required=False, choices=IndividualOptions.CHOICES, - help_text=_("Choose Termination Types that you want to be ignored. " - "If any ignored Termination Type is part of a connection, the " - "cable is not displayed.") + help_text=_('Choose Termination Types that you want to be ignored. ' + 'If any ignored Termination Type is part of a connection, the ' + 'cable is not displayed.') ) preselected_device_roles = DynamicModelMultipleChoiceField( - label=_("Preselected Device Role"), + label=_('Preselected Device Role'), queryset=DeviceRole.objects.all(), required=False, - help_text=_("Select Device Roles that you want to have " - "preselected in the filter tab.") + help_text=_('Select Device Roles that you want to have ' + 'preselected in the filter tab.') ) preselected_tags = forms.ModelMultipleChoiceField( - label=_("Preselected Tags"), + label=_('Preselected Tags'), queryset=Device.tags.all(), required=False, - help_text=_("Select Tags that you want to have " - "preselected in the filter tab.") + help_text=_('Select Tags that you want to have ' + 'preselected in the filter tab.') ) save_coords = forms.BooleanField( - label=_("Save Coordinates"), + label=_('Save Coordinates'), required=False, initial=False, - help_text=_("Coordinates of nodes will be saved if dragged to a different " - "position. This option depends on parameters set in the config file. " - "It has no effect if 'allow_coordinates_saving' has not been set or " - " 'always_save_coordinates' has been set.") + help_text=_('Coordinates of nodes will be saved if dragged to a different ' + 'position. This option depends on parameters set in the config file. ' + 'It has no effect if \'allow_coordinates_saving\' has not been set or ' + ' \'always_save_coordinates\' has been set.') ) show_unconnected = forms.BooleanField( - label=_("Show Unconnected"), + label=_('Show Unconnected'), required=False, initial=False, - help_text=_("Draws devices that have no connections or for which no " - "connection is displayed. This option depends on other parameters " - "like 'Show Cables' and 'Show Logical Connections'.") + help_text=_('Draws devices that have no connections or for which no ' + 'connection is displayed. This option depends on other parameters ' + 'like \'Show Cables\' and \'Show Logical Connections\'.') ) show_cables = forms.BooleanField( - label =_("Show Cables"), + label =_('Show Cables'), required=False, initial=False, - help_text=_("Displays connections between interfaces that are connected " - "with one or more cables. These connections are displayed as solid " - "lines in the color of the cable.") + help_text=_('Displays connections between interfaces that are connected ' + 'with one or more cables. These connections are displayed as solid ' + 'lines in the color of the cable.') ) show_logical_connections = forms.BooleanField( - label =_("Show Logical Connections"), + label =_('Show Logical Connections'), required=False, initial=False, - help_text=_("Displays connections between devices that are not " - "directly connected (e.g. via patch panels). These connections " - "are displayed as yellow dotted lines.") + help_text=_('Displays connections between devices that are not ' + 'directly connected (e.g. via patch panels). These connections ' + 'are displayed as yellow dotted lines.') ) show_single_cable_logical_conns = forms.BooleanField( - label = ("Show redundant Cable and Logical Connection"), + label = ('Show redundant Cable and Logical Connection'), required = False, initial=False, - help_text=_("Shows a logical connection (in addition to a cable), " - "even if a cable is directly connected. Leaving this option " - "disabled prevents that redundant display. This option only " - "has an effect if 'Show Logical Connections' is activated.") + help_text=_('Shows a logical connection (in addition to a cable), ' + 'even if a cable is directly connected. Leaving this option ' + 'disabled prevents that redundant display. This option only ' + 'has an effect if \'Show Logical Connections\' is activated.') ) show_neighbors = forms.BooleanField( - label =_("Show Neighbors"), + label =_('Show Neighbors'), required=False, initial=False, - help_text=_("Adds neighbors to the filter result set automatically. " - "Link peers will be added if 'Show Cables' is ticked, far-end " - "terminations will be added if 'Show Logical Connections' is ticked.") + help_text=_('Adds neighbors to the filter result set automatically. ' + 'Link peers will be added if \'Show Cables\' is ticked, far-end ' + 'terminations will be added if \'Show Logical Connections\' is ticked.') ) show_circuit = forms.BooleanField( - label=_("Show Circuit Terminations"), + label=_('Show Circuit Terminations'), required=False, initial=False, - help_text=_("Displays connections between circuit terminations. " - "These connections are displayed as blue dashed lines.") + help_text=_('Displays connections between circuit terminations. ' + 'These connections are displayed as blue dashed lines.') ) show_power = forms.BooleanField( - label=_("Show Power Feeds"), + label=_('Show Power Feeds'), required=False, initial=False, - help_text=_("Displays connections between power outlets and power " - "ports. These connections are displayed as solid lines in the " - "color of the cable. This option depends on 'Show Cables'.") + help_text=_('Displays connections between power outlets and power ' + 'ports. These connections are displayed as solid lines in the ' + 'color of the cable. This option depends on \'Show Cables\'.') ) show_wireless = forms.BooleanField( - label =_("Show Wireless Links"), + label =_('Show Wireless Links'), required=False, initial=False, - help_text=_("Displays wireless connections. These connections are " - "displayed as blue dotted lines.") + help_text=_('Displays wireless connections. These connections are ' + 'displayed as blue dotted lines.') ) draw_default_layout = forms.BooleanField( - label = ("Draw Default Layout"), + label = ('Draw Default Layout'), required=False, initial=False, - help_text=_("Enable this option if you want to draw the topology on " - "the initial load (when you go to the topology plugin page).") + help_text=_('Enable this option if you want to draw the topology on ' + 'the initial load (when you go to the topology plugin page).') ) class Meta: