Skip to content

Commit 66c7e27

Browse files
committed
schema/context: restore some backlinks support
In libyang v1 the schema nodes had a backlinks member to be able to look up dependents of the node. SONiC depends on this to provide functionality it uses and it needs to be exposed via the python module. In theory, exposing the 'dfs' functions could make this work, but it would likely be cost prohibitive since walking the tree would be expensive to create a python node for evaluation in native python. This implementation provides 2 python functions: * Context.backlinks_find_leafref_nodes(path) - This function can take the path of the base node and find all dependents. If no path is specified, then it will return all nodes that contain a leafref reference. * Context.backlinks_xpath_leafrefs(xpath) - This function takes an xpath, then returns all target nodes the xpath may reference. Typically only one will be returned, but multiples may be in the case of a union. A user can build a cache by combining Context.backlinks_find_leafref_nodes() with no path set and building a reverse table using Context.backlinks_xpath_leafrefs(xpath) Signed-off-by: Brad House <[email protected]>
1 parent 8534053 commit 66c7e27

File tree

6 files changed

+441
-0
lines changed

6 files changed

+441
-0
lines changed

cffi/cdefs.h

+6
Original file line numberDiff line numberDiff line change
@@ -1350,3 +1350,9 @@ extern "Python" void lypy_lyplg_ext_compile_free_clb(const struct ly_ctx *, stru
13501350

13511351
/* from libc, needed to free allocated strings */
13521352
void free(void *);
1353+
1354+
/* From source.c custom C code helpers for backlinks */
1355+
size_t pyly_backlinks_xpath_leafrefs(const struct ly_ctx *ctx, const char *xpath, char ***out);
1356+
size_t pyly_backlinks_find_leafref_nodes(const struct ly_ctx *ctx, const char *base_path, int include_children, char ***out);
1357+
1358+
void pyly_cstr_array_free(char **list, size_t nlist);

cffi/source.c

+257
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,260 @@
99
#if (LY_VERSION_MAJOR != 3)
1010
#error "This version of libyang bindings only works with libyang 3.x"
1111
#endif
12+
13+
typedef struct {
14+
char **results;
15+
size_t nresults;
16+
size_t alloc_results;
17+
} pyly_string_list_t;
18+
19+
/*! Takes append an entry to a dynamic array of strings
20+
* \param[in] l Pointer to pyly_string_list_t object (must be initialized to zero)
21+
* \param[in] str String, the pointer will be owned by the list
22+
*/
23+
static void pyly_strlist_append(pyly_string_list_t *l, char *str /* Takes ownership */)
24+
{
25+
if (l == NULL || str == NULL) {
26+
return;
27+
}
28+
29+
if (l->nresults + 1 > l->alloc_results) {
30+
if (l->alloc_results == 0) {
31+
l->alloc_results = 1;
32+
} else {
33+
l->alloc_results <<= 1;
34+
}
35+
l->results = realloc(l->results, l->alloc_results * sizeof(*l->results));
36+
}
37+
l->results[l->nresults++] = str;
38+
}
39+
40+
void pyly_cstr_array_free(char **list, size_t nlist)
41+
{
42+
size_t i;
43+
44+
if (list == NULL)
45+
return;
46+
47+
for (i=0; i<nlist; i++) {
48+
free(list[i]);
49+
}
50+
free(list);
51+
}
52+
53+
typedef struct {
54+
const struct ly_ctx *ctx;
55+
const char *base_path;
56+
int include_children;
57+
pyly_string_list_t *res;
58+
} pyly_dfs_data_t;
59+
60+
static char *pyly_lref_to_xpath(const struct lysc_node *node, const struct lysc_type_leafref *lref)
61+
{
62+
struct ly_set *set = NULL;
63+
LY_ERR err;
64+
char *path = NULL;
65+
66+
err = lys_find_expr_atoms(node, node->module, lref->path, lref->prefixes, 0, &set);
67+
if (err != LY_SUCCESS) {
68+
return NULL;
69+
}
70+
71+
if (set->count != 0) {
72+
path = lysc_path(set->snodes[set->count - 1], LYSC_PATH_DATA, NULL, 0);
73+
}
74+
75+
ly_set_free(set, NULL);
76+
return path;
77+
}
78+
79+
static size_t pyly_snode_fetch_leafrefs(const struct lysc_node *node, char ***out)
80+
{
81+
pyly_string_list_t res;
82+
const struct lysc_node_leaf *leaf;
83+
84+
if (node == NULL || out == NULL) {
85+
return 0;
86+
}
87+
88+
memset(&res, 0, sizeof(res));
89+
*out = NULL;
90+
91+
/* Not a node type we are interested in */
92+
if (node->nodetype != LYS_LEAF && node->nodetype != LYS_LEAFLIST) {
93+
return 0;
94+
}
95+
96+
leaf = (const struct lysc_node_leaf *)node;
97+
if (leaf->type->basetype == LY_TYPE_UNION) {
98+
/* Unions are a bit of a pain as they aren't represented by nodes,
99+
* so we need to iterate across them to see if they contain any
100+
* leafrefs */
101+
const struct lysc_type_union *un = (const struct lysc_type_union *)leaf->type;
102+
size_t i;
103+
104+
for (i=0; i<LY_ARRAY_COUNT(un->types); i++) {
105+
const struct lysc_type *utype = un->types[i];
106+
107+
if (utype->basetype != LY_TYPE_LEAFREF) {
108+
continue;
109+
}
110+
111+
pyly_strlist_append(&res, pyly_lref_to_xpath(node, (const struct lysc_type_leafref *)utype));
112+
}
113+
} else if (leaf->type->basetype == LY_TYPE_LEAFREF) {
114+
const struct lysc_node *base_node = lysc_node_lref_target(node);
115+
116+
if (base_node == NULL) {
117+
return 0;
118+
}
119+
120+
pyly_strlist_append(&res, lysc_path(base_node, LYSC_PATH_DATA, NULL, 0));
121+
} else {
122+
/* Not a node type we're interested in */
123+
return 0;
124+
}
125+
126+
*out = res.results;
127+
return res.nresults;
128+
}
129+
130+
/*! For the given xpath, return the xpaths for the nodes they reference.
131+
*
132+
* \param[in] ctx Initialized context with loaded schema
133+
* \param[in] xpath Xpath
134+
* \param[out] out Pointer passed by reference that will hold a C array
135+
* of c strings representing the paths for any leaf
136+
* references.
137+
* \return number of results, or 0 if none.
138+
*/
139+
size_t pyly_backlinks_xpath_leafrefs(const struct ly_ctx *ctx, const char *xpath, char ***out)
140+
{
141+
LY_ERR err;
142+
struct ly_set *set = NULL;
143+
size_t i;
144+
pyly_string_list_t res;
145+
146+
if (ctx == NULL || xpath == NULL || out == NULL) {
147+
return 0;
148+
}
149+
150+
memset(&res, 0, sizeof(res));
151+
152+
*out = NULL;
153+
154+
err = lys_find_xpath(ctx, NULL, xpath, 0, &set);
155+
if (err != LY_SUCCESS) {
156+
return 0;
157+
}
158+
159+
for (i=0; i<set->count; i++) {
160+
size_t cnt;
161+
size_t j;
162+
char **refs = NULL;
163+
cnt = pyly_snode_fetch_leafrefs(set->snodes[i], &refs);
164+
for (j=0; j<cnt; j++) {
165+
pyly_strlist_append(&res, strdup(refs[j]));
166+
}
167+
pyly_cstr_array_free(refs, cnt);
168+
}
169+
170+
ly_set_free(set, NULL);
171+
172+
*out = res.results;
173+
return res.nresults;
174+
}
175+
176+
static LY_ERR pyly_backlinks_find_leafref_nodes_clb(struct lysc_node *node, void *data, ly_bool *dfs_continue)
177+
{
178+
pyly_dfs_data_t *dctx = data;
179+
char **leafrefs = NULL;
180+
size_t nleafrefs;
181+
size_t i;
182+
int found = 0;
183+
184+
/* Not a node type we are interested in */
185+
if (node->nodetype != LYS_LEAF && node->nodetype != LYS_LEAFLIST) {
186+
return LY_SUCCESS;
187+
}
188+
189+
/* Fetch leafrefs for comparison against our base path. Even if we are
190+
* going to throw them away, we still need a count to know if this has
191+
* leafrefs */
192+
nleafrefs = pyly_snode_fetch_leafrefs(node, &leafrefs);
193+
if (nleafrefs == 0) {
194+
return LY_SUCCESS;
195+
}
196+
197+
for (i=0; i<nleafrefs && !found; i++) {
198+
if (dctx->base_path != NULL) {
199+
if (dctx->include_children) {
200+
if (strncmp(leafrefs[i], dctx->base_path, strlen(dctx->base_path)) != 0) {
201+
continue;
202+
}
203+
} else {
204+
if (strcmp(leafrefs[i], dctx->base_path) != 0) {
205+
continue;
206+
}
207+
}
208+
}
209+
found = 1;
210+
}
211+
pyly_cstr_array_free(leafrefs, nleafrefs);
212+
213+
if (found) {
214+
pyly_strlist_append(dctx->res, lysc_path(node, LYSC_PATH_DATA, NULL, 0));
215+
}
216+
217+
return LY_SUCCESS;
218+
}
219+
220+
/*! Search the entire loaded schema for any nodes that contain a leafref and
221+
* record the path. If a base_path is specified, only leafrefs that point to
222+
* the specified path will be recorded, if include_children is 1, then children
223+
* of the specified path are also included.
224+
*
225+
* This function is used in replacement for the concept of backlink references
226+
* that were part of libyang v1 but were subsequently removed. This is
227+
* implemented in C code due to the overhead involved with needing to produce
228+
* Python nodes for results for evaluation which has a high overhead.
229+
*
230+
* If building a data cache to more accurately replicate the prior backlinks
231+
* concept, pass NULL to base_path which will then return any paths that
232+
* reference another. It is then the caller's responsibility to look up where
233+
* the leafref is pointing as part of building the cache. It is expected most
234+
* users will not need the cache and will simply pass in the base_path as needed.
235+
*
236+
* \param[in] ctx Initialized context with loaded schema
237+
* \param[in] base_path Optional base node path to restrict output.
238+
* \param[in] include_children Whether or not to include children of the
239+
* specified base path or if the path is an
240+
* explicit reference.
241+
* \param[out] out Pointer passed by reference that will hold a C
242+
* array of c strings representing the paths for
243+
* any leaf references.
244+
* \return number of results, or 0 if none.
245+
*/
246+
size_t pyly_backlinks_find_leafref_nodes(const struct ly_ctx *ctx, const char *base_path, int include_children, char ***out)
247+
{
248+
pyly_string_list_t res;
249+
uint32_t module_idx = 0;
250+
const struct lys_module *module;
251+
252+
memset(&res, 0, sizeof(res));
253+
254+
if (ctx == NULL || out == NULL) {
255+
return 0;
256+
}
257+
258+
/* Iterate across all loaded modules */
259+
for (module_idx = 0; (module = ly_ctx_get_module_iter(ctx, &module_idx)) != NULL; ) {
260+
pyly_dfs_data_t data = { ctx, base_path, include_children, &res };
261+
262+
lysc_module_dfs_full(module, pyly_backlinks_find_leafref_nodes_clb, &data);
263+
/* Ignore error */
264+
}
265+
266+
*out = res.results;
267+
return res.nresults;
268+
}

libyang/context.py

+36
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,42 @@ def parse_data_file(
646646
json_null=json_null,
647647
)
648648

649+
def backlinks_find_leafref_nodes(self, base_path: str = None, include_children: bool = False) -> list[str]:
650+
if self.cdata is None:
651+
raise RuntimeError("context already destroyed")
652+
653+
out = []
654+
655+
carray = ffi.new("char ***")
656+
clen = lib.pyly_backlinks_find_leafref_nodes(
657+
self.cdata, str2c(base_path), 1 if include_children else 0, carray
658+
)
659+
if clen == 0:
660+
return out
661+
662+
for i in range(clen):
663+
out.append(c2str(carray[0][i]))
664+
665+
lib.pyly_cstr_array_free(carray[0], clen)
666+
return out
667+
668+
def backlinks_xpath_leafrefs(self, xpath: str) -> list[str]:
669+
if self.cdata is None:
670+
raise RuntimeError("context already destroyed")
671+
672+
out = []
673+
674+
carray = ffi.new("char ***")
675+
clen = lib.pyly_backlinks_xpath_leafrefs(self.cdata, str2c(xpath), carray)
676+
if clen == 0:
677+
return out
678+
679+
for i in range(clen):
680+
out.append(c2str(carray[0][i]))
681+
682+
lib.pyly_cstr_array_free(carray[0], clen)
683+
return out
684+
649685
def __iter__(self) -> Iterator[Module]:
650686
"""
651687
Return an iterator that yields all implemented modules from the context

tests/test_schema.py

+67
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,73 @@ def test_leaf_list_parsed(self):
801801
self.assertFalse(pnode.ordered())
802802

803803

804+
# -------------------------------------------------------------------------------------
805+
class BacklinksTest(unittest.TestCase):
806+
def setUp(self):
807+
self.ctx = Context(YANG_DIR)
808+
self.ctx.load_module("yolo-leafref-search")
809+
self.ctx.load_module("yolo-leafref-search-extmod")
810+
811+
def tearDown(self):
812+
self.ctx.destroy()
813+
self.ctx = None
814+
815+
def test_backlinks_all_nodes(self):
816+
expected = [
817+
"/yolo-leafref-search-extmod:my_extref_list/my_extref",
818+
"/yolo-leafref-search:refstr",
819+
"/yolo-leafref-search:refnum",
820+
"/yolo-leafref-search-extmod:my_extref_list/my_extref_union"
821+
]
822+
refs = self.ctx.backlinks_find_leafref_nodes()
823+
824+
expected.sort()
825+
refs.sort()
826+
self.assertEqual(expected, refs)
827+
828+
def test_backlinks_one(self):
829+
expected = [
830+
"/yolo-leafref-search-extmod:my_extref_list/my_extref",
831+
"/yolo-leafref-search:refstr",
832+
"/yolo-leafref-search-extmod:my_extref_list/my_extref_union"
833+
]
834+
refs = self.ctx.backlinks_find_leafref_nodes(
835+
base_path="/yolo-leafref-search:my_list/my_leaf_string"
836+
)
837+
838+
expected.sort()
839+
refs.sort()
840+
self.assertEqual(expected, refs)
841+
842+
def test_backlinks_children(self):
843+
expected = [
844+
"/yolo-leafref-search-extmod:my_extref_list/my_extref",
845+
"/yolo-leafref-search:refstr",
846+
"/yolo-leafref-search:refnum",
847+
"/yolo-leafref-search-extmod:my_extref_list/my_extref_union"
848+
]
849+
refs = self.ctx.backlinks_find_leafref_nodes(
850+
base_path="/yolo-leafref-search:my_list/",
851+
include_children=True
852+
)
853+
854+
expected.sort()
855+
refs.sort()
856+
self.assertEqual(expected, refs)
857+
858+
def test_backlinks_xpath_leafrefs(self):
859+
expected = [
860+
"/yolo-leafref-search:my_list/my_leaf_string"
861+
]
862+
refs = self.ctx.backlinks_xpath_leafrefs(
863+
"/yolo-leafref-search-extmod:my_extref_list/my_extref"
864+
)
865+
866+
expected.sort()
867+
refs.sort()
868+
self.assertEqual(expected, refs)
869+
870+
804871
# -------------------------------------------------------------------------------------
805872
class ChoiceTest(unittest.TestCase):
806873
def setUp(self):

0 commit comments

Comments
 (0)