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:
parent
a18f40554c
commit
82a440f5af
@ -11,11 +11,16 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
import netaddr
|
import netaddr
|
||||||
|
|
||||||
|
|
||||||
|
SEPARATOR_RE = re.compile('[\s,;|]+', re.UNICODE)
|
||||||
|
|
||||||
|
|
||||||
def fieldset(self, *args, **kwargs):
|
def fieldset(self, *args, **kwargs):
|
||||||
"""A helper function for grouping fields based on their names."""
|
"""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)
|
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):
|
class NumberInput(forms.widgets.TextInput):
|
||||||
|
"""A form input for numbers."""
|
||||||
input_type = 'number'
|
input_type = 'number'
|
||||||
|
|
||||||
|
|
||||||
class NumberPickerInput(NumberInput):
|
class NumberPickerInput(NumberInput):
|
||||||
|
"""A form input that is rendered as a big number picker."""
|
||||||
|
|
||||||
def __init__(self, attrs=None):
|
def __init__(self, attrs=None):
|
||||||
default_attrs = {'hr-number-picker': '', 'ng-cloak': '', }
|
default_attrs = {'hr-number-picker': '', 'ng-cloak': '', }
|
||||||
if attrs:
|
if attrs:
|
||||||
@ -39,20 +61,43 @@ class NumberPickerInput(NumberInput):
|
|||||||
|
|
||||||
|
|
||||||
class MACField(forms.fields.Field):
|
class MACField(forms.fields.Field):
|
||||||
|
"""A form field for entering a single MAC address."""
|
||||||
|
|
||||||
def clean(self, value):
|
def clean(self, value):
|
||||||
class mac_dialect(netaddr.mac_eui48):
|
value = super(MACField, self).clean(value)
|
||||||
"""Same validation as Nova uses."""
|
|
||||||
word_fmt = '%.02x'
|
|
||||||
word_sep = ':'
|
|
||||||
try:
|
try:
|
||||||
return str(netaddr.EUI(
|
return normalize_MAC(value)
|
||||||
value.strip(), version=48, dialect=mac_dialect)).upper()
|
except ValueError:
|
||||||
except (netaddr.AddrFormatError, TypeError):
|
|
||||||
raise forms.ValidationError(_(u'Enter a valid MAC address.'))
|
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):
|
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:
|
try:
|
||||||
return str(netaddr.IPNetwork(value, version=4))
|
return str(netaddr.IPNetwork(value, version=4))
|
||||||
except netaddr.AddrFormatError:
|
except netaddr.AddrFormatError:
|
||||||
|
@ -42,10 +42,11 @@ class NodeForm(django.forms.Form):
|
|||||||
widget=django.forms.PasswordInput(
|
widget=django.forms.PasswordInput(
|
||||||
render_value=False, attrs={'class': 'input input-medium'}),
|
render_value=False, attrs={'class': 'input input-medium'}),
|
||||||
)
|
)
|
||||||
mac_address = tuskar_ui.forms.MACField(
|
mac_addresses = tuskar_ui.forms.MultiMACField(
|
||||||
label=_("NIC MAC Address"),
|
label=_("NIC MAC Addresses"),
|
||||||
widget=django.forms.TextInput(attrs={
|
widget=django.forms.Textarea(attrs={
|
||||||
'class': 'input input-medium'
|
'class': 'input input-medium',
|
||||||
|
'rows': '2',
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
cpus = django.forms.IntegerField(
|
cpus = django.forms.IntegerField(
|
||||||
@ -94,7 +95,7 @@ class BaseNodeFormset(django.forms.formsets.BaseFormSet):
|
|||||||
form.cleaned_data.get('cpus'),
|
form.cleaned_data.get('cpus'),
|
||||||
form.cleaned_data.get('memory'),
|
form.cleaned_data.get('memory'),
|
||||||
form.cleaned_data.get('local_disk'),
|
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_username'),
|
||||||
form.cleaned_data.get('ipmi_password'),
|
form.cleaned_data.get('ipmi_password'),
|
||||||
)
|
)
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<h5>Networking</h5>
|
<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>
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div class="span4">
|
<div class="span4">
|
||||||
|
@ -167,13 +167,13 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
|
|||||||
'register_nodes-0-ipmi_address': '127.0.0.1',
|
'register_nodes-0-ipmi_address': '127.0.0.1',
|
||||||
'register_nodes-0-ipmi_username': 'username',
|
'register_nodes-0-ipmi_username': 'username',
|
||||||
'register_nodes-0-ipmi_password': 'password',
|
'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-cpus': '1',
|
||||||
'register_nodes-0-memory': '2',
|
'register_nodes-0-memory': '2',
|
||||||
'register_nodes-0-local_disk': '3',
|
'register_nodes-0-local_disk': '3',
|
||||||
|
|
||||||
'register_nodes-1-ipmi_address': '127.0.0.2',
|
'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-cpus': '4',
|
||||||
'register_nodes-1-memory': '5',
|
'register_nodes-1-memory': '5',
|
||||||
'register_nodes-1-local_disk': '6',
|
'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.
|
request = Node.create.call_args_list[0][0][0] # This is a hack.
|
||||||
self.assertListEqual(Node.create.call_args_list, [
|
self.assertListEqual(Node.create.call_args_list, [
|
||||||
call(request, u'127.0.0.1', 1, 2, 3,
|
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,
|
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)
|
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_address': '127.0.0.1',
|
||||||
'register_nodes-0-ipmi_username': 'username',
|
'register_nodes-0-ipmi_username': 'username',
|
||||||
'register_nodes-0-ipmi_password': 'password',
|
'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-cpus': '1',
|
||||||
'register_nodes-0-memory': '2',
|
'register_nodes-0-memory': '2',
|
||||||
'register_nodes-0-local_disk': '3',
|
'register_nodes-0-local_disk': '3',
|
||||||
|
|
||||||
'register_nodes-1-ipmi_address': '127.0.0.2',
|
'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-cpus': '4',
|
||||||
'register_nodes-1-memory': '5',
|
'register_nodes-1-memory': '5',
|
||||||
'register_nodes-1-local_disk': '6',
|
'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.
|
request = Node.create.call_args_list[0][0][0] # This is a hack.
|
||||||
self.assertListEqual(Node.create.call_args_list, [
|
self.assertListEqual(Node.create.call_args_list, [
|
||||||
call(request, u'127.0.0.1', 1, 2, 3,
|
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,
|
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(
|
self.assertTemplateUsed(
|
||||||
res, 'infrastructure/nodes/register.html')
|
res, 'infrastructure/nodes/register.html')
|
||||||
|
59
tuskar_ui/test/test_forms.py
Normal file
59
tuskar_ui/test/test_forms.py
Normal 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",
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user