From: Philip Joseph Date: Wed, 29 Mar 2017 11:41:32 +0000 (+0530) Subject: Add MANO project role support X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=9bbec9ddf679ddf9bfd141211c7b4e8cb925483a;p=osm%2FSO.git Add MANO project role support Signed-off-by: Philip Joseph --- diff --git a/BUILD.sh b/BUILD.sh index 49e515b9..94c8e215 100755 --- a/BUILD.sh +++ b/BUILD.sh @@ -114,10 +114,10 @@ fi if [[ $PLATFORM == ub16 ]]; then PLATFORM_REPOSITORY=${1:-OSM} - PLATFORM_VERSION=${2:-4.99.1.1.58423} + PLATFORM_VERSION=${2:-4.99.1.1.58887} elif [[ $PLATFORM == fc20 ]]; then PLATFORM_REPOSITORY=${1:-OSM} # change to OSM when published - PLATFORM_VERSION=${2:-4.99.1.1.58423} + PLATFORM_VERSION=${2:-4.99.1.1.58887} else echo "Internal error: unknown platform $PLATFORM" exit 1 diff --git a/CMakeLists.txt b/CMakeLists.txt index ae622a3e..ddc62d9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ set(subdirs rwmon rwcm rwlaunchpad + rwprojectmano ) if (NOT RIFT_AGENT_BUILD STREQUAL "XML_ONLY") diff --git a/common/python/rift/mano/utils/project.py b/common/python/rift/mano/utils/project.py index de495e9a..2609519a 100644 --- a/common/python/rift/mano/utils/project.py +++ b/common/python/rift/mano/utils/project.py @@ -398,11 +398,18 @@ class ProjectConfigCallbacks(object): class ProjectDtsHandler(object): XPATH = "C,{}/project-config".format(XPATH) - def __init__(self, dts, log, callbacks): + def __init__(self, dts, log, callbacks, sub_config=True): self._dts = dts self._log = log self._callbacks = callbacks + if sub_config: + self.xpath = ProjectDtsHandler.XPATH + self._key = 'name_ref' + else: + self.xpath = "C,{}".format(XPATH) + self._key = 'name' + self.reg = None self.projects = [] @@ -446,20 +453,29 @@ class ProjectDtsHandler(object): if name in self.projects: pass else: - self.log.error("Unrecognized project: {}". - format(name)) + self.add_project(name) def register(self): + def on_init(acg, xact, scratch): + self._log.debug("on_init") + scratch["projects"] = { + "added": [], + "deleted": [], + "updated": [], + } + return scratch + @asyncio.coroutine def apply_config(dts, acg, xact, action, scratch): - self._log.debug("Got project apply config (xact: %s) (action: %s)", xact, action) + self._log.debug("Got project apply config (xact: %s) (action: %s): %s", + xact, action, scratch) if xact.xact is None: if action == rwdts.AppconfAction.INSTALL: curr_cfg = self._reg.elements for cfg in curr_cfg: self._log.debug("Project being re-added after restart.") - self.add_project(cfg) + self.add_project(cfg.name_ref) else: # When RIFT first comes up, an INSTALL is called with the current config # Since confd doesn't actally persist data this never has any data so @@ -468,23 +484,33 @@ class ProjectDtsHandler(object): return - add_cfgs, delete_cfgs, update_cfgs = get_add_delete_update_cfgs( - dts_member_reg=self._reg, - xact=xact, - key_name="name_ref", - ) + try: + add_cfgs = scratch["projects"]["added"] + except KeyError: + add_cfgs = [] + + try: + del_cfgs = scratch["projects"]["deleted"] + except KeyError: + del_cfgs = [] + + try: + update_cfgs = scratch["projects"]["updated"] + except KeyError: + update_cfgs = [] + # Handle Deletes - for cfg in delete_cfgs: - self.delete_project(cfg.name_ref) + for name in del_cfgs: + self.delete_project(name) # Handle Adds - for cfg in add_cfgs: - self.add_project(cfg.name_ref) + for name, msg in add_cfgs: + self.add_project(name) # Handle Updates - for cfg in update_cfgs: - self.update_project(cfg.name_ref) + for name, msg in update_cfgs: + self.update_project(name) return RwTypes.RwStatus.SUCCESS @@ -493,38 +519,29 @@ class ProjectDtsHandler(object): """ Prepare callback from DTS for Project """ action = xact_info.query_action - # xpath = ks_path.to_xpath(RwProjectYang.get_schema()) - # name = ManoProject.from_xpath(xpath, self._log) - # if not name: - # self._log.error("Did not find the project name in ks: {}". - # format(xpath)) - # xact_info.respond_xpath(rwdts.XactRspCode.NACK) - # return - - # if name != msg.name_ref: - # self._log.error("The project name {} did not match the name {} in config". - # format(name, msg.name_ref)) - # projects = scratch.setdefault('projects', { - # 'create': [], - # 'update': [], - # 'delete': [], - # }) - - # self._log.error("prepare msg type {}".format(type(msg))) - name = msg.name_ref + xpath = ks_path.to_xpath(RwProjectManoYang.get_schema()) + self._log.debug("Project xpath: {}".format(xpath)) + name = ManoProject.from_xpath(xpath, self._log) 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 action == rwdts.QueryAction.CREATE: if name in self.projects: self._log.debug("Project {} already exists. Ignore request". format(name)) + else: + yield from self._callbacks.on_add_prepare(name) + scratch["projects"]["added"].append((name, msg)) + elif action == rwdts.QueryAction.UPDATE: + if name in self.projects: + scratch["projects"]["updated"].append(name, msg) else: self._log.debug("Project {}: Invoking on_prepare add request". format(name)) yield from self._callbacks.on_add_prepare(name) + scratch["projects"]["added"].append((name, msg)) elif action == rwdts.QueryAction.DELETE: @@ -538,6 +555,9 @@ class ProjectDtsHandler(object): self._log.error("Project {} should not be deleted". format(name)) xact_info.respond_xpath(rwdts.XactRspCode.NACK) + return + + scratch["projects"]["deleted"].append(name) else: self._log.warning("Delete on unknown project: {}". format(name)) @@ -555,7 +575,7 @@ class ProjectDtsHandler(object): acg_handler = rift.tasklets.AppConfGroup.Handler( on_apply=apply_config, - ) + on_init=on_init) with self._dts.appconf_group_create(acg_handler) as acg: self._reg = acg.register( @@ -564,6 +584,7 @@ class ProjectDtsHandler(object): on_prepare=on_prepare, ) + class ProjectHandler(object): def __init__(self, tasklet, project_class, **kw): self._tasklet = tasklet @@ -629,7 +650,7 @@ class ProjectHandler(object): self._class(name, self._tasklet, **(self._kw)) except Exception as e: self._log.exception("Project {} create for {} failed: {}". - formatname, self._get_tasklet_name(), e()) + format(name, self._get_tasklet_name(), e)) try: yield from self._get_project(name).register() diff --git a/models/plugins/yang/CMakeLists.txt b/models/plugins/yang/CMakeLists.txt index f31fa3c2..998ecb2d 100644 --- a/models/plugins/yang/CMakeLists.txt +++ b/models/plugins/yang/CMakeLists.txt @@ -37,16 +37,6 @@ set(source_yang_files vnffgd.yang ) -rift_add_yang_target( - TARGET rwprojectmano_yang - YANG_FILES - rw-project-mano.yang - GIR_PATHS ${CMAKE_CURRENT_BINARY_DIR} - COMPONENT ${PKG_LONG_NAME} - LIBRARIES - rw_project_yang_gen - ) - rift_add_yang_target( TARGET mano-types_yang YANG_FILES diff --git a/models/plugins/yang/rw-project-mano.yang b/models/plugins/yang/rw-project-mano.yang deleted file mode 100644 index 8cf09248..00000000 --- a/models/plugins/yang/rw-project-mano.yang +++ /dev/null @@ -1,47 +0,0 @@ -/* - * - * 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. - * - */ - -module rw-project-mano -{ - namespace "http://riftio.com/ns/riftware-1.0/rw-project-mano"; - prefix "rw-project-mano"; - - import rw-pb-ext { - prefix "rw-pb-ext"; - } - - import rw-project { - prefix "rw-project"; - } - - revision 2017-03-08 { - description - "Initial revision. This YANG file defines the - MANO extentions for project based tenancy"; - reference - "Derived from earlier versions of base YANG files"; - } - - augment /rw-project:project/rw-project:project-config { - leaf name-ref { - type leafref { - path "../../rw-project:name"; - } - } - } -} diff --git a/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsmtasklet.py b/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsmtasklet.py index 804439fc..e739684d 100755 --- a/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsmtasklet.py +++ b/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsmtasklet.py @@ -1086,7 +1086,7 @@ class VirtualNetworkFunctionRecord(object): for vnfd_cp in vlr.vld_msg.vnfd_connection_point_ref: if (vnfd_cp.vnfd_id_ref == self._vnfd.id and vnfd_cp.vnfd_connection_point_ref == conn.name and - vnfd_cp.member_vnf_index_ref == str(self.member_vnf_index) and + vnfd_cp.member_vnf_index_ref == self.member_vnf_index and vlr.cloud_account_name == self.cloud_account_name): self._log.debug("Found VLR for cp_name:%s and vnf-index:%d", conn.name, self.member_vnf_index) diff --git a/rwlaunchpad/plugins/rwvnfm/rift/tasklets/rwvnfmtasklet/rwvnfmtasklet.py b/rwlaunchpad/plugins/rwvnfm/rift/tasklets/rwvnfmtasklet/rwvnfmtasklet.py index 955f566d..b198dd8f 100755 --- a/rwlaunchpad/plugins/rwvnfm/rift/tasklets/rwvnfmtasklet/rwvnfmtasklet.py +++ b/rwlaunchpad/plugins/rwvnfm/rift/tasklets/rwvnfmtasklet/rwvnfmtasklet.py @@ -2025,8 +2025,9 @@ class VnfdDtsHandler(object): @asyncio.coroutine def on_prepare(dts, acg, xact, xact_info, ks_path, msg, scratch): """ on prepare callback """ - self._log.debug("Got on prepare for VNFD (path: %s) (action: %s)", - ks_path.to_xpath(RwVnfmYang.get_schema()), msg) + self._log.debug("Got on prepare for VNFD (path: %s) (action: %s) (msg: %s)", + ks_path.to_xpath(RwVnfmYang.get_schema()), + xact_info.query_action, msg) fref = ProtobufC.FieldReference.alloc() fref.goto_whole_message(msg.to_pbcm()) diff --git a/rwlaunchpad/test/launchpad.py b/rwlaunchpad/test/launchpad.py index c299311b..b73c5000 100755 --- a/rwlaunchpad/test/launchpad.py +++ b/rwlaunchpad/test/launchpad.py @@ -370,6 +370,34 @@ class ConfigManagerTasklet(rift.vcs.core.Tasklet): plugin_directory = ClassProperty('./usr/lib/rift/plugins/rwconmantasklet') plugin_name = ClassProperty('rwconmantasklet') + +class ProjectMgrManoTasklet(rift.vcs.core.Tasklet): + """ + This class represents a Resource Manager tasklet. + """ + + def __init__(self, name='Project-Manager-Mano', uid=None, + config_ready=True, + recovery_action=core.RecoveryType.FAILCRITICAL.value, + data_storetype=core.DataStore.NOSTORE.value, + ): + """ + Creates a ProjectMgrManoTasklet object. + + Arguments: + name - the name of the tasklet + uid - a unique identifier + """ + super(ProjectMgrManoTasklet, self).__init__(name=name, uid=uid, + config_ready=config_ready, + recovery_action=recovery_action, + data_storetype=data_storetype, + ) + + plugin_directory = ClassProperty('./usr/lib/rift/plugins/rwprojectmano') + plugin_name = ClassProperty('rwprojectmano') + + class PackageManagerTasklet(rift.vcs.core.Tasklet): """ This class represents a Resource Manager tasklet. @@ -427,6 +455,7 @@ class Demo(rift.vcs.demo.Demo): rift.vcs.uAgentTasklet(), rift.vcs.IdentityManagerTasklet(), rift.vcs.ProjectManagerTasklet(), + ProjectMgrManoTasklet(), rift.vcs.Launchpad(), ] @@ -455,6 +484,7 @@ class Demo(rift.vcs.demo.Demo): AutoscalerTasklet(recovery_action=core.RecoveryType.RESTART.value, data_storetype=datastore), PackageManagerTasklet(recovery_action=core.RecoveryType.RESTART.value, data_storetype=datastore), StagingManagerTasklet(recovery_action=core.RecoveryType.RESTART.value, data_storetype=datastore), + ProjectMgrManoTasklet(recovery_action=core.RecoveryType.RESTART.value, data_storetype=datastore), ] if not mgmt_ip_list or len(mgmt_ip_list) == 0: diff --git a/rwprojectmano/CMakeLists.txt b/rwprojectmano/CMakeLists.txt new file mode 100644 index 00000000..78e225fc --- /dev/null +++ b/rwprojectmano/CMakeLists.txt @@ -0,0 +1,28 @@ +# +# 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. +# +# + +cmake_minimum_required(VERSION 2.8) + +set(PKG_NAME rwprojectmano) +set(PKG_VERSION 1.0) +set(PKG_RELEASE 1) +set(PKG_LONG_NAME ${PKG_NAME}-${PKG_VERSION}) + +set(subdirs + plugins + ) +rift_add_subdirs(SUBDIR_LIST ${subdirs}) diff --git a/rwprojectmano/plugins/CMakeLists.txt b/rwprojectmano/plugins/CMakeLists.txt new file mode 100644 index 00000000..21c8e949 --- /dev/null +++ b/rwprojectmano/plugins/CMakeLists.txt @@ -0,0 +1,24 @@ +# +# 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. +# +# + +cmake_minimum_required(VERSION 2.8) + +set(subdirs + yang + rwprojectmano + ) +rift_add_subdirs(SUBDIR_LIST ${subdirs}) diff --git a/rwprojectmano/plugins/rwprojectmano/CMakeLists.txt b/rwprojectmano/plugins/rwprojectmano/CMakeLists.txt new file mode 100644 index 00000000..16b4b766 --- /dev/null +++ b/rwprojectmano/plugins/rwprojectmano/CMakeLists.txt @@ -0,0 +1,40 @@ +# +# 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. +# + +cmake_minimum_required(VERSION 2.8) + +include(rift_plugin) + +set(TASKLET_NAME rwprojectmano) + +## +# This function creates an install target for the plugin artifacts +## +rift_install_python_plugin( + ${TASKLET_NAME} ${TASKLET_NAME}.py) + +# Workaround RIFT-6485 - rpmbuild defaults to python2 for +# anything not in a site-packages directory so we have to +# install the plugin implementation in site-packages and then +# import it from the actual plugin. +rift_python_install_tree( + FILES + rift/tasklets/${TASKLET_NAME}/__init__.py + rift/tasklets/${TASKLET_NAME}/tasklet.py + rift/tasklets/${TASKLET_NAME}/projectmano.py + rift/tasklets/${TASKLET_NAME}/rolesmano.py + COMPONENT ${PKG_LONG_NAME} + PYTHON3_ONLY) diff --git a/rwprojectmano/plugins/rwprojectmano/rift/tasklets/rwprojectmano/__init__.py b/rwprojectmano/plugins/rwprojectmano/rift/tasklets/rwprojectmano/__init__.py new file mode 100644 index 00000000..24d7753a --- /dev/null +++ b/rwprojectmano/plugins/rwprojectmano/rift/tasklets/rwprojectmano/__init__.py @@ -0,0 +1 @@ +from .tasklet import ProjectMgrManoTasklet diff --git a/rwprojectmano/plugins/rwprojectmano/rift/tasklets/rwprojectmano/projectmano.py b/rwprojectmano/plugins/rwprojectmano/rift/tasklets/rwprojectmano/projectmano.py new file mode 100644 index 00000000..406c0a6e --- /dev/null +++ b/rwprojectmano/plugins/rwprojectmano/rift/tasklets/rwprojectmano/projectmano.py @@ -0,0 +1,278 @@ +# +# 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') +from gi.repository import ( + RwDts as rwdts, + ProtobufC, + RwTypes, +) + +import rift.tasklets +from rift.mano.utils.project import ( + NS_PROJECT, + get_add_delete_update_cfgs, + ProjectConfigCallbacks, +) + + +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 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.debug("Project being re-added after restart.") + self.add_project(cfg.name) + else: + # When RIFT first comes up, an INSTALL is called with the current config + # Since confd doesn't actally persist data this never has any data so + # skip this for now. + 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)) + yield from self._callbacks.on_add_prepare(name, msg) + + + 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 = yield from self._callbacks.on_delete_prepare(name) + if not rc: + self._log.error("Project {} should not be deleted". + format(name)) + 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.NACK) + 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=rwdts.Flag.SUBSCRIBER | rwdts.Flag.DELTA_READY | rwdts.Flag.CACHE, + 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._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): + 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())) + + try: + self._tasklet.projects[name] = \ + self._class(name, self._tasklet) + except Exception as e: + self._log.exception("Project {} create for {} failed: {}". + format(name, self._get_tasklet_name(), e)) + + try: + yield from self._get_project(name).register() + except Exception as e: + self._log.exception("Project {} register for tasklet {} failed: {}". + format(name, self._get_tasklet_name(), e)) + + self._log.debug("Project {} added to {}". + format(name, self._get_tasklet_name())) + + @asyncio.coroutine + def on_delete_prepare(self, name): + self._log.debug("Project {} being deleted for tasklet {}". + format(name, self._get_tasklet_name())) + rc = yield from self._get_project(name).delete_prepare() + return rc + + def register(self): + self.project_cfg_handler.register() diff --git a/rwprojectmano/plugins/rwprojectmano/rift/tasklets/rwprojectmano/rolesmano.py b/rwprojectmano/plugins/rwprojectmano/rift/tasklets/rwprojectmano/rolesmano.py new file mode 100644 index 00000000..ea3674a4 --- /dev/null +++ b/rwprojectmano/plugins/rwprojectmano/rift/tasklets/rwprojectmano/rolesmano.py @@ -0,0 +1,375 @@ +# +# 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 +from enum import Enum + +import gi +gi.require_version('RwDts', '1.0') +gi.require_version('RwRbacInternalYang', '1.0') +from gi.repository import ( + RwDts as rwdts, + ProtobufC, + RwTypes, + RwRbacInternalYang, +) + +import rift.tasklets +#TODO: Fix once merged to latest platform +from rift.tasklets.rwproject.project import ( + StateMachine, + User, + UserState, + RoleKeys, + RoleKeysUsers, +) +from rift.mano.utils.project import ( + NS_PROJECT, + get_add_delete_update_cfgs, +) + + +MANO_PROJECT_ROLES = [ + 'rw-project-mano:mano-oper', + 'rw-project-mano:mano-admin', +] + + +class ProjectConfigSubscriber(object): + """Config subscriber for rw-user config""" + + def __init__(self, project): + self.project_name = project.name + self._log = project.log + self._dts = project.dts + + self.users = {} + self.pub = RoleConfigPublisher(project) + + def get_xpath(self): + return "C,/{}[name='{}']/project-config/user".format(NS_PROJECT, self.project_name) + + + def role_inst(self, role, keys=None): + if not keys: + keys = self.project_name + + r = RoleKeys() + r.role = role.role + r.keys = keys + return r + + def delete_user(self, cfg): + user = User().pb(cfg) + if user.key in self.users: + roles = self.users[user.key] + for role_key in list(roles): + self.delete_role(user, role_key) + self.users.pop(user.key) + + def update_user(self, cfg): + user = User().pb(cfg) + cfg_roles = {} + for cfg_role in cfg.mano_role: + r = self.role_inst(cfg_role) + cfg_roles[r.key] = r + + if not user.key in self.users: + self.users[user.key] = set() + else: + #Check if any roles are deleted for the user + for role_key in list(self.users[user.key]): + if role_key not in cfg_roles: + self.delete_role(user, role_key) + + for role_key in cfg_roles.keys(): + if role_key not in self.users[user.key]: + self.update_role(user, cfg_roles[role_key]) + + def delete_role(self, user, role_key): + user_key = user.key + + try: + roles = self.users[user_key] + except KeyError: + roles = set() + self.users[user.key] = roles + + if role_key in roles: + roles.remove(role_key) + self.pub.delete_role(role_key, user_key) + + def update_role(self, user, role): + user_key = user.key + + try: + roles = self.users[user.key] + except KeyError: + roles = set() + self.users[user_key] = roles + + role_key = role.key + + if not role_key in roles: + roles.add(role_key) + self.pub.add_update_role(role_key, user_key) + + @asyncio.coroutine + def register(self): + @asyncio.coroutine + def apply_config(dts, acg, xact, action, scratch): + self._log.debug("Got user 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.debug("Project being re-added after restart.") + self.add_user(cfg) + else: + # When RIFT first comes up, an INSTALL is called with the current config + # Since confd doesn't actally persist data this never has any data so + # skip this for now. + self._log.debug("No xact handle. Skipping apply config") + + return + + # TODO: There is user-name and user-domain as keys. Need to fix + # this + add_cfgs, delete_cfgs, update_cfgs = get_add_delete_update_cfgs( + dts_member_reg=self._reg, + xact=xact, + key_name="user_name", + ) + + self._log.debug("Added: {}, Deleted: {}, Modified: {}". + format(add_cfgs, delete_cfgs, update_cfgs)) + # Handle Deletes + for cfg in delete_cfgs: + self.delete_user(cfg) + + # Handle Adds + for cfg in add_cfgs: + self.update_user(cfg) + + # Handle Updates + for cfg in update_cfgs: + self.update_user(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 + + self._log.debug("Project %s on_prepare config received (action: %s): %s", + self.project_name, xact_info.query_action, msg) + + user = User().pb(msg) + if action in [rwdts.QueryAction.CREATE, rwdts.QueryAction.UPDATE]: + if user.key in self.users: + self._log.debug("User {} update request". + format(user.key)) + + else: + self._log.debug("User {}: on_prepare add request". + format(user.key)) + + elif action == rwdts.QueryAction.DELETE: + # Check if the user got deleted + fref = ProtobufC.FieldReference.alloc() + fref.goto_whole_message(msg.to_pbcm()) + if fref.is_field_deleted(): + if user.key in self.users: + self._log.debug("User {} being deleted".format(user.key)) + else: + self._log.warning("Delete on unknown user: {}". + format(user.key)) + + else: + self._log.error("Action (%s) NOT SUPPORTED", action) + xact_info.respond_xpath(rwdts.XactRspCode.NACK) + return + + xact_info.respond_xpath(rwdts.XactRspCode.ACK) + + xpath = self.get_xpath() + self._log.debug("Registering for project config using xpath: %s", + 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=xpath, + flags=rwdts.Flag.SUBSCRIBER | rwdts.Flag.DELTA_READY | rwdts.Flag.CACHE, + on_prepare=on_prepare, + ) + + yield from self.pub.register() + self.pub.create_project_roles() + + def deregister(self): + self._log.debug("De-registering DTS handler for project {}". + format(self.project_name)) + if self._reg: + self._reg.deregister() + self._reg = None + + self.pub.deregister() + + +class RoleConfigPublisher(rift.tasklets.DtsConfigPublisher): + + def __init__(self, project): + super().__init__(project._tasklet) + self.project_name = project.name + self.rbac_int = RwRbacInternalYang.YangData_RwRbacInternal_RwRbacInternal() + self.roles = {} + self.proj_roles = MANO_PROJECT_ROLES + self.proj_roles_published = False + + def get_xpath(self): + return "D,/rw-rbac-internal:rw-rbac-internal/rw-rbac-internal:role" + + def role_xpath(self, role_key): + return "D,/rw-rbac-internal:rw-rbac-internal/rw-rbac-internal:role" + \ + "[rw-rbac-internal:role='{}']".format(role_key[0]) + \ + "[rw-rbac-internal:keys='{}']".format(role_key[1]) + + def role_user_xpath(self, role_key, user_key): + return self.role_xpath(role_key) + \ + "/rw-rbac-internal:user" + \ + "[rw-rbac-internal:user-name='{}']".format(user_key[1]) + \ + "[rw-rbac-internal:user-domain='{}']".format(user_key[0]) + + def create_project_roles(self): + for name in self.proj_roles: + role = RoleKeys() + role.role = name + role.keys = self.project_name + self.create_project_role(role) + + def create_project_role(self, role): + xpath = self.role_xpath(role.key) + pb_role = self.pb_role(role) + self._regh.update_element(xpath, pb_role) + + def create_role(self, role_key, user_key): + return RoleKeysUsers(role_key, user_key) + + def pb_role(self, role): + + pbRole = self.rbac_int.create_role() + pbRole.role = role.role + pbRole.keys = role.keys + + return pbRole + + def pb_role_user(self, role, user): + + pbRole = self.rbac_int.create_role() + pbRole.role = role.role + pbRole.keys = role.keys + + pbUser = pbRole.create_user() + pbUser.user_name = user.user_name + pbUser.user_domain = user.user_domain + pbUser.state_machine.state = user.state.name + + pbRole.user.append(pbUser) + + return pbRole + + def add_update_role(self, role_key, user_key): + update = True + try: + role = self.roles[role_key] + except KeyError: + role = RoleKeysUsers(role_key) + self.roles[role_key] = role + update = False + + try: + user = role.user(user_key) + except KeyError: + user = UserState(user_key) + role.add_user(user) + update = False + + user.state = StateMachine.new + + xpath = self.role_xpath(role_key) + + pb_role_user = self.pb_role_user(role, user) + self.log.debug("add_update_role: xpath:{} pb_role:{}".format(xpath, pb_role_user)) + + self._regh.update_element(xpath, pb_role_user) + + def delete_role(self, role_key, user_key): + try: + role = self.roles[role_key] + user = role.user(user_key) + except KeyError: + return + + user.state = StateMachine.delete + xpath = self.role_xpath(role_key) + self.log.debug("deleting role: {} user: {} ".format(role_key, user_key)) + + pb_role = self.pb_role_user(role, user) + self._regh.update_element(xpath, pb_role) + + def do_prepare(self, xact_info, action, ks_path, msg): + """Handle on_prepare. To be overridden by Concreate Publisher Handler + """ + self.log.debug("do_prepare: action: {}, path: {} ks_path, msg: {}".format(action, ks_path, msg)) + + xact_info.respond_xpath(rwdts.XactRspCode.ACK) + + # TODO: See if we need this as this would be called in the platform also + # role_key = tuple([msg.role, msg.keys]) + + # state = msg.state_machine.state + # if state == 'init_done': + # msg.state_machine.state = 'active' + # xpath = self.role_xpath(role_key) + # self._regh.update_element(xpath, msg) + + # for user in msg.users: + # user_key = tuple([user.user_domain, user.user_name]) + # state = user.state_machine.state + # if state == 'init_done': + # user.state_machine.state = 'active' + # xpath = self.role_xpath(role_key) + # self._regh.update_element(xpath, msg) + + def deregister(self): + if self._regh: + self._regh.deregister() + self._regh = None diff --git a/rwprojectmano/plugins/rwprojectmano/rift/tasklets/rwprojectmano/tasklet.py b/rwprojectmano/plugins/rwprojectmano/rift/tasklets/rwprojectmano/tasklet.py new file mode 100644 index 00000000..eb71fa24 --- /dev/null +++ b/rwprojectmano/plugins/rwprojectmano/rift/tasklets/rwprojectmano/tasklet.py @@ -0,0 +1,160 @@ +# +# 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. +# + +""" +Mano 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('RwLog', '1.0') +gi.require_version('RwProjectYang', '1.0') +gi.require_version('RwProjectManoYang', '1.0') +from gi.repository import ( + RwDts as rwdts, + RwLog as rwlog, + RwProjectYang, + RwProjectManoYang, +) + +import rift.tasklets + +from rift.mano.utils.project import ( + ManoProject, + ) + +from .projectmano import ( + ProjectHandler, +) + +from .rolesmano import ( + RoleConfigPublisher, + ProjectConfigSubscriber, +) + + +class ProjectMgrManoProject(ManoProject): + + def __init__(self, name, tasklet): + super(ProjectMgrManoProject, self).__init__(tasklet.log, name) + self.update(tasklet) + + self.project_sub = ProjectConfigSubscriber(self) + + @asyncio.coroutine + def register (self): + self._log.info("Initializing the ProjectMgrMano for %s", self.name) + yield from self.project_sub.register() + + def deregister(self): + self._log.debug("De-register project %s", self.name) + self.project_sub.deregister() + + +class ProjectMgrManoTasklet(rift.tasklets.Tasklet): + """Tasklet that manages the Project config + """ + def __init__(self, *args, **kwargs): + """Constructs a ProjectManager tasklet""" + try: + super().__init__(*args, **kwargs) + self.rwlog.set_category("rw-mano-log") + + self.projects = {} + + except Exception as e: + self.log.exception(e) + + + def start(self): + """Callback that gets invoked when a Tasklet is started""" + super().start() + self.log.info("Starting Mano Project Manager Tasklet") + + self.log.debug("Registering with dts") + self.dts = rift.tasklets.DTS( + self.tasklet_info, + RwProjectManoYang.get_schema(), + self.loop, + self.on_dts_state_change + ) + + self.log.debug("Created DTS Api Object: %s", self.dts) + + def stop(self): + """Callback that gets invoked when Tasklet is stopped""" + try: + self.dts.deinit() + except Exception as e: + self.log.exception(e) + + @asyncio.coroutine + def init(self): + """DTS Init state handler""" + try: + self.log.info("Registering for Project Config") + self.project_handler = ProjectHandler(self, ProjectMgrManoProject) + + self.project_handler.register() + + except Exception as e: + self.log.exception("Registering for project failed: {}".format(e)) + + @asyncio.coroutine + def run(self): + """DTS run state handler""" + pass + + @asyncio.coroutine + def on_dts_state_change(self, state): + """Handle DTS state change + + Take action according to current DTS state to transition application + into the corresponding application state + + Arguments + state - current dts state + + """ + switch = { + rwdts.State.INIT: rwdts.State.REGN_COMPLETE, + rwdts.State.CONFIG: rwdts.State.RUN, + } + + handlers = { + rwdts.State.INIT: self.init, + rwdts.State.RUN: self.run, + } + + # Transition application to next state + handler = handlers.get(state, None) + if handler is not None: + yield from handler() + + # Transition dts to next state + next_state = switch.get(state, None) + if next_state is not None: + self.dts.handle.set_state(next_state) + + def config_ready(self): + """Subscription is complete and ready to start publishing.""" + self.log.debug("Configuration Ready") + + +# vim: ts=4 sw=4 et diff --git a/rwprojectmano/plugins/rwprojectmano/rwprojectmano.py b/rwprojectmano/plugins/rwprojectmano/rwprojectmano.py new file mode 100755 index 00000000..ac7ac479 --- /dev/null +++ b/rwprojectmano/plugins/rwprojectmano/rwprojectmano.py @@ -0,0 +1,22 @@ +# +# 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. +# + +import rift.tasklets.rwprojectmano + +class Tasklet(rift.tasklets.rwprojectmano.ProjectMgrManoTasklet): + pass + +# vim: sw=4 diff --git a/rwprojectmano/plugins/yang/CMakeLists.txt b/rwprojectmano/plugins/yang/CMakeLists.txt new file mode 100644 index 00000000..00e5110f --- /dev/null +++ b/rwprojectmano/plugins/yang/CMakeLists.txt @@ -0,0 +1,27 @@ +# +# 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. +# +# + +rift_add_yang_target( + TARGET rwprojectmano_yang + YANG_FILES + rw-project-mano.yang + GIR_PATHS ${CMAKE_CURRENT_BINARY_DIR} + COMPONENT ${PKG_LONG_NAME} + LIBRARIES + rw_project_yang_gen + ) + diff --git a/rwprojectmano/plugins/yang/Makefile b/rwprojectmano/plugins/yang/Makefile new file mode 100644 index 00000000..2b691a8c --- /dev/null +++ b/rwprojectmano/plugins/yang/Makefile @@ -0,0 +1,36 @@ +# +# 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. +# +# Author(s): Tim Mortsolf +# Creation Date: 11/25/2013 +# + +## +# Define a Makefile function: find_upwards(filename) +# +# Searches for a file of the given name in the directory ., .., ../.., ../../.., etc., +# until the file is found or the root directory is reached +## +find_upward = $(word 1, $(shell while [ `pwd` != / ] ; do find `pwd` -maxdepth 1 -name $1 ; cd .. ; done)) + +## +# Call find_upward("Makefile.top") to find the nearest upwards adjacent Makefile.top +## +makefile.top := $(call find_upward, "Makefile.top") + +## +# If Makefile.top was found, then include it +## +include $(makefile.top) diff --git a/rwprojectmano/plugins/yang/rw-project-mano.yang b/rwprojectmano/plugins/yang/rw-project-mano.yang new file mode 100644 index 00000000..13690580 --- /dev/null +++ b/rwprojectmano/plugins/yang/rw-project-mano.yang @@ -0,0 +1,107 @@ +/* + * + * 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. + * + */ + +module rw-project-mano +{ + namespace "http://riftio.com/ns/riftware-1.0/rw-project-mano"; + prefix "rw-project-mano"; + + import rw-rbac-base { + prefix "rw-rbac-base"; + } + + import rw-pb-ext { + prefix "rw-pb-ext"; + } + + import rw-project { + prefix "rw-project"; + } + + import rw-rbac-internal { + prefix "rw-rbac-internal"; + } + + revision 2017-03-08 { + description + "Initial revision. This YANG file defines the + MANO extentions for project based tenancy"; + reference + "Derived from earlier versions of base YANG files"; + } + + augment /rw-project:project/rw-project:project-config/rw-project:user { + description + "Configuration for MANO application-specific Roles."; + + list mano-role { + description + "The list of MANO application-specific Roles the User has been + assigned, within the enclosing Project."; + + key "role"; + uses rw-rbac-base:simple-role; + } + } + + augment /rw-project:project/rw-project:project-state/rw-project:user { + description + "The state for MANO application-specific Roles."; + + list mano-role { + description + "The state of the MANO application-specific Role the User has + been assigned."; + + key "role"; + uses rw-rbac-base:simple-role; + + leaf state { + description + "The assignment of a User to a Role may be an asynchronous + operation. This value indicates whether the Role + assignment is active. If the value is 'active', then the + assignment is complete and active. Any other value + indicates that Role assignment is in a transitional or + failed state, as described in the value."; + type string; + } + } + } + + augment /rw-project:project/rw-project:project-state { + description + "State for MANO application-specific Roles."; + + list mano-role { + description + "The set of Roles that may be configured into + /rw-project:project/rw-project:project-config/rw-project:user/ + rw-project-mano:mano-role/rw-project-mano:role."; + + key "role"; + uses rw-rbac-base:simple-role; + + leaf description { + description + "A description of the Role."; + type string; + } + } + } +}