blob: 9cdbb330977a27fb41108409bd07bb0344f38cab [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
Adam Israel7bf2f4d2019-03-15 15:28:47 -0400808
809 # Create an observer for this model
810 await self.create_model_monitor(ns_uuid)
811
Adam Israel6d84dbd2019-03-08 18:33:35 -0500812 return True
813
814 async def DestroyNetworkService(self, ns_uuid):
815 """Destroy a Network Service.
816
817 Destroy the Network Service and any deployed charms.
818
819 :param ns_uuid The unique id of the Network Service
820
821 :returns: True if the model was created. Raises JujuError on failure.
822 """
823
824 # Do not delete the default model. The default model was used by all
825 # Network Services, prior to the implementation of a model per NS.
826 if ns_uuid.lower() is "default":
827 return False
828
829 if not self.authenticated:
830 self.log.debug("Authenticating with Juju")
831 await self.login()
832
833 # Disconnect from the Model
834 if ns_uuid in self.models:
835 await self.disconnect_model(self.models[ns_uuid])
836
837 try:
838 await self.controller.destroy_models(ns_uuid)
839 except JujuError as e:
840 raise NetworkServiceDoesNotExist(
841 "The Network Service '{}' does not exist".format(ns_uuid)
842 )
843
844 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500845
Adam Israelb5214512018-05-03 10:00:04 -0400846 async def GetMetrics(self, model_name, application_name):
847 """Get the metrics collected by the VCA.
848
Adam Israel85a4b212018-11-29 20:30:24 -0500849 :param model_name The name or unique id of the network service
Adam Israelb5214512018-05-03 10:00:04 -0400850 :param application_name The name of the application
851 """
852 metrics = {}
853 model = await self.get_model(model_name)
854 app = await self.get_application(model, application_name)
855 if app:
856 metrics = await app.get_metrics()
857
858 return metrics
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500859
Adam Israelfa329072018-09-14 11:26:13 -0400860 async def HasApplication(self, model_name, application_name):
861 model = await self.get_model(model_name)
862 app = await self.get_application(model, application_name)
863 if app:
864 return True
865 return False
866
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500867 # Non-public methods
Adam Israel136186e2018-09-14 12:01:12 -0400868 async def add_relation(self, model_name, relation1, relation2):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500869 """
870 Add a relation between two application endpoints.
871
Adam Israel85a4b212018-11-29 20:30:24 -0500872 :param str model_name: The name or unique id of the network service
873 :param str relation1: '<application>[:<relation_name>]'
874 :param str relation2: '<application>[:<relation_name>]'
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500875 """
Adam Israel136186e2018-09-14 12:01:12 -0400876
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500877 if not self.authenticated:
878 await self.login()
879
Adam Israel136186e2018-09-14 12:01:12 -0400880 m = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500881 try:
Adam Israel136186e2018-09-14 12:01:12 -0400882 await m.add_relation(relation1, relation2)
883 except JujuAPIError as e:
884 # If one of the applications in the relationship doesn't exist,
885 # or the relation has already been added, let the operation fail
886 # silently.
887 if 'not found' in e.message:
888 return
889 if 'already exists' in e.message:
890 return
891
892 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500893
Adam Israelb5214512018-05-03 10:00:04 -0400894 # async def apply_config(self, config, application):
895 # """Apply a configuration to the application."""
896 # print("JujuApi: Applying configuration to {}.".format(
897 # application
898 # ))
899 # return await self.set_config(application=application, config=config)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500900
901 def _get_config_from_dict(self, config_primitive, values):
Adam Israel88a49632018-04-10 13:04:57 -0600902 """Transform the yang config primitive to dict.
903
904 Expected result:
905
906 config = {
907 'config':
908 }
909 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500910 config = {}
911 for primitive in config_primitive:
912 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600913 # config = self._map_primitive_parameters()
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500914 for parameter in primitive['parameter']:
915 param = str(parameter['name'])
916 if parameter['value'] == "<rw_mgmt_ip>":
917 config[param] = str(values[parameter['value']])
918 else:
919 config[param] = str(parameter['value'])
920
921 return config
922
tierno1afb30a2018-12-21 13:42:43 +0000923 def _map_primitive_parameters(self, parameters, user_values):
Adam Israel88a49632018-04-10 13:04:57 -0600924 params = {}
925 for parameter in parameters:
926 param = str(parameter['name'])
tierno1afb30a2018-12-21 13:42:43 +0000927 value = parameter.get('value')
928
929 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
930 # Must exist at user_values except if there is a default value
931 if isinstance(value, str) and value.startswith("<") and value.endswith(">"):
932 if parameter['value'][1:-1] in user_values:
933 value = user_values[parameter['value'][1:-1]]
934 elif 'default-value' in parameter:
935 value = parameter['default-value']
936 else:
937 raise KeyError("parameter {}='{}' not supplied ".format(param, value))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400938
Adam Israelbf793522018-11-20 13:54:13 -0500939 # If there's no value, use the default-value (if set)
tierno1afb30a2018-12-21 13:42:43 +0000940 if value is None and 'default-value' in parameter:
Adam Israelbf793522018-11-20 13:54:13 -0500941 value = parameter['default-value']
942
Adam Israel5e08a0e2018-09-06 19:22:47 -0400943 # Typecast parameter value, if present
tierno1afb30a2018-12-21 13:42:43 +0000944 paramtype = "string"
945 try:
946 if 'data-type' in parameter:
947 paramtype = str(parameter['data-type']).lower()
Adam Israel5e08a0e2018-09-06 19:22:47 -0400948
tierno1afb30a2018-12-21 13:42:43 +0000949 if paramtype == "integer":
950 value = int(value)
951 elif paramtype == "boolean":
952 value = bool(value)
953 else:
954 value = str(value)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400955 else:
tierno1afb30a2018-12-21 13:42:43 +0000956 # If there's no data-type, assume the value is a string
957 value = str(value)
958 except ValueError:
959 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param, value, paramtype))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400960
tierno1afb30a2018-12-21 13:42:43 +0000961 params[param] = value
Adam Israel88a49632018-04-10 13:04:57 -0600962 return params
963
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500964 def _get_config_from_yang(self, config_primitive, values):
965 """Transform the yang config primitive to dict."""
966 config = {}
967 for primitive in config_primitive.values():
968 if primitive['name'] == 'config':
969 for parameter in primitive['parameter'].values():
970 param = str(parameter['name'])
971 if parameter['value'] == "<rw_mgmt_ip>":
972 config[param] = str(values[parameter['value']])
973 else:
974 config[param] = str(parameter['value'])
975
976 return config
977
978 def FormatApplicationName(self, *args):
979 """
980 Generate a Juju-compatible Application name
981
982 :param args tuple: Positional arguments to be used to construct the
983 application name.
984
985 Limitations::
986 - Only accepts characters a-z and non-consequitive dashes (-)
987 - Application name should not exceed 50 characters
988
989 Examples::
990
991 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
992 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500993 appname = ""
994 for c in "-".join(list(args)):
995 if c.isdigit():
996 c = chr(97 + int(c))
997 elif not c.isalpha():
998 c = "-"
999 appname += c
Adam Israel6d84dbd2019-03-08 18:33:35 -05001000 return re.sub('-+', '-', appname.lower())
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001001
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001002 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
1003 # """Format the name of the application
1004 #
1005 # Limitations:
1006 # - Only accepts characters a-z and non-consequitive dashes (-)
1007 # - Application name should not exceed 50 characters
1008 # """
1009 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1010 # new_name = ''
1011 # for c in name:
1012 # if c.isdigit():
1013 # c = chr(97 + int(c))
1014 # elif not c.isalpha():
1015 # c = "-"
1016 # new_name += c
1017 # return re.sub('\-+', '-', new_name.lower())
1018
1019 def format_model_name(self, name):
1020 """Format the name of model.
1021
1022 Model names may only contain lowercase letters, digits and hyphens
1023 """
1024
1025 return name.replace('_', '-').lower()
1026
1027 async def get_application(self, model, application):
1028 """Get the deployed application."""
1029 if not self.authenticated:
1030 await self.login()
1031
1032 app = None
1033 if application and model:
1034 if model.applications:
1035 if application in model.applications:
1036 app = model.applications[application]
1037
1038 return app
1039
Adam Israel85a4b212018-11-29 20:30:24 -05001040 async def get_model(self, model_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001041 """Get a model from the Juju Controller.
1042
1043 Note: Model objects returned must call disconnected() before it goes
1044 out of scope."""
1045 if not self.authenticated:
1046 await self.login()
1047
1048 if model_name not in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001049 # Get the models in the controller
1050 models = await self.controller.list_models()
1051
1052 if model_name not in models:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001053 try:
1054 self.models[model_name] = await self.controller.add_model(
1055 model_name
1056 )
1057 except JujuError as e:
1058 if "already exists" not in e.message:
1059 raise e
Adam Israel85a4b212018-11-29 20:30:24 -05001060 else:
1061 self.models[model_name] = await self.controller.get_model(
1062 model_name
1063 )
1064
Adam Israelfc511ed2018-09-21 14:20:55 +02001065 self.refcount['model'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001066
Adam Israel28a43c02018-04-23 16:04:54 -04001067 # Create an observer for this model
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001068 await self.create_model_monitor(model_name)
1069
1070 return self.models[model_name]
1071
1072 async def create_model_monitor(self, model_name):
1073 """Create a monitor for the model, if none exists."""
1074 if not self.authenticated:
1075 await self.login()
1076
1077 if model_name not in self.monitors:
Adam Israel28a43c02018-04-23 16:04:54 -04001078 self.monitors[model_name] = VCAMonitor(model_name)
1079 self.models[model_name].add_observer(self.monitors[model_name])
1080
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001081 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001082
1083 async def login(self):
1084 """Login to the Juju controller."""
1085
1086 if self.authenticated:
1087 return
1088
1089 self.connecting = True
1090
1091 self.log.debug("JujuApi: Logging into controller")
1092
1093 cacert = None
Adam Israel5e08a0e2018-09-06 19:22:47 -04001094 self.controller = Controller(loop=self.loop)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001095
1096 if self.secret:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001097 self.log.debug(
1098 "Connecting to controller... ws://{}:{} as {}/{}".format(
1099 self.endpoint,
1100 self.port,
1101 self.user,
1102 self.secret,
1103 )
1104 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001105 await self.controller.connect(
1106 endpoint=self.endpoint,
1107 username=self.user,
1108 password=self.secret,
1109 cacert=cacert,
1110 )
Adam Israelfc511ed2018-09-21 14:20:55 +02001111 self.refcount['controller'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001112 else:
1113 # current_controller no longer exists
1114 # self.log.debug("Connecting to current controller...")
1115 # await self.controller.connect_current()
Adam Israel88a49632018-04-10 13:04:57 -06001116 # await self.controller.connect(
1117 # endpoint=self.endpoint,
1118 # username=self.user,
1119 # cacert=cacert,
1120 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001121 self.log.fatal("VCA credentials not configured.")
1122
1123 self.authenticated = True
1124 self.log.debug("JujuApi: Logged into controller")
1125
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001126 async def logout(self):
1127 """Logout of the Juju controller."""
1128 if not self.authenticated:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001129 return False
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001130
1131 try:
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001132 for model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001133 await self.disconnect_model(model)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001134
1135 if self.controller:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001136 self.log.debug("Disconnecting controller {}".format(
1137 self.controller
1138 ))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001139 await self.controller.disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +02001140 self.refcount['controller'] -= 1
Adam Israel5e08a0e2018-09-06 19:22:47 -04001141 self.controller = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001142
1143 self.authenticated = False
Adam Israelfc511ed2018-09-21 14:20:55 +02001144
1145 self.log.debug(self.refcount)
1146
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001147 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001148 self.log.fatal(
1149 "Fatal error logging out of Juju Controller: {}".format(e)
1150 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001151 raise e
Adam Israel6d84dbd2019-03-08 18:33:35 -05001152 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001153
Adam Israel85a4b212018-11-29 20:30:24 -05001154 async def disconnect_model(self, model):
1155 self.log.debug("Disconnecting model {}".format(model))
1156 if model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001157 print("Disconnecting model")
1158 await self.models[model].disconnect()
1159 self.refcount['model'] -= 1
1160 self.models[model] = None
1161
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001162 # async def remove_application(self, name):
1163 # """Remove the application."""
1164 # if not self.authenticated:
1165 # await self.login()
1166 #
1167 # app = await self.get_application(name)
1168 # if app:
1169 # self.log.debug("JujuApi: Destroying application {}".format(
1170 # name,
1171 # ))
1172 #
1173 # await app.destroy()
1174
1175 async def remove_relation(self, a, b):
1176 """
1177 Remove a relation between two application endpoints
1178
1179 :param a An application endpoint
1180 :param b An application endpoint
1181 """
1182 if not self.authenticated:
1183 await self.login()
1184
1185 m = await self.get_model()
1186 try:
1187 m.remove_relation(a, b)
1188 finally:
1189 await m.disconnect()
1190
Adam Israel85a4b212018-11-29 20:30:24 -05001191 async def resolve_error(self, model_name, application=None):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001192 """Resolve units in error state."""
1193 if not self.authenticated:
1194 await self.login()
1195
Adam Israel85a4b212018-11-29 20:30:24 -05001196 model = await self.get_model(model_name)
1197
1198 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001199 if app:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001200 self.log.debug(
1201 "JujuApi: Resolving errors for application {}".format(
1202 application,
1203 )
1204 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001205
1206 for unit in app.units:
1207 app.resolved(retry=True)
1208
Adam Israel85a4b212018-11-29 20:30:24 -05001209 async def run_action(self, model_name, application, action_name, **params):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001210 """Execute an action and return an Action object."""
1211 if not self.authenticated:
1212 await self.login()
1213 result = {
1214 'status': '',
1215 'action': {
1216 'tag': None,
1217 'results': None,
1218 }
1219 }
Adam Israel85a4b212018-11-29 20:30:24 -05001220
1221 model = await self.get_model(model_name)
1222
1223 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001224 if app:
1225 # We currently only have one unit per application
1226 # so use the first unit available.
1227 unit = app.units[0]
1228
Adam Israel5e08a0e2018-09-06 19:22:47 -04001229 self.log.debug(
1230 "JujuApi: Running Action {} against Application {}".format(
1231 action_name,
1232 application,
1233 )
1234 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001235
1236 action = await unit.run_action(action_name, **params)
1237
1238 # Wait for the action to complete
1239 await action.wait()
1240
1241 result['status'] = action.status
1242 result['action']['tag'] = action.data['id']
1243 result['action']['results'] = action.results
1244
1245 return result
1246
Adam Israelb5214512018-05-03 10:00:04 -04001247 async def set_config(self, model_name, application, config):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001248 """Apply a configuration to the application."""
1249 if not self.authenticated:
1250 await self.login()
1251
Adam Israelb5214512018-05-03 10:00:04 -04001252 app = await self.get_application(model_name, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001253 if app:
1254 self.log.debug("JujuApi: Setting config for Application {}".format(
1255 application,
1256 ))
1257 await app.set_config(config)
1258
1259 # Verify the config is set
1260 newconf = await app.get_config()
1261 for key in config:
1262 if config[key] != newconf[key]['value']:
1263 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
1264
Adam Israelb5214512018-05-03 10:00:04 -04001265 # async def set_parameter(self, parameter, value, application=None):
1266 # """Set a config parameter for a service."""
1267 # if not self.authenticated:
1268 # await self.login()
1269 #
1270 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1271 # parameter,
1272 # value,
1273 # application,
1274 # ))
1275 # return await self.apply_config(
1276 # {parameter: value},
1277 # application=application,
1278 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001279
Adam Israel5e08a0e2018-09-06 19:22:47 -04001280 async def wait_for_application(self, model_name, application_name,
1281 timeout=300):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001282 """Wait for an application to become active."""
1283 if not self.authenticated:
1284 await self.login()
1285
Adam Israel5e08a0e2018-09-06 19:22:47 -04001286 model = await self.get_model(model_name)
1287
1288 app = await self.get_application(model, application_name)
1289 self.log.debug("Application: {}".format(app))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001290 if app:
1291 self.log.debug(
1292 "JujuApi: Waiting {} seconds for Application {}".format(
1293 timeout,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001294 application_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001295 )
1296 )
1297
Adam Israel5e08a0e2018-09-06 19:22:47 -04001298 await model.block_until(
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001299 lambda: all(
Adam Israel5e08a0e2018-09-06 19:22:47 -04001300 unit.agent_status == 'idle' and unit.workload_status in
1301 ['active', 'unknown'] for unit in app.units
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001302 ),
1303 timeout=timeout
1304 )