Merge lp:~gmb/maas/use-node.start-instead-of-start_nodes-bug-1330765 into lp:~maas-committers/maas/trunk

Proposed by Graham Binns
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 3277
Proposed branch: lp:~gmb/maas/use-node.start-instead-of-start_nodes-bug-1330765
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 497 lines (+106/-121)
5 files modified
src/maasserver/api/nodes.py (+8/-9)
src/maasserver/models/node.py (+10/-8)
src/maasserver/models/tests/test_node.py (+67/-82)
src/maasserver/node_action.py (+1/-1)
src/maasserver/tests/test_node_action.py (+20/-21)
To merge this branch: bzr merge lp:~gmb/maas/use-node.start-instead-of-start_nodes-bug-1330765
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+238758@code.launchpad.net

Commit message

Convert Node.objects.start_nodes() calls to use Node.start().

I've accounted for all the callsites except those in the tests for start_nodes, which I'll remove in a separate branch.

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

Smashing!

review: Approve
Revision history for this message
Graham Binns (gmb) wrote :
Download full text (22.3 KiB)

On 21 October 2014 08:45, Julian Edwards <email address hidden> wrote:
> Review: Approve
>
> Smashing!
>
> Diff comments:
>
>> === modified file 'src/maasserver/api/nodes.py'
>> --- src/maasserver/api/nodes.py 2014-10-17 16:54:22 +0000
>> +++ src/maasserver/api/nodes.py 2014-10-21 07:14:49 +0000
>> @@ -285,12 +285,14 @@
>> user_data = request.POST.get('user_data', None)
>> series = request.POST.get('distro_series', None)
>> license_key = request.POST.get('license_key', None)
>> +
>> + node = Node.objects.get_node_or_404(
>> + system_id=system_id, user=request.user,
>> + perm=NODE_PERMISSION.EDIT)
>> +
>> if user_data is not None:
>> user_data = b64decode(user_data)
>> if series is not None or license_key is not None:
>> - node = Node.objects.get_node_or_404(
>> - system_id=system_id, user=request.user,
>> - perm=NODE_PERMISSION.EDIT)
>> Form = get_node_edit_form(request.user)
>> form = Form(instance=node)
>> if series is not None:
>> @@ -301,19 +303,16 @@
>> form.save()
>> else:
>> raise ValidationError(form.errors)
>> +
>> try:
>> - nodes = Node.objects.start_nodes(
>> - [system_id], request.user, user_data=user_data)
>> + node.start(request.user, user_data=user_data)
>> except StaticIPAddressExhaustion:
>> # The API response should contain error text with the
>> # system_id in it, as that is the primary API key to a node.
>> raise StaticIPAddressExhaustion(
>> "%s: Unable to allocate static IP due to address"
>> " exhaustion." % system_id)
>> - if len(nodes) == 0:
>> - raise PermissionDenied(
>> - "You are not allowed to start up this node.")
>> - return nodes[0]
>> + return node
>>
>> @operation(idempotent=False)
>> def release(self, request, system_id):
>>
>> === modified file 'src/maasserver/models/node.py'
>> --- src/maasserver/models/node.py 2014-10-21 00:02:35 +0000
>> +++ src/maasserver/models/node.py 2014-10-21 07:14:49 +0000
>> @@ -969,11 +969,7 @@
>> self.save()
>> transaction.commit()
>> try:
>> - # We don't check for which nodes we've started here, because
>> - # it's possible we can't start the node - its power type may not
>> - # allow us to do that.
>> - Node.objects.start_nodes(
>> - [self.system_id], user, user_data=commissioning_user_data)
>> + self.start(user, user_data=commissioning_user_data)
>> except Exception as ex:
>> maaslog.error(
>> "%s: Unable to start node: %s",
>> @@ -1287,8 +1283,7 @@
>> self.save()
>> transaction.commit()
>> try:
>> - Node.objects.start_nodes(
>> - [self.system_id], user, user_data=disk_erase_user_data)
>> + self.start(user, user_data=disk_erase_user_data)
>> except E...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/nodes.py'
2--- src/maasserver/api/nodes.py 2014-10-17 16:54:22 +0000
3+++ src/maasserver/api/nodes.py 2014-10-21 08:28:19 +0000
4@@ -285,12 +285,14 @@
5 user_data = request.POST.get('user_data', None)
6 series = request.POST.get('distro_series', None)
7 license_key = request.POST.get('license_key', None)
8+
9+ node = Node.objects.get_node_or_404(
10+ system_id=system_id, user=request.user,
11+ perm=NODE_PERMISSION.EDIT)
12+
13 if user_data is not None:
14 user_data = b64decode(user_data)
15 if series is not None or license_key is not None:
16- node = Node.objects.get_node_or_404(
17- system_id=system_id, user=request.user,
18- perm=NODE_PERMISSION.EDIT)
19 Form = get_node_edit_form(request.user)
20 form = Form(instance=node)
21 if series is not None:
22@@ -301,19 +303,16 @@
23 form.save()
24 else:
25 raise ValidationError(form.errors)
26+
27 try:
28- nodes = Node.objects.start_nodes(
29- [system_id], request.user, user_data=user_data)
30+ node.start(request.user, user_data=user_data)
31 except StaticIPAddressExhaustion:
32 # The API response should contain error text with the
33 # system_id in it, as that is the primary API key to a node.
34 raise StaticIPAddressExhaustion(
35 "%s: Unable to allocate static IP due to address"
36 " exhaustion." % system_id)
37- if len(nodes) == 0:
38- raise PermissionDenied(
39- "You are not allowed to start up this node.")
40- return nodes[0]
41+ return node
42
43 @operation(idempotent=False)
44 def release(self, request, system_id):
45
46=== modified file 'src/maasserver/models/node.py'
47--- src/maasserver/models/node.py 2014-10-21 00:02:35 +0000
48+++ src/maasserver/models/node.py 2014-10-21 08:28:19 +0000
49@@ -969,11 +969,7 @@
50 self.save()
51 transaction.commit()
52 try:
53- # We don't check for which nodes we've started here, because
54- # it's possible we can't start the node - its power type may not
55- # allow us to do that.
56- Node.objects.start_nodes(
57- [self.system_id], user, user_data=commissioning_user_data)
58+ self.start(user, user_data=commissioning_user_data)
59 except Exception as ex:
60 maaslog.error(
61 "%s: Unable to start node: %s",
62@@ -1287,8 +1283,7 @@
63 self.save()
64 transaction.commit()
65 try:
66- Node.objects.start_nodes(
67- [self.system_id], user, user_data=disk_erase_user_data)
68+ self.start(user, user_data=disk_erase_user_data)
69 except Exception as ex:
70 maaslog.error(
71 "%s: Unable to start node: %s",
72@@ -1583,6 +1578,13 @@
73 from metadataserver.models import NodeUserData
74 from maasserver.dns.config import change_dns_zones
75
76+ if not by_user.has_perm(NODE_PERMISSION.EDIT, self):
77+ # You can't stop a node you don't own unless you're an
78+ # admin, so we return early. This is consistent with the
79+ # behaviour of NodeManager.stop_nodes(); it may be better to
80+ # raise an error here.
81+ return
82+
83 # Record the user data for the node. Note that we do this
84 # whether or not we can actually send power commands to the
85 # node; the user may choose to start it manually.
86@@ -1634,7 +1636,7 @@
87 :raises MultipleFailures: When there are failures originating
88 from the RPC power action.
89 """
90- if by_user != self.owner and not by_user.is_superuser:
91+ if not by_user.has_perm(NODE_PERMISSION.EDIT, self):
92 # You can't stop a node you don't own unless you're an
93 # admin, so we return early. This is consistent with the
94 # behaviour of NodeManager.stop_nodes(); it may be better to
95
96=== modified file 'src/maasserver/models/tests/test_node.py'
97--- src/maasserver/models/tests/test_node.py 2014-10-21 01:15:29 +0000
98+++ src/maasserver/models/tests/test_node.py 2014-10-21 08:28:19 +0000
99@@ -740,13 +740,13 @@
100 owner = factory.make_User()
101 node = factory.make_Node(
102 status=NODE_STATUS.ALLOCATED, owner=owner, agent_name=agent_name)
103- start_nodes = self.patch(Node.objects, "start_nodes")
104+ node_start = self.patch(node, 'start')
105 node.start_disk_erasing(owner)
106- self.assertEqual(
107- (owner, NODE_STATUS.DISK_ERASING, agent_name),
108- (node.owner, node.status, node.agent_name))
109- self.assertThat(start_nodes, MockCalledOnceWith(
110- [node.system_id], owner, user_data=ANY))
111+ self.expectThat(node.owner, Equals(owner))
112+ self.expectThat(node.status, Equals(NODE_STATUS.DISK_ERASING))
113+ self.expectThat(node.agent_name, Equals(agent_name))
114+ self.assertThat(
115+ node_start, MockCalledOnceWith(owner, user_data=ANY))
116
117 def test_abort_disk_erasing_changes_state_and_stops_node(self):
118 agent_name = factory.make_name('agent-name')
119@@ -769,53 +769,35 @@
120 # Failures encountered in one call to start_disk_erasing() won't
121 # affect subsequent calls.
122 admin = factory.make_admin()
123- nodes = [
124- factory.make_Node(
125- status=NODE_STATUS.ALLOCATED, power_type="virsh")
126- for _ in range(3)
127- ]
128+ node = factory.make_Node(status=NODE_STATUS.ALLOCATED)
129 generate_user_data = self.patch(disk_erasing, 'generate_user_data')
130- start_nodes = self.patch(Node.objects, 'start_nodes')
131- start_nodes.side_effect = [
132- None,
133- MultipleFailures(
134- Failure(NoConnectionsAvailable())),
135- None,
136- ]
137+ node_start = self.patch(node, 'start')
138+ node_start.side_effect = MultipleFailures(
139+ Failure(NoConnectionsAvailable())),
140
141 with transaction.atomic():
142- for node in nodes:
143- try:
144- node.start_disk_erasing(admin)
145- except RPC_EXCEPTIONS:
146- # Suppress all the expected errors coming out of
147- # start_disk_erasing() because they're tested
148- # eleswhere.
149- pass
150+ try:
151+ node.start_disk_erasing(admin)
152+ except RPC_EXCEPTIONS:
153+ # Suppress all the expected errors coming out of
154+ # start_disk_erasing() because they're tested
155+ # eleswhere.
156+ pass
157
158- expected_calls = (
159- call(
160- [node.system_id], admin,
161- user_data=generate_user_data.return_value)
162- for node in nodes)
163 self.assertThat(
164- start_nodes, MockCallsMatch(*expected_calls))
165- self.assertEqual(
166- [
167- NODE_STATUS.DISK_ERASING,
168- NODE_STATUS.FAILED_DISK_ERASING,
169- NODE_STATUS.DISK_ERASING,
170- ],
171- [node.status for node in nodes])
172+ node_start, MockCalledOnceWith(
173+ admin, user_data=generate_user_data.return_value))
174+ self.assertEqual(NODE_STATUS.FAILED_DISK_ERASING, node.status)
175
176 def test_start_disk_erasing_logs_and_raises_errors_in_starting(self):
177 admin = factory.make_admin()
178 node = factory.make_Node(status=NODE_STATUS.ALLOCATED)
179 maaslog = self.patch(node_module, 'maaslog')
180- exception = NoConnectionsAvailable(factory.make_name())
181- self.patch(Node.objects, 'start_nodes').side_effect = exception
182+ exception_type = factory.make_exception_type()
183+ exception = exception_type(factory.make_name())
184+ self.patch(node, 'start').side_effect = exception
185 self.assertRaises(
186- NoConnectionsAvailable, node.start_disk_erasing, admin)
187+ exception_type, node.start_disk_erasing, admin)
188 self.assertEqual(NODE_STATUS.FAILED_DISK_ERASING, node.status)
189 self.assertThat(
190 maaslog.error, MockCalledOnceWith(
191@@ -1337,8 +1319,7 @@
192 def test_start_commissioning_changes_status_and_starts_node(self):
193 node = factory.make_Node(
194 status=NODE_STATUS.NEW, power_type='ether_wake')
195- start_nodes = self.patch(Node.objects, "start_nodes")
196- start_nodes.return_value = [node]
197+ node_start = self.patch(node, 'start')
198 factory.make_MACAddress(node=node)
199 admin = factory.make_admin()
200 node.start_commissioning(admin)
201@@ -1347,21 +1328,20 @@
202 'status': NODE_STATUS.COMMISSIONING,
203 }
204 self.assertAttributes(node, expected_attrs)
205- self.assertThat(start_nodes, MockCalledOnceWith(
206- [node.system_id], admin, user_data=ANY))
207+ self.assertThat(node_start, MockCalledOnceWith(
208+ admin, user_data=ANY))
209
210 def test_start_commissioning_sets_user_data(self):
211- start_nodes = self.patch(Node.objects, "start_nodes")
212-
213 node = factory.make_Node(status=NODE_STATUS.NEW)
214+ node_start = self.patch(node, 'start')
215 user_data = factory.make_string().encode('ascii')
216 generate_user_data = self.patch(
217 commissioning, 'generate_user_data')
218 generate_user_data.return_value = user_data
219 admin = factory.make_admin()
220 node.start_commissioning(admin)
221- self.assertThat(start_nodes, MockCalledOnceWith(
222- [node.system_id], admin, user_data=user_data))
223+ self.assertThat(node_start, MockCalledOnceWith(
224+ admin, user_data=user_data))
225
226 def test_start_commissioning_clears_node_commissioning_results(self):
227 node = factory.make_Node(status=NODE_STATUS.NEW)
228@@ -1391,49 +1371,32 @@
229 # start the node, it will revert the node to its previous
230 # status.
231 admin = factory.make_admin()
232- nodes = [
233- factory.make_Node(status=NODE_STATUS.NEW, power_type="ether_wake")
234- for _ in range(3)
235- ]
236+ node = factory.make_Node(status=NODE_STATUS.NEW)
237 generate_user_data = self.patch(commissioning, 'generate_user_data')
238- start_nodes = self.patch(Node.objects, 'start_nodes')
239- start_nodes.side_effect = [
240- None,
241- MultipleFailures(
242- Failure(NoConnectionsAvailable())),
243- None,
244- ]
245+ node_start = self.patch(node, 'start')
246+ node_start.side_effect = MultipleFailures(
247+ Failure(NoConnectionsAvailable()))
248
249 with transaction.atomic():
250- for node in nodes:
251- try:
252- node.start_commissioning(admin)
253- except RPC_EXCEPTIONS:
254- # Suppress all expected errors; we test for them
255- # elsewhere.
256- pass
257+ try:
258+ node.start_commissioning(admin)
259+ except RPC_EXCEPTIONS:
260+ # Suppress all expected errors; we test for them
261+ # elsewhere.
262+ pass
263
264- expected_calls = (
265- call(
266- [node.system_id], admin,
267- user_data=generate_user_data.return_value)
268- for node in nodes)
269 self.assertThat(
270- start_nodes, MockCallsMatch(*expected_calls))
271- self.assertEqual(
272- [
273- NODE_STATUS.COMMISSIONING,
274- NODE_STATUS.NEW,
275- NODE_STATUS.COMMISSIONING
276- ],
277- [node.status for node in nodes])
278+ node_start,
279+ MockCalledOnceWith(
280+ admin, user_data=generate_user_data.return_value))
281+ self.assertEqual(NODE_STATUS.NEW, node.status)
282
283 def test_start_commissioning_logs_and_raises_errors_in_starting(self):
284 admin = factory.make_admin()
285 node = factory.make_Node(status=NODE_STATUS.NEW)
286 maaslog = self.patch(node_module, 'maaslog')
287 exception = NoConnectionsAvailable(factory.make_name())
288- self.patch(Node.objects, 'start_nodes').side_effect = exception
289+ self.patch(node, 'start').side_effect = exception
290 self.assertRaises(
291 NoConnectionsAvailable, node.start_commissioning, admin)
292 self.assertEqual(NODE_STATUS.NEW, node.status)
293@@ -2833,6 +2796,28 @@
294 node.start(user)
295 self.assertThat(power_on_nodes, MockNotCalled())
296
297+ def test__does_not_start_nodes_the_user_cannot_edit(self):
298+ power_on_nodes = self.patch_autospec(node_module, "power_on_nodes")
299+ owner = factory.make_User()
300+ node = self.make_acquired_node_with_mac(owner)
301+
302+ user = factory.make_User()
303+ node.start(user)
304+ self.assertThat(power_on_nodes, MockNotCalled())
305+
306+ def test__allows_admin_to_start_any_node(self):
307+ wait_for_power_commands = self.patch_autospec(
308+ node_module, 'wait_for_power_commands')
309+ power_on_nodes = self.patch_autospec(node_module, "power_on_nodes")
310+ owner = factory.make_User()
311+ node = self.make_acquired_node_with_mac(owner)
312+
313+ admin = factory.make_admin()
314+ node.start(admin)
315+
316+ self.expectThat(power_on_nodes, MockCalledOnceWith(ANY))
317+ self.expectThat(wait_for_power_commands, MockCalledOnceWith(ANY))
318+
319
320 class TestNode_Stop(MAASServerTestCase):
321 """Tests for Node.stop()."""
322
323=== modified file 'src/maasserver/node_action.py'
324--- src/maasserver/node_action.py 2014-10-17 16:54:22 +0000
325+++ src/maasserver/node_action.py 2014-10-21 08:28:19 +0000
326@@ -345,7 +345,7 @@
327 self.node.acquire(self.user, token=None)
328
329 try:
330- Node.objects.start_nodes([self.node.system_id], self.user)
331+ self.node.start(self.user)
332 except StaticIPAddressExhaustion:
333 raise NodeActionError(
334 "%s: Failed to start, static IP addresses are exhausted."
335
336=== modified file 'src/maasserver/tests/test_node_action.py'
337--- src/maasserver/tests/test_node_action.py 2014-10-20 20:40:50 +0000
338+++ src/maasserver/tests/test_node_action.py 2014-10-21 08:28:19 +0000
339@@ -230,17 +230,16 @@
340 )
341
342 def test_Commission_starts_commissioning(self):
343- start_nodes = self.patch(Node.objects, "start_nodes")
344 node = factory.make_Node(
345 mac=True, status=self.status,
346 power_type='ether_wake')
347+ node_start = self.patch(node, 'start')
348 admin = factory.make_admin()
349 action = Commission(node, admin)
350 action.execute()
351 self.assertEqual(NODE_STATUS.COMMISSIONING, node.status)
352 self.assertThat(
353- start_nodes, MockCalledOnceWith(
354- [node.system_id], admin, user_data=ANY))
355+ node_start, MockCalledOnceWith(admin, user_data=ANY))
356
357
358 class TestAbortCommissioningNodeAction(MAASServerTestCase):
359@@ -314,14 +313,14 @@
360 self.assertIn("SSH key", inhibition)
361
362 def test_StartNode_starts_node(self):
363- start_nodes = self.patch(Node.objects, "start_nodes")
364 user = factory.make_User()
365 node = factory.make_Node(
366 mac=True, status=NODE_STATUS.ALLOCATED,
367 power_type='ether_wake', owner=user)
368+ node_start = self.patch(node, 'start')
369 StartNode(node, user).execute()
370 self.assertThat(
371- start_nodes, MockCalledOnceWith([node.system_id], user))
372+ node_start, MockCalledOnceWith(user))
373
374 def test_StartNode_returns_error_when_no_more_static_IPs(self):
375 user = factory.make_User()
376@@ -351,9 +350,9 @@
377 self.assertFalse(StartNode(node, user).is_permitted())
378
379 def test_StartNode_allocates_node_if_node_not_already_allocated(self):
380- self.patch(Node.objects, "start_nodes")
381 user = factory.make_User()
382 node = factory.make_Node(status=NODE_STATUS.READY)
383+ self.patch(node, 'start')
384 action = StartNode(node, user)
385 action.execute()
386
387@@ -361,16 +360,16 @@
388 self.assertEqual(NODE_STATUS.ALLOCATED, node.status)
389
390 def test_StartNode_label_shows_allocate_if_unallocated(self):
391- self.patch(Node.objects, "start_nodes")
392 user = factory.make_User()
393 node = factory.make_Node(status=NODE_STATUS.READY)
394+ self.patch(node, 'start')
395 action = StartNode(node, user)
396 self.assertEqual("Acquire and start node", action.display)
397
398 def test_StartNode_label_hides_allocate_if_allocated(self):
399- self.patch(Node.objects, "start_nodes")
400 user = factory.make_User()
401 node = factory.make_Node(status=NODE_STATUS.READY)
402+ self.patch(node, 'start')
403 node.acquire(user)
404 action = StartNode(node, user)
405 self.assertEqual("Start node", action.display)
406@@ -384,17 +383,17 @@
407 self.assertEqual("Start node", action.display)
408
409 def test_StartNode_does_not_reallocate_when_run_by_non_owner(self):
410- self.patch(Node.objects, "start_nodes")
411 user = factory.make_User()
412 admin = factory.make_admin()
413 node = factory.make_Node(status=NODE_STATUS.READY)
414+ self.patch(node, 'start')
415 node.acquire(user)
416 action = StartNode(node, admin)
417
418 # This action.execute() will not fail because the non-owner is
419 # an admin, so they can start the node. Even if they weren't an
420- # admin, the node still wouldn't start;
421- # NodeManager.start_nodes() would ignore it.
422+ # admin, the node still wouldn't start; Node.start() would
423+ # ignore it.
424 action.execute()
425 self.assertEqual(user, node.owner)
426 self.assertEqual(NODE_STATUS.ALLOCATED, node.status)
427@@ -571,9 +570,9 @@
428 exception = self.exception_class(factory.make_name("exception"))
429 return exception
430
431- def patch_rpc_methods(self):
432+ def patch_rpc_methods(self, node):
433 exception = self.make_exception()
434- self.patch(Node.objects, "start_nodes").side_effect = (
435+ self.patch(node, 'start').side_effect = (
436 exception)
437 self.patch(Node.objects, "stop_nodes").side_effect = (
438 exception)
439@@ -586,17 +585,17 @@
440
441 def test_Commission_handles_rpc_errors(self):
442 action = self.make_action(Commission, NODE_STATUS.READY)
443- self.patch_rpc_methods()
444+ self.patch_rpc_methods(action.node)
445 exception = self.assertRaises(NodeActionError, action.execute)
446 self.assertEqual(
447 get_error_message_for_exception(
448- Node.objects.start_nodes.side_effect),
449+ action.node.start.side_effect),
450 unicode(exception))
451
452 def test_AbortCommissioning_handles_rpc_errors(self):
453 action = self.make_action(
454 AbortCommissioning, NODE_STATUS.COMMISSIONING)
455- self.patch_rpc_methods()
456+ self.patch_rpc_methods(action.node)
457 exception = self.assertRaises(NodeActionError, action.execute)
458 self.assertEqual(
459 get_error_message_for_exception(
460@@ -606,7 +605,7 @@
461 def test_AbortOperation_handles_rpc_errors(self):
462 action = self.make_action(
463 AbortOperation, NODE_STATUS.DISK_ERASING)
464- self.patch_rpc_methods()
465+ self.patch_rpc_methods(action.node)
466 exception = self.assertRaises(NodeActionError, action.execute)
467 self.assertEqual(
468 get_error_message_for_exception(
469@@ -615,16 +614,16 @@
470
471 def test_StartNode_handles_rpc_errors(self):
472 action = self.make_action(StartNode, NODE_STATUS.READY)
473- self.patch_rpc_methods()
474+ self.patch_rpc_methods(action.node)
475 exception = self.assertRaises(NodeActionError, action.execute)
476 self.assertEqual(
477 get_error_message_for_exception(
478- Node.objects.start_nodes.side_effect),
479+ action.node.start.side_effect),
480 unicode(exception))
481
482 def test_StopNode_handles_rpc_errors(self):
483 action = self.make_action(StopNode, NODE_STATUS.DEPLOYED)
484- self.patch_rpc_methods()
485+ self.patch_rpc_methods(action.node)
486 exception = self.assertRaises(NodeActionError, action.execute)
487 self.assertEqual(
488 get_error_message_for_exception(
489@@ -633,7 +632,7 @@
490
491 def test_ReleaseNode_handles_rpc_errors(self):
492 action = self.make_action(ReleaseNode, NODE_STATUS.ALLOCATED)
493- self.patch_rpc_methods()
494+ self.patch_rpc_methods(action.node)
495 exception = self.assertRaises(NodeActionError, action.execute)
496 self.assertEqual(
497 get_error_message_for_exception(