From: Cory Johns Date: Mon, 5 Jun 2017 14:28:16 +0000 (-0400) Subject: Add machine status properties (#133) X-Git-Tag: 0.5.0~3 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=49fe19ff5754ae8ce9365cd7bddbcd33f565bd69;p=osm%2FN2VC.git Add machine status properties (#133) * Add machine status properties Add properties to access the machine provisioning and Juju machine agent status information. Unfortunately, there seems to be a bug with the AllWatcher related to these fields (https://bugs.launchpad.net/juju/+bug/1695335) that causes the deltas to contain stale data, so this also includes a hack to work around that by fetching newer data from the FullStatus API. * Fixed test for AWS --- diff --git a/juju/machine.py b/juju/machine.py index b06d54b..18c333c 100644 --- a/juju/machine.py +++ b/juju/machine.py @@ -1,12 +1,86 @@ import logging -from . import model +from dateutil.parser import parse as parse_date + +from . import model, utils from .client import client log = logging.getLogger(__name__) class Machine(model.ModelEntity): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.on_change(self._workaround_1695335) + + async def _workaround_1695335(self, delta, old, new, model): + """ + This is a (hacky) temporary work around for a bug in Juju where the + instance status and agent version fields don't get updated properly + by the AllWatcher. + + Deltas never contain a value for `data['agent-status']['version']`, + and once the `instance-status` reaches `pending`, we no longer get + any updates for it (the deltas come in, but the `instance-status` + data is always the same after that). + + To work around this, whenever a delta comes in for this machine, we + query FullStatus and use the data from there if and only if it's newer. + Luckily, the timestamps on the `since` field does seem to be accurate. + + See https://bugs.launchpad.net/juju/+bug/1695335 + """ + if delta.data.get('synthetic', False): + # prevent infinite loops re-processing already processed deltas + return + + full_status = await utils.run_with_interrupt(model.get_status(), + model._watch_stopping, + model.loop) + if model._watch_stopping.is_set(): + return + + if self.id not in full_status.machines: + return + + if not full_status.machines[self.id]['instance-status']['since']: + return + + machine = full_status.machines[self.id] + + change_log = [] + key_map = { + 'status': 'current', + 'info': 'message', + 'since': 'since', + } + + # handle agent version specially, because it's never set in + # deltas, and we don't want even a newer delta to clear it + agent_version = machine['agent-status']['version'] + if agent_version: + delta.data['agent-status']['version'] = agent_version + change_log.append(('agent-version', '', agent_version)) + + # only update (other) delta fields if status data is newer + status_since = parse_date(machine['instance-status']['since']) + delta_since = parse_date(delta.data['instance-status']['since']) + if status_since > delta_since: + for status_key in ('status', 'info', 'since'): + delta_key = key_map[status_key] + status_value = machine['instance-status'][status_key] + delta_value = delta.data['instance-status'][delta_key] + change_log.append((delta_key, delta_value, status_value)) + delta.data['instance-status'][delta_key] = status_value + + if change_log: + log.debug('Overriding machine delta with FullStatus data') + for log_item in change_log: + log.debug(' {}: {} -> {}'.format(*log_item)) + delta.data['synthetic'] = True + old_obj, new_obj = self.model.state.apply_delta(delta) + await model._notify_observers(delta, old_obj, new_obj) + async def destroy(self, force=False): """Remove this machine from the model. @@ -85,3 +159,50 @@ class Machine(model.ModelEntity): """ raise NotImplementedError() + + @property + def agent_status(self): + """Returns the current Juju agent status string. + + """ + return self.safe_data['agent-status']['current'] + + @property + def agent_status_since(self): + """Get the time when the `agent_status` was last updated. + + """ + return parse_date(self.safe_data['agent-status']['since']) + + @property + def agent_version(self): + """Get the version of the Juju machine agent. + + May return None if the agent is not yet available. + """ + version = self.safe_data['agent-status']['version'] + if version: + return client.Number.from_json(version) + else: + return None + + @property + def status(self): + """Returns the current machine provisioning status string. + + """ + return self.safe_data['instance-status']['current'] + + @property + def status_message(self): + """Returns the current machine provisioning status message. + + """ + return self.safe_data['instance-status']['message'] + + @property + def status_since(self): + """Get the time when the `status` was last updated. + + """ + return parse_date(self.safe_data['instance-status']['since']) diff --git a/tests/integration/test_machine.py b/tests/integration/test_machine.py new file mode 100644 index 0000000..5909c05 --- /dev/null +++ b/tests/integration/test_machine.py @@ -0,0 +1,37 @@ +import asyncio + +import pytest + +from .. import base + + +@base.bootstrapped +@pytest.mark.asyncio +async def test_status(event_loop): + async with base.CleanModel() as model: + await model.deploy( + 'ubuntu-0', + application_name='ubuntu', + series='trusty', + channel='stable', + ) + + await asyncio.wait_for( + model.block_until(lambda: len(model.machines)), + timeout=240) + machine = model.machines['0'] + + assert machine.status in ('allocating', 'pending') + assert machine.agent_status == 'pending' + assert not machine.agent_version + + await asyncio.wait_for( + model.block_until(lambda: (machine.status == 'running' and + machine.agent_status == 'started')), + timeout=240) + + assert machine.status == 'running' + # there is some inconsistency in the message case between providers + assert machine.status_message.lower() == 'running' + assert machine.agent_status == 'started' + assert machine.agent_version.major >= 2