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