Merge lp:~gmb/maas/json-schema-part-1 into lp:~maas-committers/maas/trunk

Proposed by Graham Binns
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 1921
Proposed branch: lp:~gmb/maas/json-schema-part-1
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 531 lines (+379/-98)
4 files modified
required-packages/base (+1/-0)
src/maasserver/power_parameters.py (+227/-89)
src/maasserver/tests/test_power_parameters.py (+146/-4)
src/provisioningserver/enum.py (+5/-5)
To merge this branch: bzr merge lp:~gmb/maas/json-schema-part-1
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Julian Edwards (community) Approve
Review via email: mp+205305@code.launchpad.net

Commit message

Convert the definition of power type parameters from straight Python to something that's JSON-schema-validatable. This is the first step in making it possible for hardware enablement drivers to specify what power parameters they accept.

Description of the change

After a lot of dancing around, this is the first part of the work to making power type parameters specifiable in JSON.

In order to do this I've:

 - Defined a JSON schema to describe how power type parameters should look
   (this is split up so that we can validate separate parts of it individually)
 - Created a JSON-validatable version of the existing POWER_TYPE_PARAMETERS dict
 - Created a function, get_power_type_parameters_from_json() to convert the
   JSON power type parameters to something similar to the existing
   POWER_TYPE_PARAMETERS dict.
 - Use get_power_type_parameters_from_json() to create POWER_TYPE_PARAMETERS.
   This is done once at the bottom of maasserver.power_parameters so that we're
   not parsing and validating JSON all the time.

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Friday 07 Feb 2014 09:20:37 you wrote:
> Commit message:
> Convert the definition of power type parameters from straight Python to
> something that's JSON-schema-validatable.

Can you add something about why this is happening please.

> After a lot of dancing around, this is the first part of the work to making
> power type parameters specifiable in JSON.

\o/

> In order to do this I've:
>
> - Defined a JSON schema to describe how power type parameters should look
> (this is split up so that we can validate separate parts of it
> individually) - Created a JSON-validatable version of the existing
> POWER_TYPE_PARAMETERS dict - Created a function,
> get_power_type_parameters_from_json() to convert the JSON power type
> parameters to something similar to the existing POWER_TYPE_PARAMETERS dict.
> - Use get_power_type_parameters_from_json() to create
> POWER_TYPE_PARAMETERS. This is done once at the bottom of
> maasserver.power_parameters so that we're not parsing and validating JSON
> all the time.

This is mostly looking *great*. I'd like to get Gavin to take a peek to make
sure it matches his grand vision, but it looks like it's on the right track to
me.

Some nits:

[1] You're missing :param:, :type: and :return: info on the docstrings for the
new functions.

(FWIW My vim plugin reads the :type: and offers better auto-completions :)

[2] I realise it's early days, but the FIELD_TYPE_MAPPINGS are going to form
part of the API with the drivers. I think putting these in their own module
would be better (like errno), so we can do something like
powerfield.mac_address, for example. This also then forms a firm contract
with the driver, which consumes the API. (It also makes it easier to install
for packaging and is something we should have done for enums so that maas api
clients can use them)

[3] jsonschema. MY EYES!
OK this ones not a nit :)

[4] Missing tests for make_json_field.

cheers.

Revision history for this message
Julian Edwards (julian-edwards) :
review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

Looks grand :)

[1]

Couple of minor things:

+def make_json_field(name, label, field_type=None, choices=None, default=None,
+                    required=False):

Dosnae matter much, but try using the following form for indentation:

def make_json_field(
        name, label, field_type=None, choices=None, default=None,
        required=False):
    ... code here ...

+    """Helper function for build a JSON power type parameters field."""

s/for/to/

review: Approve
Revision history for this message
Graham Binns (gmb) wrote :

On 10 February 2014 02:24, Julian Edwards <email address hidden> wrote:
> On Friday 07 Feb 2014 09:20:37 you wrote:
>> Commit message:
>> Convert the definition of power type parameters from straight Python to
>> something that's JSON-schema-validatable.
>
> Can you add something about why this is happening please.

Yeah, sorry. I've gotten into a laziness habit with commit messages...

>> After a lot of dancing around, this is the first part of the work to making
>> power type parameters specifiable in JSON.
>
> \o/
>
> This is mostly looking *great*. I'd like to get Gavin to take a peek to make
> sure it matches his grand vision, but it looks like it's on the right track to
> me.

+1. Will bug Gavin.

> Some nits:
>
> [1] You're missing :param:, :type: and :return: info on the docstrings for the
> new functions.
>
> (FWIW My vim plugin reads the :type: and offers better auto-completions :)

Which vim plugin loveliness is it of which you speak? Fixed.

>
> [2] I realise it's early days, but the FIELD_TYPE_MAPPINGS are going to form
> part of the API with the drivers. I think putting these in their own module
> would be better (like errno), so we can do something like
> powerfield.mac_address, for example. This also then forms a firm contract
> with the driver, which consumes the API. (It also makes it easier to install
> for packaging and is something we should have done for enums so that maas api
> clients can use them)

Okay, I'm a bit lost as to what you mean here... I was with you up
until "their own module" but then you started talking about packaging
and I got confused. Are you talking about just adding a module under
src/maasserver (which is what I think you mean)?

Anyway, in the interests of getting this landed I'll do that in a
followup branch once I've got clarity.

> [3] jsonschema. MY EYES!
> OK this ones not a nit :)

DEAR GOD YES.

> [4] Missing tests for make_json_field.
>

Fixed. Cheers for the review.

Revision history for this message
Graham Binns (gmb) wrote :

On 10 February 2014 10:25, Gavin Panella <email address hidden> wrote:
> Couple of minor things:
>
> +def make_json_field(name, label, field_type=None, choices=None, default=None,
> + required=False):
>
> Dosnae matter much, but try using the following form for indentation:

Done. Is that the standard form for MAAS? I was doing it the LP way
but this looks better.

> + """Helper function for build a JSON power type parameters field."""

Actually should have been s/build/building/. Fixed.

Revision history for this message
Raphaël Badin (rvb) wrote :

This introduces a new dependency: python-jsonschema. This means the packaging branch needs to be updated.

Next time, let's try to do this before landing the branch on lp:maas please. Otherwise it breaks the package, causes the integration tests to fail, and whoever ends up looking into it is wasting his time.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Monday 10 Feb 2014 12:06:29 you wrote:
> > (FWIW My vim plugin reads the :type: and offers better auto-completions :)
>
> Which vim plugin loveliness is it of which you speak? Fixed.

https://github.com/Valloric/YouCompleteMe

It is seriously *awesome*.

> > [2] I realise it's early days, but the FIELD_TYPE_MAPPINGS are going to
> > form part of the API with the drivers. I think putting these in their
> > own module would be better (like errno), so we can do something like
> > powerfield.mac_address, for example. This also then forms a firm contract
> > with the driver, which consumes the API. (It also makes it easier to
> > install for packaging and is something we should have done for enums so
> > that maas api clients can use them)
>
> Okay, I'm a bit lost as to what you mean here... I was with you up
> until "their own module" but then you started talking about packaging
> and I got confused. Are you talking about just adding a module under
> src/maasserver (which is what I think you mean)?

Ok so these are a bit like the enums in that third party code (drivers) are
going to need access. Putting it in its own module will help.

For packaging, I was thinking more about maas-cli because we could install an
enums.py (for example) along with the cli itself. This doesn't translate too
well for the drivers since they are going to be part and parcel of maas
itself; it's just that we want to make life as easy as possible for driver
writers so I'd prefer if we had all things that drivers will need to import in
a separate and convenience place.

Hope that makes sense :)

> Anyway, in the interests of getting this landed I'll do that in a
> followup branch once I've got clarity.

Naw worries.

It could probably even wait until Gavin starts writing the API layer.

> Fixed. Cheers for the review.

Pleasure.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Monday 10 Feb 2014 13:17:31 you wrote:
> This introduces a new dependency: python-jsonschema. This means the
> packaging branch needs to be updated.
>
> Next time, let's try to do this before landing the branch on lp:maas please.
> Otherwise it breaks the package, causes the integration tests to fail, and
> whoever ends up looking into it is wasting his time.

Argh, I meant to say something about that when I read the branch and totally
forgot.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'required-packages/base'
2--- required-packages/base 2014-01-20 09:39:00 +0000
3+++ required-packages/base 2014-02-10 12:07:12 +0000
4@@ -24,6 +24,7 @@
5 python-docutils
6 python-formencode
7 python-httplib2
8+python-jsonschema
9 python-lockfile
10 python-netaddr
11 python-netifaces
12
13=== modified file 'src/maasserver/power_parameters.py'
14--- src/maasserver/power_parameters.py 2014-01-21 06:08:12 +0000
15+++ src/maasserver/power_parameters.py 2014-02-10 12:07:12 +0000
16@@ -33,6 +33,7 @@
17
18
19 from django import forms
20+from jsonschema import validate
21 from maasserver.config_forms import DictCharField
22 from maasserver.fields import MACAddressFormField
23 from provisioningserver.enum import (
24@@ -40,92 +41,229 @@
25 IPMI_DRIVER_CHOICES,
26 )
27
28-# FIXME: These should come from a hardware driver.
29-POWER_TYPE_PARAMETERS = {
30- # Empty type, for the case where nothing is entered in the form yet.
31- '': DictCharField(
32- [], required=False, skip_check=True),
33- 'ether_wake': DictCharField(
34- [
35- ('mac_address',
36- MACAddressFormField(label="MAC Address", required=False)),
37- ],
38- required=False,
39- skip_check=True),
40- 'virsh': DictCharField(
41- [
42- ('power_address',
43- forms.CharField(label="Address", required=False)),
44- ('power_id',
45- forms.CharField(label="Power ID", required=False)),
46- ],
47- required=False,
48- skip_check=True),
49- 'fence_cdu': DictCharField(
50- [
51- ('power_id',
52- forms.CharField(label="Power ID", required=False)),
53- ('power_address',
54- forms.CharField(label="IP Address or Hostname", required=False)),
55- ('power_user',
56- forms.CharField(label="Username", required=False)),
57- ('power_pass',
58- forms.CharField(label="Password", required=False)),
59- ],
60- required=False,
61- skip_check=True),
62- 'ipmi': DictCharField(
63- [
64- ('power_driver',
65- forms.ChoiceField(
66- label="Driver", required=False,
67- choices=IPMI_DRIVER_CHOICES,
68- initial=IPMI_DRIVER.DEFAULT)),
69- ('power_address',
70- forms.CharField(label="IP Address or Hostname", required=False)),
71- ('power_user',
72- forms.CharField(label="Username", required=False)),
73- ('power_pass',
74- forms.CharField(label="Password", required=False)),
75- ],
76- required=False,
77- skip_check=True),
78- 'moonshot': DictCharField(
79- [
80- ('power_address',
81- forms.CharField(
82- label="IP Address or Hostname", required=False)),
83- ('power_user',
84- forms.CharField(label="Username", required=False)),
85- ('power_pass',
86- forms.CharField(label="Password", required=False)),
87- ('power_hwaddress',
88- forms.CharField(label="Hardware Address", required=False)),
89- ],
90- required=False,
91- skip_check=True),
92- 'sm15k': DictCharField(
93- [
94- ('power_address',
95- forms.CharField(
96- label="IP Address or Hostname", required=False)),
97- ('power_user',
98- forms.CharField(label="Username", required=False)),
99- ('power_pass',
100- forms.CharField(label="Password", required=False)),
101- ('system_id',
102- forms.CharField(label="System ID", required=False)),
103- ],
104- required=False,
105- skip_check=True),
106- 'amt': DictCharField(
107- [
108- ('power_address',
109- forms.CharField(
110- label="IP Address or Hostname", required=False)),
111- ('power_pass',
112- forms.CharField(label="Password", required=False)),
113- ],
114- required=False,
115- skip_check=True),
116-}
117+
118+# Represent the Django choices format as JSON; an array of 2-item
119+# arrays.
120+CHOICE_FIELD_SCHEMA = {
121+ 'type': 'array',
122+ 'items': {
123+ 'title': "Power type paramter field choice",
124+ 'type': 'array',
125+ 'minItems': 2,
126+ 'maxItems': 2,
127+ 'uniqueItems': True,
128+ 'items': {
129+ 'type': 'string',
130+ }
131+ },
132+}
133+
134+
135+POWER_TYPE_PARAMETER_FIELD_SCHEMA = {
136+ 'title': "Power type parameter field",
137+ 'type': 'object',
138+ 'properties': {
139+ 'name': {
140+ 'type': 'string',
141+ },
142+ 'field_type': {
143+ 'type': 'string',
144+ },
145+ 'label': {
146+ 'type': 'string',
147+ },
148+ 'required': {
149+ 'type': 'boolean',
150+ },
151+ 'choices': CHOICE_FIELD_SCHEMA,
152+ 'default': {
153+ 'type': 'string',
154+ },
155+ },
156+ 'required': ['field_type', 'label', 'required'],
157+}
158+
159+
160+# A basic JSON schema for what power type parameters should look like.
161+JSON_POWER_TYPE_PARAMETERS_SCHEMA = {
162+ 'title': "Power parameters set",
163+ 'type': 'array',
164+ 'items': {
165+ 'title': "Power type parameters",
166+ 'type': 'object',
167+ 'properties': {
168+ 'fields': {
169+ 'type': 'array',
170+ 'items': POWER_TYPE_PARAMETER_FIELD_SCHEMA,
171+ },
172+ },
173+ 'required': ['fields'],
174+ },
175+}
176+
177+
178+def make_json_field(
179+ name, label, field_type=None, choices=None, default=None,
180+ required=False):
181+ """Helper function for building a JSON power type parameters field.
182+
183+ :param name: The name of the field.
184+ :type name: string
185+ :param label: The label to be presented to the user for this field.
186+ :type label: string
187+ :param field_type: The type of field to create. Can be one of
188+ (string, choice, mac_address). Defaults to string.
189+ :type field_type: string.
190+ :param choices: The collection of choices to present to the user.
191+ Needs to be structured as a list of lists, otherwise
192+ make_json_field() will raise a ValidationError.
193+ :type list:
194+ :param default: The default value for the field.
195+ :type default: string
196+ :param required: Whether or not a value for the field is required.
197+ :type required: boolean
198+ """
199+ if field_type not in ('string', 'mac_address', 'choice'):
200+ field_type = 'string'
201+ if choices is None:
202+ choices = []
203+ validate(choices, CHOICE_FIELD_SCHEMA)
204+ if default is None:
205+ default = ""
206+ field = {
207+ 'name': name,
208+ 'label': label,
209+ 'required': required,
210+ 'field_type': field_type,
211+ 'choices': choices,
212+ 'default': default,
213+ }
214+ return field
215+
216+
217+# FIXME this should all be produced by hardware drivers, not defined statically
218+# like this.
219+JSON_POWER_TYPE_PARAMETERS = [
220+ {
221+ 'name': 'ether_wake',
222+ 'fields': [
223+ make_json_field(
224+ 'mac_address', "MAC Address", field_type='mac_address'),
225+ ],
226+ },
227+ {
228+ 'name': 'virsh',
229+ 'fields': [
230+ make_json_field('power_address', "Power address"),
231+ make_json_field('power_id', "Power ID"),
232+ ],
233+ },
234+ {
235+ 'name': 'fence_cdu',
236+ 'fields': [
237+ make_json_field('power_address', "Power address"),
238+ make_json_field('power_id', "Power ID"),
239+ make_json_field('power_user', "Power user"),
240+ make_json_field('power_pass', "Power password"),
241+ ],
242+ },
243+ {
244+ 'name': 'ipmi',
245+ 'fields': [
246+ make_json_field(
247+ 'power_driver', "Power driver", field_type='choice',
248+ choices=IPMI_DRIVER_CHOICES, default=IPMI_DRIVER.DEFAULT),
249+ make_json_field('power_address', "Power address"),
250+ make_json_field('power_user', "Power user"),
251+ make_json_field('power_pass', "Power password"),
252+ ],
253+ },
254+ {
255+ 'name': 'moonshot',
256+ 'fields': [
257+ make_json_field('power_address', "Power address"),
258+ make_json_field('power_user', "Power user"),
259+ make_json_field('power_pass', "Power password"),
260+ make_json_field('power_hwaddress', "Power hardware address"),
261+ ],
262+ },
263+ {
264+ 'name': 'sm15k',
265+ 'fields': [
266+ make_json_field('system_id', "System ID"),
267+ make_json_field('power_address', "Power address"),
268+ make_json_field('power_user', "Power user"),
269+ make_json_field('power_pass', "Power password"),
270+ ],
271+ },
272+ {
273+ 'name': 'amt',
274+ 'fields': [
275+ make_json_field('power_address', "Power address"),
276+ make_json_field('power_pass', "Power password"),
277+ ],
278+ },
279+]
280+
281+
282+FIELD_TYPE_MAPPINGS = {
283+ 'string': forms.CharField,
284+ 'mac_address': MACAddressFormField,
285+ 'choice': forms.ChoiceField,
286+}
287+
288+
289+def make_form_field(json_field):
290+ """Build a Django form field based on the JSON spec.
291+
292+ :param json_field: The JSON-specified field to convert into a valid
293+ Djangoism.
294+ :type json_field: dict
295+ :return: The correct Django form field for the field type, as
296+ specified in FIELD_TYPE_MAPPINGS.
297+ """
298+ field_class = FIELD_TYPE_MAPPINGS.get(
299+ json_field['field_type'], forms.CharField)
300+ if json_field['field_type'] == 'choice':
301+ extra_parameters = {
302+ 'choices': json_field['choices'],
303+ 'initial': json_field['default'],
304+ }
305+ else:
306+ extra_parameters = {}
307+ form_field = field_class(
308+ label=json_field['label'], required=json_field['required'],
309+ **extra_parameters)
310+ return form_field
311+
312+
313+def get_power_type_parameters_from_json(json_power_type_parameters):
314+ """Return power type parameters.
315+
316+ :param json_power_type_parameters: Power type parameters expressed
317+ as a JSON string or as set of JSONSchema-verifiable objects.
318+ Will be validated using jsonschema.validate().
319+ :type json_power_type_parameters: JSON string or iterable.
320+ :return: A dict of power parameters for all power types, indexed by
321+ power type name.
322+ """
323+ validate(json_power_type_parameters, JSON_POWER_TYPE_PARAMETERS_SCHEMA)
324+ power_parameters = {
325+ # Empty type, for the case where nothing is entered in the form yet.
326+ '': DictCharField(
327+ [], required=False, skip_check=True),
328+ }
329+ for power_type in json_power_type_parameters:
330+ fields = []
331+ for json_field in power_type['fields']:
332+ fields.append((
333+ json_field['name'], make_form_field(json_field)))
334+ params = DictCharField(fields, required=False, skip_check=True)
335+ power_parameters[power_type['name']] = params
336+ return power_parameters
337+
338+
339+# We do this once because there's no point re-parsing the JSON every
340+# time we need to look up the power type parameters code.
341+POWER_TYPE_PARAMETERS = get_power_type_parameters_from_json(
342+ JSON_POWER_TYPE_PARAMETERS)
343
344=== modified file 'src/maasserver/tests/test_power_parameters.py'
345--- src/maasserver/tests/test_power_parameters.py 2014-01-21 06:08:12 +0000
346+++ src/maasserver/tests/test_power_parameters.py 2014-02-10 12:07:12 +0000
347@@ -14,13 +14,20 @@
348 __metaclass__ = type
349 __all__ = []
350
351+from django import forms
352+import jsonschema
353 from maasserver.config_forms import DictCharField
354-from maasserver.power_parameters import POWER_TYPE_PARAMETERS
355+from maasserver.fields import MACAddressFormField
356+from maasserver.power_parameters import (
357+ get_power_type_parameters_from_json,
358+ make_form_field,
359+ make_json_field,
360+ POWER_TYPE_PARAMETERS,
361+ POWER_TYPE_PARAMETER_FIELD_SCHEMA,
362+ )
363 from maasserver.testing.factory import factory
364 from maasserver.testing.testcase import MAASServerTestCase
365-from provisioningserver.enum import (
366- get_power_types,
367- )
368+from provisioningserver.enum import get_power_types
369 from provisioningserver.power.poweraction import PowerAction
370 from testtools.matchers import (
371 AllMatch,
372@@ -76,3 +83,138 @@
373 script = action.render_template(action.get_template(), **params)
374 # The real check is that the rendering went fine.
375 self.assertIsInstance(script, bytes)
376+
377+
378+class TestGetPowerTypeParametersFromJSON(MAASServerTestCase):
379+ """Test that get_power_type_parametrs_from_json."""
380+
381+ def test_validates_json_power_type_parameters(self):
382+ invalid_parameters = [{
383+ 'name': 'invalid_power_type',
384+ 'fields': 'nothing to see here',
385+ }]
386+ self.assertRaises(
387+ jsonschema.ValidationError, get_power_type_parameters_from_json,
388+ invalid_parameters)
389+
390+ def test_includes_empty_power_type(self):
391+ json_parameters = [{
392+ 'name': 'something',
393+ 'fields': [{
394+ 'name': 'some_field',
395+ 'label': 'Some Field',
396+ 'field_type': 'string',
397+ 'required': False,
398+ }],
399+ }]
400+ power_type_parameters = get_power_type_parameters_from_json(
401+ json_parameters)
402+ self.assertEqual(['', 'something'], power_type_parameters.keys())
403+
404+ def test_creates_dict_char_fields(self):
405+ json_parameters = [{
406+ 'name': 'something',
407+ 'fields': [{
408+ 'name': 'some_field',
409+ 'label': 'Some Field',
410+ 'field_type': 'string',
411+ 'required': False,
412+ }],
413+ }]
414+ power_type_parameters = get_power_type_parameters_from_json(
415+ json_parameters)
416+ for name, field in power_type_parameters.items():
417+ self.assertIsInstance(field, DictCharField)
418+
419+
420+class TestMakeFormField(MAASServerTestCase):
421+ """Test that make_form_field() converts JSON fields to Django."""
422+
423+ def test_creates_char_field_for_strings(self):
424+ json_field = {
425+ 'name': 'some_field',
426+ 'label': 'Some Field',
427+ 'field_type': 'string',
428+ 'required': False,
429+ }
430+ django_field = make_form_field(json_field)
431+ self.assertIsInstance(django_field, forms.CharField)
432+
433+ def test_creates_choice_field_for_choices(self):
434+ json_field = {
435+ 'name': 'some_field',
436+ 'label': 'Some Field',
437+ 'field_type': 'choice',
438+ 'choices': [
439+ ['choice-one', 'Choice One'],
440+ ['choice-two', 'Choice Two'],
441+ ],
442+ 'default': 'choice-one',
443+ 'required': False,
444+ }
445+ django_field = make_form_field(json_field)
446+ self.assertIsInstance(django_field, forms.ChoiceField)
447+ self.assertEqual(json_field['choices'], django_field.choices)
448+ self.assertEqual(json_field['default'], django_field.initial)
449+
450+ def test_creates_mac_address_field_for_mac_addresses(self):
451+ json_field = {
452+ 'name': 'some_field',
453+ 'label': 'Some Field',
454+ 'field_type': 'mac_address',
455+ 'required': False,
456+ }
457+ django_field = make_form_field(json_field)
458+ self.assertIsInstance(django_field, MACAddressFormField)
459+
460+ def test_sets_properties_on_form_field(self):
461+ json_field = {
462+ 'name': 'some_field',
463+ 'label': 'Some Field',
464+ 'field_type': 'string',
465+ 'required': False,
466+ }
467+ django_field = make_form_field(json_field)
468+ self.assertEqual(
469+ (json_field['label'], json_field['required']),
470+ (django_field.label, django_field.required))
471+
472+
473+class TestMakeJSONField(MAASServerTestCase):
474+ """Test that make_json_field() creates JSON-verifiable fields."""
475+
476+ def test_returns_json_verifiable_dict(self):
477+ json_field = make_json_field('some_field', 'Some Label')
478+ jsonschema.validate(json_field, POWER_TYPE_PARAMETER_FIELD_SCHEMA)
479+
480+ def test_provides_sane_default_values(self):
481+ json_field = make_json_field('some_field', 'Some Label')
482+ expected_field = {
483+ 'name': 'some_field',
484+ 'label': 'Some Label',
485+ 'required': False,
486+ 'field_type': 'string',
487+ 'choices': [],
488+ 'default': '',
489+ }
490+ self.assertEqual(expected_field, json_field)
491+
492+ def test_sets_field_values(self):
493+ expected_field = {
494+ 'name': 'yet_another_field',
495+ 'label': 'Can I stop writing tests now?',
496+ 'required': True,
497+ 'field_type': 'string',
498+ 'choices': [
499+ ['spam', 'Spam'],
500+ ['eggs', 'Eggs'],
501+ ],
502+ 'default': 'spam',
503+ }
504+ json_field = make_json_field(**expected_field)
505+ self.assertEqual(expected_field, json_field)
506+
507+ def test_validates_choices(self):
508+ self.assertRaises(
509+ jsonschema.ValidationError, make_json_field,
510+ 'some_field', 'Some Label', choices="Nonsense")
511
512=== modified file 'src/provisioningserver/enum.py'
513--- src/provisioningserver/enum.py 2014-01-31 10:46:57 +0000
514+++ src/provisioningserver/enum.py 2014-02-10 12:07:12 +0000
515@@ -92,11 +92,11 @@
516 LAN_2_0 = 'LAN_2_0'
517
518
519-IPMI_DRIVER_CHOICES = (
520- (IPMI_DRIVER.DEFAULT, "Auto-detect"),
521- (IPMI_DRIVER.LAN, "LAN (IPMI 1.5)"),
522- (IPMI_DRIVER.LAN_2_0, "LAN_2_0 (IPMI 2.0)"),
523- )
524+IPMI_DRIVER_CHOICES = [
525+ [IPMI_DRIVER.DEFAULT, "Auto-detect"],
526+ [IPMI_DRIVER.LAN, "LAN [IPMI 1.5]"],
527+ [IPMI_DRIVER.LAN_2_0, "LAN_2_0 [IPMI 2.0]"],
528+ ]
529
530
531 class ARP_HTYPE: