blob: e206e060da1b6d3496d4f56415810c6c4cd150c4 [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
17from juju.client import client
David Garcia4fee80e2020-05-13 12:18:38 +020018from n2vc.exceptions import EntityInvalidException
19from n2vc.n2vc_conn import N2VCConnector
20from juju.model import ModelEntity, Model
21from juju.client.overrides import Delta
David Garciac38a6962020-09-16 13:31:33 +020022from juju.status import derive_status
23from juju.application import Application
David Garcia2f66c4d2020-06-19 11:40:18 +020024from websockets.exceptions import ConnectionClosed
David Garcia4fee80e2020-05-13 12:18:38 +020025import logging
26
27logger = logging.getLogger("__main__")
28
29
David Garciac38a6962020-09-16 13:31:33 +020030def status(application: Application) -> str:
31 unit_status = []
32 for unit in application.units:
33 unit_status.append(unit.workload_status)
34 return derive_status(unit_status)
35
36
37def entity_ready(entity: ModelEntity) -> bool:
David Garciac3441172021-03-10 11:50:38 +010038 """
39 Check if the entity is ready
40
41 :param: entity: Model entity. It can be a machine, action, or application.
42
43 :returns: boolean saying if the entity is ready or not
44 """
David Garciac38a6962020-09-16 13:31:33 +020045 entity_type = entity.entity_type
46 if entity_type == "machine":
47 return entity.agent_status in ["started"]
48 elif entity_type == "action":
49 return entity.status in ["completed", "failed", "cancelled"]
50 elif entity_type == "application":
51 # Workaround for bug: https://github.com/juju/python-libjuju/issues/441
David Garciac3441172021-03-10 11:50:38 +010052 return entity.status in ["active", "blocked"]
David Garciac38a6962020-09-16 13:31:33 +020053 else:
54 raise EntityInvalidException("Unknown entity type: {}".format(entity_type))
55
56
David Garciac3441172021-03-10 11:50:38 +010057def application_ready(application: Application) -> bool:
58 """
59 Check if an application has a leader
60
61 :param: application: Application entity.
62
63 :returns: boolean saying if the application has a unit that is a leader.
64 """
65 ready_status_list = ["active", "blocked"]
66 application_ready = application.status in ready_status_list
67 units_ready = all(
68 unit.workload_status in ready_status_list for unit in application.units
69 )
70 return application_ready and units_ready
71
72
David Garcia4fee80e2020-05-13 12:18:38 +020073class JujuModelWatcher:
74 @staticmethod
David Garciaeb8943a2021-04-12 12:07:37 +020075 async def wait_for_model(
76 model: Model,
77 timeout: float = 3600
78 ):
David Garcia667696e2020-09-22 14:52:32 +020079 """
80 Wait for all entities in model to reach its final state.
81
82 :param: model: Model to observe
83 :param: timeout: Timeout for the model applications to be active
84
85 :raises: asyncio.TimeoutError when timeout reaches
86 """
87
88 if timeout is None:
89 timeout = 3600.0
90
91 # Coroutine to wait until the entity reaches the final state
David Garciac3441172021-03-10 11:50:38 +010092 async def wait_until_model_ready():
93 wait_for_entity = asyncio.ensure_future(
94 asyncio.wait_for(
95 model.block_until(
96 lambda: all(
97 application_ready(application)
98 for application in model.applications.values()
99 ),
100 ),
101 timeout=timeout,
102 )
David Garcia667696e2020-09-22 14:52:32 +0200103 )
David Garcia667696e2020-09-22 14:52:32 +0200104
David Garciac3441172021-03-10 11:50:38 +0100105 tasks = [wait_for_entity]
106 try:
107 await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
108 finally:
109 # Cancel tasks
110 for task in tasks:
111 task.cancel()
112
113 await wait_until_model_ready()
114 # Check model is still ready after 10 seconds
115
116 await asyncio.sleep(10)
117 await wait_until_model_ready()
David Garcia667696e2020-09-22 14:52:32 +0200118
119 @staticmethod
David Garcia4fee80e2020-05-13 12:18:38 +0200120 async def wait_for(
David Garcia667696e2020-09-22 14:52:32 +0200121 model: Model,
David Garcia4fee80e2020-05-13 12:18:38 +0200122 entity: ModelEntity,
123 progress_timeout: float = 3600,
124 total_timeout: float = 3600,
125 db_dict: dict = None,
126 n2vc: N2VCConnector = None,
David Garciaeb8943a2021-04-12 12:07:37 +0200127 vca_id: str = None,
David Garcia4fee80e2020-05-13 12:18:38 +0200128 ):
129 """
130 Wait for entity to reach its final state.
131
132 :param: model: Model to observe
133 :param: entity: Entity object
134 :param: progress_timeout: Maximum time between two updates in the model
135 :param: total_timeout: Timeout for the entity to be active
136 :param: db_dict: Dictionary with data of the DB to write the updates
137 :param: n2vc: N2VC Connector objector
David Garciaeb8943a2021-04-12 12:07:37 +0200138 :param: vca_id: VCA ID
David Garcia4fee80e2020-05-13 12:18:38 +0200139
140 :raises: asyncio.TimeoutError when timeout reaches
141 """
142
143 if progress_timeout is None:
144 progress_timeout = 3600.0
145 if total_timeout is None:
146 total_timeout = 3600.0
147
David Garciac38a6962020-09-16 13:31:33 +0200148 entity_type = entity.entity_type
149 if entity_type not in ["application", "action", "machine"]:
150 raise EntityInvalidException("Unknown entity type: {}".format(entity_type))
David Garcia4fee80e2020-05-13 12:18:38 +0200151
152 # Coroutine to wait until the entity reaches the final state
153 wait_for_entity = asyncio.ensure_future(
154 asyncio.wait_for(
David Garciac3441172021-03-10 11:50:38 +0100155 model.block_until(lambda: entity_ready(entity)),
156 timeout=total_timeout,
David Garcia4fee80e2020-05-13 12:18:38 +0200157 )
158 )
159
160 # Coroutine to watch the model for changes (and write them to DB)
161 watcher = asyncio.ensure_future(
162 JujuModelWatcher.model_watcher(
163 model,
164 entity_id=entity.entity_id,
165 entity_type=entity_type,
166 timeout=progress_timeout,
167 db_dict=db_dict,
168 n2vc=n2vc,
David Garciaeb8943a2021-04-12 12:07:37 +0200169 vca_id=vca_id,
David Garcia4fee80e2020-05-13 12:18:38 +0200170 )
171 )
172
173 tasks = [wait_for_entity, watcher]
174 try:
175 # Execute tasks, and stop when the first is finished
176 # The watcher task won't never finish (unless it timeouts)
177 await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
David Garcia4fee80e2020-05-13 12:18:38 +0200178 finally:
179 # Cancel tasks
180 for task in tasks:
181 task.cancel()
182
183 @staticmethod
184 async def model_watcher(
185 model: Model,
186 entity_id: str,
David Garciac38a6962020-09-16 13:31:33 +0200187 entity_type: str,
David Garcia4fee80e2020-05-13 12:18:38 +0200188 timeout: float,
189 db_dict: dict = None,
190 n2vc: N2VCConnector = None,
David Garciaeb8943a2021-04-12 12:07:37 +0200191 vca_id: str = None,
David Garcia4fee80e2020-05-13 12:18:38 +0200192 ):
193 """
194 Observes the changes related to an specific entity in a model
195
196 :param: model: Model to observe
197 :param: entity_id: ID of the entity to be observed
David Garciac38a6962020-09-16 13:31:33 +0200198 :param: entity_type: Entity Type (p.e. "application", "machine, and "action")
David Garcia4fee80e2020-05-13 12:18:38 +0200199 :param: timeout: Maximum time between two updates in the model
200 :param: db_dict: Dictionary with data of the DB to write the updates
201 :param: n2vc: N2VC Connector objector
David Garciaeb8943a2021-04-12 12:07:37 +0200202 :param: vca_id: VCA ID
David Garcia4fee80e2020-05-13 12:18:38 +0200203
204 :raises: asyncio.TimeoutError when timeout reaches
205 """
206
207 allwatcher = client.AllWatcherFacade.from_connection(model.connection())
208
209 # Genenerate array with entity types to listen
210 entity_types = (
David Garciac38a6962020-09-16 13:31:33 +0200211 [entity_type, "unit"]
212 if entity_type == "application" # TODO: Add "action" too
David Garcia4fee80e2020-05-13 12:18:38 +0200213 else [entity_type]
214 )
215
216 # Get time when it should timeout
217 timeout_end = time.time() + timeout
218
David Garcia2f66c4d2020-06-19 11:40:18 +0200219 try:
220 while True:
221 change = await allwatcher.Next()
222 for delta in change.deltas:
223 write = False
224 delta_entity = None
David Garcia4fee80e2020-05-13 12:18:38 +0200225
David Garcia2f66c4d2020-06-19 11:40:18 +0200226 # Get delta EntityType
David Garciac38a6962020-09-16 13:31:33 +0200227 delta_entity = delta.entity
David Garcia4fee80e2020-05-13 12:18:38 +0200228
David Garcia2f66c4d2020-06-19 11:40:18 +0200229 if delta_entity in entity_types:
230 # Get entity id
David Garciac38a6962020-09-16 13:31:33 +0200231 if entity_type == "application":
David Garcia2f66c4d2020-06-19 11:40:18 +0200232 id = (
233 delta.data["application"]
David Garciac38a6962020-09-16 13:31:33 +0200234 if delta_entity == "unit"
David Garcia2f66c4d2020-06-19 11:40:18 +0200235 else delta.data["name"]
236 )
237 else:
238 id = delta.data["id"]
239
240 # Write if the entity id match
241 write = True if id == entity_id else False
242
243 # Update timeout
244 timeout_end = time.time() + timeout
David Garciac38a6962020-09-16 13:31:33 +0200245 (
246 status,
247 status_message,
248 vca_status,
249 ) = JujuModelWatcher.get_status(delta)
David Garcia4fee80e2020-05-13 12:18:38 +0200250
David Garcia2f66c4d2020-06-19 11:40:18 +0200251 if write and n2vc is not None and db_dict:
252 # Write status to DB
253 status = n2vc.osm_status(delta_entity, status)
254 await n2vc.write_app_status_to_db(
255 db_dict=db_dict,
256 status=status,
257 detailed_status=status_message,
258 vca_status=vca_status,
David Garciac38a6962020-09-16 13:31:33 +0200259 entity_type=delta_entity,
David Garciaeb8943a2021-04-12 12:07:37 +0200260 vca_id=vca_id,
David Garcia2f66c4d2020-06-19 11:40:18 +0200261 )
262 # Check if timeout
263 if time.time() > timeout_end:
264 raise asyncio.TimeoutError()
265 except ConnectionClosed:
266 pass
267 # This is expected to happen when the
268 # entity reaches its final state, because
269 # the model connection is closed afterwards
David Garcia4fee80e2020-05-13 12:18:38 +0200270
271 @staticmethod
David Garciac38a6962020-09-16 13:31:33 +0200272 def get_status(delta: Delta) -> (str, str, str):
David Garcia4fee80e2020-05-13 12:18:38 +0200273 """
274 Get status from delta
275
276 :param: delta: Delta generated by the allwatcher
David Garciac38a6962020-09-16 13:31:33 +0200277 :param: entity_type: Entity Type (p.e. "application", "machine, and "action")
David Garcia4fee80e2020-05-13 12:18:38 +0200278
279 :return (status, message, vca_status)
280 """
David Garciac38a6962020-09-16 13:31:33 +0200281 if delta.entity == "machine":
David Garcia4fee80e2020-05-13 12:18:38 +0200282 return (
283 delta.data["agent-status"]["current"],
284 delta.data["instance-status"]["message"],
285 delta.data["instance-status"]["current"],
286 )
David Garciac38a6962020-09-16 13:31:33 +0200287 elif delta.entity == "action":
David Garcia4fee80e2020-05-13 12:18:38 +0200288 return (
289 delta.data["status"],
290 delta.data["status"],
291 delta.data["status"],
292 )
David Garciac38a6962020-09-16 13:31:33 +0200293 elif delta.entity == "application":
David Garcia4fee80e2020-05-13 12:18:38 +0200294 return (
295 delta.data["status"]["current"],
296 delta.data["status"]["message"],
297 delta.data["status"]["current"],
298 )
David Garciac38a6962020-09-16 13:31:33 +0200299 elif delta.entity == "unit":
David Garcia4fee80e2020-05-13 12:18:38 +0200300 return (
301 delta.data["workload-status"]["current"],
302 delta.data["workload-status"]["message"],
303 delta.data["workload-status"]["current"],
304 )