Merge lp:~allenap/maas/rpc-tags-region into lp:~maas-committers/maas/trunk
- rpc-tags-region
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Gavin Panella |
Approved revision: | no longer in the source branch. |
Merged at revision: | 3055 |
Proposed branch: | lp:~allenap/maas/rpc-tags-region |
Merge into: | lp:~maas-committers/maas/trunk |
Prerequisite: | lp:~allenap/maas/rpc-tags-cluster |
Diff against target: |
683 lines (+414/-116) 5 files modified
src/maasserver/api/tests/test_tag.py (+31/-40) src/maasserver/models/tag.py (+3/-3) src/maasserver/models/tests/test_tag.py (+6/-54) src/maasserver/populate_tags.py (+120/-18) src/maasserver/tests/test_populate_tags.py (+254/-1) |
To merge this branch: | bzr merge lp:~allenap/maas/rpc-tags-region |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | Approve | ||
Review via email: mp+235294@code.launchpad.net |
Commit message
Use the EvaluateTag RPC command from the region.
Description of the change
There are problems with the approach taken in this branch. Crucially, tags won't be evaluated for clusters that are temporarily down. Additional work is needed to fix this. However, the benefit of this branch is that it is the final branch (or penultimate, depending on which lands first) before we can remove Celery from clusters (Celery is still needed for the master cluster, for DNS, but this can be run independently).
Gavin Panella (allenap) wrote : | # |
Thanks for the review! It wasn't as scary as I had feared.
> Looks like the Tag API tests suffered from doing unit-testing at an
> integration-testing level. Good thing to see some tests lose the XML
> parsing. The last of the API tests you edited looks like it might
> benefit from self.expectThat().
I've switched to MatchesStructur
>
> .
>
> In the docstring for populate_tags, can you say why the return value
> is not meant to be used in production? Would there be any harm? The
> caller doesn't want or need to block on it, of course, but that sort
> of seems like the caller's problem.
I've added the following:
- You must not use the `Deferred` in the calling thread; it must
only be manipulated in the reactor thread. Pretending it's not
there is safer than chaining code onto it because it's easy to get
wrong.
- The call may not finish for 10 minutes or more. It is therefore
not a good thing to be waiting for in a web request.
>
> .
>
> The docstring for _do_populate_tags confuses me no end. Did you mean
> to rewrite it? There's also a typo in there: “oaram.”
My mistake. It's a copy-n-paste from _get_clients_
(or something like that; there's some shared history in there). Fixed, I
hope.
>
> More dangerous, of course, is the terminal preposition. Julian will
> never forgive you.
Done.
>
> .
>
> As you know I hate nontrivial nested callbacks with implicit closures.
> Was there no way to use @inlineCallbacks in _do_populate_tags?
I didn't know that. They're not nested though, and I wouldn't be able to
have control over the returned Deferred if I used inlineCallbacks. I did
try having a single nested function using inlineCallbacks, but it was no
clearer than adding callbacks. It was worse if anything. Using callbacks
with Deferreds gives a very clear control flow, and gives each function
its own namespace, helping refactoring.
>
> .
>
> Why “list((uuid, name) for (uuid, name, _) in clusters)” and not
> “[(uuid, name) for (uuid, name, _) in clusters]”? Just to avoid
> variables leaking out of the comprehension? I hate that too, but it
> doesn't seem worth our list comprehensions. To me, the diversity in
> closing brackets, braces, and parentheses provides a valuable visual
> cue of what the nesting levels are.
Okay, I've changed it. Don't like it, but I've changed it ;)
>
> .
>
> Can you add a bug number to the XXX comment in
> _get_clients_
Yes! https:/
>
> .
>
> test__returns_
> me. I thought _get_clients_
> the successfully obtained clients, and only left out clients for any
> clusters that it couldn't get clients for. The tests don't cover the
> difference between those two things AFAICS.
Okay, done.
>
> .
>
> Shouldn't test__obtains_
> clients _for the given clusters_, not just that it gets three clients?
Done.
>
> .
>
> In TestDoPopulateT
> a fake client. Saves you having to loop over the clients and modify
> th...
Jeroen T. Vermeulen (jtv) wrote : | # |
> > Not that it's exactly pretty, but it gives an immediate overview of
> > the nesting and which part belongs at which level. It's useful when
> > you're scanning the code looking for things that you need to read more
> > closely.
>
> I've used this style for months. I find long tests (or any code) with
> lots of vertical space harder to read. My guess is that's it's something
> like: long scans/jumps up and down, missing the mark, finding my place
> again. I honestly cope better when I can keep the bulk of the test close
> together. If I find myself squinting at something I've written, I do
> spread things out, add blank lines, or comments more often. I do try not
> to obfuscate.
Sounds like we've hit on an interesting question about code legibility. I'll try to pay more attention to what my eyes are doing in these situations.
MAAS Lander (maas-lander) wrote : | # |
There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.
Julian Edwards (julian-edwards) wrote : | # |
On Monday 22 Sep 2014 21:27:18 you wrote:
> I've used this style for months. I find long tests (or any code) with
> lots of vertical space harder to read. My guess is that's it's something
> like: long scans/jumps up and down, missing the mark, finding my place
> again. I honestly cope better when I can keep the bulk of the test close
> together. If I find myself squinting at something I've written, I do
> spread things out, add blank lines, or comments more often. I do try not
> to obfuscate.
This is something that's hard to quantify for sure. Maybe we need to take a
reviewing decision on this and put it in the coding guidelines?
Preview Diff
1 | === modified file 'src/maasserver/api/tests/test_tag.py' | |||
2 | --- src/maasserver/api/tests/test_tag.py 2014-09-23 09:14:04 +0000 | |||
3 | +++ src/maasserver/api/tests/test_tag.py 2014-09-23 09:14:04 +0000 | |||
4 | @@ -28,7 +28,21 @@ | |||
5 | 28 | from maasserver.testing.factory import factory | 28 | from maasserver.testing.factory import factory |
6 | 29 | from maasserver.testing.oauthclient import OAuthAuthenticatedClient | 29 | from maasserver.testing.oauthclient import OAuthAuthenticatedClient |
7 | 30 | from maasserver.testing.orm import reload_object | 30 | from maasserver.testing.orm import reload_object |
8 | 31 | from maastesting.matchers import ( | ||
9 | 32 | MockCalledOnceWith, | ||
10 | 33 | MockCallsMatch, | ||
11 | 34 | ) | ||
12 | 31 | from metadataserver.models.commissioningscript import inject_lshw_result | 35 | from metadataserver.models.commissioningscript import inject_lshw_result |
13 | 36 | from mock import ( | ||
14 | 37 | ANY, | ||
15 | 38 | call, | ||
16 | 39 | ) | ||
17 | 40 | from testtools.matchers import MatchesStructure | ||
18 | 41 | |||
19 | 42 | |||
20 | 43 | def patch_populate_tags(test): | ||
21 | 44 | from maasserver import populate_tags | ||
22 | 45 | return test.patch_autospec(populate_tags, "populate_tags") | ||
23 | 32 | 46 | ||
24 | 33 | 47 | ||
25 | 34 | class TestTagAPI(APITestCase): | 48 | class TestTagAPI(APITestCase): |
26 | @@ -104,22 +118,15 @@ | |||
27 | 104 | self.assertTrue(Tag.objects.filter(name='new-tag-name').exists()) | 118 | self.assertTrue(Tag.objects.filter(name='new-tag-name').exists()) |
28 | 105 | 119 | ||
29 | 106 | def test_PUT_updates_node_associations(self): | 120 | def test_PUT_updates_node_associations(self): |
36 | 107 | self.skip("Tag evaluation is being ported to RPC.") | 121 | populate_tags = patch_populate_tags(self) |
31 | 108 | |||
32 | 109 | node1 = factory.make_Node() | ||
33 | 110 | inject_lshw_result(node1, b'<node><foo/></node>') | ||
34 | 111 | node2 = factory.make_Node() | ||
35 | 112 | inject_lshw_result(node2, b'<node><bar/></node>') | ||
37 | 113 | tag = factory.make_Tag(definition='//node/foo') | 122 | tag = factory.make_Tag(definition='//node/foo') |
40 | 114 | self.assertItemsEqual([tag.name], node1.tag_names()) | 123 | self.expectThat(populate_tags, MockCalledOnceWith(tag)) |
39 | 115 | self.assertItemsEqual([], node2.tag_names()) | ||
41 | 116 | self.become_admin() | 124 | self.become_admin() |
42 | 117 | response = self.client_put( | 125 | response = self.client_put( |
43 | 118 | self.get_tag_uri(tag), | 126 | self.get_tag_uri(tag), |
44 | 119 | {'definition': '//node/bar'}) | 127 | {'definition': '//node/bar'}) |
45 | 120 | self.assertEqual(httplib.OK, response.status_code) | 128 | self.assertEqual(httplib.OK, response.status_code) |
48 | 121 | self.assertItemsEqual([], node1.tag_names()) | 129 | self.expectThat(populate_tags, MockCallsMatch(call(tag), call(tag))) |
47 | 122 | self.assertItemsEqual([tag.name], node2.tag_names()) | ||
49 | 123 | 130 | ||
50 | 124 | def test_GET_nodes_with_no_nodes(self): | 131 | def test_GET_nodes_with_no_nodes(self): |
51 | 125 | tag = factory.make_Tag() | 132 | tag = factory.make_Tag() |
52 | @@ -143,14 +150,13 @@ | |||
53 | 143 | [r['system_id'] for r in parsed_result]) | 150 | [r['system_id'] for r in parsed_result]) |
54 | 144 | 151 | ||
55 | 145 | def test_GET_nodes_hides_invisible_nodes(self): | 152 | def test_GET_nodes_hides_invisible_nodes(self): |
56 | 146 | self.skip("Tag evaluation is being ported to RPC.") | ||
57 | 147 | |||
58 | 148 | user2 = factory.make_User() | 153 | user2 = factory.make_User() |
59 | 149 | node1 = factory.make_Node() | 154 | node1 = factory.make_Node() |
60 | 150 | inject_lshw_result(node1, b'<node><foo/></node>') | ||
61 | 151 | node2 = factory.make_Node(status=NODE_STATUS.ALLOCATED, owner=user2) | 155 | node2 = factory.make_Node(status=NODE_STATUS.ALLOCATED, owner=user2) |
64 | 152 | inject_lshw_result(node2, b'<node><bar/></node>') | 156 | tag = factory.make_Tag() |
65 | 153 | tag = factory.make_Tag(definition='//node') | 157 | node1.tags.add(tag) |
66 | 158 | node2.tags.add(tag) | ||
67 | 159 | |||
68 | 154 | response = self.client.get(self.get_tag_uri(tag), {'op': 'nodes'}) | 160 | response = self.client.get(self.get_tag_uri(tag), {'op': 'nodes'}) |
69 | 155 | 161 | ||
70 | 156 | self.assertEqual(httplib.OK, response.status_code) | 162 | self.assertEqual(httplib.OK, response.status_code) |
71 | @@ -166,12 +172,11 @@ | |||
72 | 166 | [r['system_id'] for r in parsed_result]) | 172 | [r['system_id'] for r in parsed_result]) |
73 | 167 | 173 | ||
74 | 168 | def test_PUT_invalid_definition(self): | 174 | def test_PUT_invalid_definition(self): |
75 | 169 | self.skip("Tag evaluation is being ported to RPC.") | ||
76 | 170 | |||
77 | 171 | self.become_admin() | 175 | self.become_admin() |
78 | 172 | node = factory.make_Node() | 176 | node = factory.make_Node() |
79 | 173 | inject_lshw_result(node, b'<node ><child/></node>') | 177 | inject_lshw_result(node, b'<node ><child/></node>') |
80 | 174 | tag = factory.make_Tag(definition='//child') | 178 | tag = factory.make_Tag(definition='//child') |
81 | 179 | node.tags.add(tag) | ||
82 | 175 | self.assertItemsEqual([tag.name], node.tag_names()) | 180 | self.assertItemsEqual([tag.name], node.tag_names()) |
83 | 176 | response = self.client_put( | 181 | response = self.client_put( |
84 | 177 | self.get_tag_uri(tag), {'definition': 'invalid::tag'}) | 182 | self.get_tag_uri(tag), {'definition': 'invalid::tag'}) |
85 | @@ -338,24 +343,15 @@ | |||
86 | 338 | self.assertItemsEqual([], node.tags.all()) | 343 | self.assertItemsEqual([], node.tags.all()) |
87 | 339 | 344 | ||
88 | 340 | def test_POST_rebuild_rebuilds_node_mapping(self): | 345 | def test_POST_rebuild_rebuilds_node_mapping(self): |
91 | 341 | self.skip("Tag evaluation is being ported to RPC.") | 346 | populate_tags = patch_populate_tags(self) |
90 | 342 | |||
92 | 343 | tag = factory.make_Tag(definition='//foo/bar') | 347 | tag = factory.make_Tag(definition='//foo/bar') |
93 | 344 | # Only one node matches the tag definition, rebuilding should notice | ||
94 | 345 | node_matching = factory.make_Node() | ||
95 | 346 | inject_lshw_result(node_matching, b'<foo><bar/></foo>') | ||
96 | 347 | node_bogus = factory.make_Node() | ||
97 | 348 | inject_lshw_result(node_bogus, b'<foo/>') | ||
98 | 349 | node_matching.tags.add(tag) | ||
99 | 350 | node_bogus.tags.add(tag) | ||
100 | 351 | self.assertItemsEqual( | ||
101 | 352 | [node_matching, node_bogus], tag.node_set.all()) | ||
102 | 353 | self.become_admin() | 348 | self.become_admin() |
103 | 349 | self.assertThat(populate_tags, MockCalledOnceWith(tag)) | ||
104 | 354 | response = self.client.post(self.get_tag_uri(tag), {'op': 'rebuild'}) | 350 | response = self.client.post(self.get_tag_uri(tag), {'op': 'rebuild'}) |
105 | 355 | self.assertEqual(httplib.OK, response.status_code) | 351 | self.assertEqual(httplib.OK, response.status_code) |
106 | 356 | parsed_result = json.loads(response.content) | 352 | parsed_result = json.loads(response.content) |
107 | 357 | self.assertEqual({'rebuilding': tag.name}, parsed_result) | 353 | self.assertEqual({'rebuilding': tag.name}, parsed_result) |
109 | 358 | self.assertItemsEqual([node_matching], tag.node_set.all()) | 354 | self.assertThat(populate_tags, MockCallsMatch(call(tag), call(tag))) |
110 | 359 | 355 | ||
111 | 360 | def test_POST_rebuild_leaves_manual_tags(self): | 356 | def test_POST_rebuild_leaves_manual_tags(self): |
112 | 361 | tag = factory.make_Tag(definition='') | 357 | tag = factory.make_Tag(definition='') |
113 | @@ -491,16 +487,8 @@ | |||
114 | 491 | extra_kernel_opts, Tag.objects.filter(name=name)[0].kernel_opts) | 487 | extra_kernel_opts, Tag.objects.filter(name=name)[0].kernel_opts) |
115 | 492 | 488 | ||
116 | 493 | def test_POST_new_populates_nodes(self): | 489 | def test_POST_new_populates_nodes(self): |
119 | 494 | self.skip("Tag evaluation is being ported to RPC.") | 490 | populate_tags = patch_populate_tags(self) |
118 | 495 | |||
120 | 496 | self.become_admin() | 491 | self.become_admin() |
121 | 497 | node1 = factory.make_Node() | ||
122 | 498 | inject_lshw_result(node1, b'<node><child/></node>') | ||
123 | 499 | # Create another node that doesn't have a 'child' | ||
124 | 500 | node2 = factory.make_Node() | ||
125 | 501 | inject_lshw_result(node2, b'<node/>') | ||
126 | 502 | self.assertItemsEqual([], node1.tag_names()) | ||
127 | 503 | self.assertItemsEqual([], node2.tag_names()) | ||
128 | 504 | name = factory.make_string() | 492 | name = factory.make_string() |
129 | 505 | definition = '//node/child' | 493 | definition = '//node/child' |
130 | 506 | comment = factory.make_string() | 494 | comment = factory.make_string() |
131 | @@ -513,5 +501,8 @@ | |||
132 | 513 | 'definition': definition, | 501 | 'definition': definition, |
133 | 514 | }) | 502 | }) |
134 | 515 | self.assertEqual(httplib.OK, response.status_code) | 503 | self.assertEqual(httplib.OK, response.status_code) |
137 | 516 | self.assertItemsEqual([name], node1.tag_names()) | 504 | self.assertThat(populate_tags, MockCalledOnceWith(ANY)) |
138 | 517 | self.assertItemsEqual([], node2.tag_names()) | 505 | # The tag passed to populate_tags() is the one created above. |
139 | 506 | [tag], _ = populate_tags.call_args | ||
140 | 507 | self.assertThat(tag, MatchesStructure.byEquality( | ||
141 | 508 | name=name, comment=comment, definition=definition)) | ||
142 | 518 | 509 | ||
143 | === modified file 'src/maasserver/models/tag.py' | |||
144 | --- src/maasserver/models/tag.py 2014-09-23 09:14:04 +0000 | |||
145 | +++ src/maasserver/models/tag.py 2014-09-23 09:14:04 +0000 | |||
146 | @@ -111,9 +111,9 @@ | |||
147 | 111 | raise ValidationError({'definition': [msg]}) | 111 | raise ValidationError({'definition': [msg]}) |
148 | 112 | # Now delete the existing tags | 112 | # Now delete the existing tags |
149 | 113 | self.node_set.clear() | 113 | self.node_set.clear() |
153 | 114 | # Being ported to RPC. | 114 | # Avoid circular imports. |
154 | 115 | # from maasserver.populate_tags import populate_tags | 115 | from maasserver.populate_tags import populate_tags |
155 | 116 | # populate_tags(self) | 116 | populate_tags(self) |
156 | 117 | 117 | ||
157 | 118 | def save(self, *args, **kwargs): | 118 | def save(self, *args, **kwargs): |
158 | 119 | super(Tag, self).save(*args, **kwargs) | 119 | super(Tag, self).save(*args, **kwargs) |
159 | 120 | 120 | ||
160 | === modified file 'src/maasserver/models/tests/test_tag.py' | |||
161 | --- src/maasserver/models/tests/test_tag.py 2014-09-23 09:14:04 +0000 | |||
162 | +++ src/maasserver/models/tests/test_tag.py 2014-09-23 09:14:04 +0000 | |||
163 | @@ -20,7 +20,6 @@ | |||
164 | 20 | from maasserver.testing.factory import factory | 20 | from maasserver.testing.factory import factory |
165 | 21 | from maasserver.testing.testcase import MAASServerTestCase | 21 | from maasserver.testing.testcase import MAASServerTestCase |
166 | 22 | from maastesting.matchers import MockCalledOnceWith | 22 | from maastesting.matchers import MockCalledOnceWith |
167 | 23 | from metadataserver.models.commissioningscript import inject_lshw_result | ||
168 | 24 | 23 | ||
169 | 25 | 24 | ||
170 | 26 | class TagTest(MAASServerTestCase): | 25 | class TagTest(MAASServerTestCase): |
171 | @@ -67,60 +66,15 @@ | |||
172 | 67 | self.assertRaises(ValidationError, factory.make_Tag, name=invalid) | 66 | self.assertRaises(ValidationError, factory.make_Tag, name=invalid) |
173 | 68 | 67 | ||
174 | 69 | def test_applies_tags_to_nodes(self): | 68 | def test_applies_tags_to_nodes(self): |
181 | 70 | self.skip("Tag evaluation is being ported to RPC.") | 69 | populate_tags = self.patch_autospec( |
182 | 71 | 70 | populate_tags_module, "populate_tags") | |
177 | 72 | node1 = factory.make_Node() | ||
178 | 73 | inject_lshw_result(node1, b'<node><child /></node>') | ||
179 | 74 | node2 = factory.make_Node() | ||
180 | 75 | inject_lshw_result(node2, b'<node />') | ||
183 | 76 | tag = factory.make_Tag(definition='//node/child') | 71 | tag = factory.make_Tag(definition='//node/child') |
228 | 77 | self.assertItemsEqual([tag.name], node1.tag_names()) | 72 | self.assertThat(populate_tags, MockCalledOnceWith(tag)) |
229 | 78 | self.assertItemsEqual([], node2.tag_names()) | 73 | |
230 | 79 | 74 | def test_will_not_save_invalid_xpath(self): | |
231 | 80 | def test_removes_old_values(self): | 75 | tag = factory.make_Tag(definition='//node/foo') |
188 | 81 | self.skip("Tag evaluation is being ported to RPC.") | ||
189 | 82 | |||
190 | 83 | node1 = factory.make_Node() | ||
191 | 84 | inject_lshw_result(node1, b'<node><foo /></node>') | ||
192 | 85 | node2 = factory.make_Node() | ||
193 | 86 | inject_lshw_result(node2, b'<node><bar /></node>') | ||
194 | 87 | tag = factory.make_Tag(definition='//node/foo') | ||
195 | 88 | self.assertItemsEqual([tag.name], node1.tag_names()) | ||
196 | 89 | self.assertItemsEqual([], node2.tag_names()) | ||
197 | 90 | tag.definition = '//node/bar' | ||
198 | 91 | tag.save() | ||
199 | 92 | self.assertItemsEqual([], node1.tag_names()) | ||
200 | 93 | self.assertItemsEqual([tag.name], node2.tag_names()) | ||
201 | 94 | # And we notice if we change it *again* and then save. | ||
202 | 95 | tag.definition = '//node/foo' | ||
203 | 96 | tag.save() | ||
204 | 97 | self.assertItemsEqual([tag.name], node1.tag_names()) | ||
205 | 98 | self.assertItemsEqual([], node2.tag_names()) | ||
206 | 99 | |||
207 | 100 | def test_doesnt_touch_other_tags(self): | ||
208 | 101 | self.skip("Tag evaluation is being ported to RPC.") | ||
209 | 102 | |||
210 | 103 | node1 = factory.make_Node() | ||
211 | 104 | inject_lshw_result(node1, b'<node><foo /></node>') | ||
212 | 105 | node2 = factory.make_Node() | ||
213 | 106 | inject_lshw_result(node2, b'<node><bar /></node>') | ||
214 | 107 | tag1 = factory.make_Tag(definition='//node/foo') | ||
215 | 108 | self.assertItemsEqual([tag1.name], node1.tag_names()) | ||
216 | 109 | self.assertItemsEqual([], node2.tag_names()) | ||
217 | 110 | tag2 = factory.make_Tag(definition='//node/bar') | ||
218 | 111 | self.assertItemsEqual([tag1.name], node1.tag_names()) | ||
219 | 112 | self.assertItemsEqual([tag2.name], node2.tag_names()) | ||
220 | 113 | |||
221 | 114 | def test_rollsback_invalid_xpath(self): | ||
222 | 115 | self.skip("Tag evaluation is being ported to RPC.") | ||
223 | 116 | |||
224 | 117 | node = factory.make_Node() | ||
225 | 118 | inject_lshw_result(node, b'<node><foo /></node>') | ||
226 | 119 | tag = factory.make_Tag(definition='//node/foo') | ||
227 | 120 | self.assertItemsEqual([tag.name], node.tag_names()) | ||
232 | 121 | tag.definition = 'invalid::tag' | 76 | tag.definition = 'invalid::tag' |
233 | 122 | self.assertRaises(ValidationError, tag.save) | 77 | self.assertRaises(ValidationError, tag.save) |
234 | 123 | self.assertItemsEqual([tag.name], node.tag_names()) | ||
235 | 124 | 78 | ||
236 | 125 | 79 | ||
237 | 126 | class TestTagIsDefined(MAASServerTestCase): | 80 | class TestTagIsDefined(MAASServerTestCase): |
238 | @@ -167,8 +121,6 @@ | |||
239 | 167 | self.assertItemsEqual([], tag.node_set.all()) | 121 | self.assertItemsEqual([], tag.node_set.all()) |
240 | 168 | 122 | ||
241 | 169 | def test__calls_populate_tags(self): | 123 | def test__calls_populate_tags(self): |
242 | 170 | self.skip("Tag evaluation is being ported to RPC.") | ||
243 | 171 | |||
244 | 172 | populate_tags = self.patch_autospec( | 124 | populate_tags = self.patch_autospec( |
245 | 173 | populate_tags_module, "populate_tags") | 125 | populate_tags_module, "populate_tags") |
246 | 174 | 126 | ||
247 | 175 | 127 | ||
248 | === modified file 'src/maasserver/populate_tags.py' | |||
249 | --- src/maasserver/populate_tags.py 2014-09-23 09:14:04 +0000 | |||
250 | +++ src/maasserver/populate_tags.py 2014-09-23 09:14:04 +0000 | |||
251 | @@ -18,6 +18,7 @@ | |||
252 | 18 | ] | 18 | ] |
253 | 19 | 19 | ||
254 | 20 | from functools import partial | 20 | from functools import partial |
255 | 21 | from itertools import izip | ||
256 | 21 | 22 | ||
257 | 22 | from lxml import etree | 23 | from lxml import etree |
258 | 23 | from maasserver import logger | 24 | from maasserver import logger |
259 | @@ -26,10 +27,23 @@ | |||
260 | 26 | get_single_probed_details, | 27 | get_single_probed_details, |
261 | 27 | script_output_nsmap, | 28 | script_output_nsmap, |
262 | 28 | ) | 29 | ) |
264 | 29 | from maasserver.refresh_worker import refresh_worker | 30 | from maasserver.rpc import getClientFor |
265 | 31 | from provisioningserver.logger import get_maas_logger | ||
266 | 32 | from provisioningserver.rpc.cluster import EvaluateTag | ||
267 | 30 | from provisioningserver.tags import merge_details | 33 | from provisioningserver.tags import merge_details |
268 | 31 | from provisioningserver.utils import classify | 34 | from provisioningserver.utils import classify |
269 | 35 | from provisioningserver.utils.twisted import ( | ||
270 | 36 | asynchronous, | ||
271 | 37 | FOREVER, | ||
272 | 38 | synchronous, | ||
273 | 39 | ) | ||
274 | 32 | from provisioningserver.utils.xpath import try_match_xpath | 40 | from provisioningserver.utils.xpath import try_match_xpath |
275 | 41 | from twisted.internet.defer import DeferredList | ||
276 | 42 | from twisted.python import log | ||
277 | 43 | |||
278 | 44 | |||
279 | 45 | maaslog = get_maas_logger("tags") | ||
280 | 46 | |||
281 | 33 | 47 | ||
282 | 34 | # The nsmap that XPath expression must be compiled with. This will | 48 | # The nsmap that XPath expression must be compiled with. This will |
283 | 35 | # ensure that expressions like //lshw:something will work correctly. | 49 | # ensure that expressions like //lshw:something will work correctly. |
284 | @@ -39,24 +53,112 @@ | |||
285 | 39 | } | 53 | } |
286 | 40 | 54 | ||
287 | 41 | 55 | ||
288 | 56 | @synchronous | ||
289 | 42 | def populate_tags(tag): | 57 | def populate_tags(tag): |
307 | 43 | """Send worker for all nodegroups an update_node_tags request. | 58 | """Evaluate `tag` for all nodes. |
308 | 44 | """ | 59 | |
309 | 45 | items = { | 60 | This returns a `Deferred` that will fire when all tags have been |
310 | 46 | 'tag_name': tag.name, | 61 | evaluated. This is intended FOR TESTING ONLY because: |
311 | 47 | 'tag_definition': tag.definition, | 62 | |
312 | 48 | 'tag_nsmap': tag_nsmap, | 63 | - You must not use the `Deferred` in the calling thread; it must only be |
313 | 49 | } | 64 | manipulated in the reactor thread. Pretending it's not there is safer |
314 | 50 | # Rather than using NodeGroup.objects.refresh_workers() we call | 65 | than chaining code onto it because it's easy to get wrong. |
315 | 51 | # refresh_worker immediately before we pass the requet. This is mostly for | 66 | |
316 | 52 | # the test suite, where we need the single real worker to switch to the | 67 | - The call may not finish for 10 minutes or more. It is therefore not a |
317 | 53 | # worker for a given nodegroup, before we have that worker process the | 68 | good thing to be waiting for in a web request. |
318 | 54 | # request. | 69 | |
319 | 55 | logger.debug('Refreshing tag definition for %s' % (items,)) | 70 | """ |
320 | 56 | for nodegroup in NodeGroup.objects.all(): | 71 | logger.debug('Evaluating the "%s" tag for all nodes', tag.name) |
321 | 57 | refresh_worker(nodegroup) | 72 | clusters = tuple( |
322 | 58 | raise NotImplementedError( | 73 | (nodegroup.uuid, nodegroup.cluster_name, nodegroup.api_credentials) |
323 | 59 | "Tag evaluation is being ported to RPC.") | 74 | for nodegroup in NodeGroup.objects.all_accepted()) |
324 | 75 | [d] = _do_populate_tags(clusters, tag.name, tag.definition, tag_nsmap) | ||
325 | 76 | return d | ||
326 | 77 | |||
327 | 78 | |||
328 | 79 | @asynchronous(timeout=FOREVER) | ||
329 | 80 | def _do_populate_tags(clusters, tag_name, tag_definition, tag_nsmap): | ||
330 | 81 | """Send RPC calls to each cluster, requesting evaluation of tags. | ||
331 | 82 | |||
332 | 83 | :param clusters: A sequence of ``(uuid, cluster-name, creds)`` tuples for | ||
333 | 84 | each cluster. The name is used for logging only. | ||
334 | 85 | :param tag_name: The name of the tag being evaluated. | ||
335 | 86 | :param tag_definition: The definition of the tag, an XPath expression. | ||
336 | 87 | :oaram tag_nsmap: The NS mapping used to compile the expression. | ||
337 | 88 | """ | ||
338 | 89 | creds = {uuid: creds for uuid, _, creds in clusters} | ||
339 | 90 | tag_nsmap_on_wire = [ | ||
340 | 91 | {"prefix": prefix, "uri": uri} | ||
341 | 92 | for prefix, uri in tag_nsmap.viewitems() | ||
342 | 93 | ] | ||
343 | 94 | |||
344 | 95 | def make_call(client): | ||
345 | 96 | return client( | ||
346 | 97 | EvaluateTag, tag_name=tag_name, tag_definition=tag_definition, | ||
347 | 98 | tag_nsmap=tag_nsmap_on_wire, credentials=creds[client.ident]) | ||
348 | 99 | |||
349 | 100 | def evaluate_tags(clients): | ||
350 | 101 | # Call EvaluateTag on each cluster concurrently. | ||
351 | 102 | return DeferredList( | ||
352 | 103 | (make_call(client) for client in clients), | ||
353 | 104 | consumeErrors=True) | ||
354 | 105 | |||
355 | 106 | def check_results(results): | ||
356 | 107 | for (uuid, name, creds), (success, result) in izip(clusters, results): | ||
357 | 108 | if success: | ||
358 | 109 | maaslog.info( | ||
359 | 110 | "Tag %s (%s) evaluated on cluster %s (%s)", tag_name, | ||
360 | 111 | tag_definition, name, uuid) | ||
361 | 112 | else: | ||
362 | 113 | maaslog.error( | ||
363 | 114 | "Tag %s (%s) could not be evaluated on cluster %s (%s): " | ||
364 | 115 | "%s", tag_name, tag_definition, name, uuid, | ||
365 | 116 | result.getErrorMessage()) | ||
366 | 117 | |||
367 | 118 | d = _get_clients_for_populating_tags( | ||
368 | 119 | [(uuid, name) for (uuid, name, _) in clusters], tag_name) | ||
369 | 120 | d.addCallback(evaluate_tags) | ||
370 | 121 | d.addCallback(check_results) | ||
371 | 122 | d.addErrback(log.err) | ||
372 | 123 | |||
373 | 124 | # Do *not* return a Deferred; the caller is not meant to wait around for | ||
374 | 125 | # this to finish. However, for the sake of testing, we return the Deferred | ||
375 | 126 | # wrapped up in a list so that crochet does not block. | ||
376 | 127 | return [d] | ||
377 | 128 | |||
378 | 129 | |||
379 | 130 | @asynchronous(timeout=FOREVER) | ||
380 | 131 | def _get_clients_for_populating_tags(clusters, tag_name): | ||
381 | 132 | """Obtain RPC clients for the given clusters. | ||
382 | 133 | |||
383 | 134 | :param clusters: A sequence of ``(uuid, name)`` tuples for each cluster. | ||
384 | 135 | The name is used for logging only. | ||
385 | 136 | :param tag_name: The tag for which these clients are being obtained. This | ||
386 | 137 | is used only for logging. | ||
387 | 138 | """ | ||
388 | 139 | # Wait up to `timeout` seconds to obtain clients to all clusters. | ||
389 | 140 | d = DeferredList( | ||
390 | 141 | (getClientFor(uuid, timeout=30) for uuid, _ in clusters), | ||
391 | 142 | consumeErrors=True) | ||
392 | 143 | |||
393 | 144 | def got_clients(results): | ||
394 | 145 | clients = [] | ||
395 | 146 | for (uuid, name), (success, result) in izip(clusters, results): | ||
396 | 147 | if success: | ||
397 | 148 | clients.append(result) | ||
398 | 149 | else: | ||
399 | 150 | # XXX: allenap 2014-09-22 bug=1372544: Filtering out | ||
400 | 151 | # unavailable (or otherwise broken) clients means that tags | ||
401 | 152 | # aren't going to be evaluated for that cluster's nodes. | ||
402 | 153 | # That's bad. It would be good to be able to ask another | ||
403 | 154 | # cluster to evaluate them, but the cluster-side code needs | ||
404 | 155 | # changing to allow that. For now, we just skip. | ||
405 | 156 | maaslog.warning( | ||
406 | 157 | "Cannot evaluate tag %s on cluster %s (%s): %s", | ||
407 | 158 | tag_name, name, uuid, result.getErrorMessage()) | ||
408 | 159 | return clients | ||
409 | 160 | |||
410 | 161 | return d.addCallback(got_clients) | ||
411 | 60 | 162 | ||
412 | 61 | 163 | ||
413 | 62 | def populate_tags_for_single_node(tags, node): | 164 | def populate_tags_for_single_node(tags, node): |
414 | 63 | 165 | ||
415 | === modified file 'src/maasserver/tests/test_populate_tags.py' | |||
416 | --- src/maasserver/tests/test_populate_tags.py 2014-09-23 09:14:04 +0000 | |||
417 | +++ src/maasserver/tests/test_populate_tags.py 2014-09-23 09:14:04 +0000 | |||
418 | @@ -14,11 +14,264 @@ | |||
419 | 14 | __metaclass__ = type | 14 | __metaclass__ = type |
420 | 15 | __all__ = [] | 15 | __all__ = [] |
421 | 16 | 16 | ||
422 | 17 | from itertools import izip | ||
423 | 18 | |||
424 | 19 | from fixtures import FakeLogger | ||
425 | 20 | from maasserver import populate_tags as populate_tags_module | ||
426 | 21 | from maasserver.enum import NODEGROUP_STATUS | ||
427 | 17 | from maasserver.models import Tag | 22 | from maasserver.models import Tag |
429 | 18 | from maasserver.populate_tags import populate_tags_for_single_node | 23 | from maasserver.populate_tags import ( |
430 | 24 | _do_populate_tags, | ||
431 | 25 | _get_clients_for_populating_tags, | ||
432 | 26 | populate_tags, | ||
433 | 27 | populate_tags_for_single_node, | ||
434 | 28 | ) | ||
435 | 29 | from maasserver.rpc.testing.fixtures import MockLiveRegionToClusterRPCFixture | ||
436 | 30 | from maasserver.testing.eventloop import ( | ||
437 | 31 | RegionEventLoopFixture, | ||
438 | 32 | RunningEventLoopFixture, | ||
439 | 33 | ) | ||
440 | 19 | from maasserver.testing.factory import factory | 34 | from maasserver.testing.factory import factory |
441 | 20 | from maasserver.testing.testcase import MAASServerTestCase | 35 | from maasserver.testing.testcase import MAASServerTestCase |
442 | 36 | from maastesting.matchers import MockCalledOnceWith | ||
443 | 21 | from metadataserver.models import commissioningscript | 37 | from metadataserver.models import commissioningscript |
444 | 38 | from mock import ( | ||
445 | 39 | ANY, | ||
446 | 40 | create_autospec, | ||
447 | 41 | sentinel, | ||
448 | 42 | ) | ||
449 | 43 | from provisioningserver.rpc.cluster import EvaluateTag | ||
450 | 44 | from provisioningserver.rpc.common import Client | ||
451 | 45 | from provisioningserver.rpc.testing import ( | ||
452 | 46 | always_fail_with, | ||
453 | 47 | always_succeed_with, | ||
454 | 48 | ) | ||
455 | 49 | from provisioningserver.utils.twisted import asynchronous | ||
456 | 50 | from testtools.deferredruntest import extract_result | ||
457 | 51 | from testtools.monkey import MonkeyPatcher | ||
458 | 52 | from twisted.internet import defer | ||
459 | 53 | |||
460 | 54 | |||
461 | 55 | def make_accepted_NodeGroup(): | ||
462 | 56 | return factory.make_NodeGroup(status=NODEGROUP_STATUS.ACCEPTED) | ||
463 | 57 | |||
464 | 58 | |||
465 | 59 | def make_Tag_without_populating(): | ||
466 | 60 | # Create a tag but prevent evaluation when saving. | ||
467 | 61 | dont_populate = MonkeyPatcher((Tag, "populate_nodes", lambda self: None)) | ||
468 | 62 | return dont_populate.run_with_patches(factory.make_Tag) | ||
469 | 63 | |||
470 | 64 | |||
471 | 65 | class TestGetClientsForPopulatingTags(MAASServerTestCase): | ||
472 | 66 | |||
473 | 67 | def test__returns_no_clients_when_there_are_no_clusters(self): | ||
474 | 68 | tag_name = factory.make_name("tag") | ||
475 | 69 | clients = _get_clients_for_populating_tags([], tag_name) | ||
476 | 70 | self.assertEqual([], clients) | ||
477 | 71 | |||
478 | 72 | def patch_getClientFor(self): | ||
479 | 73 | return self.patch_autospec(populate_tags_module, "getClientFor") | ||
480 | 74 | |||
481 | 75 | def test__returns_no_clients_when_there_is_an_error(self): | ||
482 | 76 | nodegroup_with_connection = make_accepted_NodeGroup() | ||
483 | 77 | nodegroup_without_connection = make_accepted_NodeGroup() | ||
484 | 78 | |||
485 | 79 | def getClientFor(uuid, timeout): | ||
486 | 80 | if uuid == nodegroup_with_connection.uuid: | ||
487 | 81 | return defer.succeed(sentinel.client) | ||
488 | 82 | else: | ||
489 | 83 | return defer.fail(ZeroDivisionError()) | ||
490 | 84 | self.patch_getClientFor().side_effect = getClientFor | ||
491 | 85 | |||
492 | 86 | tag_name = factory.make_name("tag") | ||
493 | 87 | clusters = [ | ||
494 | 88 | (nodegroup_with_connection.uuid, | ||
495 | 89 | nodegroup_with_connection.cluster_name), | ||
496 | 90 | (nodegroup_without_connection.uuid, | ||
497 | 91 | nodegroup_without_connection.cluster_name), | ||
498 | 92 | ] | ||
499 | 93 | clients = _get_clients_for_populating_tags(clusters, tag_name) | ||
500 | 94 | self.assertEqual([sentinel.client], clients) | ||
501 | 95 | |||
502 | 96 | def test__logs_errors_obtaining_clients(self): | ||
503 | 97 | getClientFor = self.patch_getClientFor() | ||
504 | 98 | getClientFor.side_effect = always_fail_with( | ||
505 | 99 | ZeroDivisionError("an error message one would surmise")) | ||
506 | 100 | nodegroup = make_accepted_NodeGroup() | ||
507 | 101 | tag_name = factory.make_name("tag") | ||
508 | 102 | clusters = [(nodegroup.uuid, nodegroup.cluster_name)] | ||
509 | 103 | with FakeLogger("maas") as log: | ||
510 | 104 | _get_clients_for_populating_tags(clusters, tag_name) | ||
511 | 105 | self.assertDocTestMatches( | ||
512 | 106 | "Cannot evaluate tag ... on cluster ... (...): ... surmise", | ||
513 | 107 | log.output) | ||
514 | 108 | |||
515 | 109 | def test__waits_for_clients_for_30_seconds_by_default(self): | ||
516 | 110 | getClientFor = self.patch_getClientFor() | ||
517 | 111 | getClientFor.side_effect = always_succeed_with(sentinel.client) | ||
518 | 112 | nodegroup = make_accepted_NodeGroup() | ||
519 | 113 | tag_name = factory.make_name("tag") | ||
520 | 114 | clusters = [(nodegroup.uuid, nodegroup.cluster_name)] | ||
521 | 115 | clients = _get_clients_for_populating_tags(clusters, tag_name) | ||
522 | 116 | self.assertEqual([sentinel.client], clients) | ||
523 | 117 | self.assertThat( | ||
524 | 118 | getClientFor, MockCalledOnceWith( | ||
525 | 119 | nodegroup.uuid, timeout=30)) | ||
526 | 120 | |||
527 | 121 | def test__obtains_multiple_clients(self): | ||
528 | 122 | getClientFor = self.patch_getClientFor() | ||
529 | 123 | # Return a 2-tuple as a stand-in for a real client. | ||
530 | 124 | getClientFor.side_effect = lambda uuid, timeout: ( | ||
531 | 125 | defer.succeed((sentinel.client, uuid))) | ||
532 | 126 | nodegroups = [make_accepted_NodeGroup() for _ in xrange(3)] | ||
533 | 127 | tag_name = factory.make_name("tag") | ||
534 | 128 | clusters = [(ng.uuid, ng.cluster_name) for ng in nodegroups] | ||
535 | 129 | clients = _get_clients_for_populating_tags(clusters, tag_name) | ||
536 | 130 | self.assertItemsEqual( | ||
537 | 131 | [(sentinel.client, nodegroup.uuid) for nodegroup in nodegroups], | ||
538 | 132 | clients) | ||
539 | 133 | |||
540 | 134 | |||
541 | 135 | class TestDoPopulateTags(MAASServerTestCase): | ||
542 | 136 | |||
543 | 137 | def patch_clients(self, nodegroups): | ||
544 | 138 | clients = [create_autospec(Client, instance=True) for _ in nodegroups] | ||
545 | 139 | for nodegroup, client in izip(nodegroups, clients): | ||
546 | 140 | client.side_effect = always_succeed_with(None) | ||
547 | 141 | client.ident = nodegroup.uuid | ||
548 | 142 | |||
549 | 143 | _get_clients = self.patch_autospec( | ||
550 | 144 | populate_tags_module, "_get_clients_for_populating_tags") | ||
551 | 145 | _get_clients.return_value = defer.succeed(clients) | ||
552 | 146 | |||
553 | 147 | return clients | ||
554 | 148 | |||
555 | 149 | def test__makes_calls_to_each_client_given(self): | ||
556 | 150 | nodegroups = [make_accepted_NodeGroup() for _ in xrange(3)] | ||
557 | 151 | clients = self.patch_clients(nodegroups) | ||
558 | 152 | |||
559 | 153 | tag_name = factory.make_name("tag") | ||
560 | 154 | tag_definition = factory.make_name("definition") | ||
561 | 155 | tag_nsmap_prefix = factory.make_name("prefix") | ||
562 | 156 | tag_nsmap_uri = factory.make_name("uri") | ||
563 | 157 | tag_nsmap = {tag_nsmap_prefix: tag_nsmap_uri} | ||
564 | 158 | |||
565 | 159 | clusters = list( | ||
566 | 160 | (ng.uuid, ng.cluster_name, ng.api_credentials) | ||
567 | 161 | for ng in nodegroups) | ||
568 | 162 | |||
569 | 163 | [d] = _do_populate_tags( | ||
570 | 164 | clusters, tag_name, tag_definition, tag_nsmap) | ||
571 | 165 | |||
572 | 166 | self.assertIsNone(extract_result(d)) | ||
573 | 167 | |||
574 | 168 | for nodegroup, client in izip(nodegroups, clients): | ||
575 | 169 | self.expectThat(client, MockCalledOnceWith( | ||
576 | 170 | EvaluateTag, tag_name=tag_name, tag_definition=tag_definition, | ||
577 | 171 | tag_nsmap=[{"prefix": tag_nsmap_prefix, "uri": tag_nsmap_uri}], | ||
578 | 172 | credentials=nodegroup.api_credentials)) | ||
579 | 173 | |||
580 | 174 | def test__logs_successes(self): | ||
581 | 175 | nodegroups = [make_accepted_NodeGroup()] | ||
582 | 176 | self.patch_clients(nodegroups) | ||
583 | 177 | |||
584 | 178 | tag_name = factory.make_name("tag") | ||
585 | 179 | tag_definition = factory.make_name("definition") | ||
586 | 180 | tag_nsmap = {} | ||
587 | 181 | |||
588 | 182 | clusters = list( | ||
589 | 183 | (ng.uuid, ng.cluster_name, ng.api_credentials) | ||
590 | 184 | for ng in nodegroups) | ||
591 | 185 | |||
592 | 186 | with FakeLogger("maas") as log: | ||
593 | 187 | [d] = _do_populate_tags( | ||
594 | 188 | clusters, tag_name, tag_definition, tag_nsmap) | ||
595 | 189 | self.assertIsNone(extract_result(d)) | ||
596 | 190 | |||
597 | 191 | self.assertDocTestMatches( | ||
598 | 192 | "Tag tag-... (definition-...) evaluated on cluster ... (...)", | ||
599 | 193 | log.output) | ||
600 | 194 | |||
601 | 195 | def test__logs_failures(self): | ||
602 | 196 | nodegroups = [make_accepted_NodeGroup()] | ||
603 | 197 | [client] = self.patch_clients(nodegroups) | ||
604 | 198 | client.side_effect = always_fail_with( | ||
605 | 199 | ZeroDivisionError("splendid day for a spot of cricket")) | ||
606 | 200 | |||
607 | 201 | tag_name = factory.make_name("tag") | ||
608 | 202 | tag_definition = factory.make_name("definition") | ||
609 | 203 | tag_nsmap = {} | ||
610 | 204 | |||
611 | 205 | clusters = list( | ||
612 | 206 | (ng.uuid, ng.cluster_name, ng.api_credentials) | ||
613 | 207 | for ng in nodegroups) | ||
614 | 208 | |||
615 | 209 | with FakeLogger("maas") as log: | ||
616 | 210 | [d] = _do_populate_tags( | ||
617 | 211 | clusters, tag_name, tag_definition, tag_nsmap) | ||
618 | 212 | self.assertIsNone(extract_result(d)) | ||
619 | 213 | |||
620 | 214 | self.assertDocTestMatches( | ||
621 | 215 | "Tag tag-... (definition-...) could not be evaluated ... (...): " | ||
622 | 216 | "splendid day for a spot of cricket", log.output) | ||
623 | 217 | |||
624 | 218 | |||
625 | 219 | class TestPopulateTags(MAASServerTestCase): | ||
626 | 220 | |||
627 | 221 | def patch_do_populate_tags(self): | ||
628 | 222 | do_populate_tags = self.patch_autospec( | ||
629 | 223 | populate_tags_module, "_do_populate_tags") | ||
630 | 224 | do_populate_tags.return_value = [sentinel.d] | ||
631 | 225 | return do_populate_tags | ||
632 | 226 | |||
633 | 227 | def test__calls_do_populate_tags_with_no_clusters(self): | ||
634 | 228 | do_populate_tags = self.patch_do_populate_tags() | ||
635 | 229 | tag = make_Tag_without_populating() | ||
636 | 230 | populate_tags(tag) | ||
637 | 231 | self.assertThat(do_populate_tags, MockCalledOnceWith( | ||
638 | 232 | (), tag.name, tag.definition, populate_tags_module.tag_nsmap)) | ||
639 | 233 | |||
640 | 234 | def test__calls_do_populate_tags_with_clusters(self): | ||
641 | 235 | do_populate_tags = self.patch_do_populate_tags() | ||
642 | 236 | nodegroups = [make_accepted_NodeGroup() for _ in xrange(3)] | ||
643 | 237 | tag = make_Tag_without_populating() | ||
644 | 238 | populate_tags(tag) | ||
645 | 239 | clusters_expected = tuple( | ||
646 | 240 | (ng.uuid, ng.cluster_name, ng.api_credentials) | ||
647 | 241 | for ng in nodegroups) | ||
648 | 242 | self.assertThat(do_populate_tags, MockCalledOnceWith( | ||
649 | 243 | clusters_expected, tag.name, tag.definition, | ||
650 | 244 | populate_tags_module.tag_nsmap)) | ||
651 | 245 | |||
652 | 246 | |||
653 | 247 | class TestPopulateTagsEndToNearlyEnd(MAASServerTestCase): | ||
654 | 248 | |||
655 | 249 | def prepare_live_rpc(self): | ||
656 | 250 | self.useFixture(RegionEventLoopFixture("rpc")) | ||
657 | 251 | self.useFixture(RunningEventLoopFixture()) | ||
658 | 252 | return self.useFixture(MockLiveRegionToClusterRPCFixture()) | ||
659 | 253 | |||
660 | 254 | def test__calls_are_made_to_all_clusters(self): | ||
661 | 255 | rpc_fixture = self.prepare_live_rpc() | ||
662 | 256 | nodegroups = [make_accepted_NodeGroup() for _ in xrange(3)] | ||
663 | 257 | protocols = [] | ||
664 | 258 | for nodegroup in nodegroups: | ||
665 | 259 | protocol = rpc_fixture.makeCluster(nodegroup, EvaluateTag) | ||
666 | 260 | protocol.EvaluateTag.side_effect = always_succeed_with({}) | ||
667 | 261 | protocols.append(protocol) | ||
668 | 262 | tag = make_Tag_without_populating() | ||
669 | 263 | |||
670 | 264 | d = populate_tags(tag) | ||
671 | 265 | |||
672 | 266 | # `d` is a testing-only convenience. We must wait for it to fire, and | ||
673 | 267 | # we must do that from the reactor thread. | ||
674 | 268 | wait_for_populate = asynchronous(lambda: d) | ||
675 | 269 | wait_for_populate().wait(10) | ||
676 | 270 | |||
677 | 271 | for nodegroup, protocol in izip(nodegroups, protocols): | ||
678 | 272 | self.expectThat(protocol.EvaluateTag, MockCalledOnceWith( | ||
679 | 273 | protocol, tag_name=tag.name, tag_definition=tag.definition, | ||
680 | 274 | tag_nsmap=ANY, credentials=nodegroup.api_credentials)) | ||
681 | 22 | 275 | ||
682 | 23 | 276 | ||
683 | 24 | class TestPopulateTagsForSingleNode(MAASServerTestCase): | 277 | class TestPopulateTagsForSingleNode(MAASServerTestCase): |
Looks like the Tag API tests suffered from doing unit-testing at an integration-testing level. Good thing to see some tests lose the XML parsing. The last of the API tests you edited looks like it might benefit from self.expectThat().
.
In the docstring for populate_tags, can you say why the return value is not meant to be used in production? Would there be any harm? The caller doesn't want or need to block on it, of course, but that sort of seems like the caller's problem.
.
The docstring for _do_populate_tags confuses me no end. Did you mean to rewrite it? There's also a typo in there: “oaram.”
More dangerous, of course, is the terminal preposition. Julian will never forgive you.
.
As you know I hate nontrivial nested callbacks with implicit closures. Was there no way to use @inlineCallbacks in _do_populate_tags?
.
Why “list((uuid, name) for (uuid, name, _) in clusters)” and not “[(uuid, name) for (uuid, name, _) in clusters]”? Just to avoid variables leaking out of the comprehension? I hate that too, but it doesn't seem worth our list comprehensions. To me, the diversity in closing brackets, braces, and parentheses provides a valuable visual cue of what the nesting levels are.
.
Can you add a bug number to the XXX comment in _get_clients_ for_populating_ tags?
.
test__returns_ no_clients_ when_there_ is_an_error does not look right to me. I thought _get_clients_ for_populating_ tags was supposed to return the successfully obtained clients, and only left out clients for any clusters that it couldn't get clients for. The tests don't cover the difference between those two things AFAICS.
.
Shouldn't test__obtains_ multiple_ clients check somehow that it gets clients _for the given clusters_, not just that it gets three clients?
.
In TestDoPopulateT ags.patch_ clients, consider extracting a factory for a fake client. Saves you having to loop over the clients and modify them after you've already created the list.
.
Could TestPopulateTag s.patch_ do_populate_ tags use patch_autospec?
.
I find line breaks like these inconsistent with what we do elsewhere:
(), tag.name, tag.definition, populate_
for nodegroup, protocol in izip(nodegroups, protocols):
self. expectThat( protocol. EvaluateTag, MockCalledOnceWith(
protocol, tag_name=tag.name, tag_definition= tag.definition,
tag_nsmap= ANY, credentials= nodegroup. api_credentials ))
If you're going to line-break the arguments list to self.assertThat “inside one of the arguments,” I think that still makes it a multi-line arguments list:
...