620e9c998aff2f4c174a41a6fd29df2f4dd6d053
[osm/N2VC.git] / modules / libjuju / juju / application.py
1 # Copyright 2016 Canonical Ltd.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 import asyncio
16 import logging
17
18 from . import model
19 from .client import client
20 from .errors import JujuError
21 from .placement import parse as parse_placement
22
23 log = logging.getLogger(__name__)
24
25
26 class Application(model.ModelEntity):
27 @property
28 def _unit_match_pattern(self):
29 return r'^{}.*$'.format(self.entity_id)
30
31 def on_unit_add(self, callable_):
32 """Add a "unit added" observer to this entity, which will be called
33 whenever a unit is added to this application.
34
35 """
36 self.model.add_observer(
37 callable_, 'unit', 'add', self._unit_match_pattern)
38
39 def on_unit_remove(self, callable_):
40 """Add a "unit removed" observer to this entity, which will be called
41 whenever a unit is removed from this application.
42
43 """
44 self.model.add_observer(
45 callable_, 'unit', 'remove', self._unit_match_pattern)
46
47 @property
48 def units(self):
49 return [
50 unit for unit in self.model.units.values()
51 if unit.application == self.name
52 ]
53
54 @property
55 def status(self):
56 """Get the application status, as set by the charm's leader.
57
58 """
59 return self.safe_data['status']['current']
60
61 @property
62 def status_message(self):
63 """Get the application status message, as set by the charm's leader.
64
65 """
66 return self.safe_data['status']['message']
67
68 @property
69 def tag(self):
70 return 'application-%s' % self.name
71
72 async def add_relation(self, local_relation, remote_relation):
73 """Add a relation to another application.
74
75 :param str local_relation: Name of relation on this application
76 :param str remote_relation: Name of relation on the other
77 application in the form '<application>[:<relation_name>]'
78
79 """
80 if ':' not in local_relation:
81 local_relation = '{}:{}'.format(self.name, local_relation)
82
83 return await self.model.add_relation(local_relation, remote_relation)
84
85 async def add_unit(self, count=1, to=None):
86 """Add one or more units to this application.
87
88 :param int count: Number of units to add
89 :param str to: Placement directive, e.g.::
90 '23' - machine 23
91 'lxc:7' - new lxc container on machine 7
92 '24/lxc/3' - lxc container 3 or machine 24
93
94 If None, a new machine is provisioned.
95
96 """
97 app_facade = client.ApplicationFacade.from_connection(self.connection)
98
99 log.debug(
100 'Adding %s unit%s to %s',
101 count, '' if count == 1 else 's', self.name)
102
103 result = await app_facade.AddUnits(
104 application=self.name,
105 placement=parse_placement(to) if to else None,
106 num_units=count,
107 )
108
109 return await asyncio.gather(*[
110 asyncio.ensure_future(self.model._wait_for_new('unit', unit_id))
111 for unit_id in result.units
112 ])
113
114 add_units = add_unit
115
116 def allocate(self, budget, value):
117 """Allocate budget to this application.
118
119 :param str budget: Name of budget
120 :param int value: Budget limit
121
122 """
123 raise NotImplementedError()
124
125 def attach(self, resource_name, file_path):
126 """Upload a file as a resource for this application.
127
128 :param str resource: Name of the resource
129 :param str file_path: Path to the file to upload
130
131 """
132 raise NotImplementedError()
133
134 def collect_metrics(self):
135 """Collect metrics on this application.
136
137 """
138 raise NotImplementedError()
139
140 async def destroy_relation(self, local_relation, remote_relation):
141 """Remove a relation to another application.
142
143 :param str local_relation: Name of relation on this application
144 :param str remote_relation: Name of relation on the other
145 application in the form '<application>[:<relation_name>]'
146
147 """
148 if ':' not in local_relation:
149 local_relation = '{}:{}'.format(self.name, local_relation)
150
151 app_facade = client.ApplicationFacade.from_connection(self.connection)
152
153 log.debug(
154 'Destroying relation %s <-> %s', local_relation, remote_relation)
155
156 return await app_facade.DestroyRelation([
157 local_relation, remote_relation])
158 remove_relation = destroy_relation
159
160 async def destroy_unit(self, *unit_names):
161 """Destroy units by name.
162
163 """
164 return await self.model.destroy_units(*unit_names)
165 destroy_units = destroy_unit
166
167 async def destroy(self):
168 """Remove this application from the model.
169
170 """
171 app_facade = client.ApplicationFacade.from_connection(self.connection)
172
173 log.debug(
174 'Destroying %s', self.name)
175
176 return await app_facade.Destroy(self.name)
177 remove = destroy
178
179 async def expose(self):
180 """Make this application publicly available over the network.
181
182 """
183 app_facade = client.ApplicationFacade.from_connection(self.connection)
184
185 log.debug(
186 'Exposing %s', self.name)
187
188 return await app_facade.Expose(self.name)
189
190 async def get_config(self):
191 """Return the configuration settings dict for this application.
192
193 """
194 app_facade = client.ApplicationFacade.from_connection(self.connection)
195
196 log.debug(
197 'Getting config for %s', self.name)
198
199 return (await app_facade.Get(self.name)).config
200
201 async def get_constraints(self):
202 """Return the machine constraints dict for this application.
203
204 """
205 app_facade = client.ApplicationFacade.from_connection(self.connection)
206
207 log.debug(
208 'Getting constraints for %s', self.name)
209
210 result = (await app_facade.Get(self.name)).constraints
211 return vars(result) if result else result
212
213 def get_actions(self, schema=False):
214 """Get actions defined for this application.
215
216 :param bool schema: Return the full action schema
217
218 """
219 raise NotImplementedError()
220
221 def get_resources(self, details=False):
222 """Return resources for this application.
223
224 :param bool details: Include detailed info about resources used by each
225 unit
226
227 """
228 raise NotImplementedError()
229
230 async def run(self, command, timeout=None):
231 """Run command on all units for this application.
232
233 :param str command: The command to run
234 :param int timeout: Time to wait before command is considered failed
235
236 """
237 action = client.ActionFacade.from_connection(self.connection)
238
239 log.debug(
240 'Running `%s` on all units of %s', command, self.name)
241
242 # TODO this should return a list of Actions
243 return await action.Run(
244 [self.name],
245 command,
246 [],
247 timeout,
248 [],
249 )
250
251 async def set_annotations(self, annotations):
252 """Set annotations on this application.
253
254 :param annotations map[string]string: the annotations as key/value
255 pairs.
256
257 """
258 log.debug('Updating annotations on application %s', self.name)
259
260 self.ann_facade = client.AnnotationsFacade.from_connection(
261 self.connection)
262
263 ann = client.EntityAnnotations(
264 entity=self.tag,
265 annotations=annotations,
266 )
267 return await self.ann_facade.Set([ann])
268
269 async def set_config(self, config, to_default=False):
270 """Set configuration options for this application.
271
272 :param config: Dict of configuration to set
273 :param bool to_default: Set application options to default values
274
275 """
276 app_facade = client.ApplicationFacade.from_connection(self.connection)
277
278 log.debug(
279 'Setting config for %s: %s', self.name, config)
280
281 return await app_facade.Set(self.name, config)
282
283 async def set_constraints(self, constraints):
284 """Set machine constraints for this application.
285
286 :param dict constraints: Dict of machine constraints
287
288 """
289 app_facade = client.ApplicationFacade.from_connection(self.connection)
290
291 log.debug(
292 'Setting constraints for %s: %s', self.name, constraints)
293
294 return await app_facade.SetConstraints(self.name, constraints)
295
296 def set_meter_status(self, status, info=None):
297 """Set the meter status on this status.
298
299 :param str status: Meter status, e.g. 'RED', 'AMBER'
300 :param str info: Extra info message
301
302 """
303 raise NotImplementedError()
304
305 def set_plan(self, plan_name):
306 """Set the plan for this application, effective immediately.
307
308 :param str plan_name: Name of plan
309
310 """
311 raise NotImplementedError()
312
313 async def unexpose(self):
314 """Remove public availability over the network for this application.
315
316 """
317 app_facade = client.ApplicationFacade.from_connection(self.connection)
318
319 log.debug(
320 'Unexposing %s', self.name)
321
322 return await app_facade.Unexpose(self.name)
323
324 def update_allocation(self, allocation):
325 """Update existing allocation for this application.
326
327 :param int allocation: The allocation to set
328
329 """
330 raise NotImplementedError()
331
332 async def upgrade_charm(
333 self, channel=None, force_series=False, force_units=False,
334 path=None, resources=None, revision=None, switch=None):
335 """Upgrade the charm for this application.
336
337 :param str channel: Channel to use when getting the charm from the
338 charm store, e.g. 'development'
339 :param bool force_series: Upgrade even if series of deployed
340 application is not supported by the new charm
341 :param bool force_units: Upgrade all units immediately, even if in
342 error state
343 :param str path: Uprade to a charm located at path
344 :param dict resources: Dictionary of resource name/filepath pairs
345 :param int revision: Explicit upgrade revision
346 :param str switch: Crossgrade charm url
347
348 """
349 # TODO: Support local upgrades
350 if path is not None:
351 raise NotImplementedError("path option is not implemented")
352 if resources is not None:
353 raise NotImplementedError("resources option is not implemented")
354
355 if switch is not None and revision is not None:
356 raise ValueError("switch and revision are mutually exclusive")
357
358 client_facade = client.ClientFacade.from_connection(self.connection)
359 resources_facade = client.ResourcesFacade.from_connection(
360 self.connection)
361 app_facade = client.ApplicationFacade.from_connection(self.connection)
362
363 charmstore = self.model.charmstore
364 charmstore_entity = None
365
366 if switch is not None:
367 charm_url = switch
368 if not charm_url.startswith('cs:'):
369 charm_url = 'cs:' + charm_url
370 else:
371 charm_url = self.data['charm-url']
372 charm_url = charm_url.rpartition('-')[0]
373 if revision is not None:
374 charm_url = "%s-%d" % (charm_url, revision)
375 else:
376 charmstore_entity = await charmstore.entity(charm_url,
377 channel=channel)
378 charm_url = charmstore_entity['Id']
379
380 if charm_url == self.data['charm-url']:
381 raise JujuError('already running charm "%s"' % charm_url)
382
383 # Update charm
384 await client_facade.AddCharm(
385 url=charm_url,
386 channel=channel
387 )
388
389 # Update resources
390 if not charmstore_entity:
391 charmstore_entity = await charmstore.entity(charm_url,
392 channel=channel)
393 store_resources = charmstore_entity['Meta']['resources']
394
395 request_data = [client.Entity(self.tag)]
396 response = await resources_facade.ListResources(request_data)
397 existing_resources = {
398 resource.name: resource
399 for resource in response.results[0].resources
400 }
401
402 resources_to_update = [
403 resource for resource in store_resources
404 if resource['Name'] not in existing_resources or
405 existing_resources[resource['Name']].origin != 'upload'
406 ]
407
408 if resources_to_update:
409 request_data = [
410 client.CharmResource(
411 description=resource.get('Description'),
412 fingerprint=resource['Fingerprint'],
413 name=resource['Name'],
414 path=resource['Path'],
415 revision=resource['Revision'],
416 size=resource['Size'],
417 type_=resource['Type'],
418 origin='store',
419 ) for resource in resources_to_update
420 ]
421 response = await resources_facade.AddPendingResources(
422 self.tag,
423 charm_url,
424 request_data
425 )
426 pending_ids = response.pending_ids
427 resource_ids = {
428 resource['Name']: id
429 for resource, id in zip(resources_to_update, pending_ids)
430 }
431 else:
432 resource_ids = None
433
434 # Update application
435 await app_facade.SetCharm(
436 application=self.entity_id,
437 channel=channel,
438 charm_url=charm_url,
439 config_settings=None,
440 config_settings_yaml=None,
441 force_series=force_series,
442 force_units=force_units,
443 resource_ids=resource_ids,
444 storage_constraints=None
445 )
446
447 await self.model.block_until(
448 lambda: self.data['charm-url'] == charm_url
449 )
450
451 async def get_metrics(self):
452 """Get metrics for this application's units.
453
454 :return: Dictionary of unit_name:metrics
455
456 """
457 return await self.model.get_metrics(self.tag)