1import django_filters
2import netaddr
3from django.contrib.contenttypes.models import ContentType
4from django.core.exceptions import ValidationError
5from django.db.models import Q
6from netaddr.core import AddrFormatError
7
8from dcim.models import Device, Interface, Region, Site, SiteGroup
9from extras.filters import TagFilter
10from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
11from tenancy.filtersets import TenancyFilterSet
12from utilities.filters import (
13    ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
14)
15from virtualization.models import VirtualMachine, VMInterface
16from .choices import *
17from .models import *
18
19
20__all__ = (
21    'AggregateFilterSet',
22    'IPAddressFilterSet',
23    'IPRangeFilterSet',
24    'PrefixFilterSet',
25    'RIRFilterSet',
26    'RoleFilterSet',
27    'RouteTargetFilterSet',
28    'ServiceFilterSet',
29    'VLANFilterSet',
30    'VLANGroupFilterSet',
31    'VRFFilterSet',
32)
33
34
35class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
36    q = django_filters.CharFilter(
37        method='search',
38        label='Search',
39    )
40    import_target_id = django_filters.ModelMultipleChoiceFilter(
41        field_name='import_targets',
42        queryset=RouteTarget.objects.all(),
43        label='Import target',
44    )
45    import_target = django_filters.ModelMultipleChoiceFilter(
46        field_name='import_targets__name',
47        queryset=RouteTarget.objects.all(),
48        to_field_name='name',
49        label='Import target (name)',
50    )
51    export_target_id = django_filters.ModelMultipleChoiceFilter(
52        field_name='export_targets',
53        queryset=RouteTarget.objects.all(),
54        label='Export target',
55    )
56    export_target = django_filters.ModelMultipleChoiceFilter(
57        field_name='export_targets__name',
58        queryset=RouteTarget.objects.all(),
59        to_field_name='name',
60        label='Export target (name)',
61    )
62    tag = TagFilter()
63
64    def search(self, queryset, name, value):
65        if not value.strip():
66            return queryset
67        return queryset.filter(
68            Q(name__icontains=value) |
69            Q(rd__icontains=value) |
70            Q(description__icontains=value)
71        )
72
73    class Meta:
74        model = VRF
75        fields = ['id', 'name', 'rd', 'enforce_unique']
76
77
78class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
79    q = django_filters.CharFilter(
80        method='search',
81        label='Search',
82    )
83    importing_vrf_id = django_filters.ModelMultipleChoiceFilter(
84        field_name='importing_vrfs',
85        queryset=VRF.objects.all(),
86        label='Importing VRF',
87    )
88    importing_vrf = django_filters.ModelMultipleChoiceFilter(
89        field_name='importing_vrfs__rd',
90        queryset=VRF.objects.all(),
91        to_field_name='rd',
92        label='Import VRF (RD)',
93    )
94    exporting_vrf_id = django_filters.ModelMultipleChoiceFilter(
95        field_name='exporting_vrfs',
96        queryset=VRF.objects.all(),
97        label='Exporting VRF',
98    )
99    exporting_vrf = django_filters.ModelMultipleChoiceFilter(
100        field_name='exporting_vrfs__rd',
101        queryset=VRF.objects.all(),
102        to_field_name='rd',
103        label='Export VRF (RD)',
104    )
105    tag = TagFilter()
106
107    def search(self, queryset, name, value):
108        if not value.strip():
109            return queryset
110        return queryset.filter(
111            Q(name__icontains=value) |
112            Q(description__icontains=value)
113        )
114
115    class Meta:
116        model = RouteTarget
117        fields = ['id', 'name']
118
119
120class RIRFilterSet(OrganizationalModelFilterSet):
121
122    class Meta:
123        model = RIR
124        fields = ['id', 'name', 'slug', 'is_private', 'description']
125
126
127class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
128    q = django_filters.CharFilter(
129        method='search',
130        label='Search',
131    )
132    family = django_filters.NumberFilter(
133        field_name='prefix',
134        lookup_expr='family'
135    )
136    prefix = django_filters.CharFilter(
137        method='filter_prefix',
138        label='Prefix',
139    )
140    rir_id = django_filters.ModelMultipleChoiceFilter(
141        queryset=RIR.objects.all(),
142        label='RIR (ID)',
143    )
144    rir = django_filters.ModelMultipleChoiceFilter(
145        field_name='rir__slug',
146        queryset=RIR.objects.all(),
147        to_field_name='slug',
148        label='RIR (slug)',
149    )
150    tag = TagFilter()
151
152    class Meta:
153        model = Aggregate
154        fields = ['id', 'date_added']
155
156    def search(self, queryset, name, value):
157        if not value.strip():
158            return queryset
159        qs_filter = Q(description__icontains=value)
160        try:
161            prefix = str(netaddr.IPNetwork(value.strip()).cidr)
162            qs_filter |= Q(prefix__net_contains_or_equals=prefix)
163        except (AddrFormatError, ValueError):
164            pass
165        return queryset.filter(qs_filter)
166
167    def filter_prefix(self, queryset, name, value):
168        if not value.strip():
169            return queryset
170        try:
171            query = str(netaddr.IPNetwork(value).cidr)
172            return queryset.filter(prefix=query)
173        except (AddrFormatError, ValueError):
174            return queryset.none()
175
176
177class RoleFilterSet(OrganizationalModelFilterSet):
178    q = django_filters.CharFilter(
179        method='search',
180        label='Search',
181    )
182
183    class Meta:
184        model = Role
185        fields = ['id', 'name', 'slug']
186
187
188class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
189    q = django_filters.CharFilter(
190        method='search',
191        label='Search',
192    )
193    family = django_filters.NumberFilter(
194        field_name='prefix',
195        lookup_expr='family'
196    )
197    prefix = MultiValueCharFilter(
198        method='filter_prefix',
199        label='Prefix',
200    )
201    within = django_filters.CharFilter(
202        method='search_within',
203        label='Within prefix',
204    )
205    within_include = django_filters.CharFilter(
206        method='search_within_include',
207        label='Within and including prefix',
208    )
209    contains = django_filters.CharFilter(
210        method='search_contains',
211        label='Prefixes which contain this prefix or IP',
212    )
213    depth = MultiValueNumberFilter(
214        field_name='_depth'
215    )
216    children = MultiValueNumberFilter(
217        field_name='_children'
218    )
219    mask_length = MultiValueNumberFilter(
220        field_name='prefix',
221        lookup_expr='net_mask_length'
222    )
223    mask_length__gte = django_filters.NumberFilter(
224        field_name='prefix',
225        lookup_expr='net_mask_length__gte'
226    )
227    mask_length__lte = django_filters.NumberFilter(
228        field_name='prefix',
229        lookup_expr='net_mask_length__lte'
230    )
231    vrf_id = django_filters.ModelMultipleChoiceFilter(
232        queryset=VRF.objects.all(),
233        label='VRF',
234    )
235    vrf = django_filters.ModelMultipleChoiceFilter(
236        field_name='vrf__rd',
237        queryset=VRF.objects.all(),
238        to_field_name='rd',
239        label='VRF (RD)',
240    )
241    present_in_vrf_id = django_filters.ModelChoiceFilter(
242        queryset=VRF.objects.all(),
243        method='filter_present_in_vrf',
244        label='VRF'
245    )
246    present_in_vrf = django_filters.ModelChoiceFilter(
247        queryset=VRF.objects.all(),
248        method='filter_present_in_vrf',
249        to_field_name='rd',
250        label='VRF (RD)',
251    )
252    region_id = TreeNodeMultipleChoiceFilter(
253        queryset=Region.objects.all(),
254        field_name='site__region',
255        lookup_expr='in',
256        label='Region (ID)',
257    )
258    region = TreeNodeMultipleChoiceFilter(
259        queryset=Region.objects.all(),
260        field_name='site__region',
261        lookup_expr='in',
262        to_field_name='slug',
263        label='Region (slug)',
264    )
265    site_group_id = TreeNodeMultipleChoiceFilter(
266        queryset=SiteGroup.objects.all(),
267        field_name='site__group',
268        lookup_expr='in',
269        label='Site group (ID)',
270    )
271    site_group = TreeNodeMultipleChoiceFilter(
272        queryset=SiteGroup.objects.all(),
273        field_name='site__group',
274        lookup_expr='in',
275        to_field_name='slug',
276        label='Site group (slug)',
277    )
278    site_id = django_filters.ModelMultipleChoiceFilter(
279        queryset=Site.objects.all(),
280        label='Site (ID)',
281    )
282    site = django_filters.ModelMultipleChoiceFilter(
283        field_name='site__slug',
284        queryset=Site.objects.all(),
285        to_field_name='slug',
286        label='Site (slug)',
287    )
288    vlan_id = django_filters.ModelMultipleChoiceFilter(
289        queryset=VLAN.objects.all(),
290        label='VLAN (ID)',
291    )
292    vlan_vid = django_filters.NumberFilter(
293        field_name='vlan__vid',
294        label='VLAN number (1-4095)',
295    )
296    role_id = django_filters.ModelMultipleChoiceFilter(
297        queryset=Role.objects.all(),
298        label='Role (ID)',
299    )
300    role = django_filters.ModelMultipleChoiceFilter(
301        field_name='role__slug',
302        queryset=Role.objects.all(),
303        to_field_name='slug',
304        label='Role (slug)',
305    )
306    status = django_filters.MultipleChoiceFilter(
307        choices=PrefixStatusChoices,
308        null_value=None
309    )
310    tag = TagFilter()
311
312    class Meta:
313        model = Prefix
314        fields = ['id', 'is_pool', 'mark_utilized']
315
316    def search(self, queryset, name, value):
317        if not value.strip():
318            return queryset
319        qs_filter = Q(description__icontains=value)
320        try:
321            prefix = str(netaddr.IPNetwork(value.strip()).cidr)
322            qs_filter |= Q(prefix__net_contains_or_equals=prefix)
323        except (AddrFormatError, ValueError):
324            pass
325        return queryset.filter(qs_filter)
326
327    def filter_prefix(self, queryset, name, value):
328        query_values = []
329        for v in value:
330            try:
331                query_values.append(netaddr.IPNetwork(v))
332            except (AddrFormatError, ValueError):
333                pass
334        return queryset.filter(prefix__in=query_values)
335
336    def search_within(self, queryset, name, value):
337        value = value.strip()
338        if not value:
339            return queryset
340        try:
341            query = str(netaddr.IPNetwork(value).cidr)
342            return queryset.filter(prefix__net_contained=query)
343        except (AddrFormatError, ValueError):
344            return queryset.none()
345
346    def search_within_include(self, queryset, name, value):
347        value = value.strip()
348        if not value:
349            return queryset
350        try:
351            query = str(netaddr.IPNetwork(value).cidr)
352            return queryset.filter(prefix__net_contained_or_equal=query)
353        except (AddrFormatError, ValueError):
354            return queryset.none()
355
356    def search_contains(self, queryset, name, value):
357        value = value.strip()
358        if not value:
359            return queryset
360        try:
361            # Searching by prefix
362            if '/' in value:
363                return queryset.filter(prefix__net_contains_or_equals=str(netaddr.IPNetwork(value).cidr))
364            # Searching by IP address
365            else:
366                return queryset.filter(prefix__net_contains=str(netaddr.IPAddress(value)))
367        except (AddrFormatError, ValueError):
368            return queryset.none()
369
370    def filter_present_in_vrf(self, queryset, name, vrf):
371        if vrf is None:
372            return queryset.none
373        return queryset.filter(
374            Q(vrf=vrf) |
375            Q(vrf__export_targets__in=vrf.import_targets.all())
376        )
377
378
379class IPRangeFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
380    q = django_filters.CharFilter(
381        method='search',
382        label='Search',
383    )
384    family = django_filters.NumberFilter(
385        field_name='start_address',
386        lookup_expr='family'
387    )
388    contains = django_filters.CharFilter(
389        method='search_contains',
390        label='Ranges which contain this prefix or IP',
391    )
392    vrf_id = django_filters.ModelMultipleChoiceFilter(
393        queryset=VRF.objects.all(),
394        label='VRF',
395    )
396    vrf = django_filters.ModelMultipleChoiceFilter(
397        field_name='vrf__rd',
398        queryset=VRF.objects.all(),
399        to_field_name='rd',
400        label='VRF (RD)',
401    )
402    role_id = django_filters.ModelMultipleChoiceFilter(
403        queryset=Role.objects.all(),
404        label='Role (ID)',
405    )
406    role = django_filters.ModelMultipleChoiceFilter(
407        field_name='role__slug',
408        queryset=Role.objects.all(),
409        to_field_name='slug',
410        label='Role (slug)',
411    )
412    status = django_filters.MultipleChoiceFilter(
413        choices=IPRangeStatusChoices,
414        null_value=None
415    )
416    tag = TagFilter()
417
418    class Meta:
419        model = IPRange
420        fields = ['id']
421
422    def search(self, queryset, name, value):
423        if not value.strip():
424            return queryset
425        qs_filter = Q(description__icontains=value)
426        try:
427            ipaddress = str(netaddr.IPNetwork(value.strip()).cidr)
428            qs_filter |= Q(start_address=ipaddress)
429            qs_filter |= Q(end_address=ipaddress)
430        except (AddrFormatError, ValueError):
431            pass
432        return queryset.filter(qs_filter)
433
434    def search_contains(self, queryset, name, value):
435        value = value.strip()
436        if not value:
437            return queryset
438        try:
439            # Strip mask
440            ipaddress = netaddr.IPNetwork(value)
441            return queryset.filter(start_address__lte=ipaddress, end_address__gte=ipaddress)
442        except (AddrFormatError, ValueError):
443            return queryset.none()
444
445
446class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
447    q = django_filters.CharFilter(
448        method='search',
449        label='Search',
450    )
451    family = django_filters.NumberFilter(
452        field_name='address',
453        lookup_expr='family'
454    )
455    parent = django_filters.CharFilter(
456        method='search_by_parent',
457        label='Parent prefix',
458    )
459    address = MultiValueCharFilter(
460        method='filter_address',
461        label='Address',
462    )
463    mask_length = django_filters.NumberFilter(
464        method='filter_mask_length',
465        label='Mask length',
466    )
467    vrf_id = django_filters.ModelMultipleChoiceFilter(
468        queryset=VRF.objects.all(),
469        label='VRF',
470    )
471    vrf = django_filters.ModelMultipleChoiceFilter(
472        field_name='vrf__rd',
473        queryset=VRF.objects.all(),
474        to_field_name='rd',
475        label='VRF (RD)',
476    )
477    present_in_vrf_id = django_filters.ModelChoiceFilter(
478        queryset=VRF.objects.all(),
479        method='filter_present_in_vrf',
480        label='VRF'
481    )
482    present_in_vrf = django_filters.ModelChoiceFilter(
483        queryset=VRF.objects.all(),
484        method='filter_present_in_vrf',
485        to_field_name='rd',
486        label='VRF (RD)',
487    )
488    device = MultiValueCharFilter(
489        method='filter_device',
490        field_name='name',
491        label='Device (name)',
492    )
493    device_id = MultiValueNumberFilter(
494        method='filter_device',
495        field_name='pk',
496        label='Device (ID)',
497    )
498    virtual_machine = MultiValueCharFilter(
499        method='filter_virtual_machine',
500        field_name='name',
501        label='Virtual machine (name)',
502    )
503    virtual_machine_id = MultiValueNumberFilter(
504        method='filter_virtual_machine',
505        field_name='pk',
506        label='Virtual machine (ID)',
507    )
508    interface = django_filters.ModelMultipleChoiceFilter(
509        field_name='interface__name',
510        queryset=Interface.objects.all(),
511        to_field_name='name',
512        label='Interface (name)',
513    )
514    interface_id = django_filters.ModelMultipleChoiceFilter(
515        field_name='interface',
516        queryset=Interface.objects.all(),
517        label='Interface (ID)',
518    )
519    vminterface = django_filters.ModelMultipleChoiceFilter(
520        field_name='vminterface__name',
521        queryset=VMInterface.objects.all(),
522        to_field_name='name',
523        label='VM interface (name)',
524    )
525    vminterface_id = django_filters.ModelMultipleChoiceFilter(
526        field_name='vminterface',
527        queryset=VMInterface.objects.all(),
528        label='VM interface (ID)',
529    )
530    assigned_to_interface = django_filters.BooleanFilter(
531        method='_assigned_to_interface',
532        label='Is assigned to an interface',
533    )
534    status = django_filters.MultipleChoiceFilter(
535        choices=IPAddressStatusChoices,
536        null_value=None
537    )
538    role = django_filters.MultipleChoiceFilter(
539        choices=IPAddressRoleChoices
540    )
541    tag = TagFilter()
542
543    class Meta:
544        model = IPAddress
545        fields = ['id', 'dns_name', 'description']
546
547    def search(self, queryset, name, value):
548        if not value.strip():
549            return queryset
550        qs_filter = (
551            Q(dns_name__icontains=value) |
552            Q(description__icontains=value) |
553            Q(address__istartswith=value)
554        )
555        return queryset.filter(qs_filter)
556
557    def search_by_parent(self, queryset, name, value):
558        value = value.strip()
559        if not value:
560            return queryset
561        try:
562            query = str(netaddr.IPNetwork(value.strip()).cidr)
563            return queryset.filter(address__net_host_contained=query)
564        except (AddrFormatError, ValueError):
565            return queryset.none()
566
567    def filter_address(self, queryset, name, value):
568        try:
569            return queryset.filter(address__net_in=value)
570        except ValidationError:
571            return queryset.none()
572
573    def filter_mask_length(self, queryset, name, value):
574        if not value:
575            return queryset
576        return queryset.filter(address__net_mask_length=value)
577
578    def filter_present_in_vrf(self, queryset, name, vrf):
579        if vrf is None:
580            return queryset.none
581        return queryset.filter(
582            Q(vrf=vrf) |
583            Q(vrf__export_targets__in=vrf.import_targets.all())
584        )
585
586    def filter_device(self, queryset, name, value):
587        devices = Device.objects.filter(**{'{}__in'.format(name): value})
588        if not devices.exists():
589            return queryset.none()
590        interface_ids = []
591        for device in devices:
592            interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
593        return queryset.filter(
594            interface__in=interface_ids
595        )
596
597    def filter_virtual_machine(self, queryset, name, value):
598        virtual_machines = VirtualMachine.objects.filter(**{'{}__in'.format(name): value})
599        if not virtual_machines.exists():
600            return queryset.none()
601        interface_ids = []
602        for vm in virtual_machines:
603            interface_ids.extend(vm.interfaces.values_list('id', flat=True))
604        return queryset.filter(
605            vminterface__in=interface_ids
606        )
607
608    def _assigned_to_interface(self, queryset, name, value):
609        return queryset.exclude(assigned_object_id__isnull=value)
610
611
612class VLANGroupFilterSet(OrganizationalModelFilterSet):
613    q = django_filters.CharFilter(
614        method='search',
615        label='Search',
616    )
617    scope_type = ContentTypeFilter()
618    region = django_filters.NumberFilter(
619        method='filter_scope'
620    )
621    sitegroup = django_filters.NumberFilter(
622        method='filter_scope'
623    )
624    site = django_filters.NumberFilter(
625        method='filter_scope'
626    )
627    location = django_filters.NumberFilter(
628        method='filter_scope'
629    )
630    rack = django_filters.NumberFilter(
631        method='filter_scope'
632    )
633    clustergroup = django_filters.NumberFilter(
634        method='filter_scope'
635    )
636    cluster = django_filters.NumberFilter(
637        method='filter_scope'
638    )
639
640    class Meta:
641        model = VLANGroup
642        fields = ['id', 'name', 'slug', 'description', 'scope_id']
643
644    def search(self, queryset, name, value):
645        if not value.strip():
646            return queryset
647        qs_filter = (
648            Q(name__icontains=value) |
649            Q(description__icontains=value)
650        )
651        return queryset.filter(qs_filter)
652
653    def filter_scope(self, queryset, name, value):
654        return queryset.filter(
655            scope_type=ContentType.objects.get(model=name),
656            scope_id=value
657        )
658
659
660class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
661    q = django_filters.CharFilter(
662        method='search',
663        label='Search',
664    )
665    region_id = TreeNodeMultipleChoiceFilter(
666        queryset=Region.objects.all(),
667        field_name='site__region',
668        lookup_expr='in',
669        label='Region (ID)',
670    )
671    region = TreeNodeMultipleChoiceFilter(
672        queryset=Region.objects.all(),
673        field_name='site__region',
674        lookup_expr='in',
675        to_field_name='slug',
676        label='Region (slug)',
677    )
678    site_group_id = TreeNodeMultipleChoiceFilter(
679        queryset=SiteGroup.objects.all(),
680        field_name='site__group',
681        lookup_expr='in',
682        label='Site group (ID)',
683    )
684    site_group = TreeNodeMultipleChoiceFilter(
685        queryset=SiteGroup.objects.all(),
686        field_name='site__group',
687        lookup_expr='in',
688        to_field_name='slug',
689        label='Site group (slug)',
690    )
691    site_id = django_filters.ModelMultipleChoiceFilter(
692        queryset=Site.objects.all(),
693        label='Site (ID)',
694    )
695    site = django_filters.ModelMultipleChoiceFilter(
696        field_name='site__slug',
697        queryset=Site.objects.all(),
698        to_field_name='slug',
699        label='Site (slug)',
700    )
701    group_id = django_filters.ModelMultipleChoiceFilter(
702        queryset=VLANGroup.objects.all(),
703        label='Group (ID)',
704    )
705    group = django_filters.ModelMultipleChoiceFilter(
706        field_name='group__slug',
707        queryset=VLANGroup.objects.all(),
708        to_field_name='slug',
709        label='Group',
710    )
711    role_id = django_filters.ModelMultipleChoiceFilter(
712        queryset=Role.objects.all(),
713        label='Role (ID)',
714    )
715    role = django_filters.ModelMultipleChoiceFilter(
716        field_name='role__slug',
717        queryset=Role.objects.all(),
718        to_field_name='slug',
719        label='Role (slug)',
720    )
721    status = django_filters.MultipleChoiceFilter(
722        choices=VLANStatusChoices,
723        null_value=None
724    )
725    available_on_device = django_filters.ModelChoiceFilter(
726        queryset=Device.objects.all(),
727        method='get_for_device'
728    )
729    available_on_virtualmachine = django_filters.ModelChoiceFilter(
730        queryset=VirtualMachine.objects.all(),
731        method='get_for_virtualmachine'
732    )
733    tag = TagFilter()
734
735    class Meta:
736        model = VLAN
737        fields = ['id', 'vid', 'name']
738
739    def search(self, queryset, name, value):
740        if not value.strip():
741            return queryset
742        qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
743        try:
744            qs_filter |= Q(vid=int(value.strip()))
745        except ValueError:
746            pass
747        return queryset.filter(qs_filter)
748
749    def get_for_device(self, queryset, name, value):
750        return queryset.get_for_device(value)
751
752    def get_for_virtualmachine(self, queryset, name, value):
753        return queryset.get_for_virtualmachine(value)
754
755
756class ServiceFilterSet(PrimaryModelFilterSet):
757    q = django_filters.CharFilter(
758        method='search',
759        label='Search',
760    )
761    device_id = django_filters.ModelMultipleChoiceFilter(
762        queryset=Device.objects.all(),
763        label='Device (ID)',
764    )
765    device = django_filters.ModelMultipleChoiceFilter(
766        field_name='device__name',
767        queryset=Device.objects.all(),
768        to_field_name='name',
769        label='Device (name)',
770    )
771    virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
772        queryset=VirtualMachine.objects.all(),
773        label='Virtual machine (ID)',
774    )
775    virtual_machine = django_filters.ModelMultipleChoiceFilter(
776        field_name='virtual_machine__name',
777        queryset=VirtualMachine.objects.all(),
778        to_field_name='name',
779        label='Virtual machine (name)',
780    )
781    port = NumericArrayFilter(
782        field_name='ports',
783        lookup_expr='contains'
784    )
785    tag = TagFilter()
786
787    class Meta:
788        model = Service
789        fields = ['id', 'name', 'protocol']
790
791    def search(self, queryset, name, value):
792        if not value.strip():
793            return queryset
794        qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
795        return queryset.filter(qs_filter)
796