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

+ 1 - 0
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
 
 

+ 11 - 0
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

+ 8 - 2
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

+ 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
 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({

+ 8 - 4
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')
 
 
 #

+ 4 - 1
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'