Merge lp:~allenap/maas/rpc-tags-region 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: 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
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).

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

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 TestDoPopulateTags.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 TestPopulateTags.patch_do_populate_tags use patch_autospec?

.

I find line breaks like these inconsistent with what we do elsewhere:

        self.assertThat(do_populate_tags, MockCalledOnceWith(
            (), tag.name, tag.definition, populate_tags_module.tag_nsmap))

        self.assertThat(do_populate_tags, MockCalledOnceWith(
            clusters_expected, tag.name, tag.definition,
            populate_tags_module.tag_nsmap))

        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:

        self.assertThat(
            do_populate_tags,
            MockCalledOnceWith((), tag.name, tag.definition, populate_tags_module.tag_nsmap))

        self.assertThat(
            do_populate_tags,
            MockCalledOnceWith(
                clusters_expected, tag.name, tag.definition,
                populate_tags_module.tag_nsmap))

 ...

Read more...

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :
Download full text (5.5 KiB)

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 MatchesStructure.byEquality().

>
> .
>
> 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_for_populating_tags
(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_for_populating_tags?

Yes! https://bugs.launchpad.net/maas/+bug/1372544

>
> .
>
> 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.

Okay, done.

>
> .
>
> Shouldn't test__obtains_multiple_clients check somehow that it gets
> clients _for the given clusters_, not just that it gets three clients?

Done.

>
> .
>
> In TestDoPopulateTags.patch_clients, consider extracting a factory for
> a fake client. Saves you having to loop over the clients and modify
> th...

Read more...

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

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

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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/api/tests/test_tag.py'
--- src/maasserver/api/tests/test_tag.py 2014-09-23 09:14:04 +0000
+++ src/maasserver/api/tests/test_tag.py 2014-09-23 09:14:04 +0000
@@ -28,7 +28,21 @@
28from maasserver.testing.factory import factory28from maasserver.testing.factory import factory
29from maasserver.testing.oauthclient import OAuthAuthenticatedClient29from maasserver.testing.oauthclient import OAuthAuthenticatedClient
30from maasserver.testing.orm import reload_object30from maasserver.testing.orm import reload_object
31from maastesting.matchers import (
32 MockCalledOnceWith,
33 MockCallsMatch,
34 )
31from metadataserver.models.commissioningscript import inject_lshw_result35from metadataserver.models.commissioningscript import inject_lshw_result
36from mock import (
37 ANY,
38 call,
39 )
40from testtools.matchers import MatchesStructure
41
42
43def patch_populate_tags(test):
44 from maasserver import populate_tags
45 return test.patch_autospec(populate_tags, "populate_tags")
3246
3347
34class TestTagAPI(APITestCase):48class TestTagAPI(APITestCase):
@@ -104,22 +118,15 @@
104 self.assertTrue(Tag.objects.filter(name='new-tag-name').exists())118 self.assertTrue(Tag.objects.filter(name='new-tag-name').exists())
105119
106 def test_PUT_updates_node_associations(self):120 def test_PUT_updates_node_associations(self):
107 self.skip("Tag evaluation is being ported to RPC.")121 populate_tags = patch_populate_tags(self)
108
109 node1 = factory.make_Node()
110 inject_lshw_result(node1, b'<node><foo/></node>')
111 node2 = factory.make_Node()
112 inject_lshw_result(node2, b'<node><bar/></node>')
113 tag = factory.make_Tag(definition='//node/foo')122 tag = factory.make_Tag(definition='//node/foo')
114 self.assertItemsEqual([tag.name], node1.tag_names())123 self.expectThat(populate_tags, MockCalledOnceWith(tag))
115 self.assertItemsEqual([], node2.tag_names())
116 self.become_admin()124 self.become_admin()
117 response = self.client_put(125 response = self.client_put(
118 self.get_tag_uri(tag),126 self.get_tag_uri(tag),
119 {'definition': '//node/bar'})127 {'definition': '//node/bar'})
120 self.assertEqual(httplib.OK, response.status_code)128 self.assertEqual(httplib.OK, response.status_code)
121 self.assertItemsEqual([], node1.tag_names())129 self.expectThat(populate_tags, MockCallsMatch(call(tag), call(tag)))
122 self.assertItemsEqual([tag.name], node2.tag_names())
123130
124 def test_GET_nodes_with_no_nodes(self):131 def test_GET_nodes_with_no_nodes(self):
125 tag = factory.make_Tag()132 tag = factory.make_Tag()
@@ -143,14 +150,13 @@
143 [r['system_id'] for r in parsed_result])150 [r['system_id'] for r in parsed_result])
144151
145 def test_GET_nodes_hides_invisible_nodes(self):152 def test_GET_nodes_hides_invisible_nodes(self):
146 self.skip("Tag evaluation is being ported to RPC.")
147
148 user2 = factory.make_User()153 user2 = factory.make_User()
149 node1 = factory.make_Node()154 node1 = factory.make_Node()
150 inject_lshw_result(node1, b'<node><foo/></node>')
151 node2 = factory.make_Node(status=NODE_STATUS.ALLOCATED, owner=user2)155 node2 = factory.make_Node(status=NODE_STATUS.ALLOCATED, owner=user2)
152 inject_lshw_result(node2, b'<node><bar/></node>')156 tag = factory.make_Tag()
153 tag = factory.make_Tag(definition='//node')157 node1.tags.add(tag)
158 node2.tags.add(tag)
159
154 response = self.client.get(self.get_tag_uri(tag), {'op': 'nodes'})160 response = self.client.get(self.get_tag_uri(tag), {'op': 'nodes'})
155161
156 self.assertEqual(httplib.OK, response.status_code)162 self.assertEqual(httplib.OK, response.status_code)
@@ -166,12 +172,11 @@
166 [r['system_id'] for r in parsed_result])172 [r['system_id'] for r in parsed_result])
167173
168 def test_PUT_invalid_definition(self):174 def test_PUT_invalid_definition(self):
169 self.skip("Tag evaluation is being ported to RPC.")
170
171 self.become_admin()175 self.become_admin()
172 node = factory.make_Node()176 node = factory.make_Node()
173 inject_lshw_result(node, b'<node ><child/></node>')177 inject_lshw_result(node, b'<node ><child/></node>')
174 tag = factory.make_Tag(definition='//child')178 tag = factory.make_Tag(definition='//child')
179 node.tags.add(tag)
175 self.assertItemsEqual([tag.name], node.tag_names())180 self.assertItemsEqual([tag.name], node.tag_names())
176 response = self.client_put(181 response = self.client_put(
177 self.get_tag_uri(tag), {'definition': 'invalid::tag'})182 self.get_tag_uri(tag), {'definition': 'invalid::tag'})
@@ -338,24 +343,15 @@
338 self.assertItemsEqual([], node.tags.all())343 self.assertItemsEqual([], node.tags.all())
339344
340 def test_POST_rebuild_rebuilds_node_mapping(self):345 def test_POST_rebuild_rebuilds_node_mapping(self):
341 self.skip("Tag evaluation is being ported to RPC.")346 populate_tags = patch_populate_tags(self)
342
343 tag = factory.make_Tag(definition='//foo/bar')347 tag = factory.make_Tag(definition='//foo/bar')
344 # Only one node matches the tag definition, rebuilding should notice
345 node_matching = factory.make_Node()
346 inject_lshw_result(node_matching, b'<foo><bar/></foo>')
347 node_bogus = factory.make_Node()
348 inject_lshw_result(node_bogus, b'<foo/>')
349 node_matching.tags.add(tag)
350 node_bogus.tags.add(tag)
351 self.assertItemsEqual(
352 [node_matching, node_bogus], tag.node_set.all())
353 self.become_admin()348 self.become_admin()
349 self.assertThat(populate_tags, MockCalledOnceWith(tag))
354 response = self.client.post(self.get_tag_uri(tag), {'op': 'rebuild'})350 response = self.client.post(self.get_tag_uri(tag), {'op': 'rebuild'})
355 self.assertEqual(httplib.OK, response.status_code)351 self.assertEqual(httplib.OK, response.status_code)
356 parsed_result = json.loads(response.content)352 parsed_result = json.loads(response.content)
357 self.assertEqual({'rebuilding': tag.name}, parsed_result)353 self.assertEqual({'rebuilding': tag.name}, parsed_result)
358 self.assertItemsEqual([node_matching], tag.node_set.all())354 self.assertThat(populate_tags, MockCallsMatch(call(tag), call(tag)))
359355
360 def test_POST_rebuild_leaves_manual_tags(self):356 def test_POST_rebuild_leaves_manual_tags(self):
361 tag = factory.make_Tag(definition='')357 tag = factory.make_Tag(definition='')
@@ -491,16 +487,8 @@
491 extra_kernel_opts, Tag.objects.filter(name=name)[0].kernel_opts)487 extra_kernel_opts, Tag.objects.filter(name=name)[0].kernel_opts)
492488
493 def test_POST_new_populates_nodes(self):489 def test_POST_new_populates_nodes(self):
494 self.skip("Tag evaluation is being ported to RPC.")490 populate_tags = patch_populate_tags(self)
495
496 self.become_admin()491 self.become_admin()
497 node1 = factory.make_Node()
498 inject_lshw_result(node1, b'<node><child/></node>')
499 # Create another node that doesn't have a 'child'
500 node2 = factory.make_Node()
501 inject_lshw_result(node2, b'<node/>')
502 self.assertItemsEqual([], node1.tag_names())
503 self.assertItemsEqual([], node2.tag_names())
504 name = factory.make_string()492 name = factory.make_string()
505 definition = '//node/child'493 definition = '//node/child'
506 comment = factory.make_string()494 comment = factory.make_string()
@@ -513,5 +501,8 @@
513 'definition': definition,501 'definition': definition,
514 })502 })
515 self.assertEqual(httplib.OK, response.status_code)503 self.assertEqual(httplib.OK, response.status_code)
516 self.assertItemsEqual([name], node1.tag_names())504 self.assertThat(populate_tags, MockCalledOnceWith(ANY))
517 self.assertItemsEqual([], node2.tag_names())505 # The tag passed to populate_tags() is the one created above.
506 [tag], _ = populate_tags.call_args
507 self.assertThat(tag, MatchesStructure.byEquality(
508 name=name, comment=comment, definition=definition))
518509
=== modified file 'src/maasserver/models/tag.py'
--- src/maasserver/models/tag.py 2014-09-23 09:14:04 +0000
+++ src/maasserver/models/tag.py 2014-09-23 09:14:04 +0000
@@ -111,9 +111,9 @@
111 raise ValidationError({'definition': [msg]})111 raise ValidationError({'definition': [msg]})
112 # Now delete the existing tags112 # Now delete the existing tags
113 self.node_set.clear()113 self.node_set.clear()
114 # Being ported to RPC.114 # Avoid circular imports.
115 # from maasserver.populate_tags import populate_tags115 from maasserver.populate_tags import populate_tags
116 # populate_tags(self)116 populate_tags(self)
117117
118 def save(self, *args, **kwargs):118 def save(self, *args, **kwargs):
119 super(Tag, self).save(*args, **kwargs)119 super(Tag, self).save(*args, **kwargs)
120120
=== modified file 'src/maasserver/models/tests/test_tag.py'
--- src/maasserver/models/tests/test_tag.py 2014-09-23 09:14:04 +0000
+++ src/maasserver/models/tests/test_tag.py 2014-09-23 09:14:04 +0000
@@ -20,7 +20,6 @@
20from maasserver.testing.factory import factory20from maasserver.testing.factory import factory
21from maasserver.testing.testcase import MAASServerTestCase21from maasserver.testing.testcase import MAASServerTestCase
22from maastesting.matchers import MockCalledOnceWith22from maastesting.matchers import MockCalledOnceWith
23from metadataserver.models.commissioningscript import inject_lshw_result
2423
2524
26class TagTest(MAASServerTestCase):25class TagTest(MAASServerTestCase):
@@ -67,60 +66,15 @@
67 self.assertRaises(ValidationError, factory.make_Tag, name=invalid)66 self.assertRaises(ValidationError, factory.make_Tag, name=invalid)
6867
69 def test_applies_tags_to_nodes(self):68 def test_applies_tags_to_nodes(self):
70 self.skip("Tag evaluation is being ported to RPC.")69 populate_tags = self.patch_autospec(
7170 populate_tags_module, "populate_tags")
72 node1 = factory.make_Node()
73 inject_lshw_result(node1, b'<node><child /></node>')
74 node2 = factory.make_Node()
75 inject_lshw_result(node2, b'<node />')
76 tag = factory.make_Tag(definition='//node/child')71 tag = factory.make_Tag(definition='//node/child')
77 self.assertItemsEqual([tag.name], node1.tag_names())72 self.assertThat(populate_tags, MockCalledOnceWith(tag))
78 self.assertItemsEqual([], node2.tag_names())73
7974 def test_will_not_save_invalid_xpath(self):
80 def test_removes_old_values(self):75 tag = factory.make_Tag(definition='//node/foo')
81 self.skip("Tag evaluation is being ported to RPC.")
82
83 node1 = factory.make_Node()
84 inject_lshw_result(node1, b'<node><foo /></node>')
85 node2 = factory.make_Node()
86 inject_lshw_result(node2, b'<node><bar /></node>')
87 tag = factory.make_Tag(definition='//node/foo')
88 self.assertItemsEqual([tag.name], node1.tag_names())
89 self.assertItemsEqual([], node2.tag_names())
90 tag.definition = '//node/bar'
91 tag.save()
92 self.assertItemsEqual([], node1.tag_names())
93 self.assertItemsEqual([tag.name], node2.tag_names())
94 # And we notice if we change it *again* and then save.
95 tag.definition = '//node/foo'
96 tag.save()
97 self.assertItemsEqual([tag.name], node1.tag_names())
98 self.assertItemsEqual([], node2.tag_names())
99
100 def test_doesnt_touch_other_tags(self):
101 self.skip("Tag evaluation is being ported to RPC.")
102
103 node1 = factory.make_Node()
104 inject_lshw_result(node1, b'<node><foo /></node>')
105 node2 = factory.make_Node()
106 inject_lshw_result(node2, b'<node><bar /></node>')
107 tag1 = factory.make_Tag(definition='//node/foo')
108 self.assertItemsEqual([tag1.name], node1.tag_names())
109 self.assertItemsEqual([], node2.tag_names())
110 tag2 = factory.make_Tag(definition='//node/bar')
111 self.assertItemsEqual([tag1.name], node1.tag_names())
112 self.assertItemsEqual([tag2.name], node2.tag_names())
113
114 def test_rollsback_invalid_xpath(self):
115 self.skip("Tag evaluation is being ported to RPC.")
116
117 node = factory.make_Node()
118 inject_lshw_result(node, b'<node><foo /></node>')
119 tag = factory.make_Tag(definition='//node/foo')
120 self.assertItemsEqual([tag.name], node.tag_names())
121 tag.definition = 'invalid::tag'76 tag.definition = 'invalid::tag'
122 self.assertRaises(ValidationError, tag.save)77 self.assertRaises(ValidationError, tag.save)
123 self.assertItemsEqual([tag.name], node.tag_names())
12478
12579
126class TestTagIsDefined(MAASServerTestCase):80class TestTagIsDefined(MAASServerTestCase):
@@ -167,8 +121,6 @@
167 self.assertItemsEqual([], tag.node_set.all())121 self.assertItemsEqual([], tag.node_set.all())
168122
169 def test__calls_populate_tags(self):123 def test__calls_populate_tags(self):
170 self.skip("Tag evaluation is being ported to RPC.")
171
172 populate_tags = self.patch_autospec(124 populate_tags = self.patch_autospec(
173 populate_tags_module, "populate_tags")125 populate_tags_module, "populate_tags")
174126
175127
=== modified file 'src/maasserver/populate_tags.py'
--- src/maasserver/populate_tags.py 2014-09-23 09:14:04 +0000
+++ src/maasserver/populate_tags.py 2014-09-23 09:14:04 +0000
@@ -18,6 +18,7 @@
18 ]18 ]
1919
20from functools import partial20from functools import partial
21from itertools import izip
2122
22from lxml import etree23from lxml import etree
23from maasserver import logger24from maasserver import logger
@@ -26,10 +27,23 @@
26 get_single_probed_details,27 get_single_probed_details,
27 script_output_nsmap,28 script_output_nsmap,
28 )29 )
29from maasserver.refresh_worker import refresh_worker30from maasserver.rpc import getClientFor
31from provisioningserver.logger import get_maas_logger
32from provisioningserver.rpc.cluster import EvaluateTag
30from provisioningserver.tags import merge_details33from provisioningserver.tags import merge_details
31from provisioningserver.utils import classify34from provisioningserver.utils import classify
35from provisioningserver.utils.twisted import (
36 asynchronous,
37 FOREVER,
38 synchronous,
39 )
32from provisioningserver.utils.xpath import try_match_xpath40from provisioningserver.utils.xpath import try_match_xpath
41from twisted.internet.defer import DeferredList
42from twisted.python import log
43
44
45maaslog = get_maas_logger("tags")
46
3347
34# The nsmap that XPath expression must be compiled with. This will48# The nsmap that XPath expression must be compiled with. This will
35# ensure that expressions like //lshw:something will work correctly.49# ensure that expressions like //lshw:something will work correctly.
@@ -39,24 +53,112 @@
39}53}
4054
4155
56@synchronous
42def populate_tags(tag):57def populate_tags(tag):
43 """Send worker for all nodegroups an update_node_tags request.58 """Evaluate `tag` for all nodes.
44 """59
45 items = {60 This returns a `Deferred` that will fire when all tags have been
46 'tag_name': tag.name,61 evaluated. This is intended FOR TESTING ONLY because:
47 'tag_definition': tag.definition,62
48 'tag_nsmap': tag_nsmap,63 - You must not use the `Deferred` in the calling thread; it must only be
49 }64 manipulated in the reactor thread. Pretending it's not there is safer
50 # Rather than using NodeGroup.objects.refresh_workers() we call65 than chaining code onto it because it's easy to get wrong.
51 # refresh_worker immediately before we pass the requet. This is mostly for66
52 # the test suite, where we need the single real worker to switch to the67 - The call may not finish for 10 minutes or more. It is therefore not a
53 # worker for a given nodegroup, before we have that worker process the68 good thing to be waiting for in a web request.
54 # request.69
55 logger.debug('Refreshing tag definition for %s' % (items,))70 """
56 for nodegroup in NodeGroup.objects.all():71 logger.debug('Evaluating the "%s" tag for all nodes', tag.name)
57 refresh_worker(nodegroup)72 clusters = tuple(
58 raise NotImplementedError(73 (nodegroup.uuid, nodegroup.cluster_name, nodegroup.api_credentials)
59 "Tag evaluation is being ported to RPC.")74 for nodegroup in NodeGroup.objects.all_accepted())
75 [d] = _do_populate_tags(clusters, tag.name, tag.definition, tag_nsmap)
76 return d
77
78
79@asynchronous(timeout=FOREVER)
80def _do_populate_tags(clusters, tag_name, tag_definition, tag_nsmap):
81 """Send RPC calls to each cluster, requesting evaluation of tags.
82
83 :param clusters: A sequence of ``(uuid, cluster-name, creds)`` tuples for
84 each cluster. The name is used for logging only.
85 :param tag_name: The name of the tag being evaluated.
86 :param tag_definition: The definition of the tag, an XPath expression.
87 :oaram tag_nsmap: The NS mapping used to compile the expression.
88 """
89 creds = {uuid: creds for uuid, _, creds in clusters}
90 tag_nsmap_on_wire = [
91 {"prefix": prefix, "uri": uri}
92 for prefix, uri in tag_nsmap.viewitems()
93 ]
94
95 def make_call(client):
96 return client(
97 EvaluateTag, tag_name=tag_name, tag_definition=tag_definition,
98 tag_nsmap=tag_nsmap_on_wire, credentials=creds[client.ident])
99
100 def evaluate_tags(clients):
101 # Call EvaluateTag on each cluster concurrently.
102 return DeferredList(
103 (make_call(client) for client in clients),
104 consumeErrors=True)
105
106 def check_results(results):
107 for (uuid, name, creds), (success, result) in izip(clusters, results):
108 if success:
109 maaslog.info(
110 "Tag %s (%s) evaluated on cluster %s (%s)", tag_name,
111 tag_definition, name, uuid)
112 else:
113 maaslog.error(
114 "Tag %s (%s) could not be evaluated on cluster %s (%s): "
115 "%s", tag_name, tag_definition, name, uuid,
116 result.getErrorMessage())
117
118 d = _get_clients_for_populating_tags(
119 [(uuid, name) for (uuid, name, _) in clusters], tag_name)
120 d.addCallback(evaluate_tags)
121 d.addCallback(check_results)
122 d.addErrback(log.err)
123
124 # Do *not* return a Deferred; the caller is not meant to wait around for
125 # this to finish. However, for the sake of testing, we return the Deferred
126 # wrapped up in a list so that crochet does not block.
127 return [d]
128
129
130@asynchronous(timeout=FOREVER)
131def _get_clients_for_populating_tags(clusters, tag_name):
132 """Obtain RPC clients for the given clusters.
133
134 :param clusters: A sequence of ``(uuid, name)`` tuples for each cluster.
135 The name is used for logging only.
136 :param tag_name: The tag for which these clients are being obtained. This
137 is used only for logging.
138 """
139 # Wait up to `timeout` seconds to obtain clients to all clusters.
140 d = DeferredList(
141 (getClientFor(uuid, timeout=30) for uuid, _ in clusters),
142 consumeErrors=True)
143
144 def got_clients(results):
145 clients = []
146 for (uuid, name), (success, result) in izip(clusters, results):
147 if success:
148 clients.append(result)
149 else:
150 # XXX: allenap 2014-09-22 bug=1372544: Filtering out
151 # unavailable (or otherwise broken) clients means that tags
152 # aren't going to be evaluated for that cluster's nodes.
153 # That's bad. It would be good to be able to ask another
154 # cluster to evaluate them, but the cluster-side code needs
155 # changing to allow that. For now, we just skip.
156 maaslog.warning(
157 "Cannot evaluate tag %s on cluster %s (%s): %s",
158 tag_name, name, uuid, result.getErrorMessage())
159 return clients
160
161 return d.addCallback(got_clients)
60162
61163
62def populate_tags_for_single_node(tags, node):164def populate_tags_for_single_node(tags, node):
63165
=== modified file 'src/maasserver/tests/test_populate_tags.py'
--- src/maasserver/tests/test_populate_tags.py 2014-09-23 09:14:04 +0000
+++ src/maasserver/tests/test_populate_tags.py 2014-09-23 09:14:04 +0000
@@ -14,11 +14,264 @@
14__metaclass__ = type14__metaclass__ = type
15__all__ = []15__all__ = []
1616
17from itertools import izip
18
19from fixtures import FakeLogger
20from maasserver import populate_tags as populate_tags_module
21from maasserver.enum import NODEGROUP_STATUS
17from maasserver.models import Tag22from maasserver.models import Tag
18from maasserver.populate_tags import populate_tags_for_single_node23from maasserver.populate_tags import (
24 _do_populate_tags,
25 _get_clients_for_populating_tags,
26 populate_tags,
27 populate_tags_for_single_node,
28 )
29from maasserver.rpc.testing.fixtures import MockLiveRegionToClusterRPCFixture
30from maasserver.testing.eventloop import (
31 RegionEventLoopFixture,
32 RunningEventLoopFixture,
33 )
19from maasserver.testing.factory import factory34from maasserver.testing.factory import factory
20from maasserver.testing.testcase import MAASServerTestCase35from maasserver.testing.testcase import MAASServerTestCase
36from maastesting.matchers import MockCalledOnceWith
21from metadataserver.models import commissioningscript37from metadataserver.models import commissioningscript
38from mock import (
39 ANY,
40 create_autospec,
41 sentinel,
42 )
43from provisioningserver.rpc.cluster import EvaluateTag
44from provisioningserver.rpc.common import Client
45from provisioningserver.rpc.testing import (
46 always_fail_with,
47 always_succeed_with,
48 )
49from provisioningserver.utils.twisted import asynchronous
50from testtools.deferredruntest import extract_result
51from testtools.monkey import MonkeyPatcher
52from twisted.internet import defer
53
54
55def make_accepted_NodeGroup():
56 return factory.make_NodeGroup(status=NODEGROUP_STATUS.ACCEPTED)
57
58
59def make_Tag_without_populating():
60 # Create a tag but prevent evaluation when saving.
61 dont_populate = MonkeyPatcher((Tag, "populate_nodes", lambda self: None))
62 return dont_populate.run_with_patches(factory.make_Tag)
63
64
65class TestGetClientsForPopulatingTags(MAASServerTestCase):
66
67 def test__returns_no_clients_when_there_are_no_clusters(self):
68 tag_name = factory.make_name("tag")
69 clients = _get_clients_for_populating_tags([], tag_name)
70 self.assertEqual([], clients)
71
72 def patch_getClientFor(self):
73 return self.patch_autospec(populate_tags_module, "getClientFor")
74
75 def test__returns_no_clients_when_there_is_an_error(self):
76 nodegroup_with_connection = make_accepted_NodeGroup()
77 nodegroup_without_connection = make_accepted_NodeGroup()
78
79 def getClientFor(uuid, timeout):
80 if uuid == nodegroup_with_connection.uuid:
81 return defer.succeed(sentinel.client)
82 else:
83 return defer.fail(ZeroDivisionError())
84 self.patch_getClientFor().side_effect = getClientFor
85
86 tag_name = factory.make_name("tag")
87 clusters = [
88 (nodegroup_with_connection.uuid,
89 nodegroup_with_connection.cluster_name),
90 (nodegroup_without_connection.uuid,
91 nodegroup_without_connection.cluster_name),
92 ]
93 clients = _get_clients_for_populating_tags(clusters, tag_name)
94 self.assertEqual([sentinel.client], clients)
95
96 def test__logs_errors_obtaining_clients(self):
97 getClientFor = self.patch_getClientFor()
98 getClientFor.side_effect = always_fail_with(
99 ZeroDivisionError("an error message one would surmise"))
100 nodegroup = make_accepted_NodeGroup()
101 tag_name = factory.make_name("tag")
102 clusters = [(nodegroup.uuid, nodegroup.cluster_name)]
103 with FakeLogger("maas") as log:
104 _get_clients_for_populating_tags(clusters, tag_name)
105 self.assertDocTestMatches(
106 "Cannot evaluate tag ... on cluster ... (...): ... surmise",
107 log.output)
108
109 def test__waits_for_clients_for_30_seconds_by_default(self):
110 getClientFor = self.patch_getClientFor()
111 getClientFor.side_effect = always_succeed_with(sentinel.client)
112 nodegroup = make_accepted_NodeGroup()
113 tag_name = factory.make_name("tag")
114 clusters = [(nodegroup.uuid, nodegroup.cluster_name)]
115 clients = _get_clients_for_populating_tags(clusters, tag_name)
116 self.assertEqual([sentinel.client], clients)
117 self.assertThat(
118 getClientFor, MockCalledOnceWith(
119 nodegroup.uuid, timeout=30))
120
121 def test__obtains_multiple_clients(self):
122 getClientFor = self.patch_getClientFor()
123 # Return a 2-tuple as a stand-in for a real client.
124 getClientFor.side_effect = lambda uuid, timeout: (
125 defer.succeed((sentinel.client, uuid)))
126 nodegroups = [make_accepted_NodeGroup() for _ in xrange(3)]
127 tag_name = factory.make_name("tag")
128 clusters = [(ng.uuid, ng.cluster_name) for ng in nodegroups]
129 clients = _get_clients_for_populating_tags(clusters, tag_name)
130 self.assertItemsEqual(
131 [(sentinel.client, nodegroup.uuid) for nodegroup in nodegroups],
132 clients)
133
134
135class TestDoPopulateTags(MAASServerTestCase):
136
137 def patch_clients(self, nodegroups):
138 clients = [create_autospec(Client, instance=True) for _ in nodegroups]
139 for nodegroup, client in izip(nodegroups, clients):
140 client.side_effect = always_succeed_with(None)
141 client.ident = nodegroup.uuid
142
143 _get_clients = self.patch_autospec(
144 populate_tags_module, "_get_clients_for_populating_tags")
145 _get_clients.return_value = defer.succeed(clients)
146
147 return clients
148
149 def test__makes_calls_to_each_client_given(self):
150 nodegroups = [make_accepted_NodeGroup() for _ in xrange(3)]
151 clients = self.patch_clients(nodegroups)
152
153 tag_name = factory.make_name("tag")
154 tag_definition = factory.make_name("definition")
155 tag_nsmap_prefix = factory.make_name("prefix")
156 tag_nsmap_uri = factory.make_name("uri")
157 tag_nsmap = {tag_nsmap_prefix: tag_nsmap_uri}
158
159 clusters = list(
160 (ng.uuid, ng.cluster_name, ng.api_credentials)
161 for ng in nodegroups)
162
163 [d] = _do_populate_tags(
164 clusters, tag_name, tag_definition, tag_nsmap)
165
166 self.assertIsNone(extract_result(d))
167
168 for nodegroup, client in izip(nodegroups, clients):
169 self.expectThat(client, MockCalledOnceWith(
170 EvaluateTag, tag_name=tag_name, tag_definition=tag_definition,
171 tag_nsmap=[{"prefix": tag_nsmap_prefix, "uri": tag_nsmap_uri}],
172 credentials=nodegroup.api_credentials))
173
174 def test__logs_successes(self):
175 nodegroups = [make_accepted_NodeGroup()]
176 self.patch_clients(nodegroups)
177
178 tag_name = factory.make_name("tag")
179 tag_definition = factory.make_name("definition")
180 tag_nsmap = {}
181
182 clusters = list(
183 (ng.uuid, ng.cluster_name, ng.api_credentials)
184 for ng in nodegroups)
185
186 with FakeLogger("maas") as log:
187 [d] = _do_populate_tags(
188 clusters, tag_name, tag_definition, tag_nsmap)
189 self.assertIsNone(extract_result(d))
190
191 self.assertDocTestMatches(
192 "Tag tag-... (definition-...) evaluated on cluster ... (...)",
193 log.output)
194
195 def test__logs_failures(self):
196 nodegroups = [make_accepted_NodeGroup()]
197 [client] = self.patch_clients(nodegroups)
198 client.side_effect = always_fail_with(
199 ZeroDivisionError("splendid day for a spot of cricket"))
200
201 tag_name = factory.make_name("tag")
202 tag_definition = factory.make_name("definition")
203 tag_nsmap = {}
204
205 clusters = list(
206 (ng.uuid, ng.cluster_name, ng.api_credentials)
207 for ng in nodegroups)
208
209 with FakeLogger("maas") as log:
210 [d] = _do_populate_tags(
211 clusters, tag_name, tag_definition, tag_nsmap)
212 self.assertIsNone(extract_result(d))
213
214 self.assertDocTestMatches(
215 "Tag tag-... (definition-...) could not be evaluated ... (...): "
216 "splendid day for a spot of cricket", log.output)
217
218
219class TestPopulateTags(MAASServerTestCase):
220
221 def patch_do_populate_tags(self):
222 do_populate_tags = self.patch_autospec(
223 populate_tags_module, "_do_populate_tags")
224 do_populate_tags.return_value = [sentinel.d]
225 return do_populate_tags
226
227 def test__calls_do_populate_tags_with_no_clusters(self):
228 do_populate_tags = self.patch_do_populate_tags()
229 tag = make_Tag_without_populating()
230 populate_tags(tag)
231 self.assertThat(do_populate_tags, MockCalledOnceWith(
232 (), tag.name, tag.definition, populate_tags_module.tag_nsmap))
233
234 def test__calls_do_populate_tags_with_clusters(self):
235 do_populate_tags = self.patch_do_populate_tags()
236 nodegroups = [make_accepted_NodeGroup() for _ in xrange(3)]
237 tag = make_Tag_without_populating()
238 populate_tags(tag)
239 clusters_expected = tuple(
240 (ng.uuid, ng.cluster_name, ng.api_credentials)
241 for ng in nodegroups)
242 self.assertThat(do_populate_tags, MockCalledOnceWith(
243 clusters_expected, tag.name, tag.definition,
244 populate_tags_module.tag_nsmap))
245
246
247class TestPopulateTagsEndToNearlyEnd(MAASServerTestCase):
248
249 def prepare_live_rpc(self):
250 self.useFixture(RegionEventLoopFixture("rpc"))
251 self.useFixture(RunningEventLoopFixture())
252 return self.useFixture(MockLiveRegionToClusterRPCFixture())
253
254 def test__calls_are_made_to_all_clusters(self):
255 rpc_fixture = self.prepare_live_rpc()
256 nodegroups = [make_accepted_NodeGroup() for _ in xrange(3)]
257 protocols = []
258 for nodegroup in nodegroups:
259 protocol = rpc_fixture.makeCluster(nodegroup, EvaluateTag)
260 protocol.EvaluateTag.side_effect = always_succeed_with({})
261 protocols.append(protocol)
262 tag = make_Tag_without_populating()
263
264 d = populate_tags(tag)
265
266 # `d` is a testing-only convenience. We must wait for it to fire, and
267 # we must do that from the reactor thread.
268 wait_for_populate = asynchronous(lambda: d)
269 wait_for_populate().wait(10)
270
271 for nodegroup, protocol in izip(nodegroups, protocols):
272 self.expectThat(protocol.EvaluateTag, MockCalledOnceWith(
273 protocol, tag_name=tag.name, tag_definition=tag.definition,
274 tag_nsmap=ANY, credentials=nodegroup.api_credentials))
22275
23276
24class TestPopulateTagsForSingleNode(MAASServerTestCase):277class TestPopulateTagsForSingleNode(MAASServerTestCase):