diff --git a/cffi/cdefs.h b/cffi/cdefs.h index 304ae55d..397e7e93 100644 --- a/cffi/cdefs.h +++ b/cffi/cdefs.h @@ -708,6 +708,7 @@ char* lyd_path(const struct lyd_node *, LYD_PATH_TYPE, char *, size_t); LY_ERR lyd_new_inner(struct lyd_node *, const struct lys_module *, const char *, ly_bool, struct lyd_node **); LY_ERR lyd_new_list(struct lyd_node *, const struct lys_module *, const char *, ly_bool, struct lyd_node **, ...); LY_ERR lyd_new_list2(struct lyd_node *, const struct lys_module *, const char *, const char *, ly_bool, struct lyd_node **); +LY_ERR lyd_new_opaq(struct lyd_node *, const struct ly_ctx *, const char *, const char *, const char *, const char *, struct lyd_node **); struct lyd_node_inner { union { diff --git a/libyang/__init__.py b/libyang/__init__.py index bc79e2f0..8bc89952 100644 --- a/libyang/__init__.py +++ b/libyang/__init__.py @@ -15,6 +15,7 @@ DNode, DNodeAttrs, DNotif, + DOpaq, DRpc, ) from .diff import ( diff --git a/libyang/data.py b/libyang/data.py index f0caf240..d815c272 100644 --- a/libyang/data.py +++ b/libyang/data.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT import logging -from typing import IO, Any, Dict, Iterator, Optional, Tuple, Union +from typing import IO, Any, Callable, Dict, Iterator, Optional, Tuple, Union from _libyang import ffi, lib from .keyed_list import KeyedList @@ -22,6 +22,7 @@ LOG = logging.getLogger(__name__) +opaque_dnodes = [] # ------------------------------------------------------------------------------------- @@ -818,13 +819,22 @@ def print_dict( name_cache = {} def _node_name(node): - name = name_cache.get(node.schema) + if node.schema == ffi.NULL: + # opaq node + opaq_cdata = ffi.cast("struct lyd_node_opaq *", node) + if strip_prefixes: + name = c2str(opaq_cdata.name.name) + else: + mod_name = c2str(opaq_cdata.name.module_name) + name = "%s:%s" % (mod_name, c2str(opaq_cdata.name.name)) + else: + name = name_cache.get(node.schema) if name is None: if strip_prefixes: name = c2str(node.schema.name) else: - mod = node.schema.module - name = "%s:%s" % (c2str(mod.name), c2str(node.schema.name)) + mod_name = node.module().name() + name = "%s:%s" % (mod_name, c2str(node.schema.name)) name_cache[node.schema] = name return name @@ -861,7 +871,19 @@ def _to_dict(node, parent_dic): if not lib.lyd_node_should_print(node, flags): return name = _node_name(node) - if node.schema.nodetype == SNode.LIST: + if node.schema == ffi.NULL: + # opaq node + child = lib.lyd_child(node) + if child == ffi.NULL: + opaq_cdata = ffi.cast("struct lyd_node_opaq *", node) + parent_dic[name] = c2str(opaq_cdata.value) + else: + container = {} + while child: + _to_dict(child, container) + child = child.next + parent_dic[name] = container + elif node.schema.nodetype == SNode.LIST: list_element = {} child = lib.lyd_child(node) @@ -952,6 +974,8 @@ def free_internal(self, with_siblings: bool = True) -> None: lib.lyd_free_tree(self.cdata) def free(self, with_siblings: bool = True) -> None: + if self.cdata in opaque_dnodes: + opaque_dnodes.remove(self.cdata) try: if self.free_func: self.free_func(self) # pylint: disable=not-callable @@ -981,11 +1005,14 @@ def _decorator(nodeclass): @classmethod def new(cls, context: "libyang.Context", cdata) -> "DNode": cdata = ffi.cast("struct lyd_node *", cdata) - if not cdata.schema: - schemas = list(context.find_path(cls._get_path(cdata))) - if len(schemas) != 1: - raise LibyangError("Unable to determine schema") - nodecls = cls.NODETYPE_CLASS.get(schemas[0].nodetype(), None) + if cdata.schema == ffi.NULL: + if cdata in opaque_dnodes: + nodecls = DOpaq + else: + schemas = list(context.find_path(cls._get_path(cdata))) + if len(schemas) != 1: + raise LibyangError("Unable to determine schema") + nodecls = cls.NODETYPE_CLASS.get(schemas[0].nodetype(), None) else: nodecls = cls.NODETYPE_CLASS.get(cdata.schema.nodetype, None) if nodecls is None: @@ -1020,6 +1047,9 @@ def children(self, no_keys=False) -> Iterator[DNode]: while child: if child.schema != ffi.NULL: yield DNode.new(self.context, child) + elif child in opaque_dnodes: + # opaq node + yield DOpaq(self.context, child) child = child.next def __iter__(self): @@ -1132,6 +1162,8 @@ def dict_to_dnode( rpc: bool = False, rpcreply: bool = False, notification: bool = False, + just_opaq: bool = False, + on_dnode_created: Optional[Callable[[DNode, SNode], None]] = None, ) -> Optional[DNode]: """ Convert a python dictionary to a DNode object given a YANG module object. The return @@ -1158,6 +1190,10 @@ def dict_to_dnode( Data represents RPC or action output parameters. :arg notification: Data represents notification parameters. + :arg just_opaq: + The returned DNode tree will include just DOpaq nodes + :arg on_dnode_created: + Callback function which will be called once a new DNode instance will be created """ if not dic: return None @@ -1171,6 +1207,16 @@ def dict_to_dnode( created = [] + def _create_opaq( + _parent: Optional[DNode], + module: Module, + name: str, + value: Optional[str] = None, + ): + dnode = DOpaq.create_opaq(_parent, module.context, name, module.name(), value) + created.append(dnode.cdata) + return dnode.cdata + def _create_leaf(_parent, module, name, value, in_rpc_output=False): if value is not None: if isinstance(value, bool): @@ -1192,6 +1238,7 @@ def _create_leaf(_parent, module, name, value, in_rpc_output=False): "failed to create leaf %r as a child of %s", name, parent_path ) created.append(n[0]) + return n[0] def _create_container(_parent, module, name, in_rpc_output=False): n = ffi.new("struct lyd_node **") @@ -1300,7 +1347,12 @@ def _to_dnode(_dic, _schema, _parent=ffi.NULL, in_rpc_output=False): value = _dic[key] if isinstance(s, SLeaf): - _create_leaf(_parent, module, name, value, in_rpc_output) + if just_opaq: + n = _create_opaq(_parent, module, name, value) + else: + n = _create_leaf(_parent, module, name, value, in_rpc_output) + if on_dnode_created is not None: + on_dnode_created(DNode.new(module.context, n), s) elif isinstance(s, SLeafList): if not isinstance(value, (list, tuple)): @@ -1309,15 +1361,30 @@ def _to_dnode(_dic, _schema, _parent=ffi.NULL, in_rpc_output=False): % (s.schema_path(), value) ) for v in value: - _create_leaf(_parent, module, name, v, in_rpc_output) + if just_opaq: + n = _create_opaq(_parent, module, name, v) + else: + n = _create_leaf(_parent, module, name, v, in_rpc_output) + if on_dnode_created is not None: + on_dnode_created(DNode.new(module.context, n), s) elif isinstance(s, SRpc): - n = _create_container(_parent, module, name, in_rpc_output) + if just_opaq: + n = _create_opaq(_parent, module, name) + else: + n = _create_container(_parent, module, name, in_rpc_output) _to_dnode(value, s, n, rpcreply) + if on_dnode_created is not None: + on_dnode_created(DNode.new(module.context, n), s) elif isinstance(s, SContainer): - n = _create_container(_parent, module, name, in_rpc_output) + if just_opaq: + n = _create_opaq(_parent, module, name) + else: + n = _create_container(_parent, module, name, in_rpc_output) _to_dnode(value, s, n, in_rpc_output) + if on_dnode_created is not None: + on_dnode_created(DNode.new(module.context, n), s) elif isinstance(s, SList): if not isinstance(value, (list, tuple)): @@ -1341,12 +1408,30 @@ def _to_dnode(_dic, _schema, _parent=ffi.NULL, in_rpc_output=False): except KeyError as e: raise ValueError("Missing key %s in the list" % (k)) from e - n = _create_list(_parent, module, name, key_values, in_rpc_output) + if just_opaq: + val = v.copy() + n = _create_opaq(_parent, module, name) + if on_dnode_created is not None: + on_dnode_created(DNode.new(module.context, n), s) + else: + n = _create_list( + _parent, module, name, key_values, in_rpc_output + ) + if on_dnode_created is not None: + for n2 in n.children(): + s2 = module.context.find_path(n2.name(), root_node=s) + on_dnode_created(DNode.new(module.context, n2), s2) + on_dnode_created(DNode.new(module.context, n), s) _to_dnode(val, s, n, in_rpc_output) elif isinstance(s, SNotif): - n = _create_container(_parent, module, name, in_rpc_output) + if just_opaq: + n = _create_opaq(_parent, module, name) + else: + n = _create_container(_parent, module, name, in_rpc_output) _to_dnode(value, s, n, in_rpc_output) + if on_dnode_created is not None: + on_dnode_created(DNode.new(module.context, n), s) result = None @@ -1381,3 +1466,60 @@ def _to_dnode(_dic, _schema, _parent=ffi.NULL, in_rpc_output=False): raise return result + + +# ------------------------------------------------------------------------------------- +class DOpaq(DContainer): + __slots__ = ("cdata_opaq",) + + def __init__(self, context: "libyang.Context", cdata) -> None: + super().__init__(context, cdata) + if cdata not in opaque_dnodes: + opaque_dnodes.append(cdata) + self.cdata_opaq = ffi.cast("struct lyd_node_opaq *", cdata) + + @staticmethod + def create_opaq( + parent: Union[Optional[DNode], Any], + context: "libyang.Context", + name: str, + module_name: str, + value: Optional[str] = None, + prefix: Optional[str] = None, + ) -> "DOpaq": + n = ffi.new("struct lyd_node **") + if parent is None: + parent = ffi.NULL + elif isinstance(parent, DNode): + parent = parent.cdata + ret = lib.lyd_new_opaq( + parent, + context.cdata, + str2c(name), + str2c(value), + str2c(prefix), + str2c(module_name), + n, + ) + if ret != lib.LY_SUCCESS: + raise context.error("Cannot create opaque data node") + dnode = DOpaq(context, n[0]) + return dnode + + def name(self) -> str: + return c2str(self.cdata_opaq.name.name) + + def module(self) -> Module: + module = self.context.get_module(c2str(self.cdata_opaq.name.module_name)) + if module is None: + raise self.context.error( + f"Unable to get module '{c2str(self.cdata_opaq.name.module_name)}'" + ) + return module + + def schema(self) -> SNode: + if self.cdata.schema == ffi.NULL: + raise self.context.error( + "Opaque data node doesn't have any schema assigned" + ) + return super().schema() diff --git a/libyang/schema.py b/libyang/schema.py index d49c39a7..275487c9 100644 --- a/libyang/schema.py +++ b/libyang/schema.py @@ -1278,7 +1278,12 @@ def _decorator(nodeclass): @staticmethod def new(context: "libyang.Context", cdata) -> "SNode": - cdata = ffi.cast("struct lysc_node *", cdata) + try: + cdata = ffi.cast("struct lysc_node *", cdata) + except TypeError as e: + print("steweg", type(cdata), cdata) + raise e + nodecls = SNode.NODETYPE_CLASS.get(cdata.nodetype, None) if nodecls is None: raise TypeError("node type %s not implemented" % cdata.nodetype) diff --git a/tests/test_data.py b/tests/test_data.py index 10d9045f..c6ce38a7 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -18,9 +18,13 @@ DNode, DNodeAttrs, DNotif, + DOpaq, DRpc, IOType, LibyangError, + Module, + SContainer, + SNode, ) from libyang.data import dict_to_dnode @@ -1034,3 +1038,93 @@ def test_dnode_attrs_set_and_remove_multiple(self): attrs.remove("ietf-netconf:operation") self.assertEqual(len(attrs), 0) + + def test_dnode_opaq(self): + module = self.ctx.get_module("yolo-nodetypes") + # basic node operations + dnode = DOpaq.create_opaq(None, self.ctx, "test1", "yolo-nodetypes", "val") + self.assertIsInstance(dnode, DOpaq) + self.assertEqual(dnode.name(), "test1") + with self.assertRaises(Exception) as cm: + dnode.schema() + self.assertEqual( + str(cm.exception), "Opaque data node doesn't have any schema" + ) + + # valid module check + module2 = dnode.module() + self.assertIsInstance(module2, Module) + self.assertEqual(module.cdata, module2.cdata) + + # invalid module check + dnode = DOpaq.create_opaq(None, self.ctx, "test1", "invalid-module", "val") + with self.assertRaises(Exception) as cm: + dnode.module() + self.assertEqual(str(cm.exception), "Unable to get module 'invalid-module'") + dnode.free() + + def test_dnode_opaq_dict_to_dnode(self): + dnodes = [] + + def _on_dnode_created(dnode: DNode, snode: SNode) -> None: + if dnode.parent() is None: + self.assertIsInstance(dnode, DOpaq) + self.assertIsInstance(snode, SContainer) + dnodes.append(dnode) + + MAIN = { + "yolo-nodetypes:conf": { + "percentage": "20.2", + "list1": [ + {"leaf1": "k1", "leaf2": "val1"}, + {"leaf1": "k2", "leaf2": "val2"}, + ], + } + } + module = self.ctx.get_module("yolo-nodetypes") + dnode = dict_to_dnode(MAIN, module, None, validate=False) + self.assertIsInstance(dnode, DContainer) + dnode = dict_to_dnode( + MAIN, + module, + None, + validate=False, + just_opaq=True, + on_dnode_created=_on_dnode_created, + ) + self.assertEqual(len(dnodes), 8) + self.assertIsInstance(dnode, DOpaq) + for child in dnode: + self.assertIsInstance(child, DOpaq) + if child.name() == "list1": + for child2 in child: + self.assertIsInstance(child, DOpaq) + self.assertEqual(len(tuple(child.children())), 2) + self.assertEqual(len(tuple(dnode.children())), 3) + dnode.free() + + def test_dnode_opaq_within_print_dict(self): + dnode = DOpaq.create_opaq(None, self.ctx, "test1", "invalid-module", "val") + dic = dnode.print_dict() + self.assertEqual(dic, {"test1": "val"}) + dnode2 = DOpaq.create_opaq( + dnode, self.ctx, "test1-child", "invalid-module", "val2" + ) + self.assertIsInstance(dnode2, DOpaq) + dic = dnode.print_dict(strip_prefixes=False) + self.assertEqual( + dic, {"invalid-module:test1": {"invalid-module:test1-child": "val2"}} + ) + parent = dnode2.parent() + self.assertIsInstance(parent, DOpaq) + self.assertEqual(parent.cdata, dnode.cdata) + dnode.free() + + def test_dnode_opaq_within_container(self): + MAIN = {"yolo-nodetypes:conf": {"percentage": "20.2"}} + module = self.ctx.get_module("yolo-nodetypes") + dnode1 = dict_to_dnode(MAIN, module, None, validate=False) + dnode2 = DOpaq.create_opaq(dnode1, self.ctx, "ratios", "yolo-nodetypes", "val") + children = [c.cdata for c in dnode1.children()] + self.assertTrue(dnode2.cdata in children) + dnode1.free()