# Copyright (C) 2020 VyOS maintainers and contributors # # This library is free software; you can redistribute it and/or modify it under the terms of # the GNU Lesser General Public License as published by the Free Software Foundation; # either version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License along with this library; # if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from vyos.xml import kw # As we index by key, the name is first and then the data: # {'dummy': { # '[node]': '[tagNode]', # 'address': { ... } # } } # so when we encounter a tagNode, we are really encountering # the tagNode data. class XML(dict): def __init__(self): self[kw.tree] = {} self[kw.priorities] = {} self[kw.owners] = {} self[kw.default] = {} self[kw.tags] = [] self[kw.component_version] = {} dict.__init__(self) self.tree = self[kw.tree] # the options which matched the last incomplete world we had # or the last word in a list self.options = [] # store all the part of the command we processed self.inside = [] # should we check the data pass with the constraints self.check = False # are we still typing a word self.filling = False # do what have the tagNode value ? self.filled = False # last word seen self.word = '' # do we have all the data we want ? self.final = False # do we have too much data ? self.extra = False # what kind of node are we in plain vs data not self.plain = True def reset(self): self.tree = self[kw.tree] self.options = [] self.inside = [] self.check = False self.filling = False self.filled = False self.word = '' self.final = False self.extra = False self.plain = True # from functools import lru_cache # @lru_cache(maxsize=100) # XXX: need to use cachetool instead - for later def traverse(self, cmd): self.reset() # using split() intead of split(' ') eats the final ' ' words = cmd.split(' ') passed = [] word = '' data_node = False space = False while words: word = words.pop(0) space = word == '' perfect = False if word in self.tree: passed = [] perfect = True self.tree = self.tree[word] data_node = self.tree[kw.node] self.inside.append(word) word = '' continue if word and data_node: passed.append(word) is_valueless = self.tree.get(kw.valueless, False) is_leafNode = data_node == kw.leafNode is_dataNode = data_node in (kw.leafNode, kw.tagNode) named_options = [_ for _ in self.tree if not kw.found(_)] if is_leafNode: self.final = is_valueless or len(passed) > 0 self.extra = is_valueless and len(passed) > 0 self.check = len(passed) >= 1 else: self.final = False self.extra = False self.check = len(passed) == 1 and not space if self.final: self.word = ' '.join(passed) else: self.word = word if self.final: self.filling = True else: self.filling = not perfect and bool(cmd and word != '') self.filled = self.final or (is_dataNode and len(passed) > 0 and word == '') if is_dataNode and len(passed) == 0: self.options = [] elif word: if data_node != kw.plainNode or len(passed) == 1: self.options = [_ for _ in self.tree if _.startswith(word)] self.options.sort() else: self.options = [] else: self.options = named_options self.options.sort() self.plain = not is_dataNode # self.debug() return self.word def speculate(self): if len(self.options) == 1: self.tree = self.tree[self.options[0]] self.word = '' if self.tree.get(kw.node,'') not in (kw.tagNode, kw.leafNode): self.options = [_ for _ in self.tree if not kw.found(_)] self.options.sort() def checks(self, cmd): # as we move thought the named node twice # the first time we get the data with the node # and the second with the pass parameters xml = self[kw.tree] words = cmd.split(' ') send = True last = [] while words: word = words.pop(0) if word in xml: xml = xml[word] send = True last = [] continue if xml[kw.node] in (kw.tagNode, kw.leafNode): if kw.constraint in xml: if send: yield (word, xml[kw.constraint]) send = False else: last.append((word, None)) if len(last) >= 2: yield last[0] def summary(self): yield ('enter', '[ summary ]', str(self.inside)) if kw.help not in self.tree: yield ('skip', '[ summary ]', str(self.inside)) return if self.filled: return yield('', '', '\nHelp:') if kw.help in self.tree: summary = self.tree[kw.help].get(kw.summary) values = self.tree[kw.help].get(kw.valuehelp, []) if summary: yield(summary, '', '') for value in values: yield(value[kw.format], value[kw.description], '') def constraint(self): yield ('enter', '[ constraint ]', str(self.inside)) if kw.help in self.tree: yield ('skip', '[ constraint ]', str(self.inside)) return if kw.error not in self.tree: yield ('skip', '[ constraint ]', str(self.inside)) return if not self.word or self.filling: yield ('skip', '[ constraint ]', str(self.inside)) return yield('', '', '\nData Constraint:') yield('', 'constraint', str(self.tree[kw.error])) def listing(self): yield ('enter', '[ listing ]', str(self.inside)) # only show the details when we passed the tagNode data if not self.plain and not self.filled: yield ('skip', '[ listing ]', str(self.inside)) return yield('', '', '\nPossible completions:') options = list(self.tree.keys()) options.sort() for option in options: if kw.found(option): continue if not option.startswith(self.word): continue inner = self.tree[option] prefix = '+> ' if inner.get(kw.node, '') != kw.leafNode else ' ' if kw.help in inner: yield (prefix + option, inner[kw.help].get(kw.summary), '') else: yield (prefix + option, '(no help available)', '') def debug(self): print('------') print("word '%s'" % self.word) print("filling " + str(self.filling)) print("filled " + str(self.filled)) print("final " + str(self.final)) print("extra " + str(self.extra)) print("plain " + str(self.plain)) print("options " + str(self.options)) # from functools import lru_cache # @lru_cache(maxsize=100) # XXX: need to use cachetool instead - for later def component_versions(self) -> dict: sort_component = sorted(self[kw.component_version].items(), key = lambda kv: kv[0]) return dict(sort_component) def defaults(self, lpath, flat): d = self[kw.default] for k in lpath: d = d.get(k, {}) if not flat: # _flatten will make this conversion d = self.multi_to_list(lpath, d, defaults=True) r = {} for k in d: under = k.replace('-','_') if isinstance(d[k],dict): r[under] = self.defaults(lpath + [k], flat) continue r[under] = d[k] return r def _flatten(inside, index, d): r = {} local = inside[index:] prefix = '_'.join(_.replace('-','_') for _ in local) + '_' if local else '' for k in d: under = prefix + k.replace('-','_') level = inside + [k] if isinstance(d[k],dict): r.update(_flatten(level, index, d[k])) continue if self.is_multi(level, with_tag=False): r[under] = [_.strip() for _ in d[k].split(',')] continue r[under] = d[k] return r return _flatten(lpath, len(lpath), d) def multi_to_list(self, lpath, conf, defaults=False): r = {} for k in conf: # key mangling could also be done here # it would prevent two parsing of the config tree # under = k.replace('-','_') under = k fpath = lpath + [k] if isinstance(conf[k],dict): r[under] = self.multi_to_list(fpath, conf[k], defaults) continue value = conf[k] if self.is_multi(fpath) and not isinstance(value, list): if not defaults: value = [value] else: value = value.split(' ') r[under] = value return r # from functools import lru_cache # @lru_cache(maxsize=100) # XXX: need to use cachetool instead - for later def _tree(self, lpath, with_tag=True): """ returns the part of the tree searched or None if it does not exists if with_tag is set, this is a configuration path (with tagNode names) and tag name will be removed from the path when traversing the tree """ tree = self[kw.tree] spath = lpath.copy() while spath: p = spath.pop(0) if p not in tree: return None tree = tree[p] if with_tag and spath and tree[kw.node] == kw.tagNode: spath.pop(0) return tree def _get(self, lpath, tag, with_tag=True): tree = self._tree(lpath, with_tag) if tree is None: return None return tree.get(tag, None) def is_multi(self, lpath, with_tag=True): tree = self._get(lpath, kw.multi, with_tag) if tree is None: return None return tree is True def is_tag(self, lpath, with_tag=True): tree = self._get(lpath, kw.node, with_tag) if tree is None: return None return tree == kw.tagNode def is_leaf(self, lpath, with_tag=True): tree = self._get(lpath, kw.node, with_tag) if tree is None: return None return tree == kw.leafNode def exists(self, lpath, with_tag=True): return self._get(lpath, kw.node, with_tag) is not None