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