1
# Copyright 2014 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Tests for :py:module:`~provisioningserver.rpc.power`."""
6
from __future__ import (
20
from maastesting.factory import factory
21
from maastesting.matchers import (
26
from maastesting.testcase import MAASTestCase
32
import provisioningserver
33
from provisioningserver.events import EVENT_TYPES
34
from provisioningserver.power.poweraction import PowerActionFail
35
from provisioningserver.rpc import (
39
from provisioningserver.rpc.testing import MockClusterToRegionRPCFixture
40
from testtools.deferredruntest import (
42
AsynchronousDeferredRunTest,
44
from twisted.internet.defer import maybeDeferred
45
from twisted.internet.task import Clock
48
class TestPowerHelpers(MAASTestCase):
50
run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
52
def patch_rpc_methods(self):
53
fixture = self.useFixture(MockClusterToRegionRPCFixture())
54
protocol, io = fixture.makeEventLoop(
55
region.MarkNodeBroken, region.UpdateNodePowerState,
59
def test_power_change_success_emits_event(self):
60
system_id = factory.make_name('system_id')
61
hostname = factory.make_name('hostname')
63
protocol, io = self.patch_rpc_methods()
64
d = power.power_change_success(system_id, hostname, power_change)
67
protocol.UpdateNodePowerState,
71
power_state=power_change)
77
type_name=EVENT_TYPES.NODE_POWERED_ON,
83
def test_power_change_starting_emits_event(self):
84
system_id = factory.make_name('system_id')
85
hostname = factory.make_name('hostname')
87
protocol, io = self.patch_rpc_methods()
88
d = power.power_change_starting(system_id, hostname, power_change)
94
type_name=EVENT_TYPES.NODE_POWER_ON_STARTING,
100
def test_power_change_failure_emits_event(self):
101
system_id = factory.make_name('system_id')
102
hostname = factory.make_name('hostname')
103
message = factory.make_name('message')
105
protocol, io = self.patch_rpc_methods()
106
d = power.power_change_failure(
107
system_id, hostname, power_change, message)
113
type_name=EVENT_TYPES.NODE_POWER_ON_FAILED,
119
def test_power_query_failure_emits_event(self):
120
system_id = factory.make_name('system_id')
121
hostname = factory.make_name('hostname')
122
message = factory.make_name('message')
123
protocol, io = self.patch_rpc_methods()
124
d = power.power_query_failure(
125
system_id, hostname, message)
126
# This blocks until the deferred is complete
128
self.assertTrue(d.called)
133
type_name=EVENT_TYPES.NODE_POWER_QUERY_FAILED,
139
def test_power_query_failure_marks_node_broken(self):
140
system_id = factory.make_name('system_id')
141
hostname = factory.make_name('hostname')
142
message = factory.make_name('message')
143
protocol, io = self.patch_rpc_methods()
144
d = power.power_query_failure(
145
system_id, hostname, message)
146
# This blocks until the deferred is complete
148
self.assertTrue(d.called)
150
protocol.MarkNodeBroken,
154
error_description=message)
158
def test_power_state_update_calls_UpdateNodePowerState(self):
159
system_id = factory.make_name('system_id')
160
state = random.choice(['on', 'off'])
161
protocol, io = self.patch_rpc_methods()
162
d = power.power_state_update(
164
# This blocks until the deferred is complete
166
self.assertTrue(d.called)
168
protocol.UpdateNodePowerState,
177
class TestChangePowerChange(MAASTestCase):
179
run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
182
super(TestChangePowerChange, self).setUp()
184
provisioningserver.rpc.power, 'deferToThread', maybeDeferred)
186
def patch_power_action(self, return_value=None, side_effect=None):
187
"""Patch the PowerAction object.
189
Patch the PowerAction object so that PowerAction().execute
190
is replaced by a Mock object created using the given `return_value`
193
This can be used to simulate various successes or failures patterns
194
while manipulating the power state of a node.
196
Returns a tuple of mock objects: power.PowerAction and
197
power.PowerAction().execute.
199
power_action_obj = Mock()
200
power_action_obj_execute = Mock(
201
return_value=return_value, side_effect=side_effect)
202
power_action_obj.execute = power_action_obj_execute
203
power_action = self.patch(power, 'PowerAction')
204
power_action.return_value = power_action_obj
205
return power_action, power_action_obj_execute
207
def patch_rpc_methods(self, return_value={}, side_effect=None):
208
fixture = self.useFixture(MockClusterToRegionRPCFixture())
209
protocol, io = fixture.makeEventLoop(
210
region.MarkNodeBroken, region.UpdateNodePowerState,
212
protocol.MarkNodeBroken.return_value = return_value
213
protocol.MarkNodeBroken.side_effect = side_effect
214
return protocol.MarkNodeBroken, io
216
def test_change_power_state_changes_power_state(self):
217
system_id = factory.make_name('system_id')
218
hostname = factory.make_name('hostname')
219
power_type = random.choice(power.QUERY_POWER_TYPES)
220
power_change = random.choice(['on', 'off'])
222
factory.make_name('context-key'): factory.make_name('context-val')
224
self.patch(power, 'pause')
225
# Patch the power action utility so that it says the node is
226
# in the required power state.
227
power_action, execute = self.patch_power_action(
228
return_value=power_change)
229
markNodeBroken, io = self.patch_rpc_methods()
231
d = power.change_power_state(
232
system_id, hostname, power_type, power_change, context)
237
# One call to change the power state.
238
call(power_change=power_change, **context),
239
# One call to query the power state.
240
call(power_change='query', **context),
243
# The node hasn't been marked broken.
244
self.assertThat(markNodeBroken, MockNotCalled())
247
def test_change_power_state_doesnt_retry_for_certain_power_types(self):
248
system_id = factory.make_name('system_id')
249
hostname = factory.make_name('hostname')
250
# Use a power type that is not among power.QUERY_POWER_TYPES.
251
power_type = factory.make_name('power_type')
252
power_change = random.choice(['on', 'off'])
254
factory.make_name('context-key'): factory.make_name('context-val')
256
self.patch(power, 'pause')
257
power_action, execute = self.patch_power_action(
258
return_value=random.choice(['on', 'off']))
259
markNodeBroken, io = self.patch_rpc_methods()
261
d = power.change_power_state(
262
system_id, hostname, power_type, power_change, context)
267
# Only one call to change the power state.
268
call(power_change=power_change, **context),
271
# The node hasn't been marked broken.
272
self.assertThat(markNodeBroken, MockNotCalled())
275
def test_change_power_state_retries_if_power_state_doesnt_change(self):
276
system_id = factory.make_name('system_id')
277
hostname = factory.make_name('hostname')
278
power_type = random.choice(power.QUERY_POWER_TYPES)
281
factory.make_name('context-key'): factory.make_name('context-val')
283
self.patch(power, 'pause')
284
# Simulate a failure to power up the node, then a success.
285
power_action, execute = self.patch_power_action(
286
side_effect=[None, 'off', None, 'on'])
287
markNodeBroken, io = self.patch_rpc_methods()
289
d = power.change_power_state(
290
system_id, hostname, power_type, power_change, context)
295
call(power_change=power_change, **context),
296
call(power_change='query', **context),
297
call(power_change=power_change, **context),
298
call(power_change='query', **context),
301
# The node hasn't been marked broken.
302
self.assertThat(markNodeBroken, MockNotCalled())
305
def test_change_power_state_marks_the_node_broken_if_failure(self):
306
system_id = factory.make_name('system_id')
307
hostname = factory.make_name('hostname')
308
power_type = random.choice(power.QUERY_POWER_TYPES)
311
factory.make_name('context-key'): factory.make_name('context-val')
313
self.patch(power, 'pause')
314
# Simulate a persistent failure.
315
power_action, execute = self.patch_power_action(return_value='off')
316
markNodeBroken, io = self.patch_rpc_methods()
318
d = power.change_power_state(
319
system_id, hostname, power_type, power_change, context)
322
# The node has been marked broken.
323
msg = "Timeout after %s tries" % len(
324
power.default_waiting_policy)
330
error_description=msg)
334
def test_change_power_state_marks_the_node_broken_if_exception(self):
335
system_id = factory.make_name('system_id')
336
hostname = factory.make_name('hostname')
337
power_type = random.choice(power.QUERY_POWER_TYPES)
340
factory.make_name('context-key'): factory.make_name('context-val')
342
self.patch(power, 'pause')
343
# Simulate an exception.
344
exception_message = factory.make_name('exception')
345
power_action, execute = self.patch_power_action(
346
side_effect=PowerActionFail(exception_message))
347
markNodeBroken, io = self.patch_rpc_methods()
349
d = power.change_power_state(
350
system_id, hostname, power_type, power_change, context)
352
assert_fails_with(d, PowerActionFail)
353
error_message = "Node could not be powered on: %s" % exception_message
359
ANY, system_id=system_id, error_description=error_message))
361
return d.addCallback(check)
363
def test_change_power_state_pauses_inbetween_retries(self):
364
system_id = factory.make_name('system_id')
365
hostname = factory.make_name('hostname')
366
power_type = random.choice(power.QUERY_POWER_TYPES)
369
factory.make_name('context-key'): factory.make_name('context-val')
371
# Simulate two failures to power up the node, then a success.
372
power_action, execute = self.patch_power_action(
373
side_effect=[None, 'off', None, 'off', None, 'on'])
374
self.patch(power, "deferToThread", maybeDeferred)
375
markNodeBroken, io = self.patch_rpc_methods()
380
call(power_change=power_change, **context),
383
call(power_change='query', **context),
384
call(power_change=power_change, **context),
387
call(power_change='query', **context),
388
call(power_change=power_change, **context),
391
call(power_change='query', **context),
395
d = power.change_power_state(
396
system_id, hostname, power_type, power_change, context,
398
for newcalls, waiting_time in calls_and_pause:
399
calls.extend(newcalls)
401
self.assertThat(execute, MockCallsMatch(*calls))
402
clock.advance(waiting_time)
406
class TestPowerQuery(MAASTestCase):
408
run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
411
super(TestPowerQuery, self).setUp()
413
provisioningserver.rpc.power, 'deferToThread', maybeDeferred)
415
def patch_power_action(self, return_value=None, side_effect=None):
416
"""Patch the PowerAction object.
418
Patch the PowerAction object so that PowerAction().execute
419
is replaced by a Mock object created using the given `return_value`
422
This can be used to simulate various successes or failures patterns
423
while performing operations on the node.
425
Returns a tuple of mock objects: power.PowerAction and
426
power.PowerAction().execute.
428
power_action_obj = Mock()
429
power_action_obj_execute = Mock(
430
return_value=return_value, side_effect=side_effect)
431
power_action_obj.execute = power_action_obj_execute
432
power_action = self.patch(power, 'PowerAction')
433
power_action.return_value = power_action_obj
434
return power_action, power_action_obj_execute
436
def patch_rpc_methods(self, return_value={}, side_effect=None):
437
fixture = self.useFixture(MockClusterToRegionRPCFixture())
438
protocol, io = fixture.makeEventLoop(
439
region.MarkNodeBroken, region.SendEvent)
440
protocol.MarkNodeBroken.return_value = return_value
441
protocol.MarkNodeBroken.side_effect = side_effect
442
return protocol.SendEvent, protocol.MarkNodeBroken, io
444
def test_get_power_state_querys_node(self):
445
system_id = factory.make_name('system_id')
446
hostname = factory.make_name('hostname')
447
power_type = random.choice(power.QUERY_POWER_TYPES)
448
power_state = random.choice(['on', 'off'])
450
factory.make_name('context-key'): factory.make_name('context-val')
452
self.patch(power, 'pause')
453
# Patch the power action utility so that it says the node is
454
# in on/off power state.
455
power_action, execute = self.patch_power_action(
456
return_value=power_state)
457
_, markNodeBroken, io = self.patch_rpc_methods()
459
d = power.get_power_state(
460
system_id, hostname, power_type, context)
461
# This blocks until the deferred is complete
463
self.assertTrue(d.called)
467
# One call to change the power state.
468
call(power_change='query', **context),
471
self.assertEqual(power_state, d.result)
474
def test_get_power_state_returns_unknown_for_certain_power_types(self):
475
system_id = factory.make_name('system_id')
476
hostname = factory.make_name('hostname')
477
# Use a power type that is not among power.QUERY_POWER_TYPES.
478
power_type = factory.make_name('power_type')
480
factory.make_name('context-key'): factory.make_name('context-val')
482
_, _, io = self.patch_rpc_methods()
484
d = power.get_power_state(
485
system_id, hostname, power_type, context)
486
# This blocks until the deferred is complete
488
self.assertTrue(d.called)
489
self.assertEqual('unknown', d.result)
492
def test_get_power_state_retries_if_power_query_fails(self):
493
system_id = factory.make_name('system_id')
494
hostname = factory.make_name('hostname')
495
power_type = random.choice(power.QUERY_POWER_TYPES)
496
power_state = random.choice(['on', 'off'])
497
err_msg = factory.make_name('error')
499
factory.make_name('context-key'): factory.make_name('context-val')
501
self.patch(power, 'pause')
502
# Simulate a failure to power query the node, then success.
503
power_action, execute = self.patch_power_action(
504
side_effect=[PowerActionFail(err_msg), power_state])
505
sendEvent, markNodeBroken, io = self.patch_rpc_methods()
507
d = power.get_power_state(
508
system_id, hostname, power_type, context)
509
# This blocks until the deferred is complete
511
self.assertTrue(d.called)
515
call(power_change='query', **context),
516
call(power_change='query', **context),
519
# The node hasn't been marked broken.
520
self.assertThat(markNodeBroken, MockNotCalled())
521
self.assertEqual(power_state, d.result)
524
def test_get_power_state_marks_the_node_broken_if_failure(self):
525
system_id = factory.make_name('system_id')
526
hostname = factory.make_name('hostname')
527
power_type = random.choice(power.QUERY_POWER_TYPES)
528
err_msg = factory.make_name('error')
530
factory.make_name('context-key'): factory.make_name('context-val')
532
self.patch(power, 'pause')
533
# Simulate a persistent failure.
534
power_action, execute = self.patch_power_action(
535
side_effect=PowerActionFail(err_msg))
536
_, markNodeBroken, io = self.patch_rpc_methods()
538
d = power.get_power_state(
539
system_id, hostname, power_type, context)
540
# This blocks until the deferred is complete
542
self.assertTrue(d.called)
543
# The node has been marked broken.
549
error_description="Node could not be queried %s (%s) %s" % (
550
system_id, hostname, err_msg))
552
self.assertEqual('error', d.result)
555
def test_get_power_state_pauses_inbetween_retries(self):
556
system_id = factory.make_name('system_id')
557
hostname = factory.make_name('hostname')
558
power_type = random.choice(power.QUERY_POWER_TYPES)
560
factory.make_name('context-key'): factory.make_name('context-val')
562
# Simulate two failures to power up the node, then a success.
563
power_action, execute = self.patch_power_action(
564
side_effect=[PowerActionFail, PowerActionFail, 'off'])
565
self.patch(power, "deferToThread", maybeDeferred)
566
_, _, io = self.patch_rpc_methods()
571
call(power_change='query', **context),
574
call(power_change='query', **context),
577
call(power_change='query', **context),
581
d = power.get_power_state(
582
system_id, hostname, power_type, context, clock=clock)
583
for newcalls, waiting_time in calls_and_pause:
584
calls.extend(newcalls)
585
# This blocks until the deferred is complete
587
self.assertThat(execute, MockCallsMatch(*calls))
588
clock.advance(waiting_time)
591
def make_nodes(self):
594
system_id = factory.make_name('system_id')
595
hostname = factory.make_name('hostname')
596
power_type = random.choice(power.QUERY_POWER_TYPES)
597
state = random.choice(['on', 'off', 'unknown', 'error'])
600
'context-key'): factory.make_name('context-val')
603
'system_id': system_id,
604
'hostname': hostname,
605
'power_type': power_type,
611
def pick_alternate_state(self, state):
612
return random.choice([
613
value for value in ['on', 'off', 'unknown', 'error']
616
def test_query_all_nodes_calls_get_power_state(self):
617
nodes = self.make_nodes()
618
states = [node['state'] for node in nodes]
619
get_state = self.patch(power, 'get_power_state')
620
get_state.side_effect = states
626
node['system_id'], node['hostname'],
627
node['power_type'], node['context']))
629
self.assertThat(get_state, MockCallsMatch(*calls))
631
def test_query_all_nodes_calls_power_state_update(self):
632
nodes = self.make_nodes()
633
states = [self.pick_alternate_state(node['state']) for node in nodes]
634
get_state = self.patch(power, 'get_power_state')
635
get_state.side_effect = states
636
update_state = self.patch(power, 'power_state_update')
639
for i in range(len(nodes)):
641
new_state = states[i]
644
node['system_id'], new_state))
646
self.assertThat(update_state, MockCallsMatch(*calls))