Merge lp:~lamont/maas/bug-1372544 into lp:~maas-committers/maas/trunk
- bug-1372544
- Merge into trunk
Proposed by
LaMont Jones
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | LaMont Jones | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 5021 | ||||
Proposed branch: | lp:~lamont/maas/bug-1372544 | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
Diff against target: |
777 lines (+362/-136) 7 files modified
src/maasserver/api/tests/test_tag.py (+15/-18) src/maasserver/models/tag.py (+58/-17) src/maasserver/models/tests/test_tag.py (+95/-36) src/maasserver/populate_tags.py (+77/-43) src/maasserver/testing/factory.py (+13/-4) src/maasserver/testing/orm.py (+1/-1) src/maasserver/tests/test_populate_tags.py (+103/-17) |
||||
To merge this branch: | bzr merge lp:~lamont/maas/bug-1372544 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Blake Rouse (community) | Approve | ||
Gavin Panella (community) | Abstain | ||
Review via email: mp+294044@code.launchpad.net |
Commit message
If there are no rack controllers connected, populate tags in the region controller.
Description of the change
If there are no rack controllers connected, populate tags in the region controller.
To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote : | # |
Revision history for this message
Blake Rouse (blake-rouse) wrote : | # |
Looks like you have a conflict. I also have one question.
review:
Needs Information
Revision history for this message
Gavin Panella (allenap) wrote : | # |
Responded to Blake's question.
Revision history for this message
Gavin Panella (allenap) : | # |
review:
Abstain
Revision history for this message
LaMont Jones (lamont) wrote : | # |
replied
Revision history for this message
Blake Rouse (blake-rouse) wrote : | # |
Looks good. Thanks for the fix.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/maasserver/api/tests/test_tag.py' |
2 | --- src/maasserver/api/tests/test_tag.py 2016-05-06 11:41:05 +0000 |
3 | +++ src/maasserver/api/tests/test_tag.py 2016-05-13 14:02:27 +0000 |
4 | @@ -38,11 +38,6 @@ |
5 | from testtools.matchers import MatchesStructure |
6 | |
7 | |
8 | -def patch_populate_tags(test): |
9 | - from maasserver import populate_tags |
10 | - return test.patch_autospec(populate_tags, "populate_tags") |
11 | - |
12 | - |
13 | class TestTagAPI(APITestCase): |
14 | """Tests for /api/2.0/tags/<tagname>/.""" |
15 | |
16 | @@ -116,15 +111,16 @@ |
17 | self.assertTrue(Tag.objects.filter(name='new-tag-name').exists()) |
18 | |
19 | def test_PUT_updates_node_associations(self): |
20 | - populate_tags = patch_populate_tags(self) |
21 | - tag = factory.make_Tag(definition='//node/foo') |
22 | - self.expectThat(populate_tags, MockCalledOnceWith(tag)) |
23 | + populate_nodes = self.patch_autospec(Tag, "populate_nodes") |
24 | + tag = Tag(name=factory.make_name("tag"), definition='//node/foo') |
25 | + tag.save() |
26 | + self.expectThat(populate_nodes, MockCalledOnceWith(tag)) |
27 | self.become_admin() |
28 | response = self.client.put( |
29 | self.get_tag_uri(tag), |
30 | {'definition': '//node/bar'}) |
31 | self.assertEqual(http.client.OK, response.status_code) |
32 | - self.expectThat(populate_tags, MockCallsMatch(call(tag), call(tag))) |
33 | + self.expectThat(populate_nodes, MockCallsMatch(call(tag), call(tag))) |
34 | |
35 | def test_GET_nodes_with_no_nodes(self): |
36 | tag = factory.make_Tag() |
37 | @@ -431,7 +427,7 @@ |
38 | node = factory.make_Node() |
39 | client = make_worker_client(rack_controller) |
40 | tag.definition = '//new/node/definition' |
41 | - tag.save() |
42 | + tag.save(populate=False) |
43 | response = client.post( |
44 | self.get_tag_uri(tag), { |
45 | 'op': 'update_nodes', |
46 | @@ -444,16 +440,17 @@ |
47 | self.assertItemsEqual([], node.tags.all()) |
48 | |
49 | def test_POST_rebuild_rebuilds_node_mapping(self): |
50 | - populate_tags = patch_populate_tags(self) |
51 | - tag = factory.make_Tag(definition='//foo/bar') |
52 | + populate_nodes = self.patch_autospec(Tag, "populate_nodes") |
53 | + tag = Tag(name=factory.make_name("tag"), definition='//foo/bar') |
54 | + tag.save() |
55 | self.become_admin() |
56 | - self.assertThat(populate_tags, MockCalledOnceWith(tag)) |
57 | + self.assertThat(populate_nodes, MockCalledOnceWith(tag)) |
58 | response = self.client.post(self.get_tag_uri(tag), {'op': 'rebuild'}) |
59 | self.assertEqual(http.client.OK, response.status_code) |
60 | parsed_result = json.loads( |
61 | response.content.decode(settings.DEFAULT_CHARSET)) |
62 | self.assertEqual({'rebuilding': tag.name}, parsed_result) |
63 | - self.assertThat(populate_tags, MockCallsMatch(call(tag), call(tag))) |
64 | + self.assertThat(populate_nodes, MockCallsMatch(call(tag), call(tag))) |
65 | |
66 | def test_POST_rebuild_leaves_manual_tags(self): |
67 | tag = factory.make_Tag(definition='') |
68 | @@ -589,7 +586,7 @@ |
69 | extra_kernel_opts, Tag.objects.filter(name=name)[0].kernel_opts) |
70 | |
71 | def test_POST_new_populates_nodes(self): |
72 | - populate_tags = patch_populate_tags(self) |
73 | + populate_nodes = self.patch_autospec(Tag, "populate_nodes") |
74 | self.become_admin() |
75 | name = factory.make_string() |
76 | definition = '//node/child' |
77 | @@ -602,8 +599,8 @@ |
78 | 'definition': definition, |
79 | }) |
80 | self.assertEqual(http.client.OK, response.status_code) |
81 | - self.assertThat(populate_tags, MockCalledOnceWith(ANY)) |
82 | - # The tag passed to populate_tags() is the one created above. |
83 | - [tag], _ = populate_tags.call_args |
84 | + self.assertThat(populate_nodes, MockCalledOnceWith(ANY)) |
85 | + # The tag passed to populate_nodes() is the one created above. |
86 | + [tag], _ = populate_nodes.call_args |
87 | self.assertThat(tag, MatchesStructure.byEquality( |
88 | name=name, comment=comment, definition=definition)) |
89 | |
90 | === modified file 'src/maasserver/models/tag.py' |
91 | --- src/maasserver/models/tag.py 2015-12-01 18:12:59 +0000 |
92 | +++ src/maasserver/models/tag.py 2016-05-13 14:02:27 +0000 |
93 | @@ -22,6 +22,9 @@ |
94 | from maasserver import DefaultMeta |
95 | from maasserver.models.cleansave import CleanSave |
96 | from maasserver.models.timestampedmodel import TimestampedModel |
97 | +from maasserver.utils.orm import post_commit_do |
98 | +from maasserver.utils.threads import deferToDatabase |
99 | +from twisted.internet import reactor |
100 | |
101 | |
102 | class TagManager(Manager): |
103 | @@ -90,25 +93,63 @@ |
104 | def __str__(self): |
105 | return self.name |
106 | |
107 | + def _populate_nodes_later(self): |
108 | + """Find all nodes that match this tag, and update them, later. |
109 | + |
110 | + This schedules population to happen post-commit, without waiting for |
111 | + its outcome. |
112 | + """ |
113 | + # Avoid circular imports. |
114 | + from maasserver.populate_tags import populate_tags |
115 | + |
116 | + if self.is_defined: |
117 | + # Schedule repopulate to happen after commit. This thread does not |
118 | + # wait for it to complete. |
119 | + post_commit_do( |
120 | + reactor.callLater, 0, deferToDatabase, |
121 | + populate_tags, self) |
122 | + |
123 | + def _populate_nodes_now(self): |
124 | + """Find all nodes that match this tag, and update them, now. |
125 | + |
126 | + All computation will be done within the current transaction, within |
127 | + the current thread. This could be costly. |
128 | + """ |
129 | + # Avoid circular imports. |
130 | + from maasserver.populate_tags import populate_tag_for_multiple_nodes |
131 | + from maasserver.models.node import Node |
132 | + |
133 | + if self.is_defined: |
134 | + # Do the work here and now in this thread. This is probably a |
135 | + # terrible mistake... unless you're testing. |
136 | + populate_tag_for_multiple_nodes(self, Node.objects.all()) |
137 | + |
138 | def populate_nodes(self): |
139 | - """Find all nodes that match this tag, and update them.""" |
140 | - if not self.is_defined: |
141 | - return |
142 | - # before we pass off any work, ensure the definition is valid XPATH |
143 | - try: |
144 | - etree.XPath(self.definition) |
145 | - except etree.XPathSyntaxError as e: |
146 | - msg = 'Invalid xpath expression: %s' % (e,) |
147 | - raise ValidationError({'definition': [msg]}) |
148 | - # Now delete the existing tags |
149 | - self.node_set.clear() |
150 | - # Avoid circular imports. |
151 | - from maasserver.populate_tags import populate_tags |
152 | - populate_tags(self) |
153 | - |
154 | - def save(self, *args, **kwargs): |
155 | + """Find all nodes that match this tag, and update them. |
156 | + |
157 | + By default, node population is deferred. |
158 | + """ |
159 | + return self._populate_nodes_later() |
160 | + |
161 | + def clean_definition(self): |
162 | + if self.is_defined: |
163 | + try: |
164 | + etree.XPath(self.definition) |
165 | + except etree.XPathSyntaxError as e: |
166 | + msg = 'Invalid XPath expression: %s' % (e,) |
167 | + raise ValidationError({'definition': [msg]}) |
168 | + |
169 | + def clean(self): |
170 | + self.clean_definition() |
171 | + |
172 | + def save(self, *args, populate=True, **kwargs): |
173 | + """Save this tag. |
174 | + |
175 | + :param populate: Whether or not to call `populate_nodes` if the |
176 | + definition has changed. |
177 | + """ |
178 | super(Tag, self).save(*args, **kwargs) |
179 | - if self.definition != self._original_definition: |
180 | + if populate and (self.definition != self._original_definition): |
181 | self.populate_nodes() |
182 | self._original_definition = self.definition |
183 | |
184 | |
185 | === modified file 'src/maasserver/models/tests/test_tag.py' |
186 | --- src/maasserver/models/tests/test_tag.py 2016-03-28 13:54:47 +0000 |
187 | +++ src/maasserver/models/tests/test_tag.py 2016-05-13 14:02:27 +0000 |
188 | @@ -5,12 +5,20 @@ |
189 | |
190 | __all__ = [] |
191 | |
192 | +from unittest.mock import ANY |
193 | + |
194 | from django.core.exceptions import ValidationError |
195 | -from maasserver import populate_tags as populate_tags_module |
196 | +from maasserver import populate_tags |
197 | +from maasserver.models import tag as tag_module |
198 | from maasserver.models.tag import Tag |
199 | from maasserver.testing.factory import factory |
200 | from maasserver.testing.testcase import MAASServerTestCase |
201 | -from maastesting.matchers import MockCalledOnceWith |
202 | +from maasserver.utils.threads import deferToDatabase |
203 | +from maastesting.matchers import ( |
204 | + MockCalledOnceWith, |
205 | + MockNotCalled, |
206 | +) |
207 | +from twisted.internet import reactor |
208 | |
209 | |
210 | class TagTest(MAASServerTestCase): |
211 | @@ -56,11 +64,17 @@ |
212 | 'too-long' * 33, '\xb5']: |
213 | self.assertRaises(ValidationError, factory.make_Tag, name=invalid) |
214 | |
215 | - def test_applies_tags_to_nodes(self): |
216 | - populate_tags = self.patch_autospec( |
217 | - populate_tags_module, "populate_tags") |
218 | - tag = factory.make_Tag(definition='//node/child') |
219 | - self.assertThat(populate_tags, MockCalledOnceWith(tag)) |
220 | + def test_validate_traps_invalid_tag_definitions(self): |
221 | + self.assertRaises( |
222 | + ValidationError, factory.make_Tag, |
223 | + definition="invalid::definition") |
224 | + |
225 | + def test_applies_tags_to_nodes_on_save(self): |
226 | + populate_nodes = self.patch_autospec(Tag, "populate_nodes") |
227 | + tag = Tag(name=factory.make_name("tag"), definition='//node/child') |
228 | + self.assertThat(populate_nodes, MockNotCalled()) |
229 | + tag.save() |
230 | + self.assertThat(populate_nodes, MockCalledOnceWith(tag)) |
231 | |
232 | def test_will_not_save_invalid_xpath(self): |
233 | tag = factory.make_Tag(definition='//node/foo') |
234 | @@ -83,41 +97,86 @@ |
235 | self.assertIs(self.expected, tag.is_defined) |
236 | |
237 | |
238 | -class TestTagPopulateNodes(MAASServerTestCase): |
239 | +class TestTagPopulateNodesLater(MAASServerTestCase): |
240 | + |
241 | + def test__populates_if_tag_is_defined(self): |
242 | + post_commit_do = self.patch(tag_module, "post_commit_do") |
243 | + |
244 | + tag = Tag(name=factory.make_name("tag"), definition="//foo") |
245 | + tag.save(populate=False) |
246 | + |
247 | + self.assertTrue(tag.is_defined) |
248 | + self.assertThat(post_commit_do, MockNotCalled()) |
249 | + tag._populate_nodes_later() |
250 | + self.assertThat( |
251 | + post_commit_do, MockCalledOnceWith( |
252 | + reactor.callLater, 0, deferToDatabase, |
253 | + populate_tags.populate_tags, tag)) |
254 | |
255 | def test__does_nothing_if_tag_is_not_defined(self): |
256 | - tag = factory.make_Tag(definition="") |
257 | + post_commit_do = self.patch(tag_module, "post_commit_do") |
258 | + |
259 | + tag = Tag(name=factory.make_name("tag"), definition="") |
260 | + tag.save(populate=False) |
261 | + |
262 | self.assertFalse(tag.is_defined) |
263 | + self.assertThat(post_commit_do, MockNotCalled()) |
264 | + tag._populate_nodes_later() |
265 | + self.assertThat(post_commit_do, MockNotCalled()) |
266 | + |
267 | + def test__does_not_clear_node_set_before_populating(self): |
268 | + post_commit_do = self.patch(tag_module, "post_commit_do") |
269 | + |
270 | + tag = Tag(name=factory.make_name("tag"), definition="//foo") |
271 | + tag.save(populate=False) |
272 | + |
273 | nodes = [factory.make_Node() for _ in range(3)] |
274 | tag.node_set.add(*nodes) |
275 | - tag.populate_nodes() |
276 | - # The set of related nodes is unchanged. |
277 | + tag._populate_nodes_later() |
278 | self.assertItemsEqual(nodes, tag.node_set.all()) |
279 | - |
280 | - def test__checks_definition_before_proceeding(self): |
281 | - tag = factory.make_Tag(definition="") |
282 | - tag.definition = "invalid::definition" |
283 | - self.assertRaises(ValidationError, tag.populate_nodes) |
284 | - |
285 | - def test__clears_node_set(self): |
286 | - self.patch_autospec(populate_tags_module, "populate_tags") |
287 | - |
288 | - tag = factory.make_Tag(definition="") |
289 | - # Define the tag now but don't save because .save() calls |
290 | - # populate_nodes(), but we want to do it explicitly here. |
291 | - tag.definition = "//foo" |
292 | + self.assertThat(post_commit_do, MockCalledOnceWith( |
293 | + reactor.callLater, 0, deferToDatabase, |
294 | + populate_tags.populate_tags, tag)) |
295 | + |
296 | + def test__later_is_the_default(self): |
297 | + tag = Tag(name=factory.make_name("tag")) |
298 | + self.patch(tag, "_populate_nodes_later") |
299 | + self.assertThat(tag._populate_nodes_later, MockNotCalled()) |
300 | + tag.save() |
301 | + self.assertThat(tag._populate_nodes_later, MockCalledOnceWith()) |
302 | + |
303 | + |
304 | +class TestTagPopulateNodesNow(MAASServerTestCase): |
305 | + |
306 | + def test__populates_if_tag_is_defined(self): |
307 | + populate_multiple = self.patch_autospec( |
308 | + populate_tags, "populate_tag_for_multiple_nodes") |
309 | + |
310 | + tag = Tag(name=factory.make_name("tag"), definition="//foo") |
311 | + tag.save(populate=False) |
312 | + |
313 | + self.assertTrue(tag.is_defined) |
314 | + self.assertThat(populate_multiple, MockNotCalled()) |
315 | + tag._populate_nodes_now() |
316 | + self.assertThat(populate_multiple, MockCalledOnceWith(tag, ANY)) |
317 | + |
318 | + def test__does_nothing_if_tag_is_not_defined(self): |
319 | + populate_multiple = self.patch_autospec( |
320 | + populate_tags, "populate_tag_for_multiple_nodes") |
321 | + |
322 | + tag = Tag(name=factory.make_name("tag"), definition="") |
323 | + tag.save(populate=False) |
324 | + |
325 | + self.assertFalse(tag.is_defined) |
326 | + self.assertThat(populate_multiple, MockNotCalled()) |
327 | + tag._populate_nodes_now() |
328 | + self.assertThat(populate_multiple, MockNotCalled()) |
329 | + |
330 | + def test__clears_node_set_before_populating(self): |
331 | + tag = Tag(name=factory.make_name("tag"), definition="//foo") |
332 | + tag.save(populate=False) |
333 | + |
334 | nodes = [factory.make_Node() for _ in range(3)] |
335 | tag.node_set.add(*nodes) |
336 | - tag.populate_nodes() |
337 | + tag._populate_nodes_now() |
338 | self.assertItemsEqual([], tag.node_set.all()) |
339 | - |
340 | - def test__calls_populate_tags(self): |
341 | - populate_tags = self.patch_autospec( |
342 | - populate_tags_module, "populate_tags") |
343 | - |
344 | - tag = factory.make_Tag(definition="") |
345 | - # Define the tag now but don't save because .save() calls |
346 | - # populate_nodes(), but we want to do it explicitly here. |
347 | - tag.definition = "//foo" |
348 | - tag.populate_nodes() |
349 | - self.assertThat(populate_tags, MockCalledOnceWith(tag)) |
350 | |
351 | === modified file 'src/maasserver/populate_tags.py' |
352 | --- src/maasserver/populate_tags.py 2016-04-27 20:52:15 +0000 |
353 | +++ src/maasserver/populate_tags.py 2016-05-13 14:02:27 +0000 |
354 | @@ -4,9 +4,10 @@ |
355 | """Populate what nodes are associated with a tag.""" |
356 | |
357 | __all__ = [ |
358 | + 'populate_tag_for_multiple_nodes', |
359 | 'populate_tags', |
360 | 'populate_tags_for_single_node', |
361 | - ] |
362 | +] |
363 | |
364 | from functools import partial |
365 | from math import ceil |
366 | @@ -19,6 +20,7 @@ |
367 | RackController, |
368 | ) |
369 | from maasserver.models.nodeprobeddetails import ( |
370 | + get_probed_details, |
371 | get_single_probed_details, |
372 | script_output_nsmap, |
373 | ) |
374 | @@ -28,9 +30,14 @@ |
375 | get_creds_tuple, |
376 | ) |
377 | from maasserver.rpc import getAllClients |
378 | +from maasserver.utils.orm import transactional |
379 | from provisioningserver.logger import get_maas_logger |
380 | from provisioningserver.rpc.cluster import EvaluateTag |
381 | -from provisioningserver.tags import merge_details |
382 | +from provisioningserver.tags import ( |
383 | + DEFAULT_BATCH_SIZE, |
384 | + gen_batches, |
385 | + merge_details, |
386 | +) |
387 | from provisioningserver.utils import classify |
388 | from provisioningserver.utils.twisted import ( |
389 | asynchronous, |
390 | @@ -68,6 +75,7 @@ |
391 | |
392 | |
393 | @synchronous |
394 | +@transactional |
395 | def populate_tags(tag): |
396 | """Evaluate `tag` for all nodes. |
397 | |
398 | @@ -85,45 +93,42 @@ |
399 | logger.debug('Evaluating the "%s" tag for all nodes.', tag.name) |
400 | clients = getAllClients() |
401 | if len(clients) == 0: |
402 | - # XXX: allenap 2014-09-22 bug=1372544: No connected rack controllers. |
403 | - # Nothing can be evaluated when this occurs. |
404 | - return |
405 | - |
406 | - # Split the work between the connected rack controllers. Building all the |
407 | - # information that needs to be sent to them all. |
408 | - node_ids = Node.objects.all().values_list("system_id", flat=True) |
409 | - node_ids = [ |
410 | - {"system_id": node_id} |
411 | - for node_id in node_ids |
412 | - ] |
413 | - chunked_node_ids = list(chunk_list(node_ids, len(clients))) |
414 | - connected_racks = [] |
415 | - for idx, client in enumerate(clients): |
416 | - rack = RackController.objects.get(system_id=client.ident) |
417 | - tokens = list(get_auth_tokens(rack.owner)) |
418 | - if len(tokens) > 0: |
419 | - # Use the latest token. |
420 | - token = tokens[-1] |
421 | - else: |
422 | - token = create_auth_token(rack.owner) |
423 | - creds = convert_tuple_to_string(get_creds_tuple(token)) |
424 | - if len(chunked_node_ids) > idx: |
425 | - connected_racks.append({ |
426 | - "system_id": rack.system_id, |
427 | - "hostname": rack.hostname, |
428 | - "client": client, |
429 | - "tag_name": tag.name, |
430 | - "tag_definition": tag.definition, |
431 | - "tag_nsmap": [ |
432 | - {"prefix": prefix, "uri": uri} |
433 | - for prefix, uri in tag_nsmap.items() |
434 | - ], |
435 | - "credentials": creds, |
436 | - "nodes": list(chunked_node_ids[idx]), |
437 | - }) |
438 | - |
439 | - [d] = _do_populate_tags(connected_racks) |
440 | - return d |
441 | + # We have no clients so we need to do the work locally. |
442 | + return populate_tag_for_multiple_nodes(tag, Node.objects.all()) |
443 | + else: |
444 | + # Split the work between the connected rack controllers. |
445 | + node_ids = Node.objects.all().values_list("system_id", flat=True) |
446 | + node_ids = [{"system_id": node_id} for node_id in node_ids] |
447 | + chunked_node_ids = list(chunk_list(node_ids, len(clients))) |
448 | + connected_racks = [] |
449 | + for idx, client in enumerate(clients): |
450 | + rack = RackController.objects.get(system_id=client.ident) |
451 | + token = _get_or_create_auth_token(rack.owner) |
452 | + creds = convert_tuple_to_string(get_creds_tuple(token)) |
453 | + if len(chunked_node_ids) > idx: |
454 | + connected_racks.append({ |
455 | + "system_id": rack.system_id, |
456 | + "hostname": rack.hostname, |
457 | + "client": client, |
458 | + "tag_name": tag.name, |
459 | + "tag_definition": tag.definition, |
460 | + "tag_nsmap": [ |
461 | + {"prefix": prefix, "uri": uri} |
462 | + for prefix, uri in tag_nsmap.items() |
463 | + ], |
464 | + "credentials": creds, |
465 | + "nodes": list(chunked_node_ids[idx]), |
466 | + }) |
467 | + [d] = _do_populate_tags(connected_racks) |
468 | + return d |
469 | + |
470 | + |
471 | +def _get_or_create_auth_token(user): |
472 | + """Get the most recent OAuth token for `user`, or create one.""" |
473 | + for token in reversed(get_auth_tokens(user)): |
474 | + return token |
475 | + else: |
476 | + return create_auth_token(user) |
477 | |
478 | |
479 | @asynchronous(timeout=FOREVER) |
480 | @@ -177,11 +182,15 @@ |
481 | return [d] |
482 | |
483 | |
484 | +@synchronous |
485 | def populate_tags_for_single_node(tags, node): |
486 | """Reevaluate all tags for a single node. |
487 | |
488 | - Presumably this node's details have recently changed. Use |
489 | - `populate_tags` when many nodes need reevaluating. |
490 | + Presumably this node's details have recently changed. Use `populate_tags` |
491 | + when many nodes need reevaluating AND there are rack controllers available |
492 | + to which to farm-out work. Use `populate_tag_for_multiple_nodes` when many |
493 | + nodes need reevaluating locally, i.e. when there are no rack controllers |
494 | + connected. |
495 | """ |
496 | probed_details = get_single_probed_details(node.system_id) |
497 | probed_details_doc = merge_details(probed_details) |
498 | @@ -192,3 +201,28 @@ |
499 | tags_matching, tags_nonmatching = classify(evaluator, tags_defined) |
500 | node.tags.remove(*tags_nonmatching) |
501 | node.tags.add(*tags_matching) |
502 | + |
503 | + |
504 | +@synchronous |
505 | +def populate_tag_for_multiple_nodes(tag, nodes, batch_size=DEFAULT_BATCH_SIZE): |
506 | + """Reevaluate a single tag for a multiple nodes. |
507 | + |
508 | + Presumably this tag's expression has recently changed. Use `populate_tags` |
509 | + when many nodes need reevaluating AND there are rack controllers available |
510 | + to which to farm-out work. Use this only when many nodes need reevaluating |
511 | + locally, i.e. when there are no rack controllers connected. |
512 | + """ |
513 | + # Same expression, multuple documents: compile expression with XPath. |
514 | + xpath = etree.XPath(tag.definition, namespaces=tag_nsmap) |
515 | + # The XML details documents can be large so work in batches. |
516 | + for batch in gen_batches(nodes, batch_size): |
517 | + probed_details = get_probed_details(node.system_id for node in batch) |
518 | + probed_details_docs_by_node = { |
519 | + node: merge_details(probed_details[node.system_id]) |
520 | + for node in batch |
521 | + } |
522 | + nodes_matching, nodes_nonmatching = classify( |
523 | + partial(try_match_xpath, xpath, logger=maaslog), |
524 | + probed_details_docs_by_node.items()) |
525 | + tag.node_set.remove(*nodes_nonmatching) |
526 | + tag.node_set.add(*nodes_matching) |
527 | |
528 | === modified file 'src/maasserver/testing/factory.py' |
529 | --- src/maasserver/testing/factory.py 2016-05-11 19:31:40 +0000 |
530 | +++ src/maasserver/testing/factory.py 2016-05-13 14:02:27 +0000 |
531 | @@ -1025,8 +1025,9 @@ |
532 | else: |
533 | return self.make_ipv4_Subnet_with_IPRanges() |
534 | |
535 | - def make_Tag(self, name=None, definition=None, comment='', |
536 | - kernel_opts=None, created=None, updated=None): |
537 | + def make_Tag( |
538 | + self, name=None, definition=None, comment='', kernel_opts=None, |
539 | + created=None, updated=None, populate=True): |
540 | if name is None: |
541 | name = self.make_name('tag') |
542 | if definition is None: |
543 | @@ -1035,14 +1036,22 @@ |
544 | tag = Tag( |
545 | name=name, definition=definition, comment=comment, |
546 | kernel_opts=kernel_opts) |
547 | - tag.save() |
548 | + # Save without populating nodes. |
549 | + tag.save(populate=False) |
550 | # Update the 'updated'/'created' fields with a call to 'update' |
551 | # preventing a call to save() from overriding the values. |
552 | if updated is not None: |
553 | Tag.objects.filter(id=tag.id).update(updated=updated) |
554 | if created is not None: |
555 | Tag.objects.filter(id=tag.id).update(created=created) |
556 | - return reload_object(tag) |
557 | + # Reload if we've changed the underlying record. |
558 | + if updated is not None or created is not None: |
559 | + tag = reload_object(tag) |
560 | + # Populate nodes for this tag now that it's fully configured, if |
561 | + # requested. This avoids dealing with the post-commit hook stuff. |
562 | + if populate: |
563 | + tag._populate_nodes_now() |
564 | + return tag |
565 | |
566 | def make_user_with_keys(self, n_keys=2, user=None, **kwargs): |
567 | """Create a user with n `SSHKey`. If user is not None, use this user |
568 | |
569 | === modified file 'src/maasserver/testing/orm.py' |
570 | --- src/maasserver/testing/orm.py 2016-03-28 13:54:47 +0000 |
571 | +++ src/maasserver/testing/orm.py 2016-05-13 14:02:27 +0000 |
572 | @@ -71,7 +71,7 @@ |
573 | self.expectThat( |
574 | post_commit_hooks.hooks, HasLength(0), |
575 | "One or more post-commit tasks were present at the end of " |
576 | - "this test." + description_of_hooks) |
577 | + "this test.\n" + description_of_hooks) |
578 | super(PostCommitHooksTestMixin, self).tearDown() |
579 | finally: |
580 | # By this point we will have reported the leaked post-commit |
581 | |
582 | === modified file 'src/maasserver/tests/test_populate_tags.py' |
583 | --- src/maasserver/tests/test_populate_tags.py 2016-05-10 11:02:10 +0000 |
584 | +++ src/maasserver/tests/test_populate_tags.py 2016-05-13 14:02:27 +0000 |
585 | @@ -13,8 +13,15 @@ |
586 | |
587 | from apiclient.creds import convert_tuple_to_string |
588 | from fixtures import FakeLogger |
589 | -from maasserver import populate_tags as populate_tags_module |
590 | -from maasserver.models import Tag |
591 | +from maasserver import ( |
592 | + populate_tags as populate_tags_module, |
593 | + rpc as rpc_module, |
594 | +) |
595 | +from maasserver.models import ( |
596 | + Node, |
597 | + Tag, |
598 | + tag as tag_module, |
599 | +) |
600 | from maasserver.models.user import ( |
601 | create_auth_token, |
602 | get_auth_tokens, |
603 | @@ -22,6 +29,7 @@ |
604 | ) |
605 | from maasserver.populate_tags import ( |
606 | _do_populate_tags, |
607 | + populate_tag_for_multiple_nodes, |
608 | populate_tags, |
609 | populate_tags_for_single_node, |
610 | ) |
611 | @@ -31,7 +39,12 @@ |
612 | RunningEventLoopFixture, |
613 | ) |
614 | from maasserver.testing.factory import factory |
615 | -from maasserver.testing.testcase import MAASServerTestCase |
616 | +from maasserver.testing.testcase import ( |
617 | + MAASServerTestCase, |
618 | + MAASTransactionServerTestCase, |
619 | +) |
620 | +from maasserver.utils.orm import post_commit_hooks |
621 | +from maasserver.utils.threads import deferToDatabase |
622 | from maastesting.matchers import ( |
623 | MockCalledOnceWith, |
624 | MockCallsMatch, |
625 | @@ -48,13 +61,16 @@ |
626 | from provisioningserver.rpc.cluster import EvaluateTag |
627 | from provisioningserver.rpc.common import Client |
628 | from provisioningserver.utils.twisted import asynchronous |
629 | -from testtools.monkey import MonkeyPatcher |
630 | - |
631 | - |
632 | -def make_Tag_without_populating(): |
633 | - # Create a tag but prevent evaluation when saving. |
634 | - dont_populate = MonkeyPatcher((Tag, "populate_nodes", lambda self: None)) |
635 | - return dont_populate.run_with_patches(factory.make_Tag) |
636 | +from testtools.matchers import ( |
637 | + HasLength, |
638 | + IsInstance, |
639 | + MatchesAll, |
640 | + MatchesStructure, |
641 | +) |
642 | +from twisted.internet import reactor |
643 | +from twisted.internet.base import DelayedCall |
644 | +from twisted.internet.task import Clock |
645 | +from twisted.internet.threads import blockingCallFromThread |
646 | |
647 | |
648 | class TestDoPopulateTags(MAASServerTestCase): |
649 | @@ -186,6 +202,11 @@ |
650 | |
651 | |
652 | class TestPopulateTagsEndToNearlyEnd(MAASServerTestCase): |
653 | + """Tests for populating tags on racks. |
654 | + |
655 | + This happens when there are connected rack controllers able to carry out |
656 | + the task. |
657 | + """ |
658 | |
659 | def prepare_live_rpc(self): |
660 | self.useFixture(RegionEventLoopFixture("rpc")) |
661 | @@ -210,7 +231,7 @@ |
662 | protocol = rpc_fixture.makeCluster(rack, EvaluateTag) |
663 | protocol.EvaluateTag.side_effect = always_succeed_with({}) |
664 | protocols.append(protocol) |
665 | - tag = make_Tag_without_populating() |
666 | + tag = factory.make_Tag(populate=False) |
667 | |
668 | d = populate_tags(tag) |
669 | |
670 | @@ -227,6 +248,57 @@ |
671 | tag_nsmap=ANY, credentials=creds, nodes=ANY)) |
672 | |
673 | |
674 | +class TestPopulateTagsInRegion(MAASTransactionServerTestCase): |
675 | + """Tests for populating tags in the region. |
676 | + |
677 | + This happens when there are no rack controllers to carry out the task. |
678 | + """ |
679 | + |
680 | + def test__saving_tag_schedules_node_population(self): |
681 | + clock = self.patch(tag_module, "reactor", Clock()) |
682 | + |
683 | + with post_commit_hooks: |
684 | + # Make a Tag by hand to trigger normal node population handling |
685 | + # behaviour rather than the (generally more convenient) default |
686 | + # behaviour in the factory. |
687 | + tag = Tag(name=factory.make_name("tag"), definition='true()') |
688 | + tag.save() |
689 | + |
690 | + # A call has been scheduled to populate tags. |
691 | + calls = clock.getDelayedCalls() |
692 | + self.assertThat(calls, HasLength(1)) |
693 | + [call] = calls |
694 | + self.assertThat( |
695 | + call, MatchesAll( |
696 | + IsInstance(DelayedCall), |
697 | + MatchesStructure.byEquality( |
698 | + time=0, func=deferToDatabase, |
699 | + args=(populate_tags, tag), kw={}), |
700 | + first_only=True, |
701 | + )) |
702 | + |
703 | + def test__populate_in_region_when_no_clients(self): |
704 | + clock = self.patch(tag_module, "reactor", Clock()) |
705 | + |
706 | + # Ensure there are no clients. |
707 | + self.patch(rpc_module, 'getAllClients').return_value = [] |
708 | + |
709 | + with post_commit_hooks: |
710 | + node = factory.make_Node() |
711 | + # Make a Tag by hand to trigger normal node population handling |
712 | + # behaviour rather than the (generally more convenient) default |
713 | + # behaviour in the factory. |
714 | + tag = Tag(name=factory.make_name("tag"), definition='true()') |
715 | + tag.save() |
716 | + |
717 | + # A call has been scheduled to populate tags. |
718 | + [call] = clock.getDelayedCalls() |
719 | + # Execute the call ourselves in the real reactor. |
720 | + blockingCallFromThread(reactor, call.func, *call.args, **call.kw) |
721 | + # The tag's node set has been updated. |
722 | + self.assertItemsEqual([node], tag.node_set.all()) |
723 | + |
724 | + |
725 | class TestPopulateTagsForSingleNode(MAASServerTestCase): |
726 | |
727 | def test_updates_node_with_all_applicable_tags(self): |
728 | @@ -236,9 +308,9 @@ |
729 | factory.make_NodeResult_for_commissioning( |
730 | node, LLDP_OUTPUT_NAME, 0, b"<bar/>") |
731 | tags = [ |
732 | - factory.make_Tag("foo", "/foo"), |
733 | - factory.make_Tag("bar", "//lldp:bar"), |
734 | - factory.make_Tag("baz", "/foo/bar"), |
735 | + factory.make_Tag("foo", "/foo", populate=False), |
736 | + factory.make_Tag("bar", "//lldp:bar", populate=False), |
737 | + factory.make_Tag("baz", "/foo/bar", populate=False), |
738 | ] |
739 | populate_tags_for_single_node(tags, node) |
740 | self.assertItemsEqual( |
741 | @@ -249,8 +321,8 @@ |
742 | factory.make_NodeResult_for_commissioning( |
743 | node, LSHW_OUTPUT_NAME, 0, b"<foo/>") |
744 | tags = [ |
745 | - factory.make_Tag("foo", "/foo"), |
746 | - factory.make_Tag("lou", "//nge:bar"), |
747 | + factory.make_Tag("foo", "/foo", populate=False), |
748 | + factory.make_Tag("lou", "//nge:bar", populate=False), |
749 | ] |
750 | populate_tags_for_single_node(tags, node) # Look mom, no exception! |
751 | self.assertSequenceEqual( |
752 | @@ -261,10 +333,24 @@ |
753 | factory.make_NodeResult_for_commissioning( |
754 | node, LSHW_OUTPUT_NAME, 0, b"<foo/>") |
755 | tags = [ |
756 | - factory.make_Tag("foo", "/foo"), |
757 | + factory.make_Tag("foo", "/foo", populate=False), |
758 | Tag(name="empty", definition=""), |
759 | Tag(name="null", definition=None), |
760 | ] |
761 | populate_tags_for_single_node(tags, node) # Look mom, no exception! |
762 | self.assertSequenceEqual( |
763 | ["foo"], [tag.name for tag in node.tags.all()]) |
764 | + |
765 | + |
766 | +class TestPopulateTagForMultipleNodes(MAASServerTestCase): |
767 | + |
768 | + def test_updates_nodes_with_tag(self): |
769 | + nodes = [factory.make_Node() for _ in range(5)] |
770 | + for node in nodes[0:2]: |
771 | + factory.make_NodeResult_for_commissioning( |
772 | + node, LLDP_OUTPUT_NAME, 0, b"<bar/>") |
773 | + tag = factory.make_Tag("bar", "//lldp:bar", populate=False) |
774 | + populate_tag_for_multiple_nodes(tag, nodes) |
775 | + self.assertItemsEqual( |
776 | + [node.hostname for node in nodes[0:2]], |
777 | + [node.hostname for node in Node.objects.filter(tags__name='bar')]) |
I got a bit carried away reviewing this. I built on what you've done and came up with quite a large bunch of changes. I've made the evaluations more efficient and put everything into post-commit hooks. Take a look at lp:~allenap/maas/lamont--bug-1372544 (or the merge proposal at https:/ /code.launchpad .net/~allenap/ maas/lamont- -bug-1372544/ +merge/ 294167).
In time, a better solution might be to model a work queue, like we did for DNS zones recently. That way we can better manage the work in an HA environment, and better recover from failures. For example, we really don't want to be doing lots of tag evaluation work for more than one tag update at a time otherwise we run a real risk of DOSing the MAAS installation. There's no protection against that right now.
(We could add the DOS protection to this current implementation quite easily, but recovery would still need a queue, or some other additional support in the data model.)
Anyway, look at what I've done. If you like it, merge it into this, write some tests for `populate_ tag_for_ multiple_ nodes` and it should be good to land.