Skip to content

Commit 2bbdee5

Browse files
authored
Merge pull request #160 from CiscoTestAutomation/default
Consider default in use
2 parents ef3d7bf + 55e4cf5 commit 2bbdee5

File tree

4 files changed

+348
-4
lines changed

4 files changed

+348
-4
lines changed

ncdiff/src/yang/ncdiff/device.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,77 @@ def get_xpath(self, node, type=Tag.XPATH, instance=True):
726726

727727
return Composer(self, node).get_xpath(type, instance=instance)
728728

729+
def default_in_use(self, schema_node):
730+
'''
731+
High-level api: Given a schema node, return a list of schema nodes
732+
that are descendants and their default values are in use, if the given
733+
schema node exists in a data tree. This method is useful to determine
734+
if a default value for a leaf or leaf-list is in use in a data tree.
735+
Parameters
736+
----------
737+
schema_node : `Element`
738+
A schema node in question.
739+
Returns
740+
-------
741+
list
742+
A list of schema nodes that are descendants of the given schema
743+
node and their default values are in use if the given schema node
744+
exists in a data tree. If such schema node does not exist, it
745+
returns an empty list.
746+
Code Example::
747+
>>> m.load_model('openconfig-interfaces')
748+
>>> prefixes = {n[1]: n[2] for n in m.namespaces}
749+
>>> nodes = m.models["openconfig-interfaces"].tree.xpath(
750+
"//oc-if:interfaces/oc-if:interface",
751+
namespaces=prefixes,
752+
)
753+
>>> print(nodes)
754+
[<Element {http://openconfig.net/yang/interfaces}interface at 0x7ff632320ac0>]
755+
>>> defaults = m.default_in_use(nodes[0])
756+
>>> print([m.get_xpath(n) for n in defaults])
757+
...
758+
'''
759+
760+
default_nodes = []
761+
for child in schema_node:
762+
if (
763+
child.get('type') in ('leaf', 'leaf-list') and
764+
child.get('default') is not None
765+
):
766+
default_nodes.append(child)
767+
elif (
768+
child.get('type') == 'choice' and
769+
child.get('default') is not None
770+
):
771+
default_case = child.get('default')
772+
default_ns, _ = self.convert_tag(
773+
default_ns='',
774+
tag=child.tag,
775+
src=Tag.LXML_ETREE,
776+
dst=Tag.LXML_XPATH,
777+
)
778+
default_ns = self.convert_ns(
779+
ns=default_ns,
780+
src=Tag.NAMESPACE,
781+
dst=Tag.PREFIX,
782+
)
783+
_, tag = self.convert_tag(
784+
default_ns=default_ns,
785+
tag=default_case,
786+
src=Tag.XPATH,
787+
dst=Tag.LXML_ETREE,
788+
)
789+
default_case_node = child.find(tag)
790+
if default_case_node is not None:
791+
default_nodes.append(default_case_node)
792+
default_nodes += self.default_in_use(default_case_node)
793+
elif (
794+
child.get('type') == 'container' and
795+
child.get('presence') != 'true'
796+
):
797+
default_nodes += self.default_in_use(child)
798+
return default_nodes
799+
729800
def convert_tag(self, default_ns, tag, src=Tag.LXML_ETREE, dst=Tag.YTOOL):
730801
'''convert_tag
731802

ncdiff/src/yang/ncdiff/netconf.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ class NetconfCalculator(BaseCalculator):
141141
the specified level, depending on situations. Consider roots in a YANG
142142
module are level 0, their children are level 1, and so on so forth.
143143
The default value of replace_depth is 0.
144-
144+
145145
replace_xpath: `str` or `list
146146
Specify the xpath of the node to be replaced when diff_type is
147147
'minimum-replace'. The default value of replace_xpath is None.
@@ -1068,10 +1068,12 @@ def node_sub(self, node_self, node_other, depth=0):
10681068
child_other = etree.Element(child_self.tag,
10691069
{operation_tag: self.preferred_delete},
10701070
nsmap=child_self.nsmap)
1071-
if self.preferred_create != 'merge':
1072-
child_self.set(operation_tag, self.preferred_create)
10731071
if self.diff_type == 'replace':
10741072
child_self.set(operation_tag, 'replace')
1073+
elif self.preferred_create == 'replace':
1074+
child_self.set(operation_tag, self.preferred_create)
1075+
elif self.preferred_create == 'create':
1076+
self.set_create_operation(child_self)
10751077
siblings = list(node_other.iterchildren(tag=child_self.tag))
10761078
if siblings:
10771079
siblings[-1].addnext(child_other)
@@ -1100,8 +1102,10 @@ def node_sub(self, node_self, node_other, depth=0):
11001102
child_self = etree.Element(child_other.tag,
11011103
{operation_tag: self.preferred_delete},
11021104
nsmap=child_other.nsmap)
1103-
if self.preferred_create != 'merge':
1105+
if self.preferred_create == 'replace':
11041106
child_other.set(operation_tag, self.preferred_create)
1107+
elif self.preferred_create == 'create':
1108+
self.set_create_operation(child_other)
11051109
siblings = list(node_self.iterchildren(tag=child_other.tag))
11061110
s_node = self.device.get_schema_node(child_other)
11071111
if siblings:
@@ -1226,6 +1230,32 @@ def node_sub(self, node_self, node_other, depth=0):
12261230
]
12271231
item.set(key_tag, ''.join(id_list))
12281232

1233+
def set_create_operation(self, node):
1234+
'''set_create_operation
1235+
Low-level api: Set the `operation` attribute of a node to `create` when
1236+
it is not already set. This method is used when the preferred_create is
1237+
`create`.
1238+
Parameters
1239+
----------
1240+
node : `Element`
1241+
A config node in a config tree.
1242+
Returns
1243+
-------
1244+
None
1245+
There is no return of this method.
1246+
'''
1247+
1248+
schema_node = self.device.get_schema_node(node)
1249+
if (
1250+
schema_node.get('type') == 'container' and
1251+
schema_node.get('presence') != 'true' and
1252+
len(self.device.default_in_use(schema_node)) > 0
1253+
):
1254+
for child in node:
1255+
self.set_create_operation(child)
1256+
else:
1257+
node.set(operation_tag, 'create')
1258+
12291259
@staticmethod
12301260
def _url_to_prefix(node, id):
12311261
'''_url_to_prefix

ncdiff/src/yang/ncdiff/tests/test_ncdiff.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,6 +1367,136 @@ def test_delta_10(self):
13671367
self.assertEqual(str(delta1).strip(), expected_delta1.strip())
13681368
self.assertEqual(str(delta2).strip(), expected_delta2.strip())
13691369

1370+
def test_delta_11(self):
1371+
xml1 = """
1372+
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="101">
1373+
<data>
1374+
<numbers xmlns="urn:jon">
1375+
<first>one</first>
1376+
</numbers>
1377+
<location xmlns="urn:jon">
1378+
<ontario>
1379+
<name>Ottawa</name>
1380+
</ontario>
1381+
</location>
1382+
</data>
1383+
</rpc-reply>
1384+
"""
1385+
xml2 = """
1386+
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="101">
1387+
<data>
1388+
<numbers xmlns="urn:jon">
1389+
<first>one</first>
1390+
<third>three</third>
1391+
</numbers>
1392+
<location xmlns="urn:jon">
1393+
<alberta>
1394+
<name>Calgary</name>
1395+
</alberta>
1396+
<other-info>
1397+
<detail>Some detail</detail>
1398+
</other-info>
1399+
</location>
1400+
</data>
1401+
</rpc-reply>
1402+
"""
1403+
config1 = Config(self.d, xml1)
1404+
config2 = Config(self.d, xml2)
1405+
delta1 = config2 - config1
1406+
delta2 = config1 - config2
1407+
delta1.preferred_create = "create"
1408+
delta2.preferred_create = "create"
1409+
verification = [
1410+
(delta1, "/nc:config/jon:numbers/jon:third"),
1411+
1412+
# Create operation at list alberta is allowed as it does not have
1413+
# default.
1414+
(delta1, "/nc:config/jon:location/jon:alberta"),
1415+
1416+
# Create operation at other-info is not allowed as it has defaults.
1417+
(delta1, "/nc:config/jon:location/jon:other-info/jon:detail"),
1418+
1419+
(delta2, "/nc:config/jon:location/jon:ontario"),
1420+
]
1421+
for delta, xpath in verification:
1422+
nodes = delta.nc.xpath(
1423+
xpath,
1424+
namespaces=delta.ns)
1425+
self.assertEqual(
1426+
len(nodes),
1427+
1,
1428+
f"Expected to find xpath '{xpath}' in delta "
1429+
f"but the delta is {delta.nc}",
1430+
)
1431+
for node in nodes:
1432+
self.assertEqual(
1433+
node.get(operation_tag),
1434+
"create",
1435+
f"Expected 'create' operation at {xpath} "
1436+
f"but got the delta {delta.nc} instead.",
1437+
)
1438+
1439+
def test_delta_12(self):
1440+
xml1 = """
1441+
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="101">
1442+
<data>
1443+
<numbers xmlns="urn:jon">
1444+
<first>one</first>
1445+
</numbers>
1446+
</data>
1447+
</rpc-reply>
1448+
"""
1449+
xml2 = """
1450+
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="101">
1451+
<data>
1452+
<numbers xmlns="urn:jon">
1453+
<first>one</first>
1454+
<third>three</third>
1455+
</numbers>
1456+
<location xmlns="urn:jon">
1457+
<alberta>
1458+
<name>Calgary</name>
1459+
</alberta>
1460+
<other-info>
1461+
<detail>Some detail</detail>
1462+
</other-info>
1463+
</location>
1464+
</data>
1465+
</rpc-reply>
1466+
"""
1467+
config1 = Config(self.d, xml1)
1468+
config2 = Config(self.d, xml2)
1469+
delta1 = config2 - config1
1470+
delta2 = config1 - config2
1471+
delta1.preferred_create = "create"
1472+
delta2.preferred_create = "create"
1473+
verification = [
1474+
(delta1, "/nc:config/jon:numbers/jon:third"),
1475+
1476+
# Create operation at location is not allowed as it has defaults.
1477+
(delta1, "/nc:config/jon:location/jon:alberta"),
1478+
1479+
# Create operation at other-info is not allowed as it has defaults.
1480+
(delta1, "/nc:config/jon:location/jon:other-info/jon:detail"),
1481+
]
1482+
for delta, xpath in verification:
1483+
nodes = delta.nc.xpath(
1484+
xpath,
1485+
namespaces=delta.ns)
1486+
self.assertEqual(
1487+
len(nodes),
1488+
1,
1489+
f"Expected to find xpath '{xpath}' in delta "
1490+
f"but the delta is {delta.nc}",
1491+
)
1492+
for node in nodes:
1493+
self.assertEqual(
1494+
node.get(operation_tag),
1495+
"create",
1496+
f"Expected 'create' operation at {xpath} "
1497+
f"but got the delta {delta.nc} instead.",
1498+
)
1499+
13701500
def test_delta_replace_1(self):
13711501
config_xml1 = """
13721502
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="101">
@@ -4512,3 +4642,65 @@ def test_get_1(self):
45124642
'/oc-netinst:config/oc-netinst:name/text()')
45134643
self.assertEqual(name, ['Mgmt-intf'])
45144644

4645+
def test_default_in_use_1(self):
4646+
prefixes = {n[1]: n[2] for n in self.d.namespaces if n[1] is not None}
4647+
nodes = self.d.models["jon"].tree.xpath(
4648+
"/jon/jon:address",
4649+
namespaces=prefixes,
4650+
)
4651+
self.assertEqual(len(nodes), 1)
4652+
address = nodes[0]
4653+
defaults = self.d.default_in_use(address)
4654+
self.assertEqual(len(defaults), 1)
4655+
self.assertEqual(
4656+
defaults[0].tag,
4657+
"{urn:jon}city"
4658+
)
4659+
4660+
def test_default_in_use_2(self):
4661+
xpaths = [
4662+
"/jon:location/city/alberta",
4663+
"/jon:location/city/alberta/other-info/geo-facts/code",
4664+
]
4665+
prefixes = {n[1]: n[2] for n in self.d.namespaces if n[1] is not None}
4666+
nodes = self.d.models["jon"].tree.xpath(
4667+
"/jon/jon:location",
4668+
namespaces=prefixes,
4669+
)
4670+
self.assertEqual(len(nodes), 1)
4671+
address = nodes[0]
4672+
defaults = self.d.default_in_use(address)
4673+
xpaths = [
4674+
self.d.get_xpath(n)
4675+
for n in defaults
4676+
]
4677+
self.assertEqual(len(defaults), 2)
4678+
for xpath in xpaths:
4679+
self.assertIn(xpath, xpaths)
4680+
4681+
def test_default_in_use_3(self):
4682+
xpaths = [
4683+
"/oc-if:interfaces/interface/oc-vlan:routed-vlan/oc-ip:ipv4"
4684+
"/unnumbered/config/enabled",
4685+
"/oc-if:interfaces/interface/oc-vlan:routed-vlan/oc-ip:ipv4"
4686+
"/unnumbered/state/enabled",
4687+
"/oc-if:interfaces/interface/oc-vlan:routed-vlan/oc-ip:ipv4"
4688+
"/config/enabled",
4689+
"/oc-if:interfaces/interface/oc-vlan:routed-vlan/oc-ip:ipv4"
4690+
"/state/enabled",
4691+
]
4692+
prefixes = {n[1]: n[2] for n in self.d.namespaces if n[1] is not None}
4693+
nodes = self.d.models["openconfig-interfaces"].tree.xpath(
4694+
"//oc-if:interfaces/oc-if:interface/oc-vlan:routed-vlan/oc-ip:ipv4",
4695+
namespaces=prefixes,
4696+
)
4697+
self.assertEqual(len(nodes), 1)
4698+
interface = nodes[0]
4699+
defaults = self.d.default_in_use(interface)
4700+
xpaths = [
4701+
self.d.get_xpath(n)
4702+
for n in defaults
4703+
]
4704+
self.assertEqual(len(defaults), 4)
4705+
for xpath in xpaths:
4706+
self.assertIn(xpath, xpaths)

0 commit comments

Comments
 (0)