mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-24 16:26:09 -06:00
Initial work on new search backend
This commit is contained in:
parent
bd79a27e4d
commit
80bf6e7b2c
@ -1,34 +1,66 @@
|
||||
import circuits.filtersets
|
||||
import circuits.tables
|
||||
from circuits.models import Circuit, Provider, ProviderNetwork
|
||||
from circuits import filtersets, models
|
||||
from netbox.search import SearchIndex, register_search
|
||||
from utilities.utils import count_related
|
||||
|
||||
|
||||
@register_search()
|
||||
class ProviderIndex(SearchIndex):
|
||||
model = Provider
|
||||
queryset = Provider.objects.annotate(count_circuits=count_related(Circuit, 'provider'))
|
||||
filterset = circuits.filtersets.ProviderFilterSet
|
||||
table = circuits.tables.ProviderTable
|
||||
url = 'circuits:provider_list'
|
||||
class CircuitIndex(SearchIndex):
|
||||
model = models.Circuit
|
||||
fields = (
|
||||
('cid', 100),
|
||||
('description', 500),
|
||||
('comments', 1000),
|
||||
)
|
||||
queryset = models.Circuit.objects.prefetch_related(
|
||||
'type', 'provider', 'tenant', 'tenant__group', 'terminations__site'
|
||||
)
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
|
||||
|
||||
@register_search()
|
||||
class CircuitIndex(SearchIndex):
|
||||
model = Circuit
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'type', 'provider', 'tenant', 'tenant__group', 'terminations__site'
|
||||
class CircuitTerminationIndex(SearchIndex):
|
||||
model = models.CircuitTermination
|
||||
fields = (
|
||||
('xconnect_id', 300),
|
||||
('pp_info', 300),
|
||||
('description', 500),
|
||||
('port_speed', 2000),
|
||||
('upstream_speed', 2000),
|
||||
)
|
||||
filterset = circuits.filtersets.CircuitFilterSet
|
||||
table = circuits.tables.CircuitTable
|
||||
url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class CircuitTypeIndex(SearchIndex):
|
||||
model = models.CircuitType
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 100),
|
||||
('description', 500),
|
||||
)
|
||||
|
||||
|
||||
@register_search()
|
||||
class ProviderIndex(SearchIndex):
|
||||
model = models.Provider
|
||||
fields = (
|
||||
('name', 100),
|
||||
('account', 200),
|
||||
('comments', 1000),
|
||||
)
|
||||
queryset = models.Provider.objects.annotate(
|
||||
count_circuits=count_related(models.Circuit, 'provider')
|
||||
)
|
||||
filterset = filtersets.ProviderFilterSet
|
||||
|
||||
|
||||
@register_search()
|
||||
class ProviderNetworkIndex(SearchIndex):
|
||||
model = ProviderNetwork
|
||||
queryset = ProviderNetwork.objects.prefetch_related('provider')
|
||||
filterset = circuits.filtersets.ProviderNetworkFilterSet
|
||||
table = circuits.tables.ProviderNetworkTable
|
||||
url = 'circuits:providernetwork_list'
|
||||
model = models.ProviderNetwork
|
||||
fields = (
|
||||
('name', 100),
|
||||
('service_id', 200),
|
||||
('description', 500),
|
||||
('comments', 1000),
|
||||
)
|
||||
queryset = models.ProviderNetwork.objects.prefetch_related('provider')
|
||||
filterset = filtersets.ProviderNetworkFilterSet
|
||||
|
@ -1,81 +1,28 @@
|
||||
import dcim.filtersets
|
||||
import dcim.tables
|
||||
from dcim.models import (
|
||||
Cable,
|
||||
Device,
|
||||
DeviceType,
|
||||
Location,
|
||||
Module,
|
||||
ModuleType,
|
||||
PowerFeed,
|
||||
Rack,
|
||||
RackReservation,
|
||||
Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from dcim import filtersets, models
|
||||
from netbox.search import SearchIndex, register_search
|
||||
from utilities.utils import count_related
|
||||
|
||||
|
||||
@register_search()
|
||||
class SiteIndex(SearchIndex):
|
||||
model = Site
|
||||
queryset = Site.objects.prefetch_related('region', 'tenant', 'tenant__group')
|
||||
filterset = dcim.filtersets.SiteFilterSet
|
||||
table = dcim.tables.SiteTable
|
||||
url = 'dcim:site_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class RackIndex(SearchIndex):
|
||||
model = Rack
|
||||
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
|
||||
device_count=count_related(Device, 'rack')
|
||||
class CableIndex(SearchIndex):
|
||||
model = models.Cable
|
||||
fields = (
|
||||
('label', 100),
|
||||
)
|
||||
filterset = dcim.filtersets.RackFilterSet
|
||||
table = dcim.tables.RackTable
|
||||
url = 'dcim:rack_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class RackReservationIndex(SearchIndex):
|
||||
model = RackReservation
|
||||
queryset = RackReservation.objects.prefetch_related('rack', 'user')
|
||||
filterset = dcim.filtersets.RackReservationFilterSet
|
||||
table = dcim.tables.RackReservationTable
|
||||
url = 'dcim:rackreservation_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class LocationIndex(SearchIndex):
|
||||
model = Location
|
||||
queryset = Location.objects.add_related_count(
|
||||
Location.objects.add_related_count(Location.objects.all(), Device, 'location', 'device_count', cumulative=True),
|
||||
Rack,
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True,
|
||||
).prefetch_related('site')
|
||||
filterset = dcim.filtersets.LocationFilterSet
|
||||
table = dcim.tables.LocationTable
|
||||
url = 'dcim:location_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class DeviceTypeIndex(SearchIndex):
|
||||
model = DeviceType
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||
instance_count=count_related(Device, 'device_type')
|
||||
)
|
||||
filterset = dcim.filtersets.DeviceTypeFilterSet
|
||||
table = dcim.tables.DeviceTypeTable
|
||||
url = 'dcim:devicetype_list'
|
||||
queryset = models.Cable.objects.all()
|
||||
filterset = filtersets.CableFilterSet
|
||||
|
||||
|
||||
@register_search()
|
||||
class DeviceIndex(SearchIndex):
|
||||
model = Device
|
||||
queryset = Device.objects.prefetch_related(
|
||||
model = models.Device
|
||||
fields = (
|
||||
('asset_tag', 50),
|
||||
('serial', 60),
|
||||
('name', 100),
|
||||
('comments', 1000),
|
||||
)
|
||||
queryset = models.Device.objects.prefetch_related(
|
||||
'device_type__manufacturer',
|
||||
'device_role',
|
||||
'tenant',
|
||||
@ -85,59 +32,162 @@ class DeviceIndex(SearchIndex):
|
||||
'primary_ip4',
|
||||
'primary_ip6',
|
||||
)
|
||||
filterset = dcim.filtersets.DeviceFilterSet
|
||||
table = dcim.tables.DeviceTable
|
||||
url = 'dcim:device_list'
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
|
||||
|
||||
@register_search()
|
||||
class ModuleTypeIndex(SearchIndex):
|
||||
model = ModuleType
|
||||
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||
instance_count=count_related(Module, 'module_type')
|
||||
class DeviceRoleIndex(SearchIndex):
|
||||
model = models.DeviceRole
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 100),
|
||||
('description', 500),
|
||||
)
|
||||
filterset = dcim.filtersets.ModuleTypeFilterSet
|
||||
table = dcim.tables.ModuleTypeTable
|
||||
url = 'dcim:moduletype_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class DeviceTypeIndex(SearchIndex):
|
||||
model = models.DeviceType
|
||||
fields = (
|
||||
('model', 100),
|
||||
('part_number', 200),
|
||||
('comments', 1000),
|
||||
)
|
||||
queryset = models.DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||
instance_count=count_related(models.Device, 'device_type')
|
||||
)
|
||||
filterset = filtersets.DeviceTypeFilterSet
|
||||
|
||||
|
||||
@register_search()
|
||||
class LocationIndex(SearchIndex):
|
||||
model = models.Location
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 100),
|
||||
('description', 500),
|
||||
)
|
||||
queryset = models.Location.objects.add_related_count(
|
||||
models.Location.objects.add_related_count(
|
||||
models.Location.objects.all(), models.Device, 'location', 'device_count', cumulative=True
|
||||
),
|
||||
models.Rack,
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True,
|
||||
).prefetch_related('site')
|
||||
filterset = filtersets.LocationFilterSet
|
||||
|
||||
|
||||
@register_search()
|
||||
class ModuleIndex(SearchIndex):
|
||||
model = Module
|
||||
queryset = Module.objects.prefetch_related(
|
||||
model = models.Module
|
||||
fields = (
|
||||
('asset_tag', 50),
|
||||
('serial', 60),
|
||||
('comments', 1000),
|
||||
)
|
||||
queryset = models.Module.objects.prefetch_related(
|
||||
'module_type__manufacturer',
|
||||
'device',
|
||||
'module_bay',
|
||||
)
|
||||
filterset = dcim.filtersets.ModuleFilterSet
|
||||
table = dcim.tables.ModuleTable
|
||||
url = 'dcim:module_list'
|
||||
filterset = filtersets.ModuleFilterSet
|
||||
|
||||
|
||||
@register_search()
|
||||
class VirtualChassisIndex(SearchIndex):
|
||||
model = VirtualChassis
|
||||
queryset = VirtualChassis.objects.prefetch_related('master').annotate(
|
||||
member_count=count_related(Device, 'virtual_chassis')
|
||||
class ModuleTypeIndex(SearchIndex):
|
||||
model = models.ModuleType
|
||||
fields = (
|
||||
('model', 100),
|
||||
('part_number', 200),
|
||||
('comments', 1000),
|
||||
)
|
||||
filterset = dcim.filtersets.VirtualChassisFilterSet
|
||||
table = dcim.tables.VirtualChassisTable
|
||||
url = 'dcim:virtualchassis_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class CableIndex(SearchIndex):
|
||||
model = Cable
|
||||
queryset = Cable.objects.all()
|
||||
filterset = dcim.filtersets.CableFilterSet
|
||||
table = dcim.tables.CableTable
|
||||
url = 'dcim:cable_list'
|
||||
queryset = models.ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||
instance_count=count_related(models.Module, 'module_type')
|
||||
)
|
||||
filterset = filtersets.ModuleTypeFilterSet
|
||||
|
||||
|
||||
@register_search()
|
||||
class PowerFeedIndex(SearchIndex):
|
||||
model = PowerFeed
|
||||
queryset = PowerFeed.objects.all()
|
||||
filterset = dcim.filtersets.PowerFeedFilterSet
|
||||
table = dcim.tables.PowerFeedTable
|
||||
url = 'dcim:powerfeed_list'
|
||||
model = models.PowerFeed
|
||||
fields = (
|
||||
('name', 100),
|
||||
('comments', 1000),
|
||||
)
|
||||
queryset = models.PowerFeed.objects.all()
|
||||
filterset = filtersets.PowerFeedFilterSet
|
||||
|
||||
|
||||
@register_search()
|
||||
class RackIndex(SearchIndex):
|
||||
model = models.Rack
|
||||
fields = (
|
||||
('asset_tag', 50),
|
||||
('serial', 60),
|
||||
('name', 100),
|
||||
('facility_id', 100),
|
||||
('comments', 1000),
|
||||
)
|
||||
queryset = models.Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
|
||||
device_count=count_related(models.Device, 'rack')
|
||||
)
|
||||
filterset = filtersets.RackFilterSet
|
||||
|
||||
|
||||
@register_search()
|
||||
class RackReservationIndex(SearchIndex):
|
||||
model = models.RackReservation
|
||||
fields = (
|
||||
('description', 500),
|
||||
)
|
||||
queryset = models.RackReservation.objects.prefetch_related('rack', 'user')
|
||||
filterset = filtersets.RackReservationFilterSet
|
||||
|
||||
|
||||
@register_search()
|
||||
class RegionIndex(SearchIndex):
|
||||
model = models.Region
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 100),
|
||||
('description', 500)
|
||||
)
|
||||
|
||||
|
||||
@register_search()
|
||||
class SiteIndex(SearchIndex):
|
||||
model = models.Site
|
||||
fields = (
|
||||
('name', 100),
|
||||
('facility', 100),
|
||||
('description', 500),
|
||||
('physical_address', 1000),
|
||||
('shipping_address', 1000),
|
||||
)
|
||||
queryset = models.Site.objects.prefetch_related('region', 'tenant', 'tenant__group')
|
||||
filterset = filtersets.SiteFilterSet
|
||||
|
||||
|
||||
@register_search()
|
||||
class SiteGroupIndex(SearchIndex):
|
||||
model = models.SiteGroup
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 100),
|
||||
('description', 500)
|
||||
)
|
||||
|
||||
|
||||
@register_search()
|
||||
class VirtualChassisIndex(SearchIndex):
|
||||
model = models.VirtualChassis
|
||||
fields = (
|
||||
('name', 100),
|
||||
('domain', 300)
|
||||
)
|
||||
queryset = models.VirtualChassis.objects.prefetch_related('master').annotate(
|
||||
member_count=count_related(models.Device, 'virtual_chassis')
|
||||
)
|
||||
filterset = filtersets.VirtualChassisFilterSet
|
||||
|
25
netbox/extras/management/commands/reindex.py
Normal file
25
netbox/extras/management/commands/reindex.py
Normal file
@ -0,0 +1,25 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from extras.models import CachedValue
|
||||
from extras.registry import registry
|
||||
from netbox.search.backends import search_backend
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Reindex cached search values"""
|
||||
help = 'Reindex cached search values.'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
|
||||
self.stdout.write('Clearing cached values...', ending="\n")
|
||||
CachedValue.objects.all().delete()
|
||||
|
||||
for app_label, models in registry['search'].items():
|
||||
for name, idx in models.items():
|
||||
self.stdout.write(f'Reindexing {app_label}.{name}...', ending="\n")
|
||||
model = idx.model
|
||||
for instance in model.objects.all():
|
||||
search_backend.cache(model, instance)
|
||||
|
||||
cache_size = CachedValue.objects.count()
|
||||
self.stdout.write(f'Done. Generated {cache_size} cached values', ending="\n")
|
31
netbox/extras/migrations/0079_search.py
Normal file
31
netbox/extras/migrations/0079_search.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Generated by Django 4.1.1 on 2022-10-10 18:22
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('extras', '0078_unique_constraints'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CachedValue',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('object_id', models.PositiveBigIntegerField()),
|
||||
('field', models.CharField(max_length=200)),
|
||||
('type', models.CharField(max_length=30)),
|
||||
('value', models.TextField()),
|
||||
('weight', models.PositiveSmallIntegerField(default=1000)),
|
||||
('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('weight', 'pk'),
|
||||
},
|
||||
),
|
||||
]
|
@ -2,9 +2,11 @@ from .change_logging import ObjectChange
|
||||
from .configcontexts import ConfigContext, ConfigContextModel
|
||||
from .customfields import CustomField
|
||||
from .models import *
|
||||
from .search import *
|
||||
from .tags import Tag, TaggedItem
|
||||
|
||||
__all__ = (
|
||||
'CachedValue',
|
||||
'ConfigContext',
|
||||
'ConfigContextModel',
|
||||
'ConfigRevision',
|
||||
|
40
netbox/extras/models/search.py
Normal file
40
netbox/extras/models/search.py
Normal file
@ -0,0 +1,40 @@
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
|
||||
__all__ = (
|
||||
'CachedValue',
|
||||
)
|
||||
|
||||
|
||||
class CachedValue(models.Model):
|
||||
timestamp = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
editable=False
|
||||
)
|
||||
object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='+'
|
||||
)
|
||||
object_id = models.PositiveBigIntegerField()
|
||||
object = GenericForeignKey(
|
||||
ct_field='object_type',
|
||||
fk_field='object_id'
|
||||
)
|
||||
field = models.CharField(
|
||||
max_length=200
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=30
|
||||
)
|
||||
value = models.TextField()
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
default=1000
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('weight', 'pk')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.object_type} {self.object_id}: {self.field}={self.value}'
|
@ -7,8 +7,9 @@ from netbox.search import SearchIndex, register_search
|
||||
@register_search()
|
||||
class JournalEntryIndex(SearchIndex):
|
||||
model = JournalEntry
|
||||
fields = (
|
||||
('comments', 1000),
|
||||
)
|
||||
queryset = JournalEntry.objects.prefetch_related('assigned_object', 'created_by')
|
||||
filterset = extras.filtersets.JournalEntryFilterSet
|
||||
table = extras.tables.JournalEntryTable
|
||||
url = 'extras:journalentry_list'
|
||||
category = 'Journal'
|
||||
|
@ -5,7 +5,6 @@ from .models import DummyModel
|
||||
class DummyModelIndex(SearchIndex):
|
||||
model = DummyModel
|
||||
queryset = DummyModel.objects.all()
|
||||
url = 'plugins:dummy_plugin:dummy_models'
|
||||
|
||||
|
||||
indexes = (
|
||||
|
@ -4,66 +4,84 @@ from ipam.models import ASN, VLAN, VRF, Aggregate, IPAddress, Prefix, Service
|
||||
from netbox.search import SearchIndex, register_search
|
||||
|
||||
|
||||
@register_search()
|
||||
class VRFIndex(SearchIndex):
|
||||
model = VRF
|
||||
queryset = VRF.objects.prefetch_related('tenant', 'tenant__group')
|
||||
filterset = ipam.filtersets.VRFFilterSet
|
||||
table = ipam.tables.VRFTable
|
||||
url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class AggregateIndex(SearchIndex):
|
||||
model = Aggregate
|
||||
fields = (
|
||||
('prefix', 100),
|
||||
('description', 500),
|
||||
('date_added', 2000),
|
||||
)
|
||||
queryset = Aggregate.objects.prefetch_related('rir')
|
||||
filterset = ipam.filtersets.AggregateFilterSet
|
||||
table = ipam.tables.AggregateTable
|
||||
url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class PrefixIndex(SearchIndex):
|
||||
model = Prefix
|
||||
queryset = Prefix.objects.prefetch_related(
|
||||
'site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'
|
||||
)
|
||||
filterset = ipam.filtersets.PrefixFilterSet
|
||||
table = ipam.tables.PrefixTable
|
||||
url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class IPAddressIndex(SearchIndex):
|
||||
model = IPAddress
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group')
|
||||
filterset = ipam.filtersets.IPAddressFilterSet
|
||||
table = ipam.tables.IPAddressTable
|
||||
url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class VLANIndex(SearchIndex):
|
||||
model = VLAN
|
||||
queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role')
|
||||
filterset = ipam.filtersets.VLANFilterSet
|
||||
table = ipam.tables.VLANTable
|
||||
url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class ASNIndex(SearchIndex):
|
||||
model = ASN
|
||||
fields = (
|
||||
('asn', 100),
|
||||
('description', 500),
|
||||
)
|
||||
queryset = ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group')
|
||||
filterset = ipam.filtersets.ASNFilterSet
|
||||
table = ipam.tables.ASNTable
|
||||
url = 'ipam:asn_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class IPAddressIndex(SearchIndex):
|
||||
model = IPAddress
|
||||
fields = (
|
||||
('address', 100),
|
||||
('dns_name', 300),
|
||||
('description', 500),
|
||||
)
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group')
|
||||
filterset = ipam.filtersets.IPAddressFilterSet
|
||||
|
||||
|
||||
@register_search()
|
||||
class PrefixIndex(SearchIndex):
|
||||
model = Prefix
|
||||
fields = (
|
||||
('prefix', 100),
|
||||
('description', 500),
|
||||
)
|
||||
queryset = Prefix.objects.prefetch_related(
|
||||
'site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'
|
||||
)
|
||||
filterset = ipam.filtersets.PrefixFilterSet
|
||||
|
||||
|
||||
@register_search()
|
||||
class ServiceIndex(SearchIndex):
|
||||
model = Service
|
||||
fields = (
|
||||
('name', 100),
|
||||
('description', 500),
|
||||
)
|
||||
queryset = Service.objects.prefetch_related('device', 'virtual_machine')
|
||||
filterset = ipam.filtersets.ServiceFilterSet
|
||||
table = ipam.tables.ServiceTable
|
||||
url = 'ipam:service_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class VLANIndex(SearchIndex):
|
||||
model = VLAN
|
||||
fields = (
|
||||
('name', 100),
|
||||
('vid', 100),
|
||||
('description', 500),
|
||||
)
|
||||
queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role')
|
||||
filterset = ipam.filtersets.VLANFilterSet
|
||||
|
||||
|
||||
@register_search()
|
||||
class VRFIndex(SearchIndex):
|
||||
model = VRF
|
||||
fields = (
|
||||
('name', 100),
|
||||
('rd', 200),
|
||||
('description', 500),
|
||||
)
|
||||
queryset = VRF.objects.prefetch_related('tenant', 'tenant__group')
|
||||
filterset = ipam.filtersets.VRFFilterSet
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django import forms
|
||||
|
||||
from netbox.search.backends import default_search_engine
|
||||
from netbox.search.backends import search_backend
|
||||
from utilities.forms import BootstrapMixin
|
||||
|
||||
from .base import *
|
||||
@ -26,13 +26,13 @@ class SearchForm(BootstrapMixin, forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["obj_type"] = forms.ChoiceField(
|
||||
choices=default_search_engine.get_search_choices(),
|
||||
choices=search_backend.get_search_choices(),
|
||||
required=False,
|
||||
label='Type'
|
||||
)
|
||||
|
||||
def get_options(self):
|
||||
if not self.options:
|
||||
self.options = build_options(default_search_engine.get_search_choices())
|
||||
self.options = build_options(search_backend.get_search_choices())
|
||||
|
||||
return self.options
|
||||
|
@ -9,6 +9,7 @@ from django.db import models
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices
|
||||
from extras.registry import registry
|
||||
from extras.utils import is_taggable, register_features
|
||||
from netbox.signals import post_clean
|
||||
from utilities.json import CustomFieldJSONEncoder
|
||||
|
@ -9,6 +9,7 @@ class SearchIndex:
|
||||
model: The model class for which this index is used.
|
||||
"""
|
||||
model = None
|
||||
fields = ()
|
||||
|
||||
@classmethod
|
||||
def get_category(cls):
|
||||
@ -19,6 +20,13 @@ class SearchIndex:
|
||||
return cls.category
|
||||
return cls.model._meta.app_config.verbose_name
|
||||
|
||||
@classmethod
|
||||
def to_cache(cls, instance):
|
||||
return [
|
||||
(field, str(getattr(instance, field)), weight)
|
||||
for field, weight in cls.fields
|
||||
]
|
||||
|
||||
|
||||
def register_search():
|
||||
def _wrapper(cls):
|
||||
|
@ -2,9 +2,11 @@ from collections import defaultdict
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.urls import reverse
|
||||
from django.db.models.signals import post_save
|
||||
|
||||
from extras.models import CachedValue
|
||||
from extras.registry import registry
|
||||
from netbox.constants import SEARCH_MAX_RESULTS
|
||||
|
||||
@ -12,6 +14,13 @@ from netbox.constants import SEARCH_MAX_RESULTS
|
||||
_backends_cache = {}
|
||||
|
||||
|
||||
def get_indexer(model):
|
||||
app_label = model._meta.app_label
|
||||
model_name = model._meta.model_name
|
||||
|
||||
return registry['search'][app_label][model_name]
|
||||
|
||||
|
||||
class SearchEngineError(Exception):
|
||||
"""Something went wrong with a search engine."""
|
||||
pass
|
||||
@ -21,6 +30,11 @@ class SearchBackend:
|
||||
"""A search engine capable of performing multi-table searches."""
|
||||
_search_choice_options = tuple()
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# Connect cache handler to the model post-save signal
|
||||
post_save.connect(self.cache)
|
||||
|
||||
def get_registry(self):
|
||||
r = {}
|
||||
for app_label, models in registry['search'].items():
|
||||
@ -53,7 +67,8 @@ class SearchBackend:
|
||||
"""Execute a search query for the given value."""
|
||||
raise NotImplementedError
|
||||
|
||||
def cache(self, instance):
|
||||
@staticmethod
|
||||
def cache(sender, instance, **kwargs):
|
||||
"""Create or update the cached copy of an instance."""
|
||||
raise NotImplementedError
|
||||
|
||||
@ -70,7 +85,6 @@ class FilterSetSearchBackend(SearchBackend):
|
||||
for obj_type in search_registry.keys():
|
||||
|
||||
queryset = search_registry[obj_type].queryset
|
||||
url = search_registry[obj_type].url
|
||||
|
||||
# Restrict the queryset for the current user
|
||||
if hasattr(queryset, 'restrict'):
|
||||
@ -81,30 +95,51 @@ class FilterSetSearchBackend(SearchBackend):
|
||||
# This backend requires a FilterSet class for the model
|
||||
continue
|
||||
|
||||
table = getattr(search_registry[obj_type], 'table', None)
|
||||
if not table:
|
||||
# This backend requires a Table class for the model
|
||||
continue
|
||||
queryset = filterset({'q': value}, queryset=queryset).qs[:SEARCH_MAX_RESULTS]
|
||||
|
||||
# Construct the results table for this object type
|
||||
filtered_queryset = filterset({'q': value}, queryset=queryset).qs
|
||||
table = table(filtered_queryset, orderable=False)
|
||||
table.paginate(per_page=SEARCH_MAX_RESULTS)
|
||||
|
||||
if table.page:
|
||||
results.append({
|
||||
'name': queryset.model._meta.verbose_name_plural,
|
||||
'table': table,
|
||||
'url': f"{reverse(url)}?q={value}"
|
||||
})
|
||||
results.extend([
|
||||
{'object': obj}
|
||||
for obj in queryset
|
||||
])
|
||||
|
||||
return results
|
||||
|
||||
def cache(self, instance):
|
||||
@staticmethod
|
||||
def cache(sender, instance, **kwargs):
|
||||
# This backend does not utilize a cache
|
||||
pass
|
||||
|
||||
|
||||
class CachedValueSearchBackend(SearchBackend):
|
||||
|
||||
def search(self, request, value, **kwargs):
|
||||
return CachedValue.objects.filter(value__icontains=value)
|
||||
|
||||
@staticmethod
|
||||
def cache(sender, instance, **kwargs):
|
||||
try:
|
||||
indexer = get_indexer(instance)
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
data = indexer.to_cache(instance)
|
||||
|
||||
for field, value, weight in data:
|
||||
if not value:
|
||||
continue
|
||||
ct = ContentType.objects.get_for_model(instance)
|
||||
CachedValue.objects.update_or_create(
|
||||
defaults={
|
||||
'value': value,
|
||||
'weight': weight,
|
||||
},
|
||||
object_type=ct,
|
||||
object_id=instance.pk,
|
||||
field=field,
|
||||
type='text' # TODO
|
||||
)
|
||||
|
||||
|
||||
def get_backend():
|
||||
"""Initializes and returns the configured search backend."""
|
||||
backend_name = settings.SEARCH_BACKEND
|
||||
@ -121,5 +156,4 @@ def get_backend():
|
||||
return backend_cls()
|
||||
|
||||
|
||||
default_search_engine = get_backend()
|
||||
search = default_search_engine.search
|
||||
search_backend = get_backend()
|
||||
|
@ -23,7 +23,7 @@ from extras.tables import ObjectChangeTable
|
||||
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
|
||||
from netbox.constants import SEARCH_MAX_RESULTS
|
||||
from netbox.forms import SearchForm
|
||||
from netbox.search.backends import default_search_engine
|
||||
from netbox.search.backends import search_backend
|
||||
from tenancy.models import Tenant
|
||||
from virtualization.models import Cluster, VirtualMachine
|
||||
from wireless.models import WirelessLAN, WirelessLink
|
||||
@ -153,14 +153,14 @@ class SearchView(View):
|
||||
results = []
|
||||
|
||||
if form.is_valid():
|
||||
search_registry = default_search_engine.get_registry()
|
||||
search_registry = search_backend.get_registry()
|
||||
# If an object type has been specified, redirect to the dedicated view for it
|
||||
if form.cleaned_data['obj_type']:
|
||||
object_type = form.cleaned_data['obj_type']
|
||||
url = reverse(search_registry[object_type].url)
|
||||
return redirect(f"{url}?q={form.cleaned_data['q']}")
|
||||
|
||||
results = default_search_engine.search(request, form.cleaned_data['q'])
|
||||
results = search_backend.search(request, form.cleaned_data['q'])
|
||||
|
||||
return render(request, 'search.html', {
|
||||
'form': form,
|
||||
|
@ -20,44 +20,29 @@
|
||||
{% if request.GET.q %}
|
||||
{% if results %}
|
||||
<div class="row">
|
||||
<div class="col col-md-9">
|
||||
{% for obj_type in results %}
|
||||
<div class="card">
|
||||
<h5 class="card-header" id="{{ obj_type.name|lower }}">{{ obj_type.name|bettertitle }}</h5>
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table obj_type.table 'inc/table.html' %}
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<a href="{{ obj_type.url }}" class="btn btn-sm btn-primary my-1">
|
||||
<i class="mdi mdi-arrow-right-bold" aria-hidden="true"></i>
|
||||
{% if obj_type.table.page.has_next %}
|
||||
See All {{ obj_type.table.page.paginator.count }} Results
|
||||
{% else %}
|
||||
Refine Search
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="col col-md-3">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Search Results
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for obj_type in results %}
|
||||
<a href="#{{ obj_type.name|lower }}" class="list-group-item">
|
||||
<div class="float-end">
|
||||
{% badge obj_type.table.page.paginator.count %}
|
||||
</div>
|
||||
{{ obj_type.name|bettertitle }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body table-responsive">
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Object</th>
|
||||
<th>Field</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
{% for result in results %}
|
||||
<tr>
|
||||
<td>{{ result.object|content_type }}</td>
|
||||
<td>
|
||||
<a href="{{ result.object.get_absolute_url }}">{{ result.object }}</a>
|
||||
</td>
|
||||
<td>{{ result.field|placeholder }}</td>
|
||||
<td>{{ result.value|placeholder }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
@ -5,21 +5,32 @@ from tenancy.models import Contact, ContactAssignment, Tenant
|
||||
from utilities.utils import count_related
|
||||
|
||||
|
||||
@register_search()
|
||||
class TenantIndex(SearchIndex):
|
||||
model = Tenant
|
||||
queryset = Tenant.objects.prefetch_related('group')
|
||||
filterset = tenancy.filtersets.TenantFilterSet
|
||||
table = tenancy.tables.TenantTable
|
||||
url = 'tenancy:tenant_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class ContactIndex(SearchIndex):
|
||||
model = Contact
|
||||
fields = (
|
||||
('name', 100),
|
||||
('title', 200),
|
||||
('phone', 200),
|
||||
('email', 200),
|
||||
('address', 200),
|
||||
('link', 300),
|
||||
('comments', 1000),
|
||||
)
|
||||
queryset = Contact.objects.prefetch_related('group', 'assignments').annotate(
|
||||
assignment_count=count_related(ContactAssignment, 'contact')
|
||||
)
|
||||
filterset = tenancy.filtersets.ContactFilterSet
|
||||
table = tenancy.tables.ContactTable
|
||||
url = 'tenancy:contact_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class TenantIndex(SearchIndex):
|
||||
model = Tenant
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 100),
|
||||
('description', 500),
|
||||
('comments', 1000),
|
||||
)
|
||||
queryset = Tenant.objects.prefetch_related('group')
|
||||
filterset = tenancy.filtersets.TenantFilterSet
|
||||
|
@ -9,17 +9,23 @@ from virtualization.models import Cluster, VirtualMachine
|
||||
@register_search()
|
||||
class ClusterIndex(SearchIndex):
|
||||
model = Cluster
|
||||
fields = (
|
||||
('name', 100),
|
||||
('comments', 1000),
|
||||
)
|
||||
queryset = Cluster.objects.prefetch_related('type', 'group').annotate(
|
||||
device_count=count_related(Device, 'cluster'), vm_count=count_related(VirtualMachine, 'cluster')
|
||||
)
|
||||
filterset = virtualization.filtersets.ClusterFilterSet
|
||||
table = virtualization.tables.ClusterTable
|
||||
url = 'virtualization:cluster_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class VirtualMachineIndex(SearchIndex):
|
||||
model = VirtualMachine
|
||||
fields = (
|
||||
('name', 100),
|
||||
('comments', 1000),
|
||||
)
|
||||
queryset = VirtualMachine.objects.prefetch_related(
|
||||
'cluster',
|
||||
'tenant',
|
||||
@ -29,5 +35,3 @@ class VirtualMachineIndex(SearchIndex):
|
||||
'primary_ip6',
|
||||
)
|
||||
filterset = virtualization.filtersets.VirtualMachineFilterSet
|
||||
table = virtualization.tables.VirtualMachineTable
|
||||
url = 'virtualization:virtualmachine_list'
|
||||
|
@ -9,18 +9,24 @@ from wireless.models import WirelessLAN, WirelessLink
|
||||
@register_search()
|
||||
class WirelessLANIndex(SearchIndex):
|
||||
model = WirelessLAN
|
||||
fields = (
|
||||
('ssid', 100),
|
||||
('description', 500),
|
||||
('auth_psk', 1000),
|
||||
)
|
||||
queryset = WirelessLAN.objects.prefetch_related('group', 'vlan').annotate(
|
||||
interface_count=count_related(Interface, 'wireless_lans')
|
||||
)
|
||||
filterset = wireless.filtersets.WirelessLANFilterSet
|
||||
table = wireless.tables.WirelessLANTable
|
||||
url = 'wireless:wirelesslan_list'
|
||||
|
||||
|
||||
@register_search()
|
||||
class WirelessLinkIndex(SearchIndex):
|
||||
model = WirelessLink
|
||||
fields = (
|
||||
('ssid', 100),
|
||||
('description', 500),
|
||||
('auth_psk', 1000),
|
||||
)
|
||||
queryset = WirelessLink.objects.prefetch_related('interface_a__device', 'interface_b__device')
|
||||
filterset = wireless.filtersets.WirelessLinkFilterSet
|
||||
table = wireless.tables.WirelessLinkTable
|
||||
url = 'wireless:wirelesslink_list'
|
||||
|
Loading…
Reference in New Issue
Block a user