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
1=== modified file 'src/maasserver/node_constraint_filter_forms.py'
2--- src/maasserver/node_constraint_filter_forms.py 2015-08-28 02:01:20 +0000
3+++ src/maasserver/node_constraint_filter_forms.py 2015-10-28 08:31:25 +0000
4@@ -27,6 +27,7 @@
5 from django import forms
6 from django.core.exceptions import ValidationError
7 from django.db.models import Q
8+from django.forms.fields import Field
9 from maasserver.fields import mac_validator
10 from maasserver.forms import (
11 MultipleChoiceField,
12@@ -49,6 +50,7 @@
13 )
14 from netaddr import IPAddress
15 from netaddr.core import AddrFormatError
16+from provisioningserver.utils.constraints import LabeledConstraintMap
17
18 # Matches the storage constraint from Juju. Format is an optional label,
19 # followed by an optional colon, then size (which is mandatory) followed by an
20@@ -98,6 +100,36 @@
21 if ','.join(rendered_groups) != value:
22 raise ValidationError('Malformed storage constraint, "%s".' % value)
23
24+NETWORKING_CONSTRAINT_KEYS = {
25+ 'space',
26+ 'not_space',
27+ 'mode',
28+ 'fabric_class',
29+ 'not_fabric_class',
30+ 'subnet_cidr',
31+ 'not_subnet_cidr',
32+ 'vid',
33+ 'not_vid',
34+ 'fabric',
35+ 'not_fabric',
36+ 'subnet',
37+ 'not_subnet',
38+}
39+
40+
41+def networking_validator(constraint_map):
42+ """Validate the given LabeledConstraintMap object."""
43+ # At this point, the basic syntax of a labeled constraint map will have
44+ # already been validated by the underlying form field. However, we also
45+ # need to validate the specified things we're looking for in the
46+ # networking domain.
47+ for label in constraint_map:
48+ constraints = constraint_map[label]
49+ for constraint_name in constraints.iterkeys():
50+ if constraint_name not in NETWORKING_CONSTRAINT_KEYS:
51+ raise ValidationError(
52+ "Unknown networking constraint: '%s" % constraint_name)
53+
54
55 def generate_architecture_wildcards(arches):
56 """Map 'primary' architecture names to a list of full expansions.
57@@ -516,6 +548,20 @@
58 raise Subnet.DoesNotExist("No subnet matching '%s'." % spec)
59
60
61+class LabeledConstraintMapField(Field):
62+
63+ def __init__(self, *args, **kwargs):
64+ super(LabeledConstraintMapField, self).__init__(*args, **kwargs)
65+ self.validators.insert(
66+ 0, lambda constraint_map: constraint_map.validate(
67+ exception_type=ValidationError))
68+
69+ def to_python(self, value):
70+ """Returns a LabeledConstraintMap object."""
71+ if value is not None and len(value.strip()) != 0:
72+ return LabeledConstraintMap(value)
73+
74+
75 class AcquireNodeForm(RenamableFieldsForm):
76 """A form handling the constraints used to acquire a node."""
77
78@@ -573,6 +619,9 @@
79 storage = forms.CharField(
80 validators=[storage_validator], label="Storage", required=False)
81
82+ networking = LabeledConstraintMapField(
83+ validators=[networking_validator], label="Networking", required=False)
84+
85 ignore_unknown_constraints = True
86
87 @classmethod
88
89=== modified file 'src/maasserver/tests/test_node_constraint_filter_forms.py'
90--- src/maasserver/tests/test_node_constraint_filter_forms.py 2015-10-27 20:53:16 +0000
91+++ src/maasserver/tests/test_node_constraint_filter_forms.py 2015-10-28 08:31:25 +0000
92@@ -40,7 +40,10 @@
93 from maasserver.testing.factory import factory
94 from maasserver.testing.testcase import MAASServerTestCase
95 from maasserver.utils import ignore_unused
96-from testtools.matchers import ContainsAll
97+from testtools.matchers import (
98+ Contains,
99+ ContainsAll,
100+)
101
102
103 class TestUtils(MAASServerTestCase):
104@@ -863,6 +866,26 @@
105 id=constraint_map[node.id].keys()[1]) # 2nd constraint with name
106 self.assertGreaterEqual(disk1.size, 5 * 1000 ** 3)
107
108+ def test_networking_constraint_rejected_if_syntax_is_invalid(self):
109+ factory.make_Node_with_Interface_on_Subnet()
110+ form = AcquireNodeForm({
111+ u'networking': u'label:x'})
112+ self.assertFalse(form.is_valid(), dict(form.errors))
113+ self.assertThat(form.errors, Contains('networking'))
114+
115+ def test_networking_constraint_rejected_if_key_is_invalid(self):
116+ factory.make_Node_with_Interface_on_Subnet()
117+ form = AcquireNodeForm({
118+ u'networking': u'label:chirp_chirp_thing=silenced'})
119+ self.assertFalse(form.is_valid(), dict(form.errors))
120+ self.assertThat(form.errors, Contains('networking'))
121+
122+ def test_networking_constraint_validated(self):
123+ factory.make_Node_with_Interface_on_Subnet()
124+ form = AcquireNodeForm({
125+ u'networking': u'label:fabric=fabric-0'})
126+ self.assertTrue(form.is_valid(), dict(form.errors))
127+
128 def test_combined_constraints(self):
129 tag_big = factory.make_Tag(name='big')
130 arch = '%s/generic' % factory.make_name('arch')
131@@ -962,6 +985,7 @@
132 'zone': factory.make_Zone(),
133 'not_in_zone': [factory.make_Zone().name],
134 'storage': '0(ssd),10(ssd)',
135+ 'networking': 'label:fabric=fabric-0',
136 }
137 form = AcquireNodeForm(data=constraints)
138 self.assertTrue(form.is_valid(), form.errors)
139
140=== modified file 'src/provisioningserver/utils/constraints.py'
141--- src/provisioningserver/utils/constraints.py 2015-10-27 18:35:05 +0000
142+++ src/provisioningserver/utils/constraints.py 2015-10-28 08:31:25 +0000
143@@ -19,6 +19,41 @@
144 import re
145
146
147+class LabeledConstraintMap(object):
148+ """Class to encapsulate a labeled constraint map, so that it only
149+ needs to be parsed once.
150+ """
151+
152+ def __init__(self, value):
153+ self.value = value
154+ self.map = None
155+ self.error = None
156+ try:
157+ self.map = parse_labeled_constraint_map(value)
158+ except ValueError as error:
159+ self.error = error
160+
161+ def __repr__(self):
162+ return "%s(%s)" % (self.__class__.__name__, repr(self.value))
163+
164+ def __unicode__(self):
165+ return self.value
166+
167+ def __iter__(self):
168+ if self.map is None:
169+ return iter([])
170+ return iter(self.map)
171+
172+ def __getitem__(self, item):
173+ return self.map[item]
174+
175+ def validate(self, exception_type=ValueError):
176+ if self.error:
177+ # XXX mpontillo 2015-10-28 Need to re-raise this properly once we
178+ # get to Python 3.
179+ raise exception_type(self.error.message)
180+
181+
182 def parse_labeled_constraint_map(value, exception_type=ValueError):
183 """Returns a dictionary of constraints, given the specified constraint
184 value. Validates that the following conditions hold true:
185@@ -42,6 +77,8 @@
186 When multiple keys are contained within a constraint, the values will be
187 returned (in the order specified) inside a list.
188
189+ When a duplicate label is specified, an exception is thrown.
190+
191 Single values will also be returned inside a list, for consistency.
192
193 :return:dict