github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/acceptancetests/jujupy/status.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 json
    17  import re
    18  import yaml
    19  
    20  from collections import defaultdict
    21  from datetime import datetime
    22  from dateutil.parser import parse as datetime_parse
    23  from dateutil import tz
    24  
    25  from jujupy.exceptions import (
    26      AgentError,
    27      AgentUnresolvedError,
    28      AppError,
    29      ErroredUnit,
    30      HookFailedError,
    31      InstallError,
    32      MachineError,
    33      ProvisioningError,
    34      StuckAllocatingError,
    35      UnitError,
    36  )
    37  from jujupy.utility import (
    38      _dns_name_for_machine,
    39      )
    40  
    41  __metaclass__ = type
    42  
    43  
    44  AGENTS_READY = set(['started', 'idle'])
    45  
    46  
    47  def coalesce_agent_status(agent_item):
    48      """Return the machine agent-state or the unit agent-status."""
    49      state = agent_item.get('agent-state')
    50      if state is None and agent_item.get('agent-status') is not None:
    51          state = agent_item.get('agent-status').get('current')
    52      if state is None and agent_item.get('juju-status') is not None:
    53          state = agent_item.get('juju-status').get('current')
    54      if state is None:
    55          state = 'no-agent'
    56      return state
    57  
    58  
    59  class StatusItem:
    60  
    61      APPLICATION = 'application-status'
    62      WORKLOAD = 'workload-status'
    63      MACHINE = 'machine-status'
    64      JUJU = 'juju-status'
    65  
    66      def __init__(self, status_name, item_name, item_value):
    67          """Create a new StatusItem from its fields.
    68  
    69          :param status_name: One of the status strings.
    70          :param item_name: The name of the machine/unit/application the status
    71              information is about.
    72          :param item_value: A dictionary of status values. If there is an entry
    73              with the status_name in the dictionary its contents are used."""
    74          self.status_name = status_name
    75          self.item_name = item_name
    76          self.status = item_value.get(status_name, item_value)
    77  
    78      def __eq__(self, other):
    79          if type(other) != type(self):
    80              return False
    81          elif self.status_name != other.status_name:
    82              return False
    83          elif self.item_name != other.item_name:
    84              return False
    85          elif self.status != other.status:
    86              return False
    87          else:
    88              return True
    89  
    90      def __ne__(self, other):
    91          return bool(not self == other)
    92  
    93      @property
    94      def message(self):
    95          return self.status.get('message')
    96  
    97      @property
    98      def since(self):
    99          return self.status.get('since')
   100  
   101      @property
   102      def current(self):
   103          return self.status.get('current')
   104  
   105      @property
   106      def version(self):
   107          return self.status.get('version')
   108  
   109      @property
   110      def datetime_since(self):
   111          if self.since is None:
   112              return None
   113          return datetime_parse(self.since)
   114  
   115      def to_exception(self):
   116          """Create an exception representing the error if one exists.
   117  
   118          :return: StatusError (or subtype) to represent an error or None
   119          to show that there is no error."""
   120          if self.current not in ['error', 'failed', 'down',
   121                                  'provisioning error']:
   122              if (self.current, self.status_name) != (
   123                      'allocating', self.MACHINE):
   124                  return None
   125          if self.APPLICATION == self.status_name:
   126              return AppError(self.item_name, self.message)
   127          elif self.WORKLOAD == self.status_name:
   128              if self.message is None:
   129                  return UnitError(self.item_name, self.message)
   130              elif re.match('hook failed: ".*install.*"', self.message):
   131                  return InstallError(self.item_name, self.message)
   132              elif re.match('hook failed', self.message):
   133                  return HookFailedError(self.item_name, self.message)
   134              else:
   135                  return UnitError(self.item_name, self.message)
   136          elif self.MACHINE == self.status_name:
   137              if self.current == 'provisioning error':
   138                  return ProvisioningError(self.item_name, self.message)
   139              if self.current == 'allocating':
   140                  return StuckAllocatingError(
   141                      self.item_name,
   142                      'Stuck allocating.  Last message: {}'.format(self.message))
   143              else:
   144                  return MachineError(self.item_name, self.message)
   145          elif self.JUJU == self.status_name:
   146              if self.since is None:
   147                  return AgentError(self.item_name, self.message)
   148              time_since = datetime.now(tz.gettz('UTC')) - self.datetime_since
   149              if time_since > AgentUnresolvedError.a_reasonable_time:
   150                  return AgentUnresolvedError(self.item_name, self.message,
   151                                              time_since.total_seconds())
   152              else:
   153                  return AgentError(self.item_name, self.message)
   154          else:
   155              raise ValueError('Unknown status:{}'.format(self.status_name),
   156                               (self.item_name, self.status_value))
   157  
   158      def __repr__(self):
   159          return 'StatusItem({!r}, {!r}, {!r})'.format(
   160              self.status_name, self.item_name, self.status)
   161  
   162  
   163  class Status:
   164  
   165      def __init__(self, status, status_text):
   166          self.status = status
   167          self.status_text = status_text
   168  
   169      @classmethod
   170      def from_text(cls, text):
   171          try:
   172              # Parsing as JSON is much faster than parsing as YAML, so try
   173              # parsing as JSON first and fall back to YAML.
   174              status_yaml = json.loads(text)
   175          except ValueError:
   176              status_yaml = yaml.safe_load(text)
   177          return cls(status_yaml, text)
   178  
   179      @property
   180      def model_name(self):
   181          return self.status['model']['name']
   182  
   183      def get_applications(self):
   184          return self.status.get('applications', {})
   185  
   186      def iter_machines(self, containers=False, machines=True):
   187          for machine_name, machine in sorted(self.status['machines'].items()):
   188              if machines:
   189                  yield machine_name, machine
   190              if containers:
   191                  for contained, unit in machine.get('containers', {}).items():
   192                      yield contained, unit
   193  
   194      def iter_new_machines(self, old_status, containers=False):
   195          old = dict(old_status.iter_machines(containers=containers))
   196          for machine, data in self.iter_machines(containers=containers):
   197              if machine in old:
   198                  continue
   199              yield machine, data
   200  
   201      def _iter_units_in_application(self, app_data):
   202          """Given application data, iterate through every unit in it."""
   203          for unit_name, unit in sorted(app_data.get('units', {}).items()):
   204              yield unit_name, unit
   205              subordinates = unit.get('subordinates', ())
   206              for sub_name in sorted(subordinates):
   207                  yield sub_name, subordinates[sub_name]
   208  
   209      def iter_units(self):
   210          """Iterate over every unit in every application."""
   211          for service_name, service in sorted(self.get_applications().items()):
   212              for name, data in self._iter_units_in_application(service):
   213                  yield name, data
   214  
   215      def agent_items(self):
   216          for machine_name, machine in self.iter_machines(containers=True):
   217              yield machine_name, machine
   218          for unit_name, unit in self.iter_units():
   219              yield unit_name, unit
   220  
   221      def unit_agent_states(self, states=None):
   222          """Fill in a dictionary with the states of units.
   223  
   224          Units of a dying application are marked as dying.
   225  
   226          :param states: If not None, when it should be a defaultdict(list)),
   227          then states are added to this dictionary."""
   228          if states is None:
   229              states = defaultdict(list)
   230          for app_name, app_data in sorted(self.get_applications().items()):
   231              if app_data.get('life') == 'dying':
   232                  for unit, data in self._iter_units_in_application(app_data):
   233                      states['dying'].append(unit)
   234              else:
   235                  for unit, data in self._iter_units_in_application(app_data):
   236                      states[coalesce_agent_status(data)].append(unit)
   237          return states
   238  
   239      def agent_states(self):
   240          """Map agent states to the units and machines in those states."""
   241          states = defaultdict(list)
   242          for item_name, item in self.iter_machines(containers=True):
   243              states[coalesce_agent_status(item)].append(item_name)
   244          self.unit_agent_states(states)
   245          return states
   246  
   247      def check_agents_started(self, environment_name=None):
   248          """Check whether all agents are in the 'started' state.
   249  
   250          If not, return agent_states output.  If so, return None.
   251          If an error is encountered for an agent, raise ErroredUnit
   252          """
   253          bad_state_info = re.compile(
   254              '(.*error|^(cannot set up groups|cannot run instance)).*')
   255          for item_name, item in self.agent_items():
   256              state_info = item.get('agent-state-info', '')
   257              if bad_state_info.match(state_info):
   258                  raise ErroredUnit(item_name, state_info)
   259          states = self.agent_states()
   260          if set(states.keys()).issubset(AGENTS_READY):
   261              return None
   262          for state, entries in states.items():
   263              if 'error' in state:
   264                  # sometimes the state may be hidden in juju status message
   265                  juju_status = dict(
   266                      self.agent_items())[entries[0]].get('juju-status')
   267                  if juju_status:
   268                      juju_status_msg = juju_status.get('message')
   269                      if juju_status_msg:
   270                          state = juju_status_msg
   271                  raise ErroredUnit(entries[0], state)
   272          return states
   273  
   274      def get_service_count(self):
   275          return len(self.get_applications())
   276  
   277      def get_service_unit_count(self, service):
   278          return len(
   279              self.get_applications().get(service, {}).get('units', {}))
   280  
   281      def get_agent_versions(self):
   282          versions = defaultdict(set)
   283          for item_name, item in self.agent_items():
   284              if item.get('juju-status', None):
   285                  version = item['juju-status'].get('version', 'unknown')
   286                  versions[version].add(item_name)
   287              else:
   288                  versions[item.get('agent-version', 'unknown')].add(item_name)
   289          return versions
   290  
   291      def get_instance_id(self, machine_id):
   292          return self.status['machines'][machine_id]['instance-id']
   293  
   294      def get_machine_dns_name(self, machine_id):
   295          return _dns_name_for_machine(self, machine_id)
   296  
   297      def get_unit(self, unit_name):
   298          """Return metadata about a unit."""
   299          for name, service in sorted(self.get_applications().items()):
   300              units = service.get('units', {})
   301              if unit_name in units:
   302                  return service['units'][unit_name]
   303              # The unit might be a subordinate, in which case it won't
   304              # be under its application, but under the principal
   305              # unit.
   306              for _, unit in units.items():
   307                  if unit_name in unit.get('subordinates', {}):
   308                      return unit['subordinates'][unit_name]
   309          raise KeyError(unit_name)
   310  
   311      def service_subordinate_units(self, service_name):
   312          """Return subordinate metadata for a service_name."""
   313          services = self.get_applications()
   314          if service_name in services:
   315              for name, unit in sorted(services[service_name].get(
   316                      'units', {}).items()):
   317                  for sub_name, sub in unit.get('subordinates', {}).items():
   318                      yield sub_name, sub
   319  
   320      def get_open_ports(self, unit_name):
   321          """List the open ports for the specified unit.
   322  
   323          If no ports are listed for the unit, the empty list is returned.
   324          """
   325          return self.get_unit(unit_name).get('open-ports', [])
   326  
   327      def iter_status(self):
   328          """Iterate through every status field in the larger status data."""
   329          for machine_name, machine_value in self.iter_machines(containers=True):
   330              yield StatusItem(StatusItem.MACHINE, machine_name, machine_value)
   331              yield StatusItem(StatusItem.JUJU, machine_name, machine_value)
   332          for app_name, app_value in self.get_applications().items():
   333              yield StatusItem(StatusItem.APPLICATION, app_name, app_value)
   334              unit_iterator = self._iter_units_in_application(app_value)
   335              for unit_name, unit_value in unit_iterator:
   336                  yield StatusItem(StatusItem.WORKLOAD, unit_name, unit_value)
   337                  yield StatusItem(StatusItem.JUJU, unit_name, unit_value)
   338  
   339      def iter_errors(self, ignore_recoverable=False):
   340          """Iterate through every error, represented by exceptions."""
   341          for sub_status in self.iter_status():
   342              error = sub_status.to_exception()
   343              if error is not None:
   344                  if not (ignore_recoverable and error.recoverable):
   345                      yield error
   346  
   347      def check_for_errors(self, ignore_recoverable=False):
   348          """Return a list of errors, in order of their priority."""
   349          return sorted(self.iter_errors(ignore_recoverable),
   350                        key=lambda item: item.priority())
   351  
   352      def raise_highest_error(self, ignore_recoverable=False):
   353          """Raise an exception reperenting the highest priority error."""
   354          errors = self.check_for_errors(ignore_recoverable)
   355          if errors:
   356              raise errors[0]