github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/acceptancetests/jujupy/wait_condition.py (about) 1 # This file is part of JujuPy, a library for driving the Juju CLI. 2 # Copyright 2013-2017 Canonical Ltd. 3 # 4 # This program is free software: you can redistribute it and/or modify it 5 # under the terms of the Lesser GNU General Public License version 3, as 6 # published by the Free Software Foundation. 7 # 8 # This program is distributed in the hope that it will be useful, but WITHOUT 9 # ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, 10 # SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the Lesser 11 # GNU General Public License for more details. 12 # 13 # You should have received a copy of the Lesser GNU General Public License 14 # along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 import logging 17 from datetime import datetime 18 from time import sleep 19 from subprocess import CalledProcessError 20 21 from jujupy.exceptions import ( 22 VersionsNotUpdated, 23 AgentsNotStarted, 24 StatusNotMet, 25 LXDProfileNotAvailable, 26 ) 27 from jujupy.status import ( 28 Status, 29 ) 30 from jujupy.utility import ( 31 until_timeout, 32 ) 33 34 35 log = logging.getLogger(__name__) 36 37 __metaclass__ = type 38 39 40 class ModelCheckFailed(Exception): 41 """Exception used to signify a model status check failed or timed out.""" 42 43 44 def wait_for_model_check(client, model_check, timeout): 45 """Wrapper to have a client wait for a model_check callable to succeed. 46 47 :param client: ModelClient object to act on and pass into model_check 48 :param model_check: Callable that takes a ModelClient object. When the 49 callable reaches a success state it returns True. If model_check never 50 returns True within `timeout`, the exception ModelCheckFailed will be 51 raised. 52 """ 53 with client.check_timeouts(): 54 with client.ignore_soft_deadline(): 55 for _ in until_timeout(timeout): 56 if model_check(client): 57 return 58 sleep(1) 59 raise ModelCheckFailed() 60 61 62 def wait_until_model_upgrades(client, timeout=300): 63 # Poll using a command that will fail until the upgrade is complete. 64 def model_upgrade_status_check(client): 65 try: 66 log.info('Attempting API connection, failure is not fatal.') 67 client.juju('list-users', (), include_e=False) 68 return True 69 except CalledProcessError: 70 # Upgrade will still be in progress and thus refuse the api call. 71 return False 72 try: 73 wait_for_model_check(client, model_upgrade_status_check, timeout) 74 except ModelCheckFailed: 75 raise AssertionError( 76 'Upgrade for model {} failed to complete within the alloted ' 77 'timeout ({} seconds)'.format( 78 client.model_name, timeout)) 79 80 81 class BaseCondition: 82 """Base class for conditions that support client.wait_for.""" 83 84 def __init__(self, timeout=300, already_satisfied=False): 85 self.timeout = timeout 86 self.already_satisfied = already_satisfied 87 88 def iter_blocking_state(self, status): 89 """Identify when the condition required is met. 90 91 When the operation is complete yield nothing. Otherwise yields a 92 tuple ('<item detail>', '<state>') 93 as to why the action cannot be considered complete yet. 94 95 An example for a condition of an application being removed: 96 yield <application name>, 'still-present' 97 """ 98 raise NotImplementedError() 99 100 def do_raise(self, model_name, status): 101 """Raise exception for when success condition fails to be achieved.""" 102 raise NotImplementedError() 103 104 105 class ConditionList(BaseCondition): 106 """A list of conditions that support client.wait_for. 107 108 This combines the supplied list of conditions. It is only satisfied when 109 all conditions are met. It times out when any member times out. When 110 asked to raise, it causes the first condition to raise an exception. An 111 improvement would be to raise the first condition whose timeout has been 112 exceeded. 113 """ 114 115 def __init__(self, conditions): 116 if len(conditions) == 0: 117 timeout = 300 118 else: 119 timeout = max(c.timeout for c in conditions) 120 already_satisfied = all(c.already_satisfied for c in conditions) 121 super(ConditionList, self).__init__(timeout, already_satisfied) 122 self._conditions = conditions 123 124 def iter_blocking_state(self, status): 125 for condition in self._conditions: 126 for item, state in condition.iter_blocking_state(status): 127 yield item, state 128 129 def do_raise(self, model_name, status): 130 self._conditions[0].do_raise(model_name, status) 131 132 133 class NoopCondition(BaseCondition): 134 135 def iter_blocking_state(self, status): 136 return iter(()) 137 138 def do_raise(self, model_name, status): 139 raise Exception('NoopCondition failed: {}'.format(model_name)) 140 141 142 class AllApplicationActive(BaseCondition): 143 """Ensure all applications (incl. subordinates) are 'active' state.""" 144 145 def iter_blocking_state(self, status): 146 applications = status.get_applications() 147 all_app_status = [ 148 state['application-status']['current'] 149 for name, state in applications.items()] 150 apps_active = [state == 'active' for state in all_app_status] 151 if not all(apps_active): 152 yield 'applications', 'not-all-active' 153 154 def do_raise(self, model_name, status): 155 raise Exception('Timed out waiting for all applications to be active.') 156 157 158 class AllApplicationWorkloads(BaseCondition): 159 """Ensure all applications (incl. subordinates) are workload 'active'.""" 160 161 def iter_blocking_state(self, status): 162 app_workloads_active = [] 163 for name, unit in status.iter_units(): 164 try: 165 state = unit['workload-status']['current'] == 'active' 166 except KeyError: 167 state = False 168 app_workloads_active.append(state) 169 if not all(app_workloads_active): 170 yield 'application-workloads', 'not-all-active' 171 172 def do_raise(self, model_name, status): 173 raise Exception( 174 'Timed out waiting for all application workloads to be active.') 175 176 177 class AgentsIdle(BaseCondition): 178 """Ensure all specified agents are finished doing setup work.""" 179 180 def __init__(self, units, *args, **kws): 181 self.units = units 182 super(AgentsIdle, self).__init__(*args, **kws) 183 184 def iter_blocking_state(self, status): 185 idles = [] 186 for name in self.units: 187 try: 188 unit = status.get_unit(name) 189 state = unit['juju-status']['current'] == 'idle' 190 except KeyError: 191 state = False 192 idles.append(state) 193 if not all(idles): 194 yield 'application-agents', 'not-all-idle' 195 196 def do_raise(self, model_name, status): 197 raise Exception("Timed out waiting for all agents to be idle.") 198 199 200 class WaitMachineNotPresent(BaseCondition): 201 """Condition satisfied when a given machine is not present.""" 202 203 def __init__(self, machine, timeout=300): 204 super(WaitMachineNotPresent, self).__init__(timeout) 205 self.machine = machine 206 207 def __eq__(self, other): 208 if not type(self) is type(other): 209 return False 210 if self.timeout != other.timeout: 211 return False 212 if self.machine != other.machine: 213 return False 214 return True 215 216 def __ne__(self, other): 217 return not self.__eq__(other) 218 219 def iter_blocking_state(self, status): 220 for machine, info in status.iter_machines(): 221 if machine == self.machine: 222 yield machine, 'still-present' 223 224 def do_raise(self, model_name, status): 225 raise Exception("Timed out waiting for machine removal %s" % 226 self.machine) 227 228 229 class WaitApplicationNotPresent(BaseCondition): 230 """Condition satisfied when a given machine is not present.""" 231 232 def __init__(self, application, timeout=300): 233 super(WaitApplicationNotPresent, self).__init__(timeout) 234 self.application = application 235 236 def __eq__(self, other): 237 if not type(self) is type(other): 238 return False 239 if self.timeout != other.timeout: 240 return False 241 if self.application != other.application: 242 return False 243 return True 244 245 def __ne__(self, other): 246 return not self.__eq__(other) 247 248 def iter_blocking_state(self, status): 249 for application in status.get_applications().keys(): 250 if application == self.application: 251 yield application, 'still-present' 252 253 def do_raise(self, model_name, status): 254 raise Exception("Timed out waiting for application " 255 "removal {}".format(self.application)) 256 257 258 class MachineDown(BaseCondition): 259 """Condition satisfied when a given machine is down.""" 260 261 def __init__(self, machine_id): 262 super(MachineDown, self).__init__() 263 self.machine_id = machine_id 264 265 def iter_blocking_state(self, status): 266 """Yield the juju-status of the machine if it is not 'down'.""" 267 juju_status = status.status['machines'][self.machine_id]['juju-status'] 268 if juju_status['current'] != 'down': 269 yield self.machine_id, juju_status['current'] 270 271 def do_raise(self, model_name, status): 272 raise Exception( 273 "Timed out waiting for juju to determine machine {} down.".format( 274 self.machine_id)) 275 276 277 class WaitVersion(BaseCondition): 278 279 def __init__(self, target_version, timeout=300): 280 super(WaitVersion, self).__init__(timeout) 281 self.target_version = target_version 282 283 def iter_blocking_state(self, status): 284 for version, agents in status.get_agent_versions().items(): 285 if version == self.target_version: 286 continue 287 for agent in agents: 288 yield agent, version 289 290 def do_raise(self, model_name, status): 291 raise VersionsNotUpdated(model_name, status) 292 293 294 class WaitModelVersion(BaseCondition): 295 296 def __init__(self, target_version, timeout=300): 297 super(WaitModelVersion, self).__init__(timeout) 298 self.target_version = target_version 299 300 def iter_blocking_state(self, status): 301 model_version = status.status['model']['version'] 302 if model_version != self.target_version: 303 yield status.model_name, model_version 304 305 def do_raise(self, model_name, status): 306 raise VersionsNotUpdated(model_name, status) 307 308 309 class WaitAgentsStarted(BaseCondition): 310 """Wait until all agents are idle or started.""" 311 312 def __init__(self, timeout=1200): 313 super(WaitAgentsStarted, self).__init__(timeout) 314 315 def iter_blocking_state(self, status): 316 states = Status.check_agents_started(status) 317 318 if states is not None: 319 for state, item in states.items(): 320 yield item[0], state 321 322 def do_raise(self, model_name, status): 323 raise AgentsNotStarted(model_name, status) 324 325 326 class UnitInstallCondition(BaseCondition): 327 328 def __init__(self, unit, current, message, *args, **kwargs): 329 """Base condition for unit workload status.""" 330 self.unit = unit 331 self.current = current 332 self.message = message 333 super(UnitInstallCondition, self).__init__(*args, **kwargs) 334 335 def iter_blocking_state(self, status): 336 """Wait until 'current' status and message matches supplied values.""" 337 try: 338 unit = status.get_unit(self.unit) 339 unit_status = unit['workload-status'] 340 cond_met = (unit_status['current'] == self.current 341 and unit_status['message'] == self.message) 342 except KeyError: 343 cond_met = False 344 if not cond_met: 345 yield ('unit-workload ({})'.format(self.unit), 346 'not-{}'.format(self.current)) 347 348 def do_raise(self, model_name, status): 349 raise StatusNotMet('{} ({})'.format(model_name, self.unit), status) 350 351 352 class CommandComplete(BaseCondition): 353 """Wraps a CommandTime and gives the ability to wait_for completion.""" 354 355 def __init__(self, real_condition, command_time): 356 """Constructor. 357 358 :param real_condition: BaseCondition object. 359 :param command_time: CommandTime object representing the command to 360 wait for completion. 361 """ 362 super(CommandComplete, self).__init__( 363 real_condition.timeout, 364 real_condition.already_satisfied) 365 self._real_condition = real_condition 366 self.command_time = command_time 367 if real_condition.already_satisfied: 368 self.command_time.actual_completion() 369 370 def iter_blocking_state(self, status): 371 """Wraps the iter_blocking_state of the stored BaseCondition. 372 373 When the operation is complete iter_blocking_state yields nothing. 374 Otherwise iter_blocking_state yields details as to why the action 375 cannot be considered complete yet. 376 """ 377 completed = True 378 for item, state in self._real_condition.iter_blocking_state(status): 379 completed = False 380 yield item, state 381 if completed: 382 self.command_time.actual_completion() 383 384 def do_raise(self, status): 385 raise RuntimeError( 386 'Timed out waiting for "{}" command to complete: "{}"'.format( 387 self.command_time.cmd, 388 ' '.join(self.command_time.full_args))) 389 390 391 class CommandTime: 392 """Store timing details for a juju command.""" 393 394 def __init__(self, cmd, full_args, envvars=None, start=None): 395 """Constructor. 396 397 :param cmd: Command string for command run (e.g. bootstrap) 398 :param args: List of all args the command was called with. 399 :param envvars: Dict of any extra envvars set before command was 400 called. 401 :param start: datetime.datetime object representing when the command 402 was run. If None defaults to datetime.utcnow() 403 """ 404 self.cmd = cmd 405 self.full_args = full_args 406 self.envvars = envvars 407 self.start = start if start else datetime.utcnow() 408 self.end = None 409 410 def actual_completion(self, end=None): 411 """Signify that actual completion time of the command. 412 413 Note. ignores multiple calls after the initial call. 414 415 :param end: datetime.datetime object. If None defaults to 416 datetime.datetime.utcnow() 417 """ 418 if self.end is None: 419 self.end = end if end else datetime.utcnow() 420 421 @property 422 def total_seconds(self): 423 """Total amount of seconds a command took to complete. 424 425 :return: Int representing number of seconds or None if the command 426 timing has never been completed. 427 """ 428 if self.end is None: 429 return None 430 return (self.end - self.start).total_seconds() 431 432 class WaitForLXDProfileCondition(BaseCondition): 433 434 def __init__(self, machine, profile, *args, **kwargs): 435 """Constructor. 436 437 :param machine: machine id for machine to find the profile on. 438 :param profile: name of the LXD profile to find. 439 """ 440 self.machine = machine 441 self.profile = profile 442 super(WaitForLXDProfileCondition, self).__init__(*args, **kwargs) 443 444 def iter_blocking_state(self, status): 445 """Wait until 'profile' listed in 'machine' lxd-profiles from status.""" 446 machine_info = dict(status.iter_machines()) 447 machine = container = self.machine 448 if 'lxd' in machine: 449 # container = machine 450 machine = machine.split('/')[0] 451 try: 452 if 'lxd' in self.machine: 453 machine_lxdprofiles = machine_info[machine]['containers'][container]["lxd-profiles"] 454 else: 455 machine_lxdprofiles = machine_info[machine]["lxd-profiles"] 456 cond_met = self.profile in machine_lxdprofiles 457 except: 458 cond_met = False 459 if not cond_met: 460 yield ('lxd-profile ({})'.format(self.profile), 461 'not on machine-{}'.format(self.machine)) 462 463 def do_raise(self, model_name, status): 464 raise LXDProfileNotAvailable(self.machine, self.profile)