| Adam Israel | dcdf82b | 2017-08-15 15:26:43 -0400 | [diff] [blame] | 1 | import logging |
| 2 | |
| Adam Israel | c3e6c2e | 2018-03-01 09:31:50 -0500 | [diff] [blame] | 3 | import pyrfc3339 |
| Adam Israel | dcdf82b | 2017-08-15 15:26:43 -0400 | [diff] [blame] | 4 | |
| 5 | from . import model |
| 6 | from .client import client |
| 7 | |
| 8 | log = logging.getLogger(__name__) |
| 9 | |
| 10 | |
| 11 | class Unit(model.ModelEntity): |
| 12 | @property |
| 13 | def agent_status(self): |
| 14 | """Returns the current agent status string. |
| 15 | |
| 16 | """ |
| 17 | return self.safe_data['agent-status']['current'] |
| 18 | |
| 19 | @property |
| 20 | def agent_status_since(self): |
| 21 | """Get the time when the `agent_status` was last updated. |
| 22 | |
| 23 | """ |
| Adam Israel | c3e6c2e | 2018-03-01 09:31:50 -0500 | [diff] [blame] | 24 | return pyrfc3339.parse(self.safe_data['agent-status']['since']) |
| Adam Israel | dcdf82b | 2017-08-15 15:26:43 -0400 | [diff] [blame] | 25 | |
| 26 | @property |
| 27 | def agent_status_message(self): |
| 28 | """Get the agent status message. |
| 29 | |
| 30 | """ |
| 31 | return self.safe_data['agent-status']['message'] |
| 32 | |
| 33 | @property |
| 34 | def workload_status(self): |
| 35 | """Returns the current workload status string. |
| 36 | |
| 37 | """ |
| 38 | return self.safe_data['workload-status']['current'] |
| 39 | |
| 40 | @property |
| 41 | def workload_status_since(self): |
| 42 | """Get the time when the `workload_status` was last updated. |
| 43 | |
| 44 | """ |
| Adam Israel | c3e6c2e | 2018-03-01 09:31:50 -0500 | [diff] [blame] | 45 | return pyrfc3339.parse(self.safe_data['workload-status']['since']) |
| Adam Israel | dcdf82b | 2017-08-15 15:26:43 -0400 | [diff] [blame] | 46 | |
| 47 | @property |
| 48 | def workload_status_message(self): |
| 49 | """Get the workload status message. |
| 50 | |
| 51 | """ |
| 52 | return self.safe_data['workload-status']['message'] |
| 53 | |
| 54 | @property |
| 55 | def machine(self): |
| 56 | """Get the machine object for this unit. |
| 57 | |
| 58 | """ |
| 59 | machine_id = self.safe_data['machine-id'] |
| 60 | if machine_id: |
| 61 | return self.model.machines.get(machine_id, None) |
| 62 | else: |
| 63 | return None |
| 64 | |
| 65 | @property |
| 66 | def public_address(self): |
| 67 | """ Get the public address. |
| 68 | |
| 69 | """ |
| 70 | return self.safe_data['public-address'] or None |
| 71 | |
| 72 | @property |
| 73 | def tag(self): |
| 74 | return 'unit-%s' % self.name.replace('/', '-') |
| 75 | |
| 76 | def add_storage(self, name, constraints=None): |
| 77 | """Add unit storage dynamically. |
| 78 | |
| 79 | :param str name: Storage name, as specified by the charm |
| 80 | :param str constraints: Comma-separated list of constraints in the |
| 81 | form 'POOL,COUNT,SIZE' |
| 82 | |
| 83 | """ |
| 84 | raise NotImplementedError() |
| 85 | |
| 86 | def collect_metrics(self): |
| 87 | """Collect metrics on this unit. |
| 88 | |
| 89 | """ |
| 90 | raise NotImplementedError() |
| 91 | |
| 92 | async def destroy(self): |
| 93 | """Destroy this unit. |
| 94 | |
| 95 | """ |
| 96 | app_facade = client.ApplicationFacade.from_connection(self.connection) |
| 97 | |
| 98 | log.debug( |
| 99 | 'Destroying %s', self.name) |
| 100 | |
| 101 | return await app_facade.DestroyUnits([self.name]) |
| 102 | remove = destroy |
| 103 | |
| 104 | def get_resources(self, details=False): |
| 105 | """Return resources for this unit. |
| 106 | |
| 107 | :param bool details: Include detailed info about resources used by each |
| 108 | unit |
| 109 | |
| 110 | """ |
| 111 | raise NotImplementedError() |
| 112 | |
| 113 | def resolved(self, retry=False): |
| 114 | """Mark unit errors resolved. |
| 115 | |
| 116 | :param bool retry: Re-execute failed hooks |
| 117 | |
| 118 | """ |
| 119 | raise NotImplementedError() |
| 120 | |
| 121 | async def run(self, command, timeout=None): |
| 122 | """Run command on this unit. |
| 123 | |
| 124 | :param str command: The command to run |
| Adam Israel | b094366 | 2018-08-02 15:32:00 -0400 | [diff] [blame] | 125 | :param int timeout: Time, in seconds, to wait before command is considered failed |
| Adam Israel | dcdf82b | 2017-08-15 15:26:43 -0400 | [diff] [blame] | 126 | :returns: A :class:`juju.action.Action` instance. |
| 127 | |
| 128 | """ |
| 129 | action = client.ActionFacade.from_connection(self.connection) |
| 130 | |
| 131 | log.debug( |
| 132 | 'Running `%s` on %s', command, self.name) |
| 133 | |
| Adam Israel | b094366 | 2018-08-02 15:32:00 -0400 | [diff] [blame] | 134 | if timeout: |
| 135 | # Convert seconds to nanoseconds |
| 136 | timeout = int(timeout * 1000000000) |
| 137 | |
| Adam Israel | dcdf82b | 2017-08-15 15:26:43 -0400 | [diff] [blame] | 138 | res = await action.Run( |
| 139 | [], |
| 140 | command, |
| 141 | [], |
| 142 | timeout, |
| 143 | [self.name], |
| 144 | ) |
| 145 | return await self.model.wait_for_action(res.results[0].action.tag) |
| 146 | |
| 147 | async def run_action(self, action_name, **params): |
| 148 | """Run an action on this unit. |
| 149 | |
| 150 | :param str action_name: Name of action to run |
| 151 | :param \*\*params: Action parameters |
| 152 | :returns: A :class:`juju.action.Action` instance. |
| 153 | |
| 154 | Note that this only enqueues the action. You will need to call |
| 155 | ``action.wait()`` on the resulting `Action` instance if you wish |
| 156 | to block until the action is complete. |
| 157 | |
| 158 | """ |
| 159 | action_facade = client.ActionFacade.from_connection(self.connection) |
| 160 | |
| 161 | log.debug('Starting action `%s` on %s', action_name, self.name) |
| 162 | |
| 163 | res = await action_facade.Enqueue([client.Action( |
| 164 | name=action_name, |
| 165 | parameters=params, |
| 166 | receiver=self.tag, |
| 167 | )]) |
| 168 | action = res.results[0].action |
| 169 | error = res.results[0].error |
| 170 | if error and error.code == 'not found': |
| 171 | raise ValueError('Action `%s` not found on %s' % (action_name, |
| 172 | self.name)) |
| 173 | elif error: |
| 174 | raise Exception('Unknown action error: %s' % error.serialize()) |
| 175 | action_id = action.tag[len('action-'):] |
| 176 | log.debug('Action started as %s', action_id) |
| 177 | # we mustn't use wait_for_action because that blocks until the |
| 178 | # action is complete, rather than just being in the model |
| 179 | return await self.model._wait_for_new('action', action_id) |
| 180 | |
| 181 | async def scp_to(self, source, destination, user='ubuntu', proxy=False, |
| 182 | scp_opts=''): |
| 183 | """Transfer files to this unit. |
| 184 | |
| 185 | :param str source: Local path of file(s) to transfer |
| 186 | :param str destination: Remote destination of transferred files |
| 187 | :param str user: Remote username |
| 188 | :param bool proxy: Proxy through the Juju API server |
| 189 | :param str scp_opts: Additional options to the `scp` command |
| 190 | """ |
| 191 | await self.machine.scp_to(source, destination, user=user, proxy=proxy, |
| 192 | scp_opts=scp_opts) |
| 193 | |
| 194 | async def scp_from(self, source, destination, user='ubuntu', proxy=False, |
| 195 | scp_opts=''): |
| 196 | """Transfer files from this unit. |
| 197 | |
| 198 | :param str source: Remote path of file(s) to transfer |
| 199 | :param str destination: Local destination of transferred files |
| 200 | :param str user: Remote username |
| 201 | :param bool proxy: Proxy through the Juju API server |
| 202 | :param str scp_opts: Additional options to the `scp` command |
| 203 | """ |
| 204 | await self.machine.scp_from(source, destination, user=user, |
| 205 | proxy=proxy, scp_opts=scp_opts) |
| 206 | |
| 207 | def set_meter_status(self): |
| 208 | """Set the meter status on this unit. |
| 209 | |
| 210 | """ |
| 211 | raise NotImplementedError() |
| 212 | |
| 213 | def ssh( |
| 214 | self, command, user=None, proxy=False, ssh_opts=None): |
| 215 | """Execute a command over SSH on this unit. |
| 216 | |
| 217 | :param str command: Command to execute |
| 218 | :param str user: Remote username |
| 219 | :param bool proxy: Proxy through the Juju API server |
| 220 | :param str ssh_opts: Additional options to the `ssh` command |
| 221 | |
| 222 | """ |
| 223 | raise NotImplementedError() |
| 224 | |
| 225 | def status_history(self, num=20, utc=False): |
| 226 | """Get status history for this unit. |
| 227 | |
| 228 | :param int num: Size of history backlog |
| 229 | :param bool utc: Display time as UTC in RFC3339 format |
| 230 | |
| 231 | """ |
| 232 | raise NotImplementedError() |
| 233 | |
| 234 | async def is_leader_from_status(self): |
| 235 | """ |
| 236 | Check to see if this unit is the leader. Returns True if so, and |
| 237 | False if it is not, or if leadership does not make sense |
| 238 | (e.g., there is no leader in this application.) |
| 239 | |
| 240 | This method is a kluge that calls FullStatus in the |
| 241 | ClientFacade to get its information. Once |
| 242 | https://bugs.launchpad.net/juju/+bug/1643691 is resolved, we |
| 243 | should add a simple .is_leader property, and deprecate this |
| 244 | method. |
| 245 | |
| 246 | """ |
| 247 | app = self.name.split("/")[0] |
| 248 | |
| 249 | c = client.ClientFacade.from_connection(self.connection) |
| 250 | |
| 251 | status = await c.FullStatus(None) |
| 252 | |
| 253 | # FullStatus may be more up to date than our model, and the |
| 254 | # unit may have gone away, or we may be doing something silly, |
| 255 | # like trying to fetch leadership for a subordinate, which |
| 256 | # will not be filed where we expect in the model. In those |
| 257 | # cases, we may simply return False, as a nonexistent or |
| 258 | # subordinate unit is not a leader. |
| 259 | if not status.applications.get(app): |
| 260 | return False |
| 261 | |
| 262 | if not status.applications[app].get('units'): |
| 263 | return False |
| 264 | |
| 265 | if not status.applications[app]['units'].get(self.name): |
| 266 | return False |
| 267 | |
| 268 | return status.applications[app]['units'][self.name].get('leader', |
| 269 | False) |
| 270 | |
| 271 | async def get_metrics(self): |
| 272 | """Get metrics for the unit. |
| 273 | |
| 274 | :return: Dictionary of metrics for this unit. |
| 275 | |
| 276 | """ |
| 277 | metrics = await self.model.get_metrics(self.tag) |
| 278 | return metrics[self.name] |