Coverage for watcher/api/controllers/v1/strategy.py: 87%

176 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-17 12:22 +0000

1# -*- encoding: utf-8 -*- 

2# Copyright (c) 2016 b<>com 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); 

5# you may not use this file except in compliance with the License. 

6# You may obtain a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, 

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 

13# implied. 

14# See the License for the specific language governing permissions and 

15# limitations under the License. 

16 

17""" 

18A :ref:`Strategy <strategy_definition>` is an algorithm implementation which is 

19able to find a :ref:`Solution <solution_definition>` for a given 

20:ref:`Goal <goal_definition>`. 

21 

22There may be several potential strategies which are able to achieve the same 

23:ref:`Goal <goal_definition>`. This is why it is possible to configure which 

24specific :ref:`Strategy <strategy_definition>` should be used for each goal. 

25 

26Some strategies may provide better optimization results but may take more time 

27to find an optimal :ref:`Solution <solution_definition>`. 

28""" 

29 

30import pecan 

31from pecan import rest 

32from wsme import types as wtypes 

33import wsmeext.pecan as wsme_pecan 

34 

35from watcher.api.controllers import base 

36from watcher.api.controllers import link 

37from watcher.api.controllers.v1 import collection 

38from watcher.api.controllers.v1 import types 

39from watcher.api.controllers.v1 import utils as api_utils 

40from watcher.common import exception 

41from watcher.common import policy 

42from watcher.common import utils as common_utils 

43from watcher.decision_engine import rpcapi 

44from watcher import objects 

45 

46 

47def hide_fields_in_newer_versions(obj): 

48 """This method hides fields that were added in newer API versions. 

49 

50 Certain node fields were introduced at certain API versions. 

51 These fields are only made available when the request's API version 

52 matches or exceeds the versions when these fields were introduced. 

53 """ 

54 pass 

55 

56 

57class Strategy(base.APIBase): 

58 """API representation of a strategy. 

59 

60 This class enforces type checking and value constraints, and converts 

61 between the internal object model and the API representation of a strategy. 

62 """ 

63 _goal_uuid = None 

64 _goal_name = None 

65 

66 def _get_goal(self, value): 

67 if value == wtypes.Unset: 67 ↛ 68line 67 didn't jump to line 68 because the condition on line 67 was never true

68 return None 

69 goal = None 

70 try: 

71 if (common_utils.is_uuid_like(value) or 71 ↛ 75line 71 didn't jump to line 75 because the condition on line 71 was always true

72 common_utils.is_int_like(value)): 

73 goal = objects.Goal.get(pecan.request.context, value) 

74 else: 

75 goal = objects.Goal.get_by_name(pecan.request.context, value) 

76 except exception.GoalNotFound: 

77 pass 

78 if goal: 78 ↛ 80line 78 didn't jump to line 80 because the condition on line 78 was always true

79 self.goal_id = goal.id 

80 return goal 

81 

82 def _get_goal_uuid(self): 

83 return self._goal_uuid 

84 

85 def _set_goal_uuid(self, value): 

86 if value and self._goal_uuid != value: 86 ↛ exitline 86 didn't return from function '_set_goal_uuid' because the condition on line 86 was always true

87 self._goal_uuid = None 

88 goal = self._get_goal(value) 

89 if goal: 89 ↛ exitline 89 didn't return from function '_set_goal_uuid' because the condition on line 89 was always true

90 self._goal_uuid = goal.uuid 

91 

92 def _get_goal_name(self): 

93 return self._goal_name 

94 

95 def _set_goal_name(self, value): 

96 if value and self._goal_name != value: 96 ↛ exitline 96 didn't return from function '_set_goal_name' because the condition on line 96 was always true

97 self._goal_name = None 

98 goal = self._get_goal(value) 

99 if goal: 99 ↛ exitline 99 didn't return from function '_set_goal_name' because the condition on line 99 was always true

100 self._goal_name = goal.name 

101 

102 uuid = types.uuid 

103 """Unique UUID for this strategy""" 

104 

105 name = wtypes.text 

106 """Name of the strategy""" 

107 

108 display_name = wtypes.text 

109 """Localized name of the strategy""" 

110 

111 links = wtypes.wsattr([link.Link], readonly=True) 

112 """A list containing a self link and associated goal links""" 

113 

114 goal_uuid = wtypes.wsproperty(wtypes.text, _get_goal_uuid, _set_goal_uuid, 

115 mandatory=True) 

116 """The UUID of the goal this audit refers to""" 

117 

118 goal_name = wtypes.wsproperty(wtypes.text, _get_goal_name, _set_goal_name, 

119 mandatory=False) 

120 """The name of the goal this audit refers to""" 

121 

122 parameters_spec = {wtypes.text: types.jsontype} 

123 """Parameters spec dict""" 

124 

125 def __init__(self, **kwargs): 

126 super(Strategy, self).__init__() 

127 

128 self.fields = [] 

129 self.fields.append('uuid') 

130 self.fields.append('name') 

131 self.fields.append('display_name') 

132 self.fields.append('goal_uuid') 

133 self.fields.append('goal_name') 

134 self.fields.append('parameters_spec') 

135 setattr(self, 'uuid', kwargs.get('uuid', wtypes.Unset)) 

136 setattr(self, 'name', kwargs.get('name', wtypes.Unset)) 

137 setattr(self, 'display_name', kwargs.get('display_name', wtypes.Unset)) 

138 setattr(self, 'goal_uuid', kwargs.get('goal_id', wtypes.Unset)) 

139 setattr(self, 'goal_name', kwargs.get('goal_id', wtypes.Unset)) 

140 setattr(self, 'parameters_spec', kwargs.get('parameters_spec', 

141 wtypes.Unset)) 

142 

143 @staticmethod 

144 def _convert_with_links(strategy, url, expand=True): 

145 if not expand: 

146 strategy.unset_fields_except( 

147 ['uuid', 'name', 'display_name', 'goal_uuid', 'goal_name']) 

148 

149 strategy.links = [ 

150 link.Link.make_link('self', url, 'strategies', strategy.uuid), 

151 link.Link.make_link('bookmark', url, 'strategies', strategy.uuid, 

152 bookmark=True)] 

153 return strategy 

154 

155 @classmethod 

156 def convert_with_links(cls, strategy, expand=True): 

157 strategy = Strategy(**strategy.as_dict()) 

158 hide_fields_in_newer_versions(strategy) 

159 return cls._convert_with_links( 

160 strategy, pecan.request.host_url, expand) 

161 

162 @classmethod 

163 def sample(cls, expand=True): 

164 sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c', 

165 name='DUMMY', 

166 display_name='Dummy strategy') 

167 return cls._convert_with_links(sample, 'http://localhost:9322', expand) 

168 

169 

170class StrategyCollection(collection.Collection): 

171 """API representation of a collection of strategies.""" 

172 

173 strategies = [Strategy] 

174 """A list containing strategies objects""" 

175 

176 def __init__(self, **kwargs): 

177 super(StrategyCollection, self).__init__() 

178 self._type = 'strategies' 

179 

180 @staticmethod 

181 def convert_with_links(strategies, limit, url=None, expand=False, 

182 **kwargs): 

183 strategy_collection = StrategyCollection() 

184 strategy_collection.strategies = [ 

185 Strategy.convert_with_links(g, expand) for g in strategies] 

186 strategy_collection.next = strategy_collection.get_next( 

187 limit, url=url, **kwargs) 

188 return strategy_collection 

189 

190 @classmethod 

191 def sample(cls): 

192 sample = cls() 

193 sample.strategies = [Strategy.sample(expand=False)] 

194 return sample 

195 

196 

197class StrategiesController(rest.RestController): 

198 """REST controller for Strategies.""" 

199 

200 def __init__(self): 

201 super(StrategiesController, self).__init__() 

202 

203 from_strategies = False 

204 """A flag to indicate if the requests to this controller are coming 

205 from the top-level resource Strategies.""" 

206 

207 _custom_actions = { 

208 'detail': ['GET'], 

209 'state': ['GET'], 

210 } 

211 

212 def _get_strategies_collection(self, filters, marker, limit, sort_key, 

213 sort_dir, expand=False, resource_url=None): 

214 additional_fields = ["goal_uuid", "goal_name"] 

215 

216 api_utils.validate_sort_key( 

217 sort_key, list(objects.Strategy.fields) + additional_fields) 

218 api_utils.validate_search_filters( 

219 filters, list(objects.Strategy.fields) + additional_fields) 

220 limit = api_utils.validate_limit(limit) 

221 api_utils.validate_sort_dir(sort_dir) 

222 

223 marker_obj = None 

224 if marker: 224 ↛ 225line 224 didn't jump to line 225 because the condition on line 224 was never true

225 marker_obj = objects.Strategy.get_by_uuid( 

226 pecan.request.context, marker) 

227 

228 need_api_sort = api_utils.check_need_api_sort(sort_key, 

229 additional_fields) 

230 sort_db_key = (sort_key if not need_api_sort 

231 else None) 

232 

233 strategies = objects.Strategy.list( 

234 pecan.request.context, limit, marker_obj, filters=filters, 

235 sort_key=sort_db_key, sort_dir=sort_dir) 

236 

237 strategies_collection = StrategyCollection.convert_with_links( 

238 strategies, limit, url=resource_url, expand=expand, 

239 sort_key=sort_key, sort_dir=sort_dir) 

240 

241 if need_api_sort: 

242 api_utils.make_api_sort(strategies_collection.strategies, 

243 sort_key, sort_dir) 

244 

245 return strategies_collection 

246 

247 @wsme_pecan.wsexpose(StrategyCollection, wtypes.text, wtypes.text, 

248 int, wtypes.text, wtypes.text) 

249 def get_all(self, goal=None, marker=None, limit=None, 

250 sort_key='id', sort_dir='asc'): 

251 """Retrieve a list of strategies. 

252 

253 :param goal: goal UUID or name to filter by. 

254 :param marker: pagination marker for large data sets. 

255 :param limit: maximum number of resources to return in a single result. 

256 :param sort_key: column to sort results by. Default: id. 

257 :param sort_dir: direction to sort. "asc" or "desc". Default: asc. 

258 """ 

259 context = pecan.request.context 

260 policy.enforce(context, 'strategy:get_all', 

261 action='strategy:get_all') 

262 filters = {} 

263 if goal: 

264 if common_utils.is_uuid_like(goal): 

265 filters['goal_uuid'] = goal 

266 else: 

267 filters['goal_name'] = goal 

268 

269 return self._get_strategies_collection( 

270 filters, marker, limit, sort_key, sort_dir) 

271 

272 @wsme_pecan.wsexpose(StrategyCollection, wtypes.text, wtypes.text, int, 

273 wtypes.text, wtypes.text) 

274 def detail(self, goal=None, marker=None, limit=None, 

275 sort_key='id', sort_dir='asc'): 

276 """Retrieve a list of strategies with detail. 

277 

278 :param goal: goal UUID or name to filter by. 

279 :param marker: pagination marker for large data sets. 

280 :param limit: maximum number of resources to return in a single result. 

281 :param sort_key: column to sort results by. Default: id. 

282 :param sort_dir: direction to sort. "asc" or "desc". Default: asc. 

283 """ 

284 context = pecan.request.context 

285 policy.enforce(context, 'strategy:detail', 

286 action='strategy:detail') 

287 # NOTE(lucasagomes): /detail should only work against collections 

288 parent = pecan.request.path.split('/')[:-1][-1] 

289 if parent != "strategies": 

290 raise exception.HTTPNotFound 

291 expand = True 

292 resource_url = '/'.join(['strategies', 'detail']) 

293 

294 filters = {} 

295 if goal: 295 ↛ 296line 295 didn't jump to line 296 because the condition on line 295 was never true

296 if common_utils.is_uuid_like(goal): 

297 filters['goal_uuid'] = goal 

298 else: 

299 filters['goal_name'] = goal 

300 

301 return self._get_strategies_collection( 

302 filters, marker, limit, sort_key, sort_dir, expand, resource_url) 

303 

304 @wsme_pecan.wsexpose(wtypes.text, wtypes.text) 

305 def state(self, strategy): 

306 """Retrieve an information about strategy requirements. 

307 

308 :param strategy: name of the strategy. 

309 """ 

310 context = pecan.request.context 

311 policy.enforce(context, 'strategy:state', action='strategy:state') 

312 parents = pecan.request.path.split('/')[:-1] 

313 if parents[-2] != "strategies": 313 ↛ 314line 313 didn't jump to line 314 because the condition on line 313 was never true

314 raise exception.HTTPNotFound 

315 rpc_strategy = api_utils.get_resource('Strategy', strategy) 

316 de_client = rpcapi.DecisionEngineAPI() 

317 strategy_state = de_client.get_strategy_info(context, 

318 rpc_strategy.name) 

319 strategy_state.extend([{ 

320 'type': 'Name', 'state': rpc_strategy.name, 

321 'mandatory': '', 'comment': ''}]) 

322 return strategy_state 

323 

324 @wsme_pecan.wsexpose(Strategy, wtypes.text) 

325 def get_one(self, strategy): 

326 """Retrieve information about the given strategy. 

327 

328 :param strategy: UUID or name of the strategy. 

329 """ 

330 if self.from_strategies: 330 ↛ 331line 330 didn't jump to line 331 because the condition on line 330 was never true

331 raise exception.OperationNotPermitted 

332 

333 context = pecan.request.context 

334 rpc_strategy = api_utils.get_resource('Strategy', strategy) 

335 policy.enforce(context, 'strategy:get', rpc_strategy, 

336 action='strategy:get') 

337 

338 return Strategy.convert_with_links(rpc_strategy)