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.
25 from n2vc
.provisioner
import SSHProvisioner
27 # FIXME: this should load the juju inside or modules without having to
28 # explicitly install it. Check why it's not working.
29 # Load our subtree of the juju library
30 path
= os
.path
.abspath(os
.path
.join(os
.path
.dirname(__file__
), '..'))
31 path
= os
.path
.join(path
, "modules/libjuju/")
32 if path
not in sys
.path
:
33 sys
.path
.insert(1, path
)
35 from juju
.client
import client
36 from juju
.controller
import Controller
37 from juju
.model
import ModelObserver
38 from juju
.errors
import JujuAPIError
, JujuError
41 # We might need this to connect to the websocket securely, but test and verify.
43 ssl
._create
_default
_https
_context
= ssl
._create
_unverified
_context
44 except AttributeError:
45 # Legacy Python doesn't verify by default (see pep-0476)
46 # https://www.python.org/dev/peps/pep-0476/
51 # Deprecated. Please use n2vc.exceptions namespace.
52 class JujuCharmNotFound(Exception):
53 """The Charm can't be found or is not readable."""
56 class JujuApplicationExists(Exception):
57 """The Application already exists."""
60 class N2VCPrimitiveExecutionFailed(Exception):
61 """Something failed while attempting to execute a primitive."""
64 class NetworkServiceDoesNotExist(Exception):
65 """The Network Service being acted against does not exist."""
68 class PrimitiveDoesNotExist(Exception):
69 """The Primitive being executed does not exist."""
72 # Quiet the debug logging
73 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
74 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
75 logging
.getLogger('juju.model').setLevel(logging
.WARN
)
76 logging
.getLogger('juju.machine').setLevel(logging
.WARN
)
79 class VCAMonitor(ModelObserver
):
80 """Monitor state changes within the Juju Model."""
83 def __init__(self
, ns_name
):
84 self
.log
= logging
.getLogger(__name__
)
86 self
.ns_name
= ns_name
87 self
.applications
= {}
89 def AddApplication(self
, application_name
, callback
, *callback_args
):
90 if application_name
not in self
.applications
:
91 self
.applications
[application_name
] = {
93 'callback_args': callback_args
96 def RemoveApplication(self
, application_name
):
97 if application_name
in self
.applications
:
98 del self
.applications
[application_name
]
100 async def on_change(self
, delta
, old
, new
, model
):
101 """React to changes in the Juju model."""
103 if delta
.entity
== "unit":
104 # Ignore change events from other applications
105 if delta
.data
['application'] not in self
.applications
.keys():
110 application_name
= delta
.data
['application']
112 callback
= self
.applications
[application_name
]['callback']
114 self
.applications
[application_name
]['callback_args']
117 # Fire off a callback with the application state
121 delta
.data
['application'],
123 new
.workload_status_message
,
127 # This is a charm being removed
131 delta
.data
['application'],
135 except Exception as e
:
136 self
.log
.debug("[1] notify_callback exception: {}".format(e
))
138 elif delta
.entity
== "action":
139 # TODO: Decide how we want to notify the user of actions
141 # uuid = delta.data['id'] # The Action's unique id
142 # msg = delta.data['message'] # The output of the action
144 # if delta.data['status'] == "pending":
145 # # The action is queued
147 # elif delta.data['status'] == "completed""
148 # # The action was successful
150 # elif delta.data['status'] == "failed":
151 # # The action failed.
159 # Create unique models per network service
160 # Document all public functions
172 juju_public_key
=None,
178 Initializes the N2VC object, allowing the caller to interoperate with the VCA.
181 :param log obj: The logging object to log to
182 :param server str: The IP Address or Hostname of the Juju controller
183 :param port int: The port of the Juju Controller
184 :param user str: The Juju username to authenticate with
185 :param secret str: The Juju password to authenticate with
186 :param artifacts str: The directory where charms required by a vnfd are
188 :param loop obj: The loop to use.
189 :param juju_public_key str: The contents of the Juju public SSH key
190 :param ca_cert str: The CA certificate to use to authenticate
191 :param api_proxy str: The IP of the host machine
194 client = n2vc.vnf.N2VC(
200 artifacts='/app/storage/myvnf/charms',
202 juju_public_key='<contents of the juju public key>',
203 ca_cert='<contents of CA certificate>',
204 api_proxy='192.168.1.155'
208 # Initialize instance-level variables
211 self
.controller
= None
212 self
.connecting
= False
213 self
.authenticated
= False
214 self
.api_proxy
= api_proxy
233 self
.juju_public_key
= juju_public_key
235 self
._create
_juju
_public
_key
(juju_public_key
)
237 # TODO: Verify ca_cert is valid before using. VCA will crash
238 # if the ca_cert isn't formatted correctly.
239 # self.ca_cert = ca_cert
245 self
.log
= logging
.getLogger(__name__
)
247 # Quiet websocket traffic
248 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
249 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
250 logging
.getLogger('model').setLevel(logging
.WARN
)
251 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
253 self
.log
.debug('JujuApi: instantiated')
259 if user
.startswith('user-'):
262 self
.user
= 'user-{}'.format(user
)
264 self
.endpoint
= '%s:%d' % (server
, int(port
))
266 self
.artifacts
= artifacts
268 self
.loop
= loop
or asyncio
.get_event_loop()
271 """Close any open connections."""
274 def _create_juju_public_key(self
, public_key
):
275 """Recreate the Juju public key on disk.
277 Certain libjuju commands expect to be run from the same machine as Juju
278 is bootstrapped to. This method will write the public key to disk in
279 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
281 # Make sure that we have a public key before writing to disk
282 if public_key
is None or len(public_key
) == 0:
283 if 'OSM_VCA_PUBKEY' in os
.environ
:
284 public_key
= os
.getenv('OSM_VCA_PUBKEY', '')
285 if len(public_key
== 0):
290 path
= "{}/.local/share/juju/ssh".format(
291 os
.path
.expanduser('~'),
293 if not os
.path
.exists(path
):
296 with
open('{}/juju_id_rsa.pub'.format(path
), 'w') as f
:
299 def notify_callback(self
, model_name
, application_name
, status
, message
,
300 callback
=None, *callback_args
):
309 except Exception as e
:
310 self
.log
.error("[0] notify_callback exception {}".format(e
))
315 async def Relate(self
, model_name
, vnfd
):
316 """Create a relation between the charm-enabled VDUs in a VNF.
318 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
324 - provides: dataVM:db
327 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.
329 :param str ns_name: The name of the network service.
330 :param dict vnfd: The parsed yaml VNF descriptor.
333 # Currently, the call to Relate() is made automatically after the
334 # deployment of each charm; if the relation depends on a charm that
335 # hasn't been deployed yet, the call will fail silently. This will
336 # prevent an API breakage, with the intent of making this an explicitly
337 # required call in a more object-oriented refactor of the N2VC API.
340 vnf_config
= vnfd
.get("vnf-configuration")
342 juju
= vnf_config
['juju']
344 configs
.append(vnf_config
)
346 for vdu
in vnfd
['vdu']:
347 vdu_config
= vdu
.get('vdu-configuration')
349 juju
= vdu_config
['juju']
351 configs
.append(vdu_config
)
353 def _get_application_name(name
):
354 """Get the application name that's mapped to a vnf/vdu."""
356 vnf_name
= vnfd
['name']
358 for vdu
in vnfd
.get('vdu'):
359 # Compare the named portion of the relation to the vdu's id
360 if vdu
['id'] == name
:
361 application_name
= self
.FormatApplicationName(
364 str(vnf_member_index
),
366 return application_name
368 vnf_member_index
+= 1
372 # Loop through relations
376 if 'vca-relationships' in juju
and 'relation' in juju
['vca-relationships']:
377 for rel
in juju
['vca-relationships']['relation']:
380 # get the application name for the provides
381 (name
, endpoint
) = rel
['provides'].split(':')
382 application_name
= _get_application_name(name
)
384 provides
= "{}:{}".format(
389 # get the application name for thr requires
390 (name
, endpoint
) = rel
['requires'].split(':')
391 application_name
= _get_application_name(name
)
393 requires
= "{}:{}".format(
397 self
.log
.debug("Relation: {} <-> {}".format(
401 await self
.add_relation(
406 except Exception as e
:
407 self
.log
.debug("Exception: {}".format(e
))
411 async def DeployCharms(self
, model_name
, application_name
, vnfd
,
412 charm_path
, params
={}, machine_spec
={},
413 callback
=None, *callback_args
):
414 """Deploy one or more charms associated with a VNF.
416 Deploy the charm(s) referenced in a VNF Descriptor.
418 :param str model_name: The name or unique id of the network service.
419 :param str application_name: The name of the application
420 :param dict vnfd: The name of the application
421 :param str charm_path: The path to the Juju charm
422 :param dict params: A dictionary of runtime parameters
425 'rw_mgmt_ip': '1.2.3.4',
426 # Pass the initial-config-primitives section of the vnf or vdu
427 'initial-config-primitives': {...}
428 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
429 inside < >. rw_mgmt_ip will be included here also
431 :param dict machine_spec: A dictionary describing the machine to
435 'hostname': '1.2.3.4',
436 'username': 'ubuntu',
438 :param obj callback: A callback function to receive status changes.
439 :param tuple callback_args: A list of arguments to be passed to the
443 ########################################################
444 # Verify the path to the charm exists and is readable. #
445 ########################################################
446 if not os
.path
.exists(charm_path
):
447 self
.log
.debug("Charm path doesn't exist: {}".format(charm_path
))
448 self
.notify_callback(
455 raise JujuCharmNotFound("No artifacts configured.")
457 ################################
458 # Login to the Juju controller #
459 ################################
460 if not self
.authenticated
:
461 self
.log
.debug("Authenticating with Juju")
464 ##########################################
465 # Get the model for this network service #
466 ##########################################
467 model
= await self
.get_model(model_name
)
469 ########################################
470 # Verify the application doesn't exist #
471 ########################################
472 app
= await self
.get_application(model
, application_name
)
474 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name
, model_name
))
476 ################################################################
477 # Register this application with the model-level event monitor #
478 ################################################################
480 self
.log
.debug("JujuApi: Registering callback for {}".format(
483 await self
.Subscribe(model_name
, application_name
, callback
, *callback_args
)
485 #######################################
486 # Get the initial charm configuration #
487 #######################################
490 if 'rw_mgmt_ip' in params
:
491 rw_mgmt_ip
= params
['rw_mgmt_ip']
493 if 'initial-config-primitive' not in params
:
494 params
['initial-config-primitive'] = {}
496 initial_config
= self
._get
_config
_from
_dict
(
497 params
['initial-config-primitive'],
498 {'<rw_mgmt_ip>': rw_mgmt_ip
}
501 ########################################################
502 # Check for specific machine placement (native charms) #
503 ########################################################
507 if machine_spec
.keys():
508 if all(k
in machine_spec
for k
in ['hostname', 'username']):
510 # Allow series to be derived from the native charm
513 self
.log
.debug("Provisioning manual machine {}@{}".format(
514 machine_spec
['username'],
515 machine_spec
['hostname'],
518 """Native Charm support
520 Taking a bare VM (assumed to be an Ubuntu cloud image),
521 the provisioning process will:
522 - Create an ubuntu user w/sudo access
524 - Detect architecture
525 - Download and install Juju agent from controller
527 - Add an iptables rule to route traffic to the API proxy
530 to
= await self
.provision_machine(
531 model_name
=model_name
,
532 username
=machine_spec
['username'],
533 hostname
=machine_spec
['hostname'],
534 private_key_path
=self
.GetPrivateKeyPath(),
536 self
.log
.debug("Provisioned machine id {}".format(to
))
538 # TODO: If to is none, raise an exception
540 # The native charm won't have the sshproxy layer, typically, but LCM uses the config primitive
541 # to interpret what the values are. That's a gap to fill.
544 The ssh-* config parameters are unique to the sshproxy layer,
545 which most native charms will not be aware of.
547 Setting invalid config parameters will cause the deployment to
550 For the moment, we will strip the ssh-* parameters from native
551 charms, until the feature gap is addressed in the information
555 # Native charms don't include the ssh-* config values, so strip them
556 # from the initial_config, otherwise the deploy will raise an error.
557 # self.log.debug("Removing ssh-* from initial-config")
558 for k
in ['ssh-hostname', 'ssh-username', 'ssh-password']:
559 if k
in initial_config
:
560 self
.log
.debug("Removing parameter {}".format(k
))
561 del initial_config
[k
]
563 self
.log
.debug("JujuApi: Deploying charm ({}/{}) from {} to {}".format(
570 ########################################################
571 # Deploy the charm and apply the initial configuration #
572 ########################################################
573 app
= await model
.deploy(
574 # We expect charm_path to be either the path to the charm on disk
575 # or in the format of cs:series/name
577 # This is the formatted, unique name for this charm
578 application_name
=application_name
,
579 # Proxy charms should use the current LTS. This will need to be
580 # changed for native charms.
582 # Apply the initial 'config' primitive during deployment
583 config
=initial_config
,
584 # Where to deploy the charm to.
588 #############################
589 # Map the vdu id<->app name #
590 #############################
592 await self
.Relate(model_name
, vnfd
)
593 except KeyError as ex
:
594 # We don't currently support relations between NS and VNF/VDU charms
595 self
.log
.warn("[N2VC] Relations not supported: {}".format(ex
))
596 except Exception as ex
:
597 # This may happen if not all of the charms needed by the relation
598 # are ready. We can safely ignore this, because Relate will be
599 # retried when the endpoint of the relation is deployed.
600 self
.log
.warn("[N2VC] Relations not ready")
602 # #######################################
603 # # Execute initial config primitive(s) #
604 # #######################################
605 uuids
= await self
.ExecuteInitialPrimitives(
614 # # Build a sequential list of the primitives to execute
615 # for primitive in params['initial-config-primitive']:
617 # if primitive['name'] == 'config':
618 # # This is applied when the Application is deployed
621 # seq = primitive['seq']
624 # if 'parameter' in primitive:
625 # params = primitive['parameter']
627 # primitives[seq] = {
628 # 'name': primitive['name'],
629 # 'parameters': self._map_primitive_parameters(
631 # {'<rw_mgmt_ip>': rw_mgmt_ip}
635 # for primitive in sorted(primitives):
636 # await self.ExecutePrimitive(
639 # primitives[primitive]['name'],
642 # **primitives[primitive]['parameters'],
644 # except N2VCPrimitiveExecutionFailed as e:
646 # "[N2VC] Exception executing primitive: {}".format(e)
650 async def GetPrimitiveStatus(self
, model_name
, uuid
):
651 """Get the status of an executed Primitive.
653 The status of an executed Primitive will be one of three values:
660 if not self
.authenticated
:
663 model
= await self
.get_model(model_name
)
665 results
= await model
.get_action_status(uuid
)
668 status
= results
[uuid
]
670 except Exception as e
:
672 "Caught exception while getting primitive status: {}".format(e
)
674 raise N2VCPrimitiveExecutionFailed(e
)
678 async def GetPrimitiveOutput(self
, model_name
, uuid
):
679 """Get the output of an executed Primitive.
681 Note: this only returns output for a successfully executed primitive.
685 if not self
.authenticated
:
688 model
= await self
.get_model(model_name
)
689 results
= await model
.get_action_output(uuid
, 60)
690 except Exception as e
:
692 "Caught exception while getting primitive status: {}".format(e
)
694 raise N2VCPrimitiveExecutionFailed(e
)
698 # async def ProvisionMachine(self, model_name, hostname, username):
699 # """Provision machine for usage with Juju.
701 # Provisions a previously instantiated machine for use with Juju.
704 # if not self.authenticated:
707 # # FIXME: This is hard-coded until model-per-ns is added
708 # model_name = 'default'
710 # model = await self.get_model(model_name)
711 # model.add_machine(spec={})
713 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
720 # except Exception as e:
722 # "Caught exception while getting primitive status: {}".format(e)
724 # raise N2VCPrimitiveExecutionFailed(e)
726 def GetPrivateKeyPath(self
):
727 homedir
= os
.environ
['HOME']
728 sshdir
= "{}/.ssh".format(homedir
)
729 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
730 return private_key_path
732 async def GetPublicKey(self
):
733 """Get the N2VC SSH public key.abs
735 Returns the SSH public key, to be injected into virtual machines to
736 be managed by the VCA.
738 The first time this is run, a ssh keypair will be created. The public
739 key is injected into a VM so that we can provision the machine with
740 Juju, after which Juju will communicate with the VM directly via the
745 # Find the path to where we expect our key to live.
746 homedir
= os
.environ
['HOME']
747 sshdir
= "{}/.ssh".format(homedir
)
748 if not os
.path
.exists(sshdir
):
751 private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
752 public_key_path
= "{}.pub".format(private_key_path
)
754 # If we don't have a key generated, generate it.
755 if not os
.path
.exists(private_key_path
):
756 cmd
= "ssh-keygen -t {} -b {} -N '' -f {}".format(
761 subprocess
.check_output(shlex
.split(cmd
))
763 # Read the public key
764 with
open(public_key_path
, "r") as f
:
765 public_key
= f
.readline()
769 async def ExecuteInitialPrimitives(self
, model_name
, application_name
,
770 params
, callback
=None, *callback_args
):
771 """Execute multiple primitives.
773 Execute multiple primitives as declared in initial-config-primitive.
774 This is useful in cases where the primitives initially failed -- for
775 example, if the charm is a proxy but the proxy hasn't been configured
781 # Build a sequential list of the primitives to execute
782 for primitive
in params
['initial-config-primitive']:
784 if primitive
['name'] == 'config':
787 seq
= primitive
['seq']
790 if 'parameter' in primitive
:
791 params_
= primitive
['parameter']
793 user_values
= params
.get("user_values", {})
794 if 'rw_mgmt_ip' not in user_values
:
795 user_values
['rw_mgmt_ip'] = None
796 # just for backward compatibility, because it will be provided always by modern version of LCM
799 'name': primitive
['name'],
800 'parameters': self
._map
_primitive
_parameters
(
806 for primitive
in sorted(primitives
):
808 # self.log.debug("Queuing action {}".format(primitives[primitive]['name']))
810 await self
.ExecutePrimitive(
813 primitives
[primitive
]['name'],
816 **primitives
[primitive
]['parameters'],
819 except PrimitiveDoesNotExist
as e
:
820 self
.log
.debug("Ignoring exception PrimitiveDoesNotExist: {}".format(e
))
822 except Exception as e
:
823 self
.log
.debug("XXXXXXXXXXXXXXXXXXXXXXXXX Unexpected exception: {}".format(e
))
826 except N2VCPrimitiveExecutionFailed
as e
:
828 "[N2VC] Exception executing primitive: {}".format(e
)
833 async def ExecutePrimitive(self
, model_name
, application_name
, primitive
,
834 callback
, *callback_args
, **params
):
835 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
837 Execute a primitive defined in the VNF descriptor.
839 :param str model_name: The name or unique id of the network service.
840 :param str application_name: The name of the application
841 :param str primitive: The name of the primitive to execute.
842 :param obj callback: A callback function to receive status changes.
843 :param tuple callback_args: A list of arguments to be passed to the
845 :param dict params: A dictionary of key=value pairs representing the
846 primitive's parameters
849 'rw_mgmt_ip': '1.2.3.4',
850 # Pass the initial-config-primitives section of the vnf or vdu
851 'initial-config-primitives': {...}
854 self
.log
.debug("Executing primitive={} params={}".format(primitive
, params
))
857 if not self
.authenticated
:
860 model
= await self
.get_model(model_name
)
862 if primitive
== 'config':
863 # config is special, and expecting params to be a dictionary
864 await self
.set_config(
870 app
= await self
.get_application(model
, application_name
)
872 # Does this primitive exist?
873 actions
= await app
.get_actions()
875 if primitive
not in actions
.keys():
876 raise PrimitiveDoesNotExist("Primitive {} does not exist".format(primitive
))
878 # Run against the first (and probably only) unit in the app
881 action
= await unit
.run_action(primitive
, **params
)
883 except PrimitiveDoesNotExist
as e
:
884 # Catch and raise this exception if it's thrown from the inner block
886 except Exception as e
:
887 # An unexpected exception was caught
889 "Caught exception while executing primitive: {}".format(e
)
891 raise N2VCPrimitiveExecutionFailed(e
)
894 async def RemoveCharms(self
, model_name
, application_name
, callback
=None,
896 """Remove a charm from the VCA.
898 Remove a charm referenced in a VNF Descriptor.
900 :param str model_name: The name of the network service.
901 :param str application_name: The name of the application
902 :param obj callback: A callback function to receive status changes.
903 :param tuple callback_args: A list of arguments to be passed to the
907 if not self
.authenticated
:
910 model
= await self
.get_model(model_name
)
911 app
= await self
.get_application(model
, application_name
)
913 # Remove this application from event monitoring
914 await self
.Unsubscribe(model_name
, application_name
)
916 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
918 "Removing the application {}".format(application_name
)
922 # await self.disconnect_model(self.monitors[model_name])
924 self
.notify_callback(
928 "Removing charm {}".format(application_name
),
933 except Exception as e
:
934 print("Caught exception: {}".format(e
))
938 async def CreateNetworkService(self
, ns_uuid
):
939 """Create a new Juju model for the Network Service.
941 Creates a new Model in the Juju Controller.
943 :param str ns_uuid: A unique id representing an instaance of a
946 :returns: True if the model was created. Raises JujuError on failure.
948 if not self
.authenticated
:
951 models
= await self
.controller
.list_models()
952 if ns_uuid
not in models
:
954 self
.models
[ns_uuid
] = await self
.controller
.add_model(
957 except JujuError
as e
:
958 if "already exists" not in e
.message
:
961 # Create an observer for this model
962 await self
.create_model_monitor(ns_uuid
)
966 async def DestroyNetworkService(self
, ns_uuid
):
967 """Destroy a Network Service.
969 Destroy the Network Service and any deployed charms.
971 :param ns_uuid The unique id of the Network Service
973 :returns: True if the model was created. Raises JujuError on failure.
976 # Do not delete the default model. The default model was used by all
977 # Network Services, prior to the implementation of a model per NS.
978 if ns_uuid
.lower() == "default":
981 if not self
.authenticated
:
984 models
= await self
.controller
.list_models()
985 if ns_uuid
in models
:
986 model
= await self
.controller
.get_model(ns_uuid
)
988 for application
in model
.applications
:
989 app
= model
.applications
[application
]
991 await self
.RemoveCharms(ns_uuid
, application
)
993 self
.log
.debug("Unsubscribing Watcher for {}".format(application
))
994 await self
.Unsubscribe(ns_uuid
, application
)
996 self
.log
.debug("Waiting for application to terminate")
999 await model
.block_until(
1001 unit
.workload_status
in ['terminated'] for unit
in app
.units
1005 except Exception as e
:
1006 self
.log
.debug("Timed out waiting for {} to terminate.".format(application
))
1008 for machine
in model
.machines
:
1010 self
.log
.debug("Destroying machine {}".format(machine
))
1011 await model
.machines
[machine
].destroy(force
=True)
1012 except JujuAPIError
as e
:
1013 if 'does not exist' in str(e
):
1014 # Our cached model may be stale, because the machine
1015 # has already been removed. It's safe to continue.
1018 self
.log
.debug("Caught exception: {}".format(e
))
1021 # Disconnect from the Model
1022 if ns_uuid
in self
.models
:
1023 self
.log
.debug("Disconnecting model {}".format(ns_uuid
))
1024 # await self.disconnect_model(self.models[ns_uuid])
1025 await self
.disconnect_model(ns_uuid
)
1028 self
.log
.debug("Destroying model {}".format(ns_uuid
))
1029 await self
.controller
.destroy_models(ns_uuid
)
1031 raise NetworkServiceDoesNotExist(
1032 "The Network Service '{}' does not exist".format(ns_uuid
)
1037 async def GetMetrics(self
, model_name
, application_name
):
1038 """Get the metrics collected by the VCA.
1040 :param model_name The name or unique id of the network service
1041 :param application_name The name of the application
1044 model
= await self
.get_model(model_name
)
1045 app
= await self
.get_application(model
, application_name
)
1047 metrics
= await app
.get_metrics()
1051 async def HasApplication(self
, model_name
, application_name
):
1052 model
= await self
.get_model(model_name
)
1053 app
= await self
.get_application(model
, application_name
)
1058 async def Subscribe(self
, ns_name
, application_name
, callback
, *callback_args
):
1059 """Subscribe to callbacks for an application.
1061 :param ns_name str: The name of the Network Service
1062 :param application_name str: The name of the application
1063 :param callback obj: The callback method
1064 :param callback_args list: The list of arguments to append to calls to
1067 self
.monitors
[ns_name
].AddApplication(
1073 async def Unsubscribe(self
, ns_name
, application_name
):
1074 """Unsubscribe to callbacks for an application.
1076 Unsubscribes the caller from notifications from a deployed application.
1078 :param ns_name str: The name of the Network Service
1079 :param application_name str: The name of the application
1081 self
.monitors
[ns_name
].RemoveApplication(
1085 # Non-public methods
1086 async def add_relation(self
, model_name
, relation1
, relation2
):
1088 Add a relation between two application endpoints.
1090 :param str model_name: The name or unique id of the network service
1091 :param str relation1: '<application>[:<relation_name>]'
1092 :param str relation2: '<application>[:<relation_name>]'
1095 if not self
.authenticated
:
1098 m
= await self
.get_model(model_name
)
1100 await m
.add_relation(relation1
, relation2
)
1101 except JujuAPIError
as e
:
1102 # If one of the applications in the relationship doesn't exist,
1103 # or the relation has already been added, let the operation fail
1105 if 'not found' in e
.message
:
1107 if 'already exists' in e
.message
:
1112 # async def apply_config(self, config, application):
1113 # """Apply a configuration to the application."""
1114 # print("JujuApi: Applying configuration to {}.".format(
1117 # return await self.set_config(application=application, config=config)
1119 def _get_config_from_dict(self
, config_primitive
, values
):
1120 """Transform the yang config primitive to dict.
1129 for primitive
in config_primitive
:
1130 if primitive
['name'] == 'config':
1131 # config = self._map_primitive_parameters()
1132 for parameter
in primitive
['parameter']:
1133 param
= str(parameter
['name'])
1134 if parameter
['value'] == "<rw_mgmt_ip>":
1135 config
[param
] = str(values
[parameter
['value']])
1137 config
[param
] = str(parameter
['value'])
1141 def _map_primitive_parameters(self
, parameters
, user_values
):
1143 for parameter
in parameters
:
1144 param
= str(parameter
['name'])
1145 value
= parameter
.get('value')
1147 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
1148 # Must exist at user_values except if there is a default value
1149 if isinstance(value
, str) and value
.startswith("<") and value
.endswith(">"):
1150 if parameter
['value'][1:-1] in user_values
:
1151 value
= user_values
[parameter
['value'][1:-1]]
1152 elif 'default-value' in parameter
:
1153 value
= parameter
['default-value']
1155 raise KeyError("parameter {}='{}' not supplied ".format(param
, value
))
1157 # If there's no value, use the default-value (if set)
1158 if value
is None and 'default-value' in parameter
:
1159 value
= parameter
['default-value']
1161 # Typecast parameter value, if present
1162 paramtype
= "string"
1164 if 'data-type' in parameter
:
1165 paramtype
= str(parameter
['data-type']).lower()
1167 if paramtype
== "integer":
1169 elif paramtype
== "boolean":
1174 # If there's no data-type, assume the value is a string
1177 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param
, value
, paramtype
))
1179 params
[param
] = value
1182 def _get_config_from_yang(self
, config_primitive
, values
):
1183 """Transform the yang config primitive to dict."""
1185 for primitive
in config_primitive
.values():
1186 if primitive
['name'] == 'config':
1187 for parameter
in primitive
['parameter'].values():
1188 param
= str(parameter
['name'])
1189 if parameter
['value'] == "<rw_mgmt_ip>":
1190 config
[param
] = str(values
[parameter
['value']])
1192 config
[param
] = str(parameter
['value'])
1196 def FormatApplicationName(self
, *args
):
1198 Generate a Juju-compatible Application name
1200 :param args tuple: Positional arguments to be used to construct the
1204 - Only accepts characters a-z and non-consequitive dashes (-)
1205 - Application name should not exceed 50 characters
1209 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
1212 for c
in "-".join(list(args
)):
1214 c
= chr(97 + int(c
))
1215 elif not c
.isalpha():
1218 return re
.sub('-+', '-', appname
.lower())
1220 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
1221 # """Format the name of the application
1224 # - Only accepts characters a-z and non-consequitive dashes (-)
1225 # - Application name should not exceed 50 characters
1227 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1231 # c = chr(97 + int(c))
1232 # elif not c.isalpha():
1235 # return re.sub('\-+', '-', new_name.lower())
1237 def format_model_name(self
, name
):
1238 """Format the name of model.
1240 Model names may only contain lowercase letters, digits and hyphens
1243 return name
.replace('_', '-').lower()
1245 async def get_application(self
, model
, application
):
1246 """Get the deployed application."""
1247 if not self
.authenticated
:
1251 if application
and model
:
1252 if model
.applications
:
1253 if application
in model
.applications
:
1254 app
= model
.applications
[application
]
1258 async def get_model(self
, model_name
):
1259 """Get a model from the Juju Controller.
1261 Note: Model objects returned must call disconnected() before it goes
1263 if not self
.authenticated
:
1266 if model_name
not in self
.models
:
1267 # Get the models in the controller
1268 models
= await self
.controller
.list_models()
1270 if model_name
not in models
:
1272 self
.models
[model_name
] = await self
.controller
.add_model(
1275 except JujuError
as e
:
1276 if "already exists" not in e
.message
:
1279 self
.models
[model_name
] = await self
.controller
.get_model(
1283 self
.refcount
['model'] += 1
1285 # Create an observer for this model
1286 await self
.create_model_monitor(model_name
)
1288 return self
.models
[model_name
]
1290 async def create_model_monitor(self
, model_name
):
1291 """Create a monitor for the model, if none exists."""
1292 if not self
.authenticated
:
1295 if model_name
not in self
.monitors
:
1296 self
.monitors
[model_name
] = VCAMonitor(model_name
)
1297 self
.models
[model_name
].add_observer(self
.monitors
[model_name
])
1301 async def login(self
):
1302 """Login to the Juju controller."""
1304 if self
.authenticated
:
1307 self
.connecting
= True
1309 self
.log
.debug("JujuApi: Logging into controller")
1311 self
.controller
= Controller(loop
=self
.loop
)
1315 "Connecting to controller... ws://{}:{} as {}/{}".format(
1322 await self
.controller
.connect(
1323 endpoint
=self
.endpoint
,
1325 password
=self
.secret
,
1326 cacert
=self
.ca_cert
,
1328 self
.refcount
['controller'] += 1
1330 # current_controller no longer exists
1331 # self.log.debug("Connecting to current controller...")
1332 # await self.controller.connect_current()
1333 # await self.controller.connect(
1334 # endpoint=self.endpoint,
1335 # username=self.user,
1338 self
.log
.fatal("VCA credentials not configured.")
1340 self
.authenticated
= True
1341 self
.log
.debug("JujuApi: Logged into controller")
1343 async def logout(self
):
1344 """Logout of the Juju controller."""
1345 if not self
.authenticated
:
1349 for model
in self
.models
:
1350 await self
.disconnect_model(model
)
1353 self
.log
.debug("Disconnecting controller {}".format(
1356 await self
.controller
.disconnect()
1357 self
.refcount
['controller'] -= 1
1358 self
.controller
= None
1360 self
.authenticated
= False
1362 self
.log
.debug(self
.refcount
)
1364 except Exception as e
:
1366 "Fatal error logging out of Juju Controller: {}".format(e
)
1371 async def disconnect_model(self
, model
):
1372 self
.log
.debug("Disconnecting model {}".format(model
))
1373 if model
in self
.models
:
1375 await self
.models
[model
].disconnect()
1376 self
.refcount
['model'] -= 1
1377 self
.models
[model
] = None
1378 except Exception as e
:
1379 self
.log
.debug("Caught exception: {}".format(e
))
1381 async def provision_machine(self
, model_name
: str,
1382 hostname
: str, username
: str,
1383 private_key_path
: str) -> int:
1384 """Provision a machine.
1386 This executes the SSH provisioner, which will log in to a machine via
1387 SSH and prepare it for use with the Juju model
1389 :param model_name str: The name of the model
1390 :param hostname str: The IP or hostname of the target VM
1391 :param user str: The username to login to
1392 :param private_key_path str: The path to the private key that's been injected to the VM via cloud-init
1393 :return machine_id int: Returns the id of the machine or None if provisioning fails
1395 if not self
.authenticated
:
1401 self
.log
.debug("Instantiating SSH Provisioner for {}@{} ({})".format(
1406 provisioner
= SSHProvisioner(
1409 private_key_path
=private_key_path
,
1415 params
= provisioner
.provision_machine()
1416 except Exception as ex
:
1417 self
.log
.debug("caught exception from provision_machine: {}".format(ex
))
1421 params
.jobs
= ['JobHostUnits']
1423 model
= await self
.get_model(model_name
)
1425 connection
= model
.connection()
1427 # Submit the request.
1428 self
.log
.debug("Adding machine to model")
1429 client_facade
= client
.ClientFacade
.from_connection(connection
)
1430 results
= await client_facade
.AddMachines(params
=[params
])
1431 error
= results
.machines
[0].error
1433 raise ValueError("Error adding machine: %s" % error
.message
)
1435 machine_id
= results
.machines
[0].machine
1437 # Need to run this after AddMachines has been called,
1438 # as we need the machine_id
1439 self
.log
.debug("Installing Juju agent")
1440 await provisioner
.install_agent(
1447 self
.log
.debug("Missing API Proxy")
1450 # async def remove_application(self, name):
1451 # """Remove the application."""
1452 # if not self.authenticated:
1453 # await self.login()
1455 # app = await self.get_application(name)
1457 # self.log.debug("JujuApi: Destroying application {}".format(
1461 # await app.destroy()
1463 async def remove_relation(self
, a
, b
):
1465 Remove a relation between two application endpoints
1467 :param a An application endpoint
1468 :param b An application endpoint
1470 if not self
.authenticated
:
1473 m
= await self
.get_model()
1475 m
.remove_relation(a
, b
)
1477 await m
.disconnect()
1479 async def resolve_error(self
, model_name
, application
=None):
1480 """Resolve units in error state."""
1481 if not self
.authenticated
:
1484 model
= await self
.get_model(model_name
)
1486 app
= await self
.get_application(model
, application
)
1489 "JujuApi: Resolving errors for application {}".format(
1494 for unit
in app
.units
:
1495 app
.resolved(retry
=True)
1497 async def run_action(self
, model_name
, application
, action_name
, **params
):
1498 """Execute an action and return an Action object."""
1499 if not self
.authenticated
:
1509 model
= await self
.get_model(model_name
)
1511 app
= await self
.get_application(model
, application
)
1513 # We currently only have one unit per application
1514 # so use the first unit available.
1518 "JujuApi: Running Action {} against Application {}".format(
1524 action
= await unit
.run_action(action_name
, **params
)
1526 # Wait for the action to complete
1529 result
['status'] = action
.status
1530 result
['action']['tag'] = action
.data
['id']
1531 result
['action']['results'] = action
.results
1535 async def set_config(self
, model_name
, application
, config
):
1536 """Apply a configuration to the application."""
1537 if not self
.authenticated
:
1540 app
= await self
.get_application(model_name
, application
)
1542 self
.log
.debug("JujuApi: Setting config for Application {}".format(
1545 await app
.set_config(config
)
1547 # Verify the config is set
1548 newconf
= await app
.get_config()
1550 if config
[key
] != newconf
[key
]['value']:
1551 self
.log
.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key
, config
[key
], newconf
[key
]))
1553 # async def set_parameter(self, parameter, value, application=None):
1554 # """Set a config parameter for a service."""
1555 # if not self.authenticated:
1556 # await self.login()
1558 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1563 # return await self.apply_config(
1564 # {parameter: value},
1565 # application=application,
1568 async def wait_for_application(self
, model_name
, application_name
,
1570 """Wait for an application to become active."""
1571 if not self
.authenticated
:
1574 model
= await self
.get_model(model_name
)
1576 app
= await self
.get_application(model
, application_name
)
1577 self
.log
.debug("Application: {}".format(app
))
1580 "JujuApi: Waiting {} seconds for Application {}".format(
1586 await model
.block_until(
1588 unit
.agent_status
== 'idle' and unit
.workload_status
in
1589 ['active', 'unknown'] for unit
in app
.units