diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 988a2d59f..4c60f7058 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -269,8 +269,8 @@ class DeviceTypeSerializer(CustomFieldModelSerializer): model = DeviceType fields = [ 'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering', - 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields', - 'instance_count', + 'is_console_server', 'is_pdu', 'is_network_device', 'is_rack_furniture', 'subdevice_role', 'comments', + 'custom_fields', 'instance_count', ] @@ -289,7 +289,8 @@ class WritableDeviceTypeSerializer(CustomFieldModelSerializer): model = DeviceType fields = [ 'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering', - 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields', + 'is_console_server', 'is_pdu', 'is_network_device', 'is_rack_furniture', 'subdevice_role', 'comments', + 'custom_fields', ] diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index da8dc0457..cd00bddcf 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -512,8 +512,11 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm): class Meta: model = DeviceType - fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', - 'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments'] + fields = [ + 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', + 'is_pdu', 'is_network_device', 'is_rack_furniture', 'subdevice_role', 'interface_ordering', + 'comments' + ] labels = { 'interface_ordering': 'Order interfaces by', } @@ -562,6 +565,9 @@ class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): is_network_device = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect, label='Is a network device' ) + is_rack_furniture = forms.NullBooleanField( + required=False, widget=BulkEditNullBooleanSelect, label='Is rack furniture' + ) class Meta: nullable_fields = [] @@ -582,6 +588,9 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm): is_network_device = forms.BooleanField( required=False, label='Is a network device', widget=forms.CheckboxInput(attrs={'value': 'True'}) ) + is_rack_furniture = forms.BooleanField( + required=False, label='Is rack furniture', widget=forms.CheckboxInput(attrs={'value': 'True'}) + ) subdevice_role = forms.NullBooleanField( required=False, label='Subdevice role', widget=forms.Select(choices=( ('', '---------'), diff --git a/netbox/dcim/migrations/0056_devicetype_is_rack_furniture.py b/netbox/dcim/migrations/0056_devicetype_is_rack_furniture.py new file mode 100644 index 000000000..35613f2b6 --- /dev/null +++ b/netbox/dcim/migrations/0056_devicetype_is_rack_furniture.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-07-02 03:30 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0055_virtualchassis_ordering'), + ] + + operations = [ + migrations.AddField( + model_name='devicetype', + name='is_rack_furniture', + field=models.BooleanField(default=False, help_text='This type of device is for rack furniture such as shelves or blanks', verbose_name='Is rack furniture'), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 95e9299b8..3a8153c49 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -548,6 +548,11 @@ class DeviceType(models.Model, CustomFieldModel): help_text="This type of device has power outlets") is_network_device = models.BooleanField(default=True, verbose_name='Is a network device', help_text="This type of device has network interfaces") + is_rack_furniture = models.BooleanField( + default=False, + verbose_name="Is rack furniture", + help_text="This type of device is for rack furniture such as shelves or blanks", + ) subdevice_role = models.NullBooleanField(default=None, verbose_name='Parent/child status', choices=SUBDEVICE_ROLE_CHOICES, help_text="Parent devices house child devices in device bays. Select " @@ -557,7 +562,7 @@ class DeviceType(models.Model, CustomFieldModel): csv_headers = [ 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', - 'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments', + 'is_pdu', 'is_network_device', 'is_rack_furniture', 'subdevice_role', 'interface_ordering', 'comments', ] class Meta: @@ -590,6 +595,7 @@ class DeviceType(models.Model, CustomFieldModel): self.is_console_server, self.is_pdu, self.is_network_device, + self.is_rack_furniture, self.get_subdevice_role_display() if self.subdevice_role else None, self.get_interface_ordering_display(), self.comments, @@ -629,6 +635,37 @@ class DeviceType(models.Model, CustomFieldModel): "device before declassifying it as a network device." }) + if self.is_rack_furniture and (self.is_console_server or self.is_pdu or self.is_network_device): + raise ValidationError({ + 'is_rack_furniture': "Rack furniture device types cannot simultaneously be any other device type." + }) + + if self.is_rack_furniture and self.subdevice_role: + raise ValidationError({ + 'is_rack_furniture': "Rack furniture device types cannot be a parent or child device type." + }) + + if self.is_rack_furniture and self.interface_templates.count(): + # Rack furniture device types cannot have management interfaces + raise ValidationError({ + 'is_rack_furniture': "Must delete all interface templates associated with this " + "device before classifying it as rack furniture." + }) + + if self.is_rack_furniture and self.power_port_templates.count(): + # Rack furniture device types cannot have power ports + raise ValidationError({ + 'is_rack_furniture': "Must delete all power ports associated with this " + "device before classifying it as rack furniture." + }) + + if self.is_rack_furniture and self.console_port_templates.count(): + # Rack furniture device types cannot have power ports + raise ValidationError({ + 'is_rack_furniture': "Must delete all console ports associated with this " + "device before classifying it as rack furniture." + }) + if self.subdevice_role != SUBDEVICE_ROLE_PARENT and self.device_bay_templates.count(): raise ValidationError({ 'subdevice_role': "Must delete all device bay templates associated with this device before " @@ -668,6 +705,17 @@ class ConsolePortTemplate(models.Model): def __str__(self): return self.name + def clean(self): + """ + Validate model + """ + + # Rack furniture device types cannot have console ports + if self.device_type.is_rack_furniture: + raise ValidationError( + "Rack furniture device types cannot have console ports." + ) + @python_2_unicode_compatible class ConsoleServerPortTemplate(models.Model): @@ -684,6 +732,14 @@ class ConsoleServerPortTemplate(models.Model): def __str__(self): return self.name + def clean(self): + + # Rack furniture device types cannot have console ports + if self.device_type.is_rack_furniture: + raise ValidationError( + "Rack furniture device types cannot have console server ports." + ) + @python_2_unicode_compatible class PowerPortTemplate(models.Model): @@ -700,6 +756,17 @@ class PowerPortTemplate(models.Model): def __str__(self): return self.name + def clean(self): + """ + Validate model + """ + + # Rack furniture device types cannot have power ports + if self.device_type.is_rack_furniture: + raise ValidationError( + "Rack furniture device types cannot have power ports." + ) + @python_2_unicode_compatible class PowerOutletTemplate(models.Model): @@ -716,6 +783,14 @@ class PowerOutletTemplate(models.Model): def __str__(self): return self.name + def clean(self): + + # Rack furniture device types cannot have power outlets + if self.device_type.is_rack_furniture: + raise ValidationError( + "Rack furniture device types cannot have power outlets." + ) + @python_2_unicode_compatible class InterfaceTemplate(models.Model): @@ -736,6 +811,17 @@ class InterfaceTemplate(models.Model): def __str__(self): return self.name + def clean(self): + """ + Validate model + """ + + # Rack furniture device types cannot have interfaces + if self.device_type.is_rack_furniture: + raise ValidationError( + "Rack furniture device types cannot have interfaces." + ) + @python_2_unicode_compatible class DeviceBayTemplate(models.Model): @@ -1003,6 +1089,11 @@ class Device(CreatedUpdatedModel, CustomFieldModel): pass # Validate primary IP addresses + if self.device_type.is_rack_furniture and (self.primary_ip4 or self.primary_ip6): + # Rack furniture device types cannot have an assigned primary IP + raise ValidationError( + "Rack furniture device types cannot have an assigned primary IP address." + ) vc_interfaces = self.vc_interfaces.all() if self.primary_ip4: if self.primary_ip4.interface in vc_interfaces: @@ -1033,12 +1124,24 @@ class Device(CreatedUpdatedModel, CustomFieldModel): "to {}.".format(self.platform.manufacturer, self.device_type.manufacturer) }) + # Rack furniture device types cannot be assigned to a cluster + if self.cluster and self.device_type.is_rack_furniture: + raise ValidationError({ + 'cluster': "Rack furniture device types cannot be assigned to a cluster." + }) + # 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({ 'cluster': "The assigned cluster belongs to a different site ({})".format(self.cluster.site) }) + # Rack furniture device types cannot be assigned to a virtual chassis + if self.virtual_chassis and self.device_type.is_rack_furniture: + raise ValidationError({ + 'virtual_chassis': "Rack furniture device types cannot be assigned to a virtual chassis." + }) + # Validate virtual chassis assignment if self.virtual_chassis and self.vc_position is None: raise ValidationError({ @@ -1201,6 +1304,17 @@ class ConsolePort(models.Model): self.get_connection_status_display(), ) + def clean(self): + """ + Validate model + """ + + # Rack furniture device types cannot have console ports + if self.device.device_type.is_rack_furniture: + raise ValidationError( + "Rack furniture device types cannot have console ports." + ) + # # Console server ports @@ -1246,7 +1360,6 @@ class ConsoleServerPort(models.Model): device_type.manufacturer, device_type )) - # # Power ports # @@ -1283,6 +1396,17 @@ class PowerPort(models.Model): self.get_connection_status_display(), ) + def clean(self): + """ + Validate model + """ + + # Rack furniture device types cannot have power ports + if self.device.device_type.is_rack_furniture: + raise ValidationError( + "Rack furniture device types cannot have power ports." + ) + # # Power outlets diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 427687ef9..a9ad7e31b 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -326,6 +326,7 @@ class DeviceTypeTable(BaseTable): is_console_server = tables.BooleanColumn(verbose_name='CS') is_pdu = tables.BooleanColumn(verbose_name='PDU') is_network_device = tables.BooleanColumn(verbose_name='Net') + is_rack_furniture = tables.BooleanColumn(verbose_name='RF') subdevice_role = tables.TemplateColumn( template_code=SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role' @@ -339,7 +340,7 @@ class DeviceTypeTable(BaseTable): model = DeviceType fields = ( 'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu', - 'is_network_device', 'subdevice_role', 'instance_count', + 'is_network_device', 'is_rack_furniture', 'subdevice_role', 'instance_count', ) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 5b2cdbd51..2c9f9d6b1 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -260,3 +260,139 @@ class InterfaceTestCase(TestCase): list(Interface.objects.all().order_naturally()), [interface4, interface3, interface5, interface2, interface1, interface6] ) + + +class RackFurnitureDeviceTypeCase(TestCase): + + def setUp(self): + self.manufacturer = Manufacturer.objects.create( + name='Acme', + slug='acme' + ) + self.rack_furniture_type = DeviceType.objects.create( + manufacturer=self.manufacturer, + model='The Best Shelf 9000', + slug='rf9000', + is_network_device=False, + is_rack_furniture=True, + ) + + def test_rack_furniture_cs_port_template(self): + cs_port_template = ConsoleServerPortTemplate( + device_type=self.rack_furniture_type, + name="CS Port Template" + ) + + with self.assertRaises(ValidationError): + cs_port_template.clean() + + def test_rack_furniture_console_port_template(self): + console_port_template = ConsolePortTemplate( + device_type=self.rack_furniture_type, + name="Console Port Template" + ) + + with self.assertRaises(ValidationError): + console_port_template.clean() + + def test_rack_furniture_power_port_template(self): + power_port_template = PowerPortTemplate( + device_type=self.rack_furniture_type, + name="Power Port Template" + ) + + with self.assertRaises(ValidationError): + power_port_template.clean() + + def test_rack_furniture_power_outlet_template(self): + power_outlet_template = PowerOutletTemplate( + device_type=self.rack_furniture_type, + name="Power Outlet Template" + ) + + with self.assertRaises(ValidationError): + power_outlet_template.clean() + + def test_rack_furniture_interface_template(self): + interface_template = InterfaceTemplate( + device_type=self.rack_furniture_type, + name="Interface Template" + ) + + with self.assertRaises(ValidationError): + interface_template.clean() + + +class RackFurnitureDeviceCase(TestCase): + + def setUp(self): + self.manufacturer = Manufacturer.objects.create( + name='Acme', + slug='acme' + ) + self.rack_furniture_type = DeviceType.objects.create( + manufacturer=self.manufacturer, + model='The Best Shelf 9000', + slug='rf9000', + is_network_device=False, + is_rack_furniture=True, + ) + self.site = Site.objects.create( + name="Site 1", + slug="site-1" + ) + self.role = DeviceRole.objects.create( + name='RF', + slug='rf' + ) + self.rack_furniture = Device.objects.create( + name="1U Blank", + device_type=self.rack_furniture_type, + site=self.site, + device_role=self.role, + ) + + def test_rack_furniture_cs_port(self): + cs_port = ConsoleServerPort( + device=self.rack_furniture, + name="CS Port" + ) + + with self.assertRaises(ValidationError): + cs_port.clean() + + def test_rack_furniture_console_port(self): + console_port = ConsolePort( + device=self.rack_furniture, + name="Console Port" + ) + + with self.assertRaises(ValidationError): + console_port.clean() + + def test_rack_furniture_power_port(self): + power_port = PowerPort( + device=self.rack_furniture, + name="Power Port" + ) + + with self.assertRaises(ValidationError): + power_port.clean() + + def test_rack_furniture_power_outlet(self): + power_outlet = PowerOutlet( + device=self.rack_furniture, + name="Power Outlet" + ) + + with self.assertRaises(ValidationError): + power_outlet.clean() + + def test_rack_furniture_interface(self): + interface = Interface( + device=self.rack_furniture, + name="Interface" + ) + + with self.assertRaises(ValidationError): + interface.clean() diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 9aea44229..51f27c089 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -684,3 +684,9 @@ class Service(CreatedUpdatedModel): raise ValidationError("A service cannot be associated with both a device and a virtual machine.") if not self.device and not self.virtual_machine: raise ValidationError("A service must be associated with either a device or a virtual machine.") + + # rack furniture cannot have assgined services + if self.device and self.device.device_type.is_rack_furniture: + raise ValidationError( + "Rack furniture device types cannot have services." + ) diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index 790b665cd..70dc5d819 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -4,7 +4,10 @@ import netaddr from django.core.exceptions import ValidationError from django.test import TestCase, override_settings -from ipam.models import IPAddress, Prefix, VRF +from ipam.models import IPAddress, Prefix, VRF, Service +from ipam.models import IP_PROTOCOL_TCP +from dcim.models import Manufacturer, DeviceType, Device, Site, DeviceRole + class TestPrefix(TestCase): @@ -59,3 +62,43 @@ class TestIPAddress(TestCase): IPAddress.objects.create(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24')) duplicate_ip = IPAddress(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24')) self.assertRaises(ValidationError, duplicate_ip.clean) + + +class ServiceCase(TestCase): + + def test_rack_funiture_no_assign(self): + manufacturer = Manufacturer.objects.create( + name='Acme', + slug='acme' + ) + rack_furniture_type = DeviceType.objects.create( + manufacturer=manufacturer, + model='The Best Shelf 9000', + slug='rf9000', + is_network_device=False, + is_rack_furniture=True, + ) + site = Site.objects.create( + name="Site 1", + slug="site-1" + ) + role = DeviceRole.objects.create( + name='RF', + slug='rf' + ) + rack_furniture = Device.objects.create( + name="1U Blank", + device_type=rack_furniture_type, + site=site, + device_role=role, + ) + + s = Service( + name="Service", + protocol=IP_PROTOCOL_TCP, + port=80, + device=rack_furniture + ) + + with self.assertRaises(ValidationError): + s.clean() diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index e1f367d03..3272ac097 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -304,6 +304,17 @@ class Secret(CreatedUpdatedModel): def get_absolute_url(self): return reverse('secrets:secret', args=[self.pk]) + def clean(self): + """ + Validate model + """ + + # rack furniture cannot have assigned secrets + if self.device.device_type.is_rack_furniture: + raise ValidationError( + "Rack furniture device types cannot have secrets." + ) + def _pad(self, s): """ Prepend the length of the plaintext (2B) and pad with garbage to a multiple of 16B (minimum of 64B). diff --git a/netbox/secrets/tests/test_models.py b/netbox/secrets/tests/test_models.py index 887c048bf..ae713587c 100644 --- a/netbox/secrets/tests/test_models.py +++ b/netbox/secrets/tests/test_models.py @@ -8,6 +8,7 @@ from django.test import TestCase from secrets.hashers import SecretValidationHasher from secrets.models import UserKey, Secret, encrypt_master_key, decrypt_master_key, generate_random_key +from dcim.models import Manufacturer, DeviceType, Device, Site, DeviceRole class UserKeyTestCase(TestCase): @@ -131,3 +132,39 @@ class SecretTestCase(TestCase): self.assertEqual(duplicate_ivs, [], "One or more duplicate IVs found!") duplicate_ciphertexts = [i for i, x in enumerate(ciphertexts) if ciphertexts.count(x) > 1] self.assertEqual(duplicate_ciphertexts, [], "One or more duplicate ciphertexts (first blocks) found!") + + def test_03_rack_funiture_no_assign(self): + manufacturer = Manufacturer.objects.create( + name='Acme', + slug='acme' + ) + rack_furniture_type = DeviceType.objects.create( + manufacturer=manufacturer, + model='The Best Shelf 9000', + slug='rf9000', + is_network_device=False, + is_rack_furniture=True, + ) + site = Site.objects.create( + name="Site 1", + slug="site-1" + ) + role = DeviceRole.objects.create( + name='RF', + slug='rf' + ) + rack_furniture = Device.objects.create( + name="1U Blank", + device_type=rack_furniture_type, + site=site, + device_role=role, + ) + + plaintext = "FooBar123" + secret_key = generate_random_key() + s = Secret(plaintext=plaintext) + s.encrypt(secret_key) + s.device = rack_furniture + + with self.assertRaises(ValidationError): + s.clean() diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index e2253d4f4..5a25bb5ed 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -165,36 +165,38 @@ {{ device.get_status_display }} -