diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 4856c1a4c..00a6e2fda 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -6,6 +6,7 @@ ### Enhancements * [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces +* [#6874](https://github.com/netbox-community/netbox/issues/6874) - Add tenant assignment for locations ### Other Changes diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index a7d2e88da..d6e44c281 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -138,14 +138,15 @@ class LocationSerializer(NestedGroupModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail') site = NestedSiteSerializer() parent = NestedLocationSerializer(required=False, allow_null=True) + tenant = NestedTenantSerializer(required=False, allow_null=True) rack_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True) class Meta: model = Location fields = [ - 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'description', 'custom_fields', 'created', - 'last_updated', 'rack_count', 'device_count', '_depth', + 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'custom_fields', + 'created', 'last_updated', 'rack_count', 'device_count', '_depth', ] diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 5cfe86118..fd87d7304 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -148,13 +148,17 @@ class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): 'site_id': '$site' } ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) description = forms.CharField( max_length=200, required=False ) class Meta: - nullable_fields = ['parent', 'description'] + nullable_fields = ['parent', 'tenant', 'description'] class RackRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 8f7755869..ff9ab6fff 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -120,10 +120,16 @@ class LocationCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Location not found.', } ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) class Meta: model = Location - fields = ('site', 'parent', 'name', 'slug', 'description') + fields = ('site', 'parent', 'name', 'slug', 'tenant', 'description') class RackRoleCSVForm(CustomFieldModelCSVForm): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 0079217ab..4f4e10e96 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -175,8 +175,13 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo tag = TagFilterField(model) -class LocationFilterForm(BootstrapMixin, CustomFieldModelFilterForm): +class LocationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): model = Location + field_groups = [ + ['q'], + ['region_id', 'site_group_id', 'site_id', 'parent_id'], + ['tenant_group_id', 'tenant_id'], + ] q = forms.CharField( required=False, widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 0b6e66c3c..a8c2991a4 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -157,7 +157,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): } -class LocationForm(BootstrapMixin, CustomFieldModelForm): +class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): region = DynamicModelChoiceField( queryset=Region.objects.all(), required=False, @@ -191,7 +191,13 @@ class LocationForm(BootstrapMixin, CustomFieldModelForm): class Meta: model = Location fields = ( - 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', + 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant', + ) + fieldsets = ( + ('Location', ( + 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', + )), + ('Tenancy', ('tenant_group', 'tenant')), ) diff --git a/netbox/dcim/migrations/0135_location_tenant.py b/netbox/dcim/migrations/0135_location_tenant.py new file mode 100644 index 000000000..0b1f429f9 --- /dev/null +++ b/netbox/dcim/migrations/0135_location_tenant.py @@ -0,0 +1,18 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0002_tenant_ordering'), + ('dcim', '0134_interface_wwn'), + ] + + operations = [ + migrations.AddField( + model_name='location', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='locations', to='tenancy.tenant'), + ), + ] diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index 56946642b..b343f61f2 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -7,7 +7,6 @@ from timezone_field import TimeZoneField from dcim.choices import * from dcim.constants import * -from django.core.exceptions import ValidationError from dcim.fields import ASNField from extras.utils import extras_features from netbox.models import NestedGroupModel, PrimaryModel @@ -281,6 +280,13 @@ class Location(NestedGroupModel): null=True, db_index=True ) + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + related_name='locations', + blank=True, + null=True + ) description = models.CharField( max_length=200, blank=True diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 37fa019a1..3ff6ab75b 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -103,6 +103,7 @@ class LocationTable(BaseTable): site = tables.Column( linkify=True ) + tenant = TenantColumn() rack_count = LinkedCountColumn( viewname='dcim:rack_list', url_params={'location_id': 'pk'}, @@ -120,5 +121,5 @@ class LocationTable(BaseTable): class Meta(BaseTable.Meta): model = Location - fields = ('pk', 'name', 'site', 'rack_count', 'device_count', 'description', 'slug', 'actions') - default_columns = ('pk', 'name', 'site', 'rack_count', 'device_count', 'description', 'actions') + fields = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'actions') + default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions') diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 00904d444..545a56f81 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -12,6 +12,7 @@ from dcim.choices import * from dcim.constants import * from dcim.models import * from ipam.models import VLAN +from tenancy.models import Tenant from utilities.testing import ViewTestCases, create_tags, create_test_device @@ -157,13 +158,13 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase): @classmethod def setUpTestData(cls): - site = Site(name='Site 1', slug='site-1') - site.save() + site = Site.objects.create(name='Site 1', slug='site-1') + tenant = Tenant.objects.create(name='Tenant 1', slug='tenant-1') locations = ( - Location(name='Location 1', slug='location-1', site=site), - Location(name='Location 2', slug='location-2', site=site), - Location(name='Location 3', slug='location-3', site=site), + Location(name='Location 1', slug='location-1', site=site, tenant=tenant), + Location(name='Location 2', slug='location-2', site=site, tenant=tenant), + Location(name='Location 3', slug='location-3', site=site, tenant=tenant), ) for location in locations: location.save() @@ -172,14 +173,15 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase): 'name': 'Location X', 'slug': 'location-x', 'site': site.pk, + 'tenant': tenant.pk, 'description': 'A new location', } cls.csv_data = ( - "site,name,slug,description", - "Site 1,Location 4,location-4,Fourth location", - "Site 1,Location 5,location-5,Fifth location", - "Site 1,Location 6,location-6,Sixth location", + "site,tenant,name,slug,description", + "Site 1,Tenant 1,Location 4,location-4,Fourth location", + "Site 1,Tenant 1,Location 5,location-5,Fifth location", + "Site 1,Tenant 1,Location 6,location-6,Sixth location", ) cls.bulk_edit_data = { diff --git a/netbox/templates/dcim/location.html b/netbox/templates/dcim/location.html index b062ddcb5..cd0f2a92a 100644 --- a/netbox/templates/dcim/location.html +++ b/netbox/templates/dcim/location.html @@ -40,6 +40,19 @@ {% endif %} + + Tenant + + {% if object.tenant %} + {% if object.tenant.group %} + {{ object.tenant.group }} / + {% endif %} + {{ object.tenant }} + {% else %} + None + {% endif %} + + Racks