diff --git a/netbox/ipam/admin.py b/netbox/ipam/admin.py index 5ab79b92d..56113a44e 100644 --- a/netbox/ipam/admin.py +++ b/netbox/ipam/admin.py @@ -40,7 +40,7 @@ class AggregateAdmin(admin.ModelAdmin): @admin.register(Prefix) class PrefixAdmin(admin.ModelAdmin): - list_display = ['prefix', 'vrf', 'site', 'status', 'role', 'vlan'] + list_display = ['prefix', 'vrf', 'tenant', 'site', 'status', 'role', 'vlan'] list_filter = ['family', 'site', 'status', 'role'] search_fields = ['prefix'] @@ -51,7 +51,7 @@ class PrefixAdmin(admin.ModelAdmin): @admin.register(IPAddress) class IPAddressAdmin(admin.ModelAdmin): - list_display = ['address', 'vrf', 'nat_inside'] + list_display = ['address', 'vrf', 'tenant', 'nat_inside'] list_filter = ['family'] fields = ['address', 'vrf', 'device', 'interface', 'nat_inside'] readonly_fields = ['interface', 'device', 'nat_inside'] diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index bdcab381c..a24e1454c 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -23,6 +23,15 @@ class VRFNestedSerializer(VRFSerializer): fields = ['id', 'name', 'rd'] +class VRFTenantSerializer(VRFSerializer): + """ + Include tenant serializer. Useful for determining tenant inheritance for Prefixes and IPAddresses. + """ + + class Meta(VRFSerializer.Meta): + fields = ['id', 'name', 'rd', 'tenant'] + + # # Roles # @@ -120,13 +129,14 @@ class VLANNestedSerializer(VLANSerializer): class PrefixSerializer(serializers.ModelSerializer): site = SiteNestedSerializer() - vrf = VRFNestedSerializer() + vrf = VRFTenantSerializer() + tenant = TenantNestedSerializer() vlan = VLANNestedSerializer() role = RoleNestedSerializer() class Meta: model = Prefix - fields = ['id', 'family', 'prefix', 'site', 'vrf', 'vlan', 'status', 'role', 'description'] + fields = ['id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description'] class PrefixNestedSerializer(PrefixSerializer): @@ -140,12 +150,13 @@ class PrefixNestedSerializer(PrefixSerializer): # class IPAddressSerializer(serializers.ModelSerializer): - vrf = VRFNestedSerializer() + vrf = VRFTenantSerializer() + tenant = TenantNestedSerializer() interface = InterfaceNestedSerializer() class Meta: model = IPAddress - fields = ['id', 'family', 'address', 'vrf', 'interface', 'description', 'nat_inside', 'nat_outside'] + fields = ['id', 'family', 'address', 'vrf', 'tenant', 'interface', 'description', 'nat_inside', 'nat_outside'] class IPAddressNestedSerializer(IPAddressSerializer): diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 36bf1158d..78d2b7f8e 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -96,7 +96,7 @@ class PrefixListView(generics.ListAPIView): """ List prefixes (filterable) """ - queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role') + queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') serializer_class = serializers.PrefixSerializer filter_class = filters.PrefixFilter @@ -105,7 +105,7 @@ class PrefixDetailView(generics.RetrieveAPIView): """ Retrieve a single prefix """ - queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role') + queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') serializer_class = serializers.PrefixSerializer @@ -117,7 +117,7 @@ class IPAddressListView(generics.ListAPIView): """ List IP addresses (filterable) """ - queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\ + queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\ .prefetch_related('nat_outside') serializer_class = serializers.IPAddressSerializer filter_class = filters.IPAddressFilter @@ -127,7 +127,7 @@ class IPAddressDetailView(generics.RetrieveAPIView): """ Retrieve a single IP address """ - queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\ + queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\ .prefetch_related('nat_outside') serializer_class = serializers.IPAddressSerializer diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index badd9a08f..70a382856 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -2,6 +2,8 @@ import django_filters from netaddr import IPNetwork from netaddr.core import AddrFormatError +from django.db.models import Q + from dcim.models import Site, Device, Interface from tenancy.models import Tenant @@ -67,6 +69,14 @@ class PrefixFilter(django_filters.FilterSet): action='_vrf', label='VRF', ) + tenant_id = django_filters.MethodFilter( + action='_tenant_id', + label='Tenant (ID)', + ) + tenant = django_filters.MethodFilter( + action='_tenant', + label='Tenant', + ) site_id = django_filters.ModelMultipleChoiceFilter( name='site', queryset=Site.objects.all(), @@ -132,6 +142,24 @@ class PrefixFilter(django_filters.FilterSet): return queryset.filter(vrf__isnull=True) return queryset.filter(vrf__pk=value) + def _tenant(self, queryset, value): + if str(value) == '': + return queryset + return queryset.filter( + Q(tenant__slug=value) | + Q(tenant__isnull=True, vrf__tenant__slug=value) + ) + + def _tenant_id(self, queryset, value): + try: + value = int(value) + except ValueError: + return queryset.none() + return queryset.filter( + Q(tenant__pk=value) | + Q(tenant__isnull=True, vrf__tenant__pk=value) + ) + class IPAddressFilter(django_filters.FilterSet): q = django_filters.MethodFilter( @@ -147,6 +175,14 @@ class IPAddressFilter(django_filters.FilterSet): action='_vrf', label='VRF', ) + tenant_id = django_filters.MethodFilter( + action='_tenant_id', + label='Tenant (ID)', + ) + tenant = django_filters.MethodFilter( + action='_tenant', + label='Tenant', + ) device_id = django_filters.ModelMultipleChoiceFilter( name='interface__device', queryset=Device.objects.all(), @@ -187,6 +223,24 @@ class IPAddressFilter(django_filters.FilterSet): return queryset.filter(vrf__isnull=True) return queryset.filter(vrf__pk=value) + def _tenant(self, queryset, value): + if str(value) == '': + return queryset + return queryset.filter( + Q(tenant__slug=value) | + Q(tenant__isnull=True, vrf__tenant__slug=value) + ) + + def _tenant_id(self, queryset, value): + try: + value = int(value) + except ValueError: + return queryset.none() + return queryset.filter( + Q(tenant__pk=value) | + Q(tenant__isnull=True, vrf__tenant__pk=value) + ) + class VLANGroupFilter(django_filters.FilterSet): site_id = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 7905e3721..2a2c5413b 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -16,6 +16,24 @@ FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES +def bulkedit_vrf_choices(): + choices = [ + (None, '---------'), + (0, 'Global'), + ] + choices += [(v.pk, v.name) for v in VRF.objects.all()] + return choices + + +def bulkedit_tenant_choices(): + choices = [ + (None, '---------'), + (0, 'None'), + ] + choices += [(t.pk, t.name) for t in Tenant.objects.all()] + return choices + + # # VRFs # @@ -48,7 +66,7 @@ class VRFImportForm(BulkImportForm, BootstrapMixin): class VRFBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput) - tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) + tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') description = forms.CharField(max_length=100, required=False) @@ -145,7 +163,7 @@ class PrefixForm(forms.ModelForm, BootstrapMixin): class Meta: model = Prefix - fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'description'] + fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'description'] help_texts = { 'prefix': "IPv4 or IPv6 network", 'vrf': "VRF (if applicable)", @@ -186,6 +204,8 @@ class PrefixForm(forms.ModelForm, BootstrapMixin): class PrefixFromCSVForm(forms.ModelForm): vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd', error_messages={'invalid_choice': 'VRF not found.'}) + tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False, + error_messages={'invalid_choice': 'Tenant not found.'}) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name', error_messages={'invalid_choice': 'Site not found.'}) vlan_group_name = forms.CharField(required=False) @@ -196,7 +216,8 @@ class PrefixFromCSVForm(forms.ModelForm): class Meta: model = Prefix - fields = ['prefix', 'vrf', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', 'description'] + fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', + 'description'] def clean(self): @@ -239,24 +260,21 @@ class PrefixImportForm(BulkImportForm, BootstrapMixin): csv = CSVDataField(csv_form=PrefixFromCSVForm) -def prefix_vrf_choices(): - choices = [ - (None, '---------'), - (0, 'Global'), - ] - choices += [(v.pk, v.name) for v in VRF.objects.all()] - return choices - - class PrefixBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False) - vrf = forms.TypedChoiceField(choices=prefix_vrf_choices, coerce=int, required=False, label='VRF') + vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF') + tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False) role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False) description = forms.CharField(max_length=100, required=False) +def prefix_vrf_choices(): + vrf_choices = VRF.objects.annotate(prefix_count=Count('prefixes')) + return [(v.pk, u'{} ({})'.format(v.name, v.prefix_count)) for v in vrf_choices] + + def prefix_site_choices(): site_choices = Site.objects.annotate(prefix_count=Count('prefixes')) return [(s.slug, u'{} ({})'.format(s.name, s.prefix_count)) for s in site_choices] @@ -276,12 +294,16 @@ def prefix_role_choices(): class PrefixFilterForm(forms.Form, BootstrapMixin): parent = forms.CharField(required=False, label='Search Within') - vrf = forms.ChoiceField(required=False, choices=prefix_vrf_choices, label='VRF') - status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices) + vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF', + widget=forms.SelectMultiple(attrs={'size': 6})) + tenant = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), required=False, + widget=forms.SelectMultiple(attrs={'size': 6})) + status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices, + widget=forms.SelectMultiple(attrs={'size': 6})) site = forms.MultipleChoiceField(required=False, choices=prefix_site_choices, - widget=forms.SelectMultiple(attrs={'size': 8})) + widget=forms.SelectMultiple(attrs={'size': 6})) role = forms.MultipleChoiceField(required=False, choices=prefix_role_choices, - widget=forms.SelectMultiple(attrs={'size': 8})) + widget=forms.SelectMultiple(attrs={'size': 6})) expand = forms.BooleanField(required=False, label='Expand prefix hierarchy') @@ -304,7 +326,7 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin): class Meta: model = IPAddress - fields = ['address', 'vrf', 'nat_device', 'nat_inside', 'description'] + fields = ['address', 'vrf', 'tenant', 'nat_device', 'nat_inside', 'description'] help_texts = { 'address': "IPv4 or IPv6 address and mask", 'vrf': "VRF (if applicable)", @@ -353,6 +375,8 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin): class IPAddressFromCSVForm(forms.ModelForm): vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd', error_messages={'invalid_choice': 'VRF not found.'}) + tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False, + error_messages={'invalid_choice': 'Tenant not found.'}) device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name', error_messages={'invalid_choice': 'Device not found.'}) interface_name = forms.CharField(required=False) @@ -360,7 +384,7 @@ class IPAddressFromCSVForm(forms.ModelForm): class Meta: model = IPAddress - fields = ['address', 'vrf', 'device', 'interface_name', 'is_primary', 'description'] + fields = ['address', 'vrf', 'tenant', 'device', 'interface_name', 'is_primary', 'description'] def clean(self): @@ -403,18 +427,10 @@ class IPAddressImportForm(BulkImportForm, BootstrapMixin): csv = CSVDataField(csv_form=IPAddressFromCSVForm) -def ipaddress_vrf_choices(): - choices = [ - (None, '---------'), - (0, 'Global'), - ] - choices += [(v.pk, v.name) for v in VRF.objects.all()] - return choices - - class IPAddressBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput) - vrf = forms.TypedChoiceField(choices=ipaddress_vrf_choices, coerce=int, required=False, label='VRF') + vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF') + tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') description = forms.CharField(max_length=100, required=False) @@ -422,9 +438,17 @@ def ipaddress_family_choices(): return [('', 'All'), (4, 'IPv4'), (6, 'IPv6')] +def ipaddress_vrf_choices(): + vrf_choices = VRF.objects.annotate(ipaddress_count=Count('ip_addresses')) + return [(v.pk, u'{} ({})'.format(v.name, v.ipaddress_count)) for v in vrf_choices] + + class IPAddressFilterForm(forms.Form, BootstrapMixin): family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family') - vrf = forms.ChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF') + vrf = forms.MultipleChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF', + widget=forms.SelectMultiple(attrs={'size': 6})) + tenant = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), required=False, + widget=forms.SelectMultiple(attrs={'size': 6})) # @@ -518,7 +542,7 @@ class VLANBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False) group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False) - tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) + tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False) role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False) description = forms.CharField(max_length=100, required=False) diff --git a/netbox/ipam/migrations/0007_prefix_ipaddress_add_tenant.py b/netbox/ipam/migrations/0007_prefix_ipaddress_add_tenant.py new file mode 100644 index 000000000..eab3b9a47 --- /dev/null +++ b/netbox/ipam/migrations/0007_prefix_ipaddress_add_tenant.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.8 on 2016-07-28 15:32 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0001_initial'), + ('ipam', '0006_vrf_vlan_add_tenant'), + ] + + operations = [ + migrations.AddField( + model_name='ipaddress', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='tenancy.Tenant'), + ), + migrations.AddField( + model_name='prefix', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='tenancy.Tenant'), + ), + ] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index d28e4eb12..7c981a8cb 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -233,6 +233,7 @@ class Prefix(CreatedUpdatedModel): site = models.ForeignKey('dcim.Site', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True) vrf = models.ForeignKey('VRF', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True, verbose_name='VRF') + tenant = models.ForeignKey(Tenant, related_name='prefixes', blank=True, null=True, on_delete=models.PROTECT) vlan = models.ForeignKey('VLAN', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True, verbose_name='VLAN') status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1) @@ -308,6 +309,7 @@ class IPAddress(CreatedUpdatedModel): address = IPAddressField() vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True, verbose_name='VRF') + tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT) interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True, null=True) nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True, diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index bec2f0f49..30e383666 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -49,6 +49,16 @@ VLANGROUP_EDIT_LINK = """ {% endif %} """ +TENANT_LINK = """ +{% if record.tenant %} + {{ record.tenant }} +{% elif record.vrf.tenant %} + {{ record.vrf.tenant }}* +{% else %} + — +{% endif %} +""" + # # VRFs @@ -126,13 +136,14 @@ class PrefixTable(BaseTable): status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix') vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF') + tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') role = tables.Column(verbose_name='Role') description = tables.Column(orderable=False, verbose_name='Description') class Meta(BaseTable.Meta): model = Prefix - fields = ('pk', 'prefix', 'status', 'vrf', 'site', 'role', 'description') + fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description') class PrefixBriefTable(BaseTable): @@ -154,6 +165,7 @@ class IPAddressTable(BaseTable): pk = ToggleColumn() address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address') vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF') + tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant') device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False, verbose_name='Device') interface = tables.Column(orderable=False, verbose_name='Interface') @@ -161,7 +173,7 @@ class IPAddressTable(BaseTable): class Meta(BaseTable.Meta): model = IPAddress - fields = ('pk', 'address', 'vrf', 'device', 'interface', 'description') + fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description') class IPAddressBriefTable(BaseTable): diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 30ce9acdd..7402247b8 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -249,7 +249,7 @@ class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # class PrefixListView(ObjectListView): - queryset = Prefix.objects.select_related('site', 'vrf', 'role') + queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'role') filter = filters.PrefixFilter filter_form = forms.PrefixFilterForm table = tables.PrefixTable @@ -380,7 +380,7 @@ def prefix_ipaddresses(request, pk): # class IPAddressListView(ObjectListView): - queryset = IPAddress.objects.select_related('vrf', 'interface__device') + queryset = IPAddress.objects.select_related('vrf__tenant', 'interface__device') filter = filters.IPAddressFilter filter_form = forms.IPAddressFilterForm table = tables.IPAddressTable diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index 6834ded9e..9112707a7 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -64,6 +64,19 @@ {% endif %} + + Tenant + + {% if ipaddress.tenant %} + {{ ipaddress.tenant }} + {% elif ipaddress.vrf.tenant %} + {{ ipaddress.vrf.tenant }} + + {% else %} + None + {% endif %} + + Description diff --git a/netbox/templates/ipam/ipaddress_bulk_edit.html b/netbox/templates/ipam/ipaddress_bulk_edit.html index 8f53df143..bb809920b 100644 --- a/netbox/templates/ipam/ipaddress_bulk_edit.html +++ b/netbox/templates/ipam/ipaddress_bulk_edit.html @@ -7,7 +7,8 @@ {% for ipaddress in selected_objects %} {{ ipaddress }} - {{ ipaddress.vrf }} + {{ ipaddress.vrf|default:"Global" }} + {{ ipaddress.tenant }} {{ ipaddress.interface.device }} {{ ipaddress.interface }} {{ ipaddress.description }} diff --git a/netbox/templates/ipam/ipaddress_edit.html b/netbox/templates/ipam/ipaddress_edit.html index 1be63713b..97991c095 100644 --- a/netbox/templates/ipam/ipaddress_edit.html +++ b/netbox/templates/ipam/ipaddress_edit.html @@ -8,6 +8,7 @@
{% render_field form.address %} {% render_field form.vrf %} + {% render_field form.tenant %} {% if obj %}
diff --git a/netbox/templates/ipam/ipaddress_import.html b/netbox/templates/ipam/ipaddress_import.html index a415cb56a..b819df494 100644 --- a/netbox/templates/ipam/ipaddress_import.html +++ b/netbox/templates/ipam/ipaddress_import.html @@ -38,6 +38,11 @@ VRF route distinguisher (optional) 65000:123 + + Tenant + Name of tenant (optional) + ABC01 + Device Device name (optional) @@ -61,7 +66,7 @@

Example

-
192.0.2.42/24,65000:123,switch12,ge-0/0/31,True,Management IP
+
192.0.2.42/24,65000:123,ABC01,switch12,ge-0/0/31,True,Management IP
{% endblock %} diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 6403717f4..0ba97eeb0 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -26,6 +26,19 @@ {% endif %} + + Tenant + + {% if prefix.tenant %} + {{ prefix.tenant }} + {% elif prefix.vrf.tenant %} + {{ prefix.vrf.tenant }} + + {% else %} + None + {% endif %} + + Aggregate diff --git a/netbox/templates/ipam/prefix_bulk_edit.html b/netbox/templates/ipam/prefix_bulk_edit.html index 03e358725..6c17d5b71 100644 --- a/netbox/templates/ipam/prefix_bulk_edit.html +++ b/netbox/templates/ipam/prefix_bulk_edit.html @@ -8,6 +8,7 @@ {{ prefix }} {{ prefix.vrf|default:"Global" }} + {{ prefix.tenant }} {{ prefix.site }} {{ prefix.status }} {{ prefix.role }} diff --git a/netbox/templates/ipam/prefix_import.html b/netbox/templates/ipam/prefix_import.html index 902f23443..413732d3f 100644 --- a/netbox/templates/ipam/prefix_import.html +++ b/netbox/templates/ipam/prefix_import.html @@ -38,6 +38,11 @@ VRF route distinguisher (optional) 65000:123 + + Tenant + Name of tenant (optional) + ABC01 + Site Name of assigned site (optional) @@ -71,7 +76,7 @@

Example

-
192.168.42.0/24,65000:123,HQ,Customers,801,Active,Customer,7th floor WiFi
+
192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,7th floor WiFi
{% endblock %} diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index c52252f63..a12ed8fa8 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -91,29 +91,37 @@ Stats
-
+ -
+ - -
-
+ -
+
+
+
+

{{ tenant.prefix_count }}

+

Prefixes

+
+
+

{{ tenant.ipaddress_count }}

+

IP addresses

+
+ -
+ diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 0cf10aa80..bab624589 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -55,6 +55,8 @@ def tenant(request, slug): rack_count=Count('racks', distinct=True), device_count=Count('devices', distinct=True), vrf_count=Count('vrfs', distinct=True), + prefix_count=Count('prefixes', distinct=True), + ipaddress_count=Count('ip_addresses', distinct=True), vlan_count=Count('vlans', distinct=True), circuit_count=Count('circuits', distinct=True), ), slug=slug)