Skip to content

Commit a4f83d2

Browse files
committed
Merge branch 'develop'
2 parents 2de7510 + 8ef01a1 commit a4f83d2

File tree

5 files changed

+646
-73
lines changed

5 files changed

+646
-73
lines changed

Makefile

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,22 @@ help:
8484
helpMessage = ""; \
8585
} \
8686
}' \
87-
$(MAKEFILE_LIST)
87+
$(MAKEFILE_LIST)
88+
89+
## Setup links in virtual env for development
90+
develop:
91+
@echo "--------------------------------------------------------------------"
92+
@echo "Setting up development environment"
93+
@python setup.py develop -q
94+
@echo ""
95+
@echo "Done."
96+
@echo ""
97+
98+
## Remove development links in virtual env
99+
undevelop:
100+
@echo "--------------------------------------------------------------------"
101+
@echo "Removing development environment"
102+
@python setup.py develop -q --uninstall
103+
@echo ""
104+
@echo "Done."
105+
@echo ""

src/cisco_gnmi/client.py

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@
2424
"""Python gNMI wrapper to ease usage of gNMI."""
2525

2626
import logging
27+
from collections import OrderedDict
2728
from xml.etree.ElementPath import xpath_tokenizer_re
29+
import re
30+
import json
31+
import os
2832
from six import string_types
2933

3034
from . import proto
@@ -152,7 +156,9 @@ def get(
152156
"encoding", encoding, "Encoding", proto.gnmi_pb2.Encoding
153157
)
154158
request = proto.gnmi_pb2.GetRequest()
155-
if not isinstance(paths, (list, set)):
159+
try:
160+
iter(paths)
161+
except TypeError:
156162
raise Exception("paths must be an iterable containing Path(s)!")
157163
request.path.extend(paths)
158164
request.type = data_type
@@ -334,3 +340,172 @@ def parse_xpath_to_gnmi_path(self, xpath, origin=None):
334340
raise Exception("Unfinished elements in XPath parsing!")
335341
path.elem.extend(path_elems)
336342
return path
343+
344+
def combine_configs(self, payload, last_xpath, xpath, config):
345+
last_set = set(last_xpath.split('/'))
346+
curr_diff = set(xpath.split('/')) - last_set
347+
if len(curr_diff) > 1:
348+
print('combine_configs() error1')
349+
return payload
350+
index = curr_diff.pop()
351+
curr_xpath = xpath[xpath.find(index):]
352+
curr_xpath = curr_xpath.split('/')
353+
curr_xpath.reverse()
354+
for seg in curr_xpath:
355+
config = {seg: config}
356+
357+
last_diff = last_set - set(xpath.split('/'))
358+
if len(last_diff) > 1:
359+
print('combine_configs() error2')
360+
return payload
361+
last_xpath = last_xpath[last_xpath.find(last_diff.pop()):]
362+
last_xpath = last_xpath.split('/')
363+
last_xpath.reverse()
364+
for seg in last_xpath:
365+
if seg not in payload:
366+
payload = {seg: payload}
367+
payload.update(config)
368+
return payload
369+
370+
def xpath_to_json(self, configs, last_xpath='', payload={}):
371+
for i, cfg in enumerate(configs, 1):
372+
xpath, config, is_key = cfg
373+
if last_xpath and xpath not in last_xpath:
374+
# Branched config here
375+
# |---last xpath config
376+
# --|
377+
# |---this xpath config
378+
payload = self.combine_configs(payload, last_xpath, xpath, config)
379+
return self.xpath_to_json(configs[i:], xpath, payload)
380+
xpath_segs = xpath.split('/')
381+
xpath_segs.reverse()
382+
for seg in xpath_segs:
383+
if not seg:
384+
continue
385+
if payload:
386+
if is_key:
387+
if seg in payload:
388+
if isinstance(payload[seg], list):
389+
payload[seg].append(config)
390+
elif isinstance(payload[seg], dict):
391+
payload[seg].update(config)
392+
else:
393+
payload.update(config)
394+
payload = {seg: [payload]}
395+
else:
396+
config.update(payload)
397+
payload = {seg: config}
398+
return self.xpath_to_json(configs[i:], xpath, payload)
399+
else:
400+
if is_key:
401+
payload = {seg: [config]}
402+
else:
403+
payload = {seg: config}
404+
return self.xpath_to_json(configs[i:], xpath, payload)
405+
return payload
406+
407+
# Pattern to detect keys in an xpath
408+
RE_FIND_KEYS = re.compile(r'\[.*?\]')
409+
410+
def get_payload(self, configs):
411+
# Number of updates are limited so try to consolidate into lists.
412+
xpaths_cfg = []
413+
first_key = set()
414+
# Find first common keys for all xpaths_cfg of collection.
415+
for config in configs:
416+
xpath = next(iter(config.keys()))
417+
418+
# Change configs to tuples (xpath, config) for easier management
419+
xpaths_cfg.append((xpath, config[xpath]))
420+
421+
xpath_split = xpath.split('/')
422+
for seg in xpath_split:
423+
if '[' in seg:
424+
first_key.add(seg)
425+
break
426+
427+
# Common first key/configs represents one GNMI update
428+
updates = []
429+
for key in first_key:
430+
update = []
431+
remove_cfg = []
432+
for config in xpaths_cfg:
433+
xpath, cfg = config
434+
if key in xpath:
435+
update.append(config)
436+
else:
437+
for k, v in cfg.items():
438+
if '[{0}="{1}"]'.format(k, v) not in key:
439+
break
440+
else:
441+
# This cfg sets the first key so we don't need it
442+
remove_cfg.append((xpath, cfg))
443+
if update:
444+
for upd in update:
445+
# Remove this config out of main list
446+
xpaths_cfg.remove(upd)
447+
for rem_cfg in remove_cfg:
448+
# Sets a key in update path so remove it
449+
xpaths_cfg.remove(rem_cfg)
450+
updates.append(update)
451+
break
452+
453+
# Add remaining configs to updates
454+
if xpaths_cfg:
455+
updates.append(xpaths_cfg)
456+
457+
# Combine all xpath configs of each update if possible
458+
xpaths = []
459+
compressed_updates = []
460+
for update in updates:
461+
xpath_consolidated = {}
462+
config_compressed = []
463+
for seg in update:
464+
xpath, config = seg
465+
if xpath in xpath_consolidated:
466+
xpath_consolidated[xpath].update(config)
467+
else:
468+
xpath_consolidated[xpath] = config
469+
config_compressed.append((xpath, xpath_consolidated[xpath]))
470+
xpaths.append(xpath)
471+
472+
# Now get the update path for this batch of configs
473+
common_xpath = os.path.commonprefix(xpaths)
474+
cfg_compressed = []
475+
keys = []
476+
477+
# Need to reverse the configs to build the dict correctly
478+
config_compressed.reverse()
479+
for seg in config_compressed:
480+
is_key = False
481+
prepend_path = ''
482+
xpath, config = seg
483+
end_path = xpath[len(common_xpath):]
484+
if end_path.startswith('['):
485+
# Don't start payload with a list
486+
tmp = common_xpath.split('/')
487+
prepend_path = '/' + tmp.pop()
488+
common_xpath = '/'.join(tmp)
489+
end_path = prepend_path + end_path
490+
491+
# Building json, need to identify configs that set keys
492+
for key in keys:
493+
if [k for k in config.keys() if k in key]:
494+
is_key = True
495+
keys += re.findall(self.RE_FIND_KEYS, end_path)
496+
cfg_compressed.append((end_path, config, is_key))
497+
498+
update = (common_xpath, cfg_compressed)
499+
compressed_updates.append(update)
500+
501+
updates = []
502+
for update in compressed_updates:
503+
common_xpath, cfgs = update
504+
payload = self.xpath_to_json(cfgs)
505+
updates.append(
506+
(
507+
common_xpath,
508+
json.dumps(payload).encode('utf-8')
509+
)
510+
)
511+
return updates

0 commit comments

Comments
 (0)