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

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# 

19 

20import random 

21 

22from oslo_log import log 

23 

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 

29 

30LOG = log.getLogger(__name__) 

31 

32 

33class SavingEnergy(base.SavingEnergyBaseStrategy): 

34 """Saving Energy Strategy 

35 

36 *Description* 

37 

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. 

43 

44 After consolidation, Saving Energy Strategy produces a solution of powering 

45 off/on according to the following detailed policy: 

46 

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. 

51 

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. 

57 

58 *Requirements* 

59 

60 In this policy, in order to calculate the min_free_hosts_num, 

61 users must provide two parameters: 

62 

63 * One parameter("min_free_hosts_num") is a constant int number. 

64 This number should be int type and larger than zero. 

65 

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. 

71 

72 Then choose the larger one as the final min_free_hosts_num. 

73 

74 *Limitations* 

75 

76 * at least 2 physical compute hosts 

77 

78 *Spec URL* 

79 

80 http://specs.openstack.org/openstack/watcher-specs/specs/pike/implemented/energy-saving-strategy.html 

81 """ 

82 

83 def __init__(self, config, osc=None): 

84 

85 super(SavingEnergy, self).__init__(config, osc) 

86 self._metal_helper = None 

87 self._nova_client = None 

88 

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 

94 

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 

100 

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 

106 

107 @classmethod 

108 def get_name(cls): 

109 return "saving_energy" 

110 

111 @classmethod 

112 def get_display_name(cls): 

113 return _("Saving Energy Strategy") 

114 

115 @classmethod 

116 def get_translatable_display_name(cls): 

117 return "Saving Energy Strategy" 

118 

119 @classmethod 

120 def get_schema(cls): 

121 """return a schema of two input parameters 

122 

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 } 

145 

146 def add_action_poweronoff_node(self, node, state): 

147 """Add an action for node disability into the solution. 

148 

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) 

159 

160 def get_hosts_pool(self): 

161 """Get three pools, with_vms_node_pool, free_poweron_node_pool, 

162 

163 free_poweroff_node_pool. 

164 

165 """ 

166 

167 node_list = self.metal_helper.list_compute_nodes() 

168 for node in node_list: 

169 hypervisor_node = node.get_hypervisor_node().to_dict() 

170 

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 

180 

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) 

199 

200 def save_energy(self): 

201 

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()) 

222 

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 

227 

228 def do_execute(self, audit=None): 

229 """Strategy execution phase 

230 

231 This phase is where you should put the main logic of your strategy. 

232 """ 

233 self.get_hosts_pool() 

234 self.save_energy() 

235 

236 def post_execute(self): 

237 """Post-execution phase 

238 

239 This can be used to compute the global efficacy 

240 """ 

241 self.solution.model = self.compute_model 

242 

243 LOG.debug(self.compute_model.to_string())