diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 0d9b61964..19985132c 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -426,11 +426,12 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer): # Platforms # -class PlatformSerializer(ValidatedModelSerializer): +class PlatformSerializer(serializers.ModelSerializer): + manufacturer = NestedManufacturerSerializer() class Meta: model = Platform - fields = ['id', 'name', 'slug', 'napalm_driver', 'rpc_client'] + fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'rpc_client'] class NestedPlatformSerializer(serializers.ModelSerializer): @@ -441,6 +442,13 @@ class NestedPlatformSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'name', 'slug'] +class WritablePlatformSerializer(ValidatedModelSerializer): + + class Meta: + model = Platform + fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'rpc_client'] + + # # Devices # diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index a3ef98a15..924e39d73 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -225,6 +225,7 @@ class DeviceRoleViewSet(ModelViewSet): class PlatformViewSet(ModelViewSet): queryset = Platform.objects.all() serializer_class = serializers.PlatformSerializer + write_serializer_class = serializers.WritablePlatformSerializer filter_class = filters.PlatformFilter diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index e038da5ca..f12f8e3b9 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -344,6 +344,17 @@ class DeviceRoleFilter(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: model = Platform diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index b28a1f118..1a7c3837b 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -677,7 +677,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm): class Meta: model = Platform - fields = ['name', 'slug', 'napalm_driver', 'rpc_client'] + fields = ['name', 'slug', 'manufacturer', 'napalm_driver', 'rpc_client'] class PlatformCSVForm(forms.ModelForm): @@ -685,9 +685,10 @@ class PlatformCSVForm(forms.ModelForm): class Meta: model = Platform - fields = ['name', 'slug', 'napalm_driver'] + fields = ['name', 'slug', 'manufacturer', 'napalm_driver'] help_texts = { 'name': 'Platform name', + 'manufacturer': 'Manufacturer name', } @@ -797,6 +798,11 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): # can be flipped from one face to another. 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: # An object that doesn't exist yet can't have any IPs assigned to it diff --git a/netbox/dcim/migrations/0053_platform_manufacturer.py b/netbox/dcim/migrations/0053_platform_manufacturer.py new file mode 100644 index 000000000..62797716e --- /dev/null +++ b/netbox/dcim/migrations/0053_platform_manufacturer.py @@ -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'), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 9a86cf8c3..b35d0b078 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -768,16 +768,31 @@ class DeviceRole(models.Model): @python_2_unicode_compatible 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 - specifying an remote procedure call (RPC) client. + specifying a NAPALM driver. """ name = models.CharField(max_length=50, unique=True) slug = models.SlugField(unique=True) - 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') + manufacturer = models.ForeignKey( + to='Manufacturer', + related_name='platforms', + 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: ordering = ['name'] @@ -946,6 +961,14 @@ class Device(CreatedUpdatedModel, CustomFieldModel): 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) if self.cluster and self.cluster.site is not None and self.cluster.site != self.site: raise ValidationError({ diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index c416f3c70..de99a98bd 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -271,13 +271,14 @@ class ManufacturerTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn(verbose_name='Name') devicetype_count = tables.Column(verbose_name='Device Types') + platform_count = tables.Column(verbose_name='Platforms') slug = tables.Column(verbose_name='Slug') actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='') class Meta(BaseTable.Meta): 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') device_count = tables.Column(verbose_name='Devices') slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}}, - verbose_name='') + actions = tables.TemplateColumn( + template_code=PLATFORM_ACTIONS, + attrs={'td': {'class': 'text-right'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = Platform - fields = ('pk', 'name', 'device_count', 'slug', 'napalm_driver', 'actions') + fields = ('pk', 'name', 'manufacturer', 'device_count', 'slug', 'napalm_driver', 'actions') # diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 4e9cdf521..bc3443a7a 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -453,7 +453,10 @@ class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # 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 template_name = 'dcim/manufacturer_list.html'