Browse Source

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

Jeremy Stretch 7 years ago
parent
commit
9984238f2a

+ 10 - 2
netbox/dcim/api/serializers.py

@@ -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
 #
 #

+ 1 - 0
netbox/dcim/api/views.py

@@ -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
 
 
 
 

+ 11 - 0
netbox/dcim/filters.py

@@ -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

+ 8 - 2
netbox/dcim/forms.py

@@ -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

+ 26 - 0
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'),
+        ),
+    ]

+ 29 - 6
netbox/dcim/models.py

@@ -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',
-                                     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:
     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({

+ 8 - 4
netbox/dcim/tables.py

@@ -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'}},
-                                    verbose_name='')
+    actions = tables.TemplateColumn(
+        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')
 
 
 
 
 #
 #

+ 4 - 1
netbox/dcim/views.py

@@ -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'