1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Implemented tenancy for VRFs and VLANs

This commit is contained in:
Jeremy Stretch
2016-07-27 11:29:20 -04:00
parent 65b008a493
commit 2abee211a2
16 changed files with 144 additions and 21 deletions

View File

@ -7,7 +7,12 @@ from .models import (
@admin.register(VRF) @admin.register(VRF)
class VRFAdmin(admin.ModelAdmin): class VRFAdmin(admin.ModelAdmin):
list_display = ['name', 'rd'] list_display = ['name', 'rd', 'tenant', 'enforce_unique']
list_filter = ['tenant']
def get_queryset(self, request):
qs = super(VRFAdmin, self).get_queryset(request)
return qs.select_related('tenant')
@admin.register(Role) @admin.register(Role)
@ -67,10 +72,10 @@ class VLANGroupAdmin(admin.ModelAdmin):
@admin.register(VLAN) @admin.register(VLAN)
class VLANAdmin(admin.ModelAdmin): class VLANAdmin(admin.ModelAdmin):
list_display = ['site', 'vid', 'name', 'status', 'role'] list_display = ['site', 'vid', 'name', 'tenant', 'status', 'role']
list_filter = ['site', 'status', 'role'] list_filter = ['site', 'tenant', 'status', 'role']
search_fields = ['vid', 'name'] search_fields = ['vid', 'name']
def get_queryset(self, request): def get_queryset(self, request):
qs = super(VLANAdmin, self).get_queryset(request) qs = super(VLANAdmin, self).get_queryset(request)
return qs.select_related('site', 'role') return qs.select_related('site', 'tenant', 'role')

View File

@ -3,6 +3,7 @@ from netaddr import IPNetwork
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from dcim.models import Site, Device, Interface from dcim.models import Site, Device, Interface
from tenancy.models import Tenant
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
@ -13,6 +14,17 @@ class VRFFilter(django_filters.FilterSet):
lookup_type='icontains', lookup_type='icontains',
label='Name', label='Name',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
class Meta: class Meta:
model = VRF model = VRF
@ -226,6 +238,17 @@ class VLANFilter(django_filters.FilterSet):
name='vid', name='vid',
label='VLAN number (1-4095)', label='VLAN number (1-4095)',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
role_id = django_filters.ModelMultipleChoiceFilter( role_id = django_filters.ModelMultipleChoiceFilter(
name='role', name='role',
queryset=Role.objects.all(), queryset=Role.objects.all(),

View File

@ -4,6 +4,7 @@ from django import forms
from django.db.models import Count from django.db.models import Count
from dcim.models import Site, Device, Interface from dcim.models import Site, Device, Interface
from tenancy.models import Tenant
from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField
from .models import ( from .models import (
@ -23,7 +24,7 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
class Meta: class Meta:
model = VRF model = VRF
fields = ['name', 'rd', 'enforce_unique', 'description'] fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
labels = { labels = {
'rd': "RD", 'rd': "RD",
} }
@ -33,10 +34,12 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
class VRFFromCSVForm(forms.ModelForm): class VRFFromCSVForm(forms.ModelForm):
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
class Meta: class Meta:
model = VRF model = VRF
fields = ['name', 'rd', 'enforce_unique', 'description'] fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
class VRFImportForm(BulkImportForm, BootstrapMixin): class VRFImportForm(BulkImportForm, BootstrapMixin):
@ -45,9 +48,20 @@ class VRFImportForm(BulkImportForm, BootstrapMixin):
class VRFBulkEditForm(forms.Form, BootstrapMixin): class VRFBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
def vrf_tenant_choices():
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vrfs'))
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
class VRFFilterForm(forms.Form, BootstrapMixin):
tenant = forms.MultipleChoiceField(required=False, choices=vrf_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
# #
# RIRs # RIRs
# #
@ -444,7 +458,7 @@ class VLANForm(forms.ModelForm, BootstrapMixin):
class Meta: class Meta:
model = VLAN model = VLAN
fields = ['site', 'group', 'vid', 'name', 'description', 'status', 'role'] fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
help_texts = { help_texts = {
'site': "The site at which this VLAN exists", 'site': "The site at which this VLAN exists",
'group': "VLAN group (optional)", 'group': "VLAN group (optional)",
@ -475,13 +489,15 @@ class VLANFromCSVForm(forms.ModelForm):
error_messages={'invalid_choice': 'Device not found.'}) error_messages={'invalid_choice': 'Device not found.'})
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name', group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'VLAN group not found.'}) error_messages={'invalid_choice': 'VLAN group not found.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES]) status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name', role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid role.'}) error_messages={'invalid_choice': 'Invalid role.'})
class Meta: class Meta:
model = VLAN model = VLAN
fields = ['site', 'group', 'vid', 'name', 'status_name', 'role', 'description'] fields = ['site', 'group', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
m = super(VLANFromCSVForm, self).save(commit=False) m = super(VLANFromCSVForm, self).save(commit=False)
@ -500,6 +516,7 @@ class VLANBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False) group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False) status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False) role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
@ -515,6 +532,11 @@ def vlan_group_choices():
return [(g.pk, u'{} ({})'.format(g, g.vlan_count)) for g in group_choices] return [(g.pk, u'{} ({})'.format(g, g.vlan_count)) for g in group_choices]
def vlan_tenant_choices():
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vlans'))
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
def vlan_status_choices(): def vlan_status_choices():
status_counts = {} status_counts = {}
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'): for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
@ -532,6 +554,8 @@ class VLANFilterForm(forms.Form, BootstrapMixin):
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group', group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group',
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=vlan_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices) status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices)
role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices, role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-07-27 14:39
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', '0005_auto_20160725_1842'),
]
operations = [
migrations.AddField(
model_name='vlan',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='tenancy.Tenant'),
),
migrations.AddField(
model_name='vrf',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vrfs', to='tenancy.Tenant'),
),
]

View File

@ -7,6 +7,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from dcim.models import Interface from dcim.models import Interface
from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from .fields import IPNetworkField, IPAddressField from .fields import IPNetworkField, IPAddressField
@ -46,6 +47,7 @@ class VRF(CreatedUpdatedModel):
""" """
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher') rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher')
tenant = models.ForeignKey(Tenant, related_name='vrfs', blank=True, null=True, on_delete=models.PROTECT)
enforce_unique = models.BooleanField(default=True, verbose_name='Enforce unique space', enforce_unique = models.BooleanField(default=True, verbose_name='Enforce unique space',
help_text="Prevent duplicate prefixes/IP addresses within this VRF") help_text="Prevent duplicate prefixes/IP addresses within this VRF")
description = models.CharField(max_length=100, blank=True) description = models.CharField(max_length=100, blank=True)
@ -65,6 +67,8 @@ class VRF(CreatedUpdatedModel):
return ','.join([ return ','.join([
self.name, self.name,
self.rd, self.rd,
self.tenant.name if self.tenant else '',
'True' if self.enforce_unique else '',
self.description, self.description,
]) ])
@ -291,7 +295,7 @@ class Prefix(CreatedUpdatedModel):
class IPAddress(CreatedUpdatedModel): class IPAddress(CreatedUpdatedModel):
""" """
An IPAddress represents an individual IPV4 or IPv6 address and its mask. The mask length should match what is An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like
Prefixes, IPAddresses can optionally be assigned to a VRF. An IPAddress can optionally be assigned to an Interface. Prefixes, IPAddresses can optionally be assigned to a VRF. An IPAddress can optionally be assigned to an Interface.
Interfaces can have zero or more IPAddresses assigned to them. Interfaces can have zero or more IPAddresses assigned to them.
@ -407,9 +411,10 @@ class VLAN(CreatedUpdatedModel):
MaxValueValidator(4094) MaxValueValidator(4094)
]) ])
name = models.CharField(max_length=64) name = models.CharField(max_length=64)
description = models.CharField(max_length=100, blank=True) tenant = models.ForeignKey(Tenant, related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1) status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1)
role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True) role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True)
description = models.CharField(max_length=100, blank=True)
class Meta: class Meta:
ordering = ['site', 'group', 'vid'] ordering = ['site', 'group', 'vid']
@ -438,6 +443,7 @@ class VLAN(CreatedUpdatedModel):
self.group.name if self.group else '', self.group.name if self.group else '',
str(self.vid), str(self.vid),
self.name, self.name,
self.tenant.name if self.tenant else '',
self.get_status_display(), self.get_status_display(),
self.role.name if self.role else '', self.role.name if self.role else '',
self.description, self.description,

View File

@ -58,11 +58,12 @@ class VRFTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name') name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name')
rd = tables.Column(verbose_name='RD') rd = tables.Column(verbose_name='RD')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
description = tables.Column(orderable=False, verbose_name='Description') description = tables.Column(orderable=False, verbose_name='Description')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VRF model = VRF
fields = ('pk', 'name', 'rd', 'description') fields = ('pk', 'name', 'rd', 'tenant', 'description')
# #
@ -203,9 +204,10 @@ class VLANTable(BaseTable):
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
name = tables.Column(verbose_name='Name') name = tables.Column(verbose_name='Name')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
role = tables.Column(verbose_name='Role') role = tables.Column(verbose_name='Role')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VLAN model = VLAN
fields = ('pk', 'vid', 'site', 'group', 'name', 'status', 'role') fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role')

View File

@ -36,8 +36,9 @@ def add_available_prefixes(parent, prefix_list):
# #
class VRFListView(ObjectListView): class VRFListView(ObjectListView):
queryset = VRF.objects.all() queryset = VRF.objects.select_related('tenant')
filter = filters.VRFFilter filter = filters.VRFFilter
filter_form = forms.VRFFilterForm
table = tables.VRFTable table = tables.VRFTable
edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf'] edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf']
template_name = 'ipam/vrf_list.html' template_name = 'ipam/vrf_list.html'
@ -85,7 +86,7 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form): def update_objects(self, pk_list, form):
fields_to_update = {} fields_to_update = {}
for field in ['description']: for field in ['tenant', 'description']:
if form.cleaned_data[field]: if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field] fields_to_update[field] = form.cleaned_data[field]
@ -558,7 +559,7 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form): def update_objects(self, pk_list, form):
fields_to_update = {} fields_to_update = {}
for field in ['site', 'group', 'status', 'role', 'description']: for field in ['site', 'group', 'tenant', 'status', 'role', 'description']:
if form.cleaned_data[field]: if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field] fields_to_update[field] = form.cleaned_data[field]

View File

@ -70,10 +70,10 @@
<td>{{ vlan.name }}</td> <td>{{ vlan.name }}</td>
</tr> </tr>
<tr> <tr>
<td>Description</td> <td>Tenant</td>
<td> <td>
{% if vlan.description %} {% if vlan.tenant %}
{{ vlan.description }} <a href="{{ vlan.tenant.get_absolute_url }}">{{ vlan.tenant }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
{% endif %} {% endif %}
@ -89,6 +89,16 @@
<td>Role</td> <td>Role</td>
<td>{{ vlan.role }}</td> <td>{{ vlan.role }}</td>
</tr> </tr>
<tr>
<td>Description</td>
<td>
{% if vlan.description %}
{{ vlan.description }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr> <tr>
<td>Created</td> <td>Created</td>
<td>{{ vlan.created }}</td> <td>{{ vlan.created }}</td>

View File

@ -9,7 +9,8 @@
<td><a href="{% url 'ipam:vlan' pk=vlan.pk %}">{{ vlan.vid }}</a></td> <td><a href="{% url 'ipam:vlan' pk=vlan.pk %}">{{ vlan.vid }}</a></td>
<td>{{ vlan.name }}</td> <td>{{ vlan.name }}</td>
<td>{{ vlan.site }}</td> <td>{{ vlan.site }}</td>
<td>{{ vlan.status }}</td> <td>{{ vlan.tenant }}</td>
<td>{{ vlan.get_status_display }}</td>
<td>{{ vlan.role }}</td> <td>{{ vlan.role }}</td>
<td>{{ vlan.description }}</td> <td>{{ vlan.description }}</td>
</tr> </tr>

View File

@ -48,6 +48,11 @@
<td>Configured VLAN name</td> <td>Configured VLAN name</td>
<td>Cameras</td> <td>Cameras</td>
</tr> </tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Internal</td>
</tr>
<tr> <tr>
<td>Status</td> <td>Status</td>
<td>Current status</td> <td>Current status</td>
@ -66,7 +71,7 @@
</tbody> </tbody>
</table> </table>
<h4>Example</h4> <h4>Example</h4>
<pre>LAS2,Backend Network,1400,Cameras,Active,Security,Security team only</pre> <pre>LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only</pre>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -30,6 +30,16 @@
<td>Route Distinguisher</td> <td>Route Distinguisher</td>
<td>{{ vrf.rd }}</td> <td>{{ vrf.rd }}</td>
</tr> </tr>
<tr>
<td>Tenant</td>
<td>
{% if vrf.tenant %}
<a href="{{ vrf.tenant.get_absolute_url }}">{{ vrf.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr> <tr>
<td>Enforce Uniqueness</td> <td>Enforce Uniqueness</td>
<td> <td>

View File

@ -8,6 +8,7 @@
<tr> <tr>
<td><a href="{% url 'ipam:vrf' pk=vrf.pk %}">{{ vrf.name }}</a></td> <td><a href="{% url 'ipam:vrf' pk=vrf.pk %}">{{ vrf.name }}</a></td>
<td>{{ vrf.rd }}</td> <td>{{ vrf.rd }}</td>
<td>{{ vrf.tenant }}</td>
<td>{{ vrf.description }}</td> <td>{{ vrf.description }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -38,6 +38,11 @@
<td>Route distinguisher</td> <td>Route distinguisher</td>
<td>65000:123456</td> <td>65000:123456</td>
</tr> </tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>ABC01</td>
</tr>
<tr> <tr>
<td>Enforce uniqueness</td> <td>Enforce uniqueness</td>
<td>Prevent duplicate prefixes/IP addresses</td> <td>Prevent duplicate prefixes/IP addresses</td>
@ -51,7 +56,7 @@
</tbody> </tbody>
</table> </table>
<h4>Example</h4> <h4>Example</h4>
<pre>Customer_ABC,65000:123456,True,Native VRF for customer ABC</pre> <pre>Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC</pre>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -41,6 +41,7 @@
</form> </form>
</div> </div>
</div> </div>
{% include 'inc/filter_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -9,6 +9,7 @@
{% render_field form.name %} {% render_field form.name %}
{% render_field form.slug %} {% render_field form.slug %}
{% render_field form.group %} {% render_field form.group %}
{% render_field form.description %}
</div> </div>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">

View File

@ -46,4 +46,5 @@ class Tenant(CreatedUpdatedModel):
self.name, self.name,
self.slug, self.slug,
self.group.name, self.group.name,
self.description,
]) ])