Coverage for watcher/decision_engine/strategy/strategies/saving_energy.py: 87%
90 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-17 12:22 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-17 12:22 +0000
1# -*- encoding: utf-8 -*-
2# Copyright (c) 2017 ZTE Corporation
3#
4# Authors: licanwei <li.canwei2@zte.com.cn>
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
15# implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
18#
20import random
22from oslo_log import log
24from watcher._i18n import _
25from watcher.common import exception
26from watcher.common.metal_helper import constants as metal_constants
27from watcher.common.metal_helper import factory as metal_helper_factory
28from watcher.decision_engine.strategy.strategies import base
30LOG = log.getLogger(__name__)
33class SavingEnergy(base.SavingEnergyBaseStrategy):
34 """Saving Energy Strategy
36 *Description*
38 Saving Energy Strategy together with VM Workload Consolidation Strategy
39 can perform the Dynamic Power Management (DPM) functionality, which tries
40 to save power by dynamically consolidating workloads even further during
41 periods of low resource utilization. Virtual machines are migrated onto
42 fewer hosts and the unneeded hosts are powered off.
44 After consolidation, Saving Energy Strategy produces a solution of powering
45 off/on according to the following detailed policy:
47 In this policy, a preset number(min_free_hosts_num) is given by user, and
48 this min_free_hosts_num describes minimum free compute nodes that users
49 expect to have, where "free compute nodes" refers to those nodes unused
50 but still powered on.
52 If the actual number of unused nodes(in power-on state) is larger than
53 the given number, randomly select the redundant nodes and power off them;
54 If the actual number of unused nodes(in poweron state) is smaller than
55 the given number and there are spare unused nodes(in poweroff state),
56 randomly select some nodes(unused,poweroff) and power on them.
58 *Requirements*
60 In this policy, in order to calculate the min_free_hosts_num,
61 users must provide two parameters:
63 * One parameter("min_free_hosts_num") is a constant int number.
64 This number should be int type and larger than zero.
66 * The other parameter("free_used_percent") is a percentage number, which
67 describes the quotient of min_free_hosts_num/nodes_with_VMs_num,
68 where nodes_with_VMs_num is the number of nodes with VMs running on it.
69 This parameter is used to calculate a dynamic min_free_hosts_num.
70 The nodes with VMs refer to those nodes with VMs running on it.
72 Then choose the larger one as the final min_free_hosts_num.
74 *Limitations*
76 * at least 2 physical compute hosts
78 *Spec URL*
80 http://specs.openstack.org/openstack/watcher-specs/specs/pike/implemented/energy-saving-strategy.html
81 """
83 def __init__(self, config, osc=None):
85 super(SavingEnergy, self).__init__(config, osc)
86 self._metal_helper = None
87 self._nova_client = None
89 self.with_vms_node_pool = []
90 self.free_poweron_node_pool = []
91 self.free_poweroff_node_pool = []
92 self.free_used_percent = 0
93 self.min_free_hosts_num = 1
95 @property
96 def metal_helper(self):
97 if not self._metal_helper: 97 ↛ 98line 97 didn't jump to line 98 because the condition on line 97 was never true
98 self._metal_helper = metal_helper_factory.get_helper(self.osc)
99 return self._metal_helper
101 @property
102 def nova_client(self):
103 if not self._nova_client:
104 self._nova_client = self.osc.nova()
105 return self._nova_client
107 @classmethod
108 def get_name(cls):
109 return "saving_energy"
111 @classmethod
112 def get_display_name(cls):
113 return _("Saving Energy Strategy")
115 @classmethod
116 def get_translatable_display_name(cls):
117 return "Saving Energy Strategy"
119 @classmethod
120 def get_schema(cls):
121 """return a schema of two input parameters
123 The standby nodes refer to those nodes unused
124 but still poweredon to deal with boom of new instances.
125 """
126 return {
127 "properties": {
128 "free_used_percent": {
129 "description": ("a rational number, which describes the"
130 " quotient of"
131 " min_free_hosts_num/nodes_with_VMs_num"
132 " where nodes_with_VMs_num is the number"
133 " of nodes with VMs"),
134 "type": "number",
135 "default": 10.0
136 },
137 "min_free_hosts_num": {
138 "description": ("minimum number of hosts without VMs"
139 " but still powered on"),
140 "type": "number",
141 "default": 1
142 },
143 },
144 }
146 def add_action_poweronoff_node(self, node, state):
147 """Add an action for node disability into the solution.
149 :param node: node
150 :param state: node power state, power on or power off
151 :return: None
152 """
153 params = {'state': state,
154 'resource_name': node.get_hypervisor_hostname()}
155 self.solution.add_action(
156 action_type='change_node_power_state',
157 resource_id=node.get_id(),
158 input_parameters=params)
160 def get_hosts_pool(self):
161 """Get three pools, with_vms_node_pool, free_poweron_node_pool,
163 free_poweroff_node_pool.
165 """
167 node_list = self.metal_helper.list_compute_nodes()
168 for node in node_list:
169 hypervisor_node = node.get_hypervisor_node().to_dict()
171 compute_service = hypervisor_node.get('service', None)
172 host_name = compute_service.get('host')
173 LOG.debug("Found hypervisor: %s", hypervisor_node)
174 try:
175 self.compute_model.get_node_by_name(host_name)
176 except exception.ComputeNodeNotFound:
177 LOG.info("The compute model does not contain the host: %s",
178 host_name)
179 continue
181 if (node.hv_up_when_powered_off and 181 ↛ 184line 181 didn't jump to line 184 because the condition on line 181 was never true
182 hypervisor_node.get('state') != 'up'):
183 # filter nodes that are not in 'up' state
184 LOG.info("Ignoring node that isn't in 'up' state: %s",
185 host_name)
186 continue
187 else:
188 if (hypervisor_node['running_vms'] == 0):
189 power_state = node.get_power_state()
190 if power_state == metal_constants.PowerState.ON:
191 self.free_poweron_node_pool.append(node)
192 elif power_state == metal_constants.PowerState.OFF: 192 ↛ 195line 192 didn't jump to line 195 because the condition on line 192 was always true
193 self.free_poweroff_node_pool.append(node)
194 else:
195 LOG.info("Ignoring node %s, unknown state: %s",
196 node, power_state)
197 else:
198 self.with_vms_node_pool.append(node)
200 def save_energy(self):
202 need_poweron = int(max(
203 (len(self.with_vms_node_pool) * self.free_used_percent / 100), (
204 self.min_free_hosts_num)))
205 len_poweron = len(self.free_poweron_node_pool)
206 len_poweroff = len(self.free_poweroff_node_pool)
207 LOG.debug("need_poweron: %s, len_poweron: %s, len_poweroff: %s",
208 need_poweron, len_poweron, len_poweroff)
209 if len_poweron > need_poweron:
210 for node in random.sample(self.free_poweron_node_pool,
211 (len_poweron - need_poweron)):
212 self.add_action_poweronoff_node(node,
213 metal_constants.PowerState.OFF)
214 LOG.info("power off %s", node.get_id())
215 elif len_poweron < need_poweron: 215 ↛ exitline 215 didn't return from function 'save_energy' because the condition on line 215 was always true
216 diff = need_poweron - len_poweron
217 for node in random.sample(self.free_poweroff_node_pool,
218 min(len_poweroff, diff)):
219 self.add_action_poweronoff_node(node,
220 metal_constants.PowerState.ON)
221 LOG.info("power on %s", node.get_id())
223 def pre_execute(self):
224 self._pre_execute()
225 self.free_used_percent = self.input_parameters.free_used_percent
226 self.min_free_hosts_num = self.input_parameters.min_free_hosts_num
228 def do_execute(self, audit=None):
229 """Strategy execution phase
231 This phase is where you should put the main logic of your strategy.
232 """
233 self.get_hosts_pool()
234 self.save_energy()
236 def post_execute(self):
237 """Post-execution phase
239 This can be used to compute the global efficacy
240 """
241 self.solution.model = self.compute_model
243 LOG.debug(self.compute_model.to_string())