Skip to content

Commit

Permalink
Initial work on regions
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremystretch committed Feb 28, 2017
1 parent 5520144 commit f3b9930
Show file tree
Hide file tree
Showing 26 changed files with 379 additions and 43 deletions.
4 changes: 4 additions & 0 deletions docs/data-model/dcim.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ How you define sites will depend on the nature of your organization, but typical

Sites can be assigned an optional facility ID to identify the actual facility housing colocated equipment.

### Regions

Sites can be arranged by geographic region. A region might represent a continent, country, city, campus, or other area depending on your use case. Region assignment is optional.

---

# Racks
Expand Down
14 changes: 11 additions & 3 deletions netbox/circuits/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,17 @@ class CircuitListView(ObjectListView):

def circuit(request, pk):

circuit = get_object_or_404(Circuit, pk=pk)
termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first()
termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first()
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
termination_a = CircuitTermination.objects.select_related(
'site__region', 'interface__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_A
).first()
termination_z = CircuitTermination.objects.select_related(
'site__region', 'interface__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_Z
).first()

return render(request, 'circuits/circuit.html', {
'circuit': circuit,
Expand Down
11 changes: 10 additions & 1 deletion netbox/dcim/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Site,
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region,
Site,
)


@admin.register(Region)
class RegionAdmin(admin.ModelAdmin):
list_display = ['name', 'slug']
prepopulated_fields = {
'slug': ['name'],
}


@admin.register(Site)
class SiteAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'facility', 'asn']
Expand Down
22 changes: 20 additions & 2 deletions netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,40 @@
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_FRONT,
RACK_FACE_REAR, Site, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
RACK_FACE_REAR, Region, Site, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
)
from extras.api.serializers import CustomFieldSerializer
from tenancy.api.serializers import TenantNestedSerializer


#
# Regions
#

class RegionSerializer(serializers.ModelSerializer):

class Meta:
model = RackGroup
fields = ['id', 'name', 'slug']


class RegionNestedSerializer(RegionSerializer):

class Meta(RegionSerializer.Meta):
pass


#
# Sites
#

class SiteSerializer(CustomFieldSerializer, serializers.ModelSerializer):
region = RegionNestedSerializer()
tenant = TenantNestedSerializer()

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

Expand Down
4 changes: 4 additions & 0 deletions netbox/dcim/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

urlpatterns = [

# Regions
url(r'^regions/$', RegionListView.as_view(), name='region_list'),
url(r'^regions/(?P<pk>\d+)/$', RegionDetailView.as_view(), name='region_detail'),

# Sites
url(r'^sites/$', SiteListView.as_view(), name='site_list'),
url(r'^sites/(?P<pk>\d+)/$', SiteDetailView.as_view(), name='site_detail'),
Expand Down
22 changes: 21 additions & 1 deletion netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, Interface, InterfaceConnection,
Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Site,
Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
VIRTUAL_IFACE_TYPES,
)
from dcim import filters
Expand All @@ -22,6 +22,26 @@
from . import serializers


#
# Regions
#

class RegionListView(generics.ListAPIView):
"""
List all regions
"""
queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer


class RegionDetailView(generics.RetrieveAPIView):
"""
Retrieve a single region
"""
queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer


#
# Sites
#
Expand Down
13 changes: 12 additions & 1 deletion netbox/dcim/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from utilities.filters import NullableModelMultipleChoiceFilter
from .models import (
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection,
Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Site,
Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
VIRTUAL_IFACE_TYPES,
)

Expand All @@ -18,6 +18,17 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
action='search',
label='Search',
)
region_id = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(),
label='Region (ID)',
)
region = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(),
to_field_name='slug',
label='Region (slug)',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
Expand Down
51 changes: 42 additions & 9 deletions netbox/dcim/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
VIRTUAL_IFACE_TYPES
)

Expand Down Expand Up @@ -63,6 +63,18 @@ def __init__(self, device, *args, **kwargs):
super(DeviceComponentForm, self).__init__(*args, **kwargs)


#
# Regions
#

class RegionForm(BootstrapMixin, forms.ModelForm):
slug = SlugField()

class Meta:
model = Region
fields = ['name', 'slug']


#
# Sites
#
Expand All @@ -73,8 +85,10 @@ class SiteForm(BootstrapMixin, CustomFieldForm):

class Meta:
model = Site
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'contact_name',
'contact_phone', 'contact_email', 'comments']
fields = [
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
'contact_name', 'contact_phone', 'contact_email', 'comments',
]
widgets = {
'physical_address': SmallTextarea(attrs={'rows': 3}),
'shipping_address': SmallTextarea(attrs={'rows': 3}),
Expand All @@ -89,12 +103,22 @@ class Meta:


class SiteFromCSVForm(forms.ModelForm):
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
region = forms.ModelChoiceField(
Region.objects.all(), to_field_name='name', required=False, error_messages={
'invalid_choice': 'Tenant not found.'
}
)
tenant = forms.ModelChoiceField(
Tenant.objects.all(), to_field_name='name', required=False, error_messages={
'invalid_choice': 'Tenant not found.'
}
)

class Meta:
model = Site
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email']
fields = [
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
]


class SiteImportForm(BootstrapMixin, BulkImportForm):
Expand All @@ -103,18 +127,27 @@ class SiteImportForm(BootstrapMixin, BulkImportForm):

class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
region = forms.ModelChoiceField(queryset=Region.objects.all(), required=False)
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN')

class Meta:
nullable_fields = ['tenant', 'asn']
nullable_fields = ['region', 'tenant', 'asn']


class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Site
q = forms.CharField(required=False, label='Search')
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('sites')), to_field_name='slug',
null_option=(0, 'None'))
region = FilterChoiceField(
queryset=Region.objects.annotate(filter_count=Count('sites')),
to_field_name='slug',
null_option=(0, 'None')
)
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('sites')),
to_field_name='slug',
null_option=(0, 'None')
)


#
Expand Down
32 changes: 32 additions & 0 deletions netbox/dcim/migrations/0031_regions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-02-28 14:48
from __future__ import unicode_literals

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


class Migration(migrations.Migration):

dependencies = [
('dcim', '0030_interface_add_lag'),
]

operations = [
migrations.CreateModel(
name='Region',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(unique=True)),
],
options={
'ordering': ['name'],
},
),
migrations.AddField(
model_name='site',
name='region',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='dcim.Region'),
),
]
25 changes: 24 additions & 1 deletion netbox/dcim/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,28 @@
]


#
# Regions
#

@python_2_unicode_compatible
class Region(models.Model):
"""
Sites can be grouped within geographic Regions.
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)

class Meta:
ordering = ['name']

def __str__(self):
return self.name

def get_absolute_url(self):
return "{}?region={}".format(reverse('dcim:site_list'), self.slug)


#
# Sites
#
Expand All @@ -218,7 +240,8 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='sites', on_delete=models.PROTECT)
region = models.ForeignKey('Region', related_name='sites', blank=True, null=True, on_delete=models.SET_NULL)
tenant = models.ForeignKey(Tenant, related_name='sites', blank=True, null=True, on_delete=models.PROTECT)
facility = models.CharField(max_length=50, blank=True)
asn = ASNField(blank=True, null=True, verbose_name='ASN')
physical_address = models.CharField(max_length=200, blank=True)
Expand Down
Loading

0 comments on commit f3b9930

Please sign in to comment.