1 # Copyright 2019 Canonical Ltd.
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
27 import n2vc
.exceptions
28 from n2vc
.provisioner
import SSHProvisioner
30 # FIXME: this should load the juju inside or modules without having to
31 # explicitly install it. Check why it's not working.
32 # Load our subtree of the juju library
33 # path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
34 # path = os.path.join(path, "modules/libjuju/")
35 # if path not in sys.path:
36 # sys.path.insert(1, path)
38 from juju
.client
import client
39 from juju
.controller
import Controller
40 from juju
.model
import ModelObserver
41 from juju
.errors
import JujuAPIError
, JujuError
44 # We might need this to connect to the websocket securely, but test and verify.
46 ssl
._create
_default
_https
_context
= ssl
._create
_unverified
_context
47 except AttributeError:
48 # Legacy Python doesn't verify by default (see pep-0476)
49 # https://www.python.org/dev/peps/pep-0476/
54 # Deprecated. Please use n2vc.exceptions namespace.
55 class JujuCharmNotFound(Exception):
56 """The Charm can't be found or is not readable."""
59 class JujuApplicationExists(Exception):
60 """The Application already exists."""
63 class N2VCPrimitiveExecutionFailed(Exception):
64 """Something failed while attempting to execute a primitive."""
67 class NetworkServiceDoesNotExist(Exception):
68 """The Network Service being acted against does not exist."""
71 class PrimitiveDoesNotExist(Exception):
72 """The Primitive being executed does not exist."""
75 # Quiet the debug logging
76 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
77 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
78 logging
.getLogger('juju.model').setLevel(logging
.WARN
)
79 logging
.getLogger('juju.machine').setLevel(logging
.WARN
)
82 class VCAMonitor(ModelObserver
):
83 """Monitor state changes within the Juju Model."""
86 def __init__(self
, ns_name
):
87 self
.log
= logging
.getLogger(__name__
)
89 self
.ns_name
= ns_name
90 self
.applications
= {}
92 def AddApplication(self
, application_name
, callback
, *callback_args
):
93 if application_name
not in self
.applications
:
94 self
.applications
[application_name
] = {
96 'callback_args': callback_args
99 def RemoveApplication(self
, application_name
):
100 if application_name
in self
.applications
:
101 del self
.applications
[application_name
]
103 async def on_change(self
, delta
, old
, new
, model
):
104 """React to changes in the Juju model."""
106 if delta
.entity
== "unit":
107 # Ignore change events from other applications
108 if delta
.data
['application'] not in self
.applications
.keys():
113 application_name
= delta
.data
['application']
115 callback
= self
.applications
[application_name
]['callback']
117 self
.applications
[application_name
]['callback_args']
120 # Fire off a callback with the application state
124 delta
.data
['application'],
126 new
.workload_status_message
,
130 # This is a charm being removed
134 delta
.data
['application'],
138 except Exception as e
:
139 self
.log
.debug("[1] notify_callback exception: {}".format(e
))
141 elif delta
.entity
== "action":
142 # TODO: Decide how we want to notify the user of actions
144 # uuid = delta.data['id'] # The Action's unique id
145 # msg = delta.data['message'] # The output of the action
147 # if delta.data['status'] == "pending":
148 # # The action is queued
150 # elif delta.data['status'] == "completed""
151 # # The action was successful
153 # elif delta.data['status'] == "failed":
154 # # The action failed.
162 # Create unique models per network service
163 # Document all public functions
175 juju_public_key
=None,
181 Initializes the N2VC object, allowing the caller to interoperate with the VCA.
184 :param log obj: The logging object to log to
185 :param server str: The IP Address or Hostname of the Juju controller
186 :param port int: The port of the Juju Controller
187 :param user str: The Juju username to authenticate with
188 :param secret str: The Juju password to authenticate with
189 :param artifacts str: The directory where charms required by a vnfd are
191 :param loop obj: The loop to use.
192 :param juju_public_key str: The contents of the Juju public SSH key
193 :param ca_cert str: The CA certificate to use to authenticate
194 :param api_proxy str: The IP of the host machine
197 client = n2vc.vnf.N2VC(
203 artifacts='/app/storage/myvnf/charms',
205 juju_public_key='<contents of the juju public key>',
206 ca_cert='<contents of CA certificate>',
207 api_proxy='192.168.1.155'
211 # Initialize instance-level variables
214 self
.controller
= None
215 self
.connecting
= False
216 self
.authenticated
= False
217 self
.api_proxy
= api_proxy
222 self
.log
= logging
.getLogger(__name__
)
241 self
.juju_public_key
= juju_public_key
243 self
._create
_juju
_public
_key
(juju_public_key
)
245 self
.juju_public_key
= ''
247 # TODO: Verify ca_cert is valid before using. VCA will crash
248 # if the ca_cert isn't formatted correctly.
249 def base64_to_cacert(b64string
):
250 """Convert the base64-encoded string containing the VCA CACERT.
256 cacert
= base64
.b64decode(b64string
).decode("utf-8")
263 except binascii
.Error
as e
:
264 self
.log
.debug("Caught binascii.Error: {}".format(e
))
265 raise n2vc
.exceptions
.InvalidCACertificate("Invalid CA Certificate")
271 self
.ca_cert
= base64_to_cacert(ca_cert
)
274 # Quiet websocket traffic
275 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
276 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
277 logging
.getLogger('model').setLevel(logging
.WARN
)
278 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
280 self
.log
.debug('JujuApi: instantiated')
286 if user
.startswith('user-'):
289 self
.user
= 'user-{}'.format(user
)
291 self
.endpoint
= '%s:%d' % (server
, int(port
))
293 self
.artifacts
= artifacts
295 self
.loop
= loop
or asyncio
.get_event_loop()
298 """Close any open connections."""
301 def _create_juju_public_key(self
, public_key
):
302 """Recreate the Juju public key on disk.
304 Certain libjuju commands expect to be run from the same machine as Juju
305 is bootstrapped to. This method will write the public key to disk in
306 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
308 # Make sure that we have a public key before writing to disk
309 if public_key
is None or len(public_key
) == 0:
310 if 'OSM_VCA_PUBKEY' in os
.environ
:
311 public_key
= os
.getenv('OSM_VCA_PUBKEY', '')
312 if len(public_key
== 0):
317 path
= "{}/.local/share/juju/ssh".format(
318 os
.path
.expanduser('~'),
320 if not os
.path
.exists(path
):
323 with
open('{}/juju_id_rsa.pub'.format(path
), 'w') as f
:
326 def notify_callback(self
, model_name
, application_name
, status
, message
,
327 callback
=None, *callback_args
):
336 except Exception as e
:
337 self
.log
.error("[0] notify_callback exception {}".format(e
))
342 async def Relate(self
, model_name
, vnfd
):
343 """Create a relation between the charm-enabled VDUs in a VNF.
345 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
351 - provides: dataVM:db
354 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.
356 :param str ns_name: The name of the network service.
357 :param dict vnfd: The parsed yaml VNF descriptor.
360 # Currently, the call to Relate() is made automatically after the
361 # deployment of each charm; if the relation depends on a charm that
362 # hasn't been deployed yet, the call will fail silently. This will
363 # prevent an API breakage, with the intent of making this an explicitly
364 # required call in a more object-oriented refactor of the N2VC API.
367 vnf_config
= vnfd
.get("vnf-configuration")
369 juju
= vnf_config
['juju']
371 configs
.append(vnf_config
)
373 for vdu
in vnfd
['vdu']:
374 vdu_config
= vdu
.get('vdu-configuration')
376 juju
= vdu_config
['juju']
378 configs
.append(vdu_config
)
380 def _get_application_name(name
):
381 """Get the application name that's mapped to a vnf/vdu."""
383 vnf_name
= vnfd
['name']
385 for vdu
in vnfd
.get('vdu'):
386 # Compare the named portion of the relation to the vdu's id
387 if vdu
['id'] == name
:
388 application_name
= self
.FormatApplicationName(
391 str(vnf_member_index
),
393 return application_name
395 vnf_member_index
+= 1
399 # Loop through relations
403 if 'vca-relationships' in juju
and 'relation' in juju
['vca-relationships']:
404 for rel
in juju
['vca-relationships']['relation']:
407 # get the application name for the provides
408 (name
, endpoint
) = rel
['provides'].split(':')
409 application_name
= _get_application_name(name
)
411 provides
= "{}:{}".format(
416 # get the application name for thr requires
417 (name
, endpoint
) = rel
['requires'].split(':')
418 application_name
= _get_application_name(name
)
420 requires
= "{}:{}".format(
424 self
.log
.debug("Relation: {} <-> {}".format(
428 await self
.add_relation(
433 except Exception as e
:
434 self
.log
.debug("Exception: {}".format(e
))
438 async def DeployCharms(self
, model_name
, application_name
, vnfd
,
439 charm_path
, params
={}, machine_spec
={},
440 callback
=None, *callback_args
):
441 """Deploy one or more charms associated with a VNF.
443 Deploy the charm(s) referenced in a VNF Descriptor.
445 :param str model_name: The name or unique id of the network service.
446 :param str application_name: The name of the application
447 :param dict vnfd: The name of the application
448 :param str charm_path: The path to the Juju charm
449 :param dict params: A dictionary of runtime parameters
452 'rw_mgmt_ip': '1.2.3.4',
453 # Pass the initial-config-primitives section of the vnf or vdu
454 'initial-config-primitives': {...}
455 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
456 inside < >. rw_mgmt_ip will be included here also
458 :param dict machine_spec: A dictionary describing the machine to
462 'hostname': '1.2.3.4',
463 'username': 'ubuntu',
465 :param obj callback: A callback function to receive status changes.
466 :param tuple callback_args: A list of arguments to be passed to the
470 ########################################################
471 # Verify the path to the charm exists and is readable. #
472 ########################################################
473 if not os
.path
.exists(charm_path
):
474 self
.log
.debug("Charm path doesn't exist: {}".format(charm_path
))
475 self
.notify_callback(
483 raise JujuCharmNotFound("No artifacts configured.")
485 ################################
486 # Login to the Juju controller #
487 ################################
488 if not self
.authenticated
:
489 self
.log
.debug("Authenticating with Juju")
492 ##########################################
493 # Get the model for this network service #
494 ##########################################
495 model
= await self
.get_model(model_name
)
497 ########################################
498 # Verify the application doesn't exist #
499 ########################################
500 app
= await self
.get_application(model
, application_name
)
502 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name
, model_name
))
504 ################################################################
505 # Register this application with the model-level event monitor #
506 ################################################################
508 self
.log
.debug("JujuApi: Registering callback for {}".format(
511 await self
.Subscribe(model_name
, application_name
, callback
, *callback_args
)
513 #######################################
514 # Get the initial charm configuration #
515 #######################################
518 if 'rw_mgmt_ip' in params
:
519 rw_mgmt_ip
= params
['rw_mgmt_ip']
521 if 'initial-config-primitive' not in params
:
522 params
['initial-config-primitive'] = {}
524 initial_config
= self
._get
_config
_from
_dict
(
525 params
['initial-config-primitive'],
526 {'<rw_mgmt_ip>': rw_mgmt_ip
}
529 ########################################################
530 # Check for specific machine placement (native charms) #
531 ########################################################
535 if machine_spec
.keys():
536 if all(k
in machine_spec
for k
in ['hostname', 'username']):
538 # Allow series to be derived from the native charm
541 self
.log
.debug("Provisioning manual machine {}@{}".format(
542 machine_spec
['username'],
543 machine_spec
['hostname'],
546 """Native Charm support
548 Taking a bare VM (assumed to be an Ubuntu cloud image),
549 the provisioning process will:
550 - Create an ubuntu user w/sudo access
552 - Detect architecture
553 - Download and install Juju agent from controller
555 - Add an iptables rule to route traffic to the API proxy
558 to
= await self
.provision_machine(
559 model_name
=model_name
,
560 username
=machine_spec
['username'],
561 hostname
=machine_spec
['hostname'],
562 private_key_path
=self
.GetPrivateKeyPath(),
564 self
.log
.debug("Provisioned machine id {}".format(to
))
566 # TODO: If to is none, raise an exception
568 # The native charm won't have the sshproxy layer, typically, but LCM uses the config primitive
569 # to interpret what the values are. That's a gap to fill.
572 The ssh-* config parameters are unique to the sshproxy layer,
573 which most native charms will not be aware of.
575 Setting invalid config parameters will cause the deployment to
578 For the moment, we will strip the ssh-* parameters from native
579 charms, until the feature gap is addressed in the information
583 # Native charms don't include the ssh-* config values, so strip them
584 # from the initial_config, otherwise the deploy will raise an error.
585 # self.log.debug("Removing ssh-* from initial-config")
586 for k
in ['ssh-hostname', 'ssh-username', 'ssh-password']:
587 if k
in initial_config
:
588 self
.log
.debug("Removing parameter {}".format(k
))
589 del initial_config
[k
]
591 self
.log
.debug("JujuApi: Deploying charm ({}/{}) from {} to {}".format(
598 ########################################################
599 # Deploy the charm and apply the initial configuration #
600 ########################################################
601 app
= await model
.deploy(
602 # We expect charm_path to be either the path to the charm on disk
603 # or in the format of cs:series/name
605 # This is the formatted, unique name for this charm
606 application_name
=application_name
,
607 # Proxy charms should use the current LTS. This will need to be
608 # changed for native charms.
610 # Apply the initial 'config' primitive during deployment
611 config
=initial_config
,
612 # Where to deploy the charm to.
616 #############################
617 # Map the vdu id<->app name #
618 #############################
620 await self
.Relate(model_name
, vnfd
)
621 except KeyError as ex
:
622 # We don't currently support relations between NS and VNF/VDU charms
623 self
.log
.warn("[N2VC] Relations not supported: {}".format(ex
))
624 except Exception as ex
:
625 # This may happen if not all of the charms needed by the relation
626 # are ready. We can safely ignore this, because Relate will be
627 # retried when the endpoint of the relation is deployed.
628 self
.log
.warn("[N2VC] Relations not ready")
630 # #######################################
631 # # Execute initial config primitive(s) #
632 # #######################################
633 uuids
= await self
.ExecuteInitialPrimitives(
642 # # Build a sequential list of the primitives to execute
643 # for primitive in params['initial-config-primitive']:
645 # if primitive['name'] == 'config':
646 # # This is applied when the Application is deployed
649 # seq = primitive['seq']
652 # if 'parameter' in primitive:
653 # params = primitive['parameter']
655 # primitives[seq] = {
656 # 'name': primitive['name'],
657 # 'parameters': self._map_primitive_parameters(
659 # {'<rw_mgmt_ip>': rw_mgmt_ip}
663 # for primitive in sorted(primitives):
664 # await self.ExecutePrimitive(
667 # primitives[primitive]['name'],
670 # **primitives[primitive]['parameters'],
672 # except N2VCPrimitiveExecutionFailed as e:
674 # "[N2VC] Exception executing primitive: {}".format(e)
678 async def GetPrimitiveStatus(self
, model_name
, uuid
):
679 """Get the status of an executed Primitive.
681 The status of an executed Primitive will be one of three values:
688 if not self
.authenticated
:
691 model
= await self
.get_model(model_name
)
693 results
= await model
.get_action_status(uuid
)
696 status
= results
[uuid
]
698 except Exception as e
:
700 "Caught exception while getting primitive status: {}".format(e
)
702 raise N2VCPrimitiveExecutionFailed(e
)
706 async def GetPrimitiveOutput(self
, model_name
, uuid
):
707 """Get the output of an executed Primitive.
709 Note: this only returns output for a successfully executed primitive.
713 if not self
.authenticated
:
716 model
= await self
.get_model(model_name
)
717 results
= await model
.get_action_output(uuid
, 60)
718 except Exception as e
:
720 "Caught exception while getting primitive status: {}".format(e
)
722 raise N2VCPrimitiveExecutionFailed(e
)
726 # async def ProvisionMachine(self, model_name, hostname, username):
727 # """Provision machine for usage with Juju.
729 # Provisions a previously instantiated machine for use with Juju.
732 # if not self.authenticated:
735 # # FIXME: This is hard-coded until model-per-ns is added
736 # model_name = 'default'
738 # model = await self.get_model(model_name)
739 # model.add_machine(spec={})
741 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
748 # except Exception as e:
750 # "Caught exception while getting primitive status: {}".format(e)
752 # raise N2VCPrimitiveExecutionFailed(e)
754 def GetPrivateKeyPath(self
):
755 homedir
= os
.environ
['HOME']
756 sshdir
= "{}/.ssh".format(homedir
)
757 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
758 return private_key_path
760 async def GetPublicKey(self
):
761 """Get the N2VC SSH public key.abs
763 Returns the SSH public key, to be injected into virtual machines to
764 be managed by the VCA.
766 The first time this is run, a ssh keypair will be created. The public
767 key is injected into a VM so that we can provision the machine with
768 Juju, after which Juju will communicate with the VM directly via the
773 # Find the path to where we expect our key to live.
774 homedir
= os
.environ
['HOME']
775 sshdir
= "{}/.ssh".format(homedir
)
776 if not os
.path
.exists(sshdir
):
779 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
780 public_key_path
= "{}.pub".format(private_key_path
)
782 # If we don't have a key generated, generate it.
783 if not os
.path
.exists(private_key_path
):
784 cmd
= "ssh-keygen -t {} -b {} -N '' -f {}".format(
789 subprocess
.check_output(shlex
.split(cmd
))
791 # Read the public key
792 with
open(public_key_path
, "r") as f
:
793 public_key
= f
.readline()
797 async def ExecuteInitialPrimitives(self
, model_name
, application_name
,
798 params
, callback
=None, *callback_args
):
799 """Execute multiple primitives.
801 Execute multiple primitives as declared in initial-config-primitive.
802 This is useful in cases where the primitives initially failed -- for
803 example, if the charm is a proxy but the proxy hasn't been configured
809 # Build a sequential list of the primitives to execute
810 for primitive
in params
['initial-config-primitive']:
812 if primitive
['name'] == 'config':
815 seq
= primitive
['seq']
818 if 'parameter' in primitive
:
819 params_
= primitive
['parameter']
821 user_values
= params
.get("user_values", {})
822 if 'rw_mgmt_ip' not in user_values
:
823 user_values
['rw_mgmt_ip'] = None
824 # just for backward compatibility, because it will be provided always by modern version of LCM
827 'name': primitive
['name'],
828 'parameters': self
._map
_primitive
_parameters
(
834 for primitive
in sorted(primitives
):
836 # self.log.debug("Queuing action {}".format(primitives[primitive]['name']))
838 await self
.ExecutePrimitive(
841 primitives
[primitive
]['name'],
844 **primitives
[primitive
]['parameters'],
847 except PrimitiveDoesNotExist
as e
:
848 self
.log
.debug("Ignoring exception PrimitiveDoesNotExist: {}".format(e
))
850 except Exception as e
:
851 self
.log
.debug("XXXXXXXXXXXXXXXXXXXXXXXXX Unexpected exception: {}".format(e
))
854 except N2VCPrimitiveExecutionFailed
as e
:
856 "[N2VC] Exception executing primitive: {}".format(e
)
861 async def ExecutePrimitive(self
, model_name
, application_name
, primitive
,
862 callback
, *callback_args
, **params
):
863 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
865 Execute a primitive defined in the VNF descriptor.
867 :param str model_name: The name or unique id of the network service.
868 :param str application_name: The name of the application
869 :param str primitive: The name of the primitive to execute.
870 :param obj callback: A callback function to receive status changes.
871 :param tuple callback_args: A list of arguments to be passed to the
873 :param dict params: A dictionary of key=value pairs representing the
874 primitive's parameters
877 'rw_mgmt_ip': '1.2.3.4',
878 # Pass the initial-config-primitives section of the vnf or vdu
879 'initial-config-primitives': {...}
882 self
.log
.debug("Executing primitive={} params={}".format(primitive
, params
))
885 if not self
.authenticated
:
888 model
= await self
.get_model(model_name
)
890 if primitive
== 'config':
891 # config is special, and expecting params to be a dictionary
892 await self
.set_config(
898 app
= await self
.get_application(model
, application_name
)
900 # Does this primitive exist?
901 actions
= await app
.get_actions()
903 if primitive
not in actions
.keys():
904 raise PrimitiveDoesNotExist("Primitive {} does not exist".format(primitive
))
906 # Run against the first (and probably only) unit in the app
909 action
= await unit
.run_action(primitive
, **params
)
911 except PrimitiveDoesNotExist
as e
:
912 # Catch and raise this exception if it's thrown from the inner block
914 except Exception as e
:
915 # An unexpected exception was caught
917 "Caught exception while executing primitive: {}".format(e
)
919 raise N2VCPrimitiveExecutionFailed(e
)
922 async def RemoveCharms(self
, model_name
, application_name
, callback
=None,
924 """Remove a charm from the VCA.
926 Remove a charm referenced in a VNF Descriptor.
928 :param str model_name: The name of the network service.
929 :param str application_name: The name of the application
930 :param obj callback: A callback function to receive status changes.
931 :param tuple callback_args: A list of arguments to be passed to the
935 if not self
.authenticated
:
938 model
= await self
.get_model(model_name
)
939 app
= await self
.get_application(model
, application_name
)
941 # Remove this application from event monitoring
942 await self
.Unsubscribe(model_name
, application_name
)
944 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
946 "Removing the application {}".format(application_name
)
950 # await self.disconnect_model(self.monitors[model_name])
952 self
.notify_callback(
956 "Removing charm {}".format(application_name
),
961 except Exception as e
:
962 print("Caught exception: {}".format(e
))
966 async def CreateNetworkService(self
, ns_uuid
):
967 """Create a new Juju model for the Network Service.
969 Creates a new Model in the Juju Controller.
971 :param str ns_uuid: A unique id representing an instaance of a
974 :returns: True if the model was created. Raises JujuError on failure.
976 if not self
.authenticated
:
979 models
= await self
.controller
.list_models()
980 if ns_uuid
not in models
:
982 await self
.get_model(ns_uuid
)
986 async def DestroyNetworkService(self
, ns_uuid
):
987 """Destroy a Network Service.
989 Destroy the Network Service and any deployed charms.
991 :param ns_uuid The unique id of the Network Service
993 :returns: True if the model was created. Raises JujuError on failure.
996 # Do not delete the default model. The default model was used by all
997 # Network Services, prior to the implementation of a model per NS.
998 if ns_uuid
.lower() == "default":
1001 if not self
.authenticated
:
1004 models
= await self
.controller
.list_models()
1005 if ns_uuid
in models
:
1006 model
= await self
.controller
.get_model(ns_uuid
)
1008 for application
in model
.applications
:
1009 app
= model
.applications
[application
]
1011 await self
.RemoveCharms(ns_uuid
, application
)
1013 self
.log
.debug("Unsubscribing Watcher for {}".format(application
))
1014 await self
.Unsubscribe(ns_uuid
, application
)
1016 self
.log
.debug("Waiting for application to terminate")
1019 await model
.block_until(
1021 unit
.workload_status
in ['terminated'] for unit
in app
.units
1025 except Exception as e
:
1026 self
.log
.debug("Timed out waiting for {} to terminate.".format(application
))
1028 for machine
in model
.machines
:
1030 self
.log
.debug("Destroying machine {}".format(machine
))
1031 await model
.machines
[machine
].destroy(force
=True)
1032 except JujuAPIError
as e
:
1033 if 'does not exist' in str(e
):
1034 # Our cached model may be stale, because the machine
1035 # has already been removed. It's safe to continue.
1038 self
.log
.debug("Caught exception: {}".format(e
))
1041 # Disconnect from the Model
1042 if ns_uuid
in self
.models
:
1043 self
.log
.debug("Disconnecting model {}".format(ns_uuid
))
1044 # await self.disconnect_model(self.models[ns_uuid])
1045 await self
.disconnect_model(ns_uuid
)
1048 self
.log
.debug("Destroying model {}".format(ns_uuid
))
1049 await self
.controller
.destroy_models(ns_uuid
)
1051 raise NetworkServiceDoesNotExist(
1052 "The Network Service '{}' does not exist".format(ns_uuid
)
1057 async def GetMetrics(self
, model_name
, application_name
):
1058 """Get the metrics collected by the VCA.
1060 :param model_name The name or unique id of the network service
1061 :param application_name The name of the application
1064 model
= await self
.get_model(model_name
)
1065 app
= await self
.get_application(model
, application_name
)
1067 metrics
= await app
.get_metrics()
1071 async def HasApplication(self
, model_name
, application_name
):
1072 model
= await self
.get_model(model_name
)
1073 app
= await self
.get_application(model
, application_name
)
1078 async def Subscribe(self
, ns_name
, application_name
, callback
, *callback_args
):
1079 """Subscribe to callbacks for an application.
1081 :param ns_name str: The name of the Network Service
1082 :param application_name str: The name of the application
1083 :param callback obj: The callback method
1084 :param callback_args list: The list of arguments to append to calls to
1087 self
.monitors
[ns_name
].AddApplication(
1093 async def Unsubscribe(self
, ns_name
, application_name
):
1094 """Unsubscribe to callbacks for an application.
1096 Unsubscribes the caller from notifications from a deployed application.
1098 :param ns_name str: The name of the Network Service
1099 :param application_name str: The name of the application
1101 self
.monitors
[ns_name
].RemoveApplication(
1105 # Non-public methods
1106 async def add_relation(self
, model_name
, relation1
, relation2
):
1108 Add a relation between two application endpoints.
1110 :param str model_name: The name or unique id of the network service
1111 :param str relation1: '<application>[:<relation_name>]'
1112 :param str relation2: '<application>[:<relation_name>]'
1115 if not self
.authenticated
:
1118 m
= await self
.get_model(model_name
)
1120 await m
.add_relation(relation1
, relation2
)
1121 except JujuAPIError
as e
:
1122 # If one of the applications in the relationship doesn't exist,
1123 # or the relation has already been added, let the operation fail
1125 if 'not found' in e
.message
:
1127 if 'already exists' in e
.message
:
1132 # async def apply_config(self, config, application):
1133 # """Apply a configuration to the application."""
1134 # print("JujuApi: Applying configuration to {}.".format(
1137 # return await self.set_config(application=application, config=config)
1139 def _get_config_from_dict(self
, config_primitive
, values
):
1140 """Transform the yang config primitive to dict.
1149 for primitive
in config_primitive
:
1150 if primitive
['name'] == 'config':
1151 # config = self._map_primitive_parameters()
1152 for parameter
in primitive
['parameter']:
1153 param
= str(parameter
['name'])
1154 if parameter
['value'] == "<rw_mgmt_ip>":
1155 config
[param
] = str(values
[parameter
['value']])
1157 config
[param
] = str(parameter
['value'])
1161 def _map_primitive_parameters(self
, parameters
, user_values
):
1163 for parameter
in parameters
:
1164 param
= str(parameter
['name'])
1165 value
= parameter
.get('value')
1167 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
1168 # Must exist at user_values except if there is a default value
1169 if isinstance(value
, str) and value
.startswith("<") and value
.endswith(">"):
1170 if parameter
['value'][1:-1] in user_values
:
1171 value
= user_values
[parameter
['value'][1:-1]]
1172 elif 'default-value' in parameter
:
1173 value
= parameter
['default-value']
1175 raise KeyError("parameter {}='{}' not supplied ".format(param
, value
))
1177 # If there's no value, use the default-value (if set)
1178 if value
is None and 'default-value' in parameter
:
1179 value
= parameter
['default-value']
1181 # Typecast parameter value, if present
1182 paramtype
= "string"
1184 if 'data-type' in parameter
:
1185 paramtype
= str(parameter
['data-type']).lower()
1187 if paramtype
== "integer":
1189 elif paramtype
== "boolean":
1194 # If there's no data-type, assume the value is a string
1197 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param
, value
, paramtype
))
1199 params
[param
] = value
1202 def _get_config_from_yang(self
, config_primitive
, values
):
1203 """Transform the yang config primitive to dict."""
1205 for primitive
in config_primitive
.values():
1206 if primitive
['name'] == 'config':
1207 for parameter
in primitive
['parameter'].values():
1208 param
= str(parameter
['name'])
1209 if parameter
['value'] == "<rw_mgmt_ip>":
1210 config
[param
] = str(values
[parameter
['value']])
1212 config
[param
] = str(parameter
['value'])
1216 def FormatApplicationName(self
, *args
):
1218 Generate a Juju-compatible Application name
1220 :param args tuple: Positional arguments to be used to construct the
1224 - Only accepts characters a-z and non-consequitive dashes (-)
1225 - Application name should not exceed 50 characters
1229 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
1232 for c
in "-".join(list(args
)):
1234 c
= chr(97 + int(c
))
1235 elif not c
.isalpha():
1238 return re
.sub('-+', '-', appname
.lower())
1240 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
1241 # """Format the name of the application
1244 # - Only accepts characters a-z and non-consequitive dashes (-)
1245 # - Application name should not exceed 50 characters
1247 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1251 # c = chr(97 + int(c))
1252 # elif not c.isalpha():
1255 # return re.sub('\-+', '-', new_name.lower())
1257 def format_model_name(self
, name
):
1258 """Format the name of model.
1260 Model names may only contain lowercase letters, digits and hyphens
1263 return name
.replace('_', '-').lower()
1265 async def get_application(self
, model
, application
):
1266 """Get the deployed application."""
1267 if not self
.authenticated
:
1271 if application
and model
:
1272 if model
.applications
:
1273 if application
in model
.applications
:
1274 app
= model
.applications
[application
]
1278 async def get_model(self
, model_name
):
1279 """Get a model from the Juju Controller.
1281 Note: Model objects returned must call disconnected() before it goes
1283 if not self
.authenticated
:
1286 if model_name
not in self
.models
:
1287 # Get the models in the controller
1288 models
= await self
.controller
.list_models()
1290 if model_name
not in models
:
1292 self
.models
[model_name
] = await self
.controller
.add_model(
1294 config
={'authorized-keys': self
.juju_public_key
}
1297 except JujuError
as e
:
1298 if "already exists" not in e
.message
:
1301 self
.models
[model_name
] = await self
.controller
.get_model(
1305 self
.refcount
['model'] += 1
1307 # Create an observer for this model
1308 await self
.create_model_monitor(model_name
)
1310 return self
.models
[model_name
]
1312 async def create_model_monitor(self
, model_name
):
1313 """Create a monitor for the model, if none exists."""
1314 if not self
.authenticated
:
1317 if model_name
not in self
.monitors
:
1318 self
.monitors
[model_name
] = VCAMonitor(model_name
)
1319 self
.models
[model_name
].add_observer(self
.monitors
[model_name
])
1323 async def login(self
):
1324 """Login to the Juju controller."""
1326 if self
.authenticated
:
1329 self
.connecting
= True
1331 self
.log
.debug("JujuApi: Logging into controller")
1333 self
.controller
= Controller(loop
=self
.loop
)
1337 "Connecting to controller... ws://{} as {}/{}".format(
1344 await self
.controller
.connect(
1345 endpoint
=self
.endpoint
,
1347 password
=self
.secret
,
1348 cacert
=self
.ca_cert
,
1350 self
.refcount
['controller'] += 1
1351 self
.authenticated
= True
1352 self
.log
.debug("JujuApi: Logged into controller")
1353 except Exception as ex
:
1354 self
.log
.debug("Caught exception: {}".format(ex
))
1356 # current_controller no longer exists
1357 # self.log.debug("Connecting to current controller...")
1358 # await self.controller.connect_current()
1359 # await self.controller.connect(
1360 # endpoint=self.endpoint,
1361 # username=self.user,
1364 self
.log
.fatal("VCA credentials not configured.")
1365 self
.authenticated
= False
1368 async def logout(self
):
1369 """Logout of the Juju controller."""
1370 if not self
.authenticated
:
1374 for model
in self
.models
:
1375 await self
.disconnect_model(model
)
1378 self
.log
.debug("Disconnecting controller {}".format(
1381 await self
.controller
.disconnect()
1382 self
.refcount
['controller'] -= 1
1383 self
.controller
= None
1385 self
.authenticated
= False
1387 self
.log
.debug(self
.refcount
)
1389 except Exception as e
:
1391 "Fatal error logging out of Juju Controller: {}".format(e
)
1396 async def disconnect_model(self
, model
):
1397 self
.log
.debug("Disconnecting model {}".format(model
))
1398 if model
in self
.models
:
1400 await self
.models
[model
].disconnect()
1401 self
.refcount
['model'] -= 1
1402 self
.models
[model
] = None
1403 except Exception as e
:
1404 self
.log
.debug("Caught exception: {}".format(e
))
1406 async def provision_machine(self
, model_name
: str,
1407 hostname
: str, username
: str,
1408 private_key_path
: str) -> int:
1409 """Provision a machine.
1411 This executes the SSH provisioner, which will log in to a machine via
1412 SSH and prepare it for use with the Juju model
1414 :param model_name str: The name of the model
1415 :param hostname str: The IP or hostname of the target VM
1416 :param user str: The username to login to
1417 :param private_key_path str: The path to the private key that's been injected to the VM via cloud-init
1418 :return machine_id int: Returns the id of the machine or None if provisioning fails
1420 if not self
.authenticated
:
1426 self
.log
.debug("Instantiating SSH Provisioner for {}@{} ({})".format(
1431 provisioner
= SSHProvisioner(
1434 private_key_path
=private_key_path
,
1440 params
= provisioner
.provision_machine()
1441 except Exception as ex
:
1442 self
.log
.debug("caught exception from provision_machine: {}".format(ex
))
1446 params
.jobs
= ['JobHostUnits']
1448 model
= await self
.get_model(model_name
)
1450 connection
= model
.connection()
1452 # Submit the request.
1453 self
.log
.debug("Adding machine to model")
1454 client_facade
= client
.ClientFacade
.from_connection(connection
)
1455 results
= await client_facade
.AddMachines(params
=[params
])
1456 error
= results
.machines
[0].error
1458 raise ValueError("Error adding machine: %s" % error
.message
)
1460 machine_id
= results
.machines
[0].machine
1462 # Need to run this after AddMachines has been called,
1463 # as we need the machine_id
1464 self
.log
.debug("Installing Juju agent")
1465 await provisioner
.install_agent(
1472 self
.log
.debug("Missing API Proxy")
1475 # async def remove_application(self, name):
1476 # """Remove the application."""
1477 # if not self.authenticated:
1478 # await self.login()
1480 # app = await self.get_application(name)
1482 # self.log.debug("JujuApi: Destroying application {}".format(
1486 # await app.destroy()
1488 async def remove_relation(self
, a
, b
):
1490 Remove a relation between two application endpoints
1492 :param a An application endpoint
1493 :param b An application endpoint
1495 if not self
.authenticated
:
1498 m
= await self
.get_model()
1500 m
.remove_relation(a
, b
)
1502 await m
.disconnect()
1504 async def resolve_error(self
, model_name
, application
=None):
1505 """Resolve units in error state."""
1506 if not self
.authenticated
:
1509 model
= await self
.get_model(model_name
)
1511 app
= await self
.get_application(model
, application
)
1514 "JujuApi: Resolving errors for application {}".format(
1519 for unit
in app
.units
:
1520 app
.resolved(retry
=True)
1522 async def run_action(self
, model_name
, application
, action_name
, **params
):
1523 """Execute an action and return an Action object."""
1524 if not self
.authenticated
:
1534 model
= await self
.get_model(model_name
)
1536 app
= await self
.get_application(model
, application
)
1538 # We currently only have one unit per application
1539 # so use the first unit available.
1543 "JujuApi: Running Action {} against Application {}".format(
1549 action
= await unit
.run_action(action_name
, **params
)
1551 # Wait for the action to complete
1554 result
['status'] = action
.status
1555 result
['action']['tag'] = action
.data
['id']
1556 result
['action']['results'] = action
.results
1560 async def set_config(self
, model_name
, application
, config
):
1561 """Apply a configuration to the application."""
1562 if not self
.authenticated
:
1565 app
= await self
.get_application(model_name
, application
)
1567 self
.log
.debug("JujuApi: Setting config for Application {}".format(
1570 await app
.set_config(config
)
1572 # Verify the config is set
1573 newconf
= await app
.get_config()
1575 if config
[key
] != newconf
[key
]['value']:
1576 self
.log
.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key
, config
[key
], newconf
[key
]))
1578 # async def set_parameter(self, parameter, value, application=None):
1579 # """Set a config parameter for a service."""
1580 # if not self.authenticated:
1581 # await self.login()
1583 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1588 # return await self.apply_config(
1589 # {parameter: value},
1590 # application=application,
1593 async def wait_for_application(self
, model_name
, application_name
,
1595 """Wait for an application to become active."""
1596 if not self
.authenticated
:
1599 model
= await self
.get_model(model_name
)
1601 app
= await self
.get_application(model
, application_name
)
1602 self
.log
.debug("Application: {}".format(app
))
1605 "JujuApi: Waiting {} seconds for Application {}".format(
1611 await model
.block_until(
1613 unit
.agent_status
== 'idle' and unit
.workload_status
in
1614 ['active', 'unknown'] for unit
in app
.units