Merge lp:~allenap/maas/lldp-repopulate-tags-after-commissioning into lp:~maas-committers/maas/trunk
- lldp-repopulate-tags-after-commissioning
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Gavin Panella |
Approved revision: | no longer in the source branch. |
Merged at revision: | 1687 |
Proposed branch: | lp:~allenap/maas/lldp-repopulate-tags-after-commissioning |
Merge into: | lp:~maas-committers/maas/trunk |
Prerequisite: | lp:~allenap/maas/pass-script-result-to-commissioning-hooks |
Diff against target: |
370 lines (+136/-66) 6 files modified
src/maasserver/models/tests/test_tag.py (+8/-29) src/maasserver/tests/test_api_tag.py (+10/-31) src/metadataserver/api.py (+8/-3) src/metadataserver/models/commissioningscript.py (+33/-1) src/metadataserver/models/tests/test_commissioningscript.py (+66/-2) src/metadataserver/tests/test_api.py (+11/-0) |
To merge this branch: | bzr merge lp:~allenap/maas/lldp-repopulate-tags-after-commissioning |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | Approve | ||
Review via email: mp+189659@code.launchpad.net |
Commit message
Repopulate tags when commissioning finishes.
Also removes some temporary shims used during testing, and replaces them with the new and tested inject_
Description of the change
Gavin Panella (allenap) wrote : | # |
Thank you!
> The advertised change looks good, and I like the use of sentinel. The
> documentation for inject_result() is a bit more self-referential than I would
> like, but that's easily fixed.
I've improved that docstring.
>
> However I have strong doubts about this part:
>
> 171 === modified file 'src/metadatase
> 172 --- src/metadataser
> 173 +++ src/metadataser
> 174 @@ -252,11 +252,16 @@
>
> 177 # When moving to a terminal state, remove the allocation.
> 178 - if target_status is not None:
> 179 - node.owner = None
> 180 + node.owner = None
>
> This looks significant: you're dropping the node's ownership when it starts
> commissioning, instead of when it finishes. That doesn't seem to belong in
> the branch as described. Is it deliberate? Were you trying to fix the owned-
> ready-nodes problem?
>
> If it is deliberate, have you tried it in practice? I think it's worth at
> least a comment and possibly a test for the non-terminal signal() case. To be
> honest the change should have affected an existing test in the first place.
If I add a bit more context you can see why it's harmless (or you can
point out where I'm mistaken):
if target_status in (None, node.status):
# No status change. Nothing to be done.
return rc.ALL_OK
node.status = target_status
# When moving to a terminal state, remove the allocation.
if target_status is not None:
In the last conditional here, target_status will never be None,
because of the conditional further up.
>
> Right below that:
>
> 183 + # When moving to a successful terminal state, recalculate
> tags.
> 184 + from maasserver.
> populate_
> 185 + from maasserver.
> 186 + populate_
>
> Please make it a habit to keep the imports at the top of the function, so that
> there are no corner cases where tests might forget to exercise them.
Oops, that was an omission rather than anything deliberate. When
coding I often pull in imports right where I need them to avoid
breaking concentration, but I sometimes forget to move them up to the
top later on. Thanks for spotting this.
>
> .
>
> In src/metadataser
> TestInjectResult uses factory.
> Consider using factory.make_name() instead. It's specialized for identifier-
> style strings, and accepts a helpful prefix. The prefix makes the name
> recognizable in failure output, without making the full name deterministic.
Done. I'd forgotten about make_name().
>
> .
>
> As far as I can see, the tests for inject_lshw_result and inject_lldp_result
> would fail to notice an incompatible API change. Can you sprinkle that same
> mocking magic you used before, where the mock objects mimick the exact
> signatures of the originals?
Done. Fwiw, it's mock.create_
Jeroen T. Vermeulen (jtv) wrote : | # |
All good then, thanks!
For things like the forgotten temporary import, you may benefit from the habit of reviewing your own diff before submitting an MP. SmartBear's research with IBM/Rational (if memory serves) found a massive quality difference between branches that had been self-reviewed and branches that hadn't.
MAAS Lander (maas-lander) wrote : | # |
No proposals found for merge of https:/
Preview Diff
1 | === modified file 'src/maasserver/models/tests/test_tag.py' |
2 | --- src/maasserver/models/tests/test_tag.py 2013-10-08 11:51:31 +0000 |
3 | +++ src/maasserver/models/tests/test_tag.py 2013-10-09 10:11:44 +0000 |
4 | @@ -18,28 +18,7 @@ |
5 | from maasserver.models.tag import Tag |
6 | from maasserver.testing.factory import factory |
7 | from maasserver.testing.testcase import MAASServerTestCase |
8 | -from metadataserver.fields import Bin |
9 | -from metadataserver.models.commissioningscript import ( |
10 | - LSHW_OUTPUT_NAME, |
11 | - update_hardware_details, |
12 | - ) |
13 | -from metadataserver.models.nodecommissionresult import NodeCommissionResult |
14 | - |
15 | - |
16 | -def set_hardware_details(node, xmlbytes): |
17 | - # FIXME: Gavin Panella, bug=1235174, 2013-10-04 |
18 | - # |
19 | - # This is a temporary shim to allow removal of |
20 | - # Node.set_hardware_details(), and thus the use of the |
21 | - # Node.hardware_details field. |
22 | - # |
23 | - # It is being replaced by storing the results only in |
24 | - # NodeCommissionResult, and reprocessing tags using all |
25 | - # relevant probed details. |
26 | - assert isinstance(xmlbytes, bytes) |
27 | - NodeCommissionResult.objects.store_data( |
28 | - node, LSHW_OUTPUT_NAME, script_result=0, data=Bin(xmlbytes)) |
29 | - update_hardware_details(node, xmlbytes, 0) |
30 | +from metadataserver.models.commissioningscript import inject_lshw_result |
31 | |
32 | |
33 | class TagTest(MAASServerTestCase): |
34 | @@ -87,18 +66,18 @@ |
35 | |
36 | def test_applies_tags_to_nodes(self): |
37 | node1 = factory.make_node() |
38 | - set_hardware_details(node1, b'<node><child /></node>') |
39 | + inject_lshw_result(node1, b'<node><child /></node>') |
40 | node2 = factory.make_node() |
41 | - set_hardware_details(node2, b'<node />') |
42 | + inject_lshw_result(node2, b'<node />') |
43 | tag = factory.make_tag(definition='//node/child') |
44 | self.assertItemsEqual([tag.name], node1.tag_names()) |
45 | self.assertItemsEqual([], node2.tag_names()) |
46 | |
47 | def test_removes_old_values(self): |
48 | node1 = factory.make_node() |
49 | - set_hardware_details(node1, b'<node><foo /></node>') |
50 | + inject_lshw_result(node1, b'<node><foo /></node>') |
51 | node2 = factory.make_node() |
52 | - set_hardware_details(node2, b'<node><bar /></node>') |
53 | + inject_lshw_result(node2, b'<node><bar /></node>') |
54 | tag = factory.make_tag(definition='//node/foo') |
55 | self.assertItemsEqual([tag.name], node1.tag_names()) |
56 | self.assertItemsEqual([], node2.tag_names()) |
57 | @@ -114,9 +93,9 @@ |
58 | |
59 | def test_doesnt_touch_other_tags(self): |
60 | node1 = factory.make_node() |
61 | - set_hardware_details(node1, b'<node><foo /></node>') |
62 | + inject_lshw_result(node1, b'<node><foo /></node>') |
63 | node2 = factory.make_node() |
64 | - set_hardware_details(node2, b'<node><bar /></node>') |
65 | + inject_lshw_result(node2, b'<node><bar /></node>') |
66 | tag1 = factory.make_tag(definition='//node/foo') |
67 | self.assertItemsEqual([tag1.name], node1.tag_names()) |
68 | self.assertItemsEqual([], node2.tag_names()) |
69 | @@ -126,7 +105,7 @@ |
70 | |
71 | def test_rollsback_invalid_xpath(self): |
72 | node = factory.make_node() |
73 | - set_hardware_details(node, b'<node><foo /></node>') |
74 | + inject_lshw_result(node, b'<node><foo /></node>') |
75 | tag = factory.make_tag(definition='//node/foo') |
76 | self.assertItemsEqual([tag.name], node.tag_names()) |
77 | tag.definition = 'invalid::tag' |
78 | |
79 | === modified file 'src/maasserver/tests/test_api_tag.py' |
80 | --- src/maasserver/tests/test_api_tag.py 2013-10-08 11:51:31 +0000 |
81 | +++ src/maasserver/tests/test_api_tag.py 2013-10-09 10:11:44 +0000 |
82 | @@ -27,28 +27,7 @@ |
83 | ) |
84 | from maasserver.testing.factory import factory |
85 | from maasserver.testing.oauthclient import OAuthAuthenticatedClient |
86 | -from metadataserver.fields import Bin |
87 | -from metadataserver.models.commissioningscript import ( |
88 | - LSHW_OUTPUT_NAME, |
89 | - update_hardware_details, |
90 | - ) |
91 | -from metadataserver.models.nodecommissionresult import NodeCommissionResult |
92 | - |
93 | - |
94 | -def set_hardware_details(node, xmlbytes): |
95 | - # FIXME: Gavin Panella, bug=1235174, 2013-10-04 |
96 | - # |
97 | - # This is a temporary shim to allow removal of |
98 | - # Node.set_hardware_details(), and thus the use of the |
99 | - # Node.hardware_details field. |
100 | - # |
101 | - # It is being replaced by storing the results only in |
102 | - # NodeCommissionResult, and reprocessing tags using all |
103 | - # relevant probed details. |
104 | - assert isinstance(xmlbytes, bytes) |
105 | - NodeCommissionResult.objects.store_data( |
106 | - node, LSHW_OUTPUT_NAME, script_result=0, data=Bin(xmlbytes)) |
107 | - update_hardware_details(node, xmlbytes, 0) |
108 | +from metadataserver.models.commissioningscript import inject_lshw_result |
109 | |
110 | |
111 | class TestTagAPI(APITestCase): |
112 | @@ -117,9 +96,9 @@ |
113 | |
114 | def test_PUT_updates_node_associations(self): |
115 | node1 = factory.make_node() |
116 | - set_hardware_details(node1, b'<node><foo/></node>') |
117 | + inject_lshw_result(node1, b'<node><foo/></node>') |
118 | node2 = factory.make_node() |
119 | - set_hardware_details(node2, b'<node><bar/></node>') |
120 | + inject_lshw_result(node2, b'<node><bar/></node>') |
121 | tag = factory.make_tag(definition='//node/foo') |
122 | self.assertItemsEqual([tag.name], node1.tag_names()) |
123 | self.assertItemsEqual([], node2.tag_names()) |
124 | @@ -155,9 +134,9 @@ |
125 | def test_GET_nodes_hides_invisible_nodes(self): |
126 | user2 = factory.make_user() |
127 | node1 = factory.make_node() |
128 | - set_hardware_details(node1, b'<node><foo/></node>') |
129 | + inject_lshw_result(node1, b'<node><foo/></node>') |
130 | node2 = factory.make_node(status=NODE_STATUS.ALLOCATED, owner=user2) |
131 | - set_hardware_details(node2, b'<node><bar/></node>') |
132 | + inject_lshw_result(node2, b'<node><bar/></node>') |
133 | tag = factory.make_tag(definition='//node') |
134 | response = self.client.get(self.get_tag_uri(tag), {'op': 'nodes'}) |
135 | |
136 | @@ -176,7 +155,7 @@ |
137 | def test_PUT_invalid_definition(self): |
138 | self.become_admin() |
139 | node = factory.make_node() |
140 | - set_hardware_details(node, b'<node ><child/></node>') |
141 | + inject_lshw_result(node, b'<node ><child/></node>') |
142 | tag = factory.make_tag(definition='//child') |
143 | self.assertItemsEqual([tag.name], node.tag_names()) |
144 | response = self.client_put( |
145 | @@ -336,9 +315,9 @@ |
146 | tag = factory.make_tag(definition='//foo/bar') |
147 | # Only one node matches the tag definition, rebuilding should notice |
148 | node_matching = factory.make_node() |
149 | - set_hardware_details(node_matching, b'<foo><bar/></foo>') |
150 | + inject_lshw_result(node_matching, b'<foo><bar/></foo>') |
151 | node_bogus = factory.make_node() |
152 | - set_hardware_details(node_bogus, b'<foo/>') |
153 | + inject_lshw_result(node_bogus, b'<foo/>') |
154 | node_matching.tags.add(tag) |
155 | node_bogus.tags.add(tag) |
156 | self.assertItemsEqual( |
157 | @@ -480,10 +459,10 @@ |
158 | def test_POST_new_populates_nodes(self): |
159 | self.become_admin() |
160 | node1 = factory.make_node() |
161 | - set_hardware_details(node1, b'<node><child/></node>') |
162 | + inject_lshw_result(node1, b'<node><child/></node>') |
163 | # Create another node that doesn't have a 'child' |
164 | node2 = factory.make_node() |
165 | - set_hardware_details(node2, b'<node/>') |
166 | + inject_lshw_result(node2, b'<node/>') |
167 | self.assertItemsEqual([], node1.tag_names()) |
168 | self.assertItemsEqual([], node2.tag_names()) |
169 | name = factory.getRandomString() |
170 | |
171 | === modified file 'src/metadataserver/api.py' |
172 | --- src/metadataserver/api.py 2013-10-07 15:19:41 +0000 |
173 | +++ src/metadataserver/api.py 2013-10-09 10:11:44 +0000 |
174 | @@ -51,6 +51,8 @@ |
175 | Node, |
176 | SSHKey, |
177 | ) |
178 | +from maasserver.models.tag import Tag |
179 | +from maasserver.populate_tags import populate_tags_for_single_node |
180 | from maasserver.preseed import ( |
181 | get_curtin_userdata, |
182 | get_enlist_preseed, |
183 | @@ -252,11 +254,14 @@ |
184 | |
185 | node.status = target_status |
186 | # When moving to a terminal state, remove the allocation. |
187 | - if target_status is not None: |
188 | - node.owner = None |
189 | + node.owner = None |
190 | node.error = request.POST.get('error', '') |
191 | + |
192 | + # When moving to a successful terminal state, recalculate tags. |
193 | + populate_tags_for_single_node(Tag.objects.all(), node) |
194 | + |
195 | + # Done. |
196 | node.save() |
197 | - |
198 | return rc.ALL_OK |
199 | |
200 | @operation(idempotent=False) |
201 | |
202 | === modified file 'src/metadataserver/models/commissioningscript.py' |
203 | --- src/metadataserver/models/commissioningscript.py 2013-10-08 13:17:51 +0000 |
204 | +++ src/metadataserver/models/commissioningscript.py 2013-10-09 10:11:44 +0000 |
205 | @@ -16,6 +16,9 @@ |
206 | __all__ = [ |
207 | 'BUILTIN_COMMISSIONING_SCRIPTS', |
208 | 'CommissioningScript', |
209 | + 'inject_lldp_result', |
210 | + 'inject_lshw_result', |
211 | + 'inject_result', |
212 | 'LLDP_OUTPUT_NAME', |
213 | 'LSHW_OUTPUT_NAME', |
214 | ] |
215 | @@ -41,7 +44,11 @@ |
216 | from maasserver.fields import MAC |
217 | from maasserver.models.tag import Tag |
218 | from metadataserver import DefaultMeta |
219 | -from metadataserver.fields import BinaryField |
220 | +from metadataserver.fields import ( |
221 | + Bin, |
222 | + BinaryField, |
223 | + ) |
224 | +from metadataserver.models.nodecommissionresult import NodeCommissionResult |
225 | |
226 | |
227 | logger = logging.getLogger(__name__) |
228 | @@ -436,3 +443,28 @@ |
229 | |
230 | name = CharField(max_length=255, null=False, editable=True, unique=True) |
231 | content = BinaryField(null=False) |
232 | + |
233 | + |
234 | +def inject_result(node, name, output, exit_status=0): |
235 | + """Inject a `name` result and trigger related hooks, if any. |
236 | + |
237 | + `output` and `exit_status` are recorded as `NodeCommissionResult` |
238 | + instances with the `name` given. A built-in hook is then searched |
239 | + for; if found, it is invoked. |
240 | + """ |
241 | + assert isinstance(output, bytes) |
242 | + NodeCommissionResult.objects.store_data( |
243 | + node, name, script_result=exit_status, data=Bin(output)) |
244 | + if name in BUILTIN_COMMISSIONING_SCRIPTS: |
245 | + postprocess_hook = BUILTIN_COMMISSIONING_SCRIPTS[name]['hook'] |
246 | + postprocess_hook(node=node, output=output, exit_status=exit_status) |
247 | + |
248 | + |
249 | +def inject_lshw_result(node, output, exit_status=0): |
250 | + """Convenience to call `inject_result(name=LSHW_OUTPUT_NAME, ...)`.""" |
251 | + return inject_result(node, LSHW_OUTPUT_NAME, output, exit_status) |
252 | + |
253 | + |
254 | +def inject_lldp_result(node, output, exit_status=0): |
255 | + """Convenience to call `inject_result(name=LLDP_OUTPUT_NAME, ...)`.""" |
256 | + return inject_result(node, LLDP_OUTPUT_NAME, output, exit_status) |
257 | |
258 | === modified file 'src/metadataserver/models/tests/test_commissioningscript.py' |
259 | --- src/metadataserver/models/tests/test_commissioningscript.py 2013-10-08 13:43:12 +0000 |
260 | +++ src/metadataserver/models/tests/test_commissioningscript.py 2013-10-09 10:11:44 +0000 |
261 | @@ -50,14 +50,28 @@ |
262 | from metadataserver.models.commissioningscript import ( |
263 | ARCHIVE_PREFIX, |
264 | extract_router_mac_addresses, |
265 | + inject_lldp_result, |
266 | + inject_lshw_result, |
267 | + inject_result, |
268 | + LLDP_OUTPUT_NAME, |
269 | + LSHW_OUTPUT_NAME, |
270 | make_function_call_script, |
271 | set_node_routers, |
272 | set_virtual_tag, |
273 | update_hardware_details, |
274 | ) |
275 | -from mock import call |
276 | +from metadataserver.models.nodecommissionresult import NodeCommissionResult |
277 | +from mock import ( |
278 | + call, |
279 | + create_autospec, |
280 | + Mock, |
281 | + sentinel, |
282 | + ) |
283 | from testtools.content import text_content |
284 | -from testtools.matchers import DocTestMatches |
285 | +from testtools.matchers import ( |
286 | + DocTestMatches, |
287 | + MatchesStructure, |
288 | + ) |
289 | |
290 | |
291 | def open_tarfile(content): |
292 | @@ -332,6 +346,56 @@ |
293 | self.assertItemsEqual(routers_before, routers_after) |
294 | |
295 | |
296 | +class TestInjectResult(MAASServerTestCase): |
297 | + |
298 | + def test_inject_result_stores_data(self): |
299 | + node = factory.make_node() |
300 | + name = factory.make_name("result") |
301 | + output = factory.getRandomBytes() |
302 | + exit_status = next(factory.random_octets) |
303 | + |
304 | + inject_result(node, name, output, exit_status) |
305 | + |
306 | + self.assertThat( |
307 | + NodeCommissionResult.objects.get(node=node, name=name), |
308 | + MatchesStructure.byEquality( |
309 | + node=node, name=name, script_result=exit_status, |
310 | + data=output)) |
311 | + |
312 | + def test_inject_result_calls_hook(self): |
313 | + node = factory.make_node() |
314 | + name = factory.make_name("result") |
315 | + output = factory.getRandomBytes() |
316 | + exit_status = next(factory.random_octets) |
317 | + hook = Mock() |
318 | + self.patch( |
319 | + cs_module, "BUILTIN_COMMISSIONING_SCRIPTS", |
320 | + {name: {"hook": hook}}) |
321 | + |
322 | + inject_result(node, name, output, exit_status) |
323 | + |
324 | + hook.assert_called_once_with( |
325 | + node=node, output=output, exit_status=exit_status) |
326 | + |
327 | + def inject_lshw_result(self): |
328 | + # inject_lshw_result() just calls through to inject_result(). |
329 | + inject_result = self.patch( |
330 | + cs_module, "inject_result", |
331 | + create_autospec(cs_module.inject_result)) |
332 | + inject_lshw_result(sentinel.node, sentinel.output, sentinel.status) |
333 | + inject_result.assert_called_once_with( |
334 | + sentinel.node, LSHW_OUTPUT_NAME, sentinel.output, sentinel.status) |
335 | + |
336 | + def inject_lldp_result(self): |
337 | + # inject_lldp_result() just calls through to inject_result(). |
338 | + inject_result = self.patch( |
339 | + cs_module, "inject_result", |
340 | + create_autospec(cs_module.inject_result)) |
341 | + inject_lldp_result(sentinel.node, sentinel.output, sentinel.status) |
342 | + inject_result.assert_called_once_with( |
343 | + sentinel.node, LLDP_OUTPUT_NAME, sentinel.output, sentinel.status) |
344 | + |
345 | + |
346 | class TestSetVirtualTag(MAASServerTestCase): |
347 | |
348 | def getVirtualTag(self): |
349 | |
350 | === modified file 'src/metadataserver/tests/test_api.py' |
351 | --- src/metadataserver/tests/test_api.py 2013-10-07 15:19:41 +0000 |
352 | +++ src/metadataserver/tests/test_api.py 2013-10-09 10:11:44 +0000 |
353 | @@ -436,6 +436,17 @@ |
354 | self.assertEqual( |
355 | NODE_STATUS.COMMISSIONING, reload_object(node).status) |
356 | |
357 | + def test_signaling_commissioning_OK_repopulates_tags(self): |
358 | + populate_tags_for_single_node = self.patch( |
359 | + api, "populate_tags_for_single_node") |
360 | + node = factory.make_node(status=NODE_STATUS.COMMISSIONING) |
361 | + client = make_node_client(node) |
362 | + response = call_signal(client, status='OK', script_result='0') |
363 | + self.assertEqual(httplib.OK, response.status_code) |
364 | + self.assertEqual(NODE_STATUS.READY, reload_object(node).status) |
365 | + from mock import ANY |
366 | + populate_tags_for_single_node.assert_called_once_with(ANY, node) |
367 | + |
368 | def test_signaling_requires_status_code(self): |
369 | node = factory.make_node(status=NODE_STATUS.COMMISSIONING) |
370 | client = make_node_client(node=node) |
The advertised change looks good, and I like the use of sentinel. The documentation for inject_result() is a bit more self-referential than I would like, but that's easily fixed.
However I have strong doubts about this part:
171 === modified file 'src/metadatase rver/api. py' ver/api. py 2013-10-07 16:34:08 +0000 ver/api. py 2013-10-07 16:34:08 +0000
172 --- src/metadataser
173 +++ src/metadataser
174 @@ -252,11 +252,16 @@
177 # When moving to a terminal state, remove the allocation.
178 - if target_status is not None:
179 - node.owner = None
180 + node.owner = None
This looks significant: you're dropping the node's ownership when it starts commissioning, instead of when it finishes. That doesn't seem to belong in the branch as described. Is it deliberate? Were you trying to fix the owned-ready-nodes problem?
If it is deliberate, have you tried it in practice? I think it's worth at least a comment and possibly a test for the non-terminal signal() case. To be honest the change should have affected an existing test in the first place.
Right below that:
183 + # When moving to a successful terminal state, recalculate tags. populate_ tags import populate_ tags_for_ single_ node models. tag import Tag tags_for_ single_ node(Tag. objects. all(), node)
184 + from maasserver.
185 + from maasserver.
186 + populate_
Please make it a habit to keep the imports at the top of the function, so that there are no corner cases where tests might forget to exercise them.
.
In src/metadataser ver/models/ tests/test_ commissioningsc ript.py, TestInjectResult uses factory. getRandomString () to create randomized names. Consider using factory.make_name() instead. It's specialized for identifier-style strings, and accepts a helpful prefix. The prefix makes the name recognizable in failure output, without making the full name deterministic.
.
As far as I can see, the tests for inject_lshw_result and inject_lldp_result would fail to notice an incompatible API change. Can you sprinkle that same mocking magic you used before, where the mock objects mimick the exact signatures of the originals?