From 1188d4263dc02690a8b4c9fa89441ff4ff7c3f41 Mon Sep 17 00:00:00 2001
From: licanwei
Date: Fri, 2 Jun 2017 15:45:57 +0800
Subject: [PATCH] Add action for compute node power on/off
Add action for compute node power on/off
Change-Id: I7b0c0a7500f72f49af8201547640b2322c64baff
Implements: blueprint add-power-on-off
---
.../add-power-on-off-a77673d482568a8b.yaml | 3 +
setup.cfg | 1 +
.../actions/change_node_power_state.py | 113 ++++++++++++++
watcher/decision_engine/planner/weight.py | 12 +-
.../actions/test_change_node_power_state.py | 147 ++++++++++++++++++
5 files changed, 271 insertions(+), 5 deletions(-)
create mode 100644 releasenotes/notes/add-power-on-off-a77673d482568a8b.yaml
create mode 100644 watcher/applier/actions/change_node_power_state.py
mode change 100755 => 100644 watcher/decision_engine/planner/weight.py
create mode 100644 watcher/tests/applier/actions/test_change_node_power_state.py
diff --git a/releasenotes/notes/add-power-on-off-a77673d482568a8b.yaml b/releasenotes/notes/add-power-on-off-a77673d482568a8b.yaml
new file mode 100644
index 000000000..0a3f1724e
--- /dev/null
+++ b/releasenotes/notes/add-power-on-off-a77673d482568a8b.yaml
@@ -0,0 +1,3 @@
+---
+features:
+ - Add action for compute node power on/off
diff --git a/setup.cfg b/setup.cfg
index 24ef85770..7213f850d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -77,6 +77,7 @@ watcher_actions =
sleep = watcher.applier.actions.sleep:Sleep
change_nova_service_state = watcher.applier.actions.change_nova_service_state:ChangeNovaServiceState
resize = watcher.applier.actions.resize:Resize
+ change_node_power_state = watcher.applier.actions.change_node_power_state:ChangeNodePowerState
watcher_workflow_engines =
taskflow = watcher.applier.workflow_engine.default:DefaultWorkFlowEngine
diff --git a/watcher/applier/actions/change_node_power_state.py b/watcher/applier/actions/change_node_power_state.py
new file mode 100644
index 000000000..431612b5f
--- /dev/null
+++ b/watcher/applier/actions/change_node_power_state.py
@@ -0,0 +1,113 @@
+# -*- encoding: utf-8 -*-
+# Copyright (c) 2017 ZTE
+#
+# Authors: Li Canwei
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import enum
+import six
+import voluptuous
+
+from watcher._i18n import _
+from watcher.applier.actions import base
+from watcher.common import exception
+
+
+class NodeState(enum.Enum):
+ POWERON = 'on'
+ POWEROFF = 'off'
+
+
+class ChangeNodePowerState(base.BaseAction):
+ """Compute node power on/off
+
+ By using this action, you will be able to on/off the power of a
+ compute node.
+
+ The action schema is::
+
+ schema = Schema({
+ 'resource_id': str,
+ 'state': str,
+ })
+
+ The `resource_id` references a ironic node id (list of available
+ ironic node is returned by this command: ``ironic node-list``).
+ The `state` value should either be `on` or `off`.
+ """
+
+ STATE = 'state'
+
+ @property
+ def schema(self):
+ return voluptuous.Schema({
+ voluptuous.Required(self.RESOURCE_ID):
+ voluptuous.All(
+ voluptuous.Any(*six.string_types),
+ voluptuous.Length(min=1)),
+ voluptuous.Required(self.STATE):
+ voluptuous.Any(*[NodeState.POWERON.value,
+ NodeState.POWEROFF.value]),
+ })
+
+ @property
+ def node_uuid(self):
+ return self.resource_id
+
+ @property
+ def state(self):
+ return self.input_parameters.get(self.STATE)
+
+ def execute(self):
+ target_state = self.state
+ return self._node_manage_power(target_state)
+
+ def revert(self):
+ if self.state == NodeState.POWERON.value:
+ target_state = NodeState.POWEROFF.value
+ elif self.state == NodeState.POWEROFF.value:
+ target_state = NodeState.POWERON.value
+ return self._node_manage_power(target_state)
+
+ def _node_manage_power(self, state):
+ if state is None:
+ raise exception.IllegalArgumentException(
+ message=_("The target state is not defined"))
+
+ result = False
+ ironic_client = self.osc.ironic()
+ nova_client = self.osc.nova()
+ if state == NodeState.POWEROFF.value:
+ node_info = ironic_client.node.get(self.node_uuid).to_dict()
+ compute_node_id = node_info['extra']['compute_node_id']
+ compute_node = nova_client.hypervisors.get(compute_node_id)
+ compute_node = compute_node.to_dict()
+ if (compute_node['running_vms'] == 0):
+ result = ironic_client.node.set_power_state(
+ self.node_uuid, state)
+ else:
+ result = ironic_client.node.set_power_state(self.node_uuid, state)
+ return result
+
+ def pre_condition(self):
+ pass
+
+ def post_condition(self):
+ pass
+
+ def get_description(self):
+ """Description of the action"""
+ return ("Compute node power on/off through ironic.")
diff --git a/watcher/decision_engine/planner/weight.py b/watcher/decision_engine/planner/weight.py
old mode 100755
new mode 100644
index 3af287eed..24c707a6d
--- a/watcher/decision_engine/planner/weight.py
+++ b/watcher/decision_engine/planner/weight.py
@@ -47,12 +47,13 @@ class WeightPlanner(base.BasePlanner):
super(WeightPlanner, self).__init__(config)
action_weights = {
- 'turn_host_to_acpi_s3_state': 10,
- 'resize': 20,
- 'migrate': 30,
- 'sleep': 40,
- 'change_nova_service_state': 50,
'nop': 60,
+ 'change_nova_service_state': 50,
+ 'sleep': 40,
+ 'migrate': 30,
+ 'resize': 20,
+ 'turn_host_to_acpi_s3_state': 10,
+ 'change_node_power_state': 9,
}
parallelization = {
@@ -62,6 +63,7 @@ class WeightPlanner(base.BasePlanner):
'sleep': 1,
'change_nova_service_state': 1,
'nop': 1,
+ 'change_node_power_state': 2,
}
@classmethod
diff --git a/watcher/tests/applier/actions/test_change_node_power_state.py b/watcher/tests/applier/actions/test_change_node_power_state.py
new file mode 100644
index 000000000..bca0f21bb
--- /dev/null
+++ b/watcher/tests/applier/actions/test_change_node_power_state.py
@@ -0,0 +1,147 @@
+# Copyright (c) 2017 ZTE
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import unicode_literals
+
+import mock
+import voluptuous
+
+from watcher.applier.actions import base as baction
+from watcher.applier.actions import change_node_power_state
+from watcher.common import clients
+from watcher.tests import base
+
+COMPUTE_NODE = "compute-1"
+
+
+@mock.patch.object(clients.OpenStackClients, 'nova')
+@mock.patch.object(clients.OpenStackClients, 'ironic')
+class TestChangeNodePowerState(base.TestCase):
+
+ def setUp(self):
+ super(TestChangeNodePowerState, self).setUp()
+
+ self.input_parameters = {
+ baction.BaseAction.RESOURCE_ID: COMPUTE_NODE,
+ "state": change_node_power_state.NodeState.POWERON.value,
+ }
+ self.action = change_node_power_state.ChangeNodePowerState(
+ mock.Mock())
+ self.action.input_parameters = self.input_parameters
+
+ def test_parameters_down(self, mock_ironic, mock_nova):
+ self.action.input_parameters = {
+ baction.BaseAction.RESOURCE_ID: COMPUTE_NODE,
+ self.action.STATE:
+ change_node_power_state.NodeState.POWEROFF.value}
+ self.assertTrue(self.action.validate_parameters())
+
+ def test_parameters_up(self, mock_ironic, mock_nova):
+ self.action.input_parameters = {
+ baction.BaseAction.RESOURCE_ID: COMPUTE_NODE,
+ self.action.STATE:
+ change_node_power_state.NodeState.POWERON.value}
+ self.assertTrue(self.action.validate_parameters())
+
+ def test_parameters_exception_wrong_state(self, mock_ironic, mock_nova):
+ self.action.input_parameters = {
+ baction.BaseAction.RESOURCE_ID: COMPUTE_NODE,
+ self.action.STATE: 'error'}
+ exc = self.assertRaises(
+ voluptuous.Invalid, self.action.validate_parameters)
+ self.assertEqual(
+ [(['state'], voluptuous.ScalarInvalid)],
+ [([str(p) for p in e.path], type(e)) for e in exc.errors])
+
+ def test_parameters_resource_id_empty(self, mock_ironic, mock_nova):
+ self.action.input_parameters = {
+ self.action.STATE:
+ change_node_power_state.NodeState.POWERON.value,
+ }
+ exc = self.assertRaises(
+ voluptuous.Invalid, self.action.validate_parameters)
+ self.assertEqual(
+ [(['resource_id'], voluptuous.RequiredFieldInvalid)],
+ [([str(p) for p in e.path], type(e)) for e in exc.errors])
+
+ def test_parameters_applies_add_extra(self, mock_ironic, mock_nova):
+ self.action.input_parameters = {"extra": "failed"}
+ exc = self.assertRaises(
+ voluptuous.Invalid, self.action.validate_parameters)
+ self.assertEqual(
+ sorted([(['resource_id'], voluptuous.RequiredFieldInvalid),
+ (['state'], voluptuous.RequiredFieldInvalid),
+ (['extra'], voluptuous.Invalid)],
+ key=lambda x: str(x[0])),
+ sorted([([str(p) for p in e.path], type(e)) for e in exc.errors],
+ key=lambda x: str(x[0])))
+
+ def test_change_service_state_pre_condition(self, mock_ironic, mock_nova):
+ try:
+ self.action.pre_condition()
+ except Exception as exc:
+ self.fail(exc)
+
+ def test_change_node_state_post_condition(self, mock_ironic, mock_nova):
+ try:
+ self.action.post_condition()
+ except Exception as exc:
+ self.fail(exc)
+
+ def test_execute_node_service_state_with_poweron_target(
+ self, mock_ironic, mock_nova):
+ mock_irclient = mock_ironic.return_value
+ self.action.execute()
+
+ mock_irclient.node.set_power_state.assert_called_once_with(
+ COMPUTE_NODE, change_node_power_state.NodeState.POWERON.value)
+
+ def test_execute_change_node_state_with_poweroff_target(
+ self, mock_ironic, mock_nova):
+ mock_irclient = mock_ironic.return_value
+ mock_nvclient = mock_nova.return_value
+ mock_get = mock.MagicMock()
+ mock_get.to_dict.return_value = {'running_vms': 0}
+ mock_nvclient.hypervisors.get.return_value = mock_get
+ self.action.input_parameters["state"] = (
+ change_node_power_state.NodeState.POWEROFF.value)
+ self.action.execute()
+
+ mock_irclient.node.set_power_state.assert_called_once_with(
+ COMPUTE_NODE, change_node_power_state.NodeState.POWEROFF.value)
+
+ def test_revert_change_node_state_with_poweron_target(
+ self, mock_ironic, mock_nova):
+ mock_irclient = mock_ironic.return_value
+ mock_nvclient = mock_nova.return_value
+ mock_get = mock.MagicMock()
+ mock_get.to_dict.return_value = {'running_vms': 0}
+ mock_nvclient.hypervisors.get.return_value = mock_get
+ self.action.input_parameters["state"] = (
+ change_node_power_state.NodeState.POWERON.value)
+ self.action.revert()
+
+ mock_irclient.node.set_power_state.assert_called_once_with(
+ COMPUTE_NODE, change_node_power_state.NodeState.POWEROFF.value)
+
+ def test_revert_change_node_state_with_poweroff_target(
+ self, mock_ironic, mock_nova):
+ mock_irclient = mock_ironic.return_value
+ self.action.input_parameters["state"] = (
+ change_node_power_state.NodeState.POWEROFF.value)
+ self.action.revert()
+
+ mock_irclient.node.set_power_state.assert_called_once_with(
+ COMPUTE_NODE, change_node_power_state.NodeState.POWERON.value)