18c333c17c11e3e29ccd9348aa62f7257719247d
3 from dateutil
.parser
import parse
as parse_date
5 from . import model
, utils
6 from .client
import client
8 log
= logging
.getLogger(__name__
)
11 class Machine(model
.ModelEntity
):
12 def __init__(self
, *args
, **kwargs
):
13 super().__init
__(*args
, **kwargs
)
14 self
.on_change(self
._workaround
_1695335)
16 async def _workaround_1695335(self
, delta
, old
, new
, model
):
18 This is a (hacky) temporary work around for a bug in Juju where the
19 instance status and agent version fields don't get updated properly
22 Deltas never contain a value for `data['agent-status']['version']`,
23 and once the `instance-status` reaches `pending`, we no longer get
24 any updates for it (the deltas come in, but the `instance-status`
25 data is always the same after that).
27 To work around this, whenever a delta comes in for this machine, we
28 query FullStatus and use the data from there if and only if it's newer.
29 Luckily, the timestamps on the `since` field does seem to be accurate.
31 See https://bugs.launchpad.net/juju/+bug/1695335
33 if delta
.data
.get('synthetic', False):
34 # prevent infinite loops re-processing already processed deltas
37 full_status
= await utils
.run_with_interrupt(model
.get_status(),
38 model
._watch
_stopping
,
40 if model
._watch
_stopping
.is_set():
43 if self
.id not in full_status
.machines
:
46 if not full_status
.machines
[self
.id]['instance-status']['since']:
49 machine
= full_status
.machines
[self
.id]
58 # handle agent version specially, because it's never set in
59 # deltas, and we don't want even a newer delta to clear it
60 agent_version
= machine
['agent-status']['version']
62 delta
.data
['agent-status']['version'] = agent_version
63 change_log
.append(('agent-version', '', agent_version
))
65 # only update (other) delta fields if status data is newer
66 status_since
= parse_date(machine
['instance-status']['since'])
67 delta_since
= parse_date(delta
.data
['instance-status']['since'])
68 if status_since
> delta_since
:
69 for status_key
in ('status', 'info', 'since'):
70 delta_key
= key_map
[status_key
]
71 status_value
= machine
['instance-status'][status_key
]
72 delta_value
= delta
.data
['instance-status'][delta_key
]
73 change_log
.append((delta_key
, delta_value
, status_value
))
74 delta
.data
['instance-status'][delta_key
] = status_value
77 log
.debug('Overriding machine delta with FullStatus data')
78 for log_item
in change_log
:
79 log
.debug(' {}: {} -> {}'.format(*log_item
))
80 delta
.data
['synthetic'] = True
81 old_obj
, new_obj
= self
.model
.state
.apply_delta(delta
)
82 await model
._notify
_observers
(delta
, old_obj
, new_obj
)
84 async def destroy(self
, force
=False):
85 """Remove this machine from the model.
87 Blocks until the machine is actually removed.
90 facade
= client
.ClientFacade
.from_connection(self
.connection
)
93 'Destroying machine %s', self
.id)
95 await facade
.DestroyMachines(force
, [self
.id])
96 return await self
.model
._wait
(
97 'machine', self
.id, 'remove')
100 def run(self
, command
, timeout
=None):
101 """Run command on this machine.
103 :param str command: The command to run
104 :param int timeout: Time to wait before command is considered failed
107 raise NotImplementedError()
109 async def set_annotations(self
, annotations
):
110 """Set annotations on this machine.
112 :param annotations map[string]string: the annotations as key/value
116 log
.debug('Updating annotations on machine %s', self
.id)
118 self
.ann_facade
= client
.AnnotationsFacade
.from_connection(
121 ann
= client
.EntityAnnotations(
123 annotations
=annotations
,
125 return await self
.ann_facade
.Set([ann
])
128 self
, source_path
, user
=None, destination_path
=None, proxy
=False,
130 """Transfer files to this machine.
132 :param str source_path: Path of file(s) to transfer
133 :param str user: Remote username
134 :param str destination_path: Destination of transferred files on
136 :param bool proxy: Proxy through the Juju API server
137 :param str scp_opts: Additional options to the `scp` command
140 raise NotImplementedError()
143 self
, command
, user
=None, proxy
=False, ssh_opts
=None):
144 """Execute a command over SSH on this machine.
146 :param str command: Command to execute
147 :param str user: Remote username
148 :param bool proxy: Proxy through the Juju API server
149 :param str ssh_opts: Additional options to the `ssh` command
152 raise NotImplementedError()
154 def status_history(self
, num
=20, utc
=False):
155 """Get status history for this machine.
157 :param int num: Size of history backlog
158 :param bool utc: Display time as UTC in RFC3339 format
161 raise NotImplementedError()
164 def agent_status(self
):
165 """Returns the current Juju agent status string.
168 return self
.safe_data
['agent-status']['current']
171 def agent_status_since(self
):
172 """Get the time when the `agent_status` was last updated.
175 return parse_date(self
.safe_data
['agent-status']['since'])
178 def agent_version(self
):
179 """Get the version of the Juju machine agent.
181 May return None if the agent is not yet available.
183 version
= self
.safe_data
['agent-status']['version']
185 return client
.Number
.from_json(version
)
191 """Returns the current machine provisioning status string.
194 return self
.safe_data
['instance-status']['current']
197 def status_message(self
):
198 """Returns the current machine provisioning status message.
201 return self
.safe_data
['instance-status']['message']
204 def status_since(self
):
205 """Get the time when the `status` was last updated.
208 return parse_date(self
.safe_data
['instance-status']['since'])