Add juju support to lcm 40/5840/15
authorAdam Israel <adam.israel@canonical.com>
Mon, 5 Feb 2018 12:52:15 +0000 (13:52 +0100)
committerAdam Israel <adam.israel@canonical.com>
Thu, 8 Feb 2018 10:11:34 +0000 (11:11 +0100)
This adds basic Juju support to the lcm.
- Add a ModelObserver to watch model changes and update nsr_lcm when a
  charm is deployed and either ready or blocked.
- Refactor DeployApplication to take ns and vnf record as parameters and
  extract necessary information to deploy and configure charm
- Add basic checks for proxy/non-proxy charm
- Rebase against latest lcm changes
- Automatically remove a charm when deleting the associated network
  service
- Revert lcm.cfg
- Address changes per discussion this morning

TODO:
- Finish adding non-proxy charm support (need to pick appropriate
  example to highlight this)

Signed-off-by: Adam Israel <adam.israel@canonical.com>
Change-Id: Iffd24293722a050630bcb5812d3f15217067b238

lcm/osm_lcm/juju_api.py [new file with mode: 0644]
lcm/osm_lcm/lcm.py
lcm/osm_lcm/vca.py [new file with mode: 0644]

diff --git a/lcm/osm_lcm/juju_api.py b/lcm/osm_lcm/juju_api.py
new file mode 100644 (file)
index 0000000..9b8fa5a
--- /dev/null
@@ -0,0 +1,855 @@
+############################################################################
+# Copyright 2016 RIFT.io Inc                                               #
+#                                                                          #
+# Licensed under the Apache License, Version 2.0 (the "License");          #
+# you may not use this file except in compliance with the License.         #
+# You may obtain a copy of the License at                                  #
+#                                                                          #
+#     http://www.apache.org/licenses/LICENSE-2.0                           #
+#                                                                          #
+# Unless required by applicable law or agreed to in writing, software      #
+# distributed under the License is distributed on an "AS IS" BASIS,        #
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+# See the License for the specific language governing permissions and      #
+# limitations under the License.                                           #
+############################################################################
+
+import argparse
+import asyncio
+import logging
+import os
+import ssl
+
+import juju.loop
+from juju.controller import Controller
+from juju.model import Model, ModelObserver
+
+try:
+    ssl._create_default_https_context = ssl._create_unverified_context
+except AttributeError:
+    # Legacy Python doesn't verify by default (see pep-0476)
+    #   https://www.python.org/dev/peps/pep-0476/
+    pass
+
+
+class JujuVersionError(Exception):
+    pass
+
+
+class JujuApiError(Exception):
+    pass
+
+
+class JujuEnvError(JujuApiError):
+    pass
+
+
+class JujuModelError(JujuApiError):
+    pass
+
+
+class JujuStatusError(JujuApiError):
+    pass
+
+
+class JujuUnitsError(JujuApiError):
+    pass
+
+
+class JujuWaitUnitsError(JujuApiError):
+    pass
+
+
+class JujuSrvNotDeployedError(JujuApiError):
+    pass
+
+
+class JujuAddCharmError(JujuApiError):
+    pass
+
+
+class JujuDeployError(JujuApiError):
+    pass
+
+
+class JujuDestroyError(JujuApiError):
+    pass
+
+
+class JujuResolveError(JujuApiError):
+    pass
+
+
+class JujuActionError(JujuApiError):
+    pass
+
+
+class JujuActionApiError(JujuActionError):
+    pass
+
+
+class JujuActionInfoError(JujuActionError):
+    pass
+
+
+class JujuActionExecError(JujuActionError):
+    pass
+
+
+class JujuAuthenticationError(Exception):
+    pass
+
+
+class JujuMonitor(ModelObserver):
+    """Monitor state changes within the Juju Model."""
+    # async def on_change(self, delta, old, new, model):
+    #     """React to changes in the Juju model."""
+    #
+    #     # TODO: Setup the hook to update the UI if the status of a unit changes
+    #     # to be used when deploying a charm and waiting for it to be "ready"
+    #     if delta.entity in ['application', 'unit'] and delta.type == "change":
+    #         pass
+    #
+    #     # TODO: Add a hook when an action is complete
+
+    pass
+
+
+class JujuApi(object):
+    """JujuApi wrapper on jujuclient library.
+
+    There should be one instance of JujuApi for each VNF manged by Juju.
+
+    Assumption:
+        Currently we use one unit per service/VNF. So once a service
+        is deployed, we store the unit name and reuse it
+    """
+
+    log = None
+    controller = None
+    models = {}
+    model = None
+    model_name = None
+    model_uuid = None
+    authenticated = False
+
+    def __init__(self,
+                 log=None,
+                 loop=None,
+                 server='127.0.0.1',
+                 port=17070,
+                 user='admin',
+                 secret=None,
+                 version=None,
+                 model_name='default',
+                 ):
+        """Initialize with the Juju credentials."""
+
+        if log:
+            self.log = log
+        else:
+            self.log = logging.getLogger(__name__)
+
+        # Quiet websocket traffic
+        logging.getLogger('websockets.protocol').setLevel(logging.INFO)
+
+        self.log.debug('JujuApi: instantiated')
+
+        self.server = server
+        self.port = port
+
+        self.secret = secret
+        if user.startswith('user-'):
+            self.user = user
+        else:
+            self.user = 'user-{}'.format(user)
+
+        self.endpoint = '%s:%d' % (server, int(port))
+
+        self.model_name = model_name
+
+        if loop:
+            self.loop = loop
+
+    def __del__(self):
+        """Close any open connections."""
+        yield self.logout()
+
+    async def add_relation(self, a, b, via=None):
+        """
+        Add a relation between two application endpoints.
+
+        :param a An application endpoint
+        :param b An application endpoint
+        :param via The egress subnet(s) for outbound traffic, e.g., (192.168.0.0/16,10.0.0.0/8)
+        """
+        if not self.authenticated:
+            await self.login()
+
+        m = await self.get_model()
+        try:
+            m.add_relation(a, b, via)
+        finally:
+            await m.disconnect()
+
+    async def apply_config(self, config, application):
+        """Apply a configuration to the application."""
+        self.log.debug("JujuApi: Applying configuration to {}.".format(
+            application
+        ))
+        return await self.set_config(application=application, config=config)
+
+    async def deploy_application(self, charm, name="", path="", specs={}):
+        """
+        Deploy an application.
+
+        Deploy an application to a container or a machine already provisioned
+        by the OSM Resource Orchestrator (requires the Juju public ssh key
+        installed on the new machine via cloud-init).
+
+        :param str charm: The name of the charm
+        :param str name: The name of the application, if different than the charm
+        :param str path: The path to the charm
+        :param dict machine: A dictionary identifying the machine to manage via Juju
+            Examples::
+
+                deploy_application(..., specs={'host': '10.0.0.4', 'user': 'ubuntu'})
+        """
+        if not self.authenticated:
+            await self.login()
+
+        # Check that the charm is valid and exists.
+        if charm is None:
+            return None
+
+        app = await self.get_application(name)
+        if app is None:
+
+            # Check for specific machine placement
+            to = None
+            if all(k in specs for k in ['hostname', 'username']):
+                machine = await self.model.add_machine(spec='ssh:%@%'.format(
+                    specs['host'],
+                    specs['user'],
+                ))
+                to = machine.id
+
+            # TODO: Handle the error if the charm isn't found.
+            self.log.debug("JujuApi: Deploying charm {} ({}) from {}".format(
+                charm,
+                name,
+                path,
+                to=to,
+            ))
+            app = await self.model.deploy(
+                path,
+                application_name=name,
+                series='xenial',
+            )
+        return app
+    deploy_service = deploy_application
+
+    async def get_action_status(self, uuid):
+        """Get the status of an action."""
+        if not self.authenticated:
+            await self.login()
+
+        self.log.debug("JujuApi: Waiting for status of action uuid {}".format(uuid))
+        action = await self.model.wait_for_action(uuid)
+        return action.status
+
+    async def get_application(self, application):
+        """Get the deployed application."""
+        if not self.authenticated:
+            await self.login()
+
+        self.log.debug("JujuApi: Getting application {}".format(application))
+        app = None
+        if application and self.model:
+            if self.model.applications:
+                if application in self.model.applications:
+                    app = self.model.applications[application]
+        return app
+
+    async def get_application_status(self, application):
+        """Get the status of an application."""
+        if not self.authenticated:
+            await self.login()
+
+        status = None
+        app = await self.get_application(application)
+        if app:
+            status = app.status
+            self.log.debug("JujuApi: Status of application {} is {}".format(
+                application,
+                str(status),
+            ))
+        return status
+    get_service_status = get_application_status
+
+    async def get_config(self, application):
+        """Get the configuration of an application."""
+        if not self.authenticated:
+            await self.login()
+
+        config = None
+        app = await self.get_application(application)
+        if app:
+            config = await app.get_config()
+
+        self.log.debug("JujuApi: Config of application {} is {}".format(
+            application,
+            str(config),
+        ))
+
+        return config
+
+    async def get_model(self, name='default'):
+        """Get a model from the Juju Controller.
+
+        Note: Model objects returned must call disconnected() before it goes
+        out of scope."""
+        if not self.authenticated:
+            await self.login()
+
+        model = Model()
+
+        uuid = await self.get_model_uuid(name)
+
+        self.log.debug("JujuApi: Connecting to model {} ({})".format(
+            model,
+            uuid,
+        ))
+
+        await model.connect(
+            self.endpoint,
+            uuid,
+            self.user,
+            self.secret,
+            None,
+        )
+
+        return model
+
+    async def get_model_uuid(self, name='default'):
+        """Get the UUID of a model.
+
+        Iterate through all models in a controller and find the matching model.
+        """
+        if not self.authenticated:
+            await self.login()
+
+        uuid = None
+
+        models = await self.controller.get_models()
+
+        self.log.debug("JujuApi: Looking through {} models for model {}".format(
+            len(models.user_models),
+            name,
+        ))
+        for model in models.user_models:
+            if model.model.name == name:
+                uuid = model.model.uuid
+                break
+
+        return uuid
+
+    async def get_status(self):
+        """Get the model status."""
+        if not self.authenticated:
+            await self.login()
+
+        if not self.model:
+            self.model = self.get_model(self.model_name)
+
+        class model_state:
+            applications = {}
+            machines = {}
+            relations = {}
+
+        self.log.debug("JujuApi: Getting model status")
+        status = model_state()
+        status.applications = self.model.applications
+        status.machines = self.model.machines
+
+        return status
+
+    async def is_application_active(self, application):
+        """Check if the application is in an active state."""
+        if not self.authenticated:
+            await self.login()
+
+        state = False
+        status = await self.get_application_status(application)
+        if status and status in ['active']:
+            state = True
+
+        self.log.debug("JujuApi: Application {} is {} active".format(
+            application,
+            "" if status else "not",
+        ))
+
+        return state
+    is_service_active = is_application_active
+
+    async def is_application_blocked(self, application):
+        """Check if the application is in a blocked state."""
+        if not self.authenticated:
+            await self.login()
+
+        state = False
+        status = await self.get_application_status(application)
+        if status and status in ['blocked']:
+            state = True
+
+        self.log.debug("JujuApi: Application {} is {} blocked".format(
+            application,
+            "" if status else "not",
+        ))
+
+        return state
+    is_service_blocked = is_application_blocked
+
+    async def is_application_deployed(self, application):
+        """Check if the application is in a deployed state."""
+        if not self.authenticated:
+            await self.login()
+
+        state = False
+        status = await self.get_application_status(application)
+        if status and status in ['active']:
+            state = True
+        self.log.debug("JujuApi: Application {} is {} deployed".format(
+            application,
+            "" if status else "not",
+        ))
+
+        return state
+    is_service_deployed = is_application_deployed
+
+    async def is_application_error(self, application):
+        """Check if the application is in an error state."""
+        if not self.authenticated:
+            await self.login()
+
+        state = False
+        status = await self.get_application_status(application)
+        if status and status in ['error']:
+            state = True
+        self.log.debug("JujuApi: Application {} is {} errored".format(
+            application,
+            "" if status else "not",
+        ))
+
+        return state
+    is_service_error = is_application_error
+
+    async def is_application_maint(self, application):
+        """Check if the application is in a maintenance state."""
+        if not self.authenticated:
+            await self.login()
+
+        state = False
+        status = await self.get_application_status(application)
+        if status and status in ['maintenance']:
+            state = True
+        self.log.debug("JujuApi: Application {} is {} in maintenence".format(
+            application,
+            "" if status else "not",
+        ))
+
+        return state
+    is_service_maint = is_application_maint
+
+    async def is_application_up(self, application=None):
+        """Check if the application is up."""
+        if not self.authenticated:
+            await self.login()
+        state = False
+
+        status = await self.get_application_status(application)
+        if status and status in ['active', 'blocked']:
+            state = True
+        self.log.debug("JujuApi: Application {} is {} up".format(
+            application,
+            "" if status else "not",
+        ))
+        return state
+    is_service_up = is_application_up
+
+    async def login(self):
+        """Login to the Juju controller."""
+        if self.authenticated:
+            return
+        cacert = None
+        self.controller = Controller()
+
+        self.log.debug("JujuApi: Logging into controller")
+
+        if self.secret:
+            await self.controller.connect(
+                self.endpoint,
+                self.user,
+                self.secret,
+                cacert,
+            )
+        else:
+            await self.controller.connect_current()
+
+        self.authenticated = True
+        self.model = await self.get_model(self.model_name)
+
+    async def logout(self):
+        """Logout of the Juju controller."""
+        if not self.authenticated:
+            return
+
+        if self.model:
+            await self.model.disconnect()
+            self.model = None
+        if self.controller:
+            await self.controller.disconnect()
+            self.controller = None
+
+        self.authenticated = False
+
+    async def remove_application(self, name):
+        """Remove the application."""
+        if not self.authenticated:
+            await self.login()
+
+        app = await self.get_application(name)
+        if app:
+            self.log.debug("JujuApi: Destroying application {}".format(
+                name,
+            ))
+
+            await app.destroy()
+
+    async def remove_relation(self, a, b):
+        """
+        Remove a relation between two application endpoints
+
+        :param a An application endpoint
+        :param b An application endpoint
+        """
+        if not self.authenticated:
+            await self.login()
+
+        m = await self.get_model()
+        try:
+            m.remove_relation(a, b)
+        finally:
+            await m.disconnect()
+
+    async def resolve_error(self, application=None):
+        """Resolve units in error state."""
+        if not self.authenticated:
+            await self.login()
+
+        app = await self.get_application(application)
+        if app:
+            self.log.debug("JujuApi: Resolving errors for application {}".format(
+                application,
+            ))
+
+            for unit in app.units:
+                app.resolved(retry=True)
+
+    async def run_action(self, application, action_name, **params):
+        """Execute an action and return an Action object."""
+        if not self.authenticated:
+            await self.login()
+        result = {
+            'status': '',
+            'action': {
+                'tag': None,
+                'results': None,
+            }
+        }
+        app = await self.get_application(application)
+        if app:
+            # We currently only have one unit per application
+            # so use the first unit available.
+            unit = app.units[0]
+
+            self.log.debug("JujuApi: Running Action {} against Application {}".format(
+                action_name,
+                application,
+            ))
+
+            action = await unit.run_action(action_name, **params)
+
+            # Wait for the action to complete
+            await action.wait()
+
+            result['status'] = action.status
+            result['action']['tag'] = action.data['id']
+            result['action']['results'] = action.results
+
+        return result
+    execute_action = run_action
+
+    async def set_config(self, application, config):
+        """Apply a configuration to the application."""
+        if not self.authenticated:
+            await self.login()
+
+        app = await self.get_application(application)
+        if app:
+            self.log.debug("JujuApi: Setting config for Application {}".format(
+                application,
+            ))
+            await app.set_config(config)
+
+            # Verify the config is set
+            newconf = await app.get_config()
+            for key in config:
+                if config[key] != newconf[key]:
+                    self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
+
+
+    async def set_parameter(self, parameter, value, application=None):
+        """Set a config parameter for a service."""
+        if not self.authenticated:
+            await self.login()
+
+        self.log.debug("JujuApi: Setting {}={} for Application {}".format(
+            parameter,
+            value,
+            application,
+        ))
+        return await self.apply_config(
+            {parameter: value},
+            application=application,
+        )
+
+    async def wait_for_application(self, name, timeout=300):
+        """Wait for an application to become active."""
+        if not self.authenticated:
+            await self.login()
+
+        app = await self.get_application(name)
+        if app:
+            self.log.debug("JujuApi: Waiting {} seconds for Application {}".format(
+                timeout,
+                name,
+            ))
+
+            await self.model.block_until(
+                lambda: all(
+                    unit.agent_status == 'idle'
+                    and unit.workload_status
+                    in ['active', 'unknown'] for unit in app.units
+                    ),
+                timeout=timeout,
+                )
+
+
+def get_argparser():
+    parser = argparse.ArgumentParser(description='Test Driver for Juju API')
+
+    ###################
+    # Authentication  #
+    ###################
+    parser.add_argument(
+        "-s", "--server",
+        default='10.0.202.49',
+        help="Juju controller",
+    )
+    parser.add_argument(
+        "-u", "--user",
+        default='admin',
+        help="User, default user-admin"
+    )
+    parser.add_argument(
+        "-p", "--password",
+        default='',
+        help="Password for the user"
+    )
+    parser.add_argument(
+        "-P", "--port",
+        default=17070,
+        help="Port number, default 17070"
+    )
+    parser.add_argument(
+        "-m", "--model",
+        default='default',
+        help="The model to connect to."
+    )
+
+    ##########
+    # Charm  #
+    ##########
+    parser.add_argument(
+        "-d", "--directory",
+        help="Local directory for the charm"
+    )
+    parser.add_argument(
+        "--application",
+        help="Charm name"
+    )
+
+    #############
+    # Placement #
+    #############
+
+    """
+    To deploy to a non-Juju machine, provide the host and
+    credentials for Juju to manually provision (host, username, (password or key?))
+
+    """
+    parser.add_argument(
+        "--proxy",
+        action='store_true',
+        help="Deploy as a proxy charm.",
+    )
+    parser.add_argument(
+        "--no-proxy",
+        action='store_false',
+        dest='proxy',
+        help="Deploy as a full charm.",
+    )
+    parser.set_defaults(proxy=True)
+
+    # Test options?
+    # unit test?
+    #######
+    # VNF #
+    #######
+
+    return parser.parse_args()
+
+
+async def deploy_charm_and_wait():
+    args = get_argparser()
+
+    # Set logging level to debug so we can see verbose output from the
+    # juju library.
+    logging.basicConfig(level=logging.DEBUG)
+
+    # Quiet logging from the websocket library. If you want to see
+    # everything sent over the wire, set this to DEBUG.
+    ws_logger = logging.getLogger('websockets.protocol')
+    ws_logger.setLevel(logging.INFO)
+
+    """Here's an example of a coroutine that will deploy a charm and wait until
+    it's ready to be used."""
+    api = JujuApi(server=args.server,
+                  port=args.port,
+                  user=args.user,
+                  secret=args.password,
+                  # loop=loop,
+                  log=ws_logger,
+                  model_name=args.model
+                  )
+    print("Logging in...")
+    await api.login()
+
+    if api.authenticated:
+        status = await api.get_status()
+        print('Applications:', list(status.applications.keys()))
+        print('Machines:', list(status.machines.keys()))
+
+    if args.directory and args.application:
+
+
+
+        # Deploy the charm
+        charm = os.path.basename(
+            os.path.expanduser(
+                os.path.dirname(args.directory)
+            )
+        )
+        await api.deploy_application(charm,
+                                     name=args.application,
+                                     path=args.directory,
+                                     )
+
+        # Wait for the application to fully deploy. This will block until the
+        # agent is in an idle state, and the charm's workload is either
+        # 'active' or 'unknown', meaning it's ready but the author did not
+        # explicitly set a workload state.
+        print("Waiting for application '{}' to deploy...".format(charm))
+        while (True):
+            # Deploy the charm and wait, periodically checking its status
+            await api.wait_for_application(charm, 30)
+
+            error = await api.is_application_error(charm)
+            if error:
+                print("This application is in an error state.")
+                break
+
+            blocked = await api.is_application_blocked(charm)
+            if blocked:
+                print("This application is blocked.")
+                break
+
+            # An extra check to see if the charm is ready
+            up = await api.is_application_up(charm)
+            print("Application is {}".format("up" if up else "down"))
+
+        print("Service {} is deployed".format(args.application))
+
+        ###################################
+        # Execute config on a proxy charm #
+        ###################################
+        config = await api.get_config(args.application)
+        hostname = config['ssh-username']['value']
+        rhostname = hostname[::-1]
+
+        # Apply the configuration
+        await api.apply_config(
+            {'ssh-username': rhostname}, application=args.application
+        )
+
+        # Get the configuration
+        config = await api.get_config(args.application)
+
+        # Verify the configuration has been updated
+        assert(config['ssh-username']['value'] == rhostname)
+
+        ####################################
+        # Get the status of an application #
+        ####################################
+        status = await api.get_application_status(charm)
+        print("Application Status: {}".format(status))
+
+        ###########################
+        # Execute a simple action #
+        ###########################
+        result = await api.run_action(charm, 'get-ssh-public-key')
+        print("Action {} status is {} and returned {}".format(
+            result['status'],
+            result['action']['tag'],
+            result['action']['results']
+        ))
+
+        #####################################
+        # Execute an action with parameters #
+        #####################################
+        result = await api.run_action(charm, 'run', command='hostname')
+
+        print("Action {} status is {} and returned {}".format(
+            result['status'],
+            result['action']['tag'],
+            result['action']['results']
+        ))
+
+    print("Logging out...")
+    await api.logout()
+    api = None
+
+# get public key in juju controller? that can be pulled without need of a charm deployed and installed to vm via cloud-init
+
+if __name__ == "__main__":
+    # Create a single event loop for running code asyncronously.
+    loop = asyncio.get_event_loop()
+
+    # An initial set of tasks to run
+    tasks = [
+        deploy_charm_and_wait(),
+    ]
+
+    # TODO: optionally run forever and use a Watcher to monitor what's happening
+    loop.run_until_complete(asyncio.wait(tasks))
index 06393e5..9bff8c1 100644 (file)
@@ -14,6 +14,7 @@ from fsbase import FsException
 from msgbase import MsgException
 from os import environ
 import logging
+from vca import DeployApplication, RemoveApplication
 
 #streamformat = "%(asctime)s %(name)s %(levelname)s: %(message)s"
 streamformat = "%(name)s %(levelname)s: %(message)s"
@@ -39,7 +40,6 @@ class Lcm:
         # load configuration
         config = self.read_config_file(config_file)
         self.config = config
-        self.config = config
         self.ro_url = "http://{}:{}/openmano".format(config["RO"]["host"], config["RO"]["port"])
         self.ro_tenant = config["RO"]["tenant"]
         self.vca = config["VCA"]  # TODO VCA
@@ -185,14 +185,27 @@ class Lcm:
                 if vnfd.get("vnf-configuration") and vnfd["vnf-configuration"].get("juju"):
                     db_nsr["config-status"] = "configuring"
                     proxy_charm = vnfd["vnf-configuration"]["juju"]["charm"]
-                    config_primitive = vnfd["vnf-configuration"].get("config-primitive")
-                    # get parameters for juju charm
+
+                    # Note: The charm needs to exist on disk at the location
+                    # specified by charm_path.
                     base_folder = vnfd["_admin"]["storage"]
-                    path = "{}{}/{}/charms".format(base_folder["path"], base_folder["folder"], base_folder["file"],
-                                                   proxy_charm)
-                    mgmt_ip = nsr_lcm['nsr_ip'][vnfd_index]
-                    # TODO launch VCA charm
-                    # task = asyncio.ensure_future(DeployCharm(self.loop, path, mgmt_ip, config_primitive))
+                    charm_path = "{}{}/{}/charms/{}".format(
+                        base_folder["path"],
+                        base_folder["folder"],
+                        base_folder["file"],
+                        proxy_charm
+                    )
+                    task = asyncio.ensure_future(
+                        DeployApplication(
+                            self.config['VCA'],
+                            self.db,
+                            db_nsr,
+                            vnfd,
+                            vnfd_index,
+                            charm_path,
+                        )
+                    )
+
             db_nsr["detailed-status"] = "Done"
             db_nsr["operational-status"] = "running"
             self.db.replace("nsrs", nsr_id, db_nsr)
@@ -217,9 +230,28 @@ class Lcm:
         db_nsr["config-status"] = "terminate"
         db_nsr["detailed-status"] = "Deleting charms"
         self.db.replace("nsrs", nsr_id, db_nsr)
-        # TODO destroy VCA charm
 
+        try:
+            self.logger.debug("Deleting charms")
+            nsd = db_nsr["nsd"]
+            for c_vnf in nsd["constituent-vnfd"]:
+                vnfd_id = c_vnf["vnfd-id-ref"]
+                vnfd_index = str(c_vnf["member-vnf-index"])
+                vnfd = self.db.get_one("vnfds", {"id": vnfd_id})
+                if vnfd.get("vnf-configuration") and vnfd["vnf-configuration"].get("juju"):
+                    asyncio.ensure_future(
+                        RemoveApplication(
+                            self.config['VCA'],
+                            self.db,
+                            db_nsr,
+                            vnfd,
+                            vnfd_index,
+                        )
+                    )
+        except Exception as e:
+            self.logger.debug("Failed while deleting charms: {}".format(e))
         # remove from RO
+
         RO = ROclient.ROClient(self.loop, endpoint_url=self.ro_url, tenant=self.ro_tenant,
                                datacenter=db_nsr["datacenter"])
         # Delete ns
@@ -283,7 +315,6 @@ class Lcm:
                         nsr_id, RO_vnfd_id, e))
             self.db.replace("nsrs", nsr_id, db_nsr)
 
-
         # TODO delete from database or mark as deleted???
         db_nsr["operational-status"] = "terminated"
         self.db.del_one("nsrs", {"_id": nsr_id})
@@ -389,7 +420,6 @@ class Lcm:
             self.logger.critical("At config file '{}': {}".format(config_file, e))
 
 
-
 if __name__ == '__main__':
 
     config_file = "lcm.cfg"
@@ -433,6 +463,3 @@ if __name__ == '__main__':
     # lcm.db.create("ns_request", ns_request)
 
     lcm.start()
-
-
-
diff --git a/lcm/osm_lcm/vca.py b/lcm/osm_lcm/vca.py
new file mode 100644 (file)
index 0000000..b2b2663
--- /dev/null
@@ -0,0 +1,233 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+from juju_api import JujuApi
+from juju.model import ModelObserver
+import logging
+import os
+import os.path
+import re
+
+
+class VCAMonitor(ModelObserver):
+    """Monitor state changes within the Juju Model."""
+    context = None
+
+    async def on_change(self, delta, old, new, model):
+        """React to changes in the Juju model."""
+        status = None
+        db_nsr = self.context['db_nsr']
+        vnf_id = self.context['vnf_id']
+
+        nsr_lcm = db_nsr["_admin"]["deploy"]
+        nsr_id = nsr_lcm["id"]
+        application = self.context['application']
+
+        if delta.entity == "unit":
+            # We only care about changes to a unit
+            if delta.type == "add" and old is None:
+                if new and new.application == application:
+                    status = "BUILD"
+            elif delta.type == "change":
+                if new and new.application == application:
+                    if new.agent_status == "idle":
+                        if new.workload_status in ("active", "blocked"):
+                            status = "ACTIVE"
+
+            elif delta.type == "remove" and new is None:
+                if new and new.application == application:
+                    status = "DELETING"
+
+        if status:
+            nsr_lcm["VCA"][vnf_id]['operational-status'] = status
+
+            # TODO: Clean this up, and make it work with deletes (if we need
+            # TODO: to update the database post-delete)
+            # Figure out if we're finished configuring
+            count = len(nsr_lcm["VCA"])
+            active = 0
+            for vnf_id in nsr_lcm["VCA"]:
+                if nsr_lcm["VCA"][vnf_id]['operational-status'] == "ACTIVE":
+                    active += 1
+            if active == count:
+                db_nsr["config-status"] = "done"
+            else:
+                db_nsr["config-status"] = "configuring {}/{}".format(active, count)
+
+            try:
+                self.context['db'].replace(
+                    "nsrs",
+                    nsr_id,
+                    db_nsr
+                )
+
+                # self.context['db'].replace(
+                #     "nsr_lcm",
+                #     {"id": self.context['nsr_lcm']['id']},
+                #     self.context['nsr_lcm']
+                # )
+            except Exception as e:
+                # I've seen this happen when we handle a delete, because the
+                # db record is gone by the time we've finished deleting
+                # the charms.
+                print("Error updating database: ", e)
+
+    pass
+
+
+def GetJujuApi(config):
+    # Quiet logging from the websocket library. If you want to see
+    # everything sent over the wire, set this to DEBUG.
+    logging.basicConfig(level=logging.DEBUG)
+
+    ws_logger = logging.getLogger('websockets.protocol')
+    ws_logger.setLevel(logging.INFO)
+
+    api = JujuApi(server=config['host'],
+                  port=config['port'],
+                  user=config['user'],
+                  secret=config['secret'],
+                  log=ws_logger,
+                  model_name='default'
+                  )
+    return api
+
+
+def get_vnf_unique_name(nsr_name, vnfr_name, member_vnf_index):
+    """Get the unique VNF name.
+    Charm names accepts only a to z and non-consecutive - characters."""
+    name = "{}-{}-{}".format(nsr_name, vnfr_name, member_vnf_index)
+    new_name = ''
+    for c in name:
+        if c.isdigit():
+            c = chr(97 + int(c))
+        elif not c.isalpha():
+            c = "-"
+        new_name += c
+    return re.sub('\-+', '-', new_name.lower())
+
+
+def get_initial_config(initial_config_primitive, mgmt_ip):
+    config = {}
+    for primitive in initial_config_primitive:
+        if primitive['name'] == 'config':
+            for parameter in primitive['parameter']:
+                param = parameter['name']
+                if parameter['value'] == "<rw_mgmt_ip>":
+                    config[param] = mgmt_ip
+                else:
+                    config[param] = parameter['value']
+    return config
+
+
+async def DeployApplication(vcaconfig, db, db_nsr, vnfd,
+                            vnfd_index, charm_path):
+    """
+    Deploy a charm.
+
+    Deploy a VNF configuration charm from a local directory.
+    :param dict vcaconfig: The VCA portion of the LCM Configuration
+    :param object vnfd: The VNF descriptor
+    ...
+    :param int vnfd_index: The index of the vnf.
+
+    :Example:
+
+    DeployApplication(...)
+    """
+    nsr_lcm = db_nsr["_admin"]["deploy"]
+    nsr_id = nsr_lcm["id"]
+    vnf_id = vnfd['id']
+
+    if "proxy" in vnfd["vnf-configuration"]["juju"]:
+        use_proxy = vnfd["vnf-configuration"]["juju"]["proxy"]
+    else:
+        # TBD: We need this to handle a full charm
+        use_proxy = True
+
+    application = get_vnf_unique_name(
+        db_nsr["name"].lower().strip(),
+        vnfd['id'],
+        vnfd_index,
+    )
+
+    api = GetJujuApi(vcaconfig)
+
+    await api.login()
+    if api.authenticated:
+        charm = os.path.basename(charm_path)
+
+        # Set the INIT state; further operational status updates
+        # will be made by the VCAMonitor
+        nsr_lcm["VCA"][vnf_id] = {}
+        nsr_lcm["VCA"][vnf_id]['operational-status'] = 'INIT'
+        nsr_lcm["VCA"][vnf_id]['application'] = application
+
+        db.replace("nsrs", nsr_id, db_nsr)
+
+        model = await api.get_model()
+        context = {
+            'application': application,
+            'vnf_id': vnf_id,
+            'db_nsr': db_nsr,
+            'db': db,
+        }
+        mon = VCAMonitor()
+        mon.context = context
+        model.add_observer(mon)
+
+        await api.deploy_application(charm,
+                                     name=application,
+                                     path=charm_path,
+                                     )
+
+        # Get and apply the initial config primitive
+        cfg = get_initial_config(
+            vnfd["vnf-configuration"].get(
+                "initial-config-primitive"
+            ),
+            nsr_lcm['nsr_ip'][vnfd_index]
+        )
+
+        await api.apply_config(cfg, application)
+
+    await api.logout()
+
+
+async def RemoveApplication(vcaconfig, db, db_nsr, vnfd, vnfd_index):
+    """
+    Remove an application from the Juju Controller
+
+    Removed the named application and it's charm from the Juju controller.
+
+    :param object loop: The event loop.
+    :param str application_name: The unique name of the application.
+
+    :Example:
+
+    RemoveApplication(loop, "ping_vnf")
+    RemoveApplication(loop, "pong_vnf")
+    """
+    nsr_lcm = db_nsr["_admin"]["deploy"]
+    vnf_id = vnfd['id']
+    application = nsr_lcm["VCA"][vnf_id]['application']
+
+    api = GetJujuApi(vcaconfig)
+
+    await api.login()
+    if api.authenticated:
+        model = await api.get_model()
+        context = {
+            'application': application,
+            'vnf_id': vnf_id,
+            'db_nsr': db_nsr,
+            'db': db,
+        }
+
+        mon = VCAMonitor()
+        mon.context = context
+        model.add_observer(mon)
+
+        print("VCA: Removing application {}".format(application))
+        await api.remove_application(application)
+    await api.logout()