diff --git a/cffi/cdefs.h b/cffi/cdefs.h index 1c1d8f3b..7210adbd 100644 --- a/cffi/cdefs.h +++ b/cffi/cdefs.h @@ -174,6 +174,8 @@ enum ly_stmt { LY_STMT_ARG_VALUE }; +#define LY_STMT_OP_MASK ... +#define LY_STMT_DATA_NODE_MASK ... #define LY_STMT_NODE_MASK ... #define LY_LOLOG ... @@ -359,6 +361,7 @@ LY_ERR lys_print_module(struct ly_out *, const struct lys_module *, LYS_OUTFORMA #define LYS_PRINT_SHRINK ... struct lys_module { + struct ly_ctx *ctx; const char *name; const char *revision; const char *ns; @@ -428,6 +431,22 @@ struct lysc_node_container { struct lysc_node_notif *notifs; }; +struct lysp_stmt { + const char *stmt; + const char *arg; + LY_VALUE_FORMAT format; + void *prefix_data; + struct lysp_stmt *next; + struct lysp_stmt *child; + uint16_t flags; + enum ly_stmt kw; +}; + +struct lysp_ext_substmt { + enum ly_stmt stmt; + ...; +}; + struct lysp_ext_instance { const char *name; const char *argument; @@ -1271,6 +1290,42 @@ struct lyd_leafref_links_rec { LY_ERR lyd_leafref_get_links(const struct lyd_node_term *e, const struct lyd_leafref_links_rec **); LY_ERR lyd_leafref_link_node_tree(struct lyd_node *); +const char *lyplg_ext_stmt2str(enum ly_stmt stmt); +const struct lysp_module *lyplg_ext_parse_get_cur_pmod(const struct lysp_ctx *); +struct ly_ctx *lyplg_ext_compile_get_ctx(const struct lysc_ctx *); +void lyplg_ext_parse_log(const struct lysp_ctx *, const struct lysp_ext_instance *, LY_LOG_LEVEL, LY_ERR, const char *, ...); +void lyplg_ext_compile_log(const struct lysc_ctx *, const struct lysc_ext_instance *, LY_LOG_LEVEL, LY_ERR, const char *, ...); +LY_ERR lyplg_ext_parse_extension_instance(struct lysp_ctx *, struct lysp_ext_instance *); +LY_ERR lyplg_ext_compile_extension_instance(struct lysc_ctx *, const struct lysp_ext_instance *, struct lysc_ext_instance *); +void lyplg_ext_pfree_instance_substatements(const struct ly_ctx *ctx, struct lysp_ext_substmt *substmts); +void lyplg_ext_cfree_instance_substatements(const struct ly_ctx *ctx, struct lysc_ext_substmt *substmts); +typedef LY_ERR (*lyplg_ext_parse_clb)(struct lysp_ctx *, struct lysp_ext_instance *); +typedef LY_ERR (*lyplg_ext_compile_clb)(struct lysc_ctx *, const struct lysp_ext_instance *, struct lysc_ext_instance *); +typedef void (*lyplg_ext_parse_free_clb)(const struct ly_ctx *, struct lysp_ext_instance *); +typedef void (*lyplg_ext_compile_free_clb)(const struct ly_ctx *, struct lysc_ext_instance *); +struct lyplg_ext { + const char *id; + lyplg_ext_parse_clb parse; + lyplg_ext_compile_clb compile; + lyplg_ext_parse_free_clb pfree; + lyplg_ext_compile_free_clb cfree; + ...; +}; + +struct lyplg_ext_record { + const char *module; + const char *revision; + const char *name; + struct lyplg_ext plugin; + ...; +}; + +#define LYPLG_EXT_API_VERSION ... +LY_ERR lyplg_add_extension_plugin(struct ly_ctx *, uint32_t, const struct lyplg_ext_record *); +extern "Python" LY_ERR lypy_lyplg_ext_parse_clb(struct lysp_ctx *, struct lysp_ext_instance *); +extern "Python" LY_ERR lypy_lyplg_ext_compile_clb(struct lysc_ctx *, const struct lysp_ext_instance *, struct lysc_ext_instance *); +extern "Python" void lypy_lyplg_ext_parse_free_clb(const struct ly_ctx *, struct lysp_ext_instance *); +extern "Python" void lypy_lyplg_ext_compile_free_clb(const struct ly_ctx *, struct lysc_ext_instance *); /* from libc, needed to free allocated strings */ void free(void *); diff --git a/libyang/__init__.py b/libyang/__init__.py index cab99712..7af2794c 100644 --- a/libyang/__init__.py +++ b/libyang/__init__.py @@ -63,10 +63,12 @@ UnitsRemoved, schema_diff, ) +from .extension import ExtensionPlugin, LibyangExtensionError from .keyed_list import KeyedList from .log import configure_logging from .schema import ( Extension, + ExtensionCompiled, ExtensionParsed, Feature, IfAndFeatures, @@ -144,6 +146,9 @@ "EnumRemoved", "Extension", "ExtensionAdded", + "ExtensionCompiled", + "ExtensionParsed", + "ExtensionPlugin", "ExtensionRemoved", "Feature", "IfAndFeatures", diff --git a/libyang/extension.py b/libyang/extension.py new file mode 100644 index 00000000..57f7cb2d --- /dev/null +++ b/libyang/extension.py @@ -0,0 +1,216 @@ +# Copyright (c) 2018-2019 Robin Jarry +# Copyright (c) 2020 6WIND S.A. +# Copyright (c) 2021 RACOM s.r.o. +# SPDX-License-Identifier: MIT + +from typing import Callable, Optional + +from _libyang import ffi, lib +from .context import Context +from .log import get_libyang_level +from .schema import ExtensionCompiled, ExtensionParsed, Module +from .util import LibyangError, c2str, str2c + + +# ------------------------------------------------------------------------------------- +extensions_plugins = {} + + +class LibyangExtensionError(LibyangError): + def __init__(self, message: str, ret: int, log_level: int) -> None: + super().__init__(message) + self.ret = ret + self.log_level = log_level + + +@ffi.def_extern(name="lypy_lyplg_ext_parse_clb") +def libyang_c_lyplg_ext_parse_clb(pctx, pext): + plugin = extensions_plugins[pext.record.plugin] + module_cdata = lib.lyplg_ext_parse_get_cur_pmod(pctx).mod + context = Context(cdata=module_cdata.ctx) + module = Module(context, module_cdata) + parsed_ext = ExtensionParsed(context, pext, module) + plugin.set_parse_ctx(pctx) + try: + plugin.parse_clb(module, parsed_ext) + return lib.LY_SUCCESS + except LibyangExtensionError as e: + ly_level = get_libyang_level(e.log_level) + if ly_level is None: + ly_level = lib.LY_EPLUGIN + e_str = str(e) + plugin.add_error_message(e_str) + lib.lyplg_ext_parse_log(pctx, pext, ly_level, e.ret, str2c(e_str)) + return e.ret + + +@ffi.def_extern(name="lypy_lyplg_ext_compile_clb") +def libyang_c_lyplg_ext_compile_clb(cctx, pext, cext): + plugin = extensions_plugins[pext.record.plugin] + context = Context(cdata=lib.lyplg_ext_compile_get_ctx(cctx)) + module = Module(context, cext.module) + parsed_ext = ExtensionParsed(context, pext, module) + compiled_ext = ExtensionCompiled(context, cext) + plugin.set_compile_ctx(cctx) + try: + plugin.compile_clb(parsed_ext, compiled_ext) + return lib.LY_SUCCESS + except LibyangExtensionError as e: + ly_level = get_libyang_level(e.log_level) + if ly_level is None: + ly_level = lib.LY_EPLUGIN + e_str = str(e) + plugin.add_error_message(e_str) + lib.lyplg_ext_compile_log(cctx, cext, ly_level, e.ret, str2c(e_str)) + return e.ret + + +@ffi.def_extern(name="lypy_lyplg_ext_parse_free_clb") +def libyang_c_lyplg_ext_parse_free_clb(ctx, pext): + plugin = extensions_plugins[pext.record.plugin] + context = Context(cdata=ctx) + parsed_ext = ExtensionParsed(context, pext, None) + plugin.parse_free_clb(parsed_ext) + + +@ffi.def_extern(name="lypy_lyplg_ext_compile_free_clb") +def libyang_c_lyplg_ext_compile_free_clb(ctx, cext): + plugin = extensions_plugins[getattr(cext, "def").plugin] + context = Context(cdata=ctx) + compiled_ext = ExtensionCompiled(context, cext) + plugin.compile_free_clb(compiled_ext) + + +class ExtensionPlugin: + ERROR_SUCCESS = lib.LY_SUCCESS + ERROR_MEM = lib.LY_EMEM + ERROR_INVALID_INPUT = lib.LY_EINVAL + ERROR_NOT_VALID = lib.LY_EVALID + ERROR_DENIED = lib.LY_EDENIED + ERROR_NOT = lib.LY_ENOT + + def __init__( + self, + module_name: str, + name: str, + id_str: str, + context: Optional[Context] = None, + parse_clb: Optional[Callable[[Module, ExtensionParsed], None]] = None, + compile_clb: Optional[ + Callable[[ExtensionParsed, ExtensionCompiled], None] + ] = None, + parse_free_clb: Optional[Callable[[ExtensionParsed], None]] = None, + compile_free_clb: Optional[Callable[[ExtensionCompiled], None]] = None, + ) -> None: + """ + Set the callback functions, which will be called if libyang will be processing + given extension defined by name from module defined by module_name. + + :arg self: + This instance of extension plugin + :arg module_name: + The name of module in which the extension is defined + :arg name: + The name of extension itself + :arg id_str: + The unique ID of extension plugin within the libyang context + :arg context: + The context in which the extension plugin will be used. If set to None, + the plugin will be used for all existing and even future contexts + :arg parse_clb: + The optional callback function of which will be called during extension parsing + Expected arguments are: + module: The module which is being parsed + extension: The exact extension instance + Expected raises: + LibyangExtensionError in case of processing error + :arg compile_clb: + The optional callback function of which will be called during extension compiling + Expected arguments are: + extension_parsed: The parsed extension instance + extension_compiled: The compiled extension instance + Expected raises: + LibyangExtensionError in case of processing error + :arg parse_free_clb + The optional callback function of which will be called during freeing of parsed extension + Expected arguments are: + extension: The parsed extension instance to be freed + :arg compile_free_clb + The optional callback function of which will be called during freeing of compiled extension + Expected arguments are: + extension: The compiled extension instance to be freed + """ + self.context = context + self.module_name = module_name + self.module_name_cstr = str2c(self.module_name) + self.name = name + self.name_cstr = str2c(self.name) + self.id_str = id_str + self.id_cstr = str2c(self.id_str) + self.parse_clb = parse_clb + self.compile_clb = compile_clb + self.parse_free_clb = parse_free_clb + self.compile_free_clb = compile_free_clb + self._error_messages = [] + self._pctx = ffi.NULL + self._cctx = ffi.NULL + + self.cdata = ffi.new("struct lyplg_ext_record[2]") + self.cdata[0].module = self.module_name_cstr + self.cdata[0].name = self.name_cstr + self.cdata[0].plugin.id = self.id_cstr + if self.parse_clb is not None: + self.cdata[0].plugin.parse = lib.lypy_lyplg_ext_parse_clb + if self.compile_clb is not None: + self.cdata[0].plugin.compile = lib.lypy_lyplg_ext_compile_clb + if self.parse_free_clb is not None: + self.cdata[0].plugin.pfree = lib.lypy_lyplg_ext_parse_free_clb + if self.compile_free_clb is not None: + self.cdata[0].plugin.cfree = lib.lypy_lyplg_ext_compile_free_clb + ret = lib.lyplg_add_extension_plugin( + context.cdata if context is not None else ffi.NULL, + lib.LYPLG_EXT_API_VERSION, + ffi.cast("const void *", self.cdata), + ) + if ret != lib.LY_SUCCESS: + raise LibyangError("Unable to add extension plugin") + if self.cdata[0].plugin not in extensions_plugins: + extensions_plugins[self.cdata[0].plugin] = self + + def __del__(self) -> None: + if self.cdata[0].plugin in extensions_plugins: + del extensions_plugins[self.cdata[0].plugin] + + @staticmethod + def stmt2str(stmt: int) -> str: + return c2str(lib.lyplg_ext_stmt2str(stmt)) + + def add_error_message(self, err_msg: str) -> None: + self._error_messages.append(err_msg) + + def clear_error_messages(self) -> None: + self._error_messages.clear() + + def set_parse_ctx(self, pctx) -> None: + self._pctx = pctx + + def set_compile_ctx(self, cctx) -> None: + self._cctx = cctx + + def parse_substmts(self, ext: ExtensionParsed) -> int: + return lib.lyplg_ext_parse_extension_instance(self._pctx, ext.cdata) + + def compile_substmts(self, pext: ExtensionParsed, cext: ExtensionCompiled) -> int: + return lib.lyplg_ext_compile_extension_instance( + self._cctx, pext.cdata, cext.cdata + ) + + def free_parse_substmts(self, ext: ExtensionParsed) -> None: + lib.lyplg_ext_pfree_instance_substatements( + self.context.cdata, ext.cdata.substmts + ) + + def free_compile_substmts(self, ext: ExtensionCompiled) -> None: + lib.lyplg_ext_cfree_instance_substatements( + self.context.cdata, ext.cdata.substmts + ) diff --git a/libyang/log.py b/libyang/log.py index b033ccaa..f92c70fd 100644 --- a/libyang/log.py +++ b/libyang/log.py @@ -19,6 +19,13 @@ } +def get_libyang_level(py_level): + for ly_lvl, py_lvl in LOG_LEVELS.items(): + if py_lvl == py_level: + return ly_lvl + return None + + @ffi.def_extern(name="lypy_log_cb") def libyang_c_logging_callback(level, msg, data_path, schema_path, line): args = [c2str(msg)] @@ -50,10 +57,9 @@ def configure_logging(enable_py_logger: bool, level: int = logging.ERROR) -> Non :arg level: Python logging level. By default only ERROR messages are stored/logged. """ - for ly_lvl, py_lvl in LOG_LEVELS.items(): - if py_lvl == level: - lib.ly_log_level(ly_lvl) - break + ly_level = get_libyang_level(level) + if ly_level is not None: + lib.ly_log_level(ly_level) if enable_py_logger: lib.ly_log_options(lib.LY_LOLOG | lib.LY_LOSTORE) lib.ly_set_log_clb(lib.lypy_log_cb) diff --git a/libyang/schema.py b/libyang/schema.py index a47974ee..04b8a193 100644 --- a/libyang/schema.py +++ b/libyang/schema.py @@ -372,7 +372,7 @@ def __str__(self): class Extension: __slots__ = ("context", "cdata", "__dict__") - def __init__(self, context: "libyang.Context", cdata, module_parent: Module = None): + def __init__(self, context: "libyang.Context", cdata): self.context = context self.cdata = cdata @@ -400,6 +400,8 @@ def __init__(self, context: "libyang.Context", cdata, module_parent: Module = No def _module_from_parsed(self) -> Module: prefix = c2str(self.cdata.name).split(":")[0] + if self.module_parent is None: + raise self.context.error("cannot get module") for cdata_imp_mod in ly_array_iter(self.module_parent.cdata.parsed.imports): if ffi.string(cdata_imp_mod.prefix).decode() == prefix: return Module(self.context, cdata_imp_mod.module) @@ -415,7 +417,7 @@ def parent_node(self) -> Optional["PNode"]: if not bool(self.cdata.parent_stmt & lib.LY_STMT_NODE_MASK): return None try: - return PNode.new(self.context, self.cdata.parent, self.module()) + return PNode.new(self.context, self.cdata.parent, self.module_parent) except LibyangError: return None diff --git a/tests/test_diff.py b/tests/test_diff.py index 49bf77a2..d4b7e87e 100644 --- a/tests/test_diff.py +++ b/tests/test_diff.py @@ -12,6 +12,7 @@ EnumRemoved, EnumStatusAdded, EnumStatusRemoved, + ExtensionAdded, NodeTypeAdded, NodeTypeRemoved, SNodeAdded, @@ -82,6 +83,8 @@ class DiffTest(unittest.TestCase): (EnumRemoved, "/yolo-system:state/url/proto"), (EnumStatusAdded, "/yolo-system:conf/url/proto"), (EnumStatusAdded, "/yolo-system:state/url/proto"), + (ExtensionAdded, "/yolo-system:conf/url/proto"), + (ExtensionAdded, "/yolo-system:state/url/proto"), (EnumStatusRemoved, "/yolo-system:conf/url/proto"), (EnumStatusRemoved, "/yolo-system:state/url/proto"), (SNodeAdded, "/yolo-system:conf/pill/red/out"), diff --git a/tests/test_extension.py b/tests/test_extension.py new file mode 100644 index 00000000..b932788c --- /dev/null +++ b/tests/test_extension.py @@ -0,0 +1,193 @@ +# Copyright (c) 2018-2019 Robin Jarry +# SPDX-License-Identifier: MIT + +import logging +import os +from typing import Any, Optional +import unittest + +from libyang import ( + Context, + ExtensionCompiled, + ExtensionParsed, + ExtensionPlugin, + LibyangError, + LibyangExtensionError, + Module, + PLeaf, + SLeaf, +) + + +YANG_DIR = os.path.join(os.path.dirname(__file__), "yang") + + +# ------------------------------------------------------------------------------------- +class TestExtensionPlugin(ExtensionPlugin): + def __init__(self, context: Context) -> None: + super().__init__( + "omg-extensions", + "type-desc", + "omg-extensions-type-desc-plugin-v1", + context, + parse_clb=self._parse_clb, + compile_clb=self._compile_clb, + parse_free_clb=self._parse_free_clb, + compile_free_clb=self._compile_free_clb, + ) + self.parse_clb_called = 0 + self.compile_clb_called = 0 + self.parse_free_clb_called = 0 + self.compile_free_clb_called = 0 + self.parse_clb_exception: Optional[LibyangExtensionError] = None + self.compile_clb_exception: Optional[LibyangExtensionError] = None + self.parse_parent_stmt = None + + def reset(self) -> None: + self.parse_clb_called = 0 + self.compile_clb_called = 0 + self.parse_free_clb_called = 0 + self.compile_free_clb_called = 0 + self.parse_clb_exception = None + self.compile_clb_exception = None + + def _parse_clb(self, module: Module, ext: ExtensionParsed) -> None: + self.parse_clb_called += 1 + if self.parse_clb_exception is not None: + raise self.parse_clb_exception + self.parse_substmts(ext) + self.parse_parent_stmt = self.stmt2str(ext.cdata.parent_stmt) + + def _compile_clb(self, pext: ExtensionParsed, cext: ExtensionCompiled) -> None: + self.compile_clb_called += 1 + if self.compile_clb_exception is not None: + raise self.compile_clb_exception + self.compile_substmts(pext, cext) + + def _parse_free_clb(self, ext: ExtensionParsed) -> None: + self.parse_free_clb_called += 1 + self.free_parse_substmts(ext) + + def _compile_free_clb(self, ext: ExtensionCompiled) -> None: + self.compile_free_clb_called += 1 + self.free_compile_substmts(ext) + + +# ------------------------------------------------------------------------------------- +class ExtensionTest(unittest.TestCase): + def setUp(self): + self.ctx = Context(YANG_DIR) + self.plugin = TestExtensionPlugin(self.ctx) + + def tearDown(self): + self.ctx.destroy() + self.ctx = None + + def test_extension_basic(self): + self.ctx.load_module("yolo-system") + self.assertEqual(5, self.plugin.parse_clb_called) + self.assertEqual(6, self.plugin.compile_clb_called) + self.assertEqual(0, self.plugin.parse_free_clb_called) + self.assertEqual(0, self.plugin.compile_free_clb_called) + self.assertEqual("type", self.plugin.parse_parent_stmt) + self.ctx.destroy() + self.assertEqual(5, self.plugin.parse_clb_called) + self.assertEqual(6, self.plugin.compile_clb_called) + self.assertEqual(5, self.plugin.parse_free_clb_called) + self.assertEqual(6, self.plugin.compile_free_clb_called) + + def test_extension_invalid_parse(self): + self.plugin.parse_clb_exception = LibyangExtensionError( + "this extension cannot be parsed", + self.plugin.ERROR_NOT_VALID, + logging.ERROR, + ) + with self.assertRaises(LibyangError): + self.ctx.load_module("yolo-system") + + def test_extension_invalid_compile(self): + self.plugin.compile_clb_exception = LibyangExtensionError( + "this extension cannot be compiled", + self.plugin.ERROR_NOT_VALID, + logging.ERROR, + ) + with self.assertRaises(LibyangError): + self.ctx.load_module("yolo-system") + + +# ------------------------------------------------------------------------------------- +class ExampleParseExtensionPlugin(ExtensionPlugin): + def __init__(self, context: Context) -> None: + super().__init__( + "omg-extensions", + "parse-validation", + "omg-extensions-parse-validation-plugin-v1", + context, + parse_clb=self._parse_clb, + ) + + def _verify_single(self, parent: Any) -> None: + count = 0 + for e in parent.extensions(): + if e.name() == self.name and e.module().name() == self.module_name: + count += 1 + if count > 1: + raise LibyangExtensionError( + f"Extension {self.name} is allowed to be defined just once per given " + "parent node context.", + self.ERROR_NOT_VALID, + logging.ERROR, + ) + + def _parse_clb(self, _, ext: ExtensionParsed) -> None: + parent = ext.parent_node() + if not isinstance(parent, PLeaf): + raise LibyangExtensionError( + f"Extension {ext.name()} is allowed only in leaf nodes", + self.ERROR_NOT_VALID, + logging.ERROR, + ) + self._verify_single(parent) + # here you put code to perform something reasonable actions you need for your extension + + +class ExampleCompileExtensionPlugin(ExtensionPlugin): + def __init__(self, context: Context) -> None: + super().__init__( + "omg-extensions", + "compile-validation", + "omg-extensions-compile-validation-plugin-v1", + context, + compile_clb=self._compile_clb, + ) + + def _compile_clb(self, pext: ExtensionParsed, cext: ExtensionCompiled) -> None: + parent = cext.parent_node() + if not isinstance(parent, SLeaf): + raise LibyangExtensionError( + f"Extension {cext.name()} is allowed only in leaf nodes", + self.ERROR_NOT_VALID, + logging.ERROR, + ) + # here you put code to perform something reasonable actions you need for your extension + + +class ExtensionExampleTest(unittest.TestCase): + def setUp(self): + self.ctx = Context(YANG_DIR) + self.plugins = [] + + def tearDown(self): + self.plugins.clear() + self.ctx.destroy() + self.ctx = None + + def test_parse_validation_example(self): + self.plugins.append(ExampleParseExtensionPlugin(self.ctx)) + self.ctx.load_module("yolo-system") + + def test_compile_validation_example(self): + self.plugins.append(ExampleParseExtensionPlugin(self.ctx)) + self.plugins.append(ExampleCompileExtensionPlugin(self.ctx)) + with self.assertRaises(LibyangError): + self.ctx.load_module("yolo-system") diff --git a/tests/yang/omg/omg-extensions.yang b/tests/yang/omg/omg-extensions.yang index fe20e7e5..926bf3db 100644 --- a/tests/yang/omg/omg-extensions.yang +++ b/tests/yang/omg/omg-extensions.yang @@ -18,4 +18,14 @@ module omg-extensions { "Extend a type to add a desc."; argument name; } + + extension parse-validation { + description + "Example of parse-validation extension which should be put only under leaf nodes."; + } + + extension compile-validation { + description + "Example of compile-validation extension which should be put only under leaf nodes."; + } } diff --git a/tests/yang/yolo/yolo-system.yang b/tests/yang/yolo/yolo-system.yang index ef612546..36c76416 100644 --- a/tests/yang/yolo/yolo-system.yang +++ b/tests/yang/yolo/yolo-system.yang @@ -83,6 +83,7 @@ module yolo-system { type types:protocol { ext:type-desc ""; } + ext:parse-validation; } leaf host { type string { @@ -114,6 +115,7 @@ module yolo-system { type string; } } + ext:compile-validation; } }