Closes #1744: Allow associating a platform with a specific manufacturer

This commit is contained in:
Jeremy Stretch 2017-12-19 16:15:26 -05:00
parent 02e01b7386
commit 9984238f2a
8 changed files with 97 additions and 15 deletions

View File

@ -426,11 +426,12 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
# Platforms # Platforms
# #
class PlatformSerializer(ValidatedModelSerializer): class PlatformSerializer(serializers.ModelSerializer):
manufacturer = NestedManufacturerSerializer()
class Meta: class Meta:
model = Platform model = Platform
fields = ['id', 'name', 'slug', 'napalm_driver', 'rpc_client'] fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'rpc_client']
class NestedPlatformSerializer(serializers.ModelSerializer): class NestedPlatformSerializer(serializers.ModelSerializer):
@ -441,6 +442,13 @@ class NestedPlatformSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name', 'slug'] fields = ['id', 'url', 'name', 'slug']
class WritablePlatformSerializer(ValidatedModelSerializer):
class Meta:
model = Platform
fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'rpc_client']
# #
# Devices # Devices
# #

View File

@ -225,6 +225,7 @@ class DeviceRoleViewSet(ModelViewSet):
class PlatformViewSet(ModelViewSet): class PlatformViewSet(ModelViewSet):
queryset = Platform.objects.all() queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer serializer_class = serializers.PlatformSerializer
write_serializer_class = serializers.WritablePlatformSerializer
filter_class = filters.PlatformFilter filter_class = filters.PlatformFilter

View File

@ -344,6 +344,17 @@ class DeviceRoleFilter(django_filters.FilterSet):
class PlatformFilter(django_filters.FilterSet): class PlatformFilter(django_filters.FilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
name='manufacturer',
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
name='manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
class Meta: class Meta:
model = Platform model = Platform

View File

@ -677,7 +677,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = Platform model = Platform
fields = ['name', 'slug', 'napalm_driver', 'rpc_client'] fields = ['name', 'slug', 'manufacturer', 'napalm_driver', 'rpc_client']
class PlatformCSVForm(forms.ModelForm): class PlatformCSVForm(forms.ModelForm):
@ -685,9 +685,10 @@ class PlatformCSVForm(forms.ModelForm):
class Meta: class Meta:
model = Platform model = Platform
fields = ['name', 'slug', 'napalm_driver'] fields = ['name', 'slug', 'manufacturer', 'napalm_driver']
help_texts = { help_texts = {
'name': 'Platform name', 'name': 'Platform name',
'manufacturer': 'Manufacturer name',
} }
@ -797,6 +798,11 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
# can be flipped from one face to another. # can be flipped from one face to another.
self.fields['position'].widget.attrs['api-url'] += '&exclude={}'.format(self.instance.pk) self.fields['position'].widget.attrs['api-url'] += '&exclude={}'.format(self.instance.pk)
# Limit platform by manufacturer
self.fields['platform'].queryset = Platform.objects.filter(
Q(manufacturer__isnull=True) | Q(manufacturer=self.instance.device_type.manufacturer)
)
else: else:
# An object that doesn't exist yet can't have any IPs assigned to it # An object that doesn't exist yet can't have any IPs assigned to it

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-12-19 20:56
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0052_virtual_chassis'),
]
operations = [
migrations.AddField(
model_name='platform',
name='manufacturer',
field=models.ForeignKey(blank=True, help_text='Optionally limit this platform to devices of a certain manufacturer', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='platforms', to='dcim.Manufacturer'),
),
migrations.AlterField(
model_name='platform',
name='napalm_driver',
field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices', max_length=50, verbose_name='NAPALM driver'),
),
]

View File

@ -768,16 +768,31 @@ class DeviceRole(models.Model):
@python_2_unicode_compatible @python_2_unicode_compatible
class Platform(models.Model): class Platform(models.Model):
""" """
Platform refers to the software or firmware running on a Device; for example, "Cisco IOS-XR" or "Juniper Junos". Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
specifying an remote procedure call (RPC) client. specifying a NAPALM driver.
""" """
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
napalm_driver = models.CharField(max_length=50, blank=True, verbose_name='NAPALM driver', manufacturer = models.ForeignKey(
help_text="The name of the NAPALM driver to use when interacting with devices.") to='Manufacturer',
rpc_client = models.CharField(max_length=30, choices=RPC_CLIENT_CHOICES, blank=True, related_name='platforms',
verbose_name='Legacy RPC client') blank=True,
null=True,
help_text="Optionally limit this platform to devices of a certain manufacturer"
)
napalm_driver = models.CharField(
max_length=50,
blank=True,
verbose_name='NAPALM driver',
help_text="The name of the NAPALM driver to use when interacting with devices"
)
rpc_client = models.CharField(
max_length=30,
choices=RPC_CLIENT_CHOICES,
blank=True,
verbose_name="Legacy RPC client"
)
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -946,6 +961,14 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
self.primary_ip6), self.primary_ip6),
}) })
# Validate manufacturer/platform
if self.device_type and self.platform:
if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer:
raise ValidationError({
'platform': "The assigned platform is limited to {} device types, but this device's type belongs "
"to {}.".format(self.platform.manufacturer, self.device_type.manufacturer)
})
# A Device can only be assigned to a Cluster in the same Site (or no Site) # A Device can only be assigned to a Cluster in the same Site (or no Site)
if self.cluster and self.cluster.site is not None and self.cluster.site != self.site: if self.cluster and self.cluster.site is not None and self.cluster.site != self.site:
raise ValidationError({ raise ValidationError({

View File

@ -271,13 +271,14 @@ class ManufacturerTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name') name = tables.LinkColumn(verbose_name='Name')
devicetype_count = tables.Column(verbose_name='Device Types') devicetype_count = tables.Column(verbose_name='Device Types')
platform_count = tables.Column(verbose_name='Platforms')
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}}, actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='') verbose_name='')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Manufacturer model = Manufacturer
fields = ('pk', 'name', 'devicetype_count', 'slug', 'actions') fields = ('pk', 'name', 'devicetype_count', 'platform_count', 'slug', 'actions')
# #
@ -389,12 +390,15 @@ class PlatformTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name') name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices') device_count = tables.Column(verbose_name='Devices')
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}}, actions = tables.TemplateColumn(
verbose_name='') template_code=PLATFORM_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Platform model = Platform
fields = ('pk', 'name', 'device_count', 'slug', 'napalm_driver', 'actions') fields = ('pk', 'name', 'manufacturer', 'device_count', 'slug', 'napalm_driver', 'actions')
# #

View File

@ -453,7 +453,10 @@ class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class ManufacturerListView(ObjectListView): class ManufacturerListView(ObjectListView):
queryset = Manufacturer.objects.annotate(devicetype_count=Count('device_types')) queryset = Manufacturer.objects.annotate(
devicetype_count=Count('device_types', distinct=True),
platform_count=Count('platforms', distinct=True),
)
table = tables.ManufacturerTable table = tables.ManufacturerTable
template_name = 'dcim/manufacturer_list.html' template_name = 'dcim/manufacturer_list.html'