Patch/update schema juju 2.3 alpha1 (#142)
[osm/N2VC.git] / juju / machine.py
1 import logging
2
3 from dateutil.parser import parse as parse_date
4
5 from . import model, utils
6 from .client import client
7
8 log = logging.getLogger(__name__)
9
10
11 class Machine(model.ModelEntity):
12 def __init__(self, *args, **kwargs):
13 super().__init__(*args, **kwargs)
14 self.on_change(self._workaround_1695335)
15
16 async def _workaround_1695335(self, delta, old, new, model):
17 """
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
20 by the AllWatcher.
21
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).
26
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.
30
31 See https://bugs.launchpad.net/juju/+bug/1695335
32 """
33 if delta.data.get('synthetic', False):
34 # prevent infinite loops re-processing already processed deltas
35 return
36
37 full_status = await utils.run_with_interrupt(model.get_status(),
38 model._watch_stopping,
39 model.loop)
40 if model._watch_stopping.is_set():
41 return
42
43 if self.id not in full_status.machines:
44 return
45
46 if not full_status.machines[self.id]['instance-status']['since']:
47 return
48
49 machine = full_status.machines[self.id]
50
51 change_log = []
52 key_map = {
53 'status': 'current',
54 'info': 'message',
55 'since': 'since',
56 }
57
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']
61 if agent_version:
62 delta.data['agent-status']['version'] = agent_version
63 change_log.append(('agent-version', '', agent_version))
64
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
75
76 if change_log:
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)
83
84 async def destroy(self, force=False):
85 """Remove this machine from the model.
86
87 Blocks until the machine is actually removed.
88
89 """
90 facade = client.ClientFacade.from_connection(self.connection)
91
92 log.debug(
93 'Destroying machine %s', self.id)
94
95 await facade.DestroyMachines(force, [self.id])
96 return await self.model._wait(
97 'machine', self.id, 'remove')
98 remove = destroy
99
100 def run(self, command, timeout=None):
101 """Run command on this machine.
102
103 :param str command: The command to run
104 :param int timeout: Time to wait before command is considered failed
105
106 """
107 raise NotImplementedError()
108
109 async def set_annotations(self, annotations):
110 """Set annotations on this machine.
111
112 :param annotations map[string]string: the annotations as key/value
113 pairs.
114
115 """
116 log.debug('Updating annotations on machine %s', self.id)
117
118 self.ann_facade = client.AnnotationsFacade.from_connection(
119 self.connection)
120
121 ann = client.EntityAnnotations(
122 entity=self.id,
123 annotations=annotations,
124 )
125 return await self.ann_facade.Set([ann])
126
127 def scp(
128 self, source_path, user=None, destination_path=None, proxy=False,
129 scp_opts=None):
130 """Transfer files to this machine.
131
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
135 remote machine
136 :param bool proxy: Proxy through the Juju API server
137 :param str scp_opts: Additional options to the `scp` command
138
139 """
140 raise NotImplementedError()
141
142 def ssh(
143 self, command, user=None, proxy=False, ssh_opts=None):
144 """Execute a command over SSH on this machine.
145
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
150
151 """
152 raise NotImplementedError()
153
154 def status_history(self, num=20, utc=False):
155 """Get status history for this machine.
156
157 :param int num: Size of history backlog
158 :param bool utc: Display time as UTC in RFC3339 format
159
160 """
161 raise NotImplementedError()
162
163 @property
164 def agent_status(self):
165 """Returns the current Juju agent status string.
166
167 """
168 return self.safe_data['agent-status']['current']
169
170 @property
171 def agent_status_since(self):
172 """Get the time when the `agent_status` was last updated.
173
174 """
175 return parse_date(self.safe_data['agent-status']['since'])
176
177 @property
178 def agent_version(self):
179 """Get the version of the Juju machine agent.
180
181 May return None if the agent is not yet available.
182 """
183 version = self.safe_data['agent-status']['version']
184 if version:
185 return client.Number.from_json(version)
186 else:
187 return None
188
189 @property
190 def status(self):
191 """Returns the current machine provisioning status string.
192
193 """
194 return self.safe_data['instance-status']['current']
195
196 @property
197 def status_message(self):
198 """Returns the current machine provisioning status message.
199
200 """
201 return self.safe_data['instance-status']['message']
202
203 @property
204 def status_since(self):
205 """Get the time when the `status` was last updated.
206
207 """
208 return parse_date(self.safe_data['instance-status']['since'])