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