diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 31f708c86..7552ae0d2 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -70,11 +70,12 @@ class AggregateSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail') family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) rir = NestedRIRSerializer() + tenant = NestedTenantSerializer(required=False, allow_null=True) class Meta: model = Aggregate fields = [ - 'id', 'url', 'family', 'prefix', 'rir', 'date_added', 'description', 'tags', 'custom_fields', 'created', + 'id', 'url', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', 'tags', 'custom_fields', 'created', 'last_updated', ] read_only_fields = ['family'] diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 0cbbd3f78..988ee86fb 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -122,7 +122,7 @@ class RIRFilterSet(BaseFilterSet, NameSlugSearchFilterSet): fields = ['id', 'name', 'slug', 'is_private', 'description'] -class AggregateFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): +class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 714279859..ea7014121 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -225,7 +225,7 @@ class RIRFilterForm(BootstrapMixin, forms.Form): # Aggregates # -class AggregateForm(BootstrapMixin, CustomFieldModelForm): +class AggregateForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): rir = DynamicModelChoiceField( queryset=RIR.objects.all() ) @@ -237,7 +237,7 @@ class AggregateForm(BootstrapMixin, CustomFieldModelForm): class Meta: model = Aggregate fields = [ - 'prefix', 'rir', 'date_added', 'description', 'tags', + 'prefix', 'rir', 'date_added', 'description', 'tenant_group', 'tenant', 'tags', ] help_texts = { 'prefix': "IPv4 or IPv6 network", @@ -254,6 +254,12 @@ class AggregateCSVForm(CustomFieldModelCSVForm): to_field_name='name', help_text='Assigned RIR' ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) class Meta: model = Aggregate @@ -270,6 +276,10 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd required=False, label='RIR' ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) date_added = forms.DateField( required=False ) @@ -287,7 +297,7 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd } -class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm): +class AggregateFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Aggregate q = forms.CharField( required=False, diff --git a/netbox/ipam/migrations/0043_add_tenancy_to_aggregates.py b/netbox/ipam/migrations/0043_add_tenancy_to_aggregates.py new file mode 100644 index 000000000..a5ec9013b --- /dev/null +++ b/netbox/ipam/migrations/0043_add_tenancy_to_aggregates.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1 on 2020-10-16 01:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0011_standardize_name_length'), + ('ipam', '0042_standardize_name_length'), + ] + + operations = [ + migrations.AddField( + model_name='aggregate', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='aggregates', to='tenancy.tenant'), + ), + ] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index f6414d5d7..493df9d9a 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -222,6 +222,13 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel): related_name='aggregates', verbose_name='RIR' ) + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + related_name='aggregates', + blank=True, + null=True + ) date_added = models.DateField( blank=True, null=True @@ -234,9 +241,9 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel): objects = RestrictedQuerySet.as_manager() - csv_headers = ['prefix', 'rir', 'date_added', 'description'] + csv_headers = ['prefix', 'rir', 'tenant', 'date_added', 'description'] clone_fields = [ - 'rir', 'date_added', 'description', + 'rir', 'tenant', 'date_added', 'description', ] class Meta: @@ -289,6 +296,7 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel): return ( self.prefix, self.rir.name, + self.tenant.name if self.tenant else None, self.date_added, self.description, ) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index c6381a37a..cb1d22a39 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -253,6 +253,9 @@ class AggregateTable(BaseTable): prefix = tables.LinkColumn( verbose_name='Aggregate' ) + tenant = tables.TemplateColumn( + template_code=TENANT_LINK + ) date_added = tables.DateColumn( format="Y-m-d", verbose_name='Added' @@ -260,7 +263,7 @@ class AggregateTable(BaseTable): class Meta(BaseTable.Meta): model = Aggregate - fields = ('pk', 'prefix', 'rir', 'date_added', 'description') + fields = ('pk', 'prefix', 'rir', 'tenant', 'date_added', 'description') class AggregateDetailTable(AggregateTable): @@ -276,8 +279,8 @@ class AggregateDetailTable(AggregateTable): ) class Meta(AggregateTable.Meta): - fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description', 'tags') - default_columns = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description') + fields = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description', 'tags') + default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description') # diff --git a/netbox/ipam/tests/test_filters.py b/netbox/ipam/tests/test_filters.py index aa607eb6b..f86e3f8b6 100644 --- a/netbox/ipam/tests/test_filters.py +++ b/netbox/ipam/tests/test_filters.py @@ -240,13 +240,28 @@ class AggregateTestCase(TestCase): ) RIR.objects.bulk_create(rirs) + tenant_groups = ( + TenantGroup(name='Tenant group 1', slug='tenant-group-1'), + TenantGroup(name='Tenant group 2', slug='tenant-group-2'), + TenantGroup(name='Tenant group 3', slug='tenant-group-3'), + ) + for tenantgroup in tenant_groups: + tenantgroup.save() + + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]), + Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]), + Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]), + ) + Tenant.objects.bulk_create(tenants) + aggregates = ( - Aggregate(prefix='10.1.0.0/16', rir=rirs[0], date_added='2020-01-01'), - Aggregate(prefix='10.2.0.0/16', rir=rirs[0], date_added='2020-01-02'), - Aggregate(prefix='10.3.0.0/16', rir=rirs[1], date_added='2020-01-03'), - Aggregate(prefix='2001:db8:1::/48', rir=rirs[1], date_added='2020-01-04'), - Aggregate(prefix='2001:db8:2::/48', rir=rirs[2], date_added='2020-01-05'), - Aggregate(prefix='2001:db8:3::/48', rir=rirs[2], date_added='2020-01-06'), + Aggregate(prefix='10.1.0.0/16', rir=rirs[0], tenant=tenants[0], date_added='2020-01-01'), + Aggregate(prefix='10.2.0.0/16', rir=rirs[0], tenant=tenants[1], date_added='2020-01-02'), + Aggregate(prefix='10.3.0.0/16', rir=rirs[1], tenant=tenants[2], date_added='2020-01-03'), + Aggregate(prefix='2001:db8:1::/48', rir=rirs[1], tenant=tenants[0], date_added='2020-01-04'), + Aggregate(prefix='2001:db8:2::/48', rir=rirs[2], tenant=tenants[1], date_added='2020-01-05'), + Aggregate(prefix='2001:db8:3::/48', rir=rirs[2], tenant=tenants[2], date_added='2020-01-06'), ) Aggregate.objects.bulk_create(aggregates) @@ -274,6 +289,24 @@ class AggregateTestCase(TestCase): params = {'rir': [rirs[0].slug, rirs[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_tenant(self): + tenants = Tenant.objects.all()[:2] + params = {'tenant_id': [tenants[0].pk, tenants[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + print(self.filterset(params, self.queryset).qs.count()) + params = {'tenant': [tenants[0].slug, tenants[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + print(self.filterset(params, self.queryset).qs.count()) + + def test_tenant_group(self): + tenant_groups = TenantGroup.objects.all()[:2] + params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]} + print(self.filterset(params, self.queryset).qs.count()) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]} + print(self.filterset(params, self.queryset).qs.count()) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + class RoleTestCase(TestCase): queryset = Role.objects.all() diff --git a/netbox/templates/ipam/aggregate_edit.html b/netbox/templates/ipam/aggregate_edit.html index 3cb83ab54..afd20bb26 100644 --- a/netbox/templates/ipam/aggregate_edit.html +++ b/netbox/templates/ipam/aggregate_edit.html @@ -11,6 +11,13 @@ {% render_field form.description %} +