User parameters provided at instantiation time for Day-1 configuration mapping initia...
[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
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 # Quiet the debug logging
47 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
48 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
49 logging.getLogger('juju.model').setLevel(logging.WARN)
50 logging.getLogger('juju.machine').setLevel(logging.WARN)
51
52
53 class VCAMonitor(ModelObserver):
54 """Monitor state changes within the Juju Model."""
55 log = None
56 ns_name = None
57 applications = {}
58
59 def __init__(self, ns_name):
60 self.log = logging.getLogger(__name__)
61
62 self.ns_name = ns_name
63
64 def AddApplication(self, application_name, callback, *callback_args):
65 if application_name not in self.applications:
66 self.applications[application_name] = {
67 'callback': callback,
68 'callback_args': callback_args
69 }
70
71 def RemoveApplication(self, application_name):
72 if application_name in self.applications:
73 del self.applications[application_name]
74
75 async def on_change(self, delta, old, new, model):
76 """React to changes in the Juju model."""
77
78 if delta.entity == "unit":
79 # Ignore change events from other applications
80 if delta.data['application'] not in self.applications.keys():
81 return
82
83 try:
84
85 application_name = delta.data['application']
86
87 callback = self.applications[application_name]['callback']
88 callback_args = \
89 self.applications[application_name]['callback_args']
90
91 if old and new:
92 # Fire off a callback with the application state
93 if callback:
94 callback(
95 self.ns_name,
96 delta.data['application'],
97 new.workload_status,
98 new.workload_status_message,
99 *callback_args)
100
101 if old and not new:
102 # This is a charm being removed
103 if callback:
104 callback(
105 self.ns_name,
106 delta.data['application'],
107 "removed",
108 "",
109 *callback_args)
110 except Exception as e:
111 self.log.debug("[1] notify_callback exception: {}".format(e))
112
113 elif delta.entity == "action":
114 # TODO: Decide how we want to notify the user of actions
115
116 # uuid = delta.data['id'] # The Action's unique id
117 # msg = delta.data['message'] # The output of the action
118 #
119 # if delta.data['status'] == "pending":
120 # # The action is queued
121 # pass
122 # elif delta.data['status'] == "completed""
123 # # The action was successful
124 # pass
125 # elif delta.data['status'] == "failed":
126 # # The action failed.
127 # pass
128
129 pass
130
131 ########
132 # TODO
133 #
134 # Create unique models per network service
135 # Document all public functions
136
137
138 class N2VC:
139 def __init__(self,
140 log=None,
141 server='127.0.0.1',
142 port=17070,
143 user='admin',
144 secret=None,
145 artifacts=None,
146 loop=None,
147 ):
148 """Initialize N2VC
149
150 :param vcaconfig dict A dictionary containing the VCA configuration
151
152 :param artifacts str The directory where charms required by a vnfd are
153 stored.
154
155 :Example:
156 n2vc = N2VC(vcaconfig={
157 'secret': 'MzI3MDJhOTYxYmM0YzRjNTJiYmY1Yzdm',
158 'user': 'admin',
159 'ip-address': '10.44.127.137',
160 'port': 17070,
161 'artifacts': '/path/to/charms'
162 })
163 """
164
165 # Initialize instance-level variables
166 self.api = None
167 self.log = None
168 self.controller = None
169 self.connecting = False
170 self.authenticated = False
171
172 # For debugging
173 self.refcount = {
174 'controller': 0,
175 'model': 0,
176 }
177
178 self.models = {}
179
180 # Model Observers
181 self.monitors = {}
182
183 # VCA config
184 self.hostname = ""
185 self.port = 17070
186 self.username = ""
187 self.secret = ""
188
189 if log:
190 self.log = log
191 else:
192 self.log = logging.getLogger(__name__)
193
194 # Quiet websocket traffic
195 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
196 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
197 logging.getLogger('model').setLevel(logging.WARN)
198 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
199
200 self.log.debug('JujuApi: instantiated')
201
202 self.server = server
203 self.port = port
204
205 self.secret = secret
206 if user.startswith('user-'):
207 self.user = user
208 else:
209 self.user = 'user-{}'.format(user)
210
211 self.endpoint = '%s:%d' % (server, int(port))
212
213 self.artifacts = artifacts
214
215 self.loop = loop or asyncio.get_event_loop()
216
217 def __del__(self):
218 """Close any open connections."""
219 yield self.logout()
220
221 def notify_callback(self, model_name, application_name, status, message,
222 callback=None, *callback_args):
223 try:
224 if callback:
225 callback(
226 model_name,
227 application_name,
228 status, message,
229 *callback_args,
230 )
231 except Exception as e:
232 self.log.error("[0] notify_callback exception {}".format(e))
233 raise e
234 return True
235
236 # Public methods
237 async def Relate(self, model_name, vnfd):
238 """Create a relation between the charm-enabled VDUs in a VNF.
239
240 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
241
242 vdu:
243 ...
244 relation:
245 - provides: dataVM:db
246 requires: mgmtVM:app
247
248 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.
249
250 :param str ns_name: The name of the network service.
251 :param dict vnfd: The parsed yaml VNF descriptor.
252 """
253
254 # Currently, the call to Relate() is made automatically after the
255 # deployment of each charm; if the relation depends on a charm that
256 # hasn't been deployed yet, the call will fail silently. This will
257 # prevent an API breakage, with the intent of making this an explicitly
258 # required call in a more object-oriented refactor of the N2VC API.
259
260 configs = []
261 vnf_config = vnfd.get("vnf-configuration")
262 if vnf_config:
263 juju = vnf_config['juju']
264 if juju:
265 configs.append(vnf_config)
266
267 for vdu in vnfd['vdu']:
268 vdu_config = vdu.get('vdu-configuration')
269 if vdu_config:
270 juju = vdu_config['juju']
271 if juju:
272 configs.append(vdu_config)
273
274 def _get_application_name(name):
275 """Get the application name that's mapped to a vnf/vdu."""
276 vnf_member_index = 0
277 vnf_name = vnfd['name']
278
279 for vdu in vnfd.get('vdu'):
280 # Compare the named portion of the relation to the vdu's id
281 if vdu['id'] == name:
282 application_name = self.FormatApplicationName(
283 model_name,
284 vnf_name,
285 str(vnf_member_index),
286 )
287 return application_name
288 else:
289 vnf_member_index += 1
290
291 return None
292
293 # Loop through relations
294 for cfg in configs:
295 if 'juju' in cfg:
296 if 'relation' in juju:
297 for rel in juju['relation']:
298 try:
299
300 # get the application name for the provides
301 (name, endpoint) = rel['provides'].split(':')
302 application_name = _get_application_name(name)
303
304 provides = "{}:{}".format(
305 application_name,
306 endpoint
307 )
308
309 # get the application name for thr requires
310 (name, endpoint) = rel['requires'].split(':')
311 application_name = _get_application_name(name)
312
313 requires = "{}:{}".format(
314 application_name,
315 endpoint
316 )
317 self.log.debug("Relation: {} <-> {}".format(
318 provides,
319 requires
320 ))
321 await self.add_relation(
322 model_name,
323 provides,
324 requires,
325 )
326 except Exception as e:
327 self.log.debug("Exception: {}".format(e))
328
329 return
330
331 async def DeployCharms(self, model_name, application_name, vnfd,
332 charm_path, params={}, machine_spec={},
333 callback=None, *callback_args):
334 """Deploy one or more charms associated with a VNF.
335
336 Deploy the charm(s) referenced in a VNF Descriptor.
337
338 :param str model_name: The name or unique id of the network service.
339 :param str application_name: The name of the application
340 :param dict vnfd: The name of the application
341 :param str charm_path: The path to the Juju charm
342 :param dict params: A dictionary of runtime parameters
343 Examples::
344 {
345 'rw_mgmt_ip': '1.2.3.4',
346 # Pass the initial-config-primitives section of the vnf or vdu
347 'initial-config-primitives': {...}
348 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
349 inside < >. rw_mgmt_ip will be included here also
350 }
351 :param dict machine_spec: A dictionary describing the machine to
352 install to
353 Examples::
354 {
355 'hostname': '1.2.3.4',
356 'username': 'ubuntu',
357 }
358 :param obj callback: A callback function to receive status changes.
359 :param tuple callback_args: A list of arguments to be passed to the
360 callback
361 """
362
363 ########################################################
364 # Verify the path to the charm exists and is readable. #
365 ########################################################
366 if not os.path.exists(charm_path):
367 self.log.debug("Charm path doesn't exist: {}".format(charm_path))
368 self.notify_callback(
369 model_name,
370 application_name,
371 "failed",
372 callback,
373 *callback_args,
374 )
375 raise JujuCharmNotFound("No artifacts configured.")
376
377 ################################
378 # Login to the Juju controller #
379 ################################
380 if not self.authenticated:
381 self.log.debug("Authenticating with Juju")
382 await self.login()
383
384 ##########################################
385 # Get the model for this network service #
386 ##########################################
387 model = await self.get_model(model_name)
388
389 ########################################
390 # Verify the application doesn't exist #
391 ########################################
392 app = await self.get_application(model, application_name)
393 if app:
394 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
395
396 ################################################################
397 # Register this application with the model-level event monitor #
398 ################################################################
399 if callback:
400 self.monitors[model_name].AddApplication(
401 application_name,
402 callback,
403 *callback_args
404 )
405
406 ########################################################
407 # Check for specific machine placement (native charms) #
408 ########################################################
409 to = ""
410 if machine_spec.keys():
411 if all(k in machine_spec for k in ['host', 'user']):
412 # Enlist an existing machine as a Juju unit
413 machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
414 machine_spec['user'],
415 machine_spec['host'],
416 self.GetPrivateKeyPath(),
417 ))
418 to = machine.id
419
420 #######################################
421 # Get the initial charm configuration #
422 #######################################
423
424 rw_mgmt_ip = None
425 if 'rw_mgmt_ip' in params:
426 rw_mgmt_ip = params['rw_mgmt_ip']
427
428 if 'initial-config-primitive' not in params:
429 params['initial-config-primitive'] = {}
430
431 initial_config = self._get_config_from_dict(
432 params['initial-config-primitive'],
433 {'<rw_mgmt_ip>': rw_mgmt_ip}
434 )
435
436 self.log.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
437 model_name,
438 application_name,
439 charm_path,
440 to=to,
441 ))
442
443 ########################################################
444 # Deploy the charm and apply the initial configuration #
445 ########################################################
446 app = await model.deploy(
447 # We expect charm_path to be either the path to the charm on disk
448 # or in the format of cs:series/name
449 charm_path,
450 # This is the formatted, unique name for this charm
451 application_name=application_name,
452 # Proxy charms should use the current LTS. This will need to be
453 # changed for native charms.
454 series='xenial',
455 # Apply the initial 'config' primitive during deployment
456 config=initial_config,
457 # Where to deploy the charm to.
458 to=to,
459 )
460
461 # Map the vdu id<->app name,
462 #
463 await self.Relate(model_name, vnfd)
464
465 # #######################################
466 # # Execute initial config primitive(s) #
467 # #######################################
468 uuids = await self.ExecuteInitialPrimitives(
469 model_name,
470 application_name,
471 params,
472 )
473 return uuids
474
475 # primitives = {}
476 #
477 # # Build a sequential list of the primitives to execute
478 # for primitive in params['initial-config-primitive']:
479 # try:
480 # if primitive['name'] == 'config':
481 # # This is applied when the Application is deployed
482 # pass
483 # else:
484 # seq = primitive['seq']
485 #
486 # params = {}
487 # if 'parameter' in primitive:
488 # params = primitive['parameter']
489 #
490 # primitives[seq] = {
491 # 'name': primitive['name'],
492 # 'parameters': self._map_primitive_parameters(
493 # params,
494 # {'<rw_mgmt_ip>': rw_mgmt_ip}
495 # ),
496 # }
497 #
498 # for primitive in sorted(primitives):
499 # await self.ExecutePrimitive(
500 # model_name,
501 # application_name,
502 # primitives[primitive]['name'],
503 # callback,
504 # callback_args,
505 # **primitives[primitive]['parameters'],
506 # )
507 # except N2VCPrimitiveExecutionFailed as e:
508 # self.log.debug(
509 # "[N2VC] Exception executing primitive: {}".format(e)
510 # )
511 # raise
512
513 async def GetPrimitiveStatus(self, model_name, uuid):
514 """Get the status of an executed Primitive.
515
516 The status of an executed Primitive will be one of three values:
517 - completed
518 - failed
519 - running
520 """
521 status = None
522 try:
523 if not self.authenticated:
524 await self.login()
525
526 model = await self.get_model(model_name)
527
528 results = await model.get_action_status(uuid)
529
530 if uuid in results:
531 status = results[uuid]
532
533 except Exception as e:
534 self.log.debug(
535 "Caught exception while getting primitive status: {}".format(e)
536 )
537 raise N2VCPrimitiveExecutionFailed(e)
538
539 return status
540
541 async def GetPrimitiveOutput(self, model_name, uuid):
542 """Get the output of an executed Primitive.
543
544 Note: this only returns output for a successfully executed primitive.
545 """
546 results = None
547 try:
548 if not self.authenticated:
549 await self.login()
550
551 model = await self.get_model(model_name)
552 results = await model.get_action_output(uuid, 60)
553 except Exception as e:
554 self.log.debug(
555 "Caught exception while getting primitive status: {}".format(e)
556 )
557 raise N2VCPrimitiveExecutionFailed(e)
558
559 return results
560
561 # async def ProvisionMachine(self, model_name, hostname, username):
562 # """Provision machine for usage with Juju.
563 #
564 # Provisions a previously instantiated machine for use with Juju.
565 # """
566 # try:
567 # if not self.authenticated:
568 # await self.login()
569 #
570 # # FIXME: This is hard-coded until model-per-ns is added
571 # model_name = 'default'
572 #
573 # model = await self.get_model(model_name)
574 # model.add_machine(spec={})
575 #
576 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
577 # "ubuntu",
578 # host['address'],
579 # private_key_path,
580 # ))
581 # return machine.id
582 #
583 # except Exception as e:
584 # self.log.debug(
585 # "Caught exception while getting primitive status: {}".format(e)
586 # )
587 # raise N2VCPrimitiveExecutionFailed(e)
588
589 def GetPrivateKeyPath(self):
590 homedir = os.environ['HOME']
591 sshdir = "{}/.ssh".format(homedir)
592 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
593 return private_key_path
594
595 async def GetPublicKey(self):
596 """Get the N2VC SSH public key.abs
597
598 Returns the SSH public key, to be injected into virtual machines to
599 be managed by the VCA.
600
601 The first time this is run, a ssh keypair will be created. The public
602 key is injected into a VM so that we can provision the machine with
603 Juju, after which Juju will communicate with the VM directly via the
604 juju agent.
605 """
606 public_key = ""
607
608 # Find the path to where we expect our key to live.
609 homedir = os.environ['HOME']
610 sshdir = "{}/.ssh".format(homedir)
611 if not os.path.exists(sshdir):
612 os.mkdir(sshdir)
613
614 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
615 public_key_path = "{}.pub".format(private_key_path)
616
617 # If we don't have a key generated, generate it.
618 if not os.path.exists(private_key_path):
619 cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format(
620 "rsa",
621 "4096",
622 private_key_path
623 )
624 subprocess.check_output(shlex.split(cmd))
625
626 # Read the public key
627 with open(public_key_path, "r") as f:
628 public_key = f.readline()
629
630 return public_key
631
632 async def ExecuteInitialPrimitives(self, model_name, application_name,
633 params, callback=None, *callback_args):
634 """Execute multiple primitives.
635
636 Execute multiple primitives as declared in initial-config-primitive.
637 This is useful in cases where the primitives initially failed -- for
638 example, if the charm is a proxy but the proxy hasn't been configured
639 yet.
640 """
641 uuids = []
642 primitives = {}
643
644 # Build a sequential list of the primitives to execute
645 for primitive in params['initial-config-primitive']:
646 try:
647 if primitive['name'] == 'config':
648 pass
649 else:
650 seq = primitive['seq']
651
652 params_ = {}
653 if 'parameter' in primitive:
654 params_ = primitive['parameter']
655
656 user_values = params.get("user_values", {})
657 if 'rw_mgmt_ip' not in user_values:
658 user_values['rw_mgmt_ip'] = None
659 # just for backward compatibility, because it will be provided always by modern version of LCM
660
661 primitives[seq] = {
662 'name': primitive['name'],
663 'parameters': self._map_primitive_parameters(
664 params_,
665 user_values
666 ),
667 }
668
669 for primitive in sorted(primitives):
670 uuids.append(
671 await self.ExecutePrimitive(
672 model_name,
673 application_name,
674 primitives[primitive]['name'],
675 callback,
676 callback_args,
677 **primitives[primitive]['parameters'],
678 )
679 )
680 except N2VCPrimitiveExecutionFailed as e:
681 self.log.debug(
682 "[N2VC] Exception executing primitive: {}".format(e)
683 )
684 raise
685 return uuids
686
687 async def ExecutePrimitive(self, model_name, application_name, primitive,
688 callback, *callback_args, **params):
689 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
690
691 Execute a primitive defined in the VNF descriptor.
692
693 :param str model_name: The name or unique id of the network service.
694 :param str application_name: The name of the application
695 :param str primitive: The name of the primitive to execute.
696 :param obj callback: A callback function to receive status changes.
697 :param tuple callback_args: A list of arguments to be passed to the
698 callback function.
699 :param dict params: A dictionary of key=value pairs representing the
700 primitive's parameters
701 Examples::
702 {
703 'rw_mgmt_ip': '1.2.3.4',
704 # Pass the initial-config-primitives section of the vnf or vdu
705 'initial-config-primitives': {...}
706 }
707 """
708 self.log.debug("Executing primitive={} params={}".format(primitive, params))
709 uuid = None
710 try:
711 if not self.authenticated:
712 await self.login()
713
714 model = await self.get_model(model_name)
715
716 if primitive == 'config':
717 # config is special, and expecting params to be a dictionary
718 await self.set_config(
719 model,
720 application_name,
721 params['params'],
722 )
723 else:
724 app = await self.get_application(model, application_name)
725 if app:
726 # Run against the first (and probably only) unit in the app
727 unit = app.units[0]
728 if unit:
729 action = await unit.run_action(primitive, **params)
730 uuid = action.id
731 except Exception as e:
732 self.log.debug(
733 "Caught exception while executing primitive: {}".format(e)
734 )
735 raise N2VCPrimitiveExecutionFailed(e)
736 return uuid
737
738 async def RemoveCharms(self, model_name, application_name, callback=None,
739 *callback_args):
740 """Remove a charm from the VCA.
741
742 Remove a charm referenced in a VNF Descriptor.
743
744 :param str model_name: The name of the network service.
745 :param str application_name: The name of the application
746 :param obj callback: A callback function to receive status changes.
747 :param tuple callback_args: A list of arguments to be passed to the
748 callback function.
749 """
750 try:
751 if not self.authenticated:
752 await self.login()
753
754 model = await self.get_model(model_name)
755 app = await self.get_application(model, application_name)
756 if app:
757 # Remove this application from event monitoring
758 self.monitors[model_name].RemoveApplication(application_name)
759
760 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
761 self.log.debug(
762 "Removing the application {}".format(application_name)
763 )
764 await app.remove()
765
766 await self.disconnect_model(self.monitors[model_name])
767
768 # Notify the callback that this charm has been removed.
769 self.notify_callback(
770 model_name,
771 application_name,
772 "removed",
773 callback,
774 *callback_args,
775 )
776
777 except Exception as e:
778 print("Caught exception: {}".format(e))
779 self.log.debug(e)
780 raise e
781
782 async def DestroyNetworkService(self, nsd):
783 raise NotImplementedError()
784
785 async def GetMetrics(self, model_name, application_name):
786 """Get the metrics collected by the VCA.
787
788 :param model_name The name or unique id of the network service
789 :param application_name The name of the application
790 """
791 metrics = {}
792 model = await self.get_model(model_name)
793 app = await self.get_application(model, application_name)
794 if app:
795 metrics = await app.get_metrics()
796
797 return metrics
798
799 async def HasApplication(self, model_name, application_name):
800 model = await self.get_model(model_name)
801 app = await self.get_application(model, application_name)
802 if app:
803 return True
804 return False
805
806 # Non-public methods
807 async def add_relation(self, model_name, relation1, relation2):
808 """
809 Add a relation between two application endpoints.
810
811 :param str model_name: The name or unique id of the network service
812 :param str relation1: '<application>[:<relation_name>]'
813 :param str relation2: '<application>[:<relation_name>]'
814 """
815
816 if not self.authenticated:
817 await self.login()
818
819 m = await self.get_model(model_name)
820 try:
821 await m.add_relation(relation1, relation2)
822 except JujuAPIError as e:
823 # If one of the applications in the relationship doesn't exist,
824 # or the relation has already been added, let the operation fail
825 # silently.
826 if 'not found' in e.message:
827 return
828 if 'already exists' in e.message:
829 return
830
831 raise e
832
833 # async def apply_config(self, config, application):
834 # """Apply a configuration to the application."""
835 # print("JujuApi: Applying configuration to {}.".format(
836 # application
837 # ))
838 # return await self.set_config(application=application, config=config)
839
840 def _get_config_from_dict(self, config_primitive, values):
841 """Transform the yang config primitive to dict.
842
843 Expected result:
844
845 config = {
846 'config':
847 }
848 """
849 config = {}
850 for primitive in config_primitive:
851 if primitive['name'] == 'config':
852 # config = self._map_primitive_parameters()
853 for parameter in primitive['parameter']:
854 param = str(parameter['name'])
855 if parameter['value'] == "<rw_mgmt_ip>":
856 config[param] = str(values[parameter['value']])
857 else:
858 config[param] = str(parameter['value'])
859
860 return config
861
862 def _map_primitive_parameters(self, parameters, user_values):
863 params = {}
864 for parameter in parameters:
865 param = str(parameter['name'])
866 value = parameter.get('value')
867
868 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
869 # Must exist at user_values except if there is a default value
870 if isinstance(value, str) and value.startswith("<") and value.endswith(">"):
871 if parameter['value'][1:-1] in user_values:
872 value = user_values[parameter['value'][1:-1]]
873 elif 'default-value' in parameter:
874 value = parameter['default-value']
875 else:
876 raise KeyError("parameter {}='{}' not supplied ".format(param, value))
877
878 # If there's no value, use the default-value (if set)
879 if value is None and 'default-value' in parameter:
880 value = parameter['default-value']
881
882 # Typecast parameter value, if present
883 paramtype = "string"
884 try:
885 if 'data-type' in parameter:
886 paramtype = str(parameter['data-type']).lower()
887
888 if paramtype == "integer":
889 value = int(value)
890 elif paramtype == "boolean":
891 value = bool(value)
892 else:
893 value = str(value)
894 else:
895 # If there's no data-type, assume the value is a string
896 value = str(value)
897 except ValueError:
898 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param, value, paramtype))
899
900 params[param] = value
901 return params
902
903 def _get_config_from_yang(self, config_primitive, values):
904 """Transform the yang config primitive to dict."""
905 config = {}
906 for primitive in config_primitive.values():
907 if primitive['name'] == 'config':
908 for parameter in primitive['parameter'].values():
909 param = str(parameter['name'])
910 if parameter['value'] == "<rw_mgmt_ip>":
911 config[param] = str(values[parameter['value']])
912 else:
913 config[param] = str(parameter['value'])
914
915 return config
916
917 def FormatApplicationName(self, *args):
918 """
919 Generate a Juju-compatible Application name
920
921 :param args tuple: Positional arguments to be used to construct the
922 application name.
923
924 Limitations::
925 - Only accepts characters a-z and non-consequitive dashes (-)
926 - Application name should not exceed 50 characters
927
928 Examples::
929
930 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
931 """
932 appname = ""
933 for c in "-".join(list(args)):
934 if c.isdigit():
935 c = chr(97 + int(c))
936 elif not c.isalpha():
937 c = "-"
938 appname += c
939 return re.sub('\-+', '-', appname.lower())
940
941 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
942 # """Format the name of the application
943 #
944 # Limitations:
945 # - Only accepts characters a-z and non-consequitive dashes (-)
946 # - Application name should not exceed 50 characters
947 # """
948 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
949 # new_name = ''
950 # for c in name:
951 # if c.isdigit():
952 # c = chr(97 + int(c))
953 # elif not c.isalpha():
954 # c = "-"
955 # new_name += c
956 # return re.sub('\-+', '-', new_name.lower())
957
958 def format_model_name(self, name):
959 """Format the name of model.
960
961 Model names may only contain lowercase letters, digits and hyphens
962 """
963
964 return name.replace('_', '-').lower()
965
966 async def get_application(self, model, application):
967 """Get the deployed application."""
968 if not self.authenticated:
969 await self.login()
970
971 app = None
972 if application and model:
973 if model.applications:
974 if application in model.applications:
975 app = model.applications[application]
976
977 return app
978
979 async def get_model(self, model_name):
980 """Get a model from the Juju Controller.
981
982 Note: Model objects returned must call disconnected() before it goes
983 out of scope."""
984 if not self.authenticated:
985 await self.login()
986
987 if model_name not in self.models:
988 # Get the models in the controller
989 models = await self.controller.list_models()
990
991 if model_name not in models:
992 self.models[model_name] = await self.controller.add_model(
993 model_name
994 )
995 else:
996 self.models[model_name] = await self.controller.get_model(
997 model_name
998 )
999
1000 self.refcount['model'] += 1
1001
1002 # Create an observer for this model
1003 self.monitors[model_name] = VCAMonitor(model_name)
1004 self.models[model_name].add_observer(self.monitors[model_name])
1005
1006 return self.models[model_name]
1007
1008 async def login(self):
1009 """Login to the Juju controller."""
1010
1011 if self.authenticated:
1012 return
1013
1014 self.connecting = True
1015
1016 self.log.debug("JujuApi: Logging into controller")
1017
1018 cacert = None
1019 self.controller = Controller(loop=self.loop)
1020
1021 if self.secret:
1022 self.log.debug(
1023 "Connecting to controller... ws://{}:{} as {}/{}".format(
1024 self.endpoint,
1025 self.port,
1026 self.user,
1027 self.secret,
1028 )
1029 )
1030 await self.controller.connect(
1031 endpoint=self.endpoint,
1032 username=self.user,
1033 password=self.secret,
1034 cacert=cacert,
1035 )
1036 self.refcount['controller'] += 1
1037 else:
1038 # current_controller no longer exists
1039 # self.log.debug("Connecting to current controller...")
1040 # await self.controller.connect_current()
1041 # await self.controller.connect(
1042 # endpoint=self.endpoint,
1043 # username=self.user,
1044 # cacert=cacert,
1045 # )
1046 self.log.fatal("VCA credentials not configured.")
1047
1048 self.authenticated = True
1049 self.log.debug("JujuApi: Logged into controller")
1050
1051 async def logout(self):
1052 """Logout of the Juju controller."""
1053 if not self.authenticated:
1054 return
1055
1056 try:
1057 for model in self.models:
1058 await self.disconnect_model(model)
1059
1060 if self.controller:
1061 self.log.debug("Disconnecting controller {}".format(
1062 self.controller
1063 ))
1064 await self.controller.disconnect()
1065 self.refcount['controller'] -= 1
1066 self.controller = None
1067
1068 self.authenticated = False
1069
1070 self.log.debug(self.refcount)
1071
1072 except Exception as e:
1073 self.log.fatal(
1074 "Fatal error logging out of Juju Controller: {}".format(e)
1075 )
1076 raise e
1077
1078 async def disconnect_model(self, model):
1079 self.log.debug("Disconnecting model {}".format(model))
1080 if model in self.models:
1081 print(self.models[model].applications)
1082 if len(self.models[model].applications) == 0:
1083 print("Destroying empty model")
1084 await self.controller.destroy_models(model)
1085
1086 print("Disconnecting model")
1087 await self.models[model].disconnect()
1088 self.refcount['model'] -= 1
1089 self.models[model] = None
1090
1091 # async def remove_application(self, name):
1092 # """Remove the application."""
1093 # if not self.authenticated:
1094 # await self.login()
1095 #
1096 # app = await self.get_application(name)
1097 # if app:
1098 # self.log.debug("JujuApi: Destroying application {}".format(
1099 # name,
1100 # ))
1101 #
1102 # await app.destroy()
1103
1104 async def remove_relation(self, a, b):
1105 """
1106 Remove a relation between two application endpoints
1107
1108 :param a An application endpoint
1109 :param b An application endpoint
1110 """
1111 if not self.authenticated:
1112 await self.login()
1113
1114 m = await self.get_model()
1115 try:
1116 m.remove_relation(a, b)
1117 finally:
1118 await m.disconnect()
1119
1120 async def resolve_error(self, model_name, application=None):
1121 """Resolve units in error state."""
1122 if not self.authenticated:
1123 await self.login()
1124
1125 model = await self.get_model(model_name)
1126
1127 app = await self.get_application(model, application)
1128 if app:
1129 self.log.debug(
1130 "JujuApi: Resolving errors for application {}".format(
1131 application,
1132 )
1133 )
1134
1135 for unit in app.units:
1136 app.resolved(retry=True)
1137
1138 async def run_action(self, model_name, application, action_name, **params):
1139 """Execute an action and return an Action object."""
1140 if not self.authenticated:
1141 await self.login()
1142 result = {
1143 'status': '',
1144 'action': {
1145 'tag': None,
1146 'results': None,
1147 }
1148 }
1149
1150 model = await self.get_model(model_name)
1151
1152 app = await self.get_application(model, application)
1153 if app:
1154 # We currently only have one unit per application
1155 # so use the first unit available.
1156 unit = app.units[0]
1157
1158 self.log.debug(
1159 "JujuApi: Running Action {} against Application {}".format(
1160 action_name,
1161 application,
1162 )
1163 )
1164
1165 action = await unit.run_action(action_name, **params)
1166
1167 # Wait for the action to complete
1168 await action.wait()
1169
1170 result['status'] = action.status
1171 result['action']['tag'] = action.data['id']
1172 result['action']['results'] = action.results
1173
1174 return result
1175
1176 async def set_config(self, model_name, application, config):
1177 """Apply a configuration to the application."""
1178 if not self.authenticated:
1179 await self.login()
1180
1181 app = await self.get_application(model_name, application)
1182 if app:
1183 self.log.debug("JujuApi: Setting config for Application {}".format(
1184 application,
1185 ))
1186 await app.set_config(config)
1187
1188 # Verify the config is set
1189 newconf = await app.get_config()
1190 for key in config:
1191 if config[key] != newconf[key]['value']:
1192 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
1193
1194 # async def set_parameter(self, parameter, value, application=None):
1195 # """Set a config parameter for a service."""
1196 # if not self.authenticated:
1197 # await self.login()
1198 #
1199 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1200 # parameter,
1201 # value,
1202 # application,
1203 # ))
1204 # return await self.apply_config(
1205 # {parameter: value},
1206 # application=application,
1207 # )
1208
1209 async def wait_for_application(self, model_name, application_name,
1210 timeout=300):
1211 """Wait for an application to become active."""
1212 if not self.authenticated:
1213 await self.login()
1214
1215 model = await self.get_model(model_name)
1216
1217 app = await self.get_application(model, application_name)
1218 self.log.debug("Application: {}".format(app))
1219 if app:
1220 self.log.debug(
1221 "JujuApi: Waiting {} seconds for Application {}".format(
1222 timeout,
1223 application_name,
1224 )
1225 )
1226
1227 await model.block_until(
1228 lambda: all(
1229 unit.agent_status == 'idle' and unit.workload_status in
1230 ['active', 'unknown'] for unit in app.units
1231 ),
1232 timeout=timeout
1233 )