Network Module: EOS (#16158)

* add new module network

* move EOS to NetworkModule

* shell.py Python 3.x compatibility

* implements the Command class through the connection for eos

This implements a new Command class that specifies the cli command
and output format.  This removes the need to batch commands through
the connection

* initial add of netcmd module
This commit is contained in:
Nathaniel Case 2016-06-20 12:11:48 -04:00 committed by GitHub
commit 5dccff29bf
5 changed files with 933 additions and 335 deletions

View file

@ -18,11 +18,14 @@
#
import re
import time
import collections
import itertools
import shlex
import itertools
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.network import to_list
DEFAULT_COMMENT_TOKENS = ['#', '!']
@ -34,6 +37,13 @@ class ConfigLine(object):
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
@ -49,16 +59,20 @@ def ignore_line(text, tokens=None):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
repl = r'([{|}|;])'
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(repl, '', line)).strip()
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
@ -108,11 +122,23 @@ class NetworkConfig(object):
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
config = collections.OrderedDict()
for item in self._config:
self.expand(item, config)
return '\n'.join(self.flatten(config))
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
@ -167,6 +193,45 @@ class NetworkConfig(object):
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None):
if S is None:
S = list()
S.append(configobj)
for child in configobj.children:
if child in S:
continue
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
@ -237,155 +302,83 @@ class NetworkConfig(object):
return self.flatten(diffs)
def _build_children(self, children, parents=None, offset=0):
for item in children:
line = ConfigLine(item)
line.raw = item.rjust(len(item) + offset)
if parents:
line.parents = parents
parents[-1].children.append(line)
yield line
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
string = ignore_whitespace is True and item.text or item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None):
"""Adds one or lines of configuration
"""
ancestors = list()
offset = 0
obj = None
config = list()
parent = None
parents = parents or list()
## global config command
if not parents:
for line in to_list(lines):
item = ConfigLine(line)
item.raw = line
if item not in self.items:
self.items.append(item)
for item in parents:
line = ConfigLine(item)
line.raw = item.rjust(len(item) + offset)
config.append(line)
if parent:
parent.children.append(line)
if parent.parents:
line.parents.append(*parent.parents)
line.parents.append(parent)
parent = line
offset += self.indent
self._config.extend(config)
self._config.extend(list(self._build_children(lines, config, offset)))
class Conditional(object):
"""Used in command modules to evaluate waitfor conditions
"""
OPERATORS = {
'eq': ['eq', '=='],
'neq': ['neq', 'ne', '!='],
'gt': ['gt', '>'],
'ge': ['ge', '>='],
'lt': ['lt', '<'],
'le': ['le', '<='],
'contains': ['contains']
}
def __init__(self, conditional, encoding='json'):
self.raw = conditional
self.encoding = encoding
key, op, val = shlex.split(conditional)
self.key = key
self.func = self.func(op)
self.value = self._cast_value(val)
def __call__(self, data):
value = self.get_value(dict(result=data))
return self.func(value)
def _cast_value(self, value):
if value in BOOLEANS_TRUE:
return True
elif value in BOOLEANS_FALSE:
return False
elif re.match(r'^\d+\.d+$', value):
return float(value)
elif re.match(r'^\d+$', value):
return int(value)
else:
return unicode(value)
def func(self, oper):
for func, operators in self.OPERATORS.items():
if oper in operators:
return getattr(self, func)
raise AttributeError('unknown operator: %s' % oper)
def get_value(self, result):
if self.encoding in ['json', 'text']:
return self.get_json(result)
elif self.encoding == 'xml':
return self.get_xml(result.get('result'))
def get_xml(self, result):
parts = self.key.split('.')
value_index = None
match = re.match(r'^\S+(\[)(\d+)\]', parts[-1])
if match:
start, end = match.regs[1]
parts[-1] = parts[-1][0:start]
value_index = int(match.group(2))
path = '/'.join(parts[1:])
path = '/%s' % path
path += '/text()'
index = int(re.match(r'result\[(\d+)\]', parts[0]).group(1))
values = result[index].xpath(path)
if value_index is not None:
return values[value_index].strip()
return [v.strip() for v in values]
def get_json(self, result):
parts = re.split(r'\.(?=[^\]]*(?:\[|$))', self.key)
for part in parts:
match = re.findall(r'\[(\S+?)\]', part)
if match:
key = part[:part.find('[')]
result = result[key]
for m in match:
try:
m = int(m)
except ValueError:
m = str(m)
result = result[m]
else:
result = result.get(part)
return result
def number(self, value):
if '.' in str(value):
return float(value)
else:
return int(value)
def eq(self, value):
return value == self.value
def neq(self, value):
return value != self.value
def gt(self, value):
return self.number(value) > self.value
def ge(self, value):
return self.number(value) >= self.value
def lt(self, value):
return self.number(value) < self.value
def le(self, value):
return self.number(value) <= self.value
def contains(self, value):
return str(self.value) in value
for index, p in enumerate(parents):
try:
i = index + 1
obj = self.get_section_objects(parents[:i])[0]
ancestors.append(obj)
except ValueError:
# add parent to config
offset = index * self.indent
obj = ConfigLine(p)
obj.raw = p.rjust(len(p) + offset)
if ancestors:
obj.parents = list(ancestors)
ancestors[-1].children.append(obj)
self.items.append(obj)
ancestors.append(obj)
# add child objects
for line in to_list(lines):
# check if child already exists
for child in ancestors[-1].children:
if child.text == line:
break
else:
offset = len(parents) * self.indent
item = ConfigLine(line)
item.raw = line.rjust(len(line) + offset)
item.parents = ancestors
ancestors[-1].children.append(item)
self.items.append(item)