diff --git a/cffi/cdefs.h b/cffi/cdefs.h index 6079ecf..6d5add3 100644 --- a/cffi/cdefs.h +++ b/cffi/cdefs.h @@ -173,6 +173,10 @@ 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 ... #define LY_LOSTORE ... #define LY_LOSTORE_LAST ... @@ -354,6 +358,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; @@ -423,6 +428,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; @@ -1256,6 +1277,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 26235de..7af2794 100644 --- a/libyang/__init__.py +++ b/libyang/__init__.py @@ -63,10 +63,13 @@ 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, IfFeature, @@ -143,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 0000000..1ef021b --- /dev/null +++ b/libyang/extension.py @@ -0,0 +1,178 @@ +# 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: + 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 b033cca..f92c70f 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 f35a5e9..ee91335 100644 --- a/libyang/schema.py +++ b/libyang/schema.py @@ -394,7 +394,7 @@ def __str__(self): class ExtensionParsed(Extension): __slots__ = ("module_parent",) - def __init__(self, context: "libyang.Context", cdata, module_parent: Module = None): + def __init__(self, context: "libyang.Context", cdata, module_parent: Module): super().__init__(context, cdata) self.module_parent = module_parent @@ -411,6 +411,14 @@ def name(self) -> str: def module(self) -> Module: return self._module_from_parsed() + 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_parent) + except LibyangError: + return None + # ------------------------------------------------------------------------------------- class ExtensionCompiled(Extension): @@ -428,6 +436,14 @@ def module(self) -> Module: raise self.context.error("cannot get module") return Module(self.context, self.cdata_def.module) + def parent_node(self) -> Optional["SNode"]: + if not bool(self.cdata.parent_stmt & lib.LY_STMT_NODE_MASK): + return None + try: + return SNode.new(self.context, self.cdata.parent) + except LibyangError: + return None + # ------------------------------------------------------------------------------------- class _EnumBit: diff --git a/tests/test_extension.py b/tests/test_extension.py new file mode 100644 index 0000000..db02f5f --- /dev/null +++ b/tests/test_extension.py @@ -0,0 +1,113 @@ +# Copyright (c) 2018-2019 Robin Jarry +# SPDX-License-Identifier: MIT + +import logging +import os +from typing import Optional +import unittest + +from libyang import ( + Context, + ExtensionCompiled, + ExtensionParsed, + ExtensionPlugin, + LibyangError, + LibyangExtensionError, + Module, +) + + +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")