Merge lp:~allenap/maas/lldp-repopulate-tags-after-commissioning into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
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
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_lshw_result().

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

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/metadataserver/api.py'
172 --- src/metadataserver/api.py 2013-10-07 16:34:08 +0000
173 +++ src/metadataserver/api.py 2013-10-07 16:34:08 +0000
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.
184 + from maasserver.populate_tags import populate_tags_for_single_node
185 + from maasserver.models.tag import Tag
186 + populate_tags_for_single_node(Tag.objects.all(), node)

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/metadataserver/models/tests/test_commissioningscript.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?

review: Needs Fixing
Revision history for this message
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/metadataserver/api.py'
> 172     --- src/metadataserver/api.py   2013-10-07 16:34:08 +0000
> 173     +++ src/metadataserver/api.py   2013-10-07 16:34:08 +0000
> 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:
            node.owner = 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_tags import
> populate_tags_for_single_node
> 185     +        from maasserver.models.tag import Tag
> 186     +        populate_tags_for_single_node(Tag.objects.all(), node)
>
> 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/metadataserver/models/tests/test_commissioningscript.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.

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_autospec().

Revision history for this message
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.

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)