Skip to content

Commit eb9f1c5

Browse files
authored
YANG XPath Fixes (v2 for significant change) (#55)
* Use latest xpath parsing logic * Fix YANG XPath usage * Simplify jinja method * Ignore empty xpath split * Keep empty element in xpath builder * Don't fully qualify by default * v1 to v2 migration script
1 parent ad4a249 commit eb9f1c5

File tree

12 files changed

+489
-69
lines changed

12 files changed

+489
-69
lines changed

etl/src/yang/__init__.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -161,17 +161,14 @@ def add_version_modules(db, os_key, version, modules):
161161
def add_data_paths_to_dm(db, dm_node, module, dp_parent=None):
162162
"""Add the parsed DataPaths from the corresponding DataModels."""
163163
for _, path_data in module.items():
164-
path_key = '%s%s' % (
165-
path_data['base_module'],
166-
path_data['xpath']
167-
)
164+
path_key = path_data['machine_id']
168165
path_node = None
169166
if path_key in dp_cache.keys():
170167
path_node = dp_cache[path_key]
171168
else:
172169
path_node = db['DataPath'].createDocument({
173170
'machine_id': path_key,
174-
'human_id': path_data['cisco_xpath'],
171+
'human_id': path_data['xpath'],
175172
'description': path_data['description'],
176173
'is_leaf': False if path_data['children'] else True,
177174
'is_variable': False,

etl/src/yang/yang_base.py

+7-13
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,14 @@ def parse_versions(self):
6161
for module_key, module_revision in modules.items():
6262
module_data = version_data[module_key] = {}
6363
for revision_key, module in module_revision.items():
64-
module_data[revision_key] = self.parse_module_oper_attrs(module, module)
64+
module_data[revision_key] = self.parse_module_oper_attrs(module)
6565
logging.debug('Parsed %d revision(s) for %s.', len(module_data.keys()), module_key)
6666
logging.debug('Parsed %d module(s) for %s.', len(version_data.keys()), version)
6767
logging.debug('Parsed %d version(s).', len(version_module_map.keys()))
6868
return version_module_map
6969

70-
def parse_module_oper_attrs(self, module, base_module):
70+
def parse_module_oper_attrs(self, module):
7171
"""Parse out the readable DataPaths from parsed data models.
72-
TODO: Validate the i_config filtering with pyang.
7372
"""
7473
if not hasattr(module, 'i_children'):
7574
return {}
@@ -80,19 +79,14 @@ def parse_module_oper_attrs(self, module, base_module):
8079
parsed_modules = {}
8180
for child in module_children:
8281
attr_dict = {
83-
'base_module': base_module.arg,
84-
'xpath': yang_parser.get_xpath(child, with_prefixes=True),
85-
'cisco_xpath': yang_parser.get_cisco_xpath(child, base_module),
82+
'machine_id': '/%s' % ('/'.join(map(lambda x: ':'.join(x), yang_parser.mk_path_list(child)))),
83+
'qualified_xpath': yang_parser.get_xpath(child, qualified=True, prefix_to_module=True),
84+
'xpath': yang_parser.get_xpath(child, prefix_to_module=True),
8685
'type': yang_parser.get_qualified_type(child),
8786
'primitive_type': yang_parser.get_primitive_type(child),
8887
'rw': True if getattr(child, 'i_config', False) else False,
8988
'description': yang_parser.get_description(child),
90-
'children': self.parse_module_oper_attrs(child, base_module)
89+
'children': self.parse_module_oper_attrs(child)
9190
}
92-
# Qualify based on module and xpath, not cisco_xpath.
93-
# Would not account for derivation/augmentations.
94-
parsed_modules[str((
95-
attr_dict['base_module'],
96-
attr_dict['xpath']
97-
))] = attr_dict
91+
parsed_modules[attr_dict['machine_id']] = attr_dict
9892
return parsed_modules

etl/src/yang/yang_parser.py

+55-22
Original file line numberDiff line numberDiff line change
@@ -93,31 +93,64 @@ def get_filtered_modules(context, filter_pattern):
9393
filtered_modules[module_name] = revs
9494
return filtered_modules
9595

96-
def get_cisco_xpath(module, base_module):
97-
"""Generate the Cisco XPath representation.
98-
Cleaner but less absolute.
96+
def mk_path_list(stmt):
97+
"""Derives a list of tuples containing
98+
(module name, prefix, xpath)
99+
per node in the statement.
99100
"""
100-
module_name = base_module.arg
101-
no_prefix_xpath = get_xpath(module, with_prefixes=False)
102-
cisco_xpath = '%s:%s' % (module_name, no_prefix_xpath[1:])
103-
return cisco_xpath
104-
105-
def mk_path_str(stmt, with_prefixes=False):
106-
"""Returns the XPath path of the node"""
107-
if stmt.keyword in ['choice', 'case']:
108-
return mk_path_str(stmt.parent, with_prefixes)
109-
def name(stmt):
110-
if with_prefixes:
111-
return '%s:%s' % (stmt.i_module.i_prefix, stmt.arg)
101+
resolved_names = []
102+
def resolve_stmt(stmt, resolved_names):
103+
if stmt.keyword in ['choice', 'case']:
104+
resolve_stmt(stmt.parent, resolved_names)
105+
return
106+
def qualified_name_elements(stmt):
107+
"""(module name, prefix, name)"""
108+
return (stmt.i_module.arg, stmt.i_module.i_prefix, stmt.arg)
109+
if stmt.parent.keyword in ['module', 'submodule']:
110+
resolved_names.append(qualified_name_elements(stmt))
111+
return
112112
else:
113-
return stmt.arg
114-
if stmt.parent.keyword in ['module', 'submodule']:
115-
return '/%s' % name(stmt)
116-
else:
117-
xpath = mk_path_str(stmt.parent, with_prefixes)
118-
return '%s/%s' % (xpath, name(stmt))
113+
resolve_stmt(stmt.parent, resolved_names)
114+
resolved_names.append(qualified_name_elements(stmt))
115+
return
116+
resolve_stmt(stmt, resolved_names)
117+
return resolved_names
119118

120-
get_xpath = mk_path_str
119+
def mk_path_str(stmt, with_prefixes=False, prefix_onchange=False, prefix_to_module=False, resolve_top_prefix_to_module=False):
120+
"""Returns the XPath path of the node.
121+
with_prefixes indicates whether or not to prefix every node.
122+
prefix_onchange modifies the behavior of with_prefixes and only adds prefixes when the prefix changes mid-XPath.
123+
prefix_to_module replaces prefixes with the module name of the prefix.
124+
resolve_top_prefix_to_module resolves the module-level prefix to the module name.
125+
Prefixes may be included in the path if the prefix changes mid-path.
126+
"""
127+
resolved_names = mk_path_list(stmt)
128+
xpath_elements = []
129+
last_prefix = None
130+
for index, resolved_name in enumerate(resolved_names):
131+
module_name, prefix, node_name = resolved_name
132+
xpath_element = node_name
133+
if with_prefixes or (prefix_onchange and prefix != last_prefix):
134+
new_prefix = prefix
135+
if prefix_to_module or (index == 0 and resolve_top_prefix_to_module):
136+
new_prefix = module_name
137+
xpath_element = '%s:%s' % (new_prefix, node_name)
138+
xpath_elements.append(xpath_element)
139+
last_prefix = prefix
140+
return '/%s' % '/'.join(xpath_elements)
141+
142+
def get_xpath(stmt, qualified=False, prefix_to_module=False):
143+
"""Gets the XPath of the statement.
144+
Unless qualified=True, does not include prefixes unless the prefix changes mid-XPath.
145+
qualified will add a prefix to each node.
146+
prefix_to_module will resolve prefixes to module names instead.
147+
For RFC 8040, set prefix_to_module=True.
148+
/prefix:root/node/prefix:node/...
149+
qualified=True: /prefix:root/prefix:node/prefix:node/...
150+
qualified=True, prefix_to_module=True: /module:root/module:node/module:node/...
151+
prefix_to_module=True: /module:root/node/module:node/...
152+
"""
153+
return mk_path_str(stmt, with_prefixes=qualified, prefix_onchange=True, prefix_to_module=prefix_to_module)
121154

122155
def get_type(stmt):
123156
"""Gets the immediate, top-level type of the node.

migrate/1_to_2/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
yang/
2+
*.json

migrate/1_to_2/Pipfile

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[[source]]
2+
name = "pypi"
3+
url = "https://pypi.org/simple"
4+
verify_ssl = true
5+
6+
[dev-packages]
7+
8+
[packages]
9+
pyang = "==1.7.5"
10+
11+
[requires]
12+
python_version = "3.7"

migrate/1_to_2/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# v1.x to 2.0
2+
This migration is purely for migrating mappings within TDM from v1.x to v2.0 format. v2.0 expresses both modules and prefixes in the `machine_id` and breaks v1.x mapping portability. This script will translate v1.x mapping `machine_id` fields to the v2.x format.

migrate/1_to_2/mappings.py

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#!/usr/bin/env python
2+
"""Copyright 2019 Cisco Systems
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
"""Extremely ugly migration."""
17+
import logging
18+
import json
19+
import yang_parser
20+
21+
def parse_repository(repo_path):
22+
modules = yang_parser.parse_repository(repo_path)
23+
parsed_modules = {}
24+
for module_key, module_revision in modules.items():
25+
revision_with_data = set()
26+
for revision_key, module in module_revision.items():
27+
revision_data = None
28+
try:
29+
revision_data = __parse_node_attrs(module, module)
30+
except Exception:
31+
logging.exception("Failure while parsing %s!", module_key)
32+
if not revision_data:
33+
logging.debug("%s@%s is empty.", module_key, revision_key)
34+
continue
35+
revision_with_data.add("%s@%s" % (module_key, revision_key))
36+
if module_key in parsed_modules.keys() and parsed_modules[module_key]:
37+
logging.warn(
38+
"%s being replaced with %s@%s! Only one revision should be used.",
39+
module_key,
40+
module_key,
41+
revision_key,
42+
)
43+
parsed_modules[module_key] = revision_data
44+
if revision_with_data:
45+
logging.debug("%s have data.", ", ".join(revision_with_data))
46+
else:
47+
logging.debug("%s has no revisions with data.", module_key)
48+
return parsed_modules
49+
50+
51+
def __parse_node_attrs(node, base_module):
52+
if not hasattr(node, "i_children"):
53+
return {}
54+
children = (
55+
child
56+
for child in node.i_children
57+
if child.keyword in yang_parser.statements.data_definition_keywords
58+
)
59+
parsed_children = {}
60+
multi_map = {}
61+
for child in children:
62+
qualified_xpath = yang_parser.get_xpath(
63+
child, qualified=True, prefix_to_module=True
64+
)
65+
if qualified_xpath in parsed_children.keys():
66+
logging.debug("%s encountered more than once! Muxing." % qualified_xpath)
67+
if qualified_xpath not in multi_map.keys():
68+
multi_map[qualified_xpath] = 0
69+
multi_map[qualified_xpath] += 1
70+
qualified_xpath += "_%i" % multi_map[qualified_xpath]
71+
attr_dict = {
72+
'1_machine_id': '%s%s' % (base_module.arg, yang_parser.old_get_xpath(child, with_prefixes=True)),
73+
'2_machine_id': '/%s' % ('/'.join(map(lambda x: ':'.join(x), yang_parser.mk_path_list(child)))),
74+
'children': __parse_node_attrs(child, base_module)
75+
}
76+
parsed_children[qualified_xpath] = attr_dict
77+
return parsed_children
78+
79+
logging.basicConfig(level=logging.INFO)
80+
81+
upgrade_map = {}
82+
83+
# Any paths come out missing, go to instance of TDM
84+
# Datapath Direct the path and add path to version folder
85+
repository_paths = [
86+
'yang/vendor/cisco/nx/9.3-1',
87+
'yang/vendor/cisco/xe/16111',
88+
'yang/vendor/cisco/xr/602',
89+
'yang/vendor/cisco/xr/631',
90+
'yang/vendor/cisco/xr/662'
91+
]
92+
93+
found = 0
94+
missing = 0
95+
96+
def upgrade(key):
97+
if key in upgrade_map.keys():
98+
global found
99+
found += 1
100+
return upgrade_map[key]
101+
else:
102+
global missing
103+
if not key.startswith('1.'):
104+
missing += 1
105+
logging.warning('%s not in upgrade map!', key)
106+
return key
107+
108+
for repo in repository_paths:
109+
logging.info('Parsing %s ...', repo)
110+
modules = parse_repository(repo)
111+
def recurse_nodes(node_tree):
112+
for node_element in node_tree.values():
113+
upgrade_map[node_element['1_machine_id']] = node_element['2_machine_id']
114+
recurse_nodes(node_element['children'])
115+
module_top_children = {}
116+
for top_children in modules.values():
117+
module_top_children.update(top_children)
118+
recurse_nodes(module_top_children)
119+
tdm_mappings = None
120+
with open('tdm_mappings.json', 'r') as mappings_fd:
121+
tdm_mappings = json.load(mappings_fd)
122+
logging.info('Upgrading DataPathMatch ...')
123+
for match in tdm_mappings['DataPathMatch']:
124+
match['_from'] = upgrade(match['_from'])
125+
match['_to'] = upgrade(match['_to'])
126+
logging.info('%i found, %i missing.', found, missing)
127+
found = 0
128+
missing = 0
129+
logging.info('Upgrading Calculation ...')
130+
for calculation in tdm_mappings['Calculation']:
131+
new_in_calculation = []
132+
for path in calculation['InCalculation']:
133+
new_in_calculation.append(upgrade(path))
134+
calculation['InCalculation'] = new_in_calculation
135+
new_result = []
136+
for path in calculation['CalculationResult']:
137+
new_result.append(upgrade(path))
138+
calculation['CalculationResult'] = new_result
139+
logging.info('%i found, %i missing.', found, missing)
140+
with open('upgraded_tdm_mappings.json', 'w') as upgraded_fd:
141+
json.dump(tdm_mappings, upgraded_fd, indent=4, sort_keys=True)

migrate/1_to_2/run.sh

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env bash
2+
pipenv run python mappings.py

migrate/1_to_2/setup.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env bash
2+
git clone https://github.com/YangModels/yang
3+
pipenv --three install

0 commit comments

Comments
 (0)