update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b second try
[osm/SO.git] / rwprojectmano / plugins / rwprojectmano / rift / tasklets / rwprojectmano / projectmano.py
diff --git a/rwprojectmano/plugins/rwprojectmano/rift/tasklets/rwprojectmano/projectmano.py b/rwprojectmano/plugins/rwprojectmano/rift/tasklets/rwprojectmano/projectmano.py
new file mode 100644 (file)
index 0000000..6692341
--- /dev/null
@@ -0,0 +1,398 @@
+#
+#   Copyright 2017 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.
+#
+
+"""
+Project Manager tasklet is responsible for managing the Projects
+configurations required for Role Based Access Control feature.
+"""
+
+import asyncio
+import gi
+
+gi.require_version('RwDts', '1.0')
+gi.require_version('RwProjectManoYang', '1.0')
+from gi.repository import (
+    RwDts as rwdts,
+    ProtobufC,
+    RwTypes,
+    RwProjectManoYang,
+)
+gi.require_version('RwKeyspec', '1.0')
+from gi.repository.RwKeyspec import quoted_key
+
+import rift.tasklets
+from rift.mano.utils.project import (
+    NS_PROJECT,
+    get_add_delete_update_cfgs,
+    ProjectConfigCallbacks,
+)
+
+
+MANO_PROJECT_ROLES = [
+    { 'mano-role':"rw-project-mano:catalog-oper",
+      'description':("The catalog-oper Role has read permission to nsd-catalog "
+                     "and vnfd-catalog under specific Projects, "
+                     "as identified by /rw-project:project/rw-project:name.  The "
+                     "catatlog-oper Role may also have execute permission to specific "
+                     "non-mutating RPCs.  This Role is intended for read-only access to "
+                     "catalogs under a specific project.") },
+
+    { 'mano-role':"rw-project-mano:catalog-admin",
+      'description':("The catalog-admin Role has full CRUDX permissions to vnfd and nsd "
+                     "catalogs under specific Projects, as identified by "
+                     "/rw-project:project/rw-project:name.") },
+
+    { 'mano-role':"rw-project-mano:lcm-oper",
+      'description':("The lcm-oper Role has read permission to the VL, VNF and NS "
+                     "records within a Project.  The lcm-oper Role may also have "
+                     "execute permission to specific non-mutating RPCs.") },
+
+    { 'mano-role':"rw-project-mano:lcm-admin",
+      'description':("The lcm-admin Role has full CRUDX permissions to the VL, VNF "
+                     "and NS records within a Project.  The lcm-admin Role does "
+                     "not provide general CRUDX permissions to the Project as a whole, "
+                     "nor to the RIFT.ware platform in general.") },
+
+    { 'mano-role':"rw-project-mano:account-admin",
+      'description':("The account-admin Role has full CRUDX permissions to the VIM, SDN, VCA "
+                     "and RO accounts within a Project.  The account-admin Role does "
+                     "not provide general CRUDX permissions to the Project as a whole, "
+                     "nor to the RIFT.ware platform in general.") },
+    
+    { 'mano-role':"rw-project-mano:account-oper",
+      'description':("The account-oper Role has read permission to the VIM, SDN, VCA "
+                     "and RO accounts within a Project.  The account-oper Role may also have "
+                     "execute permission to specific non-mutating RPCs.") },
+]
+
+
+class ProjectDtsHandler(object):
+    XPATH = "C,/{}".format(NS_PROJECT)
+
+    def __init__(self, dts, log, callbacks):
+        self._dts = dts
+        self._log = log
+        self._callbacks = callbacks
+
+        self.reg = None
+        self.projects = []
+
+    @property
+    def log(self):
+        return self._log
+
+    @property
+    def dts(self):
+        return self._dts
+
+    def get_reg_flags(self):
+        return (rwdts.Flag.SUBSCRIBER  | 
+                rwdts.Flag.DELTA_READY | 
+                rwdts.Flag.CACHE       | 
+                rwdts.Flag.DATASTORE)
+
+    def add_project(self, cfg):
+        name = cfg.name
+        self._log.info("Adding project: {}".format(name))
+
+        if name not in self.projects:
+            self._callbacks.on_add_apply(name, cfg)
+            self.projects.append(name)
+        else:
+            self._log.error("Project already present: {}".
+                           format(name))
+
+    def delete_project(self, name):
+        self._log.info("Deleting project: {}".format(name))
+        if name in self.projects:
+            self._callbacks.on_delete_apply(name)
+            self.projects.remove(name)
+        else:
+            self._log.error("Unrecognized project: {}".
+                           format(name))
+
+    def update_project(self, cfg):
+        """ Update an existing project
+
+        Currently, we do not take any action on MANO for this,
+        so no callbacks are defined
+
+        Arguments:
+            msg - The project config message
+        """
+        name = cfg.name
+        self._log.info("Updating project: {}".format(name))
+        if name in self.projects:
+            pass
+        else:
+            self._log.error("Unrecognized project: {}".
+                           format(name))
+
+    def register(self):
+        @asyncio.coroutine
+        def apply_config(dts, acg, xact, action, scratch):
+            self._log.debug("Got project apply config (xact: %s) (action: %s)", xact, action)
+
+            if xact.xact is None:
+                if action == rwdts.AppconfAction.INSTALL:
+                    curr_cfg = self._reg.elements
+                    for cfg in curr_cfg:
+                        self._log.info("Project {} being re-added after restart.".
+                                       format(cfg.name))
+                        self.add_project(cfg)
+                else:
+                    self._log.debug("No xact handle.  Skipping apply config")
+
+                return
+
+            add_cfgs, delete_cfgs, update_cfgs = get_add_delete_update_cfgs(
+                    dts_member_reg=self._reg,
+                    xact=xact,
+                    key_name="name",
+                    )
+
+            # Handle Deletes
+            for cfg in delete_cfgs:
+                self.delete_project(cfg.name)
+
+            # Handle Adds
+            for cfg in add_cfgs:
+                self.add_project(cfg)
+
+            # Handle Updates
+            for cfg in update_cfgs:
+                self.update_project(cfg)
+
+            return RwTypes.RwStatus.SUCCESS
+
+        @asyncio.coroutine
+        def on_prepare(dts, acg, xact, xact_info, ks_path, msg, scratch):
+            """ Prepare callback from DTS for Project """
+
+            action = xact_info.query_action
+            name = msg.name
+
+            self._log.debug("Project %s on_prepare config received (action: %s): %s",
+                            name, xact_info.query_action, msg)
+
+            if action in [rwdts.QueryAction.CREATE, rwdts.QueryAction.UPDATE]:
+                if name in self.projects:
+                    self._log.debug("Project {} already exists. Ignore request".
+                                    format(name))
+
+                else:
+                    self._log.debug("Project {}: Invoking on_prepare add request".
+                                    format(name))
+                    rc, err_msg = yield from self._callbacks.on_add_prepare(name, msg)
+                    if rc is False:
+                        xact_info.send_error_xpath(RwTypes.RwStatus.FAILURE,
+                                                   ProjectDtsHandler.XPATH,
+                                                   err_msg)
+                        xact_info.respond_xpath(rwdts.XactRspCode.NACK)
+                        return
+
+            elif action == rwdts.QueryAction.DELETE:
+                # Check if the entire project got deleted
+                fref = ProtobufC.FieldReference.alloc()
+                fref.goto_whole_message(msg.to_pbcm())
+                if fref.is_field_deleted():
+                    if name in self.projects:
+                        rc, delete_msg = yield from self._callbacks.on_delete_prepare(name)
+                        if not rc:
+                            self._log.error("Project {} should not be deleted. Reason : {}".
+                                            format(name, delete_msg))
+
+                            xact_info.send_error_xpath(RwTypes.RwStatus.FAILURE,
+                                           ProjectDtsHandler.XPATH,
+                                           delete_msg)
+                            
+                            xact_info.respond_xpath(rwdts.XactRspCode.NACK)
+                            return
+                    else:
+                        self._log.warning("Delete on unknown project: {}".
+                                          format(name))
+
+            else:
+                self._log.error("Action (%s) NOT SUPPORTED", action)
+                xact_info.respond_xpath(rwdts.XactRspCode.NA)
+                return
+
+            xact_info.respond_xpath(rwdts.XactRspCode.ACK)
+
+        self._log.debug("Registering for project config using xpath: %s",
+                        ProjectDtsHandler.XPATH)
+
+        acg_handler = rift.tasklets.AppConfGroup.Handler(
+            on_apply=apply_config,
+        )
+
+        with self._dts.appconf_group_create(acg_handler) as acg:
+            self._reg = acg.register(
+                xpath=ProjectDtsHandler.XPATH,
+                flags=self.get_reg_flags(),
+                on_prepare=on_prepare,
+            )
+
+
+class ProjectHandler(object):
+    def __init__(self, tasklet, project_class):
+        self._tasklet = tasklet
+        self._log = tasklet.log
+        self._log_hdl = tasklet.log_hdl
+        self._dts = tasklet.dts
+        self._loop = tasklet.loop
+        self._class = project_class
+
+        self.mano_roles = [role['mano-role'] for role in MANO_PROJECT_ROLES]
+
+        self._log.debug("Creating project config handler")
+        self.project_cfg_handler = ProjectDtsHandler(
+            self._dts, self._log,
+            ProjectConfigCallbacks(
+                on_add_apply=self.on_project_added,
+                on_add_prepare=self.on_add_prepare,
+                on_delete_apply=self.on_project_deleted,
+                on_delete_prepare=self.on_delete_prepare,
+            )
+        )
+
+    def _get_tasklet_name(self):
+        return self._tasklet.tasklet_info.instance_name
+
+    def _get_project(self, name):
+        try:
+            proj = self._tasklet.projects[name]
+        except Exception as e:
+            self._log.exception("Project {} ({})not found for tasklet {}: {}".
+                                format(name, list(self._tasklet.projects.keys()),
+                                       self._get_tasklet_name(), e))
+            raise e
+
+        return proj
+
+    def on_project_deleted(self, name):
+        self._log.debug("Project {} deleted".format(name))
+        try:
+            self._get_project(name).deregister()
+        except Exception as e:
+            self._log.exception("Project {} deregister for {} failed: {}".
+                                format(name, self._get_tasklet_name(), e))
+
+        try:
+            proj = self._tasklet.projects.pop(name)
+            del proj
+        except Exception as e:
+            self._log.exception("Project {} delete for {} failed: {}".
+                                format(name, self._get_tasklet_name(), e))
+
+    def on_project_added(self, name, cfg):
+        if name not in self._tasklet.projects:
+            try:
+                self._tasklet.projects[name] = \
+                                self._class(name, self._tasklet)
+                self._loop.create_task(self._get_project(name).register())
+
+            except Exception as e:
+                self._log.exception("Project {} create for {} failed: {}".
+                                    format(name, self._get_tasklet_name(), e))
+                raise e
+
+        self._log.debug("Project {} added to tasklet {}".
+                        format(name, self._get_tasklet_name()))
+        self._get_project(name)._apply = True
+
+    @asyncio.coroutine
+    def on_add_prepare(self, name, msg):
+        self._log.debug("Project {} to be added to {}".
+                        format(name, self._get_tasklet_name()))
+
+        if name in self._tasklet.projects:
+            err_msg = ("Project already exists: {}".
+                       format(name))
+            self._log.error(err_msg)
+            return False, err_msg
+
+        # Validate mano-roles, if present
+        try:
+            cfg = msg.project_config
+            users = cfg.user
+            for user in users:
+                for role in user.mano_role:
+                    if role.role not in self.mano_roles:
+                        err_msg = ("Invalid role {} for user {} in project {}".
+                               format(role.role, user.user_name, name))
+                        self._log.error(err_msg)
+                        return False, err_msg
+
+        except AttributeError as e:
+            # If the user or mano role is not present, ignore
+            self._log.debug("Project {}: {}".format(name, e))
+
+        return True, ""
+
+    @asyncio.coroutine
+    def on_delete_prepare(self, name):
+        self._log.error("Project {} being deleted for tasklet {}".
+                        format(name, self._get_tasklet_name()))
+        rc, delete_msg = yield from self._get_project(name).delete_prepare()
+        return (rc, delete_msg)
+
+    def register(self):
+        self.project_cfg_handler.register()
+
+
+class ProjectStateRolePublisher(rift.tasklets.DtsConfigPublisher):
+
+    def __init__(self, tasklet):
+        super().__init__(tasklet)
+        self.proj_state = RwProjectManoYang.YangData_RwProject_Project_ProjectState()
+        self.projects = set()
+        self.roles = MANO_PROJECT_ROLES
+
+    def get_xpath(self):
+        return "D,/rw-project:project/rw-project:project-state/rw-project-mano:mano-role"
+
+    def get_reg_flags(self):
+        return super().get_reg_flags() | rwdts.Flag.DATASTORE
+
+    def role_xpath(self, project, role):
+        return "/rw-project:project[rw-project:name={}]".format(quoted_key(project)) + \
+            "/rw-project:project-state/rw-project-mano:mano-role" + \
+            "[rw-project-mano:role={}]".format(quoted_key(role['mano-role']))
+
+    def pb_role(self, role):
+        pbRole = self.proj_state.create_mano_role()
+        pbRole.role = role['mano-role']
+        pbRole.description = role['description']
+        return pbRole
+
+    def publish_roles(self, project):
+        if not project in self.projects:
+            self.projects.add(project)
+            for role in self.roles:
+                xpath = self.role_xpath(project, role)
+                pb_role = self.pb_role(role)
+                self.log.debug("publishing xpath:{}".format(xpath))
+                self._regh.update_element(xpath, pb_role)
+
+    def unpublish_roles(self, project):
+        if project in self.projects:
+            self.projects.remove(project)
+            for role in self.roles:
+                xpath = self.role_xpath(project, role)
+                self.log.debug("unpublishing xpath:{}".format(xpath))
+                self._regh.delete_element(xpath)
+