bd3d030e92414e896e72837a8f67eef9b963e2cc
7 from . import model
, utils
8 from .client
import client
9 from .errors
import JujuError
11 log
= logging
.getLogger(__name__
)
14 class Machine(model
.ModelEntity
):
15 def __init__(self
, *args
, **kwargs
):
16 super().__init
__(*args
, **kwargs
)
17 self
.on_change(self
._workaround
_1695335)
19 async def _workaround_1695335(self
, delta
, old
, new
, model
):
21 This is a (hacky) temporary work around for a bug in Juju where the
22 instance status and agent version fields don't get updated properly
25 Deltas never contain a value for `data['agent-status']['version']`,
26 and once the `instance-status` reaches `pending`, we no longer get
27 any updates for it (the deltas come in, but the `instance-status`
28 data is always the same after that).
30 To work around this, whenever a delta comes in for this machine, we
31 query FullStatus and use the data from there if and only if it's newer.
32 Luckily, the timestamps on the `since` field does seem to be accurate.
34 See https://bugs.launchpad.net/juju/+bug/1695335
36 if delta
.data
.get('synthetic', False):
37 # prevent infinite loops re-processing already processed deltas
40 full_status
= await utils
.run_with_interrupt(model
.get_status(),
41 model
._watch
_stopping
,
43 if model
._watch
_stopping
.is_set():
46 if self
.id not in full_status
.machines
:
49 if not full_status
.machines
[self
.id]['instance-status']['since']:
52 machine
= full_status
.machines
[self
.id]
61 # handle agent version specially, because it's never set in
62 # deltas, and we don't want even a newer delta to clear it
63 agent_version
= machine
['agent-status']['version']
65 delta
.data
['agent-status']['version'] = agent_version
66 change_log
.append(('agent-version', '', agent_version
))
68 # only update (other) delta fields if status data is newer
69 status_since
= pyrfc3339
.parse(machine
['instance-status']['since'])
70 delta_since
= pyrfc3339
.parse(delta
.data
['instance-status']['since'])
71 if status_since
> delta_since
:
72 for status_key
in ('status', 'info', 'since'):
73 delta_key
= key_map
[status_key
]
74 status_value
= machine
['instance-status'][status_key
]
75 delta_value
= delta
.data
['instance-status'][delta_key
]
76 change_log
.append((delta_key
, delta_value
, status_value
))
77 delta
.data
['instance-status'][delta_key
] = status_value
80 log
.debug('Overriding machine delta with FullStatus data')
81 for log_item
in change_log
:
82 log
.debug(' {}: {} -> {}'.format(*log_item
))
83 delta
.data
['synthetic'] = True
84 old_obj
, new_obj
= self
.model
.state
.apply_delta(delta
)
85 await model
._notify
_observers
(delta
, old_obj
, new_obj
)
87 async def destroy(self
, force
=False):
88 """Remove this machine from the model.
90 Blocks until the machine is actually removed.
93 facade
= client
.ClientFacade
.from_connection(self
.connection
)
96 'Destroying machine %s', self
.id)
98 await facade
.DestroyMachines(force
, [self
.id])
99 return await self
.model
._wait
(
100 'machine', self
.id, 'remove')
103 def run(self
, command
, timeout
=None):
104 """Run command on this machine.
106 :param str command: The command to run
107 :param int timeout: Time to wait before command is considered failed
110 raise NotImplementedError()
112 async def set_annotations(self
, annotations
):
113 """Set annotations on this machine.
115 :param annotations map[string]string: the annotations as key/value
119 log
.debug('Updating annotations on machine %s', self
.id)
121 self
.ann_facade
= client
.AnnotationsFacade
.from_connection(
124 ann
= client
.EntityAnnotations(
126 annotations
=annotations
,
128 return await self
.ann_facade
.Set([ann
])
130 async def scp_to(self
, source
, destination
, user
='ubuntu', proxy
=False,
132 """Transfer files to this machine.
134 :param str source: Local path of file(s) to transfer
135 :param str destination: Remote destination of transferred files
136 :param str user: Remote username
137 :param bool proxy: Proxy through the Juju API server
138 :param str scp_opts: Additional options to the `scp` command
141 raise NotImplementedError('proxy option is not implemented')
143 address
= self
.dns_name
144 destination
= '%s@%s:%s' % (user
, address
, destination
)
145 await self
._scp
(source
, destination
, scp_opts
)
147 async def scp_from(self
, source
, destination
, user
='ubuntu', proxy
=False,
149 """Transfer files from this machine.
151 :param str source: Remote path of file(s) to transfer
152 :param str destination: Local destination of transferred files
153 :param str user: Remote username
154 :param bool proxy: Proxy through the Juju API server
155 :param str scp_opts: Additional options to the `scp` command
158 raise NotImplementedError('proxy option is not implemented')
160 address
= self
.dns_name
161 source
= '%s@%s:%s' % (user
, address
, source
)
162 await self
._scp
(source
, destination
, scp_opts
)
164 async def _scp(self
, source
, destination
, scp_opts
):
165 """ Execute an scp command. Requires a fully qualified source and
170 '-i', os
.path
.expanduser('~/.local/share/juju/ssh/juju_id_rsa'),
171 '-o', 'StrictHostKeyChecking=no',
176 cmd
+= scp_opts
.split()
177 loop
= self
.model
.loop
178 process
= await asyncio
.create_subprocess_exec(*cmd
, loop
=loop
)
180 if process
.returncode
!= 0:
181 raise JujuError("command failed: %s" % cmd
)
184 self
, command
, user
=None, proxy
=False, ssh_opts
=None):
185 """Execute a command over SSH on this machine.
187 :param str command: Command to execute
188 :param str user: Remote username
189 :param bool proxy: Proxy through the Juju API server
190 :param str ssh_opts: Additional options to the `ssh` command
193 raise NotImplementedError()
195 def status_history(self
, num
=20, utc
=False):
196 """Get status history for this machine.
198 :param int num: Size of history backlog
199 :param bool utc: Display time as UTC in RFC3339 format
202 raise NotImplementedError()
205 def agent_status(self
):
206 """Returns the current Juju agent status string.
209 return self
.safe_data
['agent-status']['current']
212 def agent_status_since(self
):
213 """Get the time when the `agent_status` was last updated.
216 return pyrfc3339
.parse(self
.safe_data
['agent-status']['since'])
219 def agent_version(self
):
220 """Get the version of the Juju machine agent.
222 May return None if the agent is not yet available.
224 version
= self
.safe_data
['agent-status']['version']
226 return client
.Number
.from_json(version
)
232 """Returns the current machine provisioning status string.
235 return self
.safe_data
['instance-status']['current']
238 def status_message(self
):
239 """Returns the current machine provisioning status message.
242 return self
.safe_data
['instance-status']['message']
245 def status_since(self
):
246 """Get the time when the `status` was last updated.
249 return pyrfc3339
.parse(self
.safe_data
['instance-status']['since'])
253 """Get the DNS name for this machine. This is a best guess based on the
254 addresses available in current data.
256 May return None if no suitable address is found.
258 for scope
in ['public', 'local-cloud']:
259 addresses
= self
.safe_data
['addresses'] or []
260 addresses
= [address
for address
in addresses
261 if address
['scope'] == scope
]
263 return addresses
[0]['value']
268 """Returns the series of the current machine
271 return self
.safe_data
['series']