diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 2e9335069..dd3de9baf 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -334,7 +334,7 @@ class InterfaceSerializer(serializers.ModelSerializer): class Meta: model = Interface - fields = ['id', 'device', 'name', 'form_factor', 'mgmt_only', 'description', 'is_connected'] + fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected'] class InterfaceNestedSerializer(InterfaceSerializer): @@ -348,7 +348,7 @@ class InterfaceDetailSerializer(InterfaceSerializer): connected_interface = InterfaceSerializer(source='get_connected_interface') class Meta(InterfaceSerializer.Meta): - fields = ['id', 'device', 'name', 'form_factor', 'mgmt_only', 'description', 'is_connected', + fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected', 'connected_interface'] diff --git a/netbox/dcim/fields.py b/netbox/dcim/fields.py new file mode 100644 index 000000000..dafc0ad03 --- /dev/null +++ b/netbox/dcim/fields.py @@ -0,0 +1,44 @@ +from netaddr import EUI, mac_unix_expanded + +from django.core.exceptions import ValidationError +from django.db import models + +from .formfields import MACAddressFormField + + +class mac_unix_expanded_uppercase(mac_unix_expanded): + word_fmt = '%.2X' + + +class MACAddressField(models.Field): + description = "PostgreSQL MAC Address field" + + def python_type(self): + return EUI + + def from_db_value(self, value, expression, connection, context): + return self.to_python(value) + + def to_python(self, value): + if not value: + return value + try: + return EUI(value, dialect=mac_unix_expanded_uppercase) + except ValueError as e: + raise ValidationError(e) + + def db_type(self, connection): + return 'macaddr' + + def get_prep_value(self, value): + if not value: + return None + return str(self.to_python(value)) + + def form_class(self): + return MACAddressFormField + + def formfield(self, **kwargs): + defaults = {'form_class': self.form_class()} + defaults.update(kwargs) + return super(MACAddressField, self).formfield(**defaults) diff --git a/netbox/dcim/fixtures/dcim.json b/netbox/dcim/fixtures/dcim.json index ecea8eb86..89c889b65 100644 --- a/netbox/dcim/fixtures/dcim.json +++ b/netbox/dcim/fixtures/dcim.json @@ -3419,6 +3419,7 @@ "fields": { "device": 3, "name": "em0", + "mac_address": "00-00-00-AA-BB-CC", "form_factor": 800, "mgmt_only": true, "description": "" @@ -3772,6 +3773,7 @@ "device": 4, "name": "em0", "form_factor": 1000, + "mac_address": "ff-ee-dd-33-22-11", "mgmt_only": true, "description": "" } @@ -5686,6 +5688,7 @@ "device": 9, "name": "eth0", "form_factor": 1000, + "mac_address": "44-55-66-77-88-99", "mgmt_only": true, "description": "" } @@ -5865,4 +5868,4 @@ "connection_status": true } } -] \ No newline at end of file +] diff --git a/netbox/dcim/formfields.py b/netbox/dcim/formfields.py new file mode 100644 index 000000000..e3f1ae39d --- /dev/null +++ b/netbox/dcim/formfields.py @@ -0,0 +1,26 @@ +from netaddr import EUI, AddrFormatError + +from django import forms +from django.core.exceptions import ValidationError + + +# +# Form fields +# + +class MACAddressFormField(forms.Field): + default_error_messages = { + 'invalid': "Enter a valid MAC address.", + } + + def to_python(self, value): + if not value: + return None + + if isinstance(value, EUI): + return value + + try: + return EUI(value) + except AddrFormatError: + raise ValidationError("Please specify a valid MAC address.") diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 4c81ae9ff..8a0008696 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -925,7 +925,7 @@ class InterfaceForm(forms.ModelForm, BootstrapMixin): class Meta: model = Interface - fields = ['device', 'name', 'form_factor', 'mgmt_only', 'description'] + fields = ['device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description'] widgets = { 'device': forms.HiddenInput(), } @@ -936,7 +936,7 @@ class InterfaceCreateForm(forms.ModelForm, BootstrapMixin): class Meta: model = Interface - fields = ['name_pattern', 'form_factor', 'mgmt_only', 'description'] + fields = ['name_pattern', 'form_factor', 'mac_address', 'mgmt_only', 'description'] class InterfaceBulkCreateForm(InterfaceCreateForm, BootstrapMixin): diff --git a/netbox/dcim/migrations/0005_auto_20160706_1722.py b/netbox/dcim/migrations/0005_auto_20160706_1722.py new file mode 100644 index 000000000..83a5cf7cb --- /dev/null +++ b/netbox/dcim/migrations/0005_auto_20160706_1722.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-07-06 17:22 +from __future__ import unicode_literals + +import dcim.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0004_auto_20160701_2049'), + ] + + operations = [ + migrations.AddField( + model_name='interface', + name='mac_address', + field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name=b'MAC Address'), + ), + migrations.AlterField( + model_name='devicetype', + name='subdevice_role', + field=models.NullBooleanField(choices=[(None, b'None'), (True, b'Parent'), (False, b'Child')], default=None, help_text=b'Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name=b'Parent/child status'), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 21f775fd0..2f1a62d36 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -10,6 +10,7 @@ from extras.rpc import RPC_CLIENTS from utilities.fields import NullableCharField from utilities.models import CreatedUpdatedModel +from .fields import MACAddressField RACK_FACE_FRONT = 0 RACK_FACE_REAR = 1 @@ -856,6 +857,7 @@ class Interface(models.Model): device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE) name = models.CharField(max_length=30) form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS) + mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address') mgmt_only = models.BooleanField(default=False, verbose_name='OOB Management', help_text="This interface is used only for out-of-band management") description = models.CharField(max_length=100, blank=True) diff --git a/netbox/dcim/tests/test_apis.py b/netbox/dcim/tests/test_apis.py index d646f8f01..1f31fac14 100644 --- a/netbox/dcim/tests/test_apis.py +++ b/netbox/dcim/tests/test_apis.py @@ -529,6 +529,7 @@ class InterfaceTest(APITestCase): 'device', 'name', 'form_factor', + 'mac_address', 'mgmt_only', 'description', 'is_connected' @@ -541,6 +542,7 @@ class InterfaceTest(APITestCase): 'device', 'name', 'form_factor', + 'mac_address', 'mgmt_only', 'description', 'is_connected', diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index e8f3e4d77..220af4f47 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1252,6 +1252,7 @@ def interface_add(request, pk): 'device': device.pk, 'name': name, 'form_factor': form.cleaned_data['form_factor'], + 'mac_address': form.cleaned_data['mac_address'], 'mgmt_only': form.cleaned_data['mgmt_only'], 'description': form.cleaned_data['description'], }) @@ -1339,6 +1340,7 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView): iface_form = forms.InterfaceForm({ 'device': device.pk, 'name': name, + 'mac_address': mac_address, 'form_factor': form.cleaned_data['form_factor'], 'mgmt_only': form.cleaned_data['mgmt_only'], 'description': form.cleaned_data['description'], diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 0bf635e31..203c0b434 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -295,9 +295,6 @@ {% if interfaces or device.device_type.is_network_device %}