Skip to content

Commit f7bd07a

Browse files
committed
libyang3-py3: patch for backlinks support
CESNET/libyang-python#132
1 parent 640c7a0 commit f7bd07a

File tree

2 files changed

+324
-0
lines changed

2 files changed

+324
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
From 6f4c8443c219e9b4fd464eb37cc40f7085a94697 Mon Sep 17 00:00:00 2001
2+
From: Brad House <brad@brad-house.com>
3+
Date: Sun, 16 Feb 2025 11:04:50 -0500
4+
Subject: [PATCH] schema/context: restore some backlinks support
5+
6+
In libyang v1 the schema nodes had a backlinks member to be able to
7+
look up dependents of the node. SONiC depends on this to provide
8+
functionality it uses and it needs to be exposed via the python
9+
module.
10+
11+
In theory, exposing the 'dfs' functions could make this work, but
12+
it would likely be cost prohibitive since walking the tree would
13+
be expensive to create a python node for evaluation in native
14+
python.
15+
16+
Instead this PR depends on the this libyang PR:
17+
https://github.com/CESNET/libyang/pull/2351
18+
And adds thin wrappers.
19+
20+
This implementation provides 2 python functions:
21+
* Context.find_backlinks_paths() - This function can
22+
take the path of the base node and find all dependents. If
23+
no path is specified, then it will return all nodes that contain
24+
a leafref reference.
25+
* Context.find_leafref_path_target_paths() - This function takes
26+
an xpath, then returns all target nodes the xpath may reference.
27+
Typically only one will be returned, but multiples may be in the
28+
case of a union.
29+
30+
A user can build a cache by combining Context.find_backlinks_paths()
31+
with no path set and building a reverse table using
32+
Context.find_leafref_path_target_paths()
33+
34+
Signed-off-by: Brad House <brad@brad-house.com>
35+
---
36+
cffi/cdefs.h | 3 +
37+
libyang/context.py | 98 +++++++++++++++++++
38+
tests/test_schema.py | 58 +++++++++++
39+
.../yang/yolo/yolo-leafref-search-extmod.yang | 39 ++++++++
40+
tests/yang/yolo/yolo-leafref-search.yang | 36 +++++++
41+
5 files changed, 234 insertions(+)
42+
create mode 100644 tests/yang/yolo/yolo-leafref-search-extmod.yang
43+
create mode 100644 tests/yang/yolo/yolo-leafref-search.yang
44+
45+
diff --git a/cffi/cdefs.h b/cffi/cdefs.h
46+
index aa75004..bcdca04 100644
47+
--- a/cffi/cdefs.h
48+
+++ b/cffi/cdefs.h
49+
@@ -861,6 +861,9 @@ const struct lysc_node* lys_find_child(const struct lysc_node *, const struct ly
50+
const struct lysc_node* lysc_node_child(const struct lysc_node *);
51+
const struct lysc_node_action* lysc_node_actions(const struct lysc_node *);
52+
const struct lysc_node_notif* lysc_node_notifs(const struct lysc_node *);
53+
+ly_bool lysc_node_has_ancestor(const struct lysc_node *, const struct lysc_node *);
54+
+LY_ERR lysc_node_find_lref_targets(const struct lysc_node *, struct ly_set **);
55+
+LY_ERR lys_find_backlinks(const struct ly_ctx *, const struct lysc_node *, ly_bool, struct ly_set **);
56+
57+
typedef enum {
58+
LYD_PATH_STD,
59+
diff --git a/libyang/context.py b/libyang/context.py
60+
index fb4a330..e28c856 100644
61+
--- a/libyang/context.py
62+
+++ b/libyang/context.py
63+
@@ -646,6 +646,104 @@ def parse_data_file(
64+
json_null=json_null,
65+
)
66+
67+
+ def find_leafref_path_target_paths(self, leafref_path: str) -> list[str]:
68+
+ """
69+
+ Fetch all leafref targets of the specified path
70+
+
71+
+ This is an enhanced version of lysc_node_lref_target() which will return
72+
+ a set of leafref target paths retrieved from the specified schema path.
73+
+ While lysc_node_lref_target() will only work on nodetype of LYS_LEAF and
74+
+ LYS_LEAFLIST this function will also evaluate other datatypes that may
75+
+ contain leafrefs such as LYS_UNION. This does not, however, search for
76+
+ children with leafref targets.
77+
+
78+
+ :arg self
79+
+ This instance on context
80+
+ :arg leafref_path:
81+
+ Path to node to search for leafref targets
82+
+ :returns List of target paths that the leafrefs of the specified node
83+
+ point to.
84+
+ """
85+
+ if self.cdata is None:
86+
+ raise RuntimeError("context already destroyed")
87+
+ if leafref_path is None:
88+
+ raise RuntimeError("leafref_path must be defined")
89+
+
90+
+ out = []
91+
+
92+
+ node = lib.lys_find_path(self.cdata, ffi.NULL, str2c(leafref_path), 0)
93+
+ if node == ffi.NULL:
94+
+ raise self.error("leafref_path not found")
95+
+
96+
+ node_set = ffi.new("struct ly_set **")
97+
+ if (lib.lysc_node_find_lref_targets(node, node_set) != lib.LY_SUCCESS or
98+
+ node_set[0] == ffi.NULL or node_set[0].count == 0):
99+
+ raise self.error("leafref_path does not contain any leafref targets")
100+
+
101+
+ node_set = node_set[0]
102+
+ for i in range(node_set.count):
103+
+ path = lib.lysc_path(node_set.snodes[i], lib.LYSC_PATH_DATA, ffi.NULL, 0);
104+
+ out.append(c2str(path))
105+
+ lib.free(path)
106+
+
107+
+ lib.ly_set_free(node_set, ffi.NULL)
108+
+
109+
+ return out
110+
+
111+
+
112+
+ def find_backlinks_paths(self, match_path: str = None, match_ancestors: bool = False) -> list[str]:
113+
+ """
114+
+ Search entire schema for nodes that contain leafrefs and return as a
115+
+ list of schema node paths.
116+
+
117+
+ Perform a complete scan of the schema tree looking for nodes that
118+
+ contain leafref entries. When a node contains a leafref entry, and
119+
+ match_path is specified, determine if reference points to match_path,
120+
+ if so add the node's path to returned list. If no match_path is
121+
+ specified, the node containing the leafref is always added to the
122+
+ returned set. When match_ancestors is true, will evaluate if match_path
123+
+ is self or an ansestor of self.
124+
+
125+
+ This does not return the leafref targets, but the actual node that
126+
+ contains a leafref.
127+
+
128+
+ :arg self
129+
+ This instance on context
130+
+ :arg match_path:
131+
+ Target path to use for matching
132+
+ :arg match_ancestors:
133+
+ Whether match_path is a base ancestor or an exact node
134+
+ :returns List of paths. Exception of match_path is not found or if no
135+
+ backlinks are found.
136+
+ """
137+
+ if self.cdata is None:
138+
+ raise RuntimeError("context already destroyed")
139+
+ out = []
140+
+
141+
+ match_node = ffi.NULL
142+
+ if match_path is not None and match_path == "/" or match_path == "":
143+
+ match_path = None
144+
+
145+
+ if match_path:
146+
+ match_node = lib.lys_find_path(self.cdata, ffi.NULL, str2c(match_path), 0)
147+
+ if match_node == ffi.NULL:
148+
+ raise self.error("match_path not found")
149+
+
150+
+ node_set = ffi.new("struct ly_set **")
151+
+ if (lib.lys_find_backlinks(self.cdata, match_node, match_ancestors, node_set)
152+
+ != lib.LY_SUCCESS or node_set[0] == ffi.NULL or node_set[0].count == 0):
153+
+ raise self.error("backlinks not found")
154+
+
155+
+ node_set = node_set[0]
156+
+ for i in range(node_set.count):
157+
+ path = lib.lysc_path(node_set.snodes[i], lib.LYSC_PATH_DATA, ffi.NULL, 0);
158+
+ out.append(c2str(path))
159+
+ lib.free(path)
160+
+
161+
+ lib.ly_set_free(node_set, ffi.NULL)
162+
+
163+
+ return out
164+
+
165+
def __iter__(self) -> Iterator[Module]:
166+
"""
167+
Return an iterator that yields all implemented modules from the context
168+
diff --git a/tests/test_schema.py b/tests/test_schema.py
169+
index a310aad..4aae73a 100644
170+
--- a/tests/test_schema.py
171+
+++ b/tests/test_schema.py
172+
@@ -801,6 +801,64 @@ def test_leaf_list_parsed(self):
173+
self.assertFalse(pnode.ordered())
174+
175+
176+
+# -------------------------------------------------------------------------------------
177+
+class BacklinksTest(unittest.TestCase):
178+
+ def setUp(self):
179+
+ self.ctx = Context(YANG_DIR)
180+
+ self.ctx.load_module("yolo-leafref-search")
181+
+ self.ctx.load_module("yolo-leafref-search-extmod")
182+
+ def tearDown(self):
183+
+ self.ctx.destroy()
184+
+ self.ctx = None
185+
+ def test_backlinks_all_nodes(self):
186+
+ expected = [
187+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref",
188+
+ "/yolo-leafref-search:refstr",
189+
+ "/yolo-leafref-search:refnum",
190+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref_union"
191+
+ ]
192+
+ refs = self.ctx.find_backlinks_paths()
193+
+ expected.sort()
194+
+ refs.sort()
195+
+ self.assertEqual(expected, refs)
196+
+ def test_backlinks_one(self):
197+
+ expected = [
198+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref",
199+
+ "/yolo-leafref-search:refstr",
200+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref_union"
201+
+ ]
202+
+ refs = self.ctx.find_backlinks_paths(
203+
+ match_path="/yolo-leafref-search:my_list/my_leaf_string"
204+
+ )
205+
+ expected.sort()
206+
+ refs.sort()
207+
+ self.assertEqual(expected, refs)
208+
+ def test_backlinks_children(self):
209+
+ expected = [
210+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref",
211+
+ "/yolo-leafref-search:refstr",
212+
+ "/yolo-leafref-search:refnum",
213+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref_union"
214+
+ ]
215+
+ refs = self.ctx.find_backlinks_paths(
216+
+ match_path="/yolo-leafref-search:my_list",
217+
+ match_ancestors=True
218+
+ )
219+
+ expected.sort()
220+
+ refs.sort()
221+
+ self.assertEqual(expected, refs)
222+
+ def test_backlinks_leafref_target_paths(self):
223+
+ expected = [
224+
+ "/yolo-leafref-search:my_list/my_leaf_string"
225+
+ ]
226+
+ refs = self.ctx.find_leafref_path_target_paths(
227+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref"
228+
+ )
229+
+ expected.sort()
230+
+ refs.sort()
231+
+ self.assertEqual(expected, refs)
232+
+
233+
+
234+
# -------------------------------------------------------------------------------------
235+
class ChoiceTest(unittest.TestCase):
236+
def setUp(self):
237+
diff --git a/tests/yang/yolo/yolo-leafref-search-extmod.yang b/tests/yang/yolo/yolo-leafref-search-extmod.yang
238+
new file mode 100644
239+
index 0000000..046ceec
240+
--- /dev/null
241+
+++ b/tests/yang/yolo/yolo-leafref-search-extmod.yang
242+
@@ -0,0 +1,39 @@
243+
+module yolo-leafref-search-extmod {
244+
+ yang-version 1.1;
245+
+ namespace "urn:yang:yolo:leafref-search-extmod";
246+
+ prefix leafref-search-extmod;
247+
+
248+
+ import wtf-types { prefix types; }
249+
+
250+
+ import yolo-leafref-search {
251+
+ prefix leafref-search;
252+
+ }
253+
+
254+
+ revision 2025-02-11 {
255+
+ description
256+
+ "Initial version.";
257+
+ }
258+
+
259+
+ list my_extref_list {
260+
+ key my_leaf_string;
261+
+ leaf my_leaf_string {
262+
+ type string;
263+
+ }
264+
+ leaf my_extref {
265+
+ type leafref {
266+
+ path "/leafref-search:my_list/leafref-search:my_leaf_string";
267+
+ }
268+
+ }
269+
+ leaf my_extref_union {
270+
+ type union {
271+
+ type leafref {
272+
+ path "/leafref-search:my_list/leafref-search:my_leaf_string";
273+
+ }
274+
+ type leafref {
275+
+ path "/leafref-search:my_list/leafref-search:my_leaf_number";
276+
+ }
277+
+ type types:number;
278+
+ }
279+
+ }
280+
+ }
281+
+}
282+
diff --git a/tests/yang/yolo/yolo-leafref-search.yang b/tests/yang/yolo/yolo-leafref-search.yang
283+
new file mode 100644
284+
index 0000000..5f4af48
285+
--- /dev/null
286+
+++ b/tests/yang/yolo/yolo-leafref-search.yang
287+
@@ -0,0 +1,36 @@
288+
+module yolo-leafref-search {
289+
+ yang-version 1.1;
290+
+ namespace "urn:yang:yolo:leafref-search";
291+
+ prefix leafref-search;
292+
+
293+
+ import wtf-types { prefix types; }
294+
+
295+
+ revision 2025-02-11 {
296+
+ description
297+
+ "Initial version.";
298+
+ }
299+
+
300+
+ list my_list {
301+
+ key my_leaf_string;
302+
+ leaf my_leaf_string {
303+
+ type string;
304+
+ }
305+
+ leaf my_leaf_number {
306+
+ description
307+
+ "A number.";
308+
+ type types:number;
309+
+ }
310+
+ }
311+
+
312+
+ leaf refstr {
313+
+ type leafref {
314+
+ path "../my_list/my_leaf_string";
315+
+ }
316+
+ }
317+
+
318+
+ leaf refnum {
319+
+ type leafref {
320+
+ path "../my_list/my_leaf_number";
321+
+ }
322+
+ }
323+
+}

src/libyang3-py3.patch/series

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
0001-debian.patch
22
0002-pr134-json-string-datatypes.patch
33
0003-parse_module-memleak.patch
4+
0004-pr132-backlinks.patch

0 commit comments

Comments
 (0)