blob: 6e4aaf37e8bd013663eb4d03c6dbe766408cdb3f [file] [log] [blame]
Adam Israel5e08a0e2018-09-06 19:22:47 -04001import asyncio
Adam Israelc3e6c2e2018-03-01 09:31:50 -05002import logging
3import os
4import os.path
5import re
Adam Israelfa329072018-09-14 11:26:13 -04006import shlex
Adam Israelc3e6c2e2018-03-01 09:31:50 -05007import ssl
Adam Israelfa329072018-09-14 11:26:13 -04008import subprocess
Adam Israelc3e6c2e2018-03-01 09:31:50 -05009import sys
Adam Israel5e08a0e2018-09-06 19:22:47 -040010# import time
Adam Israelc3e6c2e2018-03-01 09:31:50 -050011
12# FIXME: this should load the juju inside or modules without having to
13# explicitly install it. Check why it's not working.
14# Load our subtree of the juju library
15path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
16path = os.path.join(path, "modules/libjuju/")
17if path not in sys.path:
18 sys.path.insert(1, path)
19
20from juju.controller import Controller
Adam Israel5e08a0e2018-09-06 19:22:47 -040021from juju.model import ModelObserver
Adam Israel6d84dbd2019-03-08 18:33:35 -050022from juju.errors import JujuAPIError, JujuError
Adam Israelc3e6c2e2018-03-01 09:31:50 -050023
24# We might need this to connect to the websocket securely, but test and verify.
25try:
26 ssl._create_default_https_context = ssl._create_unverified_context
27except AttributeError:
28 # Legacy Python doesn't verify by default (see pep-0476)
29 # https://www.python.org/dev/peps/pep-0476/
30 pass
31
32
33# Custom exceptions
34class JujuCharmNotFound(Exception):
35 """The Charm can't be found or is not readable."""
36
37
38class JujuApplicationExists(Exception):
39 """The Application already exists."""
40
Adam Israelb5214512018-05-03 10:00:04 -040041
Adam Israel88a49632018-04-10 13:04:57 -060042class N2VCPrimitiveExecutionFailed(Exception):
43 """Something failed while attempting to execute a primitive."""
44
Adam Israelc3e6c2e2018-03-01 09:31:50 -050045
Adam Israel6d84dbd2019-03-08 18:33:35 -050046class NetworkServiceDoesNotExist(Exception):
47 """The Network Service being acted against does not exist."""
48
49
Adam Israelc3e6c2e2018-03-01 09:31:50 -050050# Quiet the debug logging
51logging.getLogger('websockets.protocol').setLevel(logging.INFO)
52logging.getLogger('juju.client.connection').setLevel(logging.WARN)
53logging.getLogger('juju.model').setLevel(logging.WARN)
54logging.getLogger('juju.machine').setLevel(logging.WARN)
55
Adam Israelb5214512018-05-03 10:00:04 -040056
Adam Israelc3e6c2e2018-03-01 09:31:50 -050057class VCAMonitor(ModelObserver):
58 """Monitor state changes within the Juju Model."""
Adam Israelc3e6c2e2018-03-01 09:31:50 -050059 log = None
60 ns_name = None
Adam Israel28a43c02018-04-23 16:04:54 -040061 applications = {}
Adam Israelc3e6c2e2018-03-01 09:31:50 -050062
Adam Israel28a43c02018-04-23 16:04:54 -040063 def __init__(self, ns_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050064 self.log = logging.getLogger(__name__)
65
66 self.ns_name = ns_name
Adam Israel28a43c02018-04-23 16:04:54 -040067
68 def AddApplication(self, application_name, callback, *callback_args):
69 if application_name not in self.applications:
70 self.applications[application_name] = {
71 'callback': callback,
72 'callback_args': callback_args
73 }
74
75 def RemoveApplication(self, application_name):
76 if application_name in self.applications:
77 del self.applications[application_name]
Adam Israelc3e6c2e2018-03-01 09:31:50 -050078
79 async def on_change(self, delta, old, new, model):
80 """React to changes in the Juju model."""
81
82 if delta.entity == "unit":
Adam Israel28a43c02018-04-23 16:04:54 -040083 # Ignore change events from other applications
84 if delta.data['application'] not in self.applications.keys():
85 return
86
Adam Israelc3e6c2e2018-03-01 09:31:50 -050087 try:
Adam Israel28a43c02018-04-23 16:04:54 -040088
89 application_name = delta.data['application']
90
91 callback = self.applications[application_name]['callback']
Adam Israel5e08a0e2018-09-06 19:22:47 -040092 callback_args = \
93 self.applications[application_name]['callback_args']
Adam Israel28a43c02018-04-23 16:04:54 -040094
Adam Israelc3e6c2e2018-03-01 09:31:50 -050095 if old and new:
Adam Israelfc511ed2018-09-21 14:20:55 +020096 # Fire off a callback with the application state
97 if callback:
98 callback(
99 self.ns_name,
100 delta.data['application'],
101 new.workload_status,
102 new.workload_status_message,
103 *callback_args)
Adam Israel28a43c02018-04-23 16:04:54 -0400104
105 if old and not new:
106 # This is a charm being removed
107 if callback:
108 callback(
109 self.ns_name,
110 delta.data['application'],
111 "removed",
Adam Israel9562f432018-05-09 13:55:28 -0400112 "",
Adam Israel28a43c02018-04-23 16:04:54 -0400113 *callback_args)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500114 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400115 self.log.debug("[1] notify_callback exception: {}".format(e))
116
Adam Israel88a49632018-04-10 13:04:57 -0600117 elif delta.entity == "action":
118 # TODO: Decide how we want to notify the user of actions
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500119
Adam Israel88a49632018-04-10 13:04:57 -0600120 # uuid = delta.data['id'] # The Action's unique id
121 # msg = delta.data['message'] # The output of the action
122 #
123 # if delta.data['status'] == "pending":
124 # # The action is queued
125 # pass
126 # elif delta.data['status'] == "completed""
127 # # The action was successful
128 # pass
129 # elif delta.data['status'] == "failed":
130 # # The action failed.
131 # pass
132
133 pass
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500134
135########
136# TODO
137#
138# Create unique models per network service
139# Document all public functions
140
Adam Israelb5214512018-05-03 10:00:04 -0400141
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500142class N2VC:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500143 def __init__(self,
144 log=None,
145 server='127.0.0.1',
146 port=17070,
147 user='admin',
148 secret=None,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400149 artifacts=None,
150 loop=None,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500151 ):
152 """Initialize N2VC
153
154 :param vcaconfig dict A dictionary containing the VCA configuration
155
156 :param artifacts str The directory where charms required by a vnfd are
157 stored.
158
159 :Example:
160 n2vc = N2VC(vcaconfig={
161 'secret': 'MzI3MDJhOTYxYmM0YzRjNTJiYmY1Yzdm',
162 'user': 'admin',
163 'ip-address': '10.44.127.137',
164 'port': 17070,
165 'artifacts': '/path/to/charms'
166 })
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500167 """
168
Adam Israel5e08a0e2018-09-06 19:22:47 -0400169 # Initialize instance-level variables
170 self.api = None
171 self.log = None
172 self.controller = None
173 self.connecting = False
174 self.authenticated = False
175
Adam Israelfc511ed2018-09-21 14:20:55 +0200176 # For debugging
177 self.refcount = {
178 'controller': 0,
179 'model': 0,
180 }
181
Adam Israel5e08a0e2018-09-06 19:22:47 -0400182 self.models = {}
Adam Israel5e08a0e2018-09-06 19:22:47 -0400183
184 # Model Observers
185 self.monitors = {}
186
187 # VCA config
188 self.hostname = ""
189 self.port = 17070
190 self.username = ""
191 self.secret = ""
192
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500193 if log:
194 self.log = log
195 else:
196 self.log = logging.getLogger(__name__)
197
198 # Quiet websocket traffic
199 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
200 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
201 logging.getLogger('model').setLevel(logging.WARN)
202 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
203
204 self.log.debug('JujuApi: instantiated')
205
206 self.server = server
207 self.port = port
208
209 self.secret = secret
210 if user.startswith('user-'):
211 self.user = user
212 else:
213 self.user = 'user-{}'.format(user)
214
215 self.endpoint = '%s:%d' % (server, int(port))
216
217 self.artifacts = artifacts
218
Adam Israel5e08a0e2018-09-06 19:22:47 -0400219 self.loop = loop or asyncio.get_event_loop()
220
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500221 def __del__(self):
222 """Close any open connections."""
223 yield self.logout()
224
Adam Israel5e08a0e2018-09-06 19:22:47 -0400225 def notify_callback(self, model_name, application_name, status, message,
226 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500227 try:
228 if callback:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400229 callback(
230 model_name,
231 application_name,
232 status, message,
233 *callback_args,
234 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500235 except Exception as e:
236 self.log.error("[0] notify_callback exception {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600237 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500238 return True
239
240 # Public methods
Adam Israel85a4b212018-11-29 20:30:24 -0500241 async def Relate(self, model_name, vnfd):
Adam Israel136186e2018-09-14 12:01:12 -0400242 """Create a relation between the charm-enabled VDUs in a VNF.
243
244 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
245
246 vdu:
247 ...
248 relation:
249 - provides: dataVM:db
250 requires: mgmtVM:app
251
252 This tells N2VC that the charm referred to by the dataVM vdu offers a relation named 'db', and the mgmtVM vdu has an 'app' endpoint that should be connected to a database.
253
254 :param str ns_name: The name of the network service.
255 :param dict vnfd: The parsed yaml VNF descriptor.
256 """
257
258 # Currently, the call to Relate() is made automatically after the
259 # deployment of each charm; if the relation depends on a charm that
260 # hasn't been deployed yet, the call will fail silently. This will
261 # prevent an API breakage, with the intent of making this an explicitly
262 # required call in a more object-oriented refactor of the N2VC API.
263
264 configs = []
265 vnf_config = vnfd.get("vnf-configuration")
266 if vnf_config:
267 juju = vnf_config['juju']
268 if juju:
269 configs.append(vnf_config)
270
271 for vdu in vnfd['vdu']:
272 vdu_config = vdu.get('vdu-configuration')
273 if vdu_config:
274 juju = vdu_config['juju']
275 if juju:
276 configs.append(vdu_config)
277
278 def _get_application_name(name):
279 """Get the application name that's mapped to a vnf/vdu."""
280 vnf_member_index = 0
281 vnf_name = vnfd['name']
282
283 for vdu in vnfd.get('vdu'):
284 # Compare the named portion of the relation to the vdu's id
285 if vdu['id'] == name:
286 application_name = self.FormatApplicationName(
Adam Israel85a4b212018-11-29 20:30:24 -0500287 model_name,
Adam Israel136186e2018-09-14 12:01:12 -0400288 vnf_name,
289 str(vnf_member_index),
290 )
291 return application_name
292 else:
293 vnf_member_index += 1
294
295 return None
296
297 # Loop through relations
298 for cfg in configs:
299 if 'juju' in cfg:
300 if 'relation' in juju:
301 for rel in juju['relation']:
302 try:
303
304 # get the application name for the provides
305 (name, endpoint) = rel['provides'].split(':')
306 application_name = _get_application_name(name)
307
308 provides = "{}:{}".format(
309 application_name,
310 endpoint
311 )
312
313 # get the application name for thr requires
314 (name, endpoint) = rel['requires'].split(':')
315 application_name = _get_application_name(name)
316
317 requires = "{}:{}".format(
318 application_name,
319 endpoint
320 )
321 self.log.debug("Relation: {} <-> {}".format(
322 provides,
323 requires
324 ))
325 await self.add_relation(
Adam Israel85a4b212018-11-29 20:30:24 -0500326 model_name,
Adam Israel136186e2018-09-14 12:01:12 -0400327 provides,
328 requires,
329 )
330 except Exception as e:
331 self.log.debug("Exception: {}".format(e))
332
333 return
334
Adam Israel5e08a0e2018-09-06 19:22:47 -0400335 async def DeployCharms(self, model_name, application_name, vnfd,
336 charm_path, params={}, machine_spec={},
337 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500338 """Deploy one or more charms associated with a VNF.
339
340 Deploy the charm(s) referenced in a VNF Descriptor.
341
Adam Israel85a4b212018-11-29 20:30:24 -0500342 :param str model_name: The name or unique id of the network service.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500343 :param str application_name: The name of the application
344 :param dict vnfd: The name of the application
345 :param str charm_path: The path to the Juju charm
346 :param dict params: A dictionary of runtime parameters
347 Examples::
348 {
Adam Israel88a49632018-04-10 13:04:57 -0600349 'rw_mgmt_ip': '1.2.3.4',
350 # Pass the initial-config-primitives section of the vnf or vdu
351 'initial-config-primitives': {...}
tierno1afb30a2018-12-21 13:42:43 +0000352 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
353 inside < >. rw_mgmt_ip will be included here also
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500354 }
Adam Israel5e08a0e2018-09-06 19:22:47 -0400355 :param dict machine_spec: A dictionary describing the machine to
356 install to
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500357 Examples::
358 {
359 'hostname': '1.2.3.4',
360 'username': 'ubuntu',
361 }
362 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400363 :param tuple callback_args: A list of arguments to be passed to the
364 callback
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500365 """
366
367 ########################################################
368 # Verify the path to the charm exists and is readable. #
369 ########################################################
370 if not os.path.exists(charm_path):
371 self.log.debug("Charm path doesn't exist: {}".format(charm_path))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400372 self.notify_callback(
373 model_name,
374 application_name,
375 "failed",
376 callback,
377 *callback_args,
378 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500379 raise JujuCharmNotFound("No artifacts configured.")
380
381 ################################
382 # Login to the Juju controller #
383 ################################
384 if not self.authenticated:
385 self.log.debug("Authenticating with Juju")
386 await self.login()
387
388 ##########################################
389 # Get the model for this network service #
390 ##########################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500391 model = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500392
393 ########################################
394 # Verify the application doesn't exist #
395 ########################################
396 app = await self.get_application(model, application_name)
397 if app:
Adam Israel42d88e62018-07-16 14:18:41 -0400398 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500399
Adam Israel28a43c02018-04-23 16:04:54 -0400400 ################################################################
401 # Register this application with the model-level event monitor #
402 ################################################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500403 if callback:
Adam Israel28a43c02018-04-23 16:04:54 -0400404 self.monitors[model_name].AddApplication(
405 application_name,
406 callback,
407 *callback_args
408 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500409
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500410 ########################################################
411 # Check for specific machine placement (native charms) #
412 ########################################################
413 to = ""
414 if machine_spec.keys():
Adam Israel5963cb42018-09-14 11:26:13 -0400415 if all(k in machine_spec for k in ['host', 'user']):
416 # Enlist an existing machine as a Juju unit
417 machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
418 machine_spec['user'],
419 machine_spec['host'],
420 self.GetPrivateKeyPath(),
421 ))
Adam Israelfa329072018-09-14 11:26:13 -0400422 to = machine.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500423
424 #######################################
425 # Get the initial charm configuration #
426 #######################################
427
428 rw_mgmt_ip = None
429 if 'rw_mgmt_ip' in params:
430 rw_mgmt_ip = params['rw_mgmt_ip']
431
Adam Israel5afe0542018-08-08 12:54:55 -0400432 if 'initial-config-primitive' not in params:
433 params['initial-config-primitive'] = {}
434
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500435 initial_config = self._get_config_from_dict(
Adam Israel88a49632018-04-10 13:04:57 -0600436 params['initial-config-primitive'],
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500437 {'<rw_mgmt_ip>': rw_mgmt_ip}
438 )
439
Adam Israel85a4b212018-11-29 20:30:24 -0500440 self.log.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
441 model_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500442 application_name,
443 charm_path,
444 to=to,
445 ))
446
447 ########################################################
448 # Deploy the charm and apply the initial configuration #
449 ########################################################
450 app = await model.deploy(
Adam Israel88a49632018-04-10 13:04:57 -0600451 # We expect charm_path to be either the path to the charm on disk
452 # or in the format of cs:series/name
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500453 charm_path,
Adam Israel88a49632018-04-10 13:04:57 -0600454 # This is the formatted, unique name for this charm
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500455 application_name=application_name,
Adam Israel88a49632018-04-10 13:04:57 -0600456 # Proxy charms should use the current LTS. This will need to be
457 # changed for native charms.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500458 series='xenial',
Adam Israel88a49632018-04-10 13:04:57 -0600459 # Apply the initial 'config' primitive during deployment
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500460 config=initial_config,
Adam Israelfa329072018-09-14 11:26:13 -0400461 # Where to deploy the charm to.
462 to=to,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500463 )
464
Adam Israel136186e2018-09-14 12:01:12 -0400465 # Map the vdu id<->app name,
466 #
467 await self.Relate(model_name, vnfd)
468
Adam Israel88a49632018-04-10 13:04:57 -0600469 # #######################################
470 # # Execute initial config primitive(s) #
471 # #######################################
Adam Israelcf253202018-10-31 16:29:09 -0700472 uuids = await self.ExecuteInitialPrimitives(
Adam Israel5e08a0e2018-09-06 19:22:47 -0400473 model_name,
474 application_name,
475 params,
476 )
Adam Israelcf253202018-10-31 16:29:09 -0700477 return uuids
Adam Israel5e08a0e2018-09-06 19:22:47 -0400478
479 # primitives = {}
480 #
481 # # Build a sequential list of the primitives to execute
482 # for primitive in params['initial-config-primitive']:
483 # try:
484 # if primitive['name'] == 'config':
485 # # This is applied when the Application is deployed
486 # pass
487 # else:
488 # seq = primitive['seq']
489 #
490 # params = {}
491 # if 'parameter' in primitive:
492 # params = primitive['parameter']
493 #
494 # primitives[seq] = {
495 # 'name': primitive['name'],
496 # 'parameters': self._map_primitive_parameters(
497 # params,
498 # {'<rw_mgmt_ip>': rw_mgmt_ip}
499 # ),
500 # }
501 #
502 # for primitive in sorted(primitives):
503 # await self.ExecutePrimitive(
504 # model_name,
505 # application_name,
506 # primitives[primitive]['name'],
507 # callback,
508 # callback_args,
509 # **primitives[primitive]['parameters'],
510 # )
511 # except N2VCPrimitiveExecutionFailed as e:
512 # self.log.debug(
513 # "[N2VC] Exception executing primitive: {}".format(e)
514 # )
515 # raise
516
517 async def GetPrimitiveStatus(self, model_name, uuid):
518 """Get the status of an executed Primitive.
519
520 The status of an executed Primitive will be one of three values:
521 - completed
522 - failed
523 - running
524 """
525 status = None
526 try:
527 if not self.authenticated:
528 await self.login()
529
Adam Israel5e08a0e2018-09-06 19:22:47 -0400530 model = await self.get_model(model_name)
531
532 results = await model.get_action_status(uuid)
533
534 if uuid in results:
535 status = results[uuid]
536
537 except Exception as e:
538 self.log.debug(
539 "Caught exception while getting primitive status: {}".format(e)
540 )
541 raise N2VCPrimitiveExecutionFailed(e)
542
543 return status
544
545 async def GetPrimitiveOutput(self, model_name, uuid):
546 """Get the output of an executed Primitive.
547
548 Note: this only returns output for a successfully executed primitive.
549 """
550 results = None
551 try:
552 if not self.authenticated:
553 await self.login()
554
Adam Israel5e08a0e2018-09-06 19:22:47 -0400555 model = await self.get_model(model_name)
556 results = await model.get_action_output(uuid, 60)
557 except Exception as e:
558 self.log.debug(
559 "Caught exception while getting primitive status: {}".format(e)
560 )
561 raise N2VCPrimitiveExecutionFailed(e)
562
563 return results
564
Adam Israelfa329072018-09-14 11:26:13 -0400565 # async def ProvisionMachine(self, model_name, hostname, username):
566 # """Provision machine for usage with Juju.
567 #
568 # Provisions a previously instantiated machine for use with Juju.
569 # """
570 # try:
571 # if not self.authenticated:
572 # await self.login()
573 #
574 # # FIXME: This is hard-coded until model-per-ns is added
575 # model_name = 'default'
576 #
577 # model = await self.get_model(model_name)
578 # model.add_machine(spec={})
579 #
580 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
581 # "ubuntu",
582 # host['address'],
583 # private_key_path,
584 # ))
585 # return machine.id
586 #
587 # except Exception as e:
588 # self.log.debug(
589 # "Caught exception while getting primitive status: {}".format(e)
590 # )
591 # raise N2VCPrimitiveExecutionFailed(e)
592
593 def GetPrivateKeyPath(self):
594 homedir = os.environ['HOME']
595 sshdir = "{}/.ssh".format(homedir)
596 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
597 return private_key_path
598
599 async def GetPublicKey(self):
600 """Get the N2VC SSH public key.abs
601
602 Returns the SSH public key, to be injected into virtual machines to
603 be managed by the VCA.
604
605 The first time this is run, a ssh keypair will be created. The public
606 key is injected into a VM so that we can provision the machine with
607 Juju, after which Juju will communicate with the VM directly via the
608 juju agent.
609 """
610 public_key = ""
611
612 # Find the path to where we expect our key to live.
613 homedir = os.environ['HOME']
614 sshdir = "{}/.ssh".format(homedir)
615 if not os.path.exists(sshdir):
616 os.mkdir(sshdir)
617
618 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
619 public_key_path = "{}.pub".format(private_key_path)
620
621 # If we don't have a key generated, generate it.
622 if not os.path.exists(private_key_path):
623 cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format(
624 "rsa",
625 "4096",
626 private_key_path
627 )
628 subprocess.check_output(shlex.split(cmd))
629
630 # Read the public key
631 with open(public_key_path, "r") as f:
632 public_key = f.readline()
633
634 return public_key
635
Adam Israel5e08a0e2018-09-06 19:22:47 -0400636 async def ExecuteInitialPrimitives(self, model_name, application_name,
637 params, callback=None, *callback_args):
638 """Execute multiple primitives.
639
640 Execute multiple primitives as declared in initial-config-primitive.
641 This is useful in cases where the primitives initially failed -- for
642 example, if the charm is a proxy but the proxy hasn't been configured
643 yet.
644 """
645 uuids = []
Adam Israel88a49632018-04-10 13:04:57 -0600646 primitives = {}
647
648 # Build a sequential list of the primitives to execute
649 for primitive in params['initial-config-primitive']:
650 try:
651 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600652 pass
653 else:
Adam Israel88a49632018-04-10 13:04:57 -0600654 seq = primitive['seq']
655
tierno1afb30a2018-12-21 13:42:43 +0000656 params_ = {}
Adam Israel42d88e62018-07-16 14:18:41 -0400657 if 'parameter' in primitive:
tierno1afb30a2018-12-21 13:42:43 +0000658 params_ = primitive['parameter']
659
660 user_values = params.get("user_values", {})
661 if 'rw_mgmt_ip' not in user_values:
662 user_values['rw_mgmt_ip'] = None
663 # just for backward compatibility, because it will be provided always by modern version of LCM
Adam Israel42d88e62018-07-16 14:18:41 -0400664
Adam Israel88a49632018-04-10 13:04:57 -0600665 primitives[seq] = {
666 'name': primitive['name'],
667 'parameters': self._map_primitive_parameters(
tierno1afb30a2018-12-21 13:42:43 +0000668 params_,
669 user_values
Adam Israel88a49632018-04-10 13:04:57 -0600670 ),
671 }
672
673 for primitive in sorted(primitives):
Adam Israel5e08a0e2018-09-06 19:22:47 -0400674 uuids.append(
675 await self.ExecutePrimitive(
676 model_name,
677 application_name,
678 primitives[primitive]['name'],
679 callback,
680 callback_args,
681 **primitives[primitive]['parameters'],
682 )
Adam Israel88a49632018-04-10 13:04:57 -0600683 )
684 except N2VCPrimitiveExecutionFailed as e:
Adam Israel7d871fb2018-07-17 12:17:06 -0400685 self.log.debug(
Adam Israel88a49632018-04-10 13:04:57 -0600686 "[N2VC] Exception executing primitive: {}".format(e)
687 )
688 raise
Adam Israel5e08a0e2018-09-06 19:22:47 -0400689 return uuids
Adam Israel88a49632018-04-10 13:04:57 -0600690
Adam Israel5e08a0e2018-09-06 19:22:47 -0400691 async def ExecutePrimitive(self, model_name, application_name, primitive,
692 callback, *callback_args, **params):
Adam Israelc9df96f2018-05-03 14:49:56 -0400693 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
Adam Israel6817f612018-04-13 08:41:43 -0600694
Adam Israelc9df96f2018-05-03 14:49:56 -0400695 Execute a primitive defined in the VNF descriptor.
696
Adam Israel85a4b212018-11-29 20:30:24 -0500697 :param str model_name: The name or unique id of the network service.
Adam Israelc9df96f2018-05-03 14:49:56 -0400698 :param str application_name: The name of the application
699 :param str primitive: The name of the primitive to execute.
700 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400701 :param tuple callback_args: A list of arguments to be passed to the
702 callback function.
703 :param dict params: A dictionary of key=value pairs representing the
704 primitive's parameters
Adam Israelc9df96f2018-05-03 14:49:56 -0400705 Examples::
706 {
707 'rw_mgmt_ip': '1.2.3.4',
708 # Pass the initial-config-primitives section of the vnf or vdu
709 'initial-config-primitives': {...}
710 }
Adam Israel6817f612018-04-13 08:41:43 -0600711 """
tierno1afb30a2018-12-21 13:42:43 +0000712 self.log.debug("Executing primitive={} params={}".format(primitive, params))
Adam Israel6817f612018-04-13 08:41:43 -0600713 uuid = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500714 try:
715 if not self.authenticated:
716 await self.login()
717
Adam Israel5e08a0e2018-09-06 19:22:47 -0400718 model = await self.get_model(model_name)
Adam Israelb5214512018-05-03 10:00:04 -0400719
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500720 if primitive == 'config':
721 # config is special, and expecting params to be a dictionary
Adam Israelb0943662018-08-02 15:32:00 -0400722 await self.set_config(
723 model,
724 application_name,
725 params['params'],
726 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500727 else:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500728 app = await self.get_application(model, application_name)
729 if app:
730 # Run against the first (and probably only) unit in the app
731 unit = app.units[0]
732 if unit:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500733 action = await unit.run_action(primitive, **params)
Adam Israel6817f612018-04-13 08:41:43 -0600734 uuid = action.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500735 except Exception as e:
Adam Israelb0943662018-08-02 15:32:00 -0400736 self.log.debug(
737 "Caught exception while executing primitive: {}".format(e)
738 )
Adam Israel7d871fb2018-07-17 12:17:06 -0400739 raise N2VCPrimitiveExecutionFailed(e)
Adam Israel6817f612018-04-13 08:41:43 -0600740 return uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500741
Adam Israel5e08a0e2018-09-06 19:22:47 -0400742 async def RemoveCharms(self, model_name, application_name, callback=None,
743 *callback_args):
Adam Israelc9df96f2018-05-03 14:49:56 -0400744 """Remove a charm from the VCA.
745
746 Remove a charm referenced in a VNF Descriptor.
747
748 :param str model_name: The name of the network service.
749 :param str application_name: The name of the application
750 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400751 :param tuple callback_args: A list of arguments to be passed to the
752 callback function.
Adam Israelc9df96f2018-05-03 14:49:56 -0400753 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500754 try:
755 if not self.authenticated:
756 await self.login()
757
758 model = await self.get_model(model_name)
759 app = await self.get_application(model, application_name)
760 if app:
Adam Israel28a43c02018-04-23 16:04:54 -0400761 # Remove this application from event monitoring
762 self.monitors[model_name].RemoveApplication(application_name)
763
764 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400765 self.log.debug(
766 "Removing the application {}".format(application_name)
767 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500768 await app.remove()
Adam Israel28a43c02018-04-23 16:04:54 -0400769
Adam Israel85a4b212018-11-29 20:30:24 -0500770 await self.disconnect_model(self.monitors[model_name])
771
Adam Israel28a43c02018-04-23 16:04:54 -0400772 # Notify the callback that this charm has been removed.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400773 self.notify_callback(
774 model_name,
775 application_name,
776 "removed",
777 callback,
778 *callback_args,
779 )
Adam Israel28a43c02018-04-23 16:04:54 -0400780
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500781 except Exception as e:
782 print("Caught exception: {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600783 self.log.debug(e)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500784 raise e
785
Adam Israel6d84dbd2019-03-08 18:33:35 -0500786 async def CreateNetworkService(self, ns_uuid):
787 """Create a new Juju model for the Network Service.
788
789 Creates a new Model in the Juju Controller.
790
791 :param str ns_uuid: A unique id representing an instaance of a
792 Network Service.
793
794 :returns: True if the model was created. Raises JujuError on failure.
795 """
796 if not self.authenticated:
797 await self.login()
798
799 models = await self.controller.list_models()
800 if ns_uuid not in models:
801 try:
802 self.models[ns_uuid] = await self.controller.add_model(
803 ns_uuid
804 )
805 except JujuError as e:
806 if "already exists" not in e.message:
807 raise e
808 return True
809
810 async def DestroyNetworkService(self, ns_uuid):
811 """Destroy a Network Service.
812
813 Destroy the Network Service and any deployed charms.
814
815 :param ns_uuid The unique id of the Network Service
816
817 :returns: True if the model was created. Raises JujuError on failure.
818 """
819
820 # Do not delete the default model. The default model was used by all
821 # Network Services, prior to the implementation of a model per NS.
822 if ns_uuid.lower() is "default":
823 return False
824
825 if not self.authenticated:
826 self.log.debug("Authenticating with Juju")
827 await self.login()
828
829 # Disconnect from the Model
830 if ns_uuid in self.models:
831 await self.disconnect_model(self.models[ns_uuid])
832
833 try:
834 await self.controller.destroy_models(ns_uuid)
835 except JujuError as e:
836 raise NetworkServiceDoesNotExist(
837 "The Network Service '{}' does not exist".format(ns_uuid)
838 )
839
840 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500841
Adam Israelb5214512018-05-03 10:00:04 -0400842 async def GetMetrics(self, model_name, application_name):
843 """Get the metrics collected by the VCA.
844
Adam Israel85a4b212018-11-29 20:30:24 -0500845 :param model_name The name or unique id of the network service
Adam Israelb5214512018-05-03 10:00:04 -0400846 :param application_name The name of the application
847 """
848 metrics = {}
849 model = await self.get_model(model_name)
850 app = await self.get_application(model, application_name)
851 if app:
852 metrics = await app.get_metrics()
853
854 return metrics
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500855
Adam Israelfa329072018-09-14 11:26:13 -0400856 async def HasApplication(self, model_name, application_name):
857 model = await self.get_model(model_name)
858 app = await self.get_application(model, application_name)
859 if app:
860 return True
861 return False
862
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500863 # Non-public methods
Adam Israel136186e2018-09-14 12:01:12 -0400864 async def add_relation(self, model_name, relation1, relation2):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500865 """
866 Add a relation between two application endpoints.
867
Adam Israel85a4b212018-11-29 20:30:24 -0500868 :param str model_name: The name or unique id of the network service
869 :param str relation1: '<application>[:<relation_name>]'
870 :param str relation2: '<application>[:<relation_name>]'
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500871 """
Adam Israel136186e2018-09-14 12:01:12 -0400872
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500873 if not self.authenticated:
874 await self.login()
875
Adam Israel136186e2018-09-14 12:01:12 -0400876 m = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500877 try:
Adam Israel136186e2018-09-14 12:01:12 -0400878 await m.add_relation(relation1, relation2)
879 except JujuAPIError as e:
880 # If one of the applications in the relationship doesn't exist,
881 # or the relation has already been added, let the operation fail
882 # silently.
883 if 'not found' in e.message:
884 return
885 if 'already exists' in e.message:
886 return
887
888 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500889
Adam Israelb5214512018-05-03 10:00:04 -0400890 # async def apply_config(self, config, application):
891 # """Apply a configuration to the application."""
892 # print("JujuApi: Applying configuration to {}.".format(
893 # application
894 # ))
895 # return await self.set_config(application=application, config=config)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500896
897 def _get_config_from_dict(self, config_primitive, values):
Adam Israel88a49632018-04-10 13:04:57 -0600898 """Transform the yang config primitive to dict.
899
900 Expected result:
901
902 config = {
903 'config':
904 }
905 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500906 config = {}
907 for primitive in config_primitive:
908 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600909 # config = self._map_primitive_parameters()
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500910 for parameter in primitive['parameter']:
911 param = str(parameter['name'])
912 if parameter['value'] == "<rw_mgmt_ip>":
913 config[param] = str(values[parameter['value']])
914 else:
915 config[param] = str(parameter['value'])
916
917 return config
918
tierno1afb30a2018-12-21 13:42:43 +0000919 def _map_primitive_parameters(self, parameters, user_values):
Adam Israel88a49632018-04-10 13:04:57 -0600920 params = {}
921 for parameter in parameters:
922 param = str(parameter['name'])
tierno1afb30a2018-12-21 13:42:43 +0000923 value = parameter.get('value')
924
925 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
926 # Must exist at user_values except if there is a default value
927 if isinstance(value, str) and value.startswith("<") and value.endswith(">"):
928 if parameter['value'][1:-1] in user_values:
929 value = user_values[parameter['value'][1:-1]]
930 elif 'default-value' in parameter:
931 value = parameter['default-value']
932 else:
933 raise KeyError("parameter {}='{}' not supplied ".format(param, value))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400934
Adam Israelbf793522018-11-20 13:54:13 -0500935 # If there's no value, use the default-value (if set)
tierno1afb30a2018-12-21 13:42:43 +0000936 if value is None and 'default-value' in parameter:
Adam Israelbf793522018-11-20 13:54:13 -0500937 value = parameter['default-value']
938
Adam Israel5e08a0e2018-09-06 19:22:47 -0400939 # Typecast parameter value, if present
tierno1afb30a2018-12-21 13:42:43 +0000940 paramtype = "string"
941 try:
942 if 'data-type' in parameter:
943 paramtype = str(parameter['data-type']).lower()
Adam Israel5e08a0e2018-09-06 19:22:47 -0400944
tierno1afb30a2018-12-21 13:42:43 +0000945 if paramtype == "integer":
946 value = int(value)
947 elif paramtype == "boolean":
948 value = bool(value)
949 else:
950 value = str(value)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400951 else:
tierno1afb30a2018-12-21 13:42:43 +0000952 # If there's no data-type, assume the value is a string
953 value = str(value)
954 except ValueError:
955 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param, value, paramtype))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400956
tierno1afb30a2018-12-21 13:42:43 +0000957 params[param] = value
Adam Israel88a49632018-04-10 13:04:57 -0600958 return params
959
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500960 def _get_config_from_yang(self, config_primitive, values):
961 """Transform the yang config primitive to dict."""
962 config = {}
963 for primitive in config_primitive.values():
964 if primitive['name'] == 'config':
965 for parameter in primitive['parameter'].values():
966 param = str(parameter['name'])
967 if parameter['value'] == "<rw_mgmt_ip>":
968 config[param] = str(values[parameter['value']])
969 else:
970 config[param] = str(parameter['value'])
971
972 return config
973
974 def FormatApplicationName(self, *args):
975 """
976 Generate a Juju-compatible Application name
977
978 :param args tuple: Positional arguments to be used to construct the
979 application name.
980
981 Limitations::
982 - Only accepts characters a-z and non-consequitive dashes (-)
983 - Application name should not exceed 50 characters
984
985 Examples::
986
987 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
988 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500989 appname = ""
990 for c in "-".join(list(args)):
991 if c.isdigit():
992 c = chr(97 + int(c))
993 elif not c.isalpha():
994 c = "-"
995 appname += c
Adam Israel6d84dbd2019-03-08 18:33:35 -0500996 return re.sub('-+', '-', appname.lower())
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500997
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500998 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
999 # """Format the name of the application
1000 #
1001 # Limitations:
1002 # - Only accepts characters a-z and non-consequitive dashes (-)
1003 # - Application name should not exceed 50 characters
1004 # """
1005 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1006 # new_name = ''
1007 # for c in name:
1008 # if c.isdigit():
1009 # c = chr(97 + int(c))
1010 # elif not c.isalpha():
1011 # c = "-"
1012 # new_name += c
1013 # return re.sub('\-+', '-', new_name.lower())
1014
1015 def format_model_name(self, name):
1016 """Format the name of model.
1017
1018 Model names may only contain lowercase letters, digits and hyphens
1019 """
1020
1021 return name.replace('_', '-').lower()
1022
1023 async def get_application(self, model, application):
1024 """Get the deployed application."""
1025 if not self.authenticated:
1026 await self.login()
1027
1028 app = None
1029 if application and model:
1030 if model.applications:
1031 if application in model.applications:
1032 app = model.applications[application]
1033
1034 return app
1035
Adam Israel85a4b212018-11-29 20:30:24 -05001036 async def get_model(self, model_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001037 """Get a model from the Juju Controller.
1038
1039 Note: Model objects returned must call disconnected() before it goes
1040 out of scope."""
1041 if not self.authenticated:
1042 await self.login()
1043
1044 if model_name not in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001045 # Get the models in the controller
1046 models = await self.controller.list_models()
1047
1048 if model_name not in models:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001049 try:
1050 self.models[model_name] = await self.controller.add_model(
1051 model_name
1052 )
1053 except JujuError as e:
1054 if "already exists" not in e.message:
1055 raise e
Adam Israel85a4b212018-11-29 20:30:24 -05001056 else:
1057 self.models[model_name] = await self.controller.get_model(
1058 model_name
1059 )
1060
Adam Israelfc511ed2018-09-21 14:20:55 +02001061 self.refcount['model'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001062
Adam Israel28a43c02018-04-23 16:04:54 -04001063 # Create an observer for this model
1064 self.monitors[model_name] = VCAMonitor(model_name)
1065 self.models[model_name].add_observer(self.monitors[model_name])
1066
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001067 return self.models[model_name]
1068
1069 async def login(self):
1070 """Login to the Juju controller."""
1071
1072 if self.authenticated:
1073 return
1074
1075 self.connecting = True
1076
1077 self.log.debug("JujuApi: Logging into controller")
1078
1079 cacert = None
Adam Israel5e08a0e2018-09-06 19:22:47 -04001080 self.controller = Controller(loop=self.loop)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001081
1082 if self.secret:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001083 self.log.debug(
1084 "Connecting to controller... ws://{}:{} as {}/{}".format(
1085 self.endpoint,
1086 self.port,
1087 self.user,
1088 self.secret,
1089 )
1090 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001091 await self.controller.connect(
1092 endpoint=self.endpoint,
1093 username=self.user,
1094 password=self.secret,
1095 cacert=cacert,
1096 )
Adam Israelfc511ed2018-09-21 14:20:55 +02001097 self.refcount['controller'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001098 else:
1099 # current_controller no longer exists
1100 # self.log.debug("Connecting to current controller...")
1101 # await self.controller.connect_current()
Adam Israel88a49632018-04-10 13:04:57 -06001102 # await self.controller.connect(
1103 # endpoint=self.endpoint,
1104 # username=self.user,
1105 # cacert=cacert,
1106 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001107 self.log.fatal("VCA credentials not configured.")
1108
1109 self.authenticated = True
1110 self.log.debug("JujuApi: Logged into controller")
1111
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001112 async def logout(self):
1113 """Logout of the Juju controller."""
1114 if not self.authenticated:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001115 return False
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001116
1117 try:
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001118 for model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001119 await self.disconnect_model(model)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001120
1121 if self.controller:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001122 self.log.debug("Disconnecting controller {}".format(
1123 self.controller
1124 ))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001125 await self.controller.disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +02001126 self.refcount['controller'] -= 1
Adam Israel5e08a0e2018-09-06 19:22:47 -04001127 self.controller = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001128
1129 self.authenticated = False
Adam Israelfc511ed2018-09-21 14:20:55 +02001130
1131 self.log.debug(self.refcount)
1132
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001133 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001134 self.log.fatal(
1135 "Fatal error logging out of Juju Controller: {}".format(e)
1136 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001137 raise e
Adam Israel6d84dbd2019-03-08 18:33:35 -05001138 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001139
Adam Israel85a4b212018-11-29 20:30:24 -05001140 async def disconnect_model(self, model):
1141 self.log.debug("Disconnecting model {}".format(model))
1142 if model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001143 print("Disconnecting model")
1144 await self.models[model].disconnect()
1145 self.refcount['model'] -= 1
1146 self.models[model] = None
1147
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001148 # async def remove_application(self, name):
1149 # """Remove the application."""
1150 # if not self.authenticated:
1151 # await self.login()
1152 #
1153 # app = await self.get_application(name)
1154 # if app:
1155 # self.log.debug("JujuApi: Destroying application {}".format(
1156 # name,
1157 # ))
1158 #
1159 # await app.destroy()
1160
1161 async def remove_relation(self, a, b):
1162 """
1163 Remove a relation between two application endpoints
1164
1165 :param a An application endpoint
1166 :param b An application endpoint
1167 """
1168 if not self.authenticated:
1169 await self.login()
1170
1171 m = await self.get_model()
1172 try:
1173 m.remove_relation(a, b)
1174 finally:
1175 await m.disconnect()
1176
Adam Israel85a4b212018-11-29 20:30:24 -05001177 async def resolve_error(self, model_name, application=None):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001178 """Resolve units in error state."""
1179 if not self.authenticated:
1180 await self.login()
1181
Adam Israel85a4b212018-11-29 20:30:24 -05001182 model = await self.get_model(model_name)
1183
1184 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001185 if app:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001186 self.log.debug(
1187 "JujuApi: Resolving errors for application {}".format(
1188 application,
1189 )
1190 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001191
1192 for unit in app.units:
1193 app.resolved(retry=True)
1194
Adam Israel85a4b212018-11-29 20:30:24 -05001195 async def run_action(self, model_name, application, action_name, **params):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001196 """Execute an action and return an Action object."""
1197 if not self.authenticated:
1198 await self.login()
1199 result = {
1200 'status': '',
1201 'action': {
1202 'tag': None,
1203 'results': None,
1204 }
1205 }
Adam Israel85a4b212018-11-29 20:30:24 -05001206
1207 model = await self.get_model(model_name)
1208
1209 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001210 if app:
1211 # We currently only have one unit per application
1212 # so use the first unit available.
1213 unit = app.units[0]
1214
Adam Israel5e08a0e2018-09-06 19:22:47 -04001215 self.log.debug(
1216 "JujuApi: Running Action {} against Application {}".format(
1217 action_name,
1218 application,
1219 )
1220 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001221
1222 action = await unit.run_action(action_name, **params)
1223
1224 # Wait for the action to complete
1225 await action.wait()
1226
1227 result['status'] = action.status
1228 result['action']['tag'] = action.data['id']
1229 result['action']['results'] = action.results
1230
1231 return result
1232
Adam Israelb5214512018-05-03 10:00:04 -04001233 async def set_config(self, model_name, application, config):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001234 """Apply a configuration to the application."""
1235 if not self.authenticated:
1236 await self.login()
1237
Adam Israelb5214512018-05-03 10:00:04 -04001238 app = await self.get_application(model_name, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001239 if app:
1240 self.log.debug("JujuApi: Setting config for Application {}".format(
1241 application,
1242 ))
1243 await app.set_config(config)
1244
1245 # Verify the config is set
1246 newconf = await app.get_config()
1247 for key in config:
1248 if config[key] != newconf[key]['value']:
1249 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
1250
Adam Israelb5214512018-05-03 10:00:04 -04001251 # async def set_parameter(self, parameter, value, application=None):
1252 # """Set a config parameter for a service."""
1253 # if not self.authenticated:
1254 # await self.login()
1255 #
1256 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1257 # parameter,
1258 # value,
1259 # application,
1260 # ))
1261 # return await self.apply_config(
1262 # {parameter: value},
1263 # application=application,
1264 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001265
Adam Israel5e08a0e2018-09-06 19:22:47 -04001266 async def wait_for_application(self, model_name, application_name,
1267 timeout=300):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001268 """Wait for an application to become active."""
1269 if not self.authenticated:
1270 await self.login()
1271
Adam Israel5e08a0e2018-09-06 19:22:47 -04001272 model = await self.get_model(model_name)
1273
1274 app = await self.get_application(model, application_name)
1275 self.log.debug("Application: {}".format(app))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001276 if app:
1277 self.log.debug(
1278 "JujuApi: Waiting {} seconds for Application {}".format(
1279 timeout,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001280 application_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001281 )
1282 )
1283
Adam Israel5e08a0e2018-09-06 19:22:47 -04001284 await model.block_until(
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001285 lambda: all(
Adam Israel5e08a0e2018-09-06 19:22:47 -04001286 unit.agent_status == 'idle' and unit.workload_status in
1287 ['active', 'unknown'] for unit in app.units
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001288 ),
1289 timeout=timeout
1290 )