Merge lp:~blake-rouse/maas/query-power-on-status-change into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 3295
Proposed branch: lp:~blake-rouse/maas/query-power-on-status-change
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 291 lines (+263/-0)
4 files modified
src/maasserver/models/__init__.py (+3/-0)
src/maasserver/node_query.py (+108/-0)
src/maasserver/node_status.py (+20/-0)
src/maasserver/tests/test_node_query.py (+132/-0)
To merge this branch: bzr merge lp:~blake-rouse/maas/query-power-on-status-change
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+238344@code.launchpad.net

Commit message

Perform PowerQuery on a node 20 seconds after a status change.

This is done when nodes make a status transition that do not perform a power action. e.g. Commissioning -> Ready, does not perform a power action, updating the power status 20 seconds after changes gives the machine time to shutdown, while providing a more up-to-date power status.

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

Looks good, but marked needs-fixing because we can avoid the signal handler by using Node.clean_status() instead. See comments inline.

review: Needs Fixing
Revision history for this message
Blake Rouse (blake-rouse) :
Revision history for this message
Julian Edwards (julian-edwards) wrote :

 review: approve

On Wednesday 22 Oct 2014 19:54:52 you wrote:
> This is the same approach that is done to log to the event log when a node
> status transitions. This was discussed and recommended by rvba, to do it
> this way.

Ah yes, rvb loves his signals. Ok let's leave that for now.

Thanks for the other changes, looks good to go.

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

The attempt to merge lp:~blake-rouse/maas/query-power-on-status-change into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Get:2 http://security.ubuntu.com trusty-security Release [59.7 kB]
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
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 [48.3 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Get:6 http://security.ubuntu.com trusty-security/universe Sources [11.2 kB]
Get:7 http://security.ubuntu.com trusty-security/main amd64 Packages [149 kB]
Get:8 http://security.ubuntu.com trusty-security/universe amd64 Packages [50.4 kB]
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
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:9 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [129 kB]
Get:10 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [88.2 kB]
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [343 kB]
Get:12 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [214 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
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Fetched 1,155 kB in 3s (354 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 gjs 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-ws...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/models/__init__.py'
2--- src/maasserver/models/__init__.py 2014-09-29 15:26:08 +0000
3+++ src/maasserver/models/__init__.py 2014-10-23 13:33:12 +0000
4@@ -192,3 +192,6 @@
5
6 from maasserver import event_connect
7 ignore_unused(event_connect)
8+
9+from maasserver import node_query
10+ignore_unused(node_query)
11
12=== added file 'src/maasserver/node_query.py'
13--- src/maasserver/node_query.py 1970-01-01 00:00:00 +0000
14+++ src/maasserver/node_query.py 2014-10-23 13:33:12 +0000
15@@ -0,0 +1,108 @@
16+# Copyright 2014 Canonical Ltd. This software is licensed under the
17+# GNU Affero General Public License version 3 (see the file LICENSE).
18+
19+"""Query power status on node state changes."""
20+
21+from __future__ import (
22+ absolute_import,
23+ print_function,
24+ unicode_literals,
25+ )
26+
27+str = None
28+
29+__metaclass__ = type
30+__all__ = []
31+
32+from datetime import timedelta
33+
34+from crochet import TimeoutError
35+from maasserver.enum import POWER_STATE
36+from maasserver.models import Node
37+from maasserver.node_status import QUERY_TRANSITIONS
38+from maasserver.rpc import getClientFor
39+from maasserver.signals import connect_to_field_change
40+from provisioningserver.logger import get_maas_logger
41+from provisioningserver.power.poweraction import (
42+ PowerActionFail,
43+ UnknownPowerType,
44+ )
45+from provisioningserver.rpc.cluster import PowerQuery
46+from provisioningserver.rpc.exceptions import NoConnectionsAvailable
47+from provisioningserver.utils.twisted import synchronous
48+from twisted.internet import reactor
49+from twisted.internet.threads import deferToThread
50+
51+
52+maaslog = get_maas_logger('node_query')
53+
54+# Amount of time to wait after a node status has been updated to
55+# perform a power query.
56+WAIT_TO_QUERY = timedelta(seconds=20)
57+
58+
59+@synchronous
60+def update_power_state_of_node(system_id):
61+ """Update the power state of the given node."""
62+ try:
63+ node = Node.objects.get(system_id=system_id)
64+ except Node.DoesNotExist:
65+ # Just in case the Node has been deleted,
66+ # before we get to this point.
67+ return
68+
69+ try:
70+ client = getClientFor(node.nodegroup.uuid)
71+ except NoConnectionsAvailable:
72+ maaslog.error(
73+ "Unable to get RPC connection for cluster '%s' (%s)",
74+ node.nodegroup.cluster_name, node.nodegroup.uuid)
75+ return
76+
77+ try:
78+ power_info = node.get_effective_power_info()
79+ except UnknownPowerType:
80+ return
81+ if not power_info.can_be_started:
82+ # Power state is not queryable
83+ return
84+
85+ call = client(
86+ PowerQuery, system_id=system_id, hostname=node.hostname,
87+ power_type=power_info.power_type,
88+ context=power_info.power_parameters)
89+ try:
90+ state = call.wait(30).get("state", POWER_STATE.ERROR)
91+ except (TimeoutError, NotImplementedError, PowerActionFail):
92+ state = POWER_STATE.ERROR
93+ node.power_state = state
94+ node.save()
95+
96+
97+def wait_to_update_power_state_of_node(system_id, clock=reactor):
98+ """Wait "WAIT_TO_QUERY" amount of time then update the power state of
99+ the given node."""
100+ clock.callLater(
101+ WAIT_TO_QUERY.total_seconds(), deferToThread,
102+ update_power_state_of_node, system_id)
103+
104+
105+@synchronous
106+def signal_update_power_state_of_node(instance, old_values, **kwargs):
107+ """Updates the power state of a node, when its status changes."""
108+ node = instance
109+ [old_status] = old_values
110+
111+ # Check if this transition should even check for a new power state.
112+ if old_status not in QUERY_TRANSITIONS:
113+ return
114+ if node.status not in QUERY_TRANSITIONS[old_status]:
115+ return
116+
117+ # Update the power state of the node, after the waiting period.
118+ wait_to_update_power_state_of_node(node.system_id)
119+
120+
121+connect_to_field_change(
122+ signal_update_power_state_of_node,
123+ Node, ['status'], delete=False)
124
125=== modified file 'src/maasserver/node_status.py'
126--- src/maasserver/node_status.py 2014-10-15 13:55:21 +0000
127+++ src/maasserver/node_status.py 2014-10-23 13:33:12 +0000
128@@ -183,6 +183,26 @@
129 NODE_STATUS.DISK_ERASING,
130 ]
131
132+# Node state transitions that perform query actions. This is to keep the
133+# power state of the node up-to-date when transitions occur that do not
134+# perform a power action directly.
135+QUERY_TRANSITIONS = {
136+ None: [
137+ NODE_STATUS.NEW,
138+ ],
139+ NODE_STATUS.COMMISSIONING: [
140+ NODE_STATUS.FAILED_COMMISSIONING,
141+ NODE_STATUS.READY,
142+ ],
143+ NODE_STATUS.DEPLOYING: [
144+ NODE_STATUS.FAILED_DEPLOYMENT,
145+ NODE_STATUS.DEPLOYED,
146+ ],
147+ NODE_STATUS.DISK_ERASING: [
148+ NODE_STATUS.FAILED_DISK_ERASING,
149+ ],
150+ }
151+
152
153 def get_failed_status(status):
154 """Returns the failed status corresponding to the given status.
155
156=== added file 'src/maasserver/tests/test_node_query.py'
157--- src/maasserver/tests/test_node_query.py 1970-01-01 00:00:00 +0000
158+++ src/maasserver/tests/test_node_query.py 2014-10-23 13:33:12 +0000
159@@ -0,0 +1,132 @@
160+# Copyright 2014 Canonical Ltd. This software is licensed under the
161+# GNU Affero General Public License version 3 (see the file LICENSE).
162+
163+"""Tests for node power status query when state changes."""
164+
165+from __future__ import (
166+ absolute_import,
167+ print_function,
168+ unicode_literals,
169+ )
170+
171+str = None
172+
173+__metaclass__ = type
174+__all__ = []
175+
176+import random
177+
178+from maasserver.node_status import (
179+ get_failed_status,
180+ NODE_STATUS,
181+ )
182+from maasserver.rpc.testing.fixtures import MockLiveRegionToClusterRPCFixture
183+from maasserver.testing.eventloop import (
184+ RegionEventLoopFixture,
185+ RunningEventLoopFixture,
186+ )
187+from maasserver.testing.factory import factory
188+from maasserver.testing.orm import reload_object
189+from maasserver.testing.testcase import MAASServerTestCase
190+from maastesting.matchers import (
191+ MockCalledOnceWith,
192+ MockNotCalled,
193+ )
194+from provisioningserver.power.poweraction import PowerActionFail
195+from provisioningserver.rpc import cluster as cluster_module
196+from provisioningserver.rpc.testing import always_succeed_with
197+from twisted.internet.task import Clock
198+
199+
200+class TestStatusQueryEvent(MAASServerTestCase):
201+
202+ def setUp(self):
203+ super(TestStatusQueryEvent, self).setUp()
204+ # Circular imports.
205+ from maasserver import node_query
206+ self.node_query = node_query
207+
208+ def test_changing_status_of_node_emits_event(self):
209+ mock_update = self.patch(
210+ self.node_query, 'wait_to_update_power_state_of_node')
211+ old_status = NODE_STATUS.COMMISSIONING
212+ node = factory.make_Node(status=old_status, power_type='virsh')
213+ node.status = get_failed_status(old_status)
214+ node.save()
215+ self.assertThat(
216+ mock_update,
217+ MockCalledOnceWith(node.system_id))
218+
219+ def test_changing_not_tracked_status_of_node_doesnt_emit_event(self):
220+ mock_update = self.patch(
221+ self.node_query, "wait_to_update_power_state_of_node")
222+ old_status = NODE_STATUS.ALLOCATED
223+ node = factory.make_Node(status=old_status, power_type="virsh")
224+ node.status = NODE_STATUS.DEPLOYING
225+ node.save()
226+ self.assertThat(
227+ mock_update,
228+ MockNotCalled())
229+
230+
231+class TestWaitToUpdatePowerStateOfNode(MAASServerTestCase):
232+
233+ def setUp(self):
234+ super(TestWaitToUpdatePowerStateOfNode, self).setUp()
235+ # Circular imports.
236+ from maasserver import node_query
237+ self.node_query = node_query
238+
239+ def test__calls_update_power_state_of_node_after_wait_time(self):
240+ mock_defer_to_thread = self.patch(self.node_query, 'deferToThread')
241+ node = factory.make_Node(power_type="virsh")
242+ clock = Clock()
243+ self.node_query.wait_to_update_power_state_of_node(
244+ node.system_id, clock=clock)
245+
246+ self.expectThat(mock_defer_to_thread, MockNotCalled())
247+ clock.advance(self.node_query.WAIT_TO_QUERY.total_seconds())
248+ self.expectThat(
249+ mock_defer_to_thread,
250+ MockCalledOnceWith(
251+ self.node_query.update_power_state_of_node, node.system_id))
252+
253+
254+class TestUpdatePowerStateOfNode(MAASServerTestCase):
255+
256+ def setUp(self):
257+ super(TestUpdatePowerStateOfNode, self).setUp()
258+ # Circular imports.
259+ from maasserver import node_query
260+ self.node_query = node_query
261+
262+ def prepare_rpc(self, nodegroup, side_effect):
263+ self.useFixture(RegionEventLoopFixture("rpc"))
264+ self.useFixture(RunningEventLoopFixture())
265+ self.rpc_fixture = self.useFixture(MockLiveRegionToClusterRPCFixture())
266+ protocol = self.rpc_fixture.makeCluster(
267+ nodegroup, cluster_module.PowerQuery)
268+ protocol.PowerQuery.side_effect = side_effect
269+
270+ def test__updates_node_power_state(self):
271+ node = factory.make_Node(power_type="virsh")
272+ random_state = random.choice(["on", "off"])
273+ self.prepare_rpc(
274+ node.nodegroup,
275+ side_effect=always_succeed_with({"state": random_state}))
276+ self.node_query.update_power_state_of_node(node.system_id)
277+ self.assertEqual(random_state, reload_object(node).power_state)
278+
279+ def test__handles_deleted_node(self):
280+ node = factory.make_Node(power_type="virsh")
281+ node.delete()
282+ self.node_query.update_power_state_of_node(node.system_id)
283+ #: Test is that no error is raised
284+
285+ def test__updates_node_power_state_to_error_if_failure(self):
286+ node = factory.make_Node(power_type="virsh")
287+ self.prepare_rpc(
288+ node.nodegroup,
289+ side_effect=PowerActionFail())
290+ self.node_query.update_power_state_of_node(node.system_id)
291+ self.assertEqual("error", reload_object(node).power_state)