6e4aaf37e8bd013663eb4d03c6dbe766408cdb3f
[osm/N2VC.git] / n2vc / vnf.py
1 import asyncio
2 import logging
3 import os
4 import os.path
5 import re
6 import shlex
7 import ssl
8 import subprocess
9 import sys
10 # import time
11
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
15 path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
16 path = os.path.join(path, "modules/libjuju/")
17 if path not in sys.path:
18 sys.path.insert(1, path)
19
20 from juju.controller import Controller
21 from juju.model import ModelObserver
22 from juju.errors import JujuAPIError, JujuError
23
24 # We might need this to connect to the websocket securely, but test and verify.
25 try:
26 ssl._create_default_https_context = ssl._create_unverified_context
27 except 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
34 class JujuCharmNotFound(Exception):
35 """The Charm can't be found or is not readable."""
36
37
38 class JujuApplicationExists(Exception):
39 """The Application already exists."""
40
41
42 class N2VCPrimitiveExecutionFailed(Exception):
43 """Something failed while attempting to execute a primitive."""
44
45
46 class NetworkServiceDoesNotExist(Exception):
47 """The Network Service being acted against does not exist."""
48
49
50 # Quiet the debug logging
51 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
52 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
53 logging.getLogger('juju.model').setLevel(logging.WARN)
54 logging.getLogger('juju.machine').setLevel(logging.WARN)
55
56
57 class VCAMonitor(ModelObserver):
58 """Monitor state changes within the Juju Model."""
59 log = None
60 ns_name = None
61 applications = {}
62
63 def __init__(self, ns_name):
64 self.log = logging.getLogger(__name__)
65
66 self.ns_name = ns_name
67
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]
78
79 async def on_change(self, delta, old, new, model):
80 """React to changes in the Juju model."""
81
82 if delta.entity == "unit":
83 # Ignore change events from other applications
84 if delta.data['application'] not in self.applications.keys():
85 return
86
87 try:
88
89 application_name = delta.data['application']
90
91 callback = self.applications[application_name]['callback']
92 callback_args = \
93 self.applications[application_name]['callback_args']
94
95 if old and new:
96 # 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)
104
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",
112 "",
113 *callback_args)
114 except Exception as e:
115 self.log.debug("[1] notify_callback exception: {}".format(e))
116
117 elif delta.entity == "action":
118 # TODO: Decide how we want to notify the user of actions
119
120 # 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
134
135 ########
136 # TODO
137 #
138 # Create unique models per network service
139 # Document all public functions
140
141
142 class N2VC:
143 def __init__(self,
144 log=None,
145 server='127.0.0.1',
146 port=17070,
147 user='admin',
148 secret=None,
149 artifacts=None,
150 loop=None,
151 ):
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 })
167 """
168
169 # 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
176 # For debugging
177 self.refcount = {
178 'controller': 0,
179 'model': 0,
180 }
181
182 self.models = {}
183
184 # Model Observers
185 self.monitors = {}
186
187 # VCA config
188 self.hostname = ""
189 self.port = 17070
190 self.username = ""
191 self.secret = ""
192
193 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
219 self.loop = loop or asyncio.get_event_loop()
220
221 def __del__(self):
222 """Close any open connections."""
223 yield self.logout()
224
225 def notify_callback(self, model_name, application_name, status, message,
226 callback=None, *callback_args):
227 try:
228 if callback:
229 callback(
230 model_name,
231 application_name,
232 status, message,
233 *callback_args,
234 )
235 except Exception as e:
236 self.log.error("[0] notify_callback exception {}".format(e))
237 raise e
238 return True
239
240 # Public methods
241 async def Relate(self, model_name, vnfd):
242 """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(
287 model_name,
288 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(
326 model_name,
327 provides,
328 requires,
329 )
330 except Exception as e:
331 self.log.debug("Exception: {}".format(e))
332
333 return
334
335 async def DeployCharms(self, model_name, application_name, vnfd,
336 charm_path, params={}, machine_spec={},
337 callback=None, *callback_args):
338 """Deploy one or more charms associated with a VNF.
339
340 Deploy the charm(s) referenced in a VNF Descriptor.
341
342 :param str model_name: The name or unique id of the network service.
343 :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 {
349 'rw_mgmt_ip': '1.2.3.4',
350 # Pass the initial-config-primitives section of the vnf or vdu
351 'initial-config-primitives': {...}
352 '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
354 }
355 :param dict machine_spec: A dictionary describing the machine to
356 install to
357 Examples::
358 {
359 'hostname': '1.2.3.4',
360 'username': 'ubuntu',
361 }
362 :param obj callback: A callback function to receive status changes.
363 :param tuple callback_args: A list of arguments to be passed to the
364 callback
365 """
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))
372 self.notify_callback(
373 model_name,
374 application_name,
375 "failed",
376 callback,
377 *callback_args,
378 )
379 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 ##########################################
391 model = await self.get_model(model_name)
392
393 ########################################
394 # Verify the application doesn't exist #
395 ########################################
396 app = await self.get_application(model, application_name)
397 if app:
398 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
399
400 ################################################################
401 # Register this application with the model-level event monitor #
402 ################################################################
403 if callback:
404 self.monitors[model_name].AddApplication(
405 application_name,
406 callback,
407 *callback_args
408 )
409
410 ########################################################
411 # Check for specific machine placement (native charms) #
412 ########################################################
413 to = ""
414 if machine_spec.keys():
415 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 ))
422 to = machine.id
423
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
432 if 'initial-config-primitive' not in params:
433 params['initial-config-primitive'] = {}
434
435 initial_config = self._get_config_from_dict(
436 params['initial-config-primitive'],
437 {'<rw_mgmt_ip>': rw_mgmt_ip}
438 )
439
440 self.log.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
441 model_name,
442 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(
451 # We expect charm_path to be either the path to the charm on disk
452 # or in the format of cs:series/name
453 charm_path,
454 # This is the formatted, unique name for this charm
455 application_name=application_name,
456 # Proxy charms should use the current LTS. This will need to be
457 # changed for native charms.
458 series='xenial',
459 # Apply the initial 'config' primitive during deployment
460 config=initial_config,
461 # Where to deploy the charm to.
462 to=to,
463 )
464
465 # Map the vdu id<->app name,
466 #
467 await self.Relate(model_name, vnfd)
468
469 # #######################################
470 # # Execute initial config primitive(s) #
471 # #######################################
472 uuids = await self.ExecuteInitialPrimitives(
473 model_name,
474 application_name,
475 params,
476 )
477 return uuids
478
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
530 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
555 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
565 # 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
636 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 = []
646 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':
652 pass
653 else:
654 seq = primitive['seq']
655
656 params_ = {}
657 if 'parameter' in primitive:
658 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
664
665 primitives[seq] = {
666 'name': primitive['name'],
667 'parameters': self._map_primitive_parameters(
668 params_,
669 user_values
670 ),
671 }
672
673 for primitive in sorted(primitives):
674 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 )
683 )
684 except N2VCPrimitiveExecutionFailed as e:
685 self.log.debug(
686 "[N2VC] Exception executing primitive: {}".format(e)
687 )
688 raise
689 return uuids
690
691 async def ExecutePrimitive(self, model_name, application_name, primitive,
692 callback, *callback_args, **params):
693 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
694
695 Execute a primitive defined in the VNF descriptor.
696
697 :param str model_name: The name or unique id of the network service.
698 :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.
701 :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
705 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 }
711 """
712 self.log.debug("Executing primitive={} params={}".format(primitive, params))
713 uuid = None
714 try:
715 if not self.authenticated:
716 await self.login()
717
718 model = await self.get_model(model_name)
719
720 if primitive == 'config':
721 # config is special, and expecting params to be a dictionary
722 await self.set_config(
723 model,
724 application_name,
725 params['params'],
726 )
727 else:
728 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:
733 action = await unit.run_action(primitive, **params)
734 uuid = action.id
735 except Exception as e:
736 self.log.debug(
737 "Caught exception while executing primitive: {}".format(e)
738 )
739 raise N2VCPrimitiveExecutionFailed(e)
740 return uuid
741
742 async def RemoveCharms(self, model_name, application_name, callback=None,
743 *callback_args):
744 """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.
751 :param tuple callback_args: A list of arguments to be passed to the
752 callback function.
753 """
754 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:
761 # 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)
765 self.log.debug(
766 "Removing the application {}".format(application_name)
767 )
768 await app.remove()
769
770 await self.disconnect_model(self.monitors[model_name])
771
772 # Notify the callback that this charm has been removed.
773 self.notify_callback(
774 model_name,
775 application_name,
776 "removed",
777 callback,
778 *callback_args,
779 )
780
781 except Exception as e:
782 print("Caught exception: {}".format(e))
783 self.log.debug(e)
784 raise e
785
786 async def CreateNetworkService(self, ns_uuid):
787 """Create a new Juju model for the Network Service.
788
789 Creates a new Model in the Juju Controller.
790
791 :param str ns_uuid: A unique id representing an instaance of a
792 Network Service.
793
794 :returns: True if the model was created. Raises JujuError on failure.
795 """
796 if not self.authenticated:
797 await self.login()
798
799 models = await self.controller.list_models()
800 if ns_uuid not in models:
801 try:
802 self.models[ns_uuid] = await self.controller.add_model(
803 ns_uuid
804 )
805 except JujuError as e:
806 if "already exists" not in e.message:
807 raise e
808 return True
809
810 async def DestroyNetworkService(self, ns_uuid):
811 """Destroy a Network Service.
812
813 Destroy the Network Service and any deployed charms.
814
815 :param ns_uuid The unique id of the Network Service
816
817 :returns: True if the model was created. Raises JujuError on failure.
818 """
819
820 # Do not delete the default model. The default model was used by all
821 # Network Services, prior to the implementation of a model per NS.
822 if ns_uuid.lower() is "default":
823 return False
824
825 if not self.authenticated:
826 self.log.debug("Authenticating with Juju")
827 await self.login()
828
829 # Disconnect from the Model
830 if ns_uuid in self.models:
831 await self.disconnect_model(self.models[ns_uuid])
832
833 try:
834 await self.controller.destroy_models(ns_uuid)
835 except JujuError as e:
836 raise NetworkServiceDoesNotExist(
837 "The Network Service '{}' does not exist".format(ns_uuid)
838 )
839
840 return True
841
842 async def GetMetrics(self, model_name, application_name):
843 """Get the metrics collected by the VCA.
844
845 :param model_name The name or unique id of the network service
846 :param application_name The name of the application
847 """
848 metrics = {}
849 model = await self.get_model(model_name)
850 app = await self.get_application(model, application_name)
851 if app:
852 metrics = await app.get_metrics()
853
854 return metrics
855
856 async def HasApplication(self, model_name, application_name):
857 model = await self.get_model(model_name)
858 app = await self.get_application(model, application_name)
859 if app:
860 return True
861 return False
862
863 # Non-public methods
864 async def add_relation(self, model_name, relation1, relation2):
865 """
866 Add a relation between two application endpoints.
867
868 :param str model_name: The name or unique id of the network service
869 :param str relation1: '<application>[:<relation_name>]'
870 :param str relation2: '<application>[:<relation_name>]'
871 """
872
873 if not self.authenticated:
874 await self.login()
875
876 m = await self.get_model(model_name)
877 try:
878 await m.add_relation(relation1, relation2)
879 except JujuAPIError as e:
880 # If one of the applications in the relationship doesn't exist,
881 # or the relation has already been added, let the operation fail
882 # silently.
883 if 'not found' in e.message:
884 return
885 if 'already exists' in e.message:
886 return
887
888 raise e
889
890 # async def apply_config(self, config, application):
891 # """Apply a configuration to the application."""
892 # print("JujuApi: Applying configuration to {}.".format(
893 # application
894 # ))
895 # return await self.set_config(application=application, config=config)
896
897 def _get_config_from_dict(self, config_primitive, values):
898 """Transform the yang config primitive to dict.
899
900 Expected result:
901
902 config = {
903 'config':
904 }
905 """
906 config = {}
907 for primitive in config_primitive:
908 if primitive['name'] == 'config':
909 # config = self._map_primitive_parameters()
910 for parameter in primitive['parameter']:
911 param = str(parameter['name'])
912 if parameter['value'] == "<rw_mgmt_ip>":
913 config[param] = str(values[parameter['value']])
914 else:
915 config[param] = str(parameter['value'])
916
917 return config
918
919 def _map_primitive_parameters(self, parameters, user_values):
920 params = {}
921 for parameter in parameters:
922 param = str(parameter['name'])
923 value = parameter.get('value')
924
925 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
926 # Must exist at user_values except if there is a default value
927 if isinstance(value, str) and value.startswith("<") and value.endswith(">"):
928 if parameter['value'][1:-1] in user_values:
929 value = user_values[parameter['value'][1:-1]]
930 elif 'default-value' in parameter:
931 value = parameter['default-value']
932 else:
933 raise KeyError("parameter {}='{}' not supplied ".format(param, value))
934
935 # If there's no value, use the default-value (if set)
936 if value is None and 'default-value' in parameter:
937 value = parameter['default-value']
938
939 # Typecast parameter value, if present
940 paramtype = "string"
941 try:
942 if 'data-type' in parameter:
943 paramtype = str(parameter['data-type']).lower()
944
945 if paramtype == "integer":
946 value = int(value)
947 elif paramtype == "boolean":
948 value = bool(value)
949 else:
950 value = str(value)
951 else:
952 # If there's no data-type, assume the value is a string
953 value = str(value)
954 except ValueError:
955 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param, value, paramtype))
956
957 params[param] = value
958 return params
959
960 def _get_config_from_yang(self, config_primitive, values):
961 """Transform the yang config primitive to dict."""
962 config = {}
963 for primitive in config_primitive.values():
964 if primitive['name'] == 'config':
965 for parameter in primitive['parameter'].values():
966 param = str(parameter['name'])
967 if parameter['value'] == "<rw_mgmt_ip>":
968 config[param] = str(values[parameter['value']])
969 else:
970 config[param] = str(parameter['value'])
971
972 return config
973
974 def FormatApplicationName(self, *args):
975 """
976 Generate a Juju-compatible Application name
977
978 :param args tuple: Positional arguments to be used to construct the
979 application name.
980
981 Limitations::
982 - Only accepts characters a-z and non-consequitive dashes (-)
983 - Application name should not exceed 50 characters
984
985 Examples::
986
987 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
988 """
989 appname = ""
990 for c in "-".join(list(args)):
991 if c.isdigit():
992 c = chr(97 + int(c))
993 elif not c.isalpha():
994 c = "-"
995 appname += c
996 return re.sub('-+', '-', appname.lower())
997
998 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
999 # """Format the name of the application
1000 #
1001 # Limitations:
1002 # - Only accepts characters a-z and non-consequitive dashes (-)
1003 # - Application name should not exceed 50 characters
1004 # """
1005 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1006 # new_name = ''
1007 # for c in name:
1008 # if c.isdigit():
1009 # c = chr(97 + int(c))
1010 # elif not c.isalpha():
1011 # c = "-"
1012 # new_name += c
1013 # return re.sub('\-+', '-', new_name.lower())
1014
1015 def format_model_name(self, name):
1016 """Format the name of model.
1017
1018 Model names may only contain lowercase letters, digits and hyphens
1019 """
1020
1021 return name.replace('_', '-').lower()
1022
1023 async def get_application(self, model, application):
1024 """Get the deployed application."""
1025 if not self.authenticated:
1026 await self.login()
1027
1028 app = None
1029 if application and model:
1030 if model.applications:
1031 if application in model.applications:
1032 app = model.applications[application]
1033
1034 return app
1035
1036 async def get_model(self, model_name):
1037 """Get a model from the Juju Controller.
1038
1039 Note: Model objects returned must call disconnected() before it goes
1040 out of scope."""
1041 if not self.authenticated:
1042 await self.login()
1043
1044 if model_name not in self.models:
1045 # Get the models in the controller
1046 models = await self.controller.list_models()
1047
1048 if model_name not in models:
1049 try:
1050 self.models[model_name] = await self.controller.add_model(
1051 model_name
1052 )
1053 except JujuError as e:
1054 if "already exists" not in e.message:
1055 raise e
1056 else:
1057 self.models[model_name] = await self.controller.get_model(
1058 model_name
1059 )
1060
1061 self.refcount['model'] += 1
1062
1063 # Create an observer for this model
1064 self.monitors[model_name] = VCAMonitor(model_name)
1065 self.models[model_name].add_observer(self.monitors[model_name])
1066
1067 return self.models[model_name]
1068
1069 async def login(self):
1070 """Login to the Juju controller."""
1071
1072 if self.authenticated:
1073 return
1074
1075 self.connecting = True
1076
1077 self.log.debug("JujuApi: Logging into controller")
1078
1079 cacert = None
1080 self.controller = Controller(loop=self.loop)
1081
1082 if self.secret:
1083 self.log.debug(
1084 "Connecting to controller... ws://{}:{} as {}/{}".format(
1085 self.endpoint,
1086 self.port,
1087 self.user,
1088 self.secret,
1089 )
1090 )
1091 await self.controller.connect(
1092 endpoint=self.endpoint,
1093 username=self.user,
1094 password=self.secret,
1095 cacert=cacert,
1096 )
1097 self.refcount['controller'] += 1
1098 else:
1099 # current_controller no longer exists
1100 # self.log.debug("Connecting to current controller...")
1101 # await self.controller.connect_current()
1102 # await self.controller.connect(
1103 # endpoint=self.endpoint,
1104 # username=self.user,
1105 # cacert=cacert,
1106 # )
1107 self.log.fatal("VCA credentials not configured.")
1108
1109 self.authenticated = True
1110 self.log.debug("JujuApi: Logged into controller")
1111
1112 async def logout(self):
1113 """Logout of the Juju controller."""
1114 if not self.authenticated:
1115 return False
1116
1117 try:
1118 for model in self.models:
1119 await self.disconnect_model(model)
1120
1121 if self.controller:
1122 self.log.debug("Disconnecting controller {}".format(
1123 self.controller
1124 ))
1125 await self.controller.disconnect()
1126 self.refcount['controller'] -= 1
1127 self.controller = None
1128
1129 self.authenticated = False
1130
1131 self.log.debug(self.refcount)
1132
1133 except Exception as e:
1134 self.log.fatal(
1135 "Fatal error logging out of Juju Controller: {}".format(e)
1136 )
1137 raise e
1138 return True
1139
1140 async def disconnect_model(self, model):
1141 self.log.debug("Disconnecting model {}".format(model))
1142 if model in self.models:
1143 print("Disconnecting model")
1144 await self.models[model].disconnect()
1145 self.refcount['model'] -= 1
1146 self.models[model] = None
1147
1148 # async def remove_application(self, name):
1149 # """Remove the application."""
1150 # if not self.authenticated:
1151 # await self.login()
1152 #
1153 # app = await self.get_application(name)
1154 # if app:
1155 # self.log.debug("JujuApi: Destroying application {}".format(
1156 # name,
1157 # ))
1158 #
1159 # await app.destroy()
1160
1161 async def remove_relation(self, a, b):
1162 """
1163 Remove a relation between two application endpoints
1164
1165 :param a An application endpoint
1166 :param b An application endpoint
1167 """
1168 if not self.authenticated:
1169 await self.login()
1170
1171 m = await self.get_model()
1172 try:
1173 m.remove_relation(a, b)
1174 finally:
1175 await m.disconnect()
1176
1177 async def resolve_error(self, model_name, application=None):
1178 """Resolve units in error state."""
1179 if not self.authenticated:
1180 await self.login()
1181
1182 model = await self.get_model(model_name)
1183
1184 app = await self.get_application(model, application)
1185 if app:
1186 self.log.debug(
1187 "JujuApi: Resolving errors for application {}".format(
1188 application,
1189 )
1190 )
1191
1192 for unit in app.units:
1193 app.resolved(retry=True)
1194
1195 async def run_action(self, model_name, application, action_name, **params):
1196 """Execute an action and return an Action object."""
1197 if not self.authenticated:
1198 await self.login()
1199 result = {
1200 'status': '',
1201 'action': {
1202 'tag': None,
1203 'results': None,
1204 }
1205 }
1206
1207 model = await self.get_model(model_name)
1208
1209 app = await self.get_application(model, application)
1210 if app:
1211 # We currently only have one unit per application
1212 # so use the first unit available.
1213 unit = app.units[0]
1214
1215 self.log.debug(
1216 "JujuApi: Running Action {} against Application {}".format(
1217 action_name,
1218 application,
1219 )
1220 )
1221
1222 action = await unit.run_action(action_name, **params)
1223
1224 # Wait for the action to complete
1225 await action.wait()
1226
1227 result['status'] = action.status
1228 result['action']['tag'] = action.data['id']
1229 result['action']['results'] = action.results
1230
1231 return result
1232
1233 async def set_config(self, model_name, application, config):
1234 """Apply a configuration to the application."""
1235 if not self.authenticated:
1236 await self.login()
1237
1238 app = await self.get_application(model_name, application)
1239 if app:
1240 self.log.debug("JujuApi: Setting config for Application {}".format(
1241 application,
1242 ))
1243 await app.set_config(config)
1244
1245 # Verify the config is set
1246 newconf = await app.get_config()
1247 for key in config:
1248 if config[key] != newconf[key]['value']:
1249 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
1250
1251 # async def set_parameter(self, parameter, value, application=None):
1252 # """Set a config parameter for a service."""
1253 # if not self.authenticated:
1254 # await self.login()
1255 #
1256 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1257 # parameter,
1258 # value,
1259 # application,
1260 # ))
1261 # return await self.apply_config(
1262 # {parameter: value},
1263 # application=application,
1264 # )
1265
1266 async def wait_for_application(self, model_name, application_name,
1267 timeout=300):
1268 """Wait for an application to become active."""
1269 if not self.authenticated:
1270 await self.login()
1271
1272 model = await self.get_model(model_name)
1273
1274 app = await self.get_application(model, application_name)
1275 self.log.debug("Application: {}".format(app))
1276 if app:
1277 self.log.debug(
1278 "JujuApi: Waiting {} seconds for Application {}".format(
1279 timeout,
1280 application_name,
1281 )
1282 )
1283
1284 await model.block_until(
1285 lambda: all(
1286 unit.agent_status == 'idle' and unit.workload_status in
1287 ['active', 'unknown'] for unit in app.units
1288 ),
1289 timeout=timeout
1290 )