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