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]