Merge lp:~mpontillo/maas/networking-constraints-django-form-support into lp:~maas-committers/maas/trunk

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: no longer in the source branch.
Merged at revision: 4427
Proposed branch: lp:~mpontillo/maas/networking-constraints-django-form-support
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 193 lines (+111/-1)
3 files modified
src/maasserver/node_constraint_filter_forms.py (+49/-0)
src/maasserver/tests/test_node_constraint_filter_forms.py (+25/-1)
src/provisioningserver/utils/constraints.py (+37/-0)
To merge this branch: bzr merge lp:~mpontillo/maas/networking-constraints-django-form-support
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+275958@code.launchpad.net

Commit message

Add a Django form field type (and backing object) for labeled constraint maps. Add a networking field, which makes use of it (and a corresponding validator).

Description of the change

Note that this is still just plumbing. The constraint code itself is still being worked on. This is just a chunk I could carve out to commit separately.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good. Just one question about the python3 comment. Go ahead and make this work with python3, so we don't have to find it later.

review: Approve
Revision history for this message
Mike Pontillo (mpontillo) wrote :

This isn't anything that is broken, just something that works better in Python 3. (Re-raising a different exception while keeping the stack trace.)

I looked into this a bit, and didn't see a clean Python 2 equivalent.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

The "workaround" way to do this in Python 2 is to use sys.exc_info() to capture sys.exc_info()[2] (the traceback at the time the exception is being handled).

I want to save the traceback for later, and there seem to be some caveats to doing this[1]. So I'll just keep it as-is until we drop support for Python 2.

[1]: https://docs.python.org/2/library/sys.html#sys.exc_info

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/node_constraint_filter_forms.py'
--- src/maasserver/node_constraint_filter_forms.py 2015-08-28 02:01:20 +0000
+++ src/maasserver/node_constraint_filter_forms.py 2015-10-28 08:31:25 +0000
@@ -27,6 +27,7 @@
27from django import forms27from django import forms
28from django.core.exceptions import ValidationError28from django.core.exceptions import ValidationError
29from django.db.models import Q29from django.db.models import Q
30from django.forms.fields import Field
30from maasserver.fields import mac_validator31from maasserver.fields import mac_validator
31from maasserver.forms import (32from maasserver.forms import (
32 MultipleChoiceField,33 MultipleChoiceField,
@@ -49,6 +50,7 @@
49)50)
50from netaddr import IPAddress51from netaddr import IPAddress
51from netaddr.core import AddrFormatError52from netaddr.core import AddrFormatError
53from provisioningserver.utils.constraints import LabeledConstraintMap
5254
53# Matches the storage constraint from Juju. Format is an optional label,55# Matches the storage constraint from Juju. Format is an optional label,
54# followed by an optional colon, then size (which is mandatory) followed by an56# followed by an optional colon, then size (which is mandatory) followed by an
@@ -98,6 +100,36 @@
98 if ','.join(rendered_groups) != value:100 if ','.join(rendered_groups) != value:
99 raise ValidationError('Malformed storage constraint, "%s".' % value)101 raise ValidationError('Malformed storage constraint, "%s".' % value)
100102
103NETWORKING_CONSTRAINT_KEYS = {
104 'space',
105 'not_space',
106 'mode',
107 'fabric_class',
108 'not_fabric_class',
109 'subnet_cidr',
110 'not_subnet_cidr',
111 'vid',
112 'not_vid',
113 'fabric',
114 'not_fabric',
115 'subnet',
116 'not_subnet',
117}
118
119
120def networking_validator(constraint_map):
121 """Validate the given LabeledConstraintMap object."""
122 # At this point, the basic syntax of a labeled constraint map will have
123 # already been validated by the underlying form field. However, we also
124 # need to validate the specified things we're looking for in the
125 # networking domain.
126 for label in constraint_map:
127 constraints = constraint_map[label]
128 for constraint_name in constraints.iterkeys():
129 if constraint_name not in NETWORKING_CONSTRAINT_KEYS:
130 raise ValidationError(
131 "Unknown networking constraint: '%s" % constraint_name)
132
101133
102def generate_architecture_wildcards(arches):134def generate_architecture_wildcards(arches):
103 """Map 'primary' architecture names to a list of full expansions.135 """Map 'primary' architecture names to a list of full expansions.
@@ -516,6 +548,20 @@
516 raise Subnet.DoesNotExist("No subnet matching '%s'." % spec)548 raise Subnet.DoesNotExist("No subnet matching '%s'." % spec)
517549
518550
551class LabeledConstraintMapField(Field):
552
553 def __init__(self, *args, **kwargs):
554 super(LabeledConstraintMapField, self).__init__(*args, **kwargs)
555 self.validators.insert(
556 0, lambda constraint_map: constraint_map.validate(
557 exception_type=ValidationError))
558
559 def to_python(self, value):
560 """Returns a LabeledConstraintMap object."""
561 if value is not None and len(value.strip()) != 0:
562 return LabeledConstraintMap(value)
563
564
519class AcquireNodeForm(RenamableFieldsForm):565class AcquireNodeForm(RenamableFieldsForm):
520 """A form handling the constraints used to acquire a node."""566 """A form handling the constraints used to acquire a node."""
521567
@@ -573,6 +619,9 @@
573 storage = forms.CharField(619 storage = forms.CharField(
574 validators=[storage_validator], label="Storage", required=False)620 validators=[storage_validator], label="Storage", required=False)
575621
622 networking = LabeledConstraintMapField(
623 validators=[networking_validator], label="Networking", required=False)
624
576 ignore_unknown_constraints = True625 ignore_unknown_constraints = True
577626
578 @classmethod627 @classmethod
579628
=== modified file 'src/maasserver/tests/test_node_constraint_filter_forms.py'
--- src/maasserver/tests/test_node_constraint_filter_forms.py 2015-10-27 20:53:16 +0000
+++ src/maasserver/tests/test_node_constraint_filter_forms.py 2015-10-28 08:31:25 +0000
@@ -40,7 +40,10 @@
40from maasserver.testing.factory import factory40from maasserver.testing.factory import factory
41from maasserver.testing.testcase import MAASServerTestCase41from maasserver.testing.testcase import MAASServerTestCase
42from maasserver.utils import ignore_unused42from maasserver.utils import ignore_unused
43from testtools.matchers import ContainsAll43from testtools.matchers import (
44 Contains,
45 ContainsAll,
46)
4447
4548
46class TestUtils(MAASServerTestCase):49class TestUtils(MAASServerTestCase):
@@ -863,6 +866,26 @@
863 id=constraint_map[node.id].keys()[1]) # 2nd constraint with name866 id=constraint_map[node.id].keys()[1]) # 2nd constraint with name
864 self.assertGreaterEqual(disk1.size, 5 * 1000 ** 3)867 self.assertGreaterEqual(disk1.size, 5 * 1000 ** 3)
865868
869 def test_networking_constraint_rejected_if_syntax_is_invalid(self):
870 factory.make_Node_with_Interface_on_Subnet()
871 form = AcquireNodeForm({
872 u'networking': u'label:x'})
873 self.assertFalse(form.is_valid(), dict(form.errors))
874 self.assertThat(form.errors, Contains('networking'))
875
876 def test_networking_constraint_rejected_if_key_is_invalid(self):
877 factory.make_Node_with_Interface_on_Subnet()
878 form = AcquireNodeForm({
879 u'networking': u'label:chirp_chirp_thing=silenced'})
880 self.assertFalse(form.is_valid(), dict(form.errors))
881 self.assertThat(form.errors, Contains('networking'))
882
883 def test_networking_constraint_validated(self):
884 factory.make_Node_with_Interface_on_Subnet()
885 form = AcquireNodeForm({
886 u'networking': u'label:fabric=fabric-0'})
887 self.assertTrue(form.is_valid(), dict(form.errors))
888
866 def test_combined_constraints(self):889 def test_combined_constraints(self):
867 tag_big = factory.make_Tag(name='big')890 tag_big = factory.make_Tag(name='big')
868 arch = '%s/generic' % factory.make_name('arch')891 arch = '%s/generic' % factory.make_name('arch')
@@ -962,6 +985,7 @@
962 'zone': factory.make_Zone(),985 'zone': factory.make_Zone(),
963 'not_in_zone': [factory.make_Zone().name],986 'not_in_zone': [factory.make_Zone().name],
964 'storage': '0(ssd),10(ssd)',987 'storage': '0(ssd),10(ssd)',
988 'networking': 'label:fabric=fabric-0',
965 }989 }
966 form = AcquireNodeForm(data=constraints)990 form = AcquireNodeForm(data=constraints)
967 self.assertTrue(form.is_valid(), form.errors)991 self.assertTrue(form.is_valid(), form.errors)
968992
=== modified file 'src/provisioningserver/utils/constraints.py'
--- src/provisioningserver/utils/constraints.py 2015-10-27 18:35:05 +0000
+++ src/provisioningserver/utils/constraints.py 2015-10-28 08:31:25 +0000
@@ -19,6 +19,41 @@
19import re19import re
2020
2121
22class LabeledConstraintMap(object):
23 """Class to encapsulate a labeled constraint map, so that it only
24 needs to be parsed once.
25 """
26
27 def __init__(self, value):
28 self.value = value
29 self.map = None
30 self.error = None
31 try:
32 self.map = parse_labeled_constraint_map(value)
33 except ValueError as error:
34 self.error = error
35
36 def __repr__(self):
37 return "%s(%s)" % (self.__class__.__name__, repr(self.value))
38
39 def __unicode__(self):
40 return self.value
41
42 def __iter__(self):
43 if self.map is None:
44 return iter([])
45 return iter(self.map)
46
47 def __getitem__(self, item):
48 return self.map[item]
49
50 def validate(self, exception_type=ValueError):
51 if self.error:
52 # XXX mpontillo 2015-10-28 Need to re-raise this properly once we
53 # get to Python 3.
54 raise exception_type(self.error.message)
55
56
22def parse_labeled_constraint_map(value, exception_type=ValueError):57def parse_labeled_constraint_map(value, exception_type=ValueError):
23 """Returns a dictionary of constraints, given the specified constraint58 """Returns a dictionary of constraints, given the specified constraint
24 value. Validates that the following conditions hold true:59 value. Validates that the following conditions hold true:
@@ -42,6 +77,8 @@
42 When multiple keys are contained within a constraint, the values will be77 When multiple keys are contained within a constraint, the values will be
43 returned (in the order specified) inside a list.78 returned (in the order specified) inside a list.
4479
80 When a duplicate label is specified, an exception is thrown.
81
45 Single values will also be returned inside a list, for consistency.82 Single values will also be returned inside a list, for consistency.
4683
47 :return:dict84 :return:dict