Skip to content

Commit

Permalink
Merge pull request #393 from digitalocean/multitenancy
Browse files Browse the repository at this point in the history
Multitenancy
  • Loading branch information
jeremystretch authored Jul 27, 2016
2 parents b790d7d + 4cc84ae commit 1413f5d
Show file tree
Hide file tree
Showing 76 changed files with 1,327 additions and 124 deletions.
9 changes: 9 additions & 0 deletions docs/data-model/tenancy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
NetBox supports the concept of individual tenants within its parent organization. Typically, these are used to represent individual customers or internal departments.

# Tenants

A tenant represents a discrete organization. Certain resources within NetBox can be assigned to a tenant. This makes it very convenient to track which resources are assigned to which customers, for instance.

### Tenant Groups

Tenants are grouped by type. For instance, you might create one group called "Customers" and one called "Acquisitions."
7 changes: 4 additions & 3 deletions netbox/circuits/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ class CircuitTypeAdmin(admin.ModelAdmin):

@admin.register(Circuit)
class CircuitAdmin(admin.ModelAdmin):
list_display = ['cid', 'provider', 'type', 'site', 'install_date', 'port_speed', 'commit_rate', 'xconnect_id']
list_filter = ['provider']
list_display = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'commit_rate',
'xconnect_id']
list_filter = ['provider', 'type', 'tenant']
exclude = ['interface']

def get_queryset(self, request):
qs = super(CircuitAdmin, self).get_queryset(request)
return qs.select_related('provider', 'type', 'site')
return qs.select_related('provider', 'type', 'tenant', 'site')
6 changes: 4 additions & 2 deletions netbox/circuits/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from circuits.models import Provider, CircuitType, Circuit
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
from tenancy.api.serializers import TenantNestedSerializer


#
Expand Down Expand Up @@ -45,13 +46,14 @@ class Meta(CircuitTypeSerializer.Meta):
class CircuitSerializer(serializers.ModelSerializer):
provider = ProviderNestedSerializer()
type = CircuitTypeNestedSerializer()
tenant = TenantNestedSerializer()
site = SiteNestedSerializer()
interface = InterfaceNestedSerializer()

class Meta:
model = Circuit
fields = ['id', 'cid', 'provider', 'type', 'site', 'interface', 'install_date', 'port_speed', 'commit_rate',
'xconnect_id', 'comments']
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed',
'commit_rate', 'xconnect_id', 'comments']


class CircuitNestedSerializer(CircuitSerializer):
Expand Down
4 changes: 2 additions & 2 deletions netbox/circuits/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class CircuitListView(generics.ListAPIView):
"""
List circuits (filterable)
"""
queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
serializer_class = serializers.CircuitSerializer
filter_class = CircuitFilter

Expand All @@ -51,5 +51,5 @@ class CircuitDetailView(generics.RetrieveAPIView):
"""
Retrieve a single circuit
"""
queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
serializer_class = serializers.CircuitSerializer
12 changes: 12 additions & 0 deletions netbox/circuits/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.db.models import Q

from dcim.models import Site
from tenancy.models import Tenant
from .models import Provider, Circuit, CircuitType


Expand Down Expand Up @@ -62,6 +63,17 @@ class CircuitFilter(django_filters.FilterSet):
to_field_name='slug',
label='Circuit type (slug)',
)
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)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
Expand Down
17 changes: 14 additions & 3 deletions netbox/circuits/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.db.models import Count

from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
from tenancy.models import Tenant
from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, Livesearch, SmallTextarea, SlugField,
)
Expand Down Expand Up @@ -99,7 +100,7 @@ class CircuitForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Circuit
fields = [
'cid', 'type', 'provider', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
'port_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
]
help_texts = {
Expand Down Expand Up @@ -160,13 +161,15 @@ class CircuitFromCSVForm(forms.ModelForm):
error_messages={'invalid_choice': 'Provider not found.'})
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Invalid circuit type.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
site = forms.ModelChoiceField(Site.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Site not found.'})

class Meta:
model = Circuit
fields = ['cid', 'provider', 'type', 'site', 'install_date', 'port_speed', 'commit_rate', 'xconnect_id',
'pp_info']
fields = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'commit_rate',
'xconnect_id', 'pp_info']


class CircuitImportForm(BulkImportForm, BootstrapMixin):
Expand All @@ -177,6 +180,7 @@ class CircuitBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
comments = CommentField()
Expand All @@ -192,6 +196,11 @@ def circuit_provider_choices():
return [(p.slug, u'{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]


def circuit_tenant_choices():
tenant_choices = Tenant.objects.annotate(circuit_count=Count('circuits'))
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in tenant_choices]


def circuit_site_choices():
site_choices = Site.objects.annotate(circuit_count=Count('circuits'))
return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
Expand All @@ -201,5 +210,7 @@ class CircuitFilterForm(forms.Form, BootstrapMixin):
type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices)
provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=circuit_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
22 changes: 22 additions & 0 deletions netbox/circuits/migrations/0004_circuit_add_tenant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-07-26 21:59
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('tenancy', '0001_initial'),
('circuits', '0003_provider_32bit_asn_support'),
]

operations = [
migrations.AddField(
model_name='circuit',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.Tenant'),
),
]
3 changes: 3 additions & 0 deletions netbox/circuits/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from dcim.fields import ASNField
from dcim.models import Site, Interface
from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel


Expand Down Expand Up @@ -66,6 +67,7 @@ class Circuit(CreatedUpdatedModel):
cid = models.CharField(max_length=50, verbose_name='Circuit ID')
provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT)
tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT)
site = models.ForeignKey(Site, related_name='circuits', on_delete=models.PROTECT)
interface = models.OneToOneField(Interface, related_name='circuit', blank=True, null=True)
install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
Expand All @@ -90,6 +92,7 @@ def to_csv(self):
self.cid,
self.provider.name,
self.type.name,
self.tenant.name if self.tenant else '',
self.site.name,
self.install_date.isoformat() if self.install_date else '',
str(self.port_speed),
Expand Down
3 changes: 2 additions & 1 deletion netbox/circuits/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ class CircuitTable(BaseTable):
cid = tables.LinkColumn('circuits:circuit', args=[Accessor('pk')], verbose_name='ID')
type = tables.Column(verbose_name='Type')
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
port_speed_human = tables.Column(verbose_name='Port Speed')
commit_rate_human = tables.Column(verbose_name='Commit Rate')

class Meta(BaseTable.Meta):
model = Circuit
fields = ('pk', 'cid', 'type', 'provider', 'site', 'port_speed_human', 'commit_rate_human')
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'site', 'port_speed_human', 'commit_rate_human')
4 changes: 2 additions & 2 deletions netbox/circuits/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#

class CircuitListView(ObjectListView):
queryset = Circuit.objects.select_related('provider', 'type', 'site')
queryset = Circuit.objects.select_related('provider', 'type', 'tenant', 'site')
filter = filters.CircuitFilter
filter_form = forms.CircuitFilterForm
table = tables.CircuitTable
Expand Down Expand Up @@ -159,7 +159,7 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):

fields_to_update = {}
for field in ['type', 'provider', 'port_speed', 'commit_rate', 'comments']:
for field in ['type', 'provider', 'tenant', 'port_speed', 'commit_rate', 'comments']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]

Expand Down
16 changes: 10 additions & 6 deletions netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
)
from tenancy.api.serializers import TenantNestedSerializer


#
# Sites
#

class SiteSerializer(serializers.ModelSerializer):
tenant = TenantNestedSerializer()

class Meta:
model = Site
fields = ['id', 'name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']


Expand Down Expand Up @@ -52,10 +54,11 @@ class Meta(SiteSerializer.Meta):
class RackSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
group = RackGroupNestedSerializer()
tenant = TenantNestedSerializer()

class Meta:
model = Rack
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'u_height', 'comments']
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'u_height', 'comments']


class RackNestedSerializer(RackSerializer):
Expand All @@ -69,8 +72,8 @@ class RackDetailSerializer(RackSerializer):
rear_units = serializers.SerializerMethodField()

class Meta(RackSerializer.Meta):
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'u_height', 'comments', 'front_units',
'rear_units']
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'u_height', 'comments',
'front_units', 'rear_units']

def get_front_units(self, obj):
units = obj.get_rack_units(face=RACK_FACE_FRONT)
Expand Down Expand Up @@ -218,6 +221,7 @@ class Meta:
class DeviceSerializer(serializers.ModelSerializer):
device_type = DeviceTypeNestedSerializer()
device_role = DeviceRoleNestedSerializer()
tenant = TenantNestedSerializer()
platform = PlatformNestedSerializer()
rack = RackNestedSerializer()
primary_ip = DeviceIPAddressNestedSerializer()
Expand All @@ -227,8 +231,8 @@ class DeviceSerializer(serializers.ModelSerializer):

class Meta:
model = Device
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'platform', 'serial', 'rack', 'position',
'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments']
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'rack',
'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments']

def get_parent_device(self, obj):
try:
Expand Down
13 changes: 7 additions & 6 deletions netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ class SiteListView(generics.ListAPIView):
"""
List all sites
"""
queryset = Site.objects.all()
queryset = Site.objects.select_related('tenant')
serializer_class = serializers.SiteSerializer


class SiteDetailView(generics.RetrieveAPIView):
"""
Retrieve a single site
"""
queryset = Site.objects.all()
queryset = Site.objects.select_related('tenant')
serializer_class = serializers.SiteSerializer


Expand Down Expand Up @@ -68,7 +68,7 @@ class RackListView(generics.ListAPIView):
"""
List racks (filterable)
"""
queryset = Rack.objects.select_related('site')
queryset = Rack.objects.select_related('site', 'tenant')
serializer_class = serializers.RackSerializer
filter_class = filters.RackFilter

Expand All @@ -77,7 +77,7 @@ class RackDetailView(generics.RetrieveAPIView):
"""
Retrieve a single rack
"""
queryset = Rack.objects.select_related('site')
queryset = Rack.objects.select_related('site', 'tenant')
serializer_class = serializers.RackDetailSerializer


Expand Down Expand Up @@ -193,8 +193,9 @@ class DeviceListView(generics.ListAPIView):
"""
List devices (filterable)
"""
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'platform', 'rack__site')\
.prefetch_related('primary_ip4__nat_outside', 'primary_ip6__nat_outside')
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
'rack__site').prefetch_related('primary_ip4__nat_outside',
'primary_ip6__nat_outside')
serializer_class = serializers.DeviceSerializer
filter_class = filters.DeviceFilter
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
Expand Down
Loading

0 comments on commit 1413f5d

Please sign in to comment.