diff --git a/netbox/netbox/forms.py b/netbox/netbox/forms.py new file mode 100644 index 000000000..2842668b4 --- /dev/null +++ b/netbox/netbox/forms.py @@ -0,0 +1,40 @@ +from django import forms + +from utilities.forms import BootstrapMixin + + +OBJ_TYPE_CHOICES = ( + ('', 'All Objects'), + ('Circuits', ( + ('provider', 'Providers'), + ('circuit', 'Circuits'), + )), + ('DCIM', ( + ('site', 'Sites'), + ('rack', 'Racks'), + ('devicetype', 'Device types'), + ('device', 'Devices'), + )), + ('IPAM', ( + ('vrf', 'VRFs'), + ('aggregate', 'Aggregates'), + ('prefix', 'Prefixes'), + ('ipaddress', 'IP addresses'), + ('vlan', 'VLANs'), + )), + ('Secrets', ( + ('secret', 'Secrets'), + )), + ('Tenancy', ( + ('tenant', 'Tenants'), + )), +) + + +class SearchForm(BootstrapMixin, forms.Form): + q = forms.CharField( + label='Query' + ) + obj_type = forms.ChoiceField( + choices=OBJ_TYPE_CHOICES, required=False, label='Type' + ) diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index e7a76bed9..724ab3090 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -2,7 +2,7 @@ from django.conf import settings from django.conf.urls import include, url from django.contrib import admin -from netbox.views import APIRootView, home, handle_500, trigger_500 +from netbox.views import APIRootView, home, handle_500, SearchView, trigger_500 from users.views import login, logout @@ -10,8 +10,9 @@ handler500 = handle_500 _patterns = [ - # Default page + # Base views url(r'^$', home, name='home'), + url(r'^search/$', SearchView.as_view(), name='search'), # Login/logout url(r'^login/$', login, name='login'), diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 17a1e19d2..12df44e6f 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -1,18 +1,122 @@ import sys -from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.reverse import reverse +from django.db.models import Count from django.shortcuts import render +from django.views.generic import View -from circuits.models import Provider, Circuit -from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection +from circuits.filters import CircuitFilter, ProviderFilter +from circuits.models import Circuit, Provider +from circuits.tables import CircuitTable, ProviderTable +from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter +from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site +from dcim.tables import DeviceTable, DeviceTypeTable, RackTable, SiteTable from extras.models import UserAction -from ipam.models import Aggregate, Prefix, IPAddress, VLAN, VRF +from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter +from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF +from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable +from secrets.filters import SecretFilter from secrets.models import Secret +from secrets.tables import SecretTable +from tenancy.filters import TenantFilter from tenancy.models import Tenant +from tenancy.tables import TenantTable +from .forms import SearchForm + + +SEARCH_MAX_RESULTS = 15 +SEARCH_TYPES = { + # Circuits + 'provider': { + 'queryset': Provider.objects.annotate(count_circuits=Count('circuits')), + 'filter': ProviderFilter, + 'table': ProviderTable, + 'url': 'circuits:provider_list', + }, + 'circuit': { + 'queryset': Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related( + 'terminations__site' + ), + 'filter': CircuitFilter, + 'table': CircuitTable, + 'url': 'circuits:circuit_list', + }, + # DCIM + 'site': { + 'queryset': Site.objects.select_related('region', 'tenant'), + 'filter': SiteFilter, + 'table': SiteTable, + 'url': 'dcim:site_list', + }, + 'rack': { + 'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('devices__device_type').annotate(device_count=Count('devices', distinct=True)), + 'filter': RackFilter, + 'table': RackTable, + 'url': 'dcim:rack_list', + }, + 'devicetype': { + 'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')), + 'filter': DeviceTypeFilter, + 'table': DeviceTypeTable, + 'url': 'dcim:devicetype_list', + }, + 'device': { + 'queryset': Device.objects.select_related( + 'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6' + ), + 'filter': DeviceFilter, + 'table': DeviceTable, + 'url': 'dcim:device_list', + }, + # IPAM + 'vrf': { + 'queryset': VRF.objects.select_related('tenant'), + 'filter': VRFFilter, + 'table': VRFTable, + 'url': 'ipam:vrf_list', + }, + 'aggregate': { + 'queryset': Aggregate.objects.select_related('rir'), + 'filter': AggregateFilter, + 'table': AggregateTable, + 'url': 'ipam:aggregate_list', + }, + 'prefix': { + 'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'), + 'filter': PrefixFilter, + 'table': PrefixTable, + 'url': 'ipam:prefix_list', + }, + 'ipaddress': { + 'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'), + 'filter': IPAddressFilter, + 'table': IPAddressTable, + 'url': 'ipam:ipaddress_list', + }, + 'vlan': { + 'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes'), + 'filter': VLANFilter, + 'table': VLANTable, + 'url': 'ipam:vlan_list', + }, + # Secrets + 'secret': { + 'queryset': Secret.objects.select_related('role', 'device'), + 'filter': SecretFilter, + 'table': SecretTable, + 'url': 'secrets:secret_list', + }, + # Tenancy + 'tenant': { + 'queryset': Tenant.objects.select_related('group'), + 'filter': TenantFilter, + 'table': TenantTable, + 'url': 'tenancy:tenant_list', + }, +} def home(request): @@ -47,11 +151,56 @@ def home(request): } return render(request, 'home.html', { + 'search_form': SearchForm(), 'stats': stats, 'recent_activity': UserAction.objects.select_related('user')[:50] }) +class SearchView(View): + + def get(self, request): + + # No query + if 'q' not in request.GET: + return render(request, 'search.html', { + 'form': SearchForm(), + }) + + form = SearchForm(request.GET) + results = [] + + if form.is_valid(): + + # Searching for a single type of object + if form.cleaned_data['obj_type']: + obj_types = [form.cleaned_data['obj_type']] + # Searching all object types + else: + obj_types = SEARCH_TYPES.keys() + + for obj_type in obj_types: + queryset = SEARCH_TYPES[obj_type]['queryset'] + filter = SEARCH_TYPES[obj_type]['filter'] + table = SEARCH_TYPES[obj_type]['table'] + url = SEARCH_TYPES[obj_type]['url'] + filtered_queryset = filter({'q': form.cleaned_data['q']}, queryset=queryset).qs + total_count = filtered_queryset.count() + if total_count: + results.append({ + 'name': queryset.model._meta.verbose_name_plural, + 'table': table(filtered_queryset[:SEARCH_MAX_RESULTS]), + 'total': total_count, + 'url': '{}?q={}'.format(reverse(url), form.cleaned_data['q']) + }) + + return render(request, 'search.html', { + 'form': form, + 'results': results, + }) + + + class APIRootView(APIView): _ignore_model_permissions = True exclude_from_schema = True diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 8a1b35a6b..308e9de3d 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -48,7 +48,7 @@ class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): @method_decorator(login_required, name='dispatch') class SecretListView(ObjectListView): - queryset = Secret.objects.select_related('role').prefetch_related('device') + queryset = Secret.objects.select_related('role', 'device') filter = filters.SecretFilter filter_form = forms.SecretFilterForm table = tables.SecretTable diff --git a/netbox/templates/home.html b/netbox/templates/home.html index 043526332..b489d90e5 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -3,62 +3,13 @@ {% block content %}