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