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)