1c79aed6544af489e8f561d6a3b81df65c6b5944
[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 ['host', 'user']):
433 # Enlist an existing machine as a Juju unit
434 machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
435 machine_spec['user'],
436 machine_spec['host'],
437 self.GetPrivateKeyPath(),
438 ))
439 to = machine.id
440
441 #######################################
442 # Get the initial charm configuration #
443 #######################################
444
445 rw_mgmt_ip = None
446 if 'rw_mgmt_ip' in params:
447 rw_mgmt_ip = params['rw_mgmt_ip']
448
449 if 'initial-config-primitive' not in params:
450 params['initial-config-primitive'] = {}
451
452 initial_config = self._get_config_from_dict(
453 params['initial-config-primitive'],
454 {'<rw_mgmt_ip>': rw_mgmt_ip}
455 )
456
457 self.log.debug("JujuApi: Deploying charm ({}) from {}".format(
458 application_name,
459 charm_path,
460 to=to,
461 ))
462
463 ########################################################
464 # Deploy the charm and apply the initial configuration #
465 ########################################################
466 app = await model.deploy(
467 # We expect charm_path to be either the path to the charm on disk
468 # or in the format of cs:series/name
469 charm_path,
470 # This is the formatted, unique name for this charm
471 application_name=application_name,
472 # Proxy charms should use the current LTS. This will need to be
473 # changed for native charms.
474 series='xenial',
475 # Apply the initial 'config' primitive during deployment
476 config=initial_config,
477 # Where to deploy the charm to.
478 to=to,
479 )
480
481 # Map the vdu id<->app name,
482 #
483 await self.Relate(model_name, vnfd)
484
485 # #######################################
486 # # Execute initial config primitive(s) #
487 # #######################################
488 uuids = await self.ExecuteInitialPrimitives(
489 model_name,
490 application_name,
491 params,
492 )
493 return uuids
494
495 # primitives = {}
496 #
497 # # Build a sequential list of the primitives to execute
498 # for primitive in params['initial-config-primitive']:
499 # try:
500 # if primitive['name'] == 'config':
501 # # This is applied when the Application is deployed
502 # pass
503 # else:
504 # seq = primitive['seq']
505 #
506 # params = {}
507 # if 'parameter' in primitive:
508 # params = primitive['parameter']
509 #
510 # primitives[seq] = {
511 # 'name': primitive['name'],
512 # 'parameters': self._map_primitive_parameters(
513 # params,
514 # {'<rw_mgmt_ip>': rw_mgmt_ip}
515 # ),
516 # }
517 #
518 # for primitive in sorted(primitives):
519 # await self.ExecutePrimitive(
520 # model_name,
521 # application_name,
522 # primitives[primitive]['name'],
523 # callback,
524 # callback_args,
525 # **primitives[primitive]['parameters'],
526 # )
527 # except N2VCPrimitiveExecutionFailed as e:
528 # self.log.debug(
529 # "[N2VC] Exception executing primitive: {}".format(e)
530 # )
531 # raise
532
533 async def GetPrimitiveStatus(self, model_name, uuid):
534 """Get the status of an executed Primitive.
535
536 The status of an executed Primitive will be one of three values:
537 - completed
538 - failed
539 - running
540 """
541 status = None
542 try:
543 if not self.authenticated:
544 await self.login()
545
546 # FIXME: This is hard-coded until model-per-ns is added
547 model_name = 'default'
548
549 model = await self.get_model(model_name)
550
551 results = await model.get_action_status(uuid)
552
553 if uuid in results:
554 status = results[uuid]
555
556 except Exception as e:
557 self.log.debug(
558 "Caught exception while getting primitive status: {}".format(e)
559 )
560 raise N2VCPrimitiveExecutionFailed(e)
561
562 return status
563
564 async def GetPrimitiveOutput(self, model_name, uuid):
565 """Get the output of an executed Primitive.
566
567 Note: this only returns output for a successfully executed primitive.
568 """
569 results = None
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 results = await model.get_action_output(uuid, 60)
579 except Exception as e:
580 self.log.debug(
581 "Caught exception while getting primitive status: {}".format(e)
582 )
583 raise N2VCPrimitiveExecutionFailed(e)
584
585 return results
586
587 # async def ProvisionMachine(self, model_name, hostname, username):
588 # """Provision machine for usage with Juju.
589 #
590 # Provisions a previously instantiated machine for use with Juju.
591 # """
592 # try:
593 # if not self.authenticated:
594 # await self.login()
595 #
596 # # FIXME: This is hard-coded until model-per-ns is added
597 # model_name = 'default'
598 #
599 # model = await self.get_model(model_name)
600 # model.add_machine(spec={})
601 #
602 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
603 # "ubuntu",
604 # host['address'],
605 # private_key_path,
606 # ))
607 # return machine.id
608 #
609 # except Exception as e:
610 # self.log.debug(
611 # "Caught exception while getting primitive status: {}".format(e)
612 # )
613 # raise N2VCPrimitiveExecutionFailed(e)
614
615 def GetPrivateKeyPath(self):
616 homedir = os.environ['HOME']
617 sshdir = "{}/.ssh".format(homedir)
618 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
619 return private_key_path
620
621 async def GetPublicKey(self):
622 """Get the N2VC SSH public key.abs
623
624 Returns the SSH public key, to be injected into virtual machines to
625 be managed by the VCA.
626
627 The first time this is run, a ssh keypair will be created. The public
628 key is injected into a VM so that we can provision the machine with
629 Juju, after which Juju will communicate with the VM directly via the
630 juju agent.
631 """
632 public_key = ""
633
634 # Find the path to where we expect our key to live.
635 homedir = os.environ['HOME']
636 sshdir = "{}/.ssh".format(homedir)
637 if not os.path.exists(sshdir):
638 os.mkdir(sshdir)
639
640 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
641 public_key_path = "{}.pub".format(private_key_path)
642
643 # If we don't have a key generated, generate it.
644 if not os.path.exists(private_key_path):
645 cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format(
646 "rsa",
647 "4096",
648 private_key_path
649 )
650 subprocess.check_output(shlex.split(cmd))
651
652 # Read the public key
653 with open(public_key_path, "r") as f:
654 public_key = f.readline()
655
656 return public_key
657
658 async def ExecuteInitialPrimitives(self, model_name, application_name,
659 params, callback=None, *callback_args):
660 """Execute multiple primitives.
661
662 Execute multiple primitives as declared in initial-config-primitive.
663 This is useful in cases where the primitives initially failed -- for
664 example, if the charm is a proxy but the proxy hasn't been configured
665 yet.
666 """
667 uuids = []
668 primitives = {}
669
670 # Build a sequential list of the primitives to execute
671 for primitive in params['initial-config-primitive']:
672 try:
673 if primitive['name'] == 'config':
674 pass
675 else:
676 seq = primitive['seq']
677
678 params = {}
679 if 'parameter' in primitive:
680 params = primitive['parameter']
681
682 primitives[seq] = {
683 'name': primitive['name'],
684 'parameters': self._map_primitive_parameters(
685 params,
686 {'<rw_mgmt_ip>': None}
687 ),
688 }
689
690 for primitive in sorted(primitives):
691 uuids.append(
692 await self.ExecutePrimitive(
693 model_name,
694 application_name,
695 primitives[primitive]['name'],
696 callback,
697 callback_args,
698 **primitives[primitive]['parameters'],
699 )
700 )
701 except N2VCPrimitiveExecutionFailed as e:
702 self.log.debug(
703 "[N2VC] Exception executing primitive: {}".format(e)
704 )
705 raise
706 return uuids
707
708 async def ExecutePrimitive(self, model_name, application_name, primitive,
709 callback, *callback_args, **params):
710 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
711
712 Execute a primitive defined in the VNF descriptor.
713
714 :param str model_name: The name of the network service.
715 :param str application_name: The name of the application
716 :param str primitive: The name of the primitive to execute.
717 :param obj callback: A callback function to receive status changes.
718 :param tuple callback_args: A list of arguments to be passed to the
719 callback function.
720 :param dict params: A dictionary of key=value pairs representing the
721 primitive's parameters
722 Examples::
723 {
724 'rw_mgmt_ip': '1.2.3.4',
725 # Pass the initial-config-primitives section of the vnf or vdu
726 'initial-config-primitives': {...}
727 }
728 """
729 self.log.debug("Executing {}".format(primitive))
730 uuid = None
731 try:
732 if not self.authenticated:
733 await self.login()
734
735 # FIXME: This is hard-coded until model-per-ns is added
736 model_name = 'default'
737
738 model = await self.get_model(model_name)
739
740 if primitive == 'config':
741 # config is special, and expecting params to be a dictionary
742 await self.set_config(
743 model,
744 application_name,
745 params['params'],
746 )
747 else:
748 app = await self.get_application(model, application_name)
749 if app:
750 # Run against the first (and probably only) unit in the app
751 unit = app.units[0]
752 if unit:
753 action = await unit.run_action(primitive, **params)
754 uuid = action.id
755 except Exception as e:
756 self.log.debug(
757 "Caught exception while executing primitive: {}".format(e)
758 )
759 raise N2VCPrimitiveExecutionFailed(e)
760 return uuid
761
762 async def RemoveCharms(self, model_name, application_name, callback=None,
763 *callback_args):
764 """Remove a charm from the VCA.
765
766 Remove a charm referenced in a VNF Descriptor.
767
768 :param str model_name: The name of the network service.
769 :param str application_name: The name of the application
770 :param obj callback: A callback function to receive status changes.
771 :param tuple callback_args: A list of arguments to be passed to the
772 callback function.
773 """
774 try:
775 if not self.authenticated:
776 await self.login()
777
778 model = await self.get_model(model_name)
779 app = await self.get_application(model, application_name)
780 if app:
781 # Remove this application from event monitoring
782 self.monitors[model_name].RemoveApplication(application_name)
783
784 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
785 self.log.debug(
786 "Removing the application {}".format(application_name)
787 )
788 await app.remove()
789
790 # Notify the callback that this charm has been removed.
791 self.notify_callback(
792 model_name,
793 application_name,
794 "removed",
795 callback,
796 *callback_args,
797 )
798
799 except Exception as e:
800 print("Caught exception: {}".format(e))
801 self.log.debug(e)
802 raise e
803
804 async def DestroyNetworkService(self, nsd):
805 raise NotImplementedError()
806
807 async def GetMetrics(self, model_name, application_name):
808 """Get the metrics collected by the VCA.
809
810 :param model_name The name of the model
811 :param application_name The name of the application
812 """
813 metrics = {}
814 model = await self.get_model(model_name)
815 app = await self.get_application(model, application_name)
816 if app:
817 metrics = await app.get_metrics()
818
819 return metrics
820
821 async def HasApplication(self, model_name, application_name):
822 model = await self.get_model(model_name)
823 app = await self.get_application(model, application_name)
824 if app:
825 return True
826 return False
827
828 # Non-public methods
829 async def add_relation(self, model_name, relation1, relation2):
830 """
831 Add a relation between two application endpoints.
832
833 :param str model_name Name of the network service.
834 :param str relation1 '<application>[:<relation_name>]'
835 :param str relation12 '<application>[:<relation_name>]'
836 """
837
838 if not self.authenticated:
839 await self.login()
840
841 m = await self.get_model(model_name)
842 try:
843 await m.add_relation(relation1, relation2)
844 except JujuAPIError as e:
845 # If one of the applications in the relationship doesn't exist,
846 # or the relation has already been added, let the operation fail
847 # silently.
848 if 'not found' in e.message:
849 return
850 if 'already exists' in e.message:
851 return
852
853 raise e
854
855 # async def apply_config(self, config, application):
856 # """Apply a configuration to the application."""
857 # print("JujuApi: Applying configuration to {}.".format(
858 # application
859 # ))
860 # return await self.set_config(application=application, config=config)
861
862 def _get_config_from_dict(self, config_primitive, values):
863 """Transform the yang config primitive to dict.
864
865 Expected result:
866
867 config = {
868 'config':
869 }
870 """
871 config = {}
872 for primitive in config_primitive:
873 if primitive['name'] == 'config':
874 # config = self._map_primitive_parameters()
875 for parameter in primitive['parameter']:
876 param = str(parameter['name'])
877 if parameter['value'] == "<rw_mgmt_ip>":
878 config[param] = str(values[parameter['value']])
879 else:
880 config[param] = str(parameter['value'])
881
882 return config
883
884 def _map_primitive_parameters(self, parameters, values):
885 params = {}
886 for parameter in parameters:
887 param = str(parameter['name'])
888 value = None
889
890 # If there's no value, use the default-value (if set)
891 if parameter['value'] is None and 'default-value' in parameter:
892 value = parameter['default-value']
893
894 # Typecast parameter value, if present
895 if 'data-type' in parameter:
896 paramtype = str(parameter['data-type']).lower()
897
898 if paramtype == "integer":
899 value = int(parameter['value'])
900 elif paramtype == "boolean":
901 value = bool(parameter['value'])
902 else:
903 value = str(parameter['value'])
904 else:
905 # If there's no data-type, assume the value is a string
906 value = str(parameter['value'])
907
908 if parameter['value'] == "<rw_mgmt_ip>":
909 params[param] = str(values[parameter['value']])
910 else:
911 params[param] = value
912 return params
913
914 def _get_config_from_yang(self, config_primitive, values):
915 """Transform the yang config primitive to dict."""
916 config = {}
917 for primitive in config_primitive.values():
918 if primitive['name'] == 'config':
919 for parameter in primitive['parameter'].values():
920 param = str(parameter['name'])
921 if parameter['value'] == "<rw_mgmt_ip>":
922 config[param] = str(values[parameter['value']])
923 else:
924 config[param] = str(parameter['value'])
925
926 return config
927
928 def FormatApplicationName(self, *args):
929 """
930 Generate a Juju-compatible Application name
931
932 :param args tuple: Positional arguments to be used to construct the
933 application name.
934
935 Limitations::
936 - Only accepts characters a-z and non-consequitive dashes (-)
937 - Application name should not exceed 50 characters
938
939 Examples::
940
941 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
942 """
943 appname = ""
944 for c in "-".join(list(args)):
945 if c.isdigit():
946 c = chr(97 + int(c))
947 elif not c.isalpha():
948 c = "-"
949 appname += c
950 return re.sub('\-+', '-', appname.lower())
951
952 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
953 # """Format the name of the application
954 #
955 # Limitations:
956 # - Only accepts characters a-z and non-consequitive dashes (-)
957 # - Application name should not exceed 50 characters
958 # """
959 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
960 # new_name = ''
961 # for c in name:
962 # if c.isdigit():
963 # c = chr(97 + int(c))
964 # elif not c.isalpha():
965 # c = "-"
966 # new_name += c
967 # return re.sub('\-+', '-', new_name.lower())
968
969 def format_model_name(self, name):
970 """Format the name of model.
971
972 Model names may only contain lowercase letters, digits and hyphens
973 """
974
975 return name.replace('_', '-').lower()
976
977 async def get_application(self, model, application):
978 """Get the deployed application."""
979 if not self.authenticated:
980 await self.login()
981
982 app = None
983 if application and model:
984 if model.applications:
985 if application in model.applications:
986 app = model.applications[application]
987
988 return app
989
990 async def get_model(self, model_name='default'):
991 """Get a model from the Juju Controller.
992
993 Note: Model objects returned must call disconnected() before it goes
994 out of scope."""
995 if not self.authenticated:
996 await self.login()
997
998 if model_name not in self.models:
999 self.models[model_name] = await self.controller.get_model(
1000 model_name,
1001 )
1002 self.refcount['model'] += 1
1003
1004 # Create an observer for this model
1005 self.monitors[model_name] = VCAMonitor(model_name)
1006 self.models[model_name].add_observer(self.monitors[model_name])
1007
1008 return self.models[model_name]
1009
1010 async def login(self):
1011 """Login to the Juju controller."""
1012
1013 if self.authenticated:
1014 return
1015
1016 self.connecting = True
1017
1018 self.log.debug("JujuApi: Logging into controller")
1019
1020 cacert = None
1021 self.controller = Controller(loop=self.loop)
1022
1023 if self.secret:
1024 self.log.debug(
1025 "Connecting to controller... ws://{}:{} as {}/{}".format(
1026 self.endpoint,
1027 self.port,
1028 self.user,
1029 self.secret,
1030 )
1031 )
1032 await self.controller.connect(
1033 endpoint=self.endpoint,
1034 username=self.user,
1035 password=self.secret,
1036 cacert=cacert,
1037 )
1038 self.refcount['controller'] += 1
1039 else:
1040 # current_controller no longer exists
1041 # self.log.debug("Connecting to current controller...")
1042 # await self.controller.connect_current()
1043 # await self.controller.connect(
1044 # endpoint=self.endpoint,
1045 # username=self.user,
1046 # cacert=cacert,
1047 # )
1048 self.log.fatal("VCA credentials not configured.")
1049
1050 self.authenticated = True
1051 self.log.debug("JujuApi: Logged into controller")
1052
1053 async def logout(self):
1054 """Logout of the Juju controller."""
1055 if not self.authenticated:
1056 return
1057
1058 try:
1059 if self.default_model:
1060 self.log.debug("Disconnecting model {}".format(
1061 self.default_model
1062 ))
1063 await self.default_model.disconnect()
1064 self.refcount['model'] -= 1
1065 self.default_model = None
1066
1067 for model in self.models:
1068 await self.models[model].disconnect()
1069 self.refcount['model'] -= 1
1070 self.models[model] = None
1071
1072 if self.controller:
1073 self.log.debug("Disconnecting controller {}".format(
1074 self.controller
1075 ))
1076 await self.controller.disconnect()
1077 self.refcount['controller'] -= 1
1078 self.controller = None
1079
1080 self.authenticated = False
1081
1082 self.log.debug(self.refcount)
1083
1084 except Exception as e:
1085 self.log.fatal(
1086 "Fatal error logging out of Juju Controller: {}".format(e)
1087 )
1088 raise e
1089
1090 # async def remove_application(self, name):
1091 # """Remove the application."""
1092 # if not self.authenticated:
1093 # await self.login()
1094 #
1095 # app = await self.get_application(name)
1096 # if app:
1097 # self.log.debug("JujuApi: Destroying application {}".format(
1098 # name,
1099 # ))
1100 #
1101 # await app.destroy()
1102
1103 async def remove_relation(self, a, b):
1104 """
1105 Remove a relation between two application endpoints
1106
1107 :param a An application endpoint
1108 :param b An application endpoint
1109 """
1110 if not self.authenticated:
1111 await self.login()
1112
1113 m = await self.get_model()
1114 try:
1115 m.remove_relation(a, b)
1116 finally:
1117 await m.disconnect()
1118
1119 async def resolve_error(self, application=None):
1120 """Resolve units in error state."""
1121 if not self.authenticated:
1122 await self.login()
1123
1124 app = await self.get_application(self.default_model, application)
1125 if app:
1126 self.log.debug(
1127 "JujuApi: Resolving errors for application {}".format(
1128 application,
1129 )
1130 )
1131
1132 for unit in app.units:
1133 app.resolved(retry=True)
1134
1135 async def run_action(self, application, action_name, **params):
1136 """Execute an action and return an Action object."""
1137 if not self.authenticated:
1138 await self.login()
1139 result = {
1140 'status': '',
1141 'action': {
1142 'tag': None,
1143 'results': None,
1144 }
1145 }
1146 app = await self.get_application(self.default_model, application)
1147 if app:
1148 # We currently only have one unit per application
1149 # so use the first unit available.
1150 unit = app.units[0]
1151
1152 self.log.debug(
1153 "JujuApi: Running Action {} against Application {}".format(
1154 action_name,
1155 application,
1156 )
1157 )
1158
1159 action = await unit.run_action(action_name, **params)
1160
1161 # Wait for the action to complete
1162 await action.wait()
1163
1164 result['status'] = action.status
1165 result['action']['tag'] = action.data['id']
1166 result['action']['results'] = action.results
1167
1168 return result
1169
1170 async def set_config(self, model_name, application, config):
1171 """Apply a configuration to the application."""
1172 if not self.authenticated:
1173 await self.login()
1174
1175 app = await self.get_application(model_name, application)
1176 if app:
1177 self.log.debug("JujuApi: Setting config for Application {}".format(
1178 application,
1179 ))
1180 await app.set_config(config)
1181
1182 # Verify the config is set
1183 newconf = await app.get_config()
1184 for key in config:
1185 if config[key] != newconf[key]['value']:
1186 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
1187
1188 # async def set_parameter(self, parameter, value, application=None):
1189 # """Set a config parameter for a service."""
1190 # if not self.authenticated:
1191 # await self.login()
1192 #
1193 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1194 # parameter,
1195 # value,
1196 # application,
1197 # ))
1198 # return await self.apply_config(
1199 # {parameter: value},
1200 # application=application,
1201 # )
1202
1203 async def wait_for_application(self, model_name, application_name,
1204 timeout=300):
1205 """Wait for an application to become active."""
1206 if not self.authenticated:
1207 await self.login()
1208
1209 # TODO: In a point release, we will use a model per deployed network
1210 # service. In the meantime, we will always use the 'default' model.
1211 model_name = 'default'
1212 model = await self.get_model(model_name)
1213
1214 app = await self.get_application(model, application_name)
1215 self.log.debug("Application: {}".format(app))
1216 # app = await self.get_application(model_name, application_name)
1217 if app:
1218 self.log.debug(
1219 "JujuApi: Waiting {} seconds for Application {}".format(
1220 timeout,
1221 application_name,
1222 )
1223 )
1224
1225 await model.block_until(
1226 lambda: all(
1227 unit.agent_status == 'idle' and unit.workload_status in
1228 ['active', 'unknown'] for unit in app.units
1229 ),
1230 timeout=timeout
1231 )