Add juju support to lcm
[osm/RO.git] / lcm / osm_lcm / vca.py
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()