Skip to content

Commit

Permalink
#7016 base search classes
Browse files Browse the repository at this point in the history
  • Loading branch information
arthanson committed Sep 19, 2022
1 parent 4208dbd commit 7fd7c87
Show file tree
Hide file tree
Showing 6 changed files with 397 additions and 0 deletions.
148 changes: 148 additions & 0 deletions netbox/dcim/search_indexes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import dcim.filtersets
import dcim.tables
from dcim.models import (
Cable,
Device,
DeviceType,
Interface,
Location,
Module,
ModuleType,
PowerFeed,
Rack,
RackReservation,
Site,
VirtualChassis,
)
from django.db import models
from search.models import SearchMixin


class SiteIndex(SearchMixin):
model = Site
queryset = Site.objects.prefetch_related('region', 'tenant', 'tenant__group')
filterset = dcim.filtersets.SiteFilterSet
table = dcim.tables.SiteTable
url = 'dcim:site_list'


class RackIndex(SearchMixin):
model = Rack
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
device_count=count_related(Device, 'rack')
)
filterset = dcim.filtersets.RackFilterSet
table = dcim.tables.RackTable
url = 'dcim:rack_list'


class RackReservationIndex(SearchMixin):
model = RackReservation
queryset = RackReservation.objects.prefetch_related('rack', 'user')
filterset = dcim.filtersets.RackReservationFilterSet
table = dcim.tables.RackReservationTable
url = 'dcim:rackreservation_list'


class LocationIndex(SearchMixin):
model = Site
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'


class DeviceTypeIndex(SearchMixin):
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'


class DeviceIndex(SearchMixin):
model = DeviceIndex
queryset = Device.objects.prefetch_related(
'device_type__manufacturer',
'device_role',
'tenant',
'tenant__group',
'site',
'rack',
'primary_ip4',
'primary_ip6',
)
filterset = dcim.filtersets.DeviceFilterSet
table = dcim.tables.DeviceTable
url = 'dcim:device_list'


class ModuleTypeIndex(SearchMixin):
model = ModuleType
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
instance_count=count_related(Module, 'module_type')
)
filterset = dcim.filtersets.ModuleTypeFilterSet
table = dcim.tables.ModuleTypeTable
url = 'dcim:moduletype_list'


class ModuleIndex(SearchMixin):
model = Module
queryset = Module.objects.prefetch_related(
'module_type__manufacturer',
'device',
'module_bay',
)
filterset = dcim.filtersets.ModuleFilterSet
table = dcim.tables.ModuleTable
url = 'dcim:module_list'


class VirtualChassisIndex(SearchMixin):
model = VirtualChassis
queryset = VirtualChassis.objects.prefetch_related('master').annotate(
member_count=count_related(Device, 'virtual_chassis')
)
filterset = dcim.filtersets.VirtualChassisFilterSet
table = dcim.tables.VirtualChassisTable
url = 'dcim:virtualchassis_list'


class CableIndex(SearchMixin):
model = Cable
queryset = Cable.objects.all()
filterset = dcim.filtersets.CableFilterSet
table = dcim.tables.CableTable
url = 'dcim:cable_list'


class PowerFeedIndex(SearchMixin):
model = PowerFeed
queryset = PowerFeed.objects.all()
filterset = dcim.filtersets.PowerFeedFilterSet
table = dcim.tables.PowerFeedTable
url = 'dcim:powerfeed_list'


DCIM_SEARCH_ORDERING = [
SiteIndex,
RackIndex,
RackReservationIndex,
LocationIndex,
DeviceTypeIndex,
DeviceIndex,
ModuleTypeIndex,
ModuleIndex,
VirtualChassisIndex,
CableIndex,
PowerFeedIndex,
]
1 change: 1 addition & 0 deletions netbox/netbox/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ def _setting(name, default=None):
'wireless',
'django_rq', # Must come after extras to allow overriding management commands
'drf_yasg',
'search',
]

# Middleware
Expand Down
Empty file added netbox/search/__init__.py
Empty file.
33 changes: 33 additions & 0 deletions netbox/search/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from django.apps import AppConfig

from django.apps import apps
from django.utils.module_loading import module_has_submodule
from netbox import denormalized


def get_app_modules():
"""
Returns all app modules (installed apps) - yields tuples of (app_name, module)
"""
for app in apps.get_app_configs():
yield app.name, app.module


def get_app_submodules(submodule_name):
"""
Searches each app module for the specified submodule - yields tuples of (app_name, module)
"""
for name, module in get_app_modules():
if module_has_submodule(module, submodule_name):
yield name, import_module(f"{name}.{submodule_name}")


class SearchConfig(AppConfig):
name = "search"
verbose_name = "search"

def ready(self):
for name, module in get_app_modules():
submodule_name = "search_indexes"
if module_has_submodule(module, submodule_name):
print(f"{name}.{submodule_name}")
113 changes: 113 additions & 0 deletions netbox/search/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from abc import ABC
from importlib import import_module

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.db.models.signals import post_save, pre_delete

# The cache for the initialized backend.
_backends_cache = {}


def get_backend(backend_name=None):
"""Initializes and returns the search backend."""
global _backends_cache
if not backend_name:
backend_name = getattr(settings, "SEARCH_BACKEND", "search.backends.PostgresIcontainsSearchBackend")

# Try to use the cached backend.
if backend_name in _backends_cache:
return _backends_cache[backend_name]

# Load the backend class.
backend_module_name, backend_cls_name = backend_name.rsplit(".", 1)
backend_module = import_module(backend_module_name)
try:
backend_cls = getattr(backend_module, backend_cls_name)
except AttributeError:
raise ImproperlyConfigured(f"Could not find a class named {backend_module_name} in {backend_cls_name}")

# Initialize the backend.
backend = backend_cls()
_backends_cache[backend_name] = backend
return backend


class SearchEngineError(Exception):

"""Something went wrong with a search engine."""


class SearchBackend(object):

"""A search engine capable of performing multi-table searches."""

_created_engines: dict = dict()

@classmethod
def get_created_engines(cls):
"""Returns all created search engines."""
return list(cls._created_engines.items())

def __init__(self, engine_slug: str):
"""Initializes the search engine."""
# Check the slug is unique for this project.
if engine_slug in SearchBackend._created_engines:
raise SearchEngineError(f"A search engine has already been created with the slug {engine_slug}")

# Initialize this engine.
self._registered_models = {}
self._engine_slug = engine_slug

# Store a reference to this engine.
self.__class__._created_engines[engine_slug] = self

def is_registered(self, model):
"""Checks whether the given model is registered with this search engine."""
return model in self._registered_models

def register(self, model):
"""
Registers the given model with this search engine.
If the given model is already registered with this search engine, a
RegistrationError will be raised.
"""
# Check for existing registration.
if self.is_registered(model):
raise RegistrationError(f"{model} is already registered with this search engine")

# Connect to the signalling framework.
if self._use_hooks():
post_save.connect(self._post_save_receiver, model)
pre_delete.connect(self._pre_delete_receiver, model)

# Signalling hooks.

def _use_hooks(self):
raise NotImplementedError

def _post_save_receiver(self, instance, **kwargs):
"""Signal handler for when a registered model has been saved."""
raise NotImplementedError

def _pre_delete_receiver(self, instance, **kwargs):
"""Signal handler for when a registered model has been deleted."""
raise NotImplementedError

# Searching.

def search(self, search_text, models=(), exclude=(), ranking=True, backend_name=None):
"""Performs a search using the given text, returning a queryset of SearchEntry."""
raise NotImplementedError


class PostgresIcontainsSearchBackend(SearchBackend):
def _use_hooks(self):
return False


# The main search methods.
default_search_engine = SearchBackend("default")
search = default_search_engine.search
Loading

0 comments on commit 7fd7c87

Please sign in to comment.