blob: 747f08ef8e633932ec89ee1b7f51b64486bac5e4 [file] [log] [blame]
David Garcia4fee80e2020-05-13 12:18:38 +02001# Copyright 2020 Canonical Ltd.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import asyncio
16import time
beierlmdb1d37b2022-04-14 16:33:51 -040017
David Garcia4fee80e2020-05-13 12:18:38 +020018from juju.client import client
David Garcia4fee80e2020-05-13 12:18:38 +020019from n2vc.exceptions import EntityInvalidException
20from n2vc.n2vc_conn import N2VCConnector
21from juju.model import ModelEntity, Model
22from juju.client.overrides import Delta
David Garciac38a6962020-09-16 13:31:33 +020023from juju.status import derive_status
24from juju.application import Application
David Garcia2f66c4d2020-06-19 11:40:18 +020025from websockets.exceptions import ConnectionClosed
David Garcia4fee80e2020-05-13 12:18:38 +020026import logging
27
28logger = logging.getLogger("__main__")
29
30
David Garciac38a6962020-09-16 13:31:33 +020031def status(application: Application) -> str:
32 unit_status = []
33 for unit in application.units:
34 unit_status.append(unit.workload_status)
35 return derive_status(unit_status)
36
37
38def entity_ready(entity: ModelEntity) -> bool:
David Garciac3441172021-03-10 11:50:38 +010039 """
40 Check if the entity is ready
41
42 :param: entity: Model entity. It can be a machine, action, or application.
43
44 :returns: boolean saying if the entity is ready or not
45 """
beierlmdb1d37b2022-04-14 16:33:51 -040046
David Garciac38a6962020-09-16 13:31:33 +020047 entity_type = entity.entity_type
48 if entity_type == "machine":
49 return entity.agent_status in ["started"]
50 elif entity_type == "action":
51 return entity.status in ["completed", "failed", "cancelled"]
52 elif entity_type == "application":
53 # Workaround for bug: https://github.com/juju/python-libjuju/issues/441
David Garciac3441172021-03-10 11:50:38 +010054 return entity.status in ["active", "blocked"]
beierlmdb1d37b2022-04-14 16:33:51 -040055 elif entity_type == "unit":
56 return entity.agent_status in ["idle"]
David Garciac38a6962020-09-16 13:31:33 +020057 else:
58 raise EntityInvalidException("Unknown entity type: {}".format(entity_type))
59
60
David Garciac3441172021-03-10 11:50:38 +010061def application_ready(application: Application) -> bool:
62 """
63 Check if an application has a leader
64
65 :param: application: Application entity.
66
67 :returns: boolean saying if the application has a unit that is a leader.
68 """
69 ready_status_list = ["active", "blocked"]
70 application_ready = application.status in ready_status_list
71 units_ready = all(
72 unit.workload_status in ready_status_list for unit in application.units
73 )
74 return application_ready and units_ready
75
76
David Garcia4fee80e2020-05-13 12:18:38 +020077class JujuModelWatcher:
78 @staticmethod
garciadeblas82b591c2021-03-24 09:22:13 +010079 async def wait_for_model(model: Model, timeout: float = 3600):
David Garcia667696e2020-09-22 14:52:32 +020080 """
81 Wait for all entities in model to reach its final state.
82
83 :param: model: Model to observe
84 :param: timeout: Timeout for the model applications to be active
85
86 :raises: asyncio.TimeoutError when timeout reaches
87 """
88
89 if timeout is None:
90 timeout = 3600.0
91
92 # Coroutine to wait until the entity reaches the final state
David Garciac3441172021-03-10 11:50:38 +010093 async def wait_until_model_ready():
94 wait_for_entity = asyncio.ensure_future(
95 asyncio.wait_for(
96 model.block_until(
97 lambda: all(
98 application_ready(application)
99 for application in model.applications.values()
100 ),
101 ),
102 timeout=timeout,
103 )
David Garcia667696e2020-09-22 14:52:32 +0200104 )
David Garcia667696e2020-09-22 14:52:32 +0200105
David Garciac3441172021-03-10 11:50:38 +0100106 tasks = [wait_for_entity]
107 try:
108 await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
109 finally:
110 # Cancel tasks
111 for task in tasks:
112 task.cancel()
113
114 await wait_until_model_ready()
115 # Check model is still ready after 10 seconds
116
117 await asyncio.sleep(10)
118 await wait_until_model_ready()
David Garcia667696e2020-09-22 14:52:32 +0200119
120 @staticmethod
David Garcia4fee80e2020-05-13 12:18:38 +0200121 async def wait_for(
David Garcia667696e2020-09-22 14:52:32 +0200122 model: Model,
David Garcia4fee80e2020-05-13 12:18:38 +0200123 entity: ModelEntity,
124 progress_timeout: float = 3600,
125 total_timeout: float = 3600,
126 db_dict: dict = None,
127 n2vc: N2VCConnector = None,
David Garciaeb8943a2021-04-12 12:07:37 +0200128 vca_id: str = None,
David Garcia4fee80e2020-05-13 12:18:38 +0200129 ):
130 """
131 Wait for entity to reach its final state.
132
133 :param: model: Model to observe
134 :param: entity: Entity object
135 :param: progress_timeout: Maximum time between two updates in the model
136 :param: total_timeout: Timeout for the entity to be active
137 :param: db_dict: Dictionary with data of the DB to write the updates
138 :param: n2vc: N2VC Connector objector
David Garciaeb8943a2021-04-12 12:07:37 +0200139 :param: vca_id: VCA ID
David Garcia4fee80e2020-05-13 12:18:38 +0200140
141 :raises: asyncio.TimeoutError when timeout reaches
142 """
143
144 if progress_timeout is None:
145 progress_timeout = 3600.0
146 if total_timeout is None:
147 total_timeout = 3600.0
148
David Garciac38a6962020-09-16 13:31:33 +0200149 entity_type = entity.entity_type
beierlmdb1d37b2022-04-14 16:33:51 -0400150 if entity_type not in ["application", "action", "machine", "unit"]:
David Garciac38a6962020-09-16 13:31:33 +0200151 raise EntityInvalidException("Unknown entity type: {}".format(entity_type))
David Garcia4fee80e2020-05-13 12:18:38 +0200152
153 # Coroutine to wait until the entity reaches the final state
154 wait_for_entity = asyncio.ensure_future(
155 asyncio.wait_for(
David Garciac3441172021-03-10 11:50:38 +0100156 model.block_until(lambda: entity_ready(entity)),
157 timeout=total_timeout,
David Garcia4fee80e2020-05-13 12:18:38 +0200158 )
159 )
160
161 # Coroutine to watch the model for changes (and write them to DB)
162 watcher = asyncio.ensure_future(
163 JujuModelWatcher.model_watcher(
164 model,
165 entity_id=entity.entity_id,
166 entity_type=entity_type,
167 timeout=progress_timeout,
168 db_dict=db_dict,
169 n2vc=n2vc,
David Garciaeb8943a2021-04-12 12:07:37 +0200170 vca_id=vca_id,
David Garcia4fee80e2020-05-13 12:18:38 +0200171 )
172 )
173
174 tasks = [wait_for_entity, watcher]
175 try:
176 # Execute tasks, and stop when the first is finished
177 # The watcher task won't never finish (unless it timeouts)
178 await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
David Garcia4fee80e2020-05-13 12:18:38 +0200179 finally:
180 # Cancel tasks
181 for task in tasks:
182 task.cancel()
183
184 @staticmethod
beierlmdb1d37b2022-04-14 16:33:51 -0400185 async def wait_for_units_idle(
186 model: Model, application: Application, timeout: float = 60
187 ):
188 """
189 Waits for the application and all its units to transition back to idle
190
191 :param: model: Model to observe
192 :param: application: The application to be observed
193 :param: timeout: Maximum time between two updates in the model
194
195 :raises: asyncio.TimeoutError when timeout reaches
196 """
197
198 ensure_units_idle = asyncio.ensure_future(
199 asyncio.wait_for(
200 JujuModelWatcher.ensure_units_idle(model, application), timeout
201 )
202 )
203 tasks = [
204 ensure_units_idle,
205 ]
206 (done, pending) = await asyncio.wait(
207 tasks, timeout=timeout, return_when=asyncio.FIRST_COMPLETED
208 )
209
210 if ensure_units_idle in pending:
211 ensure_units_idle.cancel()
212 raise TimeoutError(
213 "Application's units failed to return to idle after {} seconds".format(
214 timeout
215 )
216 )
217 if ensure_units_idle.result():
218 pass
219
220 @staticmethod
221 async def ensure_units_idle(model: Model, application: Application):
222 """
223 Waits forever until the application's units to transition back to idle
224
225 :param: model: Model to observe
226 :param: application: The application to be observed
227 """
228
229 try:
230 allwatcher = client.AllWatcherFacade.from_connection(model.connection())
231 unit_wanted_state = "executing"
232 final_state_reached = False
233
234 units = application.units
235 final_state_seen = {unit.entity_id: False for unit in units}
236 agent_state_seen = {unit.entity_id: False for unit in units}
237 workload_state = {unit.entity_id: False for unit in units}
238
239 try:
240 while not final_state_reached:
241 change = await allwatcher.Next()
242
243 # Keep checking to see if new units were added during the change
244 for unit in units:
245 if unit.entity_id not in final_state_seen:
246 final_state_seen[unit.entity_id] = False
247 agent_state_seen[unit.entity_id] = False
248 workload_state[unit.entity_id] = False
249
250 for delta in change.deltas:
251 await asyncio.sleep(0)
252 if delta.entity != units[0].entity_type:
253 continue
254
255 final_state_reached = True
256 for unit in units:
257 if delta.data["name"] == unit.entity_id:
258 status = delta.data["agent-status"]["current"]
259 workload_state[unit.entity_id] = delta.data[
260 "workload-status"
261 ]["current"]
262
263 if status == unit_wanted_state:
264 agent_state_seen[unit.entity_id] = True
265 final_state_seen[unit.entity_id] = False
266
267 if (
268 status == "idle"
269 and agent_state_seen[unit.entity_id]
270 ):
271 final_state_seen[unit.entity_id] = True
272
273 final_state_reached = (
274 final_state_reached
275 and final_state_seen[unit.entity_id]
276 and workload_state[unit.entity_id]
277 in [
278 "active",
279 "error",
280 ]
281 )
282
283 except ConnectionClosed:
284 pass
285 # This is expected to happen when the
286 # entity reaches its final state, because
287 # the model connection is closed afterwards
288 except Exception as e:
289 raise e
290
291 @staticmethod
David Garcia4fee80e2020-05-13 12:18:38 +0200292 async def model_watcher(
293 model: Model,
294 entity_id: str,
David Garciac38a6962020-09-16 13:31:33 +0200295 entity_type: str,
David Garcia4fee80e2020-05-13 12:18:38 +0200296 timeout: float,
297 db_dict: dict = None,
298 n2vc: N2VCConnector = None,
David Garciaeb8943a2021-04-12 12:07:37 +0200299 vca_id: str = None,
David Garcia4fee80e2020-05-13 12:18:38 +0200300 ):
301 """
302 Observes the changes related to an specific entity in a model
303
304 :param: model: Model to observe
305 :param: entity_id: ID of the entity to be observed
David Garciac38a6962020-09-16 13:31:33 +0200306 :param: entity_type: Entity Type (p.e. "application", "machine, and "action")
David Garcia4fee80e2020-05-13 12:18:38 +0200307 :param: timeout: Maximum time between two updates in the model
308 :param: db_dict: Dictionary with data of the DB to write the updates
309 :param: n2vc: N2VC Connector objector
David Garciaeb8943a2021-04-12 12:07:37 +0200310 :param: vca_id: VCA ID
David Garcia4fee80e2020-05-13 12:18:38 +0200311
312 :raises: asyncio.TimeoutError when timeout reaches
313 """
314
David Garcia2f66c4d2020-06-19 11:40:18 +0200315 try:
beierlmdb1d37b2022-04-14 16:33:51 -0400316 allwatcher = client.AllWatcherFacade.from_connection(model.connection())
David Garcia4fee80e2020-05-13 12:18:38 +0200317
beierlmdb1d37b2022-04-14 16:33:51 -0400318 # Genenerate array with entity types to listen
319 entity_types = (
320 [entity_type, "unit"]
321 if entity_type == "application" # TODO: Add "action" too
322 else [entity_type]
323 )
David Garcia4fee80e2020-05-13 12:18:38 +0200324
beierlmdb1d37b2022-04-14 16:33:51 -0400325 # Get time when it should timeout
326 timeout_end = time.time() + timeout
David Garcia2f66c4d2020-06-19 11:40:18 +0200327
beierlmdb1d37b2022-04-14 16:33:51 -0400328 try:
329 while True:
330 change = await allwatcher.Next()
331 for delta in change.deltas:
332 write = False
333 delta_entity = None
David Garcia2f66c4d2020-06-19 11:40:18 +0200334
beierlmdb1d37b2022-04-14 16:33:51 -0400335 # Get delta EntityType
336 delta_entity = delta.entity
David Garcia4fee80e2020-05-13 12:18:38 +0200337
beierlmdb1d37b2022-04-14 16:33:51 -0400338 if delta_entity in entity_types:
339 # Get entity id
340 id = None
341 if entity_type == "application":
342 id = (
343 delta.data["application"]
344 if delta_entity == "unit"
345 else delta.data["name"]
346 )
347 else:
348 if "id" in delta.data:
349 id = delta.data["id"]
350 else:
351 print("No id {}".format(delta.data))
352
353 # Write if the entity id match
354 write = True if id == entity_id else False
355
356 # Update timeout
357 timeout_end = time.time() + timeout
358 (
359 status,
360 status_message,
361 vca_status,
362 ) = JujuModelWatcher.get_status(delta)
363
364 if write and n2vc is not None and db_dict:
365 # Write status to DB
366 status = n2vc.osm_status(delta_entity, status)
367 await n2vc.write_app_status_to_db(
368 db_dict=db_dict,
369 status=status,
370 detailed_status=status_message,
371 vca_status=vca_status,
372 entity_type=delta_entity,
373 vca_id=vca_id,
374 )
375 # Check if timeout
376 if time.time() > timeout_end:
377 raise asyncio.TimeoutError()
378 except ConnectionClosed:
379 pass
380 # This is expected to happen when the
381 # entity reaches its final state, because
382 # the model connection is closed afterwards
383 except Exception as e:
384 raise e
David Garcia4fee80e2020-05-13 12:18:38 +0200385
386 @staticmethod
David Garciac38a6962020-09-16 13:31:33 +0200387 def get_status(delta: Delta) -> (str, str, str):
David Garcia4fee80e2020-05-13 12:18:38 +0200388 """
389 Get status from delta
390
391 :param: delta: Delta generated by the allwatcher
David Garciac38a6962020-09-16 13:31:33 +0200392 :param: entity_type: Entity Type (p.e. "application", "machine, and "action")
David Garcia4fee80e2020-05-13 12:18:38 +0200393
394 :return (status, message, vca_status)
395 """
David Garciac38a6962020-09-16 13:31:33 +0200396 if delta.entity == "machine":
David Garcia4fee80e2020-05-13 12:18:38 +0200397 return (
398 delta.data["agent-status"]["current"],
399 delta.data["instance-status"]["message"],
400 delta.data["instance-status"]["current"],
401 )
David Garciac38a6962020-09-16 13:31:33 +0200402 elif delta.entity == "action":
David Garcia4fee80e2020-05-13 12:18:38 +0200403 return (
404 delta.data["status"],
405 delta.data["status"],
406 delta.data["status"],
407 )
David Garciac38a6962020-09-16 13:31:33 +0200408 elif delta.entity == "application":
David Garcia4fee80e2020-05-13 12:18:38 +0200409 return (
410 delta.data["status"]["current"],
411 delta.data["status"]["message"],
412 delta.data["status"]["current"],
413 )
David Garciac38a6962020-09-16 13:31:33 +0200414 elif delta.entity == "unit":
David Garcia4fee80e2020-05-13 12:18:38 +0200415 return (
416 delta.data["workload-status"]["current"],
417 delta.data["workload-status"]["message"],
418 delta.data["workload-status"]["current"],
419 )