blob: 842e990b9295e7ecb990801f7df630a83fb1eb87 [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 Garciaaded5832020-09-16 13:31:33 +020022from juju.status import derive_status
23from juju.application import Application
David Garcia677f4442020-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 Garciaaded5832020-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:
38 entity_type = entity.entity_type
39 if entity_type == "machine":
40 return entity.agent_status in ["started"]
41 elif entity_type == "action":
42 return entity.status in ["completed", "failed", "cancelled"]
43 elif entity_type == "application":
44 # Workaround for bug: https://github.com/juju/python-libjuju/issues/441
45 return status(entity) in ["active", "blocked"]
46 else:
47 raise EntityInvalidException("Unknown entity type: {}".format(entity_type))
48
49
David Garcia4fee80e2020-05-13 12:18:38 +020050class JujuModelWatcher:
51 @staticmethod
52 async def wait_for(
53 model,
54 entity: ModelEntity,
55 progress_timeout: float = 3600,
56 total_timeout: float = 3600,
57 db_dict: dict = None,
58 n2vc: N2VCConnector = None,
59 ):
60 """
61 Wait for entity to reach its final state.
62
63 :param: model: Model to observe
64 :param: entity: Entity object
65 :param: progress_timeout: Maximum time between two updates in the model
66 :param: total_timeout: Timeout for the entity to be active
67 :param: db_dict: Dictionary with data of the DB to write the updates
68 :param: n2vc: N2VC Connector objector
69
70 :raises: asyncio.TimeoutError when timeout reaches
71 """
72
73 if progress_timeout is None:
74 progress_timeout = 3600.0
75 if total_timeout is None:
76 total_timeout = 3600.0
77
David Garciaaded5832020-09-16 13:31:33 +020078 entity_type = entity.entity_type
79 if entity_type not in ["application", "action", "machine"]:
80 raise EntityInvalidException("Unknown entity type: {}".format(entity_type))
David Garcia4fee80e2020-05-13 12:18:38 +020081
82 # Coroutine to wait until the entity reaches the final state
83 wait_for_entity = asyncio.ensure_future(
84 asyncio.wait_for(
David Garciaaded5832020-09-16 13:31:33 +020085 model.block_until(lambda: entity_ready(entity)), timeout=total_timeout,
David Garcia4fee80e2020-05-13 12:18:38 +020086 )
87 )
88
89 # Coroutine to watch the model for changes (and write them to DB)
90 watcher = asyncio.ensure_future(
91 JujuModelWatcher.model_watcher(
92 model,
93 entity_id=entity.entity_id,
94 entity_type=entity_type,
95 timeout=progress_timeout,
96 db_dict=db_dict,
97 n2vc=n2vc,
98 )
99 )
100
101 tasks = [wait_for_entity, watcher]
102 try:
103 # Execute tasks, and stop when the first is finished
104 # The watcher task won't never finish (unless it timeouts)
105 await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
106 except Exception as e:
107 raise e
108 finally:
109 # Cancel tasks
110 for task in tasks:
111 task.cancel()
112
113 @staticmethod
114 async def model_watcher(
115 model: Model,
116 entity_id: str,
David Garciaaded5832020-09-16 13:31:33 +0200117 entity_type: str,
David Garcia4fee80e2020-05-13 12:18:38 +0200118 timeout: float,
119 db_dict: dict = None,
120 n2vc: N2VCConnector = None,
121 ):
122 """
123 Observes the changes related to an specific entity in a model
124
125 :param: model: Model to observe
126 :param: entity_id: ID of the entity to be observed
David Garciaaded5832020-09-16 13:31:33 +0200127 :param: entity_type: Entity Type (p.e. "application", "machine, and "action")
David Garcia4fee80e2020-05-13 12:18:38 +0200128 :param: timeout: Maximum time between two updates in the model
129 :param: db_dict: Dictionary with data of the DB to write the updates
130 :param: n2vc: N2VC Connector objector
131
132 :raises: asyncio.TimeoutError when timeout reaches
133 """
134
135 allwatcher = client.AllWatcherFacade.from_connection(model.connection())
136
137 # Genenerate array with entity types to listen
138 entity_types = (
David Garciaaded5832020-09-16 13:31:33 +0200139 [entity_type, "unit"]
140 if entity_type == "application" # TODO: Add "action" too
David Garcia4fee80e2020-05-13 12:18:38 +0200141 else [entity_type]
142 )
143
144 # Get time when it should timeout
145 timeout_end = time.time() + timeout
146
David Garcia677f4442020-06-19 11:40:18 +0200147 try:
148 while True:
149 change = await allwatcher.Next()
150 for delta in change.deltas:
151 write = False
152 delta_entity = None
David Garcia4fee80e2020-05-13 12:18:38 +0200153
David Garcia677f4442020-06-19 11:40:18 +0200154 # Get delta EntityType
David Garciaaded5832020-09-16 13:31:33 +0200155 delta_entity = delta.entity
David Garcia4fee80e2020-05-13 12:18:38 +0200156
David Garcia677f4442020-06-19 11:40:18 +0200157 if delta_entity in entity_types:
158 # Get entity id
David Garciaaded5832020-09-16 13:31:33 +0200159 if entity_type == "application":
David Garcia677f4442020-06-19 11:40:18 +0200160 id = (
161 delta.data["application"]
David Garciaaded5832020-09-16 13:31:33 +0200162 if delta_entity == "unit"
David Garcia677f4442020-06-19 11:40:18 +0200163 else delta.data["name"]
164 )
165 else:
166 id = delta.data["id"]
167
168 # Write if the entity id match
169 write = True if id == entity_id else False
170
171 # Update timeout
172 timeout_end = time.time() + timeout
David Garciaaded5832020-09-16 13:31:33 +0200173 (
174 status,
175 status_message,
176 vca_status,
177 ) = JujuModelWatcher.get_status(delta)
David Garcia4fee80e2020-05-13 12:18:38 +0200178
David Garcia677f4442020-06-19 11:40:18 +0200179 if write and n2vc is not None and db_dict:
180 # Write status to DB
181 status = n2vc.osm_status(delta_entity, status)
182 await n2vc.write_app_status_to_db(
183 db_dict=db_dict,
184 status=status,
185 detailed_status=status_message,
186 vca_status=vca_status,
David Garciaaded5832020-09-16 13:31:33 +0200187 entity_type=delta_entity,
David Garcia677f4442020-06-19 11:40:18 +0200188 )
189 # Check if timeout
190 if time.time() > timeout_end:
191 raise asyncio.TimeoutError()
192 except ConnectionClosed:
193 pass
194 # This is expected to happen when the
195 # entity reaches its final state, because
196 # the model connection is closed afterwards
David Garcia4fee80e2020-05-13 12:18:38 +0200197
198 @staticmethod
David Garciaaded5832020-09-16 13:31:33 +0200199 def get_status(delta: Delta) -> (str, str, str):
David Garcia4fee80e2020-05-13 12:18:38 +0200200 """
201 Get status from delta
202
203 :param: delta: Delta generated by the allwatcher
David Garciaaded5832020-09-16 13:31:33 +0200204 :param: entity_type: Entity Type (p.e. "application", "machine, and "action")
David Garcia4fee80e2020-05-13 12:18:38 +0200205
206 :return (status, message, vca_status)
207 """
David Garciaaded5832020-09-16 13:31:33 +0200208 if delta.entity == "machine":
David Garcia4fee80e2020-05-13 12:18:38 +0200209 return (
210 delta.data["agent-status"]["current"],
211 delta.data["instance-status"]["message"],
212 delta.data["instance-status"]["current"],
213 )
David Garciaaded5832020-09-16 13:31:33 +0200214 elif delta.entity == "action":
David Garcia4fee80e2020-05-13 12:18:38 +0200215 return (
216 delta.data["status"],
217 delta.data["status"],
218 delta.data["status"],
219 )
David Garciaaded5832020-09-16 13:31:33 +0200220 elif delta.entity == "application":
David Garcia4fee80e2020-05-13 12:18:38 +0200221 return (
222 delta.data["status"]["current"],
223 delta.data["status"]["message"],
224 delta.data["status"]["current"],
225 )
David Garciaaded5832020-09-16 13:31:33 +0200226 elif delta.entity == "unit":
David Garcia4fee80e2020-05-13 12:18:38 +0200227 return (
228 delta.data["workload-status"]["current"],
229 delta.data["workload-status"]["message"],
230 delta.data["workload-status"]["current"],
231 )