Skip to content

Commit c8313b7

Browse files
bradh352rjarry
authored andcommitted
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. Instead this PR depends on the this libyang PR: CESNET/libyang#2352 And adds thin wrappers. This implementation provides 2 python functions: * Context.find_backlinks_paths() - 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.find_leafref_path_target_paths() - 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.find_backlinks_paths() with no path set and building a reverse table using Context.find_leafref_path_target_paths() Signed-off-by: Brad House <brad@brad-house.com>
1 parent 18faa1e commit c8313b7

File tree

5 files changed

+246
-1
lines changed

5 files changed

+246
-1
lines changed

cffi/cdefs.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,8 @@ const struct lysc_node* lys_find_child(const struct lysc_node *, const struct ly
862862
const struct lysc_node* lysc_node_child(const struct lysc_node *);
863863
const struct lysc_node_action* lysc_node_actions(const struct lysc_node *);
864864
const struct lysc_node_notif* lysc_node_notifs(const struct lysc_node *);
865+
LY_ERR lysc_node_lref_targets(const struct lysc_node *, struct ly_set **);
866+
LY_ERR lysc_node_lref_backlinks(const struct ly_ctx *, const struct lysc_node *, ly_bool, struct ly_set **);
865867

866868
typedef enum {
867869
LYD_PATH_STD,

libyang/context.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# SPDX-License-Identifier: MIT
55

66
import os
7-
from typing import IO, Any, Callable, Iterator, Optional, Sequence, Tuple, Union
7+
from typing import IO, Any, Callable, Iterator, List, Optional, Sequence, Tuple, Union
88

99
from _libyang import ffi, lib
1010
from .data import (
@@ -661,6 +661,114 @@ def parse_data_file(
661661
json_string_datatypes=json_string_datatypes,
662662
)
663663

664+
def find_leafref_path_target_paths(self, leafref_path: str) -> List[str]:
665+
"""
666+
Fetch all leafref targets of the specified path
667+
668+
This is an enhanced version of lysc_node_lref_target() which will return
669+
a set of leafref target paths retrieved from the specified schema path.
670+
While lysc_node_lref_target() will only work on nodetype of LYS_LEAF and
671+
LYS_LEAFLIST this function will also evaluate other datatypes that may
672+
contain leafrefs such as LYS_UNION. This does not, however, search for
673+
children with leafref targets.
674+
675+
:arg self
676+
This instance on context
677+
:arg leafref_path:
678+
Path to node to search for leafref targets
679+
:returns List of target paths that the leafrefs of the specified node
680+
point to.
681+
"""
682+
if self.cdata is None:
683+
raise RuntimeError("context already destroyed")
684+
if leafref_path is None:
685+
raise RuntimeError("leafref_path must be defined")
686+
687+
out = []
688+
689+
node = lib.lys_find_path(self.cdata, ffi.NULL, str2c(leafref_path), 0)
690+
if node == ffi.NULL:
691+
raise self.error("leafref_path not found")
692+
693+
node_set = ffi.new("struct ly_set **")
694+
if (
695+
lib.lysc_node_lref_targets(node, node_set) != lib.LY_SUCCESS
696+
or node_set[0] == ffi.NULL
697+
or node_set[0].count == 0
698+
):
699+
raise self.error("leafref_path does not contain any leafref targets")
700+
701+
node_set = node_set[0]
702+
for i in range(node_set.count):
703+
path = lib.lysc_path(node_set.snodes[i], lib.LYSC_PATH_DATA, ffi.NULL, 0)
704+
out.append(c2str(path))
705+
lib.free(path)
706+
707+
lib.ly_set_free(node_set, ffi.NULL)
708+
709+
return out
710+
711+
def find_backlinks_paths(
712+
self, match_path: str = None, match_ancestors: bool = False
713+
) -> List[str]:
714+
"""
715+
Search entire schema for nodes that contain leafrefs and return as a
716+
list of schema node paths.
717+
718+
Perform a complete scan of the schema tree looking for nodes that
719+
contain leafref entries. When a node contains a leafref entry, and
720+
match_path is specified, determine if reference points to match_path,
721+
if so add the node's path to returned list. If no match_path is
722+
specified, the node containing the leafref is always added to the
723+
returned set. When match_ancestors is true, will evaluate if match_path
724+
is self or an ansestor of self.
725+
726+
This does not return the leafref targets, but the actual node that
727+
contains a leafref.
728+
729+
:arg self
730+
This instance on context
731+
:arg match_path:
732+
Target path to use for matching
733+
:arg match_ancestors:
734+
Whether match_path is a base ancestor or an exact node
735+
:returns List of paths. Exception of match_path is not found or if no
736+
backlinks are found.
737+
"""
738+
if self.cdata is None:
739+
raise RuntimeError("context already destroyed")
740+
out = []
741+
742+
match_node = ffi.NULL
743+
if match_path is not None and match_path == "/" or match_path == "":
744+
match_path = None
745+
746+
if match_path:
747+
match_node = lib.lys_find_path(self.cdata, ffi.NULL, str2c(match_path), 0)
748+
if match_node == ffi.NULL:
749+
raise self.error("match_path not found")
750+
751+
node_set = ffi.new("struct ly_set **")
752+
if (
753+
lib.lysc_node_lref_backlinks(
754+
self.cdata, match_node, match_ancestors, node_set
755+
)
756+
!= lib.LY_SUCCESS
757+
or node_set[0] == ffi.NULL
758+
or node_set[0].count == 0
759+
):
760+
raise self.error("backlinks not found")
761+
762+
node_set = node_set[0]
763+
for i in range(node_set.count):
764+
path = lib.lysc_path(node_set.snodes[i], lib.LYSC_PATH_DATA, ffi.NULL, 0)
765+
out.append(c2str(path))
766+
lib.free(path)
767+
768+
lib.ly_set_free(node_set, ffi.NULL)
769+
770+
return out
771+
664772
def __iter__(self) -> Iterator[Module]:
665773
"""
666774
Return an iterator that yields all implemented modules from the context

tests/test_schema.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,66 @@ 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.find_backlinks_paths()
823+
expected.sort()
824+
refs.sort()
825+
self.assertEqual(expected, refs)
826+
827+
def test_backlinks_one(self):
828+
expected = [
829+
"/yolo-leafref-search-extmod:my_extref_list/my_extref",
830+
"/yolo-leafref-search:refstr",
831+
"/yolo-leafref-search-extmod:my_extref_list/my_extref_union",
832+
]
833+
refs = self.ctx.find_backlinks_paths(
834+
match_path="/yolo-leafref-search:my_list/my_leaf_string"
835+
)
836+
expected.sort()
837+
refs.sort()
838+
self.assertEqual(expected, refs)
839+
840+
def test_backlinks_children(self):
841+
expected = [
842+
"/yolo-leafref-search-extmod:my_extref_list/my_extref",
843+
"/yolo-leafref-search:refstr",
844+
"/yolo-leafref-search:refnum",
845+
"/yolo-leafref-search-extmod:my_extref_list/my_extref_union",
846+
]
847+
refs = self.ctx.find_backlinks_paths(
848+
match_path="/yolo-leafref-search:my_list", match_ancestors=True
849+
)
850+
expected.sort()
851+
refs.sort()
852+
self.assertEqual(expected, refs)
853+
854+
def test_backlinks_leafref_target_paths(self):
855+
expected = ["/yolo-leafref-search:my_list/my_leaf_string"]
856+
refs = self.ctx.find_leafref_path_target_paths(
857+
"/yolo-leafref-search-extmod:my_extref_list/my_extref"
858+
)
859+
expected.sort()
860+
refs.sort()
861+
self.assertEqual(expected, refs)
862+
863+
804864
# -------------------------------------------------------------------------------------
805865
class ChoiceTest(unittest.TestCase):
806866
def setUp(self):
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module yolo-leafref-search-extmod {
2+
yang-version 1.1;
3+
namespace "urn:yang:yolo:leafref-search-extmod";
4+
prefix leafref-search-extmod;
5+
6+
import wtf-types { prefix types; }
7+
8+
import yolo-leafref-search {
9+
prefix leafref-search;
10+
}
11+
12+
revision 2025-02-11 {
13+
description
14+
"Initial version.";
15+
}
16+
17+
list my_extref_list {
18+
key my_leaf_string;
19+
leaf my_leaf_string {
20+
type string;
21+
}
22+
leaf my_extref {
23+
type leafref {
24+
path "/leafref-search:my_list/leafref-search:my_leaf_string";
25+
}
26+
}
27+
leaf my_extref_union {
28+
type union {
29+
type leafref {
30+
path "/leafref-search:my_list/leafref-search:my_leaf_string";
31+
}
32+
type leafref {
33+
path "/leafref-search:my_list/leafref-search:my_leaf_number";
34+
}
35+
type types:number;
36+
}
37+
}
38+
}
39+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
module yolo-leafref-search {
2+
yang-version 1.1;
3+
namespace "urn:yang:yolo:leafref-search";
4+
prefix leafref-search;
5+
6+
import wtf-types { prefix types; }
7+
8+
revision 2025-02-11 {
9+
description
10+
"Initial version.";
11+
}
12+
13+
list my_list {
14+
key my_leaf_string;
15+
leaf my_leaf_string {
16+
type string;
17+
}
18+
leaf my_leaf_number {
19+
description
20+
"A number.";
21+
type types:number;
22+
}
23+
}
24+
25+
leaf refstr {
26+
type leafref {
27+
path "../my_list/my_leaf_string";
28+
}
29+
}
30+
31+
leaf refnum {
32+
type leafref {
33+
path "../my_list/my_leaf_number";
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)