Merge lp:~allenap/maas/rpc-tags-cluster 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: 3054
Proposed branch: lp:~allenap/maas/rpc-tags-cluster
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 814 lines (+263/-207)
14 files modified
src/maasserver/api/tests/test_tag.py (+10/-0)
src/maasserver/models/tag.py (+3/-2)
src/maasserver/models/tests/test_tag.py (+10/-0)
src/maasserver/populate_tags.py (+2/-2)
src/maasserver/tests/test_populate_tags.py (+1/-39)
src/provisioningserver/rpc/cluster.py (+20/-0)
src/provisioningserver/rpc/clusterservice.py (+19/-0)
src/provisioningserver/rpc/tags.py (+46/-0)
src/provisioningserver/rpc/tests/test_clusterservice.py (+66/-0)
src/provisioningserver/rpc/tests/test_tags.py (+73/-0)
src/provisioningserver/tags.py (+6/-42)
src/provisioningserver/tasks.py (+1/-36)
src/provisioningserver/tests/test_tags.py (+6/-41)
src/provisioningserver/tests/test_tasks.py (+0/-45)
To merge this branch: bzr merge lp:~allenap/maas/rpc-tags-cluster
Reviewer Review Type Date Requested Status
Graham Binns (community) Approve
Review via email: mp+234492@code.launchpad.net

Commit message

New EvaluateTag RPC command on the cluster.

Description of the change

This hobbles tag evaluation in the region, but I won't land this as-is. It's a merge proposal for review purposes only.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Looking good so far. One minor nitpick, but otherwise it's cool beans.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

Thanks for the review :)

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (18.7 KiB)

The attempt to merge lp:~allenap/maas/rpc-tags-cluster into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Get:2 http://security.ubuntu.com trusty-security Release [59.7 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates Release [59.7 kB]
Get:5 http://security.ubuntu.com trusty-security/main Sources [44.9 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Get:6 http://security.ubuntu.com trusty-security/universe Sources [10.8 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Get:7 http://security.ubuntu.com trusty-security/main amd64 Packages [144 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Get:8 http://security.ubuntu.com trusty-security/universe amd64 Packages [48.4 kB]
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [120 kB]
Get:10 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [84.7 kB]
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [322 kB]
Get:12 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [204 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Fetched 1,101 kB in 0s (1,663 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make pep8 postgresql pyflakes python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-mimeparse python-mock python-netaddr python-netifaces python-nose python-oauth python-oops python-oops-amqp python-oops-datedir-repo python-oops-twisted python-oops-wsgi python-openssl ...

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-10 16:20:31 +0000
+++ src/maasserver/api/tests/test_tag.py 2014-09-23 09:14:01 +0000
@@ -104,6 +104,8 @@
104 self.assertTrue(Tag.objects.filter(name='new-tag-name').exists())104 self.assertTrue(Tag.objects.filter(name='new-tag-name').exists())
105105
106 def test_PUT_updates_node_associations(self):106 def test_PUT_updates_node_associations(self):
107 self.skip("Tag evaluation is being ported to RPC.")
108
107 node1 = factory.make_Node()109 node1 = factory.make_Node()
108 inject_lshw_result(node1, b'<node><foo/></node>')110 inject_lshw_result(node1, b'<node><foo/></node>')
109 node2 = factory.make_Node()111 node2 = factory.make_Node()
@@ -141,6 +143,8 @@
141 [r['system_id'] for r in parsed_result])143 [r['system_id'] for r in parsed_result])
142144
143 def test_GET_nodes_hides_invisible_nodes(self):145 def test_GET_nodes_hides_invisible_nodes(self):
146 self.skip("Tag evaluation is being ported to RPC.")
147
144 user2 = factory.make_User()148 user2 = factory.make_User()
145 node1 = factory.make_Node()149 node1 = factory.make_Node()
146 inject_lshw_result(node1, b'<node><foo/></node>')150 inject_lshw_result(node1, b'<node><foo/></node>')
@@ -162,6 +166,8 @@
162 [r['system_id'] for r in parsed_result])166 [r['system_id'] for r in parsed_result])
163167
164 def test_PUT_invalid_definition(self):168 def test_PUT_invalid_definition(self):
169 self.skip("Tag evaluation is being ported to RPC.")
170
165 self.become_admin()171 self.become_admin()
166 node = factory.make_Node()172 node = factory.make_Node()
167 inject_lshw_result(node, b'<node ><child/></node>')173 inject_lshw_result(node, b'<node ><child/></node>')
@@ -332,6 +338,8 @@
332 self.assertItemsEqual([], node.tags.all())338 self.assertItemsEqual([], node.tags.all())
333339
334 def test_POST_rebuild_rebuilds_node_mapping(self):340 def test_POST_rebuild_rebuilds_node_mapping(self):
341 self.skip("Tag evaluation is being ported to RPC.")
342
335 tag = factory.make_Tag(definition='//foo/bar')343 tag = factory.make_Tag(definition='//foo/bar')
336 # Only one node matches the tag definition, rebuilding should notice344 # Only one node matches the tag definition, rebuilding should notice
337 node_matching = factory.make_Node()345 node_matching = factory.make_Node()
@@ -483,6 +491,8 @@
483 extra_kernel_opts, Tag.objects.filter(name=name)[0].kernel_opts)491 extra_kernel_opts, Tag.objects.filter(name=name)[0].kernel_opts)
484492
485 def test_POST_new_populates_nodes(self):493 def test_POST_new_populates_nodes(self):
494 self.skip("Tag evaluation is being ported to RPC.")
495
486 self.become_admin()496 self.become_admin()
487 node1 = factory.make_Node()497 node1 = factory.make_Node()
488 inject_lshw_result(node1, b'<node><child/></node>')498 inject_lshw_result(node1, b'<node><child/></node>')
489499
=== modified file 'src/maasserver/models/tag.py'
--- src/maasserver/models/tag.py 2013-12-02 14:29:41 +0000
+++ src/maasserver/models/tag.py 2014-09-23 09:14:01 +0000
@@ -101,7 +101,6 @@
101101
102 def populate_nodes(self):102 def populate_nodes(self):
103 """Find all nodes that match this tag, and update them."""103 """Find all nodes that match this tag, and update them."""
104 from maasserver.populate_tags import populate_tags
105 if not self.is_defined:104 if not self.is_defined:
106 return105 return
107 # before we pass off any work, ensure the definition is valid XPATH106 # before we pass off any work, ensure the definition is valid XPATH
@@ -112,7 +111,9 @@
112 raise ValidationError({'definition': [msg]})111 raise ValidationError({'definition': [msg]})
113 # Now delete the existing tags112 # Now delete the existing tags
114 self.node_set.clear()113 self.node_set.clear()
115 populate_tags(self)114 # Being ported to RPC.
115 # from maasserver.populate_tags import populate_tags
116 # populate_tags(self)
116117
117 def save(self, *args, **kwargs):118 def save(self, *args, **kwargs):
118 super(Tag, self).save(*args, **kwargs)119 super(Tag, self).save(*args, **kwargs)
119120
=== modified file 'src/maasserver/models/tests/test_tag.py'
--- src/maasserver/models/tests/test_tag.py 2014-09-19 13:27:37 +0000
+++ src/maasserver/models/tests/test_tag.py 2014-09-23 09:14:01 +0000
@@ -67,6 +67,8 @@
67 self.assertRaises(ValidationError, factory.make_Tag, name=invalid)67 self.assertRaises(ValidationError, factory.make_Tag, name=invalid)
6868
69 def test_applies_tags_to_nodes(self):69 def test_applies_tags_to_nodes(self):
70 self.skip("Tag evaluation is being ported to RPC.")
71
70 node1 = factory.make_Node()72 node1 = factory.make_Node()
71 inject_lshw_result(node1, b'<node><child /></node>')73 inject_lshw_result(node1, b'<node><child /></node>')
72 node2 = factory.make_Node()74 node2 = factory.make_Node()
@@ -76,6 +78,8 @@
76 self.assertItemsEqual([], node2.tag_names())78 self.assertItemsEqual([], node2.tag_names())
7779
78 def test_removes_old_values(self):80 def test_removes_old_values(self):
81 self.skip("Tag evaluation is being ported to RPC.")
82
79 node1 = factory.make_Node()83 node1 = factory.make_Node()
80 inject_lshw_result(node1, b'<node><foo /></node>')84 inject_lshw_result(node1, b'<node><foo /></node>')
81 node2 = factory.make_Node()85 node2 = factory.make_Node()
@@ -94,6 +98,8 @@
94 self.assertItemsEqual([], node2.tag_names())98 self.assertItemsEqual([], node2.tag_names())
9599
96 def test_doesnt_touch_other_tags(self):100 def test_doesnt_touch_other_tags(self):
101 self.skip("Tag evaluation is being ported to RPC.")
102
97 node1 = factory.make_Node()103 node1 = factory.make_Node()
98 inject_lshw_result(node1, b'<node><foo /></node>')104 inject_lshw_result(node1, b'<node><foo /></node>')
99 node2 = factory.make_Node()105 node2 = factory.make_Node()
@@ -106,6 +112,8 @@
106 self.assertItemsEqual([tag2.name], node2.tag_names())112 self.assertItemsEqual([tag2.name], node2.tag_names())
107113
108 def test_rollsback_invalid_xpath(self):114 def test_rollsback_invalid_xpath(self):
115 self.skip("Tag evaluation is being ported to RPC.")
116
109 node = factory.make_Node()117 node = factory.make_Node()
110 inject_lshw_result(node, b'<node><foo /></node>')118 inject_lshw_result(node, b'<node><foo /></node>')
111 tag = factory.make_Tag(definition='//node/foo')119 tag = factory.make_Tag(definition='//node/foo')
@@ -159,6 +167,8 @@
159 self.assertItemsEqual([], tag.node_set.all())167 self.assertItemsEqual([], tag.node_set.all())
160168
161 def test__calls_populate_tags(self):169 def test__calls_populate_tags(self):
170 self.skip("Tag evaluation is being ported to RPC.")
171
162 populate_tags = self.patch_autospec(172 populate_tags = self.patch_autospec(
163 populate_tags_module, "populate_tags")173 populate_tags_module, "populate_tags")
164174
165175
=== modified file 'src/maasserver/populate_tags.py'
--- src/maasserver/populate_tags.py 2014-08-13 21:49:35 +0000
+++ src/maasserver/populate_tags.py 2014-09-23 09:14:01 +0000
@@ -28,7 +28,6 @@
28 )28 )
29from maasserver.refresh_worker import refresh_worker29from maasserver.refresh_worker import refresh_worker
30from provisioningserver.tags import merge_details30from provisioningserver.tags import merge_details
31from provisioningserver.tasks import update_node_tags
32from provisioningserver.utils import classify31from provisioningserver.utils import classify
33from provisioningserver.utils.xpath import try_match_xpath32from provisioningserver.utils.xpath import try_match_xpath
3433
@@ -56,7 +55,8 @@
56 logger.debug('Refreshing tag definition for %s' % (items,))55 logger.debug('Refreshing tag definition for %s' % (items,))
57 for nodegroup in NodeGroup.objects.all():56 for nodegroup in NodeGroup.objects.all():
58 refresh_worker(nodegroup)57 refresh_worker(nodegroup)
59 update_node_tags.apply_async(queue=nodegroup.work_queue, kwargs=items)58 raise NotImplementedError(
59 "Tag evaluation is being ported to RPC.")
6060
6161
62def populate_tags_for_single_node(tags, node):62def populate_tags_for_single_node(tags, node):
6363
=== modified file 'src/maasserver/tests/test_populate_tags.py'
--- src/maasserver/tests/test_populate_tags.py 2014-09-15 14:28:28 +0000
+++ src/maasserver/tests/test_populate_tags.py 2014-09-23 09:14:01 +0000
@@ -14,49 +14,11 @@
14__metaclass__ = type14__metaclass__ = type
15__all__ = []15__all__ = []
1616
17from maasserver import populate_tags as populate_tags_module
18from maasserver.models import Tag17from maasserver.models import Tag
19from maasserver.populate_tags import (18from maasserver.populate_tags import populate_tags_for_single_node
20 populate_tags,
21 populate_tags_for_single_node,
22 tag_nsmap,
23 )
24from maasserver.testing.factory import factory19from maasserver.testing.factory import factory
25from maasserver.testing.testcase import MAASServerTestCase20from maasserver.testing.testcase import MAASServerTestCase
26from metadataserver.models import commissioningscript21from metadataserver.models import commissioningscript
27import mock
28
29
30class TestPopulateTags(MAASServerTestCase):
31
32 def test_populate_tags_task_routed_to_nodegroup_worker(self):
33 nodegroup = factory.make_NodeGroup()
34 tag = factory.make_Tag()
35 task = self.patch(populate_tags_module, 'update_node_tags')
36 populate_tags(tag)
37 args, kwargs = task.apply_async.call_args
38 self.assertEqual(nodegroup.work_queue, kwargs['queue'])
39
40 def test_populate_tags_task_routed_to_all_nodegroup_workers(self):
41 nodegroups = [factory.make_NodeGroup() for _ in range(5)]
42 tag = factory.make_Tag()
43 refresh = self.patch(populate_tags_module, 'refresh_worker')
44 task = self.patch(populate_tags_module, 'update_node_tags')
45 populate_tags(tag)
46 refresh_calls = [mock.call(nodegroup) for nodegroup in nodegroups]
47 refresh.assert_has_calls(refresh_calls, any_order=True)
48 task_calls = [
49 mock.call(
50 queue=nodegroup.work_queue,
51 kwargs={
52 'tag_name': tag.name,
53 'tag_definition': tag.definition,
54 'tag_nsmap': tag_nsmap,
55 },
56 )
57 for nodegroup in nodegroups
58 ]
59 task.apply_async.assert_has_calls(task_calls, any_order=True)
6022
6123
62class TestPopulateTagsForSingleNode(MAASServerTestCase):24class TestPopulateTagsForSingleNode(MAASServerTestCase):
6325
=== modified file 'src/provisioningserver/rpc/cluster.py'
--- src/provisioningserver/rpc/cluster.py 2014-09-18 21:21:18 +0000
+++ src/provisioningserver/rpc/cluster.py 2014-09-23 09:14:01 +0000
@@ -345,6 +345,26 @@
345 error = []345 error = []
346346
347347
348class EvaluateTag(amp.Command):
349 """Evaluate a tag against all of the cluster's nodes.
350
351 :since: 1.7
352 """
353
354 arguments = [
355 (b"tag_name", amp.Unicode()),
356 (b"tag_definition", amp.Unicode()),
357 (b"tag_nsmap", amp.AmpList([
358 (b"prefix", amp.Unicode()),
359 (b"uri", amp.Unicode()),
360 ])),
361 # A 3-part credential string for the web API.
362 (b"credentials", amp.Unicode()),
363 ]
364 response = []
365 errors = []
366
367
348class AddVirsh(amp.Command):368class AddVirsh(amp.Command):
349 """Probe for and enlist virsh VMs attached to the cluster.369 """Probe for and enlist virsh VMs attached to the cluster.
350370
351371
=== modified file 'src/provisioningserver/rpc/clusterservice.py'
--- src/provisioningserver/rpc/clusterservice.py 2014-09-22 11:31:14 +0000
+++ src/provisioningserver/rpc/clusterservice.py 2014-09-23 09:14:01 +0000
@@ -21,6 +21,7 @@
21import random21import random
22from urlparse import urlparse22from urlparse import urlparse
2323
24from apiclient.creds import convert_string_to_tuple
24from apiclient.utils import ascii_url25from apiclient.utils import ascii_url
25from provisioningserver.cluster_config import (26from provisioningserver.cluster_config import (
26 get_cluster_uuid,27 get_cluster_uuid,
@@ -68,6 +69,7 @@
68 change_power_state,69 change_power_state,
69 get_power_state,70 get_power_state,
70 )71 )
72from provisioningserver.rpc.tags import evaluate_tag
71from provisioningserver.utils.network import find_ip_via_arp73from provisioningserver.utils.network import find_ip_via_arp
72from twisted.application.internet import TimerService74from twisted.application.internet import TimerService
73from twisted.internet.defer import inlineCallbacks75from twisted.internet.defer import inlineCallbacks
@@ -76,6 +78,7 @@
76 TCP4ClientEndpoint,78 TCP4ClientEndpoint,
77 )79 )
78from twisted.internet.error import ConnectError80from twisted.internet.error import ConnectError
81from twisted.internet.threads import deferToThread
79from twisted.protocols import amp82from twisted.protocols import amp
80from twisted.python import log83from twisted.python import log
81from twisted.web.client import getPage84from twisted.web.client import getPage
@@ -245,6 +248,22 @@
245 else:248 else:
246 return tls.get_tls_parameters_for_cluster()249 return tls.get_tls_parameters_for_cluster()
247250
251 @cluster.EvaluateTag.responder
252 def evaluate_tag(self, tag_name, tag_definition, tag_nsmap, credentials):
253 """evaluate_tag()
254
255 Implementation of
256 :py:class:`~provisioningserver.rpc.cluster.EvaluateTag`.
257 """
258 # It's got to run in a thread because it does blocking IO.
259 d = deferToThread(
260 evaluate_tag, tag_name, tag_definition,
261 # Transform tag_nsmap into a format that LXML likes.
262 {entry["prefix"]: entry["uri"] for entry in tag_nsmap},
263 # Parse the credential string into a 3-tuple.
264 convert_string_to_tuple(credentials))
265 return d.addCallback(lambda _: {})
266
248 @cluster.AddVirsh.responder267 @cluster.AddVirsh.responder
249 def add_virsh(self, poweraddr, password):268 def add_virsh(self, poweraddr, password):
250 """add_virsh()269 """add_virsh()
251270
=== added file 'src/provisioningserver/rpc/tags.py'
--- src/provisioningserver/rpc/tags.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/rpc/tags.py 2014-09-23 09:14:01 +0000
@@ -0,0 +1,46 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""RPC helpers for dealing with tags."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = [
16 "evaluate_tag",
17]
18
19from apiclient.maas_client import (
20 MAASClient,
21 MAASDispatcher,
22 MAASOAuth,
23 )
24from provisioningserver.cluster_config import (
25 get_cluster_uuid,
26 get_maas_url,
27 )
28from provisioningserver.tags import process_node_tags
29from provisioningserver.utils.twisted import synchronous
30
31
32@synchronous
33def evaluate_tag(tag_name, tag_definition, tag_nsmap, credentials):
34 """Evaluate `tag_definition` against this cluster's nodes' details.
35
36 :param tag_name: The name of the tag, used for logging.
37 :param tag_definition: The XPath expression of the tag.
38 :param tag_nsmap: The namespace map as used by LXML's ETree library.
39 :param credentials: A 3-tuple of OAuth credentials.
40 """
41 client = MAASClient(
42 auth=MAASOAuth(*credentials), dispatcher=MAASDispatcher(),
43 base_url=get_maas_url())
44 process_node_tags(
45 tag_name=tag_name, tag_definition=tag_definition, tag_nsmap=tag_nsmap,
46 client=client, nodegroup_uuid=get_cluster_uuid())
047
=== modified file 'src/provisioningserver/rpc/tests/test_clusterservice.py'
--- src/provisioningserver/rpc/tests/test_clusterservice.py 2014-09-22 21:34:33 +0000
+++ src/provisioningserver/rpc/tests/test_clusterservice.py 2014-09-23 09:14:01 +0000
@@ -25,6 +25,7 @@
25from random import randint25from random import randint
26from urlparse import urlparse26from urlparse import urlparse
2727
28from apiclient.creds import convert_tuple_to_string
28from fixtures import EnvironmentVariable29from fixtures import EnvironmentVariable
29from maastesting.factory import factory30from maastesting.factory import factory
30from maastesting.matchers import (31from maastesting.matchers import (
@@ -64,6 +65,7 @@
64 osystems as osystems_rpc_module,65 osystems as osystems_rpc_module,
65 power as power_module,66 power as power_module,
66 region,67 region,
68 tags,
67 )69 )
68from provisioningserver.rpc.clusterservice import (70from provisioningserver.rpc.clusterservice import (
69 Cluster,71 Cluster,
@@ -1185,6 +1187,70 @@
1185 self.assertThat(running_monitors, Not(Contains(monitors[0]["id"])))1187 self.assertThat(running_monitors, Not(Contains(monitors[0]["id"])))
11861188
11871189
1190class TestClusterProtocol_EvaluateTag(MAASTestCase):
1191
1192 run_tests_with = MAASTwistedRunTest.make_factory(timeout=5)
1193
1194 def test__is_registered(self):
1195 protocol = Cluster()
1196 responder = protocol.locateResponder(
1197 cluster.EvaluateTag.commandName)
1198 self.assertIsNot(responder, None)
1199
1200 @inlineCallbacks
1201 def test_happy_path(self):
1202 get_maas_url = self.patch_autospec(tags, "get_maas_url")
1203 get_maas_url.return_value = sentinel.maas_url
1204 get_cluster_uuid = self.patch_autospec(tags, "get_cluster_uuid")
1205 get_cluster_uuid.return_value = sentinel.cluster_uuid
1206
1207 # Prevent real work being done, which would involve HTTP calls.
1208 self.patch_autospec(tags, "process_node_tags")
1209
1210 response = yield call_responder(
1211 Cluster(), cluster.EvaluateTag, {
1212 "tag_name": "all-nodes",
1213 "tag_definition": "//*",
1214 "tag_nsmap": [
1215 {"prefix": "foo",
1216 "uri": "http://foo.example.com/"},
1217 ],
1218 "credentials": "abc:def:ghi",
1219 })
1220
1221 self.assertEqual({}, response)
1222
1223 @inlineCallbacks
1224 def test__calls_through_to_evaluate_tag_helper(self):
1225 evaluate_tag = self.patch_autospec(clusterservice, "evaluate_tag")
1226
1227 tag_name = factory.make_name("tag-name")
1228 tag_definition = factory.make_name("tag-definition")
1229 tag_ns_prefix = factory.make_name("tag-ns-prefix")
1230 tag_ns_uri = factory.make_name("tag-ns-uri")
1231
1232 consumer_key = factory.make_name("ckey")
1233 resource_token = factory.make_name("rtok")
1234 resource_secret = factory.make_name("rsec")
1235 credentials = convert_tuple_to_string(
1236 (consumer_key, resource_token, resource_secret))
1237
1238 yield call_responder(
1239 Cluster(), cluster.EvaluateTag, {
1240 "tag_name": tag_name,
1241 "tag_definition": tag_definition,
1242 "tag_nsmap": [
1243 {"prefix": tag_ns_prefix, "uri": tag_ns_uri},
1244 ],
1245 "credentials": credentials,
1246 })
1247
1248 self.assertThat(evaluate_tag, MockCalledOnceWith(
1249 tag_name, tag_definition, {tag_ns_prefix: tag_ns_uri},
1250 (consumer_key, resource_token, resource_secret),
1251 ))
1252
1253
1188class TestClusterProtocol_AddVirsh(MAASTestCase):1254class TestClusterProtocol_AddVirsh(MAASTestCase):
11891255
1190 def test__is_registered(self):1256 def test__is_registered(self):
11911257
=== added file 'src/provisioningserver/rpc/tests/test_tags.py'
--- src/provisioningserver/rpc/tests/test_tags.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/rpc/tests/test_tags.py 2014-09-23 09:14:01 +0000
@@ -0,0 +1,73 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for :py:module:`~provisioningserver.rpc.dhcp`."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = []
16
17from apiclient.maas_client import (
18 MAASClient,
19 MAASDispatcher,
20 MAASOAuth,
21 )
22from maastesting.factory import factory
23from maastesting.matchers import MockCalledOnceWith
24from maastesting.testcase import MAASTestCase
25from mock import (
26 ANY,
27 sentinel,
28 )
29from provisioningserver.rpc import tags
30
31
32class TestEvaluateTag(MAASTestCase):
33
34 def setUp(self):
35 super(TestEvaluateTag, self).setUp()
36 get_maas_url = self.patch_autospec(tags, "get_maas_url")
37 get_maas_url.return_value = sentinel.maas_url
38 get_cluster_uuid = self.patch_autospec(tags, "get_cluster_uuid")
39 get_cluster_uuid.return_value = sentinel.cluster_uuid
40
41 def test__calls_process_node_tags(self):
42 credentials = "aaa", "bbb", "ccc"
43 process_node_tags = self.patch_autospec(tags, "process_node_tags")
44 tags.evaluate_tag(
45 sentinel.tag_name, sentinel.tag_definition, sentinel.tag_nsmap,
46 credentials)
47 self.assertThat(
48 process_node_tags, MockCalledOnceWith(
49 tag_name=sentinel.tag_name,
50 tag_definition=sentinel.tag_definition,
51 tag_nsmap=sentinel.tag_nsmap, client=ANY,
52 nodegroup_uuid=sentinel.cluster_uuid))
53
54 def test__constructs_client_with_credentials(self):
55 consumer_key = factory.make_name("ckey")
56 resource_token = factory.make_name("rtok")
57 resource_secret = factory.make_name("rsec")
58 credentials = consumer_key, resource_token, resource_secret
59
60 self.patch_autospec(tags, "process_node_tags")
61 self.patch_autospec(tags, "MAASOAuth").side_effect = MAASOAuth
62
63 tags.evaluate_tag(
64 sentinel.tag_name, sentinel.tag_definition, sentinel.tag_nsmap,
65 credentials)
66
67 client = tags.process_node_tags.call_args[1]["client"]
68 self.assertIsInstance(client, MAASClient)
69 self.assertEqual(sentinel.maas_url, client.url)
70 self.assertIsInstance(client.dispatcher, MAASDispatcher)
71 self.assertIsInstance(client.auth, MAASOAuth)
72 self.assertThat(tags.MAASOAuth, MockCalledOnceWith(
73 consumer_key, resource_token, resource_secret))
074
=== modified file 'src/provisioningserver/tags.py'
--- src/provisioningserver/tags.py 2014-08-13 21:49:35 +0000
+++ src/provisioningserver/tags.py 2014-09-23 09:14:01 +0000
@@ -17,7 +17,6 @@
17__all__ = [17__all__ = [
18 'merge_details',18 'merge_details',
19 'merge_details_cleanly',19 'merge_details_cleanly',
20 'MissingCredentials',
21 'process_node_tags',20 'process_node_tags',
22 ]21 ]
2322
@@ -27,18 +26,8 @@
27import httplib26import httplib
28import urllib227import urllib2
2928
30from apiclient.maas_client import (
31 MAASClient,
32 MAASDispatcher,
33 MAASOAuth,
34 )
35import bson29import bson
36from lxml import etree30from lxml import etree
37from provisioningserver.auth import (
38 get_recorded_api_credentials,
39 get_recorded_nodegroup_uuid,
40 )
41from provisioningserver.cluster_config import get_maas_url
42from provisioningserver.logger import get_maas_logger31from provisioningserver.logger import get_maas_logger
43from provisioningserver.utils import classify32from provisioningserver.utils import classify
44from provisioningserver.utils.xpath import try_match_xpath33from provisioningserver.utils.xpath import try_match_xpath
@@ -48,10 +37,6 @@
48maaslog = get_maas_logger("tag_processing")37maaslog = get_maas_logger("tag_processing")
4938
5039
51class MissingCredentials(Exception):
52 """The MAAS URL or credentials are not yet set."""
53
54
55# An example laptop's lshw XML dump was 135kB. An example lab's LLDP40# An example laptop's lshw XML dump was 135kB. An example lab's LLDP
56# XML dump was 1.6kB. A batch size of 100 would mean downloading ~14MB41# XML dump was 1.6kB. A batch size of 100 would mean downloading ~14MB
57# from the region controller, which seems workable. The previous batch42# from the region controller, which seems workable. The previous batch
@@ -60,25 +45,6 @@
60DEFAULT_BATCH_SIZE = 10045DEFAULT_BATCH_SIZE = 100
6146
6247
63def get_cached_knowledge():
64 """Get all the information that we need to know, or raise an error.
65
66 :return: (client, nodegroup_uuid)
67 """
68 api_credentials = get_recorded_api_credentials()
69 if api_credentials is None:
70 maaslog.error("Not updating tags: don't have API key yet.")
71 return None, None
72 nodegroup_uuid = get_recorded_nodegroup_uuid()
73 if nodegroup_uuid is None:
74 maaslog.error("Not updating tags: don't have UUID yet.")
75 return None, None
76 client = MAASClient(
77 MAASOAuth(*api_credentials), MAASDispatcher(),
78 get_maas_url())
79 return client, nodegroup_uuid
80
81
82# A content-type: function mapping that can decode data of that type.48# A content-type: function mapping that can decode data of that type.
83decoders = {49decoders = {
84 "application/json": lambda data: json.loads(data),50 "application/json": lambda data: json.loads(data),
@@ -346,20 +312,18 @@
346 nodes_matched, nodes_unmatched)312 nodes_matched, nodes_unmatched)
347313
348314
349def process_node_tags(tag_name, tag_definition, tag_nsmap, batch_size=None):315def process_node_tags(
316 tag_name, tag_definition, tag_nsmap,
317 client, nodegroup_uuid, batch_size=None):
350 """Update the nodes for a new/changed tag definition.318 """Update the nodes for a new/changed tag definition.
351319
320 :param client: A `MAASClient` used to fetch the node's details via
321 calls to the web API.
322 :param nodegroup_uuid: The UUID for this cluster.
352 :param tag_name: Name of the tag to update nodes for323 :param tag_name: Name of the tag to update nodes for
353 :param tag_definition: Tag definition324 :param tag_definition: Tag definition
354 :param batch_size: Size of batch325 :param batch_size: Size of batch
355 """326 """
356 client, nodegroup_uuid = get_cached_knowledge()
357 if not all([client, nodegroup_uuid]):
358 maaslog.error(
359 "Unable to update tag: %s for definition %r. "
360 "Please refresh secrets, then rebuild this tag."
361 % (tag_name, tag_definition))
362 raise MissingCredentials()
363 # We evaluate this early, so we can fail before sending a bunch of data to327 # We evaluate this early, so we can fail before sending a bunch of data to
364 # the server328 # the server
365 xpath = etree.XPath(tag_definition, namespaces=tag_nsmap)329 xpath = etree.XPath(tag_definition, namespaces=tag_nsmap)
366330
=== modified file 'src/provisioningserver/tasks.py'
--- src/provisioningserver/tasks.py 2014-09-17 08:51:31 +0000
+++ src/provisioningserver/tasks.py 2014-09-23 09:14:01 +0000
@@ -28,10 +28,7 @@
2828
29from celery.app import app_or_default29from celery.app import app_or_default
30from celery.task import task30from celery.task import task
31from provisioningserver import (31from provisioningserver import boot_images
32 boot_images,
33 tags,
34 )
35from provisioningserver.auth import (32from provisioningserver.auth import (
36 record_api_credentials,33 record_api_credentials,
37 record_nodegroup_uuid,34 record_nodegroup_uuid,
@@ -242,35 +239,3 @@
242def report_boot_images():239def report_boot_images():
243 """For master worker only: report available netboot images."""240 """For master worker only: report available netboot images."""
244 boot_images.report_to_server()241 boot_images.report_to_server()
245
246
247# How many times should a update node tags task be retried?
248UPDATE_NODE_TAGS_MAX_RETRY = 10
249
250# How long to wait between update node tags task retries (in seconds)?
251UPDATE_NODE_TAGS_RETRY_DELAY = 2
252
253
254# =====================================================================
255# Tags-related tasks
256# =====================================================================
257
258
259@task(max_retries=UPDATE_NODE_TAGS_MAX_RETRY)
260@log_call()
261@log_exception_text
262def update_node_tags(tag_name, tag_definition, tag_nsmap, retry=True):
263 """Update the nodes for a new/changed tag definition.
264
265 :param tag_name: Name of the tag to update nodes for
266 :param tag_definition: Tag definition
267 :param retry: Whether to retry on failure
268 """
269 try:
270 tags.process_node_tags(tag_name, tag_definition, tag_nsmap)
271 except tags.MissingCredentials, exc:
272 if retry:
273 return update_node_tags.retry(
274 exc=exc, countdown=UPDATE_NODE_TAGS_RETRY_DELAY)
275 else:
276 raise
277242
=== modified file 'src/provisioningserver/tests/test_tags.py'
--- src/provisioningserver/tests/test_tags.py 2014-09-11 09:19:44 +0000
+++ src/provisioningserver/tests/test_tags.py 2014-09-23 09:14:01 +0000
@@ -37,7 +37,6 @@
37 sentinel,37 sentinel,
38 )38 )
39from provisioningserver import tags39from provisioningserver import tags
40from provisioningserver.auth import get_recorded_nodegroup_uuid
41from provisioningserver.testing.testcase import PservTestCase40from provisioningserver.testing.testcase import PservTestCase
42from testtools.matchers import (41from testtools.matchers import (
43 DocTestMatches,42 DocTestMatches,
@@ -484,29 +483,6 @@
484 super(TestTagUpdating, self).setUp()483 super(TestTagUpdating, self).setUp()
485 self.useFixture(FakeLogger())484 self.useFixture(FakeLogger())
486485
487 def test_get_cached_knowledge_knows_nothing(self):
488 # If we haven't given it any secrets, we should get back nothing
489 self.assertEqual((None, None), tags.get_cached_knowledge())
490
491 def test_get_cached_knowledge_with_only_url(self):
492 self.set_maas_url()
493 self.assertEqual((None, None), tags.get_cached_knowledge())
494
495 def test_get_cached_knowledge_with_only_url_creds(self):
496 self.set_maas_url()
497 self.set_api_credentials()
498 self.assertEqual((None, None), tags.get_cached_knowledge())
499
500 def test_get_cached_knowledge_with_all_info(self):
501 self.set_maas_url()
502 self.set_api_credentials()
503 self.set_node_group_uuid()
504 client, uuid = tags.get_cached_knowledge()
505 self.assertIsNot(None, client)
506 self.assertIsInstance(client, MAASClient)
507 self.assertIsNot(None, uuid)
508 self.assertEqual(get_recorded_nodegroup_uuid(), uuid)
509
510 def fake_client(self):486 def fake_client(self):
511 return MAASClient(None, None, self.make_maas_url())487 return MAASClient(None, None, self.make_maas_url())
512488
@@ -615,16 +591,6 @@
615 (['a', 'c'], ['b']),591 (['a', 'c'], ['b']),
616 tags.classify(xpath, node_details))592 tags.classify(xpath, node_details))
617593
618 def test_process_node_tags_no_secrets(self):
619 self.patch(MAASClient, 'get')
620 self.patch(MAASClient, 'post')
621 tag_name = factory.make_name('tag')
622 self.assertRaises(
623 tags.MissingCredentials,
624 tags.process_node_tags, tag_name, '//node', None)
625 self.assertFalse(MAASClient.get.called)
626 self.assertFalse(MAASClient.post.called)
627
628 def test_process_node_tags_integration(self):594 def test_process_node_tags_integration(self):
629 self.set_secrets()595 self.set_secrets()
630 get_nodes = FakeMethod(596 get_nodes = FakeMethod(
@@ -653,10 +619,12 @@
653 self.patch(MAASClient, 'get', get_fake)619 self.patch(MAASClient, 'get', get_fake)
654 self.patch(MAASClient, 'post', post_fake)620 self.patch(MAASClient, 'post', post_fake)
655 tag_name = factory.make_name('tag')621 tag_name = factory.make_name('tag')
656 nodegroup_uuid = get_recorded_nodegroup_uuid()622 nodegroup_uuid = factory.make_name("nodegroup-uuid")
657 tag_definition = '//lshw:node'623 tag_definition = '//lshw:node'
658 tag_nsmap = {"lshw": "lshw"}624 tag_nsmap = {"lshw": "lshw"}
659 tags.process_node_tags(tag_name, tag_definition, tag_nsmap=tag_nsmap)625 tags.process_node_tags(
626 tag_name, tag_definition, tag_nsmap,
627 self.fake_client(), nodegroup_uuid)
660 nodegroup_url = '/api/1.0/nodegroups/%s/' % (nodegroup_uuid,)628 nodegroup_url = '/api/1.0/nodegroups/%s/' % (nodegroup_uuid,)
661 tag_url = '/api/1.0/tags/%s/' % (tag_name,)629 tag_url = '/api/1.0/tags/%s/' % (tag_name,)
662 self.assertEqual(630 self.assertEqual(
@@ -687,9 +655,6 @@
687 client = object()655 client = object()
688 uuid = factory.make_name('nodegroupuuid')656 uuid = factory.make_name('nodegroupuuid')
689 self.patch(657 self.patch(
690 tags, 'get_cached_knowledge',
691 MagicMock(return_value=(client, uuid)))
692 self.patch(
693 tags, 'get_nodes_for_node_group',658 tags, 'get_nodes_for_node_group',
694 MagicMock(return_value=['a', 'b', 'c']))659 MagicMock(return_value=['a', 'b', 'c']))
695 fake_first = FakeMethod(result={660 fake_first = FakeMethod(result={
@@ -706,8 +671,8 @@
706 tag_name = factory.make_name('tag')671 tag_name = factory.make_name('tag')
707 tag_definition = '//node'672 tag_definition = '//node'
708 tags.process_node_tags(673 tags.process_node_tags(
709 tag_name, tag_definition, tag_nsmap=None, batch_size=2)674 tag_name, tag_definition, tag_nsmap=None, client=client,
710 tags.get_cached_knowledge.assert_called_once_with()675 nodegroup_uuid=uuid, batch_size=2)
711 tags.get_nodes_for_node_group.assert_called_once_with(client, uuid)676 tags.get_nodes_for_node_group.assert_called_once_with(client, uuid)
712 self.assertEqual([((client, uuid, ['a', 'c']), {})], fake_first.calls)677 self.assertEqual([((client, uuid, ['a', 'c']), {})], fake_first.calls)
713 self.assertEqual([((client, uuid, ['b']), {})], fake_second.calls)678 self.assertEqual([((client, uuid, ['b']), {})], fake_second.calls)
714679
=== modified file 'src/provisioningserver/tests/test_tasks.py'
--- src/provisioningserver/tests/test_tasks.py 2014-09-15 14:28:28 +0000
+++ src/provisioningserver/tests/test_tasks.py 2014-09-23 09:14:01 +0000
@@ -37,7 +37,6 @@
37 auth,37 auth,
38 boot_images,38 boot_images,
39 cache,39 cache,
40 tags,
41 tasks,40 tasks,
42 )41 )
43from provisioningserver.boot import tftppath42from provisioningserver.boot import tftppath
@@ -52,15 +51,12 @@
52 DNSForwardZoneConfig,51 DNSForwardZoneConfig,
53 DNSReverseZoneConfig,52 DNSReverseZoneConfig,
54 )53 )
55from provisioningserver.tags import MissingCredentials
56from provisioningserver.tasks import (54from provisioningserver.tasks import (
57 refresh_secrets,55 refresh_secrets,
58 report_boot_images,56 report_boot_images,
59 rndc_command,57 rndc_command,
60 RNDC_COMMAND_MAX_RETRY,58 RNDC_COMMAND_MAX_RETRY,
61 setup_rndc_configuration,59 setup_rndc_configuration,
62 update_node_tags,
63 UPDATE_NODE_TAGS_MAX_RETRY,
64 write_dns_config,60 write_dns_config,
65 write_dns_zone_config,61 write_dns_zone_config,
66 write_full_dns_config,62 write_full_dns_config,
@@ -348,44 +344,3 @@
348344
349 args, kwargs = MAASClient.post.call_args345 args, kwargs = MAASClient.post.call_args
350 self.assertItemsEqual([image], json.loads(kwargs['images']))346 self.assertItemsEqual([image], json.loads(kwargs['images']))
351
352
353class TestTagTasks(PservTestCase):
354
355 def setUp(self):
356 super(TestTagTasks, self).setUp()
357 self.celery = self.useFixture(CeleryFixture())
358
359 def test_update_node_tags_can_be_retried(self):
360 self.set_secrets()
361 # The update_node_tags task can be retried.
362 # Simulate a temporary failure.
363 number_of_failures = UPDATE_NODE_TAGS_MAX_RETRY
364 raised_exception = MissingCredentials(
365 factory.make_name('exception'), random.randint(100, 200))
366 simulate_failures = MultiFakeMethod(
367 [FakeMethod(failure=raised_exception)] * number_of_failures +
368 [FakeMethod()])
369 self.patch(tags, 'process_node_tags', simulate_failures)
370 tag = factory.make_string()
371 result = update_node_tags.delay(
372 tag, '//node', tag_nsmap=None, retry=True)
373 assertTaskRetried(
374 self, result, UPDATE_NODE_TAGS_MAX_RETRY + 1,
375 'provisioningserver.tasks.update_node_tags')
376
377 def test_update_node_tags_is_retried_a_limited_number_of_times(self):
378 self.set_secrets()
379 # If we simulate UPDATE_NODE_TAGS_MAX_RETRY + 1 failures, the
380 # task fails.
381 number_of_failures = UPDATE_NODE_TAGS_MAX_RETRY + 1
382 raised_exception = MissingCredentials(
383 factory.make_name('exception'), random.randint(100, 200))
384 simulate_failures = MultiFakeMethod(
385 [FakeMethod(failure=raised_exception)] * number_of_failures +
386 [FakeMethod()])
387 self.patch(tags, 'process_node_tags', simulate_failures)
388 tag = factory.make_string()
389 self.assertRaises(
390 MissingCredentials, update_node_tags.delay, tag,
391 '//node', tag_nsmap=None, retry=True)