Coverage for watcher/objects/audit.py: 98%

118 statements  

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

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

2# Copyright 2013 IBM Corp. 

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""" 

18In the Watcher system, an :ref:`Audit <audit_definition>` is a request for 

19optimizing a :ref:`Cluster <cluster_definition>`. 

20 

21The optimization is done in order to satisfy one :ref:`Goal <goal_definition>` 

22on a given :ref:`Cluster <cluster_definition>`. 

23 

24For each :ref:`Audit <audit_definition>`, the Watcher system generates an 

25:ref:`Action Plan <action_plan_definition>`. 

26 

27An :ref:`Audit <audit_definition>` has a life-cycle and its current state may 

28be one of the following: 

29 

30- **PENDING** : a request for an :ref:`Audit <audit_definition>` has been 

31 submitted (either manually by the 

32 :ref:`Administrator <administrator_definition>` or automatically via some 

33 event handling mechanism) and is in the queue for being processed by the 

34 :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` 

35- **ONGOING** : the :ref:`Audit <audit_definition>` is currently being 

36 processed by the 

37 :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` 

38- **SUCCEEDED** : the :ref:`Audit <audit_definition>` has been executed 

39 successfully (note that it may not necessarily produce a 

40 :ref:`Solution <solution_definition>`). 

41- **FAILED** : an error occurred while executing the 

42 :ref:`Audit <audit_definition>` 

43- **DELETED** : the :ref:`Audit <audit_definition>` is still stored in the 

44 :ref:`Watcher database <watcher_database_definition>` but is not returned 

45 any more through the Watcher APIs. 

46- **CANCELLED** : the :ref:`Audit <audit_definition>` was in **PENDING** or 

47 **ONGOING** state and was cancelled by the 

48 :ref:`Administrator <administrator_definition>` 

49- **SUSPENDED** : the :ref:`Audit <audit_definition>` was in **ONGOING** 

50 state and was suspended by the 

51 :ref:`Administrator <administrator_definition>` 

52""" 

53 

54import enum 

55 

56from watcher.common import exception 

57from watcher.common import utils 

58from watcher.db import api as db_api 

59from watcher import notifications 

60from watcher import objects 

61from watcher.objects import base 

62from watcher.objects import fields as wfields 

63 

64 

65class State(object): 

66 ONGOING = 'ONGOING' 

67 SUCCEEDED = 'SUCCEEDED' 

68 FAILED = 'FAILED' 

69 CANCELLED = 'CANCELLED' 

70 DELETED = 'DELETED' 

71 PENDING = 'PENDING' 

72 SUSPENDED = 'SUSPENDED' 

73 

74 

75class AuditType(enum.Enum): 

76 ONESHOT = 'ONESHOT' 

77 CONTINUOUS = 'CONTINUOUS' 

78 EVENT = 'EVENT' 

79 

80 

81@base.WatcherObjectRegistry.register 

82class Audit(base.WatcherPersistentObject, base.WatcherObject, 

83 base.WatcherObjectDictCompat): 

84 

85 # Version 1.0: Initial version 

86 # Version 1.1: Added 'goal' and 'strategy' object field 

87 # Version 1.2: Added 'auto_trigger' boolean field 

88 # Version 1.3: Added 'next_run_time' DateTime field, 

89 # 'interval' type has been changed from Integer to String 

90 # Version 1.4: Added 'name' string field 

91 # Version 1.5: Added 'hostname' field 

92 # Version 1.6: Added 'start_time' and 'end_time' DateTime fields 

93 # Version 1.7: Added 'force' boolean field 

94 VERSION = '1.7' 

95 

96 dbapi = db_api.get_instance() 

97 

98 fields = { 

99 'id': wfields.IntegerField(), 

100 'uuid': wfields.UUIDField(), 

101 'name': wfields.StringField(), 

102 'audit_type': wfields.StringField(), 

103 'state': wfields.StringField(), 

104 'parameters': wfields.FlexibleDictField(nullable=True), 

105 'interval': wfields.StringField(nullable=True), 

106 'scope': wfields.FlexibleListOfDictField(nullable=True), 

107 'goal_id': wfields.IntegerField(), 

108 'strategy_id': wfields.IntegerField(nullable=True), 

109 'auto_trigger': wfields.BooleanField(), 

110 'next_run_time': wfields.DateTimeField(nullable=True, 

111 tzinfo_aware=False), 

112 'hostname': wfields.StringField(nullable=True), 

113 'start_time': wfields.DateTimeField(nullable=True, tzinfo_aware=False), 

114 'end_time': wfields.DateTimeField(nullable=True, tzinfo_aware=False), 

115 'force': wfields.BooleanField(default=False, nullable=False), 

116 

117 'goal': wfields.ObjectField('Goal', nullable=True), 

118 'strategy': wfields.ObjectField('Strategy', nullable=True), 

119 } 

120 

121 object_fields = { 

122 'goal': (objects.Goal, 'goal_id'), 

123 'strategy': (objects.Strategy, 'strategy_id'), 

124 } 

125 

126 def __init__(self, *args, **kwargs): 

127 if 'force' not in kwargs: 

128 kwargs['force'] = False 

129 super(Audit, self).__init__(*args, **kwargs) 

130 

131 # Proxified field so we can keep the previous value after an update 

132 _state = None 

133 _old_state = None 

134 

135 # NOTE(v-francoise): The way oslo.versionedobjects works is by using a 

136 # __new__ that will automatically create the attributes referenced in 

137 # fields. These attributes are properties that raise an exception if no 

138 # value has been assigned, which means that they store the actual field 

139 # value in an "_obj_%(field)s" attribute. So because we want to proxify a 

140 # value that is already proxified, we have to do what you see below. 

141 @property 

142 def _obj_state(self): 

143 return self._state 

144 

145 @property 

146 def _obj_old_state(self): 

147 return self._old_state 

148 

149 @property 

150 def old_state(self): 

151 return self._old_state 

152 

153 @_obj_old_state.setter 

154 def _obj_old_state(self, value): 

155 self._old_state = value 

156 

157 @_obj_state.setter 

158 def _obj_state(self, value): 

159 if self._old_state is None and self._state is None: 

160 self._state = value 

161 else: 

162 self._old_state, self._state = self._state, value 

163 

164 @base.remotable_classmethod 

165 def get(cls, context, audit_id, eager=False): 

166 """Find a audit based on its id or uuid and return a Audit object. 

167 

168 :param context: Security context. NOTE: This should only 

169 be used internally by the indirection_api. 

170 Unfortunately, RPC requires context as the first 

171 argument, even though we don't use it. 

172 A context should be set when instantiating the 

173 object, e.g.: Audit(context) 

174 :param audit_id: the id *or* uuid of a audit. 

175 :param eager: Load object fields if True (Default: False) 

176 :returns: a :class:`Audit` object. 

177 """ 

178 if utils.is_int_like(audit_id): 

179 return cls.get_by_id(context, audit_id, eager=eager) 

180 elif utils.is_uuid_like(audit_id): 

181 return cls.get_by_uuid(context, audit_id, eager=eager) 

182 else: 

183 raise exception.InvalidIdentity(identity=audit_id) 

184 

185 @base.remotable_classmethod 

186 def get_by_id(cls, context, audit_id, eager=False): 

187 """Find a audit based on its integer id and return a Audit object. 

188 

189 :param context: Security context. NOTE: This should only 

190 be used internally by the indirection_api. 

191 Unfortunately, RPC requires context as the first 

192 argument, even though we don't use it. 

193 A context should be set when instantiating the 

194 object, e.g.: Audit(context) 

195 :param audit_id: the id of a audit. 

196 :param eager: Load object fields if True (Default: False) 

197 :returns: a :class:`Audit` object. 

198 """ 

199 db_audit = cls.dbapi.get_audit_by_id(context, audit_id, eager=eager) 

200 audit = cls._from_db_object(cls(context), db_audit, eager=eager) 

201 return audit 

202 

203 @base.remotable_classmethod 

204 def get_by_uuid(cls, context, uuid, eager=False): 

205 """Find a audit based on uuid and return a :class:`Audit` object. 

206 

207 :param context: Security context. NOTE: This should only 

208 be used internally by the indirection_api. 

209 Unfortunately, RPC requires context as the first 

210 argument, even though we don't use it. 

211 A context should be set when instantiating the 

212 object, e.g.: Audit(context) 

213 :param uuid: the uuid of a audit. 

214 :param eager: Load object fields if True (Default: False) 

215 :returns: a :class:`Audit` object. 

216 """ 

217 

218 db_audit = cls.dbapi.get_audit_by_uuid(context, uuid, eager=eager) 

219 audit = cls._from_db_object(cls(context), db_audit, eager=eager) 

220 return audit 

221 

222 @base.remotable_classmethod 

223 def get_by_name(cls, context, name, eager=False): 

224 """Find an audit based on name and return a :class:`Audit` object. 

225 

226 :param context: Security context. NOTE: This should only 

227 be used internally by the indirection_api. 

228 Unfortunately, RPC requires context as the first 

229 argument, even though we don't use it. 

230 A context should be set when instantiating the 

231 object, e.g.: Audit(context) 

232 :param name: the name of an audit. 

233 :param eager: Load object fields if True (Default: False) 

234 :returns: a :class:`Audit` object. 

235 """ 

236 

237 db_audit = cls.dbapi.get_audit_by_name(context, name, eager=eager) 

238 audit = cls._from_db_object(cls(context), db_audit, eager=eager) 

239 return audit 

240 

241 @base.remotable_classmethod 

242 def list(cls, context, limit=None, marker=None, filters=None, 

243 sort_key=None, sort_dir=None, eager=False): 

244 """Return a list of Audit objects. 

245 

246 :param context: Security context. NOTE: This should only 

247 be used internally by the indirection_api. 

248 Unfortunately, RPC requires context as the first 

249 argument, even though we don't use it. 

250 A context should be set when instantiating the 

251 object, e.g.: Audit(context) 

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

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

254 :param filters: Filters to apply. Defaults to None. 

255 :param sort_key: column to sort results by. 

256 :param sort_dir: direction to sort. "asc" or "desc". 

257 :param eager: Load object fields if True (Default: False) 

258 :returns: a list of :class:`Audit` object. 

259 

260 """ 

261 db_audits = cls.dbapi.get_audit_list(context, 

262 limit=limit, 

263 marker=marker, 

264 filters=filters, 

265 sort_key=sort_key, 

266 sort_dir=sort_dir, 

267 eager=eager) 

268 return [cls._from_db_object(cls(context), obj, eager=eager) 

269 for obj in db_audits] 

270 

271 @base.remotable 

272 def create(self): 

273 """Create an :class:`Audit` record in the DB. 

274 

275 :returns: An :class:`Audit` object. 

276 """ 

277 values = self.obj_get_changes() 

278 db_audit = self.dbapi.create_audit(values) 

279 # Note(v-francoise): Always load eagerly upon creation so we can send 

280 # notifications containing information about the related relationships 

281 self._from_db_object(self, db_audit, eager=True) 

282 

283 def _notify(): 

284 notifications.audit.send_create(self._context, self) 

285 

286 _notify() 

287 

288 @base.remotable 

289 def destroy(self): 

290 """Delete the Audit from the DB.""" 

291 self.dbapi.destroy_audit(self.uuid) 

292 self.obj_reset_changes() 

293 

294 @base.remotable 

295 def save(self): 

296 """Save updates to this Audit. 

297 

298 Updates will be made column by column based on the result 

299 of self.what_changed(). 

300 """ 

301 updates = self.obj_get_changes() 

302 db_obj = self.dbapi.update_audit(self.uuid, updates) 

303 obj = self._from_db_object( 

304 self.__class__(self._context), db_obj, eager=False) 

305 self.obj_refresh(obj) 

306 

307 def _notify(): 

308 notifications.audit.send_update( 

309 self._context, self, old_state=self.old_state) 

310 

311 _notify() 

312 

313 self.obj_reset_changes() 

314 

315 @base.remotable 

316 def refresh(self, eager=False): 

317 """Loads updates for this Audit. 

318 

319 Loads a audit with the same uuid from the database and 

320 checks for updated attributes. Updates are applied from 

321 the loaded audit column by column, if there are any updates. 

322 :param eager: Load object fields if True (Default: False) 

323 """ 

324 current = self.get_by_uuid(self._context, uuid=self.uuid, eager=eager) 

325 self.obj_refresh(current) 

326 

327 @base.remotable 

328 def soft_delete(self): 

329 """Soft Delete the Audit from the DB.""" 

330 self.state = State.DELETED 

331 self.save() 

332 db_obj = self.dbapi.soft_delete_audit(self.uuid) 

333 obj = self._from_db_object( 

334 self.__class__(self._context), db_obj, eager=False) 

335 self.obj_refresh(obj) 

336 

337 def _notify(): 

338 notifications.audit.send_delete(self._context, self) 

339 

340 _notify() 

341 

342 

343class AuditStateTransitionManager(object): 

344 

345 TRANSITIONS = { 

346 State.PENDING: [State.ONGOING, State.CANCELLED], 

347 State.ONGOING: [State.FAILED, State.SUCCEEDED, 

348 State.CANCELLED, State.SUSPENDED], 

349 State.FAILED: [State.DELETED], 

350 State.SUCCEEDED: [State.DELETED], 

351 State.CANCELLED: [State.DELETED], 

352 State.SUSPENDED: [State.ONGOING, State.DELETED], 

353 } 

354 

355 INACTIVE_STATES = (State.CANCELLED, State.DELETED, 

356 State.FAILED, State.SUSPENDED) 

357 

358 def check_transition(self, initial, new): 

359 return new in self.TRANSITIONS.get(initial, []) 

360 

361 def is_inactive(self, audit): 

362 return audit.state in self.INACTIVE_STATES