Coverage for ipam/views.py: 79%
982 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1import django_filters
2from django.contrib.contenttypes.models import ContentType
3from django.db.models import Prefetch
4from django.db.models.expressions import RawSQL
5from django.shortcuts import get_object_or_404, redirect, render
6from django.urls import reverse
7from django.utils.translation import gettext_lazy as _
8from django_filters.constants import EMPTY_VALUES
10from circuits.models import Provider
11from dcim.filtersets import InterfaceFilterSet
12from dcim.forms import InterfaceFilterForm
13from dcim.models import Device, Interface, Site
14from extras.ui.panels import CustomFieldsPanel, TagsPanel
15from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
16from netbox.ui import actions, layout
17from netbox.ui.breadcrumbs import Breadcrumb, filtered_list_url
18from netbox.ui.panels import (
19 CommentsPanel,
20 ContextTablePanel,
21 ObjectsTablePanel,
22 PluginContentPanel,
23 RelatedObjectsPanel,
24 TemplatePanel,
25)
26from netbox.views import generic
27from utilities.query import count_related
28from utilities.tables import get_table_ordering
29from utilities.views import GetRelatedModelsMixin, GetReturnURLMixin, ViewTab, register_model_view
30from virtualization.filtersets import VMInterfaceFilterSet
31from virtualization.forms import VMInterfaceFilterForm
32from virtualization.models import VirtualMachine, VMInterface
34from . import filtersets, forms, tables
35from .choices import PrefixStatusChoices
36from .constants import *
37from .models import *
38from .ui import panels
39from .utils import add_available_vlans, add_requested_prefixes, annotate_ip_space
41#
42# VRFs
43#
46@register_model_view(VRF, 'list', path='', detail=False)
47class VRFListView(generic.ObjectListView):
48 queryset = VRF.objects.all()
49 filterset = filtersets.VRFFilterSet
50 filterset_form = forms.VRFFilterForm
51 table = tables.VRFTable
54@register_model_view(VRF)
55class VRFView(GetRelatedModelsMixin, generic.ObjectView):
56 queryset = VRF.objects.all()
57 template_name = 'generic/object.html'
58 layout = layout.Layout(
59 layout.Row(
60 layout.Column(
61 panels.VRFPanel(),
62 TagsPanel(),
63 PluginContentPanel('left_page'),
64 ),
65 layout.Column(
66 RelatedObjectsPanel(),
67 CustomFieldsPanel(),
68 CommentsPanel(),
69 PluginContentPanel('right_page'),
70 ),
71 ),
72 layout.Row(
73 layout.Column(
74 ContextTablePanel('import_targets_table', title=_('Import route targets')),
75 ),
76 layout.Column(
77 ContextTablePanel('export_targets_table', title=_('Export route targets')),
78 ),
79 ),
80 layout.Row(
81 layout.Column(
82 PluginContentPanel('full_width_page'),
83 ),
84 ),
85 )
87 def get_extra_context(self, request, instance):
88 import_targets_table = tables.RouteTargetTable(
89 instance.import_targets.all(),
90 orderable=False
91 )
92 import_targets_table.configure(request)
94 export_targets_table = tables.RouteTargetTable(
95 instance.export_targets.all(),
96 orderable=False
97 )
98 export_targets_table.configure(request)
100 related_models = self.get_related_models(
101 request,
102 instance,
103 omit=(Interface, VMInterface),
104 extra=(
105 (
106 Interface.objects.restrict(request.user, 'view').filter(vrf=instance),
107 'vrf_id',
108 _('Device Interfaces')
109 ),
110 (
111 VMInterface.objects.restrict(request.user, 'view').filter(vrf=instance),
112 'vrf_id',
113 _('VM Interfaces')
114 ),
115 ),
116 )
118 return {
119 'related_models': related_models,
120 'import_targets_table': import_targets_table,
121 'export_targets_table': export_targets_table,
122 }
125@register_model_view(VRF, 'add', detail=False)
126@register_model_view(VRF, 'edit')
127class VRFEditView(generic.ObjectEditView):
128 queryset = VRF.objects.all()
129 form = forms.VRFForm
132@register_model_view(VRF, 'delete')
133class VRFDeleteView(generic.ObjectDeleteView):
134 queryset = VRF.objects.all()
137@register_model_view(VRF, 'bulk_import', path='import', detail=False)
138class VRFBulkImportView(generic.BulkImportView):
139 queryset = VRF.objects.all()
140 model_form = forms.VRFImportForm
143@register_model_view(VRF, 'bulk_edit', path='edit', detail=False)
144class VRFBulkEditView(generic.BulkEditView):
145 queryset = VRF.objects.all()
146 filterset = filtersets.VRFFilterSet
147 table = tables.VRFTable
148 form = forms.VRFBulkEditForm
151@register_model_view(VRF, 'bulk_rename', path='rename', detail=False)
152class VRFBulkRenameView(generic.BulkRenameView):
153 queryset = VRF.objects.all()
154 filterset = filtersets.VRFFilterSet
157@register_model_view(VRF, 'bulk_delete', path='delete', detail=False)
158class VRFBulkDeleteView(generic.BulkDeleteView):
159 queryset = VRF.objects.all()
160 filterset = filtersets.VRFFilterSet
161 table = tables.VRFTable
164#
165# Route targets
166#
168@register_model_view(RouteTarget, 'list', path='', detail=False)
169class RouteTargetListView(generic.ObjectListView):
170 queryset = RouteTarget.objects.all()
171 filterset = filtersets.RouteTargetFilterSet
172 filterset_form = forms.RouteTargetFilterForm
173 table = tables.RouteTargetTable
176@register_model_view(RouteTarget)
177class RouteTargetView(generic.ObjectView):
178 queryset = RouteTarget.objects.all()
179 template_name = 'generic/object.html'
180 layout = layout.Layout(
181 layout.Row(
182 layout.Column(
183 panels.RouteTargetPanel(),
184 TagsPanel(),
185 PluginContentPanel('left_page'),
186 ),
187 layout.Column(
188 CustomFieldsPanel(),
189 CommentsPanel(),
190 PluginContentPanel('right_page'),
191 ),
192 ),
193 layout.Row(
194 layout.Column(
195 ObjectsTablePanel(
196 'ipam.vrf',
197 filters={'import_target_id': lambda ctx: ctx['object'].pk},
198 title=_('Importing VRFs'),
199 ),
200 ),
201 layout.Column(
202 ObjectsTablePanel(
203 'ipam.vrf',
204 filters={'export_target_id': lambda ctx: ctx['object'].pk},
205 title=_('Exporting VRFs'),
206 ),
207 ),
208 ),
209 layout.Row(
210 layout.Column(
211 ObjectsTablePanel(
212 'vpn.l2vpn',
213 filters={'import_target_id': lambda ctx: ctx['object'].pk},
214 title=_('Importing L2VPNs'),
215 ),
216 ),
217 layout.Column(
218 ObjectsTablePanel(
219 'vpn.l2vpn',
220 filters={'export_target_id': lambda ctx: ctx['object'].pk},
221 title=_('Exporting L2VPNs'),
222 ),
223 ),
224 ),
225 layout.Row(
226 layout.Column(
227 PluginContentPanel('full_width_page'),
228 ),
229 ),
230 )
233@register_model_view(RouteTarget, 'add', detail=False)
234@register_model_view(RouteTarget, 'edit')
235class RouteTargetEditView(generic.ObjectEditView):
236 queryset = RouteTarget.objects.all()
237 form = forms.RouteTargetForm
240@register_model_view(RouteTarget, 'delete')
241class RouteTargetDeleteView(generic.ObjectDeleteView):
242 queryset = RouteTarget.objects.all()
245@register_model_view(RouteTarget, 'bulk_import', path='import', detail=False)
246class RouteTargetBulkImportView(generic.BulkImportView):
247 queryset = RouteTarget.objects.all()
248 model_form = forms.RouteTargetImportForm
251@register_model_view(RouteTarget, 'bulk_edit', path='edit', detail=False)
252class RouteTargetBulkEditView(generic.BulkEditView):
253 queryset = RouteTarget.objects.all()
254 filterset = filtersets.RouteTargetFilterSet
255 table = tables.RouteTargetTable
256 form = forms.RouteTargetBulkEditForm
259@register_model_view(RouteTarget, 'bulk_rename', path='rename', detail=False)
260class RouteTargetBulkRenameView(generic.BulkRenameView):
261 queryset = RouteTarget.objects.all()
262 filterset = filtersets.RouteTargetFilterSet
265@register_model_view(RouteTarget, 'bulk_delete', path='delete', detail=False)
266class RouteTargetBulkDeleteView(generic.BulkDeleteView):
267 queryset = RouteTarget.objects.all()
268 filterset = filtersets.RouteTargetFilterSet
269 table = tables.RouteTargetTable
272#
273# RIRs
274#
276@register_model_view(RIR, 'list', path='', detail=False)
277class RIRListView(generic.ObjectListView):
278 queryset = RIR.objects.annotate(
279 aggregate_count=count_related(Aggregate, 'rir')
280 )
281 filterset = filtersets.RIRFilterSet
282 filterset_form = forms.RIRFilterForm
283 table = tables.RIRTable
286@register_model_view(RIR)
287class RIRView(GetRelatedModelsMixin, generic.ObjectView):
288 queryset = RIR.objects.all()
289 layout = layout.SimpleLayout(
290 left_panels=[
291 panels.RIRPanel(),
292 TagsPanel(),
293 ],
294 right_panels=[
295 RelatedObjectsPanel(),
296 CommentsPanel(),
297 CustomFieldsPanel(),
298 ],
299 )
301 def get_extra_context(self, request, instance):
302 return {
303 'related_models': self.get_related_models(request, instance),
304 }
307@register_model_view(RIR, 'add', detail=False)
308@register_model_view(RIR, 'edit')
309class RIREditView(generic.ObjectEditView):
310 queryset = RIR.objects.all()
311 form = forms.RIRForm
314@register_model_view(RIR, 'delete')
315class RIRDeleteView(generic.ObjectDeleteView):
316 queryset = RIR.objects.all()
319@register_model_view(RIR, 'bulk_import', path='import', detail=False)
320class RIRBulkImportView(generic.BulkImportView):
321 queryset = RIR.objects.all()
322 model_form = forms.RIRImportForm
325@register_model_view(RIR, 'bulk_edit', path='edit', detail=False)
326class RIRBulkEditView(generic.BulkEditView):
327 queryset = RIR.objects.annotate(
328 aggregate_count=count_related(Aggregate, 'rir')
329 )
330 filterset = filtersets.RIRFilterSet
331 table = tables.RIRTable
332 form = forms.RIRBulkEditForm
335@register_model_view(RIR, 'bulk_rename', path='rename', detail=False)
336class RIRBulkRenameView(generic.BulkRenameView):
337 queryset = RIR.objects.all()
338 filterset = filtersets.RIRFilterSet
341@register_model_view(RIR, 'bulk_delete', path='delete', detail=False)
342class RIRBulkDeleteView(generic.BulkDeleteView):
343 queryset = RIR.objects.annotate(
344 aggregate_count=count_related(Aggregate, 'rir')
345 )
346 filterset = filtersets.RIRFilterSet
347 table = tables.RIRTable
350#
351# ASN ranges
352#
354@register_model_view(ASNRange, 'list', path='', detail=False)
355class ASNRangeListView(generic.ObjectListView):
356 queryset = ASNRange.objects.annotate_asn_counts()
357 filterset = filtersets.ASNRangeFilterSet
358 filterset_form = forms.ASNRangeFilterForm
359 table = tables.ASNRangeTable
362@register_model_view(ASNRange)
363class ASNRangeView(generic.ObjectView):
364 template_name = 'generic/object.html'
365 queryset = ASNRange.objects.all()
366 layout = layout.SimpleLayout(
367 breadcrumbs=[
368 Breadcrumb('rir', url=filtered_list_url('ipam:asnrange_list', 'rir_id')),
369 ],
370 left_panels=[
371 panels.ASNRangePanel(),
372 TagsPanel(),
373 ],
374 right_panels=[
375 CommentsPanel(),
376 CustomFieldsPanel(),
377 ],
378 )
381@register_model_view(ASNRange, 'asns')
382class ASNRangeASNsView(generic.ObjectChildrenView):
383 queryset = ASNRange.objects.all()
384 child_model = ASN
385 table = tables.ASNTable
386 filterset = filtersets.ASNFilterSet
387 filterset_form = forms.ASNFilterForm
388 tab = ViewTab(
389 label=_('ASNs'),
390 badge=lambda x: x.get_child_asns().count(),
391 permission='ipam.view_asn',
392 weight=500
393 )
395 def get_children(self, request, parent):
396 return parent.get_child_asns().restrict(request.user, 'view').annotate(
397 site_count=count_related(Site, 'asns'),
398 provider_count=count_related(Provider, 'asns')
399 )
402@register_model_view(ASNRange, 'add', detail=False)
403@register_model_view(ASNRange, 'edit')
404class ASNRangeEditView(generic.ObjectEditView):
405 queryset = ASNRange.objects.all()
406 form = forms.ASNRangeForm
409@register_model_view(ASNRange, 'delete')
410class ASNRangeDeleteView(generic.ObjectDeleteView):
411 queryset = ASNRange.objects.all()
414@register_model_view(ASNRange, 'bulk_import', path='import', detail=False)
415class ASNRangeBulkImportView(generic.BulkImportView):
416 queryset = ASNRange.objects.all()
417 model_form = forms.ASNRangeImportForm
420@register_model_view(ASNRange, 'bulk_edit', path='edit', detail=False)
421class ASNRangeBulkEditView(generic.BulkEditView):
422 queryset = ASNRange.objects.annotate_asn_counts()
423 filterset = filtersets.ASNRangeFilterSet
424 table = tables.ASNRangeTable
425 form = forms.ASNRangeBulkEditForm
428@register_model_view(ASNRange, 'bulk_rename', path='rename', detail=False)
429class ASNRangeBulkRenameView(generic.BulkRenameView):
430 queryset = ASNRange.objects.all()
431 filterset = filtersets.ASNRangeFilterSet
434@register_model_view(ASNRange, 'bulk_delete', path='delete', detail=False)
435class ASNRangeBulkDeleteView(generic.BulkDeleteView):
436 queryset = ASNRange.objects.annotate_asn_counts()
437 filterset = filtersets.ASNRangeFilterSet
438 table = tables.ASNRangeTable
441#
442# ASNs
443#
445@register_model_view(ASN, 'list', path='', detail=False)
446class ASNListView(generic.ObjectListView):
447 queryset = ASN.objects.annotate(
448 site_count=count_related(Site, 'asns'),
449 provider_count=count_related(Provider, 'asns')
450 )
451 filterset = filtersets.ASNFilterSet
452 filterset_form = forms.ASNFilterForm
453 table = tables.ASNTable
456@register_model_view(ASN)
457class ASNView(GetRelatedModelsMixin, generic.ObjectView):
458 template_name = 'generic/object.html'
459 queryset = ASN.objects.all()
460 layout = layout.SimpleLayout(
461 breadcrumbs=[
462 Breadcrumb('rir', url=filtered_list_url('ipam:asn_list', 'rir_id')),
463 ],
464 left_panels=[
465 panels.ASNPanel(),
466 TagsPanel(),
467 ],
468 right_panels=[
469 RelatedObjectsPanel(),
470 CustomFieldsPanel(),
471 CommentsPanel(),
472 ],
473 )
475 def get_extra_context(self, request, instance):
476 return {
477 'related_models': self.get_related_models(
478 request,
479 instance,
480 extra=(
481 (Site.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'),
482 (Provider.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'),
483 ),
484 ),
485 }
488@register_model_view(ASN, 'add', detail=False)
489@register_model_view(ASN, 'edit')
490class ASNEditView(generic.ObjectEditView):
491 queryset = ASN.objects.all()
492 form = forms.ASNForm
495@register_model_view(ASN, 'delete')
496class ASNDeleteView(generic.ObjectDeleteView):
497 queryset = ASN.objects.all()
500@register_model_view(ASN, 'bulk_import', path='import', detail=False)
501class ASNBulkImportView(generic.BulkImportView):
502 queryset = ASN.objects.all()
503 model_form = forms.ASNImportForm
506@register_model_view(ASN, 'bulk_edit', path='edit', detail=False)
507class ASNBulkEditView(generic.BulkEditView):
508 queryset = ASN.objects.annotate(
509 site_count=count_related(Site, 'asns')
510 )
511 filterset = filtersets.ASNFilterSet
512 table = tables.ASNTable
513 form = forms.ASNBulkEditForm
516@register_model_view(ASN, 'bulk_rename', path='rename', detail=False)
517class ASNBulkRenameView(generic.BulkRenameView):
518 queryset = ASN.objects.all()
519 filterset = filtersets.ASNFilterSet
522@register_model_view(ASN, 'bulk_delete', path='delete', detail=False)
523class ASNBulkDeleteView(generic.BulkDeleteView):
524 queryset = ASN.objects.annotate(
525 site_count=count_related(Site, 'asns')
526 )
527 filterset = filtersets.ASNFilterSet
528 table = tables.ASNTable
531#
532# Aggregates
533#
535@register_model_view(Aggregate, 'list', path='', detail=False)
536class AggregateListView(generic.ObjectListView):
537 queryset = Aggregate.objects.annotate(
538 child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
539 )
540 filterset = filtersets.AggregateFilterSet
541 filterset_form = forms.AggregateFilterForm
542 table = tables.AggregateTable
543 actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
546@register_model_view(Aggregate)
547class AggregateView(generic.ObjectView):
548 template_name = 'generic/object.html'
549 queryset = Aggregate.objects.all()
550 layout = layout.SimpleLayout(
551 breadcrumbs=[
552 Breadcrumb('rir', url=filtered_list_url('ipam:aggregate_list', 'rir_id')),
553 ],
554 left_panels=[
555 panels.AggregatePanel(),
556 ],
557 right_panels=[
558 CustomFieldsPanel(),
559 TagsPanel(),
560 CommentsPanel(),
561 ],
562 )
565class ChildAvailabilityMixin:
566 """
567 Mixin for ObjectChildrenView subclasses that render synthetic "available" rows
568 (available IP space, prefixes, or VLANs) and must suppress them when the request
569 activates a child object filter, so objects excluded by the filter are not
570 misrepresented as available space.
571 """
573 def _get_detection_filterset(self, request):
574 """
575 Return the bound FilterSet used to evaluate the request, or None if the view declares no
576 filterset. ObjectChildrenView.get() has already built one and validated its form while
577 resolving the child queryset, so reuse it rather than paying for a second FilterSet:
578 get_filters() regenerates every dynamic lookup variant and NetBoxModelFilterSet.__init__
579 queries the custom fields for the model. Build one only for direct calls where get() has
580 not run.
581 """
582 if self.filterset_instance is not None:
583 return self.filterset_instance
584 if self.filterset is None:
585 return None
587 return self.filterset(request.GET, request=request)
589 def _has_active_child_filters(self, request):
590 """
591 Return True if the request supplies a valid, non-empty value for any filter declared by
592 the view's filterset. Saved filters are expanded during filterset instantiation and
593 dynamic custom field filters are registered on the instance, so both are detected.
594 Permission constraints and parent scoping never appear in the request, so they cannot
595 affect the result. The result is memoized because a single request evaluates it from
596 both prep_table_data and get_extra_context.
597 """
598 if hasattr(self, '_active_child_filters'):
599 return self._active_child_filters
601 self._active_child_filters = False
603 # An empty request cannot activate a filter, so skip validating a form for nothing.
604 if not request.GET:
605 return False
607 filterset = self._get_detection_filterset(request)
608 if filterset is None:
609 return False
611 # A non-empty cleaned value means the request activated a declared filter. Emptiness
612 # follows each filter's own semantics, so absent multi-value fields stay inactive.
613 # This is a no-op for a reused FilterSet: .qs validated the form to build the queryset.
614 filterset.form.is_valid()
615 for name, value in filterset.form.cleaned_data.items():
616 if isinstance(filterset.filters[name], django_filters.MultipleChoiceFilter):
617 if value: # mirrors MultipleChoiceFilter.filter()
618 self._active_child_filters = True
619 break
620 elif value not in EMPTY_VALUES: # mirrors Filter.filter()
621 self._active_child_filters = True
622 break
624 return self._active_child_filters
627@register_model_view(Aggregate, 'prefixes')
628class AggregatePrefixesView(ChildAvailabilityMixin, generic.ObjectChildrenView):
629 queryset = Aggregate.objects.all()
630 child_model = Prefix
631 table = tables.PrefixTable
632 filterset = filtersets.PrefixFilterSet
633 filterset_form = forms.PrefixFilterForm
634 template_name = 'ipam/aggregate/prefixes.html'
635 tab = ViewTab(
636 label=_('Prefixes'),
637 badge=lambda x: x.get_child_prefixes().count(),
638 permission='ipam.view_prefix',
639 weight=500
640 )
642 def get_children(self, request, parent):
643 return Prefix.objects.restrict(request.user, 'view').filter(
644 prefix__net_contained_or_equal=str(parent.prefix)
645 ).prefetch_related('scope', 'role', 'tenant', 'tenant__group', 'vlan')
647 def prep_table_data(self, request, queryset, parent):
648 # Determine whether to show assigned prefixes, available prefixes, or both
649 show_available = bool(request.GET.get('show_available', 'true') == 'true')
650 show_assigned = bool(request.GET.get('show_assigned', 'true') == 'true')
652 if show_available and self._has_active_child_filters(request):
653 show_available = False
655 return add_requested_prefixes(parent.prefix, queryset, show_available, show_assigned)
657 def get_extra_context(self, request, instance):
658 show_available = (
659 bool(request.GET.get('show_available', 'true') == 'true') and
660 not self._has_active_child_filters(request)
661 )
663 return {
664 'bulk_querystring': f'within={instance.prefix}',
665 'first_available_prefix': instance.get_first_available_prefix(),
666 'show_available': show_available,
667 'show_assigned': bool(request.GET.get('show_assigned', 'true') == 'true'),
668 }
671@register_model_view(Aggregate, 'add', detail=False)
672@register_model_view(Aggregate, 'edit')
673class AggregateEditView(generic.ObjectEditView):
674 queryset = Aggregate.objects.all()
675 form = forms.AggregateForm
678@register_model_view(Aggregate, 'delete')
679class AggregateDeleteView(generic.ObjectDeleteView):
680 queryset = Aggregate.objects.all()
683@register_model_view(Aggregate, 'bulk_import', path='import', detail=False)
684class AggregateBulkImportView(generic.BulkImportView):
685 queryset = Aggregate.objects.all()
686 model_form = forms.AggregateImportForm
689@register_model_view(Aggregate, 'bulk_edit', path='edit', detail=False)
690class AggregateBulkEditView(generic.BulkEditView):
691 queryset = Aggregate.objects.annotate(
692 child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
693 )
694 filterset = filtersets.AggregateFilterSet
695 table = tables.AggregateTable
696 form = forms.AggregateBulkEditForm
699@register_model_view(Aggregate, 'bulk_delete', path='delete', detail=False)
700class AggregateBulkDeleteView(generic.BulkDeleteView):
701 queryset = Aggregate.objects.annotate(
702 child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
703 )
704 filterset = filtersets.AggregateFilterSet
705 table = tables.AggregateTable
708#
709# Prefix/VLAN roles
710#
712@register_model_view(Role, 'list', path='', detail=False)
713class RoleListView(generic.ObjectListView):
714 queryset = Role.objects.annotate(
715 prefix_count=count_related(Prefix, 'role'),
716 iprange_count=count_related(IPRange, 'role'),
717 vlan_count=count_related(VLAN, 'role'),
718 asn_count=count_related(ASN, 'role')
719 )
720 filterset = filtersets.RoleFilterSet
721 filterset_form = forms.RoleFilterForm
722 table = tables.RoleTable
725@register_model_view(Role)
726class RoleView(GetRelatedModelsMixin, generic.ObjectView):
727 queryset = Role.objects.all()
728 layout = layout.SimpleLayout(
729 left_panels=[
730 panels.RolePanel(),
731 TagsPanel(),
732 ],
733 right_panels=[
734 RelatedObjectsPanel(),
735 CommentsPanel(),
736 CustomFieldsPanel(),
737 ],
738 )
740 def get_extra_context(self, request, instance):
741 return {
742 'related_models': self.get_related_models(request, instance),
743 }
746@register_model_view(Role, 'add', detail=False)
747@register_model_view(Role, 'edit')
748class RoleEditView(generic.ObjectEditView):
749 queryset = Role.objects.all()
750 form = forms.RoleForm
753@register_model_view(Role, 'delete')
754class RoleDeleteView(generic.ObjectDeleteView):
755 queryset = Role.objects.all()
758@register_model_view(Role, 'bulk_import', path='import', detail=False)
759class RoleBulkImportView(generic.BulkImportView):
760 queryset = Role.objects.all()
761 model_form = forms.RoleImportForm
764@register_model_view(Role, 'bulk_edit', path='edit', detail=False)
765class RoleBulkEditView(generic.BulkEditView):
766 queryset = Role.objects.all()
767 filterset = filtersets.RoleFilterSet
768 table = tables.RoleTable
769 form = forms.RoleBulkEditForm
772@register_model_view(Role, 'bulk_rename', path='rename', detail=False)
773class RoleBulkRenameView(generic.BulkRenameView):
774 queryset = Role.objects.all()
775 filterset = filtersets.RoleFilterSet
778@register_model_view(Role, 'bulk_delete', path='delete', detail=False)
779class RoleBulkDeleteView(generic.BulkDeleteView):
780 queryset = Role.objects.all()
781 filterset = filtersets.RoleFilterSet
782 table = tables.RoleTable
785#
786# Prefixes
787#
789@register_model_view(Prefix, 'list', path='', detail=False)
790class PrefixListView(generic.ObjectListView):
791 queryset = Prefix.objects.all()
792 filterset = filtersets.PrefixFilterSet
793 filterset_form = forms.PrefixFilterForm
794 table = tables.PrefixTable
795 template_name = 'ipam/prefix_list.html'
796 actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
799@register_model_view(Prefix)
800class PrefixView(generic.ObjectView):
801 queryset = Prefix.objects.all()
802 layout = layout.SimpleLayout(
803 breadcrumbs=[
804 Breadcrumb('vrf', url=filtered_list_url('ipam:prefix_list', 'vrf_id')),
805 ],
806 left_panels=[
807 panels.PrefixPanel(),
808 ],
809 right_panels=[
810 TemplatePanel('ipam/panels/prefix_addressing.html'),
811 CustomFieldsPanel(),
812 TagsPanel(),
813 CommentsPanel(),
814 ],
815 bottom_panels=[
816 ContextTablePanel('duplicate_prefix_table', title=_('Duplicate prefixes')),
817 ContextTablePanel('parent_prefix_table', title=_('Parent prefixes')),
818 ],
819 )
821 def get_extra_context(self, request, instance):
822 # Parent prefixes table
823 parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
824 Q(vrf=instance.vrf) | Q(vrf__isnull=True, status=PrefixStatusChoices.STATUS_CONTAINER)
825 ).filter(
826 prefix__net_contains=str(instance.prefix)
827 ).prefetch_related(
828 'scope', 'role', 'tenant', 'vlan',
829 )
830 parent_prefix_table = tables.PrefixTable(
831 list(parent_prefixes),
832 exclude=('vrf', 'utilization'),
833 orderable=False
834 )
835 parent_prefix_table.configure(request)
837 # Duplicate prefixes table
838 duplicate_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
839 vrf=instance.vrf, prefix=str(instance.prefix)
840 ).exclude(
841 pk=instance.pk
842 ).prefetch_related(
843 'scope', 'role', 'tenant', 'vlan',
844 )
845 duplicate_prefix_table = tables.PrefixTable(
846 list(duplicate_prefixes),
847 exclude=('vrf', 'utilization'),
848 orderable=False
849 )
850 duplicate_prefix_table.configure(request)
852 context = {
853 'parent_prefix_table': parent_prefix_table,
854 }
855 if duplicate_prefixes.exists():
856 context['duplicate_prefix_table'] = duplicate_prefix_table
857 return context
860@register_model_view(Prefix, 'prefixes')
861class PrefixPrefixesView(ChildAvailabilityMixin, generic.ObjectChildrenView):
862 queryset = Prefix.objects.all()
863 child_model = Prefix
864 table = tables.PrefixTable
865 filterset = filtersets.PrefixFilterSet
866 filterset_form = forms.PrefixFilterForm
867 template_name = 'ipam/prefix/prefixes.html'
868 tab = ViewTab(
869 label=_('Child Prefixes'),
870 badge=lambda x: x.get_child_prefixes().count(),
871 permission='ipam.view_prefix',
872 weight=500
873 )
875 def get_children(self, request, parent):
876 return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
877 'scope', 'vrf', 'vlan', 'role', 'tenant', 'tenant__group'
878 )
880 def prep_table_data(self, request, queryset, parent):
881 # Determine whether to show assigned prefixes, available prefixes, or both
882 show_available = bool(request.GET.get('show_available', 'true') == 'true')
883 show_assigned = bool(request.GET.get('show_assigned', 'true') == 'true')
885 if show_available and self._has_active_child_filters(request):
886 show_available = False
888 return add_requested_prefixes(parent.prefix, queryset, show_available, show_assigned)
890 def get_extra_context(self, request, instance):
891 show_available = (
892 bool(request.GET.get('show_available', 'true') == 'true') and
893 not self._has_active_child_filters(request)
894 )
896 return {
897 'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&within={instance.prefix}",
898 'first_available_prefix': instance.get_first_available_prefix(),
899 'show_available': show_available,
900 'show_assigned': bool(request.GET.get('show_assigned', 'true') == 'true'),
901 }
904@register_model_view(Prefix, 'ipranges', path='ip-ranges')
905class PrefixIPRangesView(generic.ObjectChildrenView):
906 queryset = Prefix.objects.all()
907 child_model = IPRange
908 table = tables.IPRangeTable
909 filterset = filtersets.IPRangeFilterSet
910 filterset_form = forms.IPRangeFilterForm
911 template_name = 'ipam/prefix/ip_ranges.html'
912 tab = ViewTab(
913 label=_('Child Ranges'),
914 badge=lambda x: x.get_child_ranges().count(),
915 permission='ipam.view_iprange',
916 weight=600
917 )
919 def get_children(self, request, parent):
920 return parent.get_child_ranges().restrict(request.user, 'view').prefetch_related(
921 'tenant__group',
922 )
924 def get_extra_context(self, request, instance):
925 return {
926 'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",
927 'first_available_ip': instance.get_first_available_ip(),
928 }
931@register_model_view(Prefix, 'ipaddresses', path='ip-addresses')
932class PrefixIPAddressesView(ChildAvailabilityMixin, generic.ObjectChildrenView):
933 queryset = Prefix.objects.all()
934 child_model = IPAddress
935 table = tables.AnnotatedIPAddressTable
936 filterset = filtersets.IPAddressFilterSet
937 filterset_form = forms.IPAddressFilterForm
938 template_name = 'ipam/prefix/ip_addresses.html'
939 tab = ViewTab(
940 label=_('IP Addresses'),
941 badge=lambda x: x.get_child_ips().count(),
942 permission='ipam.view_ipaddress',
943 weight=700
944 )
946 def get_children(self, request, parent):
947 return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')
949 def prep_table_data(self, request, queryset, parent):
950 # Ordering is checked first: it reads request.GET directly, so a sorted request never
951 # builds the detection filterset.
952 if not get_table_ordering(request, self.table) and not self._has_active_child_filters(request):
953 ip_ranges = parent.get_child_ranges(mark_populated=True).restrict(request.user, 'view')
954 return annotate_ip_space(parent, ip_addresses=queryset, ip_ranges=ip_ranges)
956 return super().prep_table_data(request, queryset, parent)
958 def get_extra_context(self, request, instance):
959 return {
960 'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",
961 'first_available_ip': instance.get_first_available_ip(),
962 }
965@register_model_view(Prefix, 'add', detail=False)
966@register_model_view(Prefix, 'edit')
967class PrefixEditView(generic.ObjectEditView):
968 queryset = Prefix.objects.all()
969 form = forms.PrefixForm
970 template_name = 'ipam/prefix_edit.html'
973@register_model_view(Prefix, 'delete')
974class PrefixDeleteView(generic.ObjectDeleteView):
975 queryset = Prefix.objects.all()
978@register_model_view(Prefix, 'bulk_add', path='bulk-add', detail=False)
979class PrefixBulkCreateView(generic.BulkCreateView):
980 queryset = Prefix.objects.all()
981 form = forms.IPNetworkBulkCreateForm
982 model_form = forms.PrefixBulkAddForm
983 pattern_target = 'prefix'
984 template_name = 'ipam/prefix_bulk_add.html'
987@register_model_view(Prefix, 'bulk_import', path='import', detail=False)
988class PrefixBulkImportView(generic.BulkImportView):
989 queryset = Prefix.objects.all()
990 model_form = forms.PrefixImportForm
993@register_model_view(Prefix, 'bulk_edit', path='edit', detail=False)
994class PrefixBulkEditView(generic.BulkEditView):
995 queryset = Prefix.objects.prefetch_related('vrf__tenant')
996 filterset = filtersets.PrefixFilterSet
997 table = tables.PrefixTable
998 form = forms.PrefixBulkEditForm
1001@register_model_view(Prefix, 'bulk_delete', path='delete', detail=False)
1002class PrefixBulkDeleteView(generic.BulkDeleteView):
1003 queryset = Prefix.objects.prefetch_related('vrf__tenant')
1004 filterset = filtersets.PrefixFilterSet
1005 table = tables.PrefixTable
1008#
1009# IP Ranges
1010#
1012@register_model_view(IPRange, 'list', path='', detail=False)
1013class IPRangeListView(generic.ObjectListView):
1014 queryset = IPRange.objects.all()
1015 filterset = filtersets.IPRangeFilterSet
1016 filterset_form = forms.IPRangeFilterForm
1017 table = tables.IPRangeTable
1020@register_model_view(IPRange)
1021class IPRangeView(generic.ObjectView):
1022 template_name = 'generic/object.html'
1023 queryset = IPRange.objects.all()
1024 layout = layout.SimpleLayout(
1025 breadcrumbs=[
1026 Breadcrumb('vrf', url=filtered_list_url('ipam:iprange_list', 'vrf_id')),
1027 ],
1028 left_panels=[
1029 panels.IPRangePanel(),
1030 ],
1031 right_panels=[
1032 TagsPanel(),
1033 CustomFieldsPanel(),
1034 CommentsPanel(),
1035 ],
1036 bottom_panels=[
1037 ContextTablePanel('parent_prefixes_table', title=_('Parent prefixes')),
1038 ],
1039 )
1041 def get_extra_context(self, request, instance):
1043 # Parent prefixes table
1044 parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
1045 Q(prefix__net_contains_or_equals=str(instance.start_address.ip)),
1046 Q(prefix__net_contains_or_equals=str(instance.end_address.ip)),
1047 vrf=instance.vrf
1048 ).prefetch_related(
1049 'scope', 'role', 'tenant', 'vlan', 'role'
1050 )
1051 parent_prefixes_table = tables.PrefixTable(
1052 list(parent_prefixes),
1053 exclude=('vrf', 'utilization'),
1054 orderable=False
1055 )
1056 parent_prefixes_table.configure(request)
1058 return {
1059 'parent_prefixes_table': parent_prefixes_table,
1060 }
1063@register_model_view(IPRange, 'ipaddresses', path='ip-addresses')
1064class IPRangeIPAddressesView(generic.ObjectChildrenView):
1065 queryset = IPRange.objects.all()
1066 child_model = IPAddress
1067 table = tables.IPAddressTable
1068 filterset = filtersets.IPAddressFilterSet
1069 filterset_form = forms.IPRangeFilterForm
1070 template_name = 'ipam/iprange/ip_addresses.html'
1071 tab = ViewTab(
1072 label=_('IP Addresses'),
1073 badge=lambda x: x.get_child_ips().count(),
1074 permission='ipam.view_ipaddress',
1075 weight=500
1076 )
1078 def get_children(self, request, parent):
1079 return parent.get_child_ips().restrict(request.user, 'view')
1082@register_model_view(IPRange, 'add', detail=False)
1083@register_model_view(IPRange, 'edit')
1084class IPRangeEditView(generic.ObjectEditView):
1085 queryset = IPRange.objects.all()
1086 form = forms.IPRangeForm
1089@register_model_view(IPRange, 'delete')
1090class IPRangeDeleteView(generic.ObjectDeleteView):
1091 queryset = IPRange.objects.all()
1094@register_model_view(IPRange, 'bulk_import', path='import', detail=False)
1095class IPRangeBulkImportView(generic.BulkImportView):
1096 queryset = IPRange.objects.all()
1097 model_form = forms.IPRangeImportForm
1100@register_model_view(IPRange, 'bulk_edit', path='edit', detail=False)
1101class IPRangeBulkEditView(generic.BulkEditView):
1102 queryset = IPRange.objects.all()
1103 filterset = filtersets.IPRangeFilterSet
1104 table = tables.IPRangeTable
1105 form = forms.IPRangeBulkEditForm
1108@register_model_view(IPRange, 'bulk_rename', path='rename', detail=False)
1109class IPRangeBulkRenameView(generic.BulkRenameView):
1110 queryset = IPRange.objects.all()
1111 filterset = filtersets.IPRangeFilterSet
1114@register_model_view(IPRange, 'bulk_delete', path='delete', detail=False)
1115class IPRangeBulkDeleteView(generic.BulkDeleteView):
1116 queryset = IPRange.objects.all()
1117 filterset = filtersets.IPRangeFilterSet
1118 table = tables.IPRangeTable
1121#
1122# IP addresses
1123#
1125@register_model_view(IPAddress, 'list', path='', detail=False)
1126class IPAddressListView(generic.ObjectListView):
1127 queryset = IPAddress.objects.all()
1128 filterset = filtersets.IPAddressFilterSet
1129 filterset_form = forms.IPAddressFilterForm
1130 table = tables.IPAddressTable
1131 actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
1134@register_model_view(IPAddress)
1135class IPAddressView(generic.ObjectView):
1136 template_name = 'generic/object.html'
1137 queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
1138 layout = layout.SimpleLayout(
1139 breadcrumbs=[
1140 Breadcrumb('vrf', url=filtered_list_url('ipam:ipaddress_list', 'vrf_id')),
1141 ],
1142 left_panels=[
1143 panels.IPAddressPanel(),
1144 TagsPanel(),
1145 CustomFieldsPanel(),
1146 CommentsPanel(),
1147 ],
1148 right_panels=[
1149 ContextTablePanel('parent_prefixes_table', title=_('Parent prefixes')),
1150 ContextTablePanel('duplicate_ips_table', title=_('Duplicate IPs')),
1151 ObjectsTablePanel(
1152 'ipam.service',
1153 filters={'ip_address_id': lambda ctx: ctx['object'].pk},
1154 title=_('Application services'),
1155 ),
1156 ],
1157 )
1159 def get_extra_context(self, request, instance):
1160 # Parent prefixes table
1161 parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
1162 vrf=instance.vrf,
1163 prefix__net_contains_or_equals=str(instance.address.ip)
1164 ).prefetch_related(
1165 'scope', 'role'
1166 )
1167 parent_prefixes_table = tables.PrefixTable(
1168 list(parent_prefixes),
1169 exclude=('vrf', 'utilization'),
1170 orderable=False
1171 )
1172 parent_prefixes_table.configure(request)
1174 # Duplicate IPs table
1175 duplicate_ips = IPAddress.objects.restrict(request.user, 'view').filter(
1176 vrf=instance.vrf,
1177 address=str(instance.address)
1178 ).exclude(
1179 pk=instance.pk
1180 ).prefetch_related(
1181 'nat_inside'
1182 )
1183 # Exclude anycast IPs if this IP is anycast
1184 if instance.role == IPAddressRoleChoices.ROLE_ANYCAST:
1185 duplicate_ips = duplicate_ips.exclude(role=IPAddressRoleChoices.ROLE_ANYCAST)
1186 # Limit to a maximum of 10 duplicates displayed here
1187 duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
1188 duplicate_ips_table.configure(request)
1190 context = {
1191 'parent_prefixes_table': parent_prefixes_table,
1192 }
1193 if duplicate_ips.exists():
1194 context['duplicate_ips_table'] = duplicate_ips_table
1195 return context
1198@register_model_view(IPAddress, 'add', detail=False)
1199@register_model_view(IPAddress, 'edit')
1200class IPAddressEditView(generic.ObjectEditView):
1201 queryset = IPAddress.objects.all()
1202 form = forms.IPAddressForm
1203 template_name = 'ipam/ipaddress_edit.html'
1205 def alter_object(self, obj, request, url_args, url_kwargs):
1207 if 'interface' in request.GET:
1208 try:
1209 obj.assigned_object = Interface.objects.get(pk=request.GET['interface'])
1210 except (ValueError, Interface.DoesNotExist):
1211 pass
1213 elif 'vminterface' in request.GET:
1214 try:
1215 obj.assigned_object = VMInterface.objects.get(pk=request.GET['vminterface'])
1216 except (ValueError, VMInterface.DoesNotExist):
1217 pass
1219 elif 'fhrpgroup' in request.GET:
1220 try:
1221 obj.assigned_object = FHRPGroup.objects.get(pk=request.GET['fhrpgroup'])
1222 except (ValueError, FHRPGroup.DoesNotExist):
1223 pass
1225 return obj
1227 def get_extra_addanother_params(self, request):
1228 if 'interface' in request.GET:
1229 return {'interface': request.GET['interface']}
1230 if 'vminterface' in request.GET:
1231 return {'vminterface': request.GET['vminterface']}
1232 return {}
1235# TODO: Standardize or remove this view
1236@register_model_view(IPAddress, 'assign', path='assign', detail=False)
1237class IPAddressAssignView(GetReturnURLMixin, generic.ObjectView):
1238 """
1239 Search for IPAddresses to be assigned to an Interface.
1240 """
1241 queryset = IPAddress.objects.all()
1243 def dispatch(self, request, *args, **kwargs):
1245 # Redirect user if an interface has not been provided
1246 if 'interface' not in request.GET and 'vminterface' not in request.GET:
1247 return redirect('ipam:ipaddress_add')
1249 return super().dispatch(request, *args, **kwargs)
1251 def get(self, request):
1252 form = forms.IPAddressAssignForm()
1254 return render(request, 'ipam/ipaddress_assign.html', {
1255 'form': form,
1256 'return_url': self.get_return_url(request),
1257 })
1259 def post(self, request):
1260 form = forms.IPAddressAssignForm(request.POST)
1261 table = None
1263 if form.is_valid():
1264 addresses = self.queryset.prefetch_related('vrf', 'tenant')
1265 # Limit to 100 results
1266 addresses = filtersets.IPAddressFilterSet(request.POST, addresses).qs[:100]
1267 table = tables.IPAddressAssignTable(addresses)
1268 table.configure(request)
1270 return render(request, 'ipam/ipaddress_assign.html', {
1271 'form': form,
1272 'table': table,
1273 'return_url': self.get_return_url(request),
1274 })
1277@register_model_view(IPAddress, 'delete')
1278class IPAddressDeleteView(generic.ObjectDeleteView):
1279 queryset = IPAddress.objects.all()
1282@register_model_view(IPAddress, 'bulk_add', path='bulk-add', detail=False)
1283class IPAddressBulkCreateView(generic.BulkCreateView):
1284 queryset = IPAddress.objects.all()
1285 form = forms.IPNetworkBulkCreateForm
1286 model_form = forms.IPAddressBulkAddForm
1287 pattern_target = 'address'
1288 template_name = 'ipam/ipaddress_bulk_add.html'
1291@register_model_view(IPAddress, 'bulk_import', path='import', detail=False)
1292class IPAddressBulkImportView(generic.BulkImportView):
1293 queryset = IPAddress.objects.all()
1294 model_form = forms.IPAddressImportForm
1297@register_model_view(IPAddress, 'bulk_edit', path='edit', detail=False)
1298class IPAddressBulkEditView(generic.BulkEditView):
1299 queryset = IPAddress.objects.prefetch_related('vrf__tenant')
1300 filterset = filtersets.IPAddressFilterSet
1301 table = tables.IPAddressTable
1302 form = forms.IPAddressBulkEditForm
1305@register_model_view(IPAddress, 'bulk_delete', path='delete', detail=False)
1306class IPAddressBulkDeleteView(generic.BulkDeleteView):
1307 queryset = IPAddress.objects.prefetch_related('vrf__tenant')
1308 filterset = filtersets.IPAddressFilterSet
1309 table = tables.IPAddressTable
1312@register_model_view(IPAddress, 'related_ips', path='related-ip-addresses')
1313class IPAddressRelatedIPsView(generic.ObjectChildrenView):
1314 queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
1315 child_model = IPAddress
1316 table = tables.IPAddressTable
1317 filterset = filtersets.IPAddressFilterSet
1318 filterset_form = forms.IPAddressFilterForm
1319 tab = ViewTab(
1320 label=_('Related IPs'),
1321 badge=lambda x: x.get_related_ips().count(),
1322 weight=500,
1323 hide_if_empty=True,
1324 )
1326 def get_children(self, request, parent):
1327 return parent.get_related_ips().restrict(request.user, 'view')
1330#
1331# VLAN groups
1332#
1334@register_model_view(VLANGroup, 'list', path='', detail=False)
1335class VLANGroupListView(generic.ObjectListView):
1336 queryset = VLANGroup.objects.annotate_utilization()
1337 filterset = filtersets.VLANGroupFilterSet
1338 filterset_form = forms.VLANGroupFilterForm
1339 table = tables.VLANGroupTable
1342@register_model_view(VLANGroup)
1343class VLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
1344 queryset = VLANGroup.objects.annotate_utilization()
1345 layout = layout.SimpleLayout(
1346 breadcrumbs=[
1347 Breadcrumb('scope'),
1348 ],
1349 left_panels=[
1350 panels.VLANGroupPanel(),
1351 TagsPanel(),
1352 ],
1353 right_panels=[
1354 RelatedObjectsPanel(),
1355 CommentsPanel(),
1356 CustomFieldsPanel(),
1357 ],
1358 )
1360 def get_extra_context(self, request, instance):
1361 return {
1362 'related_models': self.get_related_models(request, instance),
1363 }
1366@register_model_view(VLANGroup, 'add', detail=False)
1367@register_model_view(VLANGroup, 'edit')
1368class VLANGroupEditView(generic.ObjectEditView):
1369 queryset = VLANGroup.objects.all()
1370 form = forms.VLANGroupForm
1373@register_model_view(VLANGroup, 'delete')
1374class VLANGroupDeleteView(generic.ObjectDeleteView):
1375 queryset = VLANGroup.objects.all()
1378@register_model_view(VLANGroup, 'bulk_import', path='import', detail=False)
1379class VLANGroupBulkImportView(generic.BulkImportView):
1380 queryset = VLANGroup.objects.all()
1381 model_form = forms.VLANGroupImportForm
1384@register_model_view(VLANGroup, 'bulk_edit', path='edit', detail=False)
1385class VLANGroupBulkEditView(generic.BulkEditView):
1386 queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
1387 filterset = filtersets.VLANGroupFilterSet
1388 table = tables.VLANGroupTable
1389 form = forms.VLANGroupBulkEditForm
1392@register_model_view(VLANGroup, 'bulk_rename', path='rename', detail=False)
1393class VLANGroupBulkRenameView(generic.BulkRenameView):
1394 queryset = VLANGroup.objects.all()
1395 filterset = filtersets.VLANGroupFilterSet
1398@register_model_view(VLANGroup, 'bulk_delete', path='delete', detail=False)
1399class VLANGroupBulkDeleteView(generic.BulkDeleteView):
1400 queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
1401 filterset = filtersets.VLANGroupFilterSet
1402 table = tables.VLANGroupTable
1405@register_model_view(VLANGroup, 'vlans')
1406class VLANGroupVLANsView(ChildAvailabilityMixin, generic.ObjectChildrenView):
1407 queryset = VLANGroup.objects.all()
1408 child_model = VLAN
1409 table = tables.VLANTable
1410 filterset = filtersets.VLANFilterSet
1411 filterset_form = forms.VLANFilterForm
1412 tab = ViewTab(
1413 label=_('VLANs'),
1414 badge=lambda x: x.get_child_vlans().count(),
1415 permission='ipam.view_vlan',
1416 weight=500
1417 )
1419 def get_children(self, request, parent):
1420 return parent.get_child_vlans().restrict(request.user, 'view').prefetch_related(
1421 Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)),
1422 'tenant', 'site', 'role',
1423 )
1425 def prep_table_data(self, request, queryset, parent):
1426 # Skip synthetic available rows under active filters: filtered-out VLANs would otherwise look available.
1427 if not get_table_ordering(request, self.table) and not self._has_active_child_filters(request):
1428 return add_available_vlans(queryset, parent)
1430 return super().prep_table_data(request, queryset, parent)
1433#
1434# VLAN Translation Policies
1435#
1437@register_model_view(VLANTranslationPolicy, 'list', path='', detail=False)
1438class VLANTranslationPolicyListView(generic.ObjectListView):
1439 queryset = VLANTranslationPolicy.objects.annotate(
1440 rule_count=count_related(VLANTranslationRule, 'policy'),
1441 )
1442 filterset = filtersets.VLANTranslationPolicyFilterSet
1443 filterset_form = forms.VLANTranslationPolicyFilterForm
1444 table = tables.VLANTranslationPolicyTable
1447@register_model_view(VLANTranslationPolicy)
1448class VLANTranslationPolicyView(generic.ObjectView):
1449 queryset = VLANTranslationPolicy.objects.all()
1450 template_name = 'generic/object.html'
1451 layout = layout.SimpleLayout(
1452 left_panels=[
1453 panels.VLANTranslationPolicyPanel(),
1454 ],
1455 right_panels=[
1456 TagsPanel(),
1457 CustomFieldsPanel(),
1458 CommentsPanel(),
1459 ],
1460 bottom_panels=[
1461 ObjectsTablePanel(
1462 'ipam.vlantranslationrule',
1463 filters={'policy_id': lambda ctx: ctx['object'].pk},
1464 title=_('VLAN translation rules'),
1465 exclude_columns=['policy'],
1466 actions=[
1467 actions.AddObject(
1468 'ipam.vlantranslationrule',
1469 url_params={'policy': lambda ctx: ctx['object'].pk},
1470 label=_('Add Rule'),
1471 ),
1472 ],
1473 ),
1474 ],
1475 )
1478@register_model_view(VLANTranslationPolicy, 'add', detail=False)
1479@register_model_view(VLANTranslationPolicy, 'edit')
1480class VLANTranslationPolicyEditView(generic.ObjectEditView):
1481 queryset = VLANTranslationPolicy.objects.all()
1482 form = forms.VLANTranslationPolicyForm
1485@register_model_view(VLANTranslationPolicy, 'delete')
1486class VLANTranslationPolicyDeleteView(generic.ObjectDeleteView):
1487 queryset = VLANTranslationPolicy.objects.all()
1490@register_model_view(VLANTranslationPolicy, 'bulk_import', path='import', detail=False)
1491class VLANTranslationPolicyBulkImportView(generic.BulkImportView):
1492 queryset = VLANTranslationPolicy.objects.all()
1493 model_form = forms.VLANTranslationPolicyImportForm
1496@register_model_view(VLANTranslationPolicy, 'bulk_edit', path='edit', detail=False)
1497class VLANTranslationPolicyBulkEditView(generic.BulkEditView):
1498 queryset = VLANTranslationPolicy.objects.all()
1499 filterset = filtersets.VLANTranslationPolicyFilterSet
1500 table = tables.VLANTranslationPolicyTable
1501 form = forms.VLANTranslationPolicyBulkEditForm
1504@register_model_view(VLANTranslationPolicy, 'bulk_rename', path='rename', detail=False)
1505class VLANTranslationPolicyBulkRenameView(generic.BulkRenameView):
1506 queryset = VLANTranslationPolicy.objects.all()
1507 filterset = filtersets.VLANTranslationPolicyFilterSet
1510@register_model_view(VLANTranslationPolicy, 'bulk_delete', path='delete', detail=False)
1511class VLANTranslationPolicyBulkDeleteView(generic.BulkDeleteView):
1512 queryset = VLANTranslationPolicy.objects.all()
1513 filterset = filtersets.VLANTranslationPolicyFilterSet
1514 table = tables.VLANTranslationPolicyTable
1517#
1518# VLAN Translation Rules
1519#
1521@register_model_view(VLANTranslationRule, 'list', path='', detail=False)
1522class VLANTranslationRuleListView(generic.ObjectListView):
1523 queryset = VLANTranslationRule.objects.all()
1524 filterset = filtersets.VLANTranslationRuleFilterSet
1525 filterset_form = forms.VLANTranslationRuleFilterForm
1526 table = tables.VLANTranslationRuleTable
1527 actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
1530@register_model_view(VLANTranslationRule)
1531class VLANTranslationRuleView(generic.ObjectView):
1532 queryset = VLANTranslationRule.objects.all()
1533 template_name = 'generic/object.html'
1534 layout = layout.SimpleLayout(
1535 left_panels=[
1536 panels.VLANTranslationRulePanel(),
1537 ],
1538 right_panels=[
1539 TagsPanel(),
1540 CustomFieldsPanel(),
1541 ],
1542 )
1545@register_model_view(VLANTranslationRule, 'add', detail=False)
1546@register_model_view(VLANTranslationRule, 'edit')
1547class VLANTranslationRuleEditView(generic.ObjectEditView):
1548 queryset = VLANTranslationRule.objects.all()
1549 form = forms.VLANTranslationRuleForm
1552@register_model_view(VLANTranslationRule, 'delete')
1553class VLANTranslationRuleDeleteView(generic.ObjectDeleteView):
1554 queryset = VLANTranslationRule.objects.all()
1557@register_model_view(VLANTranslationRule, 'bulk_import', path='import', detail=False)
1558class VLANTranslationRuleBulkImportView(generic.BulkImportView):
1559 queryset = VLANTranslationRule.objects.all()
1560 model_form = forms.VLANTranslationRuleImportForm
1563@register_model_view(VLANTranslationRule, 'bulk_edit', path='edit', detail=False)
1564class VLANTranslationRuleBulkEditView(generic.BulkEditView):
1565 queryset = VLANTranslationRule.objects.all()
1566 filterset = filtersets.VLANTranslationRuleFilterSet
1567 table = tables.VLANTranslationRuleTable
1568 form = forms.VLANTranslationRuleBulkEditForm
1571@register_model_view(VLANTranslationRule, 'bulk_delete', path='delete', detail=False)
1572class VLANTranslationRuleBulkDeleteView(generic.BulkDeleteView):
1573 queryset = VLANTranslationRule.objects.all()
1574 filterset = filtersets.VLANTranslationRuleFilterSet
1575 table = tables.VLANTranslationRuleTable
1578#
1579# FHRP groups
1580#
1582@register_model_view(FHRPGroup, 'list', path='', detail=False)
1583class FHRPGroupListView(generic.ObjectListView):
1584 queryset = FHRPGroup.objects.annotate(
1585 member_count=count_related(FHRPGroupAssignment, 'group')
1586 )
1587 filterset = filtersets.FHRPGroupFilterSet
1588 filterset_form = forms.FHRPGroupFilterForm
1589 table = tables.FHRPGroupTable
1592@register_model_view(FHRPGroup)
1593class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
1594 queryset = FHRPGroup.objects.annotate(
1595 member_count=count_related(FHRPGroupAssignment, 'group')
1596 )
1597 layout = layout.SimpleLayout(
1598 left_panels=[
1599 panels.FHRPGroupPanel(),
1600 TagsPanel(),
1601 CommentsPanel(),
1602 ],
1603 right_panels=[
1604 panels.FHRPGroupAuthPanel(),
1605 RelatedObjectsPanel(),
1606 CustomFieldsPanel(),
1607 ],
1608 bottom_panels=[
1609 ObjectsTablePanel(
1610 'ipam.ipaddress',
1611 filters={'fhrpgroup_id': lambda ctx: ctx['object'].pk},
1612 title=_('Virtual IP addresses'),
1613 actions=[
1614 actions.AddObject(
1615 'ipam.ipaddress',
1616 url_params={'fhrpgroup': lambda ctx: ctx['object'].pk},
1617 label=_('Add IP Address'),
1618 ),
1619 ],
1620 ),
1621 ContextTablePanel('members_table', title=_('Members')),
1622 ],
1623 )
1625 def get_extra_context(self, request, instance):
1626 # Get assigned interfaces
1627 members_table = tables.FHRPGroupAssignmentTable(
1628 data=FHRPGroupAssignment.objects.restrict(request.user, 'view').filter(group=instance),
1629 orderable=False
1630 )
1631 members_table.configure(request)
1632 members_table.columns.hide('group')
1634 return {
1635 'related_models': self.get_related_models(
1636 request, instance,
1637 extra=(
1638 (
1639 Service.objects.restrict(request.user, 'view').filter(
1640 parent_object_type=ContentType.objects.get_for_model(FHRPGroup),
1641 parent_object_id=instance.id,
1642 ),
1643 'fhrpgroup_id'
1644 ),
1645 ),
1646 ),
1647 'members_table': members_table,
1648 }
1651@register_model_view(FHRPGroup, 'add', detail=False)
1652@register_model_view(FHRPGroup, 'edit')
1653class FHRPGroupEditView(generic.ObjectEditView):
1654 queryset = FHRPGroup.objects.all()
1655 form = forms.FHRPGroupForm
1657 def get_return_url(self, request, obj=None):
1658 return_url = super().get_return_url(request, obj)
1660 # If we're redirecting the user to the FHRPGroupAssignment creation form,
1661 # initialize the group field with the FHRPGroup we just saved.
1662 if return_url.startswith(reverse('ipam:fhrpgroupassignment_add')):
1663 return_url += f'&group={obj.pk}'
1665 return return_url
1667 def alter_object(self, obj, request, url_args, url_kwargs):
1668 # Workaround to solve #10719. Capture the current user on the FHRPGroup instance so that
1669 # we can evaluate permissions during the creation of a new IPAddress within the form.
1670 obj._user = request.user
1671 return obj
1674@register_model_view(FHRPGroup, 'delete')
1675class FHRPGroupDeleteView(generic.ObjectDeleteView):
1676 queryset = FHRPGroup.objects.all()
1679@register_model_view(FHRPGroup, 'bulk_import', path='import', detail=False)
1680class FHRPGroupBulkImportView(generic.BulkImportView):
1681 queryset = FHRPGroup.objects.all()
1682 model_form = forms.FHRPGroupImportForm
1685@register_model_view(FHRPGroup, 'bulk_edit', path='edit', detail=False)
1686class FHRPGroupBulkEditView(generic.BulkEditView):
1687 queryset = FHRPGroup.objects.all()
1688 filterset = filtersets.FHRPGroupFilterSet
1689 table = tables.FHRPGroupTable
1690 form = forms.FHRPGroupBulkEditForm
1693@register_model_view(FHRPGroup, 'bulk_rename', path='rename', detail=False)
1694class FHRPGroupBulkRenameView(generic.BulkRenameView):
1695 queryset = FHRPGroup.objects.all()
1696 filterset = filtersets.FHRPGroupFilterSet
1699@register_model_view(FHRPGroup, 'bulk_delete', path='delete', detail=False)
1700class FHRPGroupBulkDeleteView(generic.BulkDeleteView):
1701 queryset = FHRPGroup.objects.all()
1702 filterset = filtersets.FHRPGroupFilterSet
1703 table = tables.FHRPGroupTable
1706#
1707# FHRP group assignments
1708#
1710@register_model_view(FHRPGroupAssignment, 'add', detail=False)
1711@register_model_view(FHRPGroupAssignment, 'edit')
1712class FHRPGroupAssignmentEditView(generic.ObjectEditView):
1713 queryset = FHRPGroupAssignment.objects.all()
1714 form = forms.FHRPGroupAssignmentForm
1716 def alter_object(self, instance, request, args, kwargs):
1717 if not instance.pk:
1718 # Assign the interface based on URL kwargs
1719 content_type = get_object_or_404(ContentType, pk=request.GET.get('interface_type'))
1720 instance.interface = get_object_or_404(content_type.model_class(), pk=request.GET.get('interface_id'))
1721 return instance
1723 def get_extra_addanother_params(self, request):
1724 return {
1725 'interface_type': request.GET.get('interface_type'),
1726 'interface_id': request.GET.get('interface_id'),
1727 }
1730@register_model_view(FHRPGroupAssignment, 'delete')
1731class FHRPGroupAssignmentDeleteView(generic.ObjectDeleteView):
1732 queryset = FHRPGroupAssignment.objects.all()
1735#
1736# VLANs
1737#
1739@register_model_view(VLAN, 'list', path='', detail=False)
1740class VLANListView(generic.ObjectListView):
1741 queryset = VLAN.objects.all()
1742 filterset = filtersets.VLANFilterSet
1743 filterset_form = forms.VLANFilterForm
1744 table = tables.VLANTable
1747@register_model_view(VLAN)
1748class VLANView(generic.ObjectView):
1749 queryset = VLAN.objects.all()
1750 template_name = 'generic/object.html'
1751 layout = layout.SimpleLayout(
1752 breadcrumbs=[
1753 Breadcrumb('site', url=filtered_list_url('ipam:vlan_list', 'site_id')),
1754 Breadcrumb('group', url=filtered_list_url('ipam:vlan_list', 'group_id')),
1755 ],
1756 left_panels=[
1757 panels.VLANPanel(),
1758 ],
1759 right_panels=[
1760 CustomFieldsPanel(),
1761 TagsPanel(),
1762 CommentsPanel(),
1763 ],
1764 bottom_panels=[
1765 ObjectsTablePanel(
1766 'ipam.prefix',
1767 filters={'vlan_id': lambda ctx: ctx['object'].pk},
1768 title=_('Prefixes'),
1769 exclude_columns=['vlan'],
1770 actions=[
1771 actions.AddObject(
1772 'ipam.prefix',
1773 url_params={
1774 'tenant': lambda ctx: ctx['object'].tenant_id,
1775 'scope_content_type': lambda ctx: (
1776 ContentType.objects.get_for_model(Site).pk if ctx['object'].site_id else None
1777 ),
1778 'scope_object_id': lambda ctx: ctx['object'].site_id,
1779 'vlan': lambda ctx: ctx['object'].pk,
1780 },
1781 label=_('Add a Prefix'),
1782 ),
1783 ],
1784 ),
1785 panels.VLANCustomerVLANsPanel(),
1786 ],
1787 )
1790@register_model_view(VLAN, 'interfaces')
1791class VLANInterfacesView(generic.ObjectChildrenView):
1792 queryset = VLAN.objects.all()
1793 child_model = Interface
1794 table = tables.VLANDevicesTable
1795 filterset = InterfaceFilterSet
1796 filterset_form = InterfaceFilterForm
1797 tab = ViewTab(
1798 label=_('Device Interfaces'),
1799 badge=lambda x: x.get_interfaces().count(),
1800 permission='dcim.view_interface',
1801 weight=500
1802 )
1804 def get_children(self, request, parent):
1805 return parent.get_interfaces().restrict(request.user, 'view')
1808@register_model_view(VLAN, 'vminterfaces', path='vm-interfaces')
1809class VLANVMInterfacesView(generic.ObjectChildrenView):
1810 queryset = VLAN.objects.all()
1811 child_model = VMInterface
1812 table = tables.VLANVirtualMachinesTable
1813 filterset = VMInterfaceFilterSet
1814 filterset_form = VMInterfaceFilterForm
1815 tab = ViewTab(
1816 label=_('VM Interfaces'),
1817 badge=lambda x: x.get_vminterfaces().count(),
1818 permission='virtualization.view_vminterface',
1819 weight=510
1820 )
1822 def get_children(self, request, parent):
1823 return parent.get_vminterfaces().restrict(request.user, 'view')
1826@register_model_view(VLAN, 'add', detail=False)
1827@register_model_view(VLAN, 'edit')
1828class VLANEditView(generic.ObjectEditView):
1829 queryset = VLAN.objects.all()
1830 form = forms.VLANForm
1831 template_name = 'ipam/vlan_edit.html'
1834@register_model_view(VLAN, 'delete')
1835class VLANDeleteView(generic.ObjectDeleteView):
1836 queryset = VLAN.objects.all()
1839@register_model_view(VLAN, 'bulk_add', path='bulk-add', detail=False)
1840class VLANBulkCreateView(generic.BulkCreateView):
1841 queryset = VLAN.objects.all()
1842 form = forms.VLANIDBulkCreateForm
1843 model_form = forms.VLANBulkAddForm
1844 pattern_target = 'vid'
1845 pattern_template_fields = ('name',)
1846 template_name = 'ipam/vlan_bulk_add.html'
1849@register_model_view(VLAN, 'bulk_import', path='import', detail=False)
1850class VLANBulkImportView(generic.BulkImportView):
1851 queryset = VLAN.objects.all()
1852 model_form = forms.VLANImportForm
1855@register_model_view(VLAN, 'bulk_edit', path='edit', detail=False)
1856class VLANBulkEditView(generic.BulkEditView):
1857 queryset = VLAN.objects.all()
1858 filterset = filtersets.VLANFilterSet
1859 table = tables.VLANTable
1860 form = forms.VLANBulkEditForm
1863@register_model_view(VLAN, 'bulk_rename', path='rename', detail=False)
1864class VLANBulkRenameView(generic.BulkRenameView):
1865 queryset = VLAN.objects.all()
1866 filterset = filtersets.VLANFilterSet
1869@register_model_view(VLAN, 'bulk_delete', path='delete', detail=False)
1870class VLANBulkDeleteView(generic.BulkDeleteView):
1871 queryset = VLAN.objects.all()
1872 filterset = filtersets.VLANFilterSet
1873 table = tables.VLANTable
1876#
1877# Service templates
1878#
1880@register_model_view(ServiceTemplate, 'list', path='', detail=False)
1881class ServiceTemplateListView(generic.ObjectListView):
1882 queryset = ServiceTemplate.objects.all()
1883 filterset = filtersets.ServiceTemplateFilterSet
1884 filterset_form = forms.ServiceTemplateFilterForm
1885 table = tables.ServiceTemplateTable
1888@register_model_view(ServiceTemplate)
1889class ServiceTemplateView(generic.ObjectView):
1890 queryset = ServiceTemplate.objects.all()
1891 template_name = 'generic/object.html'
1892 layout = layout.SimpleLayout(
1893 left_panels=[
1894 panels.ServiceTemplatePanel(),
1895 ],
1896 right_panels=[
1897 CustomFieldsPanel(),
1898 TagsPanel(),
1899 CommentsPanel(),
1900 ],
1901 )
1904@register_model_view(ServiceTemplate, 'add', detail=False)
1905@register_model_view(ServiceTemplate, 'edit')
1906class ServiceTemplateEditView(generic.ObjectEditView):
1907 queryset = ServiceTemplate.objects.all()
1908 form = forms.ServiceTemplateForm
1911@register_model_view(ServiceTemplate, 'delete')
1912class ServiceTemplateDeleteView(generic.ObjectDeleteView):
1913 queryset = ServiceTemplate.objects.all()
1916@register_model_view(ServiceTemplate, 'bulk_import', path='import', detail=False)
1917class ServiceTemplateBulkImportView(generic.BulkImportView):
1918 queryset = ServiceTemplate.objects.all()
1919 model_form = forms.ServiceTemplateImportForm
1922class ServicePortMappingsBulkEditMixin:
1923 """
1924 Fold the ``add_port_mappings`` / ``remove_port_mappings`` bulk-edit deltas into each object before it
1925 is validated and saved, keeping the model unaware of the bulk-edit form's fields.
1926 """
1927 def pre_save_operations(self, form, obj):
1928 super().pre_save_operations(form, obj)
1929 add = form.cleaned_data.get('add_port_mappings')
1930 remove = form.cleaned_data.get('remove_port_mappings')
1931 if add:
1932 obj._add_port_mappings(add)
1933 if remove:
1934 obj._remove_port_mappings(remove)
1937@register_model_view(ServiceTemplate, 'bulk_edit', path='edit', detail=False)
1938class ServiceTemplateBulkEditView(ServicePortMappingsBulkEditMixin, generic.BulkEditView):
1939 queryset = ServiceTemplate.objects.all()
1940 filterset = filtersets.ServiceTemplateFilterSet
1941 table = tables.ServiceTemplateTable
1942 form = forms.ServiceTemplateBulkEditForm
1945@register_model_view(ServiceTemplate, 'bulk_rename', path='rename', detail=False)
1946class ServiceTemplateBulkRenameView(generic.BulkRenameView):
1947 queryset = ServiceTemplate.objects.all()
1948 filterset = filtersets.ServiceTemplateFilterSet
1951@register_model_view(ServiceTemplate, 'bulk_delete', path='delete', detail=False)
1952class ServiceTemplateBulkDeleteView(generic.BulkDeleteView):
1953 queryset = ServiceTemplate.objects.all()
1954 filterset = filtersets.ServiceTemplateFilterSet
1955 table = tables.ServiceTemplateTable
1958#
1959# Services
1960#
1962@register_model_view(Service, 'list', path='', detail=False)
1963class ServiceListView(generic.ObjectListView):
1964 queryset = Service.objects.prefetch_related('parent')
1965 filterset = filtersets.ServiceFilterSet
1966 filterset_form = forms.ServiceFilterForm
1967 table = tables.ServiceTable
1970@register_model_view(Service)
1971class ServiceView(generic.ObjectView):
1972 queryset = Service.objects.all()
1973 layout = layout.SimpleLayout(
1974 left_panels=[
1975 panels.ServicePanel(),
1976 ],
1977 right_panels=[
1978 CustomFieldsPanel(),
1979 TagsPanel(),
1980 CommentsPanel(),
1981 ],
1982 )
1984 def get_extra_context(self, request, instance):
1985 context = {}
1986 match instance.parent:
1987 case Device():
1988 context['breadcrumb_queryparam'] = 'device_id'
1989 case VirtualMachine():
1990 context['breadcrumb_queryparam'] = 'virtual_machine_id'
1991 case FHRPGroup():
1992 context['breadcrumb_queryparam'] = 'fhrpgroup_id'
1994 return context
1997@register_model_view(Service, 'add', detail=False)
1998class ServiceCreateView(generic.ObjectEditView):
1999 queryset = Service.objects.all()
2000 form = forms.ServiceCreateForm
2003@register_model_view(Service, 'edit')
2004class ServiceEditView(generic.ObjectEditView):
2005 queryset = Service.objects.all()
2006 form = forms.ServiceForm
2009@register_model_view(Service, 'delete')
2010class ServiceDeleteView(generic.ObjectDeleteView):
2011 queryset = Service.objects.all()
2014@register_model_view(Service, 'bulk_import', path='import', detail=False)
2015class ServiceBulkImportView(generic.BulkImportView):
2016 queryset = Service.objects.all()
2017 model_form = forms.ServiceImportForm
2020@register_model_view(Service, 'bulk_edit', path='edit', detail=False)
2021class ServiceBulkEditView(ServicePortMappingsBulkEditMixin, generic.BulkEditView):
2022 queryset = Service.objects.prefetch_related('parent')
2023 filterset = filtersets.ServiceFilterSet
2024 table = tables.ServiceTable
2025 form = forms.ServiceBulkEditForm
2028@register_model_view(Service, 'bulk_rename', path='rename', detail=False)
2029class ServiceBulkRenameView(generic.BulkRenameView):
2030 queryset = Service.objects.all()
2031 filterset = filtersets.ServiceFilterSet
2034@register_model_view(Service, 'bulk_delete', path='delete', detail=False)
2035class ServiceBulkDeleteView(generic.BulkDeleteView):
2036 queryset = Service.objects.prefetch_related('parent')
2037 filterset = filtersets.ServiceFilterSet
2038 table = tables.ServiceTable