Use a MultiMACField in the Register Nodes form

Make it possible to specify multiple MAC addresses and use
a textarea field for entering them.

Change-Id: I1a2241d591f4174e08e7c7cc560f4e43facc6457
This commit is contained in:
Radomir Dopieralski 2014-07-11 15:00:51 +02:00
parent a18f40554c
commit 82a440f5af
5 changed files with 127 additions and 22 deletions

View File

@ -11,11 +11,16 @@
# License for the specific language governing permissions and limitations
# under the License.
import re
from django import forms
from django.utils.translation import ugettext_lazy as _
import netaddr
SEPARATOR_RE = re.compile('[\s,;|]+', re.UNICODE)
def fieldset(self, *args, **kwargs):
"""A helper function for grouping fields based on their names."""
@ -26,11 +31,28 @@ def fieldset(self, *args, **kwargs):
yield forms.forms.BoundField(self, self.fields[name], name)
class MACDialect(netaddr.mac_eui48):
"""For validating MAC addresses. Same validation as Nova uses."""
word_fmt = '%.02x'
word_sep = ':'
def normalize_MAC(value):
try:
return str(netaddr.EUI(
value.strip(), version=48, dialect=MACDialect)).upper()
except (netaddr.AddrFormatError, TypeError):
raise ValueError('Invalid MAC address')
class NumberInput(forms.widgets.TextInput):
"""A form input for numbers."""
input_type = 'number'
class NumberPickerInput(NumberInput):
"""A form input that is rendered as a big number picker."""
def __init__(self, attrs=None):
default_attrs = {'hr-number-picker': '', 'ng-cloak': '', }
if attrs:
@ -39,20 +61,43 @@ class NumberPickerInput(NumberInput):
class MACField(forms.fields.Field):
"""A form field for entering a single MAC address."""
def clean(self, value):
class mac_dialect(netaddr.mac_eui48):
"""Same validation as Nova uses."""
word_fmt = '%.02x'
word_sep = ':'
value = super(MACField, self).clean(value)
try:
return str(netaddr.EUI(
value.strip(), version=48, dialect=mac_dialect)).upper()
except (netaddr.AddrFormatError, TypeError):
return normalize_MAC(value)
except ValueError:
raise forms.ValidationError(_(u'Enter a valid MAC address.'))
class NetworkField(forms.fields.Field):
class MultiMACField(forms.fields.Field):
"""A form field for entering multiple MAC addresses.
The individual MAC addresses can be separated by any whitespace,
commas, semicolons or pipe characters.
Gives a string of normalized MAC addresses separated by spaces.
"""
def clean(self, value):
value = super(MultiMACField, self).clean(value)
try:
macs = []
for mac in SEPARATOR_RE.split(value):
if mac:
macs.append(normalize_MAC(mac))
return ' '.join(macs)
except ValueError:
raise forms.ValidationError(
_(u'%r is not a valid MAC address.') % mac)
class NetworkField(forms.fields.Field):
"""A form field for entering a network specification with a mask."""
def clean(self, value):
value = super(NetworkField, self).clean(value)
try:
return str(netaddr.IPNetwork(value, version=4))
except netaddr.AddrFormatError:

View File

@ -42,10 +42,11 @@ class NodeForm(django.forms.Form):
widget=django.forms.PasswordInput(
render_value=False, attrs={'class': 'input input-medium'}),
)
mac_address = tuskar_ui.forms.MACField(
label=_("NIC MAC Address"),
widget=django.forms.TextInput(attrs={
'class': 'input input-medium'
mac_addresses = tuskar_ui.forms.MultiMACField(
label=_("NIC MAC Addresses"),
widget=django.forms.Textarea(attrs={
'class': 'input input-medium',
'rows': '2',
}),
)
cpus = django.forms.IntegerField(
@ -94,7 +95,7 @@ class BaseNodeFormset(django.forms.formsets.BaseFormSet):
form.cleaned_data.get('cpus'),
form.cleaned_data.get('memory'),
form.cleaned_data.get('local_disk'),
form.cleaned_data['mac_address'],
form.cleaned_data['mac_addresses'].split(),
form.cleaned_data.get('ipmi_username'),
form.cleaned_data.get('ipmi_password'),
)

View File

@ -13,7 +13,7 @@
</div>
<div class="row-fluid">
<h5>Networking</h5>
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.mac_address required=True %}
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.mac_addresses required=True %}
</div>
<div class="row-fluid">
<div class="span4">

View File

@ -167,13 +167,13 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
'register_nodes-0-ipmi_address': '127.0.0.1',
'register_nodes-0-ipmi_username': 'username',
'register_nodes-0-ipmi_password': 'password',
'register_nodes-0-mac_address': 'de:ad:be:ef:ca:fe',
'register_nodes-0-mac_addresses': 'de:ad:be:ef:ca:fe',
'register_nodes-0-cpus': '1',
'register_nodes-0-memory': '2',
'register_nodes-0-local_disk': '3',
'register_nodes-1-ipmi_address': '127.0.0.2',
'register_nodes-1-mac_address': 'de:ad:be:ef:ca:ff',
'register_nodes-1-mac_addresses': 'de:ad:be:ef:ca:ff',
'register_nodes-1-cpus': '4',
'register_nodes-1-memory': '5',
'register_nodes-1-local_disk': '6',
@ -186,9 +186,9 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
request = Node.create.call_args_list[0][0][0] # This is a hack.
self.assertListEqual(Node.create.call_args_list, [
call(request, u'127.0.0.1', 1, 2, 3,
'DE:AD:BE:EF:CA:FE', u'username', u'password'),
['DE:AD:BE:EF:CA:FE'], u'username', u'password'),
call(request, u'127.0.0.2', 4, 5, 6,
'DE:AD:BE:EF:CA:FF', None, None),
['DE:AD:BE:EF:CA:FF'], None, None),
])
self.assertRedirectsNoFollow(res, INDEX_URL)
@ -201,13 +201,13 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
'register_nodes-0-ipmi_address': '127.0.0.1',
'register_nodes-0-ipmi_username': 'username',
'register_nodes-0-ipmi_password': 'password',
'register_nodes-0-mac_address': 'de:ad:be:ef:ca:fe',
'register_nodes-0-mac_addresses': 'de:ad:be:ef:ca:fe',
'register_nodes-0-cpus': '1',
'register_nodes-0-memory': '2',
'register_nodes-0-local_disk': '3',
'register_nodes-1-ipmi_address': '127.0.0.2',
'register_nodes-1-mac_address': 'de:ad:be:ef:ca:ff',
'register_nodes-1-mac_addresses': 'de:ad:be:ef:ca:ff',
'register_nodes-1-cpus': '4',
'register_nodes-1-memory': '5',
'register_nodes-1-local_disk': '6',
@ -220,9 +220,9 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
request = Node.create.call_args_list[0][0][0] # This is a hack.
self.assertListEqual(Node.create.call_args_list, [
call(request, u'127.0.0.1', 1, 2, 3,
'DE:AD:BE:EF:CA:FE', u'username', u'password'),
['DE:AD:BE:EF:CA:FE'], u'username', u'password'),
call(request, u'127.0.0.2', 4, 5, 6,
'DE:AD:BE:EF:CA:FF', None, None),
['DE:AD:BE:EF:CA:FF'], None, None),
])
self.assertTemplateUsed(
res, 'infrastructure/nodes/register.html')

View File

@ -0,0 +1,59 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
from tuskar_ui import forms
from tuskar_ui.test import helpers as test
class MultiMACFieldTests(test.TestCase):
def test_empty(self):
field = forms.MultiMACField(required=False)
cleaned = field.clean("")
self.assertEqual(cleaned, "")
def test_required(self):
field = forms.MultiMACField(required=True)
with self.assertRaises(forms.forms.ValidationError) as raised:
field.clean("")
self.assertEqual(unicode(raised.exception.messages[0]),
unicode(_('This field is required.')))
def test_malformed(self):
field = forms.MultiMACField(required=True)
with self.assertRaises(forms.forms.ValidationError) as raised:
field.clean("de.ad:be.ef:ca.fe")
self.assertEqual(
unicode(raised.exception.messages[0]),
unicode(_(u"'de.ad:be.ef:ca.fe' is not a valid MAC address.")),
)
def test_single(self):
field = forms.MultiMACField(required=False)
cleaned = field.clean("de:AD:be:ef:Ca:FE")
self.assertEqual(cleaned, "DE:AD:BE:EF:CA:FE")
def test_multiple(self):
field = forms.MultiMACField(required=False)
cleaned = field.clean(
"de:AD:be:ef:Ca:FC, de:AD:be:ef:Ca:FD de:AD:be:ef:Ca:FE\n"
"de:AD:be:ef:Ca:FF",
)
self.assertEqual(
cleaned,
"DE:AD:BE:EF:CA:FC DE:AD:BE:EF:CA:FD DE:AD:BE:EF:CA:FE "
"DE:AD:BE:EF:CA:FF",
)