blob: a486f2709b24e85350e4b913fbb6737eaf884f95 [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
Adam Israelc3e6c2e2018-03-01 09:31:50 -050060
Adam Israel28a43c02018-04-23 16:04:54 -040061 def __init__(self, ns_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050062 self.log = logging.getLogger(__name__)
63
64 self.ns_name = ns_name
Adam Israeld420a8b2019-04-09 16:07:53 -040065 self.applications = {}
Adam Israel28a43c02018-04-23 16:04:54 -040066
67 def AddApplication(self, application_name, callback, *callback_args):
68 if application_name not in self.applications:
69 self.applications[application_name] = {
70 'callback': callback,
71 'callback_args': callback_args
72 }
73
74 def RemoveApplication(self, application_name):
75 if application_name in self.applications:
76 del self.applications[application_name]
Adam Israelc3e6c2e2018-03-01 09:31:50 -050077
78 async def on_change(self, delta, old, new, model):
79 """React to changes in the Juju model."""
80
81 if delta.entity == "unit":
Adam Israel28a43c02018-04-23 16:04:54 -040082 # Ignore change events from other applications
83 if delta.data['application'] not in self.applications.keys():
84 return
85
Adam Israelc3e6c2e2018-03-01 09:31:50 -050086 try:
Adam Israel28a43c02018-04-23 16:04:54 -040087
88 application_name = delta.data['application']
89
90 callback = self.applications[application_name]['callback']
Adam Israel5e08a0e2018-09-06 19:22:47 -040091 callback_args = \
92 self.applications[application_name]['callback_args']
Adam Israel28a43c02018-04-23 16:04:54 -040093
Adam Israelc3e6c2e2018-03-01 09:31:50 -050094 if old and new:
Adam Israelfc511ed2018-09-21 14:20:55 +020095 # Fire off a callback with the application state
96 if callback:
97 callback(
98 self.ns_name,
99 delta.data['application'],
100 new.workload_status,
101 new.workload_status_message,
102 *callback_args)
Adam Israel28a43c02018-04-23 16:04:54 -0400103
104 if old and not new:
105 # This is a charm being removed
106 if callback:
107 callback(
108 self.ns_name,
109 delta.data['application'],
110 "removed",
Adam Israel9562f432018-05-09 13:55:28 -0400111 "",
Adam Israel28a43c02018-04-23 16:04:54 -0400112 *callback_args)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500113 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400114 self.log.debug("[1] notify_callback exception: {}".format(e))
115
Adam Israel88a49632018-04-10 13:04:57 -0600116 elif delta.entity == "action":
117 # TODO: Decide how we want to notify the user of actions
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500118
Adam Israel88a49632018-04-10 13:04:57 -0600119 # uuid = delta.data['id'] # The Action's unique id
120 # msg = delta.data['message'] # The output of the action
121 #
122 # if delta.data['status'] == "pending":
123 # # The action is queued
124 # pass
125 # elif delta.data['status'] == "completed""
126 # # The action was successful
127 # pass
128 # elif delta.data['status'] == "failed":
129 # # The action failed.
130 # pass
131
132 pass
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500133
134########
135# TODO
136#
137# Create unique models per network service
138# Document all public functions
139
Adam Israelb5214512018-05-03 10:00:04 -0400140
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500141class N2VC:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500142 def __init__(self,
143 log=None,
144 server='127.0.0.1',
145 port=17070,
146 user='admin',
147 secret=None,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400148 artifacts=None,
149 loop=None,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500150 ):
151 """Initialize N2VC
152
153 :param vcaconfig dict A dictionary containing the VCA configuration
154
155 :param artifacts str The directory where charms required by a vnfd are
156 stored.
157
158 :Example:
159 n2vc = N2VC(vcaconfig={
160 'secret': 'MzI3MDJhOTYxYmM0YzRjNTJiYmY1Yzdm',
161 'user': 'admin',
162 'ip-address': '10.44.127.137',
163 'port': 17070,
164 'artifacts': '/path/to/charms'
165 })
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500166 """
167
Adam Israel5e08a0e2018-09-06 19:22:47 -0400168 # Initialize instance-level variables
169 self.api = None
170 self.log = None
171 self.controller = None
172 self.connecting = False
173 self.authenticated = False
174
Adam Israelfc511ed2018-09-21 14:20:55 +0200175 # For debugging
176 self.refcount = {
177 'controller': 0,
178 'model': 0,
179 }
180
Adam Israel5e08a0e2018-09-06 19:22:47 -0400181 self.models = {}
Adam Israel5e08a0e2018-09-06 19:22:47 -0400182
183 # Model Observers
184 self.monitors = {}
185
186 # VCA config
187 self.hostname = ""
188 self.port = 17070
189 self.username = ""
190 self.secret = ""
191
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500192 if log:
193 self.log = log
194 else:
195 self.log = logging.getLogger(__name__)
196
197 # Quiet websocket traffic
198 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
199 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
200 logging.getLogger('model').setLevel(logging.WARN)
201 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
202
203 self.log.debug('JujuApi: instantiated')
204
205 self.server = server
206 self.port = port
207
208 self.secret = secret
209 if user.startswith('user-'):
210 self.user = user
211 else:
212 self.user = 'user-{}'.format(user)
213
214 self.endpoint = '%s:%d' % (server, int(port))
215
216 self.artifacts = artifacts
217
Adam Israel5e08a0e2018-09-06 19:22:47 -0400218 self.loop = loop or asyncio.get_event_loop()
219
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500220 def __del__(self):
221 """Close any open connections."""
222 yield self.logout()
223
Adam Israel5e08a0e2018-09-06 19:22:47 -0400224 def notify_callback(self, model_name, application_name, status, message,
225 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500226 try:
227 if callback:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400228 callback(
229 model_name,
230 application_name,
231 status, message,
232 *callback_args,
233 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500234 except Exception as e:
235 self.log.error("[0] notify_callback exception {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600236 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500237 return True
238
239 # Public methods
Adam Israel85a4b212018-11-29 20:30:24 -0500240 async def Relate(self, model_name, vnfd):
Adam Israel136186e2018-09-14 12:01:12 -0400241 """Create a relation between the charm-enabled VDUs in a VNF.
242
243 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
244
245 vdu:
246 ...
247 relation:
248 - provides: dataVM:db
249 requires: mgmtVM:app
250
251 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.
252
253 :param str ns_name: The name of the network service.
254 :param dict vnfd: The parsed yaml VNF descriptor.
255 """
256
257 # Currently, the call to Relate() is made automatically after the
258 # deployment of each charm; if the relation depends on a charm that
259 # hasn't been deployed yet, the call will fail silently. This will
260 # prevent an API breakage, with the intent of making this an explicitly
261 # required call in a more object-oriented refactor of the N2VC API.
262
263 configs = []
264 vnf_config = vnfd.get("vnf-configuration")
265 if vnf_config:
266 juju = vnf_config['juju']
267 if juju:
268 configs.append(vnf_config)
269
270 for vdu in vnfd['vdu']:
271 vdu_config = vdu.get('vdu-configuration')
272 if vdu_config:
273 juju = vdu_config['juju']
274 if juju:
275 configs.append(vdu_config)
276
277 def _get_application_name(name):
278 """Get the application name that's mapped to a vnf/vdu."""
279 vnf_member_index = 0
280 vnf_name = vnfd['name']
281
282 for vdu in vnfd.get('vdu'):
283 # Compare the named portion of the relation to the vdu's id
284 if vdu['id'] == name:
285 application_name = self.FormatApplicationName(
Adam Israel85a4b212018-11-29 20:30:24 -0500286 model_name,
Adam Israel136186e2018-09-14 12:01:12 -0400287 vnf_name,
288 str(vnf_member_index),
289 )
290 return application_name
291 else:
292 vnf_member_index += 1
293
294 return None
295
296 # Loop through relations
297 for cfg in configs:
298 if 'juju' in cfg:
299 if 'relation' in juju:
300 for rel in juju['relation']:
301 try:
302
303 # get the application name for the provides
304 (name, endpoint) = rel['provides'].split(':')
305 application_name = _get_application_name(name)
306
307 provides = "{}:{}".format(
308 application_name,
309 endpoint
310 )
311
312 # get the application name for thr requires
313 (name, endpoint) = rel['requires'].split(':')
314 application_name = _get_application_name(name)
315
316 requires = "{}:{}".format(
317 application_name,
318 endpoint
319 )
320 self.log.debug("Relation: {} <-> {}".format(
321 provides,
322 requires
323 ))
324 await self.add_relation(
Adam Israel85a4b212018-11-29 20:30:24 -0500325 model_name,
Adam Israel136186e2018-09-14 12:01:12 -0400326 provides,
327 requires,
328 )
329 except Exception as e:
330 self.log.debug("Exception: {}".format(e))
331
332 return
333
Adam Israel5e08a0e2018-09-06 19:22:47 -0400334 async def DeployCharms(self, model_name, application_name, vnfd,
335 charm_path, params={}, machine_spec={},
336 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500337 """Deploy one or more charms associated with a VNF.
338
339 Deploy the charm(s) referenced in a VNF Descriptor.
340
Adam Israel85a4b212018-11-29 20:30:24 -0500341 :param str model_name: The name or unique id of the network service.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500342 :param str application_name: The name of the application
343 :param dict vnfd: The name of the application
344 :param str charm_path: The path to the Juju charm
345 :param dict params: A dictionary of runtime parameters
346 Examples::
347 {
Adam Israel88a49632018-04-10 13:04:57 -0600348 'rw_mgmt_ip': '1.2.3.4',
349 # Pass the initial-config-primitives section of the vnf or vdu
350 'initial-config-primitives': {...}
tierno1afb30a2018-12-21 13:42:43 +0000351 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
352 inside < >. rw_mgmt_ip will be included here also
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500353 }
Adam Israel5e08a0e2018-09-06 19:22:47 -0400354 :param dict machine_spec: A dictionary describing the machine to
355 install to
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500356 Examples::
357 {
358 'hostname': '1.2.3.4',
359 'username': 'ubuntu',
360 }
361 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400362 :param tuple callback_args: A list of arguments to be passed to the
363 callback
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500364 """
365
366 ########################################################
367 # Verify the path to the charm exists and is readable. #
368 ########################################################
369 if not os.path.exists(charm_path):
370 self.log.debug("Charm path doesn't exist: {}".format(charm_path))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400371 self.notify_callback(
372 model_name,
373 application_name,
374 "failed",
375 callback,
376 *callback_args,
377 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500378 raise JujuCharmNotFound("No artifacts configured.")
379
380 ################################
381 # Login to the Juju controller #
382 ################################
383 if not self.authenticated:
384 self.log.debug("Authenticating with Juju")
385 await self.login()
386
387 ##########################################
388 # Get the model for this network service #
389 ##########################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500390 model = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500391
392 ########################################
393 # Verify the application doesn't exist #
394 ########################################
395 app = await self.get_application(model, application_name)
396 if app:
Adam Israel42d88e62018-07-16 14:18:41 -0400397 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500398
Adam Israel28a43c02018-04-23 16:04:54 -0400399 ################################################################
400 # Register this application with the model-level event monitor #
401 ################################################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500402 if callback:
Adam Israel28a43c02018-04-23 16:04:54 -0400403 self.monitors[model_name].AddApplication(
404 application_name,
405 callback,
406 *callback_args
407 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500408
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500409 ########################################################
410 # Check for specific machine placement (native charms) #
411 ########################################################
412 to = ""
413 if machine_spec.keys():
Adam Israel5963cb42018-09-14 11:26:13 -0400414 if all(k in machine_spec for k in ['host', 'user']):
415 # Enlist an existing machine as a Juju unit
416 machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
417 machine_spec['user'],
418 machine_spec['host'],
419 self.GetPrivateKeyPath(),
420 ))
Adam Israelfa329072018-09-14 11:26:13 -0400421 to = machine.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500422
423 #######################################
424 # Get the initial charm configuration #
425 #######################################
426
427 rw_mgmt_ip = None
428 if 'rw_mgmt_ip' in params:
429 rw_mgmt_ip = params['rw_mgmt_ip']
430
Adam Israel5afe0542018-08-08 12:54:55 -0400431 if 'initial-config-primitive' not in params:
432 params['initial-config-primitive'] = {}
433
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500434 initial_config = self._get_config_from_dict(
Adam Israel88a49632018-04-10 13:04:57 -0600435 params['initial-config-primitive'],
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500436 {'<rw_mgmt_ip>': rw_mgmt_ip}
437 )
438
Adam Israel85a4b212018-11-29 20:30:24 -0500439 self.log.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
440 model_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500441 application_name,
442 charm_path,
443 to=to,
444 ))
445
446 ########################################################
447 # Deploy the charm and apply the initial configuration #
448 ########################################################
449 app = await model.deploy(
Adam Israel88a49632018-04-10 13:04:57 -0600450 # We expect charm_path to be either the path to the charm on disk
451 # or in the format of cs:series/name
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500452 charm_path,
Adam Israel88a49632018-04-10 13:04:57 -0600453 # This is the formatted, unique name for this charm
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500454 application_name=application_name,
Adam Israel88a49632018-04-10 13:04:57 -0600455 # Proxy charms should use the current LTS. This will need to be
456 # changed for native charms.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500457 series='xenial',
Adam Israel88a49632018-04-10 13:04:57 -0600458 # Apply the initial 'config' primitive during deployment
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500459 config=initial_config,
Adam Israelfa329072018-09-14 11:26:13 -0400460 # Where to deploy the charm to.
461 to=to,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500462 )
463
Adam Israel136186e2018-09-14 12:01:12 -0400464 # Map the vdu id<->app name,
465 #
466 await self.Relate(model_name, vnfd)
467
Adam Israel88a49632018-04-10 13:04:57 -0600468 # #######################################
469 # # Execute initial config primitive(s) #
470 # #######################################
Adam Israelcf253202018-10-31 16:29:09 -0700471 uuids = await self.ExecuteInitialPrimitives(
Adam Israel5e08a0e2018-09-06 19:22:47 -0400472 model_name,
473 application_name,
474 params,
475 )
Adam Israelcf253202018-10-31 16:29:09 -0700476 return uuids
Adam Israel5e08a0e2018-09-06 19:22:47 -0400477
478 # primitives = {}
479 #
480 # # Build a sequential list of the primitives to execute
481 # for primitive in params['initial-config-primitive']:
482 # try:
483 # if primitive['name'] == 'config':
484 # # This is applied when the Application is deployed
485 # pass
486 # else:
487 # seq = primitive['seq']
488 #
489 # params = {}
490 # if 'parameter' in primitive:
491 # params = primitive['parameter']
492 #
493 # primitives[seq] = {
494 # 'name': primitive['name'],
495 # 'parameters': self._map_primitive_parameters(
496 # params,
497 # {'<rw_mgmt_ip>': rw_mgmt_ip}
498 # ),
499 # }
500 #
501 # for primitive in sorted(primitives):
502 # await self.ExecutePrimitive(
503 # model_name,
504 # application_name,
505 # primitives[primitive]['name'],
506 # callback,
507 # callback_args,
508 # **primitives[primitive]['parameters'],
509 # )
510 # except N2VCPrimitiveExecutionFailed as e:
511 # self.log.debug(
512 # "[N2VC] Exception executing primitive: {}".format(e)
513 # )
514 # raise
515
516 async def GetPrimitiveStatus(self, model_name, uuid):
517 """Get the status of an executed Primitive.
518
519 The status of an executed Primitive will be one of three values:
520 - completed
521 - failed
522 - running
523 """
524 status = None
525 try:
526 if not self.authenticated:
527 await self.login()
528
Adam Israel5e08a0e2018-09-06 19:22:47 -0400529 model = await self.get_model(model_name)
530
531 results = await model.get_action_status(uuid)
532
533 if uuid in results:
534 status = results[uuid]
535
536 except Exception as e:
537 self.log.debug(
538 "Caught exception while getting primitive status: {}".format(e)
539 )
540 raise N2VCPrimitiveExecutionFailed(e)
541
542 return status
543
544 async def GetPrimitiveOutput(self, model_name, uuid):
545 """Get the output of an executed Primitive.
546
547 Note: this only returns output for a successfully executed primitive.
548 """
549 results = None
550 try:
551 if not self.authenticated:
552 await self.login()
553
Adam Israel5e08a0e2018-09-06 19:22:47 -0400554 model = await self.get_model(model_name)
555 results = await model.get_action_output(uuid, 60)
556 except Exception as e:
557 self.log.debug(
558 "Caught exception while getting primitive status: {}".format(e)
559 )
560 raise N2VCPrimitiveExecutionFailed(e)
561
562 return results
563
Adam Israelfa329072018-09-14 11:26:13 -0400564 # async def ProvisionMachine(self, model_name, hostname, username):
565 # """Provision machine for usage with Juju.
566 #
567 # Provisions a previously instantiated machine for use with Juju.
568 # """
569 # try:
570 # if not self.authenticated:
571 # await self.login()
572 #
573 # # FIXME: This is hard-coded until model-per-ns is added
574 # model_name = 'default'
575 #
576 # model = await self.get_model(model_name)
577 # model.add_machine(spec={})
578 #
579 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
580 # "ubuntu",
581 # host['address'],
582 # private_key_path,
583 # ))
584 # return machine.id
585 #
586 # except Exception as e:
587 # self.log.debug(
588 # "Caught exception while getting primitive status: {}".format(e)
589 # )
590 # raise N2VCPrimitiveExecutionFailed(e)
591
592 def GetPrivateKeyPath(self):
593 homedir = os.environ['HOME']
594 sshdir = "{}/.ssh".format(homedir)
595 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
596 return private_key_path
597
598 async def GetPublicKey(self):
599 """Get the N2VC SSH public key.abs
600
601 Returns the SSH public key, to be injected into virtual machines to
602 be managed by the VCA.
603
604 The first time this is run, a ssh keypair will be created. The public
605 key is injected into a VM so that we can provision the machine with
606 Juju, after which Juju will communicate with the VM directly via the
607 juju agent.
608 """
609 public_key = ""
610
611 # Find the path to where we expect our key to live.
612 homedir = os.environ['HOME']
613 sshdir = "{}/.ssh".format(homedir)
614 if not os.path.exists(sshdir):
615 os.mkdir(sshdir)
616
617 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
618 public_key_path = "{}.pub".format(private_key_path)
619
620 # If we don't have a key generated, generate it.
621 if not os.path.exists(private_key_path):
622 cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format(
623 "rsa",
624 "4096",
625 private_key_path
626 )
627 subprocess.check_output(shlex.split(cmd))
628
629 # Read the public key
630 with open(public_key_path, "r") as f:
631 public_key = f.readline()
632
633 return public_key
634
Adam Israel5e08a0e2018-09-06 19:22:47 -0400635 async def ExecuteInitialPrimitives(self, model_name, application_name,
636 params, callback=None, *callback_args):
637 """Execute multiple primitives.
638
639 Execute multiple primitives as declared in initial-config-primitive.
640 This is useful in cases where the primitives initially failed -- for
641 example, if the charm is a proxy but the proxy hasn't been configured
642 yet.
643 """
644 uuids = []
Adam Israel88a49632018-04-10 13:04:57 -0600645 primitives = {}
646
647 # Build a sequential list of the primitives to execute
648 for primitive in params['initial-config-primitive']:
649 try:
650 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600651 pass
652 else:
Adam Israel88a49632018-04-10 13:04:57 -0600653 seq = primitive['seq']
654
tierno1afb30a2018-12-21 13:42:43 +0000655 params_ = {}
Adam Israel42d88e62018-07-16 14:18:41 -0400656 if 'parameter' in primitive:
tierno1afb30a2018-12-21 13:42:43 +0000657 params_ = primitive['parameter']
658
659 user_values = params.get("user_values", {})
660 if 'rw_mgmt_ip' not in user_values:
661 user_values['rw_mgmt_ip'] = None
662 # just for backward compatibility, because it will be provided always by modern version of LCM
Adam Israel42d88e62018-07-16 14:18:41 -0400663
Adam Israel88a49632018-04-10 13:04:57 -0600664 primitives[seq] = {
665 'name': primitive['name'],
666 'parameters': self._map_primitive_parameters(
tierno1afb30a2018-12-21 13:42:43 +0000667 params_,
668 user_values
Adam Israel88a49632018-04-10 13:04:57 -0600669 ),
670 }
671
672 for primitive in sorted(primitives):
Adam Israel5e08a0e2018-09-06 19:22:47 -0400673 uuids.append(
674 await self.ExecutePrimitive(
675 model_name,
676 application_name,
677 primitives[primitive]['name'],
678 callback,
679 callback_args,
680 **primitives[primitive]['parameters'],
681 )
Adam Israel88a49632018-04-10 13:04:57 -0600682 )
683 except N2VCPrimitiveExecutionFailed as e:
Adam Israel7d871fb2018-07-17 12:17:06 -0400684 self.log.debug(
Adam Israel88a49632018-04-10 13:04:57 -0600685 "[N2VC] Exception executing primitive: {}".format(e)
686 )
687 raise
Adam Israel5e08a0e2018-09-06 19:22:47 -0400688 return uuids
Adam Israel88a49632018-04-10 13:04:57 -0600689
Adam Israel5e08a0e2018-09-06 19:22:47 -0400690 async def ExecutePrimitive(self, model_name, application_name, primitive,
691 callback, *callback_args, **params):
Adam Israelc9df96f2018-05-03 14:49:56 -0400692 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
Adam Israel6817f612018-04-13 08:41:43 -0600693
Adam Israelc9df96f2018-05-03 14:49:56 -0400694 Execute a primitive defined in the VNF descriptor.
695
Adam Israel85a4b212018-11-29 20:30:24 -0500696 :param str model_name: The name or unique id of the network service.
Adam Israelc9df96f2018-05-03 14:49:56 -0400697 :param str application_name: The name of the application
698 :param str primitive: The name of the primitive to execute.
699 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400700 :param tuple callback_args: A list of arguments to be passed to the
701 callback function.
702 :param dict params: A dictionary of key=value pairs representing the
703 primitive's parameters
Adam Israelc9df96f2018-05-03 14:49:56 -0400704 Examples::
705 {
706 'rw_mgmt_ip': '1.2.3.4',
707 # Pass the initial-config-primitives section of the vnf or vdu
708 'initial-config-primitives': {...}
709 }
Adam Israel6817f612018-04-13 08:41:43 -0600710 """
tierno1afb30a2018-12-21 13:42:43 +0000711 self.log.debug("Executing primitive={} params={}".format(primitive, params))
Adam Israel6817f612018-04-13 08:41:43 -0600712 uuid = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500713 try:
714 if not self.authenticated:
715 await self.login()
716
Adam Israel5e08a0e2018-09-06 19:22:47 -0400717 model = await self.get_model(model_name)
Adam Israelb5214512018-05-03 10:00:04 -0400718
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500719 if primitive == 'config':
720 # config is special, and expecting params to be a dictionary
Adam Israelb0943662018-08-02 15:32:00 -0400721 await self.set_config(
722 model,
723 application_name,
724 params['params'],
725 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500726 else:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500727 app = await self.get_application(model, application_name)
728 if app:
729 # Run against the first (and probably only) unit in the app
730 unit = app.units[0]
731 if unit:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500732 action = await unit.run_action(primitive, **params)
Adam Israel6817f612018-04-13 08:41:43 -0600733 uuid = action.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500734 except Exception as e:
Adam Israelb0943662018-08-02 15:32:00 -0400735 self.log.debug(
736 "Caught exception while executing primitive: {}".format(e)
737 )
Adam Israel7d871fb2018-07-17 12:17:06 -0400738 raise N2VCPrimitiveExecutionFailed(e)
Adam Israel6817f612018-04-13 08:41:43 -0600739 return uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500740
Adam Israel5e08a0e2018-09-06 19:22:47 -0400741 async def RemoveCharms(self, model_name, application_name, callback=None,
742 *callback_args):
Adam Israelc9df96f2018-05-03 14:49:56 -0400743 """Remove a charm from the VCA.
744
745 Remove a charm referenced in a VNF Descriptor.
746
747 :param str model_name: The name of the network service.
748 :param str application_name: The name of the application
749 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400750 :param tuple callback_args: A list of arguments to be passed to the
751 callback function.
Adam Israelc9df96f2018-05-03 14:49:56 -0400752 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500753 try:
754 if not self.authenticated:
755 await self.login()
756
757 model = await self.get_model(model_name)
758 app = await self.get_application(model, application_name)
759 if app:
Adam Israel28a43c02018-04-23 16:04:54 -0400760 # Remove this application from event monitoring
761 self.monitors[model_name].RemoveApplication(application_name)
762
763 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400764 self.log.debug(
765 "Removing the application {}".format(application_name)
766 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500767 await app.remove()
Adam Israel28a43c02018-04-23 16:04:54 -0400768
Adam Israel85a4b212018-11-29 20:30:24 -0500769 await self.disconnect_model(self.monitors[model_name])
770
Adam Israel5e08a0e2018-09-06 19:22:47 -0400771 self.notify_callback(
772 model_name,
773 application_name,
774 "removed",
Adam Israelc4f393e2019-03-19 16:33:30 -0400775 "Removing charm {}".format(application_name),
Adam Israel5e08a0e2018-09-06 19:22:47 -0400776 callback,
777 *callback_args,
778 )
Adam Israel28a43c02018-04-23 16:04:54 -0400779
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500780 except Exception as e:
781 print("Caught exception: {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600782 self.log.debug(e)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500783 raise e
784
Adam Israel6d84dbd2019-03-08 18:33:35 -0500785 async def CreateNetworkService(self, ns_uuid):
786 """Create a new Juju model for the Network Service.
787
788 Creates a new Model in the Juju Controller.
789
790 :param str ns_uuid: A unique id representing an instaance of a
791 Network Service.
792
793 :returns: True if the model was created. Raises JujuError on failure.
794 """
795 if not self.authenticated:
796 await self.login()
797
798 models = await self.controller.list_models()
799 if ns_uuid not in models:
800 try:
801 self.models[ns_uuid] = await self.controller.add_model(
802 ns_uuid
803 )
804 except JujuError as e:
805 if "already exists" not in e.message:
806 raise e
Adam Israel7bf2f4d2019-03-15 15:28:47 -0400807
808 # Create an observer for this model
809 await self.create_model_monitor(ns_uuid)
810
Adam Israel6d84dbd2019-03-08 18:33:35 -0500811 return True
812
813 async def DestroyNetworkService(self, ns_uuid):
814 """Destroy a Network Service.
815
816 Destroy the Network Service and any deployed charms.
817
818 :param ns_uuid The unique id of the Network Service
819
820 :returns: True if the model was created. Raises JujuError on failure.
821 """
822
823 # Do not delete the default model. The default model was used by all
824 # Network Services, prior to the implementation of a model per NS.
Adam Israelc4f393e2019-03-19 16:33:30 -0400825 if ns_uuid.lower() == "default":
Adam Israel6d84dbd2019-03-08 18:33:35 -0500826 return False
827
828 if not self.authenticated:
829 self.log.debug("Authenticating with Juju")
830 await self.login()
831
832 # Disconnect from the Model
833 if ns_uuid in self.models:
834 await self.disconnect_model(self.models[ns_uuid])
835
836 try:
837 await self.controller.destroy_models(ns_uuid)
Adam Israelc4f393e2019-03-19 16:33:30 -0400838 except JujuError:
Adam Israel6d84dbd2019-03-08 18:33:35 -0500839 raise NetworkServiceDoesNotExist(
840 "The Network Service '{}' does not exist".format(ns_uuid)
841 )
842
843 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500844
Adam Israelb5214512018-05-03 10:00:04 -0400845 async def GetMetrics(self, model_name, application_name):
846 """Get the metrics collected by the VCA.
847
Adam Israel85a4b212018-11-29 20:30:24 -0500848 :param model_name The name or unique id of the network service
Adam Israelb5214512018-05-03 10:00:04 -0400849 :param application_name The name of the application
850 """
851 metrics = {}
852 model = await self.get_model(model_name)
853 app = await self.get_application(model, application_name)
854 if app:
855 metrics = await app.get_metrics()
856
857 return metrics
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500858
Adam Israelfa329072018-09-14 11:26:13 -0400859 async def HasApplication(self, model_name, application_name):
860 model = await self.get_model(model_name)
861 app = await self.get_application(model, application_name)
862 if app:
863 return True
864 return False
865
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500866 # Non-public methods
Adam Israel136186e2018-09-14 12:01:12 -0400867 async def add_relation(self, model_name, relation1, relation2):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500868 """
869 Add a relation between two application endpoints.
870
Adam Israel85a4b212018-11-29 20:30:24 -0500871 :param str model_name: The name or unique id of the network service
872 :param str relation1: '<application>[:<relation_name>]'
873 :param str relation2: '<application>[:<relation_name>]'
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500874 """
Adam Israel136186e2018-09-14 12:01:12 -0400875
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500876 if not self.authenticated:
877 await self.login()
878
Adam Israel136186e2018-09-14 12:01:12 -0400879 m = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500880 try:
Adam Israel136186e2018-09-14 12:01:12 -0400881 await m.add_relation(relation1, relation2)
882 except JujuAPIError as e:
883 # If one of the applications in the relationship doesn't exist,
884 # or the relation has already been added, let the operation fail
885 # silently.
886 if 'not found' in e.message:
887 return
888 if 'already exists' in e.message:
889 return
890
891 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500892
Adam Israelb5214512018-05-03 10:00:04 -0400893 # async def apply_config(self, config, application):
894 # """Apply a configuration to the application."""
895 # print("JujuApi: Applying configuration to {}.".format(
896 # application
897 # ))
898 # return await self.set_config(application=application, config=config)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500899
900 def _get_config_from_dict(self, config_primitive, values):
Adam Israel88a49632018-04-10 13:04:57 -0600901 """Transform the yang config primitive to dict.
902
903 Expected result:
904
905 config = {
906 'config':
907 }
908 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500909 config = {}
910 for primitive in config_primitive:
911 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600912 # config = self._map_primitive_parameters()
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500913 for parameter in primitive['parameter']:
914 param = str(parameter['name'])
915 if parameter['value'] == "<rw_mgmt_ip>":
916 config[param] = str(values[parameter['value']])
917 else:
918 config[param] = str(parameter['value'])
919
920 return config
921
tierno1afb30a2018-12-21 13:42:43 +0000922 def _map_primitive_parameters(self, parameters, user_values):
Adam Israel88a49632018-04-10 13:04:57 -0600923 params = {}
924 for parameter in parameters:
925 param = str(parameter['name'])
tierno1afb30a2018-12-21 13:42:43 +0000926 value = parameter.get('value')
927
928 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
929 # Must exist at user_values except if there is a default value
930 if isinstance(value, str) and value.startswith("<") and value.endswith(">"):
931 if parameter['value'][1:-1] in user_values:
932 value = user_values[parameter['value'][1:-1]]
933 elif 'default-value' in parameter:
934 value = parameter['default-value']
935 else:
936 raise KeyError("parameter {}='{}' not supplied ".format(param, value))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400937
Adam Israelbf793522018-11-20 13:54:13 -0500938 # If there's no value, use the default-value (if set)
tierno1afb30a2018-12-21 13:42:43 +0000939 if value is None and 'default-value' in parameter:
Adam Israelbf793522018-11-20 13:54:13 -0500940 value = parameter['default-value']
941
Adam Israel5e08a0e2018-09-06 19:22:47 -0400942 # Typecast parameter value, if present
tierno1afb30a2018-12-21 13:42:43 +0000943 paramtype = "string"
944 try:
945 if 'data-type' in parameter:
946 paramtype = str(parameter['data-type']).lower()
Adam Israel5e08a0e2018-09-06 19:22:47 -0400947
tierno1afb30a2018-12-21 13:42:43 +0000948 if paramtype == "integer":
949 value = int(value)
950 elif paramtype == "boolean":
951 value = bool(value)
952 else:
953 value = str(value)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400954 else:
tierno1afb30a2018-12-21 13:42:43 +0000955 # If there's no data-type, assume the value is a string
956 value = str(value)
957 except ValueError:
958 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param, value, paramtype))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400959
tierno1afb30a2018-12-21 13:42:43 +0000960 params[param] = value
Adam Israel88a49632018-04-10 13:04:57 -0600961 return params
962
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500963 def _get_config_from_yang(self, config_primitive, values):
964 """Transform the yang config primitive to dict."""
965 config = {}
966 for primitive in config_primitive.values():
967 if primitive['name'] == 'config':
968 for parameter in primitive['parameter'].values():
969 param = str(parameter['name'])
970 if parameter['value'] == "<rw_mgmt_ip>":
971 config[param] = str(values[parameter['value']])
972 else:
973 config[param] = str(parameter['value'])
974
975 return config
976
977 def FormatApplicationName(self, *args):
978 """
979 Generate a Juju-compatible Application name
980
981 :param args tuple: Positional arguments to be used to construct the
982 application name.
983
984 Limitations::
985 - Only accepts characters a-z and non-consequitive dashes (-)
986 - Application name should not exceed 50 characters
987
988 Examples::
989
990 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
991 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500992 appname = ""
993 for c in "-".join(list(args)):
994 if c.isdigit():
995 c = chr(97 + int(c))
996 elif not c.isalpha():
997 c = "-"
998 appname += c
Adam Israel6d84dbd2019-03-08 18:33:35 -0500999 return re.sub('-+', '-', appname.lower())
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001000
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001001 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
1002 # """Format the name of the application
1003 #
1004 # Limitations:
1005 # - Only accepts characters a-z and non-consequitive dashes (-)
1006 # - Application name should not exceed 50 characters
1007 # """
1008 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1009 # new_name = ''
1010 # for c in name:
1011 # if c.isdigit():
1012 # c = chr(97 + int(c))
1013 # elif not c.isalpha():
1014 # c = "-"
1015 # new_name += c
1016 # return re.sub('\-+', '-', new_name.lower())
1017
1018 def format_model_name(self, name):
1019 """Format the name of model.
1020
1021 Model names may only contain lowercase letters, digits and hyphens
1022 """
1023
1024 return name.replace('_', '-').lower()
1025
1026 async def get_application(self, model, application):
1027 """Get the deployed application."""
1028 if not self.authenticated:
1029 await self.login()
1030
1031 app = None
1032 if application and model:
1033 if model.applications:
1034 if application in model.applications:
1035 app = model.applications[application]
1036
1037 return app
1038
Adam Israel85a4b212018-11-29 20:30:24 -05001039 async def get_model(self, model_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001040 """Get a model from the Juju Controller.
1041
1042 Note: Model objects returned must call disconnected() before it goes
1043 out of scope."""
1044 if not self.authenticated:
1045 await self.login()
1046
1047 if model_name not in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001048 # Get the models in the controller
1049 models = await self.controller.list_models()
1050
1051 if model_name not in models:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001052 try:
1053 self.models[model_name] = await self.controller.add_model(
1054 model_name
1055 )
1056 except JujuError as e:
1057 if "already exists" not in e.message:
1058 raise e
Adam Israel85a4b212018-11-29 20:30:24 -05001059 else:
1060 self.models[model_name] = await self.controller.get_model(
1061 model_name
1062 )
1063
Adam Israelfc511ed2018-09-21 14:20:55 +02001064 self.refcount['model'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001065
Adam Israel28a43c02018-04-23 16:04:54 -04001066 # Create an observer for this model
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001067 await self.create_model_monitor(model_name)
1068
1069 return self.models[model_name]
1070
1071 async def create_model_monitor(self, model_name):
1072 """Create a monitor for the model, if none exists."""
1073 if not self.authenticated:
1074 await self.login()
1075
1076 if model_name not in self.monitors:
Adam Israel28a43c02018-04-23 16:04:54 -04001077 self.monitors[model_name] = VCAMonitor(model_name)
1078 self.models[model_name].add_observer(self.monitors[model_name])
1079
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001080 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001081
1082 async def login(self):
1083 """Login to the Juju controller."""
1084
1085 if self.authenticated:
1086 return
1087
1088 self.connecting = True
1089
1090 self.log.debug("JujuApi: Logging into controller")
1091
1092 cacert = None
Adam Israel5e08a0e2018-09-06 19:22:47 -04001093 self.controller = Controller(loop=self.loop)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001094
1095 if self.secret:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001096 self.log.debug(
1097 "Connecting to controller... ws://{}:{} as {}/{}".format(
1098 self.endpoint,
1099 self.port,
1100 self.user,
1101 self.secret,
1102 )
1103 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001104 await self.controller.connect(
1105 endpoint=self.endpoint,
1106 username=self.user,
1107 password=self.secret,
1108 cacert=cacert,
1109 )
Adam Israelfc511ed2018-09-21 14:20:55 +02001110 self.refcount['controller'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001111 else:
1112 # current_controller no longer exists
1113 # self.log.debug("Connecting to current controller...")
1114 # await self.controller.connect_current()
Adam Israel88a49632018-04-10 13:04:57 -06001115 # await self.controller.connect(
1116 # endpoint=self.endpoint,
1117 # username=self.user,
1118 # cacert=cacert,
1119 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001120 self.log.fatal("VCA credentials not configured.")
1121
1122 self.authenticated = True
1123 self.log.debug("JujuApi: Logged into controller")
1124
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001125 async def logout(self):
1126 """Logout of the Juju controller."""
1127 if not self.authenticated:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001128 return False
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001129
1130 try:
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001131 for model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001132 await self.disconnect_model(model)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001133
1134 if self.controller:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001135 self.log.debug("Disconnecting controller {}".format(
1136 self.controller
1137 ))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001138 await self.controller.disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +02001139 self.refcount['controller'] -= 1
Adam Israel5e08a0e2018-09-06 19:22:47 -04001140 self.controller = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001141
1142 self.authenticated = False
Adam Israelfc511ed2018-09-21 14:20:55 +02001143
1144 self.log.debug(self.refcount)
1145
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001146 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001147 self.log.fatal(
1148 "Fatal error logging out of Juju Controller: {}".format(e)
1149 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001150 raise e
Adam Israel6d84dbd2019-03-08 18:33:35 -05001151 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001152
Adam Israel85a4b212018-11-29 20:30:24 -05001153 async def disconnect_model(self, model):
1154 self.log.debug("Disconnecting model {}".format(model))
1155 if model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001156 print("Disconnecting model")
1157 await self.models[model].disconnect()
1158 self.refcount['model'] -= 1
1159 self.models[model] = None
1160
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001161 # async def remove_application(self, name):
1162 # """Remove the application."""
1163 # if not self.authenticated:
1164 # await self.login()
1165 #
1166 # app = await self.get_application(name)
1167 # if app:
1168 # self.log.debug("JujuApi: Destroying application {}".format(
1169 # name,
1170 # ))
1171 #
1172 # await app.destroy()
1173
1174 async def remove_relation(self, a, b):
1175 """
1176 Remove a relation between two application endpoints
1177
1178 :param a An application endpoint
1179 :param b An application endpoint
1180 """
1181 if not self.authenticated:
1182 await self.login()
1183
1184 m = await self.get_model()
1185 try:
1186 m.remove_relation(a, b)
1187 finally:
1188 await m.disconnect()
1189
Adam Israel85a4b212018-11-29 20:30:24 -05001190 async def resolve_error(self, model_name, application=None):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001191 """Resolve units in error state."""
1192 if not self.authenticated:
1193 await self.login()
1194
Adam Israel85a4b212018-11-29 20:30:24 -05001195 model = await self.get_model(model_name)
1196
1197 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001198 if app:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001199 self.log.debug(
1200 "JujuApi: Resolving errors for application {}".format(
1201 application,
1202 )
1203 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001204
1205 for unit in app.units:
1206 app.resolved(retry=True)
1207
Adam Israel85a4b212018-11-29 20:30:24 -05001208 async def run_action(self, model_name, application, action_name, **params):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001209 """Execute an action and return an Action object."""
1210 if not self.authenticated:
1211 await self.login()
1212 result = {
1213 'status': '',
1214 'action': {
1215 'tag': None,
1216 'results': None,
1217 }
1218 }
Adam Israel85a4b212018-11-29 20:30:24 -05001219
1220 model = await self.get_model(model_name)
1221
1222 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001223 if app:
1224 # We currently only have one unit per application
1225 # so use the first unit available.
1226 unit = app.units[0]
1227
Adam Israel5e08a0e2018-09-06 19:22:47 -04001228 self.log.debug(
1229 "JujuApi: Running Action {} against Application {}".format(
1230 action_name,
1231 application,
1232 )
1233 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001234
1235 action = await unit.run_action(action_name, **params)
1236
1237 # Wait for the action to complete
1238 await action.wait()
1239
1240 result['status'] = action.status
1241 result['action']['tag'] = action.data['id']
1242 result['action']['results'] = action.results
1243
1244 return result
1245
Adam Israelb5214512018-05-03 10:00:04 -04001246 async def set_config(self, model_name, application, config):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001247 """Apply a configuration to the application."""
1248 if not self.authenticated:
1249 await self.login()
1250
Adam Israelb5214512018-05-03 10:00:04 -04001251 app = await self.get_application(model_name, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001252 if app:
1253 self.log.debug("JujuApi: Setting config for Application {}".format(
1254 application,
1255 ))
1256 await app.set_config(config)
1257
1258 # Verify the config is set
1259 newconf = await app.get_config()
1260 for key in config:
1261 if config[key] != newconf[key]['value']:
1262 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
1263
Adam Israelb5214512018-05-03 10:00:04 -04001264 # async def set_parameter(self, parameter, value, application=None):
1265 # """Set a config parameter for a service."""
1266 # if not self.authenticated:
1267 # await self.login()
1268 #
1269 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1270 # parameter,
1271 # value,
1272 # application,
1273 # ))
1274 # return await self.apply_config(
1275 # {parameter: value},
1276 # application=application,
1277 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001278
Adam Israel5e08a0e2018-09-06 19:22:47 -04001279 async def wait_for_application(self, model_name, application_name,
1280 timeout=300):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001281 """Wait for an application to become active."""
1282 if not self.authenticated:
1283 await self.login()
1284
Adam Israel5e08a0e2018-09-06 19:22:47 -04001285 model = await self.get_model(model_name)
1286
1287 app = await self.get_application(model, application_name)
1288 self.log.debug("Application: {}".format(app))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001289 if app:
1290 self.log.debug(
1291 "JujuApi: Waiting {} seconds for Application {}".format(
1292 timeout,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001293 application_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001294 )
1295 )
1296
Adam Israel5e08a0e2018-09-06 19:22:47 -04001297 await model.block_until(
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001298 lambda: all(
Adam Israel5e08a0e2018-09-06 19:22:47 -04001299 unit.agent_status == 'idle' and unit.workload_status in
1300 ['active', 'unknown'] for unit in app.units
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001301 ),
1302 timeout=timeout
1303 )