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