mirror of https://github.com/ansible/ansible.git
Migrated to ansible.netcommon
parent
6d910034a0
commit
9a7a574ffc
File diff suppressed because it is too large
Load Diff
@ -1,24 +0,0 @@
|
|||||||
#
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2019 Red Hat
|
|
||||||
# GNU General Public License v3.0+
|
|
||||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
"""
|
|
||||||
The base class for all resource modules
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ansible.module_utils.network.common.network import get_resource_connection
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigBase(object):
|
|
||||||
""" The base class for all resource modules
|
|
||||||
"""
|
|
||||||
ACTION_STATES = ['merged', 'replaced', 'overridden', 'deleted']
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
self._module = module
|
|
||||||
self.state = module.params['state']
|
|
||||||
self._connection = None
|
|
||||||
|
|
||||||
if self.state not in ['rendered', 'parsed']:
|
|
||||||
self._connection = get_resource_connection(module)
|
|
@ -1,468 +0,0 @@
|
|||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# (c) 2016 Red Hat Inc.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
#
|
|
||||||
import re
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from ansible.module_utils.six.moves import zip
|
|
||||||
from ansible.module_utils._text import to_bytes, to_native
|
|
||||||
|
|
||||||
DEFAULT_COMMENT_TOKENS = ['#', '!', '/*', '*/', 'echo']
|
|
||||||
|
|
||||||
DEFAULT_IGNORE_LINES_RE = set([
|
|
||||||
re.compile(r"Using \d+ out of \d+ bytes"),
|
|
||||||
re.compile(r"Building configuration"),
|
|
||||||
re.compile(r"Current configuration : \d+ bytes")
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
Pattern = re._pattern_type
|
|
||||||
except AttributeError:
|
|
||||||
Pattern = re.Pattern
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigLine(object):
|
|
||||||
|
|
||||||
def __init__(self, raw):
|
|
||||||
self.text = str(raw).strip()
|
|
||||||
self.raw = raw
|
|
||||||
self._children = list()
|
|
||||||
self._parents = list()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.raw
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.line == other.line
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not self.__eq__(other)
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
for item in self._children:
|
|
||||||
if item.text == key:
|
|
||||||
return item
|
|
||||||
raise KeyError(key)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def line(self):
|
|
||||||
line = self.parents
|
|
||||||
line.append(self.text)
|
|
||||||
return ' '.join(line)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def children(self):
|
|
||||||
return _obj_to_text(self._children)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def child_objs(self):
|
|
||||||
return self._children
|
|
||||||
|
|
||||||
@property
|
|
||||||
def parents(self):
|
|
||||||
return _obj_to_text(self._parents)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self):
|
|
||||||
config = _obj_to_raw(self._parents)
|
|
||||||
config.append(self.raw)
|
|
||||||
return '\n'.join(config)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_children(self):
|
|
||||||
return len(self._children) > 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_parents(self):
|
|
||||||
return len(self._parents) > 0
|
|
||||||
|
|
||||||
def add_child(self, obj):
|
|
||||||
if not isinstance(obj, ConfigLine):
|
|
||||||
raise AssertionError('child must be of type `ConfigLine`')
|
|
||||||
self._children.append(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def ignore_line(text, tokens=None):
|
|
||||||
for item in (tokens or DEFAULT_COMMENT_TOKENS):
|
|
||||||
if text.startswith(item):
|
|
||||||
return True
|
|
||||||
for regex in DEFAULT_IGNORE_LINES_RE:
|
|
||||||
if regex.match(text):
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _obj_to_text(x):
|
|
||||||
return [o.text for o in x]
|
|
||||||
|
|
||||||
|
|
||||||
def _obj_to_raw(x):
|
|
||||||
return [o.raw for o in x]
|
|
||||||
|
|
||||||
|
|
||||||
def _obj_to_block(objects, visited=None):
|
|
||||||
items = list()
|
|
||||||
for o in objects:
|
|
||||||
if o not in items:
|
|
||||||
items.append(o)
|
|
||||||
for child in o._children:
|
|
||||||
if child not in items:
|
|
||||||
items.append(child)
|
|
||||||
return _obj_to_raw(items)
|
|
||||||
|
|
||||||
|
|
||||||
def dumps(objects, output='block', comments=False):
|
|
||||||
if output == 'block':
|
|
||||||
items = _obj_to_block(objects)
|
|
||||||
elif output == 'commands':
|
|
||||||
items = _obj_to_text(objects)
|
|
||||||
elif output == 'raw':
|
|
||||||
items = _obj_to_raw(objects)
|
|
||||||
else:
|
|
||||||
raise TypeError('unknown value supplied for keyword output')
|
|
||||||
|
|
||||||
if output == 'block':
|
|
||||||
if comments:
|
|
||||||
for index, item in enumerate(items):
|
|
||||||
nextitem = index + 1
|
|
||||||
if nextitem < len(items) and not item.startswith(' ') and items[nextitem].startswith(' '):
|
|
||||||
item = '!\n%s' % item
|
|
||||||
items[index] = item
|
|
||||||
items.append('!')
|
|
||||||
items.append('end')
|
|
||||||
|
|
||||||
return '\n'.join(items)
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkConfig(object):
|
|
||||||
|
|
||||||
def __init__(self, indent=1, contents=None, ignore_lines=None):
|
|
||||||
self._indent = indent
|
|
||||||
self._items = list()
|
|
||||||
self._config_text = None
|
|
||||||
|
|
||||||
if ignore_lines:
|
|
||||||
for item in ignore_lines:
|
|
||||||
if not isinstance(item, Pattern):
|
|
||||||
item = re.compile(item)
|
|
||||||
DEFAULT_IGNORE_LINES_RE.add(item)
|
|
||||||
|
|
||||||
if contents:
|
|
||||||
self.load(contents)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def items(self):
|
|
||||||
return self._items
|
|
||||||
|
|
||||||
@property
|
|
||||||
def config_text(self):
|
|
||||||
return self._config_text
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sha1(self):
|
|
||||||
sha1 = hashlib.sha1()
|
|
||||||
sha1.update(to_bytes(str(self), errors='surrogate_or_strict'))
|
|
||||||
return sha1.digest()
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
for line in self:
|
|
||||||
if line.text == key:
|
|
||||||
return line
|
|
||||||
raise KeyError(key)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return iter(self._items)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return '\n'.join([c.raw for c in self.items])
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self._items)
|
|
||||||
|
|
||||||
def load(self, s):
|
|
||||||
self._config_text = s
|
|
||||||
self._items = self.parse(s)
|
|
||||||
|
|
||||||
def loadfp(self, fp):
|
|
||||||
with open(fp) as f:
|
|
||||||
return self.load(f.read())
|
|
||||||
|
|
||||||
def parse(self, lines, comment_tokens=None):
|
|
||||||
toplevel = re.compile(r'\S')
|
|
||||||
childline = re.compile(r'^\s*(.+)$')
|
|
||||||
entry_reg = re.compile(r'([{};])')
|
|
||||||
|
|
||||||
ancestors = list()
|
|
||||||
config = list()
|
|
||||||
|
|
||||||
indents = [0]
|
|
||||||
|
|
||||||
for linenum, line in enumerate(to_native(lines, errors='surrogate_or_strict').split('\n')):
|
|
||||||
text = entry_reg.sub('', line).strip()
|
|
||||||
|
|
||||||
cfg = ConfigLine(line)
|
|
||||||
|
|
||||||
if not text or ignore_line(text, comment_tokens):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# handle top level commands
|
|
||||||
if toplevel.match(line):
|
|
||||||
ancestors = [cfg]
|
|
||||||
indents = [0]
|
|
||||||
|
|
||||||
# handle sub level commands
|
|
||||||
else:
|
|
||||||
match = childline.match(line)
|
|
||||||
line_indent = match.start(1)
|
|
||||||
|
|
||||||
if line_indent < indents[-1]:
|
|
||||||
while indents[-1] > line_indent:
|
|
||||||
indents.pop()
|
|
||||||
|
|
||||||
if line_indent > indents[-1]:
|
|
||||||
indents.append(line_indent)
|
|
||||||
|
|
||||||
curlevel = len(indents) - 1
|
|
||||||
parent_level = curlevel - 1
|
|
||||||
|
|
||||||
cfg._parents = ancestors[:curlevel]
|
|
||||||
|
|
||||||
if curlevel > len(ancestors):
|
|
||||||
config.append(cfg)
|
|
||||||
continue
|
|
||||||
|
|
||||||
for i in range(curlevel, len(ancestors)):
|
|
||||||
ancestors.pop()
|
|
||||||
|
|
||||||
ancestors.append(cfg)
|
|
||||||
ancestors[parent_level].add_child(cfg)
|
|
||||||
|
|
||||||
config.append(cfg)
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
def get_object(self, path):
|
|
||||||
for item in self.items:
|
|
||||||
if item.text == path[-1]:
|
|
||||||
if item.parents == path[:-1]:
|
|
||||||
return item
|
|
||||||
|
|
||||||
def get_block(self, path):
|
|
||||||
if not isinstance(path, list):
|
|
||||||
raise AssertionError('path argument must be a list object')
|
|
||||||
obj = self.get_object(path)
|
|
||||||
if not obj:
|
|
||||||
raise ValueError('path does not exist in config')
|
|
||||||
return self._expand_block(obj)
|
|
||||||
|
|
||||||
def get_block_config(self, path):
|
|
||||||
block = self.get_block(path)
|
|
||||||
return dumps(block, 'block')
|
|
||||||
|
|
||||||
def _expand_block(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_block(child, S)
|
|
||||||
return S
|
|
||||||
|
|
||||||
def _diff_line(self, other):
|
|
||||||
updates = list()
|
|
||||||
for item in self.items:
|
|
||||||
if item not in other:
|
|
||||||
updates.append(item)
|
|
||||||
return updates
|
|
||||||
|
|
||||||
def _diff_strict(self, other):
|
|
||||||
updates = list()
|
|
||||||
# block extracted from other does not have all parents
|
|
||||||
# but the last one. In case of multiple parents we need
|
|
||||||
# to add additional parents.
|
|
||||||
if other and isinstance(other, list) and len(other) > 0:
|
|
||||||
start_other = other[0]
|
|
||||||
if start_other.parents:
|
|
||||||
for parent in start_other.parents:
|
|
||||||
other.insert(0, ConfigLine(parent))
|
|
||||||
for index, line in enumerate(self.items):
|
|
||||||
try:
|
|
||||||
if str(line).strip() != str(other[index]).strip():
|
|
||||||
updates.append(line)
|
|
||||||
except (AttributeError, IndexError):
|
|
||||||
updates.append(line)
|
|
||||||
return updates
|
|
||||||
|
|
||||||
def _diff_exact(self, other):
|
|
||||||
updates = list()
|
|
||||||
if len(other) != len(self.items):
|
|
||||||
updates.extend(self.items)
|
|
||||||
else:
|
|
||||||
for ours, theirs in zip(self.items, other):
|
|
||||||
if ours != theirs:
|
|
||||||
updates.extend(self.items)
|
|
||||||
break
|
|
||||||
return updates
|
|
||||||
|
|
||||||
def difference(self, other, match='line', path=None, replace=None):
|
|
||||||
"""Perform a config diff against the another network config
|
|
||||||
|
|
||||||
:param other: instance of NetworkConfig to diff against
|
|
||||||
:param match: type of diff to perform. valid values are 'line',
|
|
||||||
'strict', 'exact'
|
|
||||||
:param path: context in the network config to filter the diff
|
|
||||||
:param replace: the method used to generate the replacement lines.
|
|
||||||
valid values are 'block', 'line'
|
|
||||||
|
|
||||||
:returns: a string of lines that are different
|
|
||||||
"""
|
|
||||||
if path and match != 'line':
|
|
||||||
try:
|
|
||||||
other = other.get_block(path)
|
|
||||||
except ValueError:
|
|
||||||
other = list()
|
|
||||||
else:
|
|
||||||
other = other.items
|
|
||||||
|
|
||||||
# generate a list of ConfigLines that aren't in other
|
|
||||||
meth = getattr(self, '_diff_%s' % match)
|
|
||||||
updates = meth(other)
|
|
||||||
|
|
||||||
if replace == 'block':
|
|
||||||
parents = list()
|
|
||||||
for item in updates:
|
|
||||||
if not item.has_parents:
|
|
||||||
parents.append(item)
|
|
||||||
else:
|
|
||||||
for p in item._parents:
|
|
||||||
if p not in parents:
|
|
||||||
parents.append(p)
|
|
||||||
|
|
||||||
updates = list()
|
|
||||||
for item in parents:
|
|
||||||
updates.extend(self._expand_block(item))
|
|
||||||
|
|
||||||
visited = set()
|
|
||||||
expanded = list()
|
|
||||||
|
|
||||||
for item in updates:
|
|
||||||
for p in item._parents:
|
|
||||||
if p.line not in visited:
|
|
||||||
visited.add(p.line)
|
|
||||||
expanded.append(p)
|
|
||||||
expanded.append(item)
|
|
||||||
visited.add(item.line)
|
|
||||||
|
|
||||||
return expanded
|
|
||||||
|
|
||||||
def add(self, lines, parents=None):
|
|
||||||
ancestors = list()
|
|
||||||
offset = 0
|
|
||||||
obj = None
|
|
||||||
|
|
||||||
# global config command
|
|
||||||
if not parents:
|
|
||||||
for line in lines:
|
|
||||||
# handle ignore lines
|
|
||||||
if ignore_line(line):
|
|
||||||
continue
|
|
||||||
|
|
||||||
item = ConfigLine(line)
|
|
||||||
item.raw = line
|
|
||||||
if item not in self.items:
|
|
||||||
self.items.append(item)
|
|
||||||
|
|
||||||
else:
|
|
||||||
for index, p in enumerate(parents):
|
|
||||||
try:
|
|
||||||
i = index + 1
|
|
||||||
obj = self.get_block(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 lines:
|
|
||||||
# handle ignore lines
|
|
||||||
if ignore_line(line):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomNetworkConfig(NetworkConfig):
|
|
||||||
|
|
||||||
def items_text(self):
|
|
||||||
return [item.text for item in self.items]
|
|
||||||
|
|
||||||
def expand_section(self, configobj, S=None):
|
|
||||||
if S is None:
|
|
||||||
S = list()
|
|
||||||
S.append(configobj)
|
|
||||||
for child in configobj.child_objs:
|
|
||||||
if child in S:
|
|
||||||
continue
|
|
||||||
self.expand_section(child, S)
|
|
||||||
return S
|
|
||||||
|
|
||||||
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)
|
|
||||||
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)
|
|
@ -1,132 +0,0 @@
|
|||||||
#
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2019 Red Hat
|
|
||||||
# GNU General Public License v3.0+
|
|
||||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
"""
|
|
||||||
The facts base class
|
|
||||||
this contains methods common to all facts subsets
|
|
||||||
"""
|
|
||||||
from ansible.module_utils.network.common.network import get_resource_connection
|
|
||||||
from ansible.module_utils.six import iteritems
|
|
||||||
|
|
||||||
|
|
||||||
class FactsBase(object):
|
|
||||||
"""
|
|
||||||
The facts base class
|
|
||||||
"""
|
|
||||||
def __init__(self, module):
|
|
||||||
self._module = module
|
|
||||||
self._warnings = []
|
|
||||||
self._gather_subset = module.params.get('gather_subset')
|
|
||||||
self._gather_network_resources = module.params.get('gather_network_resources')
|
|
||||||
self._connection = None
|
|
||||||
if module.params.get('state') not in ['rendered', 'parsed']:
|
|
||||||
self._connection = get_resource_connection(module)
|
|
||||||
|
|
||||||
self.ansible_facts = {'ansible_network_resources': {}}
|
|
||||||
self.ansible_facts['ansible_net_gather_network_resources'] = list()
|
|
||||||
self.ansible_facts['ansible_net_gather_subset'] = list()
|
|
||||||
|
|
||||||
if not self._gather_subset:
|
|
||||||
self._gather_subset = ['!config']
|
|
||||||
if not self._gather_network_resources:
|
|
||||||
self._gather_network_resources = ['!all']
|
|
||||||
|
|
||||||
def gen_runable(self, subsets, valid_subsets, resource_facts=False):
|
|
||||||
""" Generate the runable subset
|
|
||||||
|
|
||||||
:param module: The module instance
|
|
||||||
:param subsets: The provided subsets
|
|
||||||
:param valid_subsets: The valid subsets
|
|
||||||
:param resource_facts: A boolean flag
|
|
||||||
:rtype: list
|
|
||||||
:returns: The runable subsets
|
|
||||||
"""
|
|
||||||
runable_subsets = set()
|
|
||||||
exclude_subsets = set()
|
|
||||||
minimal_gather_subset = set()
|
|
||||||
if not resource_facts:
|
|
||||||
minimal_gather_subset = frozenset(['default'])
|
|
||||||
|
|
||||||
for subset in subsets:
|
|
||||||
if subset == 'all':
|
|
||||||
runable_subsets.update(valid_subsets)
|
|
||||||
continue
|
|
||||||
if subset == 'min' and minimal_gather_subset:
|
|
||||||
runable_subsets.update(minimal_gather_subset)
|
|
||||||
continue
|
|
||||||
if subset.startswith('!'):
|
|
||||||
subset = subset[1:]
|
|
||||||
if subset == 'min':
|
|
||||||
exclude_subsets.update(minimal_gather_subset)
|
|
||||||
continue
|
|
||||||
if subset == 'all':
|
|
||||||
exclude_subsets.update(
|
|
||||||
valid_subsets - minimal_gather_subset)
|
|
||||||
continue
|
|
||||||
exclude = True
|
|
||||||
else:
|
|
||||||
exclude = False
|
|
||||||
|
|
||||||
if subset not in valid_subsets:
|
|
||||||
self._module.fail_json(msg='Subset must be one of [%s], got %s' %
|
|
||||||
(', '.join(sorted([item for item in valid_subsets])), subset))
|
|
||||||
|
|
||||||
if exclude:
|
|
||||||
exclude_subsets.add(subset)
|
|
||||||
else:
|
|
||||||
runable_subsets.add(subset)
|
|
||||||
|
|
||||||
if not runable_subsets:
|
|
||||||
runable_subsets.update(valid_subsets)
|
|
||||||
runable_subsets.difference_update(exclude_subsets)
|
|
||||||
return runable_subsets
|
|
||||||
|
|
||||||
def get_network_resources_facts(self, facts_resource_obj_map, resource_facts_type=None, data=None):
|
|
||||||
"""
|
|
||||||
:param fact_resource_subsets:
|
|
||||||
:param data: previously collected configuration
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if not resource_facts_type:
|
|
||||||
resource_facts_type = self._gather_network_resources
|
|
||||||
|
|
||||||
restorun_subsets = self.gen_runable(resource_facts_type, frozenset(facts_resource_obj_map.keys()), resource_facts=True)
|
|
||||||
if restorun_subsets:
|
|
||||||
self.ansible_facts['ansible_net_gather_network_resources'] = list(restorun_subsets)
|
|
||||||
instances = list()
|
|
||||||
for key in restorun_subsets:
|
|
||||||
fact_cls_obj = facts_resource_obj_map.get(key)
|
|
||||||
if fact_cls_obj:
|
|
||||||
instances.append(fact_cls_obj(self._module))
|
|
||||||
else:
|
|
||||||
self._warnings.extend(["network resource fact gathering for '%s' is not supported" % key])
|
|
||||||
|
|
||||||
for inst in instances:
|
|
||||||
inst.populate_facts(self._connection, self.ansible_facts, data)
|
|
||||||
|
|
||||||
def get_network_legacy_facts(self, fact_legacy_obj_map, legacy_facts_type=None):
|
|
||||||
if not legacy_facts_type:
|
|
||||||
legacy_facts_type = self._gather_subset
|
|
||||||
|
|
||||||
runable_subsets = self.gen_runable(legacy_facts_type, frozenset(fact_legacy_obj_map.keys()))
|
|
||||||
if runable_subsets:
|
|
||||||
facts = dict()
|
|
||||||
# default subset should always returned be with legacy facts subsets
|
|
||||||
if 'default' not in runable_subsets:
|
|
||||||
runable_subsets.add('default')
|
|
||||||
self.ansible_facts['ansible_net_gather_subset'] = list(runable_subsets)
|
|
||||||
|
|
||||||
instances = list()
|
|
||||||
for key in runable_subsets:
|
|
||||||
instances.append(fact_legacy_obj_map[key](self._module))
|
|
||||||
|
|
||||||
for inst in instances:
|
|
||||||
inst.populate()
|
|
||||||
facts.update(inst.facts)
|
|
||||||
self._warnings.extend(inst.warnings)
|
|
||||||
|
|
||||||
for key, value in iteritems(facts):
|
|
||||||
key = 'ansible_net_%s' % key
|
|
||||||
self.ansible_facts[key] = value
|
|
@ -1,162 +0,0 @@
|
|||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# (c) 2017 Red Hat Inc.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
#
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from ansible.module_utils._text import to_text, to_bytes
|
|
||||||
from ansible.module_utils.connection import Connection, ConnectionError
|
|
||||||
|
|
||||||
try:
|
|
||||||
from ncclient.xml_ import NCElement, new_ele, sub_ele
|
|
||||||
HAS_NCCLIENT = True
|
|
||||||
except (ImportError, AttributeError):
|
|
||||||
HAS_NCCLIENT = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
from lxml.etree import Element, fromstring, XMLSyntaxError
|
|
||||||
except ImportError:
|
|
||||||
from xml.etree.ElementTree import Element, fromstring
|
|
||||||
if sys.version_info < (2, 7):
|
|
||||||
from xml.parsers.expat import ExpatError as XMLSyntaxError
|
|
||||||
else:
|
|
||||||
from xml.etree.ElementTree import ParseError as XMLSyntaxError
|
|
||||||
|
|
||||||
NS_MAP = {'nc': "urn:ietf:params:xml:ns:netconf:base:1.0"}
|
|
||||||
|
|
||||||
|
|
||||||
def exec_rpc(module, *args, **kwargs):
|
|
||||||
connection = NetconfConnection(module._socket_path)
|
|
||||||
return connection.execute_rpc(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class NetconfConnection(Connection):
|
|
||||||
|
|
||||||
def __init__(self, socket_path):
|
|
||||||
super(NetconfConnection, self).__init__(socket_path)
|
|
||||||
|
|
||||||
def __rpc__(self, name, *args, **kwargs):
|
|
||||||
"""Executes the json-rpc and returns the output received
|
|
||||||
from remote device.
|
|
||||||
:name: rpc method to be executed over connection plugin that implements jsonrpc 2.0
|
|
||||||
:args: Ordered list of params passed as arguments to rpc method
|
|
||||||
:kwargs: Dict of valid key, value pairs passed as arguments to rpc method
|
|
||||||
|
|
||||||
For usage refer the respective connection plugin docs.
|
|
||||||
"""
|
|
||||||
self.check_rc = kwargs.pop('check_rc', True)
|
|
||||||
self.ignore_warning = kwargs.pop('ignore_warning', True)
|
|
||||||
|
|
||||||
response = self._exec_jsonrpc(name, *args, **kwargs)
|
|
||||||
if 'error' in response:
|
|
||||||
rpc_error = response['error'].get('data')
|
|
||||||
return self.parse_rpc_error(to_bytes(rpc_error, errors='surrogate_then_replace'))
|
|
||||||
|
|
||||||
return fromstring(to_bytes(response['result'], errors='surrogate_then_replace'))
|
|
||||||
|
|
||||||
def parse_rpc_error(self, rpc_error):
|
|
||||||
if self.check_rc:
|
|
||||||
try:
|
|
||||||
error_root = fromstring(rpc_error)
|
|
||||||
root = Element('root')
|
|
||||||
root.append(error_root)
|
|
||||||
|
|
||||||
error_list = root.findall('.//nc:rpc-error', NS_MAP)
|
|
||||||
if not error_list:
|
|
||||||
raise ConnectionError(to_text(rpc_error, errors='surrogate_then_replace'))
|
|
||||||
|
|
||||||
warnings = []
|
|
||||||
for error in error_list:
|
|
||||||
message_ele = error.find('./nc:error-message', NS_MAP)
|
|
||||||
|
|
||||||
if message_ele is None:
|
|
||||||
message_ele = error.find('./nc:error-info', NS_MAP)
|
|
||||||
|
|
||||||
message = message_ele.text if message_ele is not None else None
|
|
||||||
|
|
||||||
severity = error.find('./nc:error-severity', NS_MAP).text
|
|
||||||
|
|
||||||
if severity == 'warning' and self.ignore_warning and message is not None:
|
|
||||||
warnings.append(message)
|
|
||||||
else:
|
|
||||||
raise ConnectionError(to_text(rpc_error, errors='surrogate_then_replace'))
|
|
||||||
return warnings
|
|
||||||
except XMLSyntaxError:
|
|
||||||
raise ConnectionError(rpc_error)
|
|
||||||
|
|
||||||
|
|
||||||
def transform_reply():
|
|
||||||
return b'''<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
||||||
<xsl:output method="xml" indent="no"/>
|
|
||||||
|
|
||||||
<xsl:template match="/|comment()|processing-instruction()">
|
|
||||||
<xsl:copy>
|
|
||||||
<xsl:apply-templates/>
|
|
||||||
</xsl:copy>
|
|
||||||
</xsl:template>
|
|
||||||
|
|
||||||
<xsl:template match="*">
|
|
||||||
<xsl:element name="{local-name()}">
|
|
||||||
<xsl:apply-templates select="@*|node()"/>
|
|
||||||
</xsl:element>
|
|
||||||
</xsl:template>
|
|
||||||
|
|
||||||
<xsl:template match="@*">
|
|
||||||
<xsl:attribute name="{local-name()}">
|
|
||||||
<xsl:value-of select="."/>
|
|
||||||
</xsl:attribute>
|
|
||||||
</xsl:template>
|
|
||||||
</xsl:stylesheet>
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
# Note: Workaround for ncclient 0.5.3
|
|
||||||
def remove_namespaces(data):
|
|
||||||
if not HAS_NCCLIENT:
|
|
||||||
raise ImportError("ncclient is required but does not appear to be installed. "
|
|
||||||
"It can be installed using `pip install ncclient`")
|
|
||||||
return NCElement(data, transform_reply()).data_xml
|
|
||||||
|
|
||||||
|
|
||||||
def build_root_xml_node(tag):
|
|
||||||
return new_ele(tag)
|
|
||||||
|
|
||||||
|
|
||||||
def build_child_xml_node(parent, tag, text=None, attrib=None):
|
|
||||||
element = sub_ele(parent, tag)
|
|
||||||
if text:
|
|
||||||
element.text = to_text(text)
|
|
||||||
if attrib:
|
|
||||||
element.attrib.update(attrib)
|
|
||||||
return element
|
|
||||||
|
|
||||||
|
|
||||||
def build_subtree(parent, path):
|
|
||||||
element = parent
|
|
||||||
for field in path.split('/'):
|
|
||||||
sub_element = build_child_xml_node(element, field)
|
|
||||||
element = sub_element
|
|
||||||
return element
|
|
@ -1,249 +0,0 @@
|
|||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# Copyright (c) 2015 Peter Sprygada, <psprygada@ansible.com>
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
import traceback
|
|
||||||
import json
|
|
||||||
|
|
||||||
from ansible.module_utils._text import to_text, to_native
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
from ansible.module_utils.basic import env_fallback
|
|
||||||
from ansible.module_utils.connection import Connection, ConnectionError
|
|
||||||
from ansible.module_utils.network.common.netconf import NetconfConnection
|
|
||||||
from ansible.module_utils.network.common.parsing import Cli
|
|
||||||
from ansible.module_utils.six import iteritems
|
|
||||||
|
|
||||||
|
|
||||||
NET_TRANSPORT_ARGS = dict(
|
|
||||||
host=dict(required=True),
|
|
||||||
port=dict(type='int'),
|
|
||||||
|
|
||||||
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
|
|
||||||
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
|
|
||||||
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
|
|
||||||
|
|
||||||
authorize=dict(default=False, fallback=(env_fallback, ['ANSIBLE_NET_AUTHORIZE']), type='bool'),
|
|
||||||
auth_pass=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_AUTH_PASS'])),
|
|
||||||
|
|
||||||
provider=dict(type='dict', no_log=True),
|
|
||||||
transport=dict(choices=list()),
|
|
||||||
|
|
||||||
timeout=dict(default=10, type='int')
|
|
||||||
)
|
|
||||||
|
|
||||||
NET_CONNECTION_ARGS = dict()
|
|
||||||
|
|
||||||
NET_CONNECTIONS = dict()
|
|
||||||
|
|
||||||
|
|
||||||
def _transitional_argument_spec():
|
|
||||||
argument_spec = {}
|
|
||||||
for key, value in iteritems(NET_TRANSPORT_ARGS):
|
|
||||||
value['required'] = False
|
|
||||||
argument_spec[key] = value
|
|
||||||
return argument_spec
|
|
||||||
|
|
||||||
|
|
||||||
def to_list(val):
|
|
||||||
if isinstance(val, (list, tuple)):
|
|
||||||
return list(val)
|
|
||||||
elif val is not None:
|
|
||||||
return [val]
|
|
||||||
else:
|
|
||||||
return list()
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleStub(object):
|
|
||||||
def __init__(self, argument_spec, fail_json):
|
|
||||||
self.params = dict()
|
|
||||||
for key, value in argument_spec.items():
|
|
||||||
self.params[key] = value.get('default')
|
|
||||||
self.fail_json = fail_json
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkError(Exception):
|
|
||||||
|
|
||||||
def __init__(self, msg, **kwargs):
|
|
||||||
super(NetworkError, self).__init__(msg)
|
|
||||||
self.kwargs = kwargs
|
|
||||||
|
|
||||||
|
|
||||||
class Config(object):
|
|
||||||
|
|
||||||
def __init__(self, connection):
|
|
||||||
self.connection = connection
|
|
||||||
|
|
||||||
def __call__(self, commands, **kwargs):
|
|
||||||
lines = to_list(commands)
|
|
||||||
return self.connection.configure(lines, **kwargs)
|
|
||||||
|
|
||||||
def load_config(self, commands, **kwargs):
|
|
||||||
commands = to_list(commands)
|
|
||||||
return self.connection.load_config(commands, **kwargs)
|
|
||||||
|
|
||||||
def get_config(self, **kwargs):
|
|
||||||
return self.connection.get_config(**kwargs)
|
|
||||||
|
|
||||||
def save_config(self):
|
|
||||||
return self.connection.save_config()
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkModule(AnsibleModule):
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
connect_on_load = kwargs.pop('connect_on_load', True)
|
|
||||||
|
|
||||||
argument_spec = NET_TRANSPORT_ARGS.copy()
|
|
||||||
argument_spec['transport']['choices'] = NET_CONNECTIONS.keys()
|
|
||||||
argument_spec.update(NET_CONNECTION_ARGS.copy())
|
|
||||||
|
|
||||||
if kwargs.get('argument_spec'):
|
|
||||||
argument_spec.update(kwargs['argument_spec'])
|
|
||||||
kwargs['argument_spec'] = argument_spec
|
|
||||||
|
|
||||||
super(NetworkModule, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.connection = None
|
|
||||||
self._cli = None
|
|
||||||
self._config = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
transport = self.params['transport'] or '__default__'
|
|
||||||
cls = NET_CONNECTIONS[transport]
|
|
||||||
self.connection = cls()
|
|
||||||
except KeyError:
|
|
||||||
self.fail_json(msg='Unknown transport or no default transport specified')
|
|
||||||
except (TypeError, NetworkError) as exc:
|
|
||||||
self.fail_json(msg=to_native(exc), exception=traceback.format_exc())
|
|
||||||
|
|
||||||
if connect_on_load:
|
|
||||||
self.connect()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cli(self):
|
|
||||||
if not self.connected:
|
|
||||||
self.connect()
|
|
||||||
if self._cli:
|
|
||||||
return self._cli
|
|
||||||
self._cli = Cli(self.connection)
|
|
||||||
return self._cli
|
|
||||||
|
|
||||||
@property
|
|
||||||
def config(self):
|
|
||||||
if not self.connected:
|
|
||||||
self.connect()
|
|
||||||
if self._config:
|
|
||||||
return self._config
|
|
||||||
self._config = Config(self.connection)
|
|
||||||
return self._config
|
|
||||||
|
|
||||||
@property
|
|
||||||
def connected(self):
|
|
||||||
return self.connection._connected
|
|
||||||
|
|
||||||
def _load_params(self):
|
|
||||||
super(NetworkModule, self)._load_params()
|
|
||||||
provider = self.params.get('provider') or dict()
|
|
||||||
for key, value in provider.items():
|
|
||||||
for args in [NET_TRANSPORT_ARGS, NET_CONNECTION_ARGS]:
|
|
||||||
if key in args:
|
|
||||||
if self.params.get(key) is None and value is not None:
|
|
||||||
self.params[key] = value
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
try:
|
|
||||||
if not self.connected:
|
|
||||||
self.connection.connect(self.params)
|
|
||||||
if self.params['authorize']:
|
|
||||||
self.connection.authorize(self.params)
|
|
||||||
self.log('connected to %s:%s using %s' % (self.params['host'],
|
|
||||||
self.params['port'], self.params['transport']))
|
|
||||||
except NetworkError as exc:
|
|
||||||
self.fail_json(msg=to_native(exc), exception=traceback.format_exc())
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
try:
|
|
||||||
if self.connected:
|
|
||||||
self.connection.disconnect()
|
|
||||||
self.log('disconnected from %s' % self.params['host'])
|
|
||||||
except NetworkError as exc:
|
|
||||||
self.fail_json(msg=to_native(exc), exception=traceback.format_exc())
|
|
||||||
|
|
||||||
|
|
||||||
def register_transport(transport, default=False):
|
|
||||||
def register(cls):
|
|
||||||
NET_CONNECTIONS[transport] = cls
|
|
||||||
if default:
|
|
||||||
NET_CONNECTIONS['__default__'] = cls
|
|
||||||
return cls
|
|
||||||
return register
|
|
||||||
|
|
||||||
|
|
||||||
def add_argument(key, value):
|
|
||||||
NET_CONNECTION_ARGS[key] = value
|
|
||||||
|
|
||||||
|
|
||||||
def get_resource_connection(module):
|
|
||||||
if hasattr(module, '_connection'):
|
|
||||||
return module._connection
|
|
||||||
|
|
||||||
capabilities = get_capabilities(module)
|
|
||||||
network_api = capabilities.get('network_api')
|
|
||||||
if network_api in ('cliconf', 'nxapi', 'eapi', 'exosapi'):
|
|
||||||
module._connection = Connection(module._socket_path)
|
|
||||||
elif network_api == 'netconf':
|
|
||||||
module._connection = NetconfConnection(module._socket_path)
|
|
||||||
elif network_api == "local":
|
|
||||||
# This isn't supported, but we shouldn't fail here.
|
|
||||||
# Set the connection to a fake connection so it fails sensibly.
|
|
||||||
module._connection = LocalResourceConnection(module)
|
|
||||||
else:
|
|
||||||
module.fail_json(msg='Invalid connection type {0!s}'.format(network_api))
|
|
||||||
|
|
||||||
return module._connection
|
|
||||||
|
|
||||||
|
|
||||||
def get_capabilities(module):
|
|
||||||
if hasattr(module, 'capabilities'):
|
|
||||||
return module._capabilities
|
|
||||||
try:
|
|
||||||
capabilities = Connection(module._socket_path).get_capabilities()
|
|
||||||
except ConnectionError as exc:
|
|
||||||
module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
|
||||||
except AssertionError:
|
|
||||||
# No socket_path, connection most likely local.
|
|
||||||
return dict(network_api="local")
|
|
||||||
module._capabilities = json.loads(capabilities)
|
|
||||||
|
|
||||||
return module._capabilities
|
|
||||||
|
|
||||||
|
|
||||||
class LocalResourceConnection:
|
|
||||||
def __init__(self, module):
|
|
||||||
self.module = module
|
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
self.module.fail_json(msg="Network resource modules not supported over local connection.")
|
|
@ -1,305 +0,0 @@
|
|||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# Copyright (c) 2015 Peter Sprygada, <psprygada@ansible.com>
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
import re
|
|
||||||
import shlex
|
|
||||||
import time
|
|
||||||
|
|
||||||
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE, BOOLEANS_FALSE
|
|
||||||
from ansible.module_utils.six import string_types, text_type
|
|
||||||
from ansible.module_utils.six.moves import zip
|
|
||||||
|
|
||||||
|
|
||||||
def to_list(val):
|
|
||||||
if isinstance(val, (list, tuple)):
|
|
||||||
return list(val)
|
|
||||||
elif val is not None:
|
|
||||||
return [val]
|
|
||||||
else:
|
|
||||||
return list()
|
|
||||||
|
|
||||||
|
|
||||||
class FailedConditionsError(Exception):
|
|
||||||
def __init__(self, msg, failed_conditions):
|
|
||||||
super(FailedConditionsError, self).__init__(msg)
|
|
||||||
self.failed_conditions = failed_conditions
|
|
||||||
|
|
||||||
|
|
||||||
class FailedConditionalError(Exception):
|
|
||||||
def __init__(self, msg, failed_conditional):
|
|
||||||
super(FailedConditionalError, self).__init__(msg)
|
|
||||||
self.failed_conditional = failed_conditional
|
|
||||||
|
|
||||||
|
|
||||||
class AddCommandError(Exception):
|
|
||||||
def __init__(self, msg, command):
|
|
||||||
super(AddCommandError, self).__init__(msg)
|
|
||||||
self.command = command
|
|
||||||
|
|
||||||
|
|
||||||
class AddConditionError(Exception):
|
|
||||||
def __init__(self, msg, condition):
|
|
||||||
super(AddConditionError, self).__init__(msg)
|
|
||||||
self.condition = condition
|
|
||||||
|
|
||||||
|
|
||||||
class Cli(object):
|
|
||||||
|
|
||||||
def __init__(self, connection):
|
|
||||||
self.connection = connection
|
|
||||||
self.default_output = connection.default_output or 'text'
|
|
||||||
self._commands = list()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def commands(self):
|
|
||||||
return [str(c) for c in self._commands]
|
|
||||||
|
|
||||||
def __call__(self, commands, output=None):
|
|
||||||
objects = list()
|
|
||||||
for cmd in to_list(commands):
|
|
||||||
objects.append(self.to_command(cmd, output))
|
|
||||||
return self.connection.run_commands(objects)
|
|
||||||
|
|
||||||
def to_command(self, command, output=None, prompt=None, response=None, **kwargs):
|
|
||||||
output = output or self.default_output
|
|
||||||
if isinstance(command, Command):
|
|
||||||
return command
|
|
||||||
if isinstance(prompt, string_types):
|
|
||||||
prompt = re.compile(re.escape(prompt))
|
|
||||||
return Command(command, output, prompt=prompt, response=response, **kwargs)
|
|
||||||
|
|
||||||
def add_commands(self, commands, output=None, **kwargs):
|
|
||||||
for cmd in commands:
|
|
||||||
self._commands.append(self.to_command(cmd, output, **kwargs))
|
|
||||||
|
|
||||||
def run_commands(self):
|
|
||||||
responses = self.connection.run_commands(self._commands)
|
|
||||||
for resp, cmd in zip(responses, self._commands):
|
|
||||||
cmd.response = resp
|
|
||||||
|
|
||||||
# wipe out the commands list to avoid issues if additional
|
|
||||||
# commands are executed later
|
|
||||||
self._commands = list()
|
|
||||||
|
|
||||||
return responses
|
|
||||||
|
|
||||||
|
|
||||||
class Command(object):
|
|
||||||
|
|
||||||
def __init__(self, command, output=None, prompt=None, response=None,
|
|
||||||
**kwargs):
|
|
||||||
|
|
||||||
self.command = command
|
|
||||||
self.output = output
|
|
||||||
self.command_string = command
|
|
||||||
|
|
||||||
self.prompt = prompt
|
|
||||||
self.response = response
|
|
||||||
|
|
||||||
self.args = kwargs
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.command_string
|
|
||||||
|
|
||||||
|
|
||||||
class CommandRunner(object):
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
self.module = module
|
|
||||||
|
|
||||||
self.items = list()
|
|
||||||
self.conditionals = set()
|
|
||||||
|
|
||||||
self.commands = list()
|
|
||||||
|
|
||||||
self.retries = 10
|
|
||||||
self.interval = 1
|
|
||||||
|
|
||||||
self.match = 'all'
|
|
||||||
|
|
||||||
self._default_output = module.connection.default_output
|
|
||||||
|
|
||||||
def add_command(self, command, output=None, prompt=None, response=None,
|
|
||||||
**kwargs):
|
|
||||||
if command in [str(c) for c in self.commands]:
|
|
||||||
raise AddCommandError('duplicated command detected', command=command)
|
|
||||||
cmd = self.module.cli.to_command(command, output=output, prompt=prompt,
|
|
||||||
response=response, **kwargs)
|
|
||||||
self.commands.append(cmd)
|
|
||||||
|
|
||||||
def get_command(self, command, output=None):
|
|
||||||
for cmd in self.commands:
|
|
||||||
if cmd.command == command:
|
|
||||||
return cmd.response
|
|
||||||
raise ValueError("command '%s' not found" % command)
|
|
||||||
|
|
||||||
def get_responses(self):
|
|
||||||
return [cmd.response for cmd in self.commands]
|
|
||||||
|
|
||||||
def add_conditional(self, condition):
|
|
||||||
try:
|
|
||||||
self.conditionals.add(Conditional(condition))
|
|
||||||
except AttributeError as exc:
|
|
||||||
raise AddConditionError(msg=str(exc), condition=condition)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
while self.retries > 0:
|
|
||||||
self.module.cli.add_commands(self.commands)
|
|
||||||
responses = self.module.cli.run_commands()
|
|
||||||
|
|
||||||
for item in list(self.conditionals):
|
|
||||||
if item(responses):
|
|
||||||
if self.match == 'any':
|
|
||||||
return item
|
|
||||||
self.conditionals.remove(item)
|
|
||||||
|
|
||||||
if not self.conditionals:
|
|
||||||
break
|
|
||||||
|
|
||||||
time.sleep(self.interval)
|
|
||||||
self.retries -= 1
|
|
||||||
else:
|
|
||||||
failed_conditions = [item.raw for item in self.conditionals]
|
|
||||||
errmsg = 'One or more conditional statements have not been satisfied'
|
|
||||||
raise FailedConditionsError(errmsg, failed_conditions)
|
|
||||||
|
|
||||||
|
|
||||||
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'],
|
|
||||||
'matches': ['matches']
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, conditional, encoding=None):
|
|
||||||
self.raw = conditional
|
|
||||||
self.negate = False
|
|
||||||
try:
|
|
||||||
components = shlex.split(conditional)
|
|
||||||
key, val = components[0], components[-1]
|
|
||||||
op_components = components[1:-1]
|
|
||||||
if 'not' in op_components:
|
|
||||||
self.negate = True
|
|
||||||
op_components.pop(op_components.index('not'))
|
|
||||||
op = op_components[0]
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError('failed to parse 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))
|
|
||||||
if not self.negate:
|
|
||||||
return self.func(value)
|
|
||||||
else:
|
|
||||||
return not 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 text_type(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):
|
|
||||||
try:
|
|
||||||
return self.get_json(result)
|
|
||||||
except (IndexError, TypeError, AttributeError):
|
|
||||||
msg = 'unable to apply conditional to result'
|
|
||||||
raise FailedConditionalError(msg, self.raw)
|
|
||||||
|
|
||||||
def get_json(self, result):
|
|
||||||
string = re.sub(r"\[[\'|\"]", ".", self.key)
|
|
||||||
string = re.sub(r"[\'|\"]\]", ".", string)
|
|
||||||
parts = re.split(r'\.(?=[^\]]*(?:\[|$))', string)
|
|
||||||
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
|
|
||||||
|
|
||||||
def matches(self, value):
|
|
||||||
match = re.search(self.value, value, re.M)
|
|
||||||
return match is not None
|
|
@ -1,643 +0,0 @@
|
|||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# (c) 2016 Red Hat Inc.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
#
|
|
||||||
|
|
||||||
# Networking tools for network modules only
|
|
||||||
|
|
||||||
import re
|
|
||||||
import ast
|
|
||||||
import operator
|
|
||||||
import socket
|
|
||||||
import json
|
|
||||||
|
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from ansible.module_utils._text import to_text, to_bytes
|
|
||||||
from ansible.module_utils.common._collections_compat import Mapping
|
|
||||||
from ansible.module_utils.six import iteritems, string_types
|
|
||||||
from ansible.module_utils import basic
|
|
||||||
from ansible.module_utils.parsing.convert_bool import boolean
|
|
||||||
|
|
||||||
# Backwards compatibility for 3rd party modules
|
|
||||||
# TODO(pabelanger): With move to ansible.netcommon, we should clean this code
|
|
||||||
# up and have modules import directly themself.
|
|
||||||
from ansible.module_utils.common.network import ( # noqa: F401
|
|
||||||
to_bits, is_netmask, is_masklen, to_netmask, to_masklen, to_subnet, to_ipv6_network, VALID_MASKS
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
from jinja2 import Environment, StrictUndefined
|
|
||||||
from jinja2.exceptions import UndefinedError
|
|
||||||
HAS_JINJA2 = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_JINJA2 = False
|
|
||||||
|
|
||||||
|
|
||||||
OPERATORS = frozenset(['ge', 'gt', 'eq', 'neq', 'lt', 'le'])
|
|
||||||
ALIASES = frozenset([('min', 'ge'), ('max', 'le'), ('exactly', 'eq'), ('neq', 'ne')])
|
|
||||||
|
|
||||||
|
|
||||||
def to_list(val):
|
|
||||||
if isinstance(val, (list, tuple, set)):
|
|
||||||
return list(val)
|
|
||||||
elif val is not None:
|
|
||||||
return [val]
|
|
||||||
else:
|
|
||||||
return list()
|
|
||||||
|
|
||||||
|
|
||||||
def to_lines(stdout):
|
|
||||||
for item in stdout:
|
|
||||||
if isinstance(item, string_types):
|
|
||||||
item = to_text(item).split('\n')
|
|
||||||
yield item
|
|
||||||
|
|
||||||
|
|
||||||
def transform_commands(module):
|
|
||||||
transform = ComplexList(dict(
|
|
||||||
command=dict(key=True),
|
|
||||||
output=dict(),
|
|
||||||
prompt=dict(type='list'),
|
|
||||||
answer=dict(type='list'),
|
|
||||||
newline=dict(type='bool', default=True),
|
|
||||||
sendonly=dict(type='bool', default=False),
|
|
||||||
check_all=dict(type='bool', default=False),
|
|
||||||
), module)
|
|
||||||
|
|
||||||
return transform(module.params['commands'])
|
|
||||||
|
|
||||||
|
|
||||||
def sort_list(val):
|
|
||||||
if isinstance(val, list):
|
|
||||||
return sorted(val)
|
|
||||||
return val
|
|
||||||
|
|
||||||
|
|
||||||
class Entity(object):
|
|
||||||
"""Transforms a dict to with an argument spec
|
|
||||||
|
|
||||||
This class will take a dict and apply an Ansible argument spec to the
|
|
||||||
values. The resulting dict will contain all of the keys in the param
|
|
||||||
with appropriate values set.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
argument_spec = dict(
|
|
||||||
command=dict(key=True),
|
|
||||||
display=dict(default='text', choices=['text', 'json']),
|
|
||||||
validate=dict(type='bool')
|
|
||||||
)
|
|
||||||
transform = Entity(module, argument_spec)
|
|
||||||
value = dict(command='foo')
|
|
||||||
result = transform(value)
|
|
||||||
print result
|
|
||||||
{'command': 'foo', 'display': 'text', 'validate': None}
|
|
||||||
|
|
||||||
Supported argument spec:
|
|
||||||
* key - specifies how to map a single value to a dict
|
|
||||||
* read_from - read and apply the argument_spec from the module
|
|
||||||
* required - a value is required
|
|
||||||
* type - type of value (uses AnsibleModule type checker)
|
|
||||||
* fallback - implements fallback function
|
|
||||||
* choices - set of valid options
|
|
||||||
* default - default value
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, module, attrs=None, args=None, keys=None, from_argspec=False):
|
|
||||||
args = [] if args is None else args
|
|
||||||
|
|
||||||
self._attributes = attrs or {}
|
|
||||||
self._module = module
|
|
||||||
|
|
||||||
for arg in args:
|
|
||||||
self._attributes[arg] = dict()
|
|
||||||
if from_argspec:
|
|
||||||
self._attributes[arg]['read_from'] = arg
|
|
||||||
if keys and arg in keys:
|
|
||||||
self._attributes[arg]['key'] = True
|
|
||||||
|
|
||||||
self.attr_names = frozenset(self._attributes.keys())
|
|
||||||
|
|
||||||
_has_key = False
|
|
||||||
|
|
||||||
for name, attr in iteritems(self._attributes):
|
|
||||||
if attr.get('read_from'):
|
|
||||||
if attr['read_from'] not in self._module.argument_spec:
|
|
||||||
module.fail_json(msg='argument %s does not exist' % attr['read_from'])
|
|
||||||
spec = self._module.argument_spec.get(attr['read_from'])
|
|
||||||
for key, value in iteritems(spec):
|
|
||||||
if key not in attr:
|
|
||||||
attr[key] = value
|
|
||||||
|
|
||||||
if attr.get('key'):
|
|
||||||
if _has_key:
|
|
||||||
module.fail_json(msg='only one key value can be specified')
|
|
||||||
_has_key = True
|
|
||||||
attr['required'] = True
|
|
||||||
|
|
||||||
def serialize(self):
|
|
||||||
return self._attributes
|
|
||||||
|
|
||||||
def to_dict(self, value):
|
|
||||||
obj = {}
|
|
||||||
for name, attr in iteritems(self._attributes):
|
|
||||||
if attr.get('key'):
|
|
||||||
obj[name] = value
|
|
||||||
else:
|
|
||||||
obj[name] = attr.get('default')
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def __call__(self, value, strict=True):
|
|
||||||
if not isinstance(value, dict):
|
|
||||||
value = self.to_dict(value)
|
|
||||||
|
|
||||||
if strict:
|
|
||||||
unknown = set(value).difference(self.attr_names)
|
|
||||||
if unknown:
|
|
||||||
self._module.fail_json(msg='invalid keys: %s' % ','.join(unknown))
|
|
||||||
|
|
||||||
for name, attr in iteritems(self._attributes):
|
|
||||||
if value.get(name) is None:
|
|
||||||
value[name] = attr.get('default')
|
|
||||||
|
|
||||||
if attr.get('fallback') and not value.get(name):
|
|
||||||
fallback = attr.get('fallback', (None,))
|
|
||||||
fallback_strategy = fallback[0]
|
|
||||||
fallback_args = []
|
|
||||||
fallback_kwargs = {}
|
|
||||||
if fallback_strategy is not None:
|
|
||||||
for item in fallback[1:]:
|
|
||||||
if isinstance(item, dict):
|
|
||||||
fallback_kwargs = item
|
|
||||||
else:
|
|
||||||
fallback_args = item
|
|
||||||
try:
|
|
||||||
value[name] = fallback_strategy(*fallback_args, **fallback_kwargs)
|
|
||||||
except basic.AnsibleFallbackNotFound:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if attr.get('required') and value.get(name) is None:
|
|
||||||
self._module.fail_json(msg='missing required attribute %s' % name)
|
|
||||||
|
|
||||||
if 'choices' in attr:
|
|
||||||
if value[name] not in attr['choices']:
|
|
||||||
self._module.fail_json(msg='%s must be one of %s, got %s' % (name, ', '.join(attr['choices']), value[name]))
|
|
||||||
|
|
||||||
if value[name] is not None:
|
|
||||||
value_type = attr.get('type', 'str')
|
|
||||||
type_checker = self._module._CHECK_ARGUMENT_TYPES_DISPATCHER[value_type]
|
|
||||||
type_checker(value[name])
|
|
||||||
elif value.get(name):
|
|
||||||
value[name] = self._module.params[name]
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class EntityCollection(Entity):
|
|
||||||
"""Extends ```Entity``` to handle a list of dicts """
|
|
||||||
|
|
||||||
def __call__(self, iterable, strict=True):
|
|
||||||
if iterable is None:
|
|
||||||
iterable = [super(EntityCollection, self).__call__(self._module.params, strict)]
|
|
||||||
|
|
||||||
if not isinstance(iterable, (list, tuple)):
|
|
||||||
self._module.fail_json(msg='value must be an iterable')
|
|
||||||
|
|
||||||
return [(super(EntityCollection, self).__call__(i, strict)) for i in iterable]
|
|
||||||
|
|
||||||
|
|
||||||
# these two are for backwards compatibility and can be removed once all of the
|
|
||||||
# modules that use them are updated
|
|
||||||
class ComplexDict(Entity):
|
|
||||||
def __init__(self, attrs, module, *args, **kwargs):
|
|
||||||
super(ComplexDict, self).__init__(module, attrs, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class ComplexList(EntityCollection):
|
|
||||||
def __init__(self, attrs, module, *args, **kwargs):
|
|
||||||
super(ComplexList, self).__init__(module, attrs, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def dict_diff(base, comparable):
|
|
||||||
""" Generate a dict object of differences
|
|
||||||
|
|
||||||
This function will compare two dict objects and return the difference
|
|
||||||
between them as a dict object. For scalar values, the key will reflect
|
|
||||||
the updated value. If the key does not exist in `comparable`, then then no
|
|
||||||
key will be returned. For lists, the value in comparable will wholly replace
|
|
||||||
the value in base for the key. For dicts, the returned value will only
|
|
||||||
return keys that are different.
|
|
||||||
|
|
||||||
:param base: dict object to base the diff on
|
|
||||||
:param comparable: dict object to compare against base
|
|
||||||
|
|
||||||
:returns: new dict object with differences
|
|
||||||
"""
|
|
||||||
if not isinstance(base, dict):
|
|
||||||
raise AssertionError("`base` must be of type <dict>")
|
|
||||||
if not isinstance(comparable, dict):
|
|
||||||
if comparable is None:
|
|
||||||
comparable = dict()
|
|
||||||
else:
|
|
||||||
raise AssertionError("`comparable` must be of type <dict>")
|
|
||||||
|
|
||||||
updates = dict()
|
|
||||||
|
|
||||||
for key, value in iteritems(base):
|
|
||||||
if isinstance(value, dict):
|
|
||||||
item = comparable.get(key)
|
|
||||||
if item is not None:
|
|
||||||
sub_diff = dict_diff(value, comparable[key])
|
|
||||||
if sub_diff:
|
|
||||||
updates[key] = sub_diff
|
|
||||||
else:
|
|
||||||
comparable_value = comparable.get(key)
|
|
||||||
if comparable_value is not None:
|
|
||||||
if sort_list(base[key]) != sort_list(comparable_value):
|
|
||||||
updates[key] = comparable_value
|
|
||||||
|
|
||||||
for key in set(comparable.keys()).difference(base.keys()):
|
|
||||||
updates[key] = comparable.get(key)
|
|
||||||
|
|
||||||
return updates
|
|
||||||
|
|
||||||
|
|
||||||
def dict_merge(base, other):
|
|
||||||
""" Return a new dict object that combines base and other
|
|
||||||
|
|
||||||
This will create a new dict object that is a combination of the key/value
|
|
||||||
pairs from base and other. When both keys exist, the value will be
|
|
||||||
selected from other. If the value is a list object, the two lists will
|
|
||||||
be combined and duplicate entries removed.
|
|
||||||
|
|
||||||
:param base: dict object to serve as base
|
|
||||||
:param other: dict object to combine with base
|
|
||||||
|
|
||||||
:returns: new combined dict object
|
|
||||||
"""
|
|
||||||
if not isinstance(base, dict):
|
|
||||||
raise AssertionError("`base` must be of type <dict>")
|
|
||||||
if not isinstance(other, dict):
|
|
||||||
raise AssertionError("`other` must be of type <dict>")
|
|
||||||
|
|
||||||
combined = dict()
|
|
||||||
|
|
||||||
for key, value in iteritems(base):
|
|
||||||
if isinstance(value, dict):
|
|
||||||
if key in other:
|
|
||||||
item = other.get(key)
|
|
||||||
if item is not None:
|
|
||||||
if isinstance(other[key], Mapping):
|
|
||||||
combined[key] = dict_merge(value, other[key])
|
|
||||||
else:
|
|
||||||
combined[key] = other[key]
|
|
||||||
else:
|
|
||||||
combined[key] = item
|
|
||||||
else:
|
|
||||||
combined[key] = value
|
|
||||||
elif isinstance(value, list):
|
|
||||||
if key in other:
|
|
||||||
item = other.get(key)
|
|
||||||
if item is not None:
|
|
||||||
try:
|
|
||||||
combined[key] = list(set(chain(value, item)))
|
|
||||||
except TypeError:
|
|
||||||
value.extend([i for i in item if i not in value])
|
|
||||||
combined[key] = value
|
|
||||||
else:
|
|
||||||
combined[key] = item
|
|
||||||
else:
|
|
||||||
combined[key] = value
|
|
||||||
else:
|
|
||||||
if key in other:
|
|
||||||
other_value = other.get(key)
|
|
||||||
if other_value is not None:
|
|
||||||
if sort_list(base[key]) != sort_list(other_value):
|
|
||||||
combined[key] = other_value
|
|
||||||
else:
|
|
||||||
combined[key] = value
|
|
||||||
else:
|
|
||||||
combined[key] = other_value
|
|
||||||
else:
|
|
||||||
combined[key] = value
|
|
||||||
|
|
||||||
for key in set(other.keys()).difference(base.keys()):
|
|
||||||
combined[key] = other.get(key)
|
|
||||||
|
|
||||||
return combined
|
|
||||||
|
|
||||||
|
|
||||||
def param_list_to_dict(param_list, unique_key="name", remove_key=True):
|
|
||||||
"""Rotates a list of dictionaries to be a dictionary of dictionaries.
|
|
||||||
|
|
||||||
:param param_list: The aforementioned list of dictionaries
|
|
||||||
:param unique_key: The name of a key which is present and unique in all of param_list's dictionaries. The value
|
|
||||||
behind this key will be the key each dictionary can be found at in the new root dictionary
|
|
||||||
:param remove_key: If True, remove unique_key from the individual dictionaries before returning.
|
|
||||||
"""
|
|
||||||
param_dict = {}
|
|
||||||
for params in param_list:
|
|
||||||
params = params.copy()
|
|
||||||
if remove_key:
|
|
||||||
name = params.pop(unique_key)
|
|
||||||
else:
|
|
||||||
name = params.get(unique_key)
|
|
||||||
param_dict[name] = params
|
|
||||||
|
|
||||||
return param_dict
|
|
||||||
|
|
||||||
|
|
||||||
def conditional(expr, val, cast=None):
|
|
||||||
match = re.match(r'^(.+)\((.+)\)$', str(expr), re.I)
|
|
||||||
if match:
|
|
||||||
op, arg = match.groups()
|
|
||||||
else:
|
|
||||||
op = 'eq'
|
|
||||||
if ' ' in str(expr):
|
|
||||||
raise AssertionError('invalid expression: cannot contain spaces')
|
|
||||||
arg = expr
|
|
||||||
|
|
||||||
if cast is None and val is not None:
|
|
||||||
arg = type(val)(arg)
|
|
||||||
elif callable(cast):
|
|
||||||
arg = cast(arg)
|
|
||||||
val = cast(val)
|
|
||||||
|
|
||||||
op = next((oper for alias, oper in ALIASES if op == alias), op)
|
|
||||||
|
|
||||||
if not hasattr(operator, op) and op not in OPERATORS:
|
|
||||||
raise ValueError('unknown operator: %s' % op)
|
|
||||||
|
|
||||||
func = getattr(operator, op)
|
|
||||||
return func(val, arg)
|
|
||||||
|
|
||||||
|
|
||||||
def ternary(value, true_val, false_val):
|
|
||||||
''' value ? true_val : false_val '''
|
|
||||||
if value:
|
|
||||||
return true_val
|
|
||||||
else:
|
|
||||||
return false_val
|
|
||||||
|
|
||||||
|
|
||||||
def remove_default_spec(spec):
|
|
||||||
for item in spec:
|
|
||||||
if 'default' in spec[item]:
|
|
||||||
del spec[item]['default']
|
|
||||||
|
|
||||||
|
|
||||||
def validate_ip_address(address):
|
|
||||||
try:
|
|
||||||
socket.inet_aton(address)
|
|
||||||
except socket.error:
|
|
||||||
return False
|
|
||||||
return address.count('.') == 3
|
|
||||||
|
|
||||||
|
|
||||||
def validate_ip_v6_address(address):
|
|
||||||
try:
|
|
||||||
socket.inet_pton(socket.AF_INET6, address)
|
|
||||||
except socket.error:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def validate_prefix(prefix):
|
|
||||||
if prefix and not 0 <= int(prefix) <= 32:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def load_provider(spec, args):
|
|
||||||
provider = args.get('provider') or {}
|
|
||||||
for key, value in iteritems(spec):
|
|
||||||
if key not in provider:
|
|
||||||
if 'fallback' in value:
|
|
||||||
provider[key] = _fallback(value['fallback'])
|
|
||||||
elif 'default' in value:
|
|
||||||
provider[key] = value['default']
|
|
||||||
else:
|
|
||||||
provider[key] = None
|
|
||||||
if 'authorize' in provider:
|
|
||||||
# Coerce authorize to provider if a string has somehow snuck in.
|
|
||||||
provider['authorize'] = boolean(provider['authorize'] or False)
|
|
||||||
args['provider'] = provider
|
|
||||||
return provider
|
|
||||||
|
|
||||||
|
|
||||||
def _fallback(fallback):
|
|
||||||
strategy = fallback[0]
|
|
||||||
args = []
|
|
||||||
kwargs = {}
|
|
||||||
|
|
||||||
for item in fallback[1:]:
|
|
||||||
if isinstance(item, dict):
|
|
||||||
kwargs = item
|
|
||||||
else:
|
|
||||||
args = item
|
|
||||||
try:
|
|
||||||
return strategy(*args, **kwargs)
|
|
||||||
except basic.AnsibleFallbackNotFound:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def generate_dict(spec):
|
|
||||||
"""
|
|
||||||
Generate dictionary which is in sync with argspec
|
|
||||||
|
|
||||||
:param spec: A dictionary that is the argspec of the module
|
|
||||||
:rtype: A dictionary
|
|
||||||
:returns: A dictionary in sync with argspec with default value
|
|
||||||
"""
|
|
||||||
obj = {}
|
|
||||||
if not spec:
|
|
||||||
return obj
|
|
||||||
|
|
||||||
for key, val in iteritems(spec):
|
|
||||||
if 'default' in val:
|
|
||||||
dct = {key: val['default']}
|
|
||||||
elif 'type' in val and val['type'] == 'dict':
|
|
||||||
dct = {key: generate_dict(val['options'])}
|
|
||||||
else:
|
|
||||||
dct = {key: None}
|
|
||||||
obj.update(dct)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
def parse_conf_arg(cfg, arg):
|
|
||||||
"""
|
|
||||||
Parse config based on argument
|
|
||||||
|
|
||||||
:param cfg: A text string which is a line of configuration.
|
|
||||||
:param arg: A text string which is to be matched.
|
|
||||||
:rtype: A text string
|
|
||||||
:returns: A text string if match is found
|
|
||||||
"""
|
|
||||||
match = re.search(r'%s (.+)(\n|$)' % arg, cfg, re.M)
|
|
||||||
if match:
|
|
||||||
result = match.group(1).strip()
|
|
||||||
else:
|
|
||||||
result = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def parse_conf_cmd_arg(cfg, cmd, res1, res2=None, delete_str='no'):
|
|
||||||
"""
|
|
||||||
Parse config based on command
|
|
||||||
|
|
||||||
:param cfg: A text string which is a line of configuration.
|
|
||||||
:param cmd: A text string which is the command to be matched
|
|
||||||
:param res1: A text string to be returned if the command is present
|
|
||||||
:param res2: A text string to be returned if the negate command
|
|
||||||
is present
|
|
||||||
:param delete_str: A text string to identify the start of the
|
|
||||||
negate command
|
|
||||||
:rtype: A text string
|
|
||||||
:returns: A text string if match is found
|
|
||||||
"""
|
|
||||||
match = re.search(r'\n\s+%s(\n|$)' % cmd, cfg)
|
|
||||||
if match:
|
|
||||||
return res1
|
|
||||||
if res2 is not None:
|
|
||||||
match = re.search(r'\n\s+%s %s(\n|$)' % (delete_str, cmd), cfg)
|
|
||||||
if match:
|
|
||||||
return res2
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_xml_conf_arg(cfg, path, data='text'):
|
|
||||||
"""
|
|
||||||
:param cfg: The top level configuration lxml Element tree object
|
|
||||||
:param path: The relative xpath w.r.t to top level element (cfg)
|
|
||||||
to be searched in the xml hierarchy
|
|
||||||
:param data: The type of data to be returned for the matched xml node.
|
|
||||||
Valid values are text, tag, attrib, with default as text.
|
|
||||||
:return: Returns the required type for the matched xml node or else None
|
|
||||||
"""
|
|
||||||
match = cfg.xpath(path)
|
|
||||||
if len(match):
|
|
||||||
if data == 'tag':
|
|
||||||
result = getattr(match[0], 'tag')
|
|
||||||
elif data == 'attrib':
|
|
||||||
result = getattr(match[0], 'attrib')
|
|
||||||
else:
|
|
||||||
result = getattr(match[0], 'text')
|
|
||||||
else:
|
|
||||||
result = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def remove_empties(cfg_dict):
|
|
||||||
"""
|
|
||||||
Generate final config dictionary
|
|
||||||
|
|
||||||
:param cfg_dict: A dictionary parsed in the facts system
|
|
||||||
:rtype: A dictionary
|
|
||||||
:returns: A dictionary by eliminating keys that have null values
|
|
||||||
"""
|
|
||||||
final_cfg = {}
|
|
||||||
if not cfg_dict:
|
|
||||||
return final_cfg
|
|
||||||
|
|
||||||
for key, val in iteritems(cfg_dict):
|
|
||||||
dct = None
|
|
||||||
if isinstance(val, dict):
|
|
||||||
child_val = remove_empties(val)
|
|
||||||
if child_val:
|
|
||||||
dct = {key: child_val}
|
|
||||||
elif (isinstance(val, list) and val
|
|
||||||
and all([isinstance(x, dict) for x in val])):
|
|
||||||
child_val = [remove_empties(x) for x in val]
|
|
||||||
if child_val:
|
|
||||||
dct = {key: child_val}
|
|
||||||
elif val not in [None, [], {}, (), '']:
|
|
||||||
dct = {key: val}
|
|
||||||
if dct:
|
|
||||||
final_cfg.update(dct)
|
|
||||||
return final_cfg
|
|
||||||
|
|
||||||
|
|
||||||
def validate_config(spec, data):
|
|
||||||
"""
|
|
||||||
Validate if the input data against the AnsibleModule spec format
|
|
||||||
:param spec: Ansible argument spec
|
|
||||||
:param data: Data to be validated
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
params = basic._ANSIBLE_ARGS
|
|
||||||
basic._ANSIBLE_ARGS = to_bytes(json.dumps({'ANSIBLE_MODULE_ARGS': data}))
|
|
||||||
validated_data = basic.AnsibleModule(spec).params
|
|
||||||
basic._ANSIBLE_ARGS = params
|
|
||||||
return validated_data
|
|
||||||
|
|
||||||
|
|
||||||
def search_obj_in_list(name, lst, key='name'):
|
|
||||||
if not lst:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
for item in lst:
|
|
||||||
if item.get(key) == name:
|
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
class Template:
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if not HAS_JINJA2:
|
|
||||||
raise ImportError("jinja2 is required but does not appear to be installed. "
|
|
||||||
"It can be installed using `pip install jinja2`")
|
|
||||||
|
|
||||||
self.env = Environment(undefined=StrictUndefined)
|
|
||||||
self.env.filters.update({'ternary': ternary})
|
|
||||||
|
|
||||||
def __call__(self, value, variables=None, fail_on_undefined=True):
|
|
||||||
variables = variables or {}
|
|
||||||
|
|
||||||
if not self.contains_vars(value):
|
|
||||||
return value
|
|
||||||
|
|
||||||
try:
|
|
||||||
value = self.env.from_string(value).render(variables)
|
|
||||||
except UndefinedError:
|
|
||||||
if not fail_on_undefined:
|
|
||||||
return None
|
|
||||||
raise
|
|
||||||
|
|
||||||
if value:
|
|
||||||
try:
|
|
||||||
return ast.literal_eval(value)
|
|
||||||
except Exception:
|
|
||||||
return str(value)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def contains_vars(self, data):
|
|
||||||
if isinstance(data, string_types):
|
|
||||||
for marker in (self.env.block_start_string, self.env.variable_start_string, self.env.comment_start_string):
|
|
||||||
if marker in data:
|
|
||||||
return True
|
|
||||||
return False
|
|
@ -1,137 +0,0 @@
|
|||||||
#
|
|
||||||
# (c) 2018 Red Hat, Inc.
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
import json
|
|
||||||
|
|
||||||
from copy import deepcopy
|
|
||||||
from contextlib import contextmanager
|
|
||||||
|
|
||||||
try:
|
|
||||||
from lxml.etree import fromstring, tostring
|
|
||||||
except ImportError:
|
|
||||||
from xml.etree.ElementTree import fromstring, tostring
|
|
||||||
|
|
||||||
from ansible.module_utils._text import to_text, to_bytes
|
|
||||||
from ansible.module_utils.connection import Connection, ConnectionError
|
|
||||||
from ansible.module_utils.network.common.netconf import NetconfConnection
|
|
||||||
|
|
||||||
|
|
||||||
IGNORE_XML_ATTRIBUTE = ()
|
|
||||||
|
|
||||||
|
|
||||||
def get_connection(module):
|
|
||||||
if hasattr(module, '_netconf_connection'):
|
|
||||||
return module._netconf_connection
|
|
||||||
|
|
||||||
capabilities = get_capabilities(module)
|
|
||||||
network_api = capabilities.get('network_api')
|
|
||||||
if network_api == 'netconf':
|
|
||||||
module._netconf_connection = NetconfConnection(module._socket_path)
|
|
||||||
else:
|
|
||||||
module.fail_json(msg='Invalid connection type %s' % network_api)
|
|
||||||
|
|
||||||
return module._netconf_connection
|
|
||||||
|
|
||||||
|
|
||||||
def get_capabilities(module):
|
|
||||||
if hasattr(module, '_netconf_capabilities'):
|
|
||||||
return module._netconf_capabilities
|
|
||||||
|
|
||||||
capabilities = Connection(module._socket_path).get_capabilities()
|
|
||||||
module._netconf_capabilities = json.loads(capabilities)
|
|
||||||
return module._netconf_capabilities
|
|
||||||
|
|
||||||
|
|
||||||
def lock_configuration(module, target=None):
|
|
||||||
conn = get_connection(module)
|
|
||||||
return conn.lock(target=target)
|
|
||||||
|
|
||||||
|
|
||||||
def unlock_configuration(module, target=None):
|
|
||||||
conn = get_connection(module)
|
|
||||||
return conn.unlock(target=target)
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def locked_config(module, target=None):
|
|
||||||
try:
|
|
||||||
lock_configuration(module, target=target)
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
unlock_configuration(module, target=target)
|
|
||||||
|
|
||||||
|
|
||||||
def get_config(module, source, filter=None, lock=False):
|
|
||||||
conn = get_connection(module)
|
|
||||||
try:
|
|
||||||
locked = False
|
|
||||||
if lock:
|
|
||||||
conn.lock(target=source)
|
|
||||||
locked = True
|
|
||||||
response = conn.get_config(source=source, filter=filter)
|
|
||||||
|
|
||||||
except ConnectionError as e:
|
|
||||||
module.fail_json(msg=to_text(e, errors='surrogate_then_replace').strip())
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if locked:
|
|
||||||
conn.unlock(target=source)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def get(module, filter, lock=False):
|
|
||||||
conn = get_connection(module)
|
|
||||||
try:
|
|
||||||
locked = False
|
|
||||||
if lock:
|
|
||||||
conn.lock(target='running')
|
|
||||||
locked = True
|
|
||||||
|
|
||||||
response = conn.get(filter=filter)
|
|
||||||
|
|
||||||
except ConnectionError as e:
|
|
||||||
module.fail_json(msg=to_text(e, errors='surrogate_then_replace').strip())
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if locked:
|
|
||||||
conn.unlock(target='running')
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def dispatch(module, request):
|
|
||||||
conn = get_connection(module)
|
|
||||||
try:
|
|
||||||
response = conn.dispatch(request)
|
|
||||||
except ConnectionError as e:
|
|
||||||
module.fail_json(msg=to_text(e, errors='surrogate_then_replace').strip())
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize_xml(data):
|
|
||||||
tree = fromstring(to_bytes(deepcopy(data), errors='surrogate_then_replace'))
|
|
||||||
for element in tree.getiterator():
|
|
||||||
# remove attributes
|
|
||||||
attribute = element.attrib
|
|
||||||
if attribute:
|
|
||||||
for key in list(attribute):
|
|
||||||
if key not in IGNORE_XML_ATTRIBUTE:
|
|
||||||
attribute.pop(key)
|
|
||||||
return to_text(tostring(tree), errors='surrogate_then_replace').strip()
|
|
@ -1,57 +0,0 @@
|
|||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# (c) 2018 Red Hat Inc.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
#
|
|
||||||
|
|
||||||
from ansible.module_utils.connection import Connection
|
|
||||||
|
|
||||||
|
|
||||||
def get(module, path=None, content=None, fields=None, output='json'):
|
|
||||||
if path is None:
|
|
||||||
raise ValueError('path value must be provided')
|
|
||||||
if content:
|
|
||||||
path += '?' + 'content=%s' % content
|
|
||||||
if fields:
|
|
||||||
path += '?' + 'field=%s' % fields
|
|
||||||
|
|
||||||
accept = None
|
|
||||||
if output == 'xml':
|
|
||||||
accept = 'application/yang-data+xml'
|
|
||||||
|
|
||||||
connection = Connection(module._socket_path)
|
|
||||||
return connection.send_request(None, path=path, method='GET', accept=accept)
|
|
||||||
|
|
||||||
|
|
||||||
def edit_config(module, path=None, content=None, method='GET', format='json'):
|
|
||||||
if path is None:
|
|
||||||
raise ValueError('path value must be provided')
|
|
||||||
|
|
||||||
content_type = None
|
|
||||||
if format == 'xml':
|
|
||||||
content_type = 'application/yang-data+xml'
|
|
||||||
|
|
||||||
connection = Connection(module._socket_path)
|
|
||||||
return connection.send_request(content, path=path, method=method, content_type=content_type)
|
|
@ -1,112 +0,0 @@
|
|||||||
# this is a virtual module that is entirely implemented server side
|
|
||||||
# Copyright: Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
|
||||||
---
|
|
||||||
module: telnet
|
|
||||||
short_description: Executes a low-down and dirty telnet command
|
|
||||||
version_added: 2.4
|
|
||||||
description:
|
|
||||||
- Executes a low-down and dirty telnet command, not going through the module subsystem.
|
|
||||||
- This is mostly to be used for enabling ssh on devices that only have telnet enabled by default.
|
|
||||||
options:
|
|
||||||
command:
|
|
||||||
description:
|
|
||||||
- List of commands to be executed in the telnet session.
|
|
||||||
required: True
|
|
||||||
aliases: ['commands']
|
|
||||||
host:
|
|
||||||
description:
|
|
||||||
- The host/target on which to execute the command
|
|
||||||
required: False
|
|
||||||
default: remote_addr
|
|
||||||
user:
|
|
||||||
description:
|
|
||||||
- The user for login
|
|
||||||
required: False
|
|
||||||
default: remote_user
|
|
||||||
password:
|
|
||||||
description:
|
|
||||||
- The password for login
|
|
||||||
port:
|
|
||||||
description:
|
|
||||||
- Remote port to use
|
|
||||||
default: 23
|
|
||||||
timeout:
|
|
||||||
description:
|
|
||||||
- timeout for remote operations
|
|
||||||
default: 120
|
|
||||||
prompts:
|
|
||||||
description:
|
|
||||||
- List of prompts expected before sending next command
|
|
||||||
required: False
|
|
||||||
default: ['$']
|
|
||||||
login_prompt:
|
|
||||||
description:
|
|
||||||
- Login or username prompt to expect
|
|
||||||
required: False
|
|
||||||
default: 'login: '
|
|
||||||
password_prompt:
|
|
||||||
description:
|
|
||||||
- Login or username prompt to expect
|
|
||||||
required: False
|
|
||||||
default: 'Password: '
|
|
||||||
pause:
|
|
||||||
description:
|
|
||||||
- Seconds to pause between each command issued
|
|
||||||
required: False
|
|
||||||
default: 1
|
|
||||||
send_newline:
|
|
||||||
description:
|
|
||||||
- Sends a newline character upon successful connection to start the
|
|
||||||
terminal session.
|
|
||||||
required: False
|
|
||||||
default: False
|
|
||||||
type: bool
|
|
||||||
version_added: "2.7"
|
|
||||||
notes:
|
|
||||||
- The C(environment) keyword does not work with this task
|
|
||||||
author:
|
|
||||||
- Ansible Core Team
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = '''
|
|
||||||
- name: send configuration commands to IOS
|
|
||||||
telnet:
|
|
||||||
user: cisco
|
|
||||||
password: cisco
|
|
||||||
login_prompt: "Username: "
|
|
||||||
prompts:
|
|
||||||
- "[>#]"
|
|
||||||
command:
|
|
||||||
- terminal length 0
|
|
||||||
- configure terminal
|
|
||||||
- hostname ios01
|
|
||||||
|
|
||||||
- name: run show commands
|
|
||||||
telnet:
|
|
||||||
user: cisco
|
|
||||||
password: cisco
|
|
||||||
login_prompt: "Username: "
|
|
||||||
prompts:
|
|
||||||
- "[>#]"
|
|
||||||
command:
|
|
||||||
- terminal length 0
|
|
||||||
- show version
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = '''
|
|
||||||
output:
|
|
||||||
description: output of each command is an element in this list
|
|
||||||
type: list
|
|
||||||
returned: always
|
|
||||||
sample: [ 'success', 'success', '', 'warning .. something' ]
|
|
||||||
'''
|
|
@ -1,185 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# Copyright: Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: cli_command
|
|
||||||
version_added: "2.7"
|
|
||||||
author: "Nathaniel Case (@Qalthos)"
|
|
||||||
short_description: Run a cli command on cli-based network devices
|
|
||||||
description:
|
|
||||||
- Sends a command to a network device and returns the result read from the device.
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
command:
|
|
||||||
description:
|
|
||||||
- The command to send to the remote network device. The resulting output
|
|
||||||
from the command is returned, unless I(sendonly) is set.
|
|
||||||
required: true
|
|
||||||
prompt:
|
|
||||||
description:
|
|
||||||
- A single regex pattern or a sequence of patterns to evaluate the expected
|
|
||||||
prompt from I(command).
|
|
||||||
required: false
|
|
||||||
type: list
|
|
||||||
answer:
|
|
||||||
description:
|
|
||||||
- The answer to reply with if I(prompt) is matched. The value can be a single answer
|
|
||||||
or a list of answer for multiple prompts. In case the command execution results in
|
|
||||||
multiple prompts the sequence of the prompt and excepted answer should be in same order.
|
|
||||||
required: false
|
|
||||||
type: list
|
|
||||||
sendonly:
|
|
||||||
description:
|
|
||||||
- The boolean value, that when set to true will send I(command) to the
|
|
||||||
device but not wait for a result.
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
required: false
|
|
||||||
newline:
|
|
||||||
description:
|
|
||||||
- The boolean value, that when set to false will send I(answer) to the
|
|
||||||
device without a trailing newline.
|
|
||||||
type: bool
|
|
||||||
default: true
|
|
||||||
required: false
|
|
||||||
version_added: 2.9
|
|
||||||
check_all:
|
|
||||||
description:
|
|
||||||
- By default if any one of the prompts mentioned in C(prompt) option is matched it won't check
|
|
||||||
for other prompts. This boolean flag, that when set to I(True) will check for all the prompts
|
|
||||||
mentioned in C(prompt) option in the given order. If the option is set to I(True) all the prompts
|
|
||||||
should be received from remote host if not it will result in timeout.
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: run show version on remote devices
|
|
||||||
cli_command:
|
|
||||||
command: show version
|
|
||||||
|
|
||||||
- name: run command with json formatted output
|
|
||||||
cli_command:
|
|
||||||
command: show version | json
|
|
||||||
|
|
||||||
- name: run command expecting user confirmation
|
|
||||||
cli_command:
|
|
||||||
command: commit replace
|
|
||||||
prompt: This commit will replace or remove the entire running configuration
|
|
||||||
answer: yes
|
|
||||||
|
|
||||||
- name: run command expecting user confirmation
|
|
||||||
cli_command:
|
|
||||||
command: show interface summary
|
|
||||||
prompt: Press any key to continue
|
|
||||||
answer: y
|
|
||||||
newline: false
|
|
||||||
|
|
||||||
- name: run config mode command and handle prompt/answer
|
|
||||||
cli_command:
|
|
||||||
command: "{{ item }}"
|
|
||||||
prompt:
|
|
||||||
- "Exit with uncommitted changes"
|
|
||||||
answer: 'y'
|
|
||||||
loop:
|
|
||||||
- configure
|
|
||||||
- set system syslog file test any any
|
|
||||||
- exit
|
|
||||||
|
|
||||||
- name: multiple prompt, multiple answer (mandatory check for all prompts)
|
|
||||||
cli_command:
|
|
||||||
command: "copy sftp sftp://user@host//user/test.img"
|
|
||||||
check_all: True
|
|
||||||
prompt:
|
|
||||||
- "Confirm download operation"
|
|
||||||
- "Password"
|
|
||||||
- "Do you want to change that to the standby image"
|
|
||||||
answer:
|
|
||||||
- 'y'
|
|
||||||
- <password>
|
|
||||||
- 'y'
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
stdout:
|
|
||||||
description: The response from the command
|
|
||||||
returned: when sendonly is false
|
|
||||||
type: str
|
|
||||||
sample: 'Version: VyOS 1.1.7[...]'
|
|
||||||
|
|
||||||
json:
|
|
||||||
description: A dictionary representing a JSON-formatted response
|
|
||||||
returned: when the device response is valid JSON
|
|
||||||
type: dict
|
|
||||||
sample: |
|
|
||||||
{
|
|
||||||
"architecture": "i386",
|
|
||||||
"bootupTimestamp": 1532649700.56,
|
|
||||||
"modelName": "vEOS",
|
|
||||||
"version": "4.15.9M"
|
|
||||||
[...]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ansible.module_utils._text import to_text
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
from ansible.module_utils.connection import Connection, ConnectionError
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""entry point for module execution
|
|
||||||
"""
|
|
||||||
argument_spec = dict(
|
|
||||||
command=dict(type='str', required=True),
|
|
||||||
prompt=dict(type='list', required=False),
|
|
||||||
answer=dict(type='list', required=False),
|
|
||||||
newline=dict(type='bool', default=True, required=False),
|
|
||||||
sendonly=dict(type='bool', default=False, required=False),
|
|
||||||
check_all=dict(type='bool', default=False, required=False),
|
|
||||||
)
|
|
||||||
required_together = [['prompt', 'answer']]
|
|
||||||
module = AnsibleModule(argument_spec=argument_spec, required_together=required_together,
|
|
||||||
supports_check_mode=True)
|
|
||||||
|
|
||||||
if module.check_mode and not module.params['command'].startswith('show'):
|
|
||||||
module.fail_json(
|
|
||||||
msg='Only show commands are supported when using check_mode, not '
|
|
||||||
'executing %s' % module.params['command']
|
|
||||||
)
|
|
||||||
|
|
||||||
warnings = list()
|
|
||||||
result = {'changed': False, 'warnings': warnings}
|
|
||||||
|
|
||||||
connection = Connection(module._socket_path)
|
|
||||||
response = ''
|
|
||||||
try:
|
|
||||||
response = connection.get(**module.params)
|
|
||||||
except ConnectionError as exc:
|
|
||||||
module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
|
||||||
|
|
||||||
if not module.params['sendonly']:
|
|
||||||
try:
|
|
||||||
result['json'] = module.from_json(response)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
result.update({
|
|
||||||
'stdout': response,
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,408 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2018, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: cli_config
|
|
||||||
version_added: "2.7"
|
|
||||||
author: "Trishna Guha (@trishnaguha)"
|
|
||||||
notes:
|
|
||||||
- The commands will be returned only for platforms that do not support onbox diff.
|
|
||||||
The C(--diff) option with the playbook will return the difference in configuration for devices that has support for onbox diff
|
|
||||||
short_description: Push text based configuration to network devices over network_cli
|
|
||||||
description:
|
|
||||||
- This module provides platform agnostic way of pushing text based
|
|
||||||
configuration to network devices over network_cli connection plugin.
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
config:
|
|
||||||
description:
|
|
||||||
- The config to be pushed to the network device. This argument
|
|
||||||
is mutually exclusive with C(rollback) and either one of the
|
|
||||||
option should be given as input. The config should have
|
|
||||||
indentation that the device uses.
|
|
||||||
type: 'str'
|
|
||||||
commit:
|
|
||||||
description:
|
|
||||||
- The C(commit) argument instructs the module to push the
|
|
||||||
configuration to the device. This is mapped to module check mode.
|
|
||||||
type: 'bool'
|
|
||||||
replace:
|
|
||||||
description:
|
|
||||||
- If the C(replace) argument is set to C(yes), it will replace
|
|
||||||
the entire running-config of the device with the C(config)
|
|
||||||
argument value. For devices that support replacing running
|
|
||||||
configuration from file on device like NXOS/JUNOS, the
|
|
||||||
C(replace) argument takes path to the file on the device
|
|
||||||
that will be used for replacing the entire running-config.
|
|
||||||
The value of C(config) option should be I(None) for such devices.
|
|
||||||
Nexus 9K devices only support replace. Use I(net_put) or
|
|
||||||
I(nxos_file_copy) in case of NXOS module to copy the flat file
|
|
||||||
to remote device and then use set the fullpath to this argument.
|
|
||||||
type: 'str'
|
|
||||||
backup:
|
|
||||||
description:
|
|
||||||
- This argument will cause the module to create a full backup of
|
|
||||||
the current running config from the remote device before any
|
|
||||||
changes are made. If the C(backup_options) value is not given,
|
|
||||||
the backup file is written to the C(backup) folder in the playbook
|
|
||||||
root directory or role root directory, if playbook is part of an
|
|
||||||
ansible role. If the directory does not exist, it is created.
|
|
||||||
type: bool
|
|
||||||
default: 'no'
|
|
||||||
version_added: "2.8"
|
|
||||||
rollback:
|
|
||||||
description:
|
|
||||||
- The C(rollback) argument instructs the module to rollback the
|
|
||||||
current configuration to the identifier specified in the
|
|
||||||
argument. If the specified rollback identifier does not
|
|
||||||
exist on the remote device, the module will fail. To rollback
|
|
||||||
to the most recent commit, set the C(rollback) argument to 0.
|
|
||||||
This option is mutually exclusive with C(config).
|
|
||||||
commit_comment:
|
|
||||||
description:
|
|
||||||
- The C(commit_comment) argument specifies a text string to be used
|
|
||||||
when committing the configuration. If the C(commit) argument
|
|
||||||
is set to False, this argument is silently ignored. This argument
|
|
||||||
is only valid for the platforms that support commit operation
|
|
||||||
with comment.
|
|
||||||
type: 'str'
|
|
||||||
defaults:
|
|
||||||
description:
|
|
||||||
- The I(defaults) argument will influence how the running-config
|
|
||||||
is collected from the device. When the value is set to true,
|
|
||||||
the command used to collect the running-config is append with
|
|
||||||
the all keyword. When the value is set to false, the command
|
|
||||||
is issued without the all keyword.
|
|
||||||
default: 'no'
|
|
||||||
type: 'bool'
|
|
||||||
multiline_delimiter:
|
|
||||||
description:
|
|
||||||
- This argument is used when pushing a multiline configuration
|
|
||||||
element to the device. It specifies the character to use as
|
|
||||||
the delimiting character. This only applies to the configuration
|
|
||||||
action.
|
|
||||||
type: 'str'
|
|
||||||
diff_replace:
|
|
||||||
description:
|
|
||||||
- Instructs the module on the way to perform the configuration
|
|
||||||
on the device. If the C(diff_replace) argument is set to I(line)
|
|
||||||
then the modified lines are pushed to the device in configuration
|
|
||||||
mode. If the argument is set to I(block) then the entire command
|
|
||||||
block is pushed to the device in configuration mode if any
|
|
||||||
line is not correct. Note that this parameter will be ignored if
|
|
||||||
the platform has onbox diff support.
|
|
||||||
choices: ['line', 'block', 'config']
|
|
||||||
diff_match:
|
|
||||||
description:
|
|
||||||
- Instructs the module on the way to perform the matching of
|
|
||||||
the set of commands against the current device config. If C(diff_match)
|
|
||||||
is set to I(line), commands are matched line by line. If C(diff_match)
|
|
||||||
is set to I(strict), command lines are matched with respect to position.
|
|
||||||
If C(diff_match) is set to I(exact), command lines must be an equal match.
|
|
||||||
Finally, if C(diff_match) is set to I(none), the module will not attempt
|
|
||||||
to compare the source configuration with the running configuration on the
|
|
||||||
remote device. Note that this parameter will be ignored if the platform
|
|
||||||
has onbox diff support.
|
|
||||||
choices: ['line', 'strict', 'exact', 'none']
|
|
||||||
diff_ignore_lines:
|
|
||||||
description:
|
|
||||||
- Use this argument to specify one or more lines that should be
|
|
||||||
ignored during the diff. This is used for lines in the configuration
|
|
||||||
that are automatically updated by the system. This argument takes
|
|
||||||
a list of regular expressions or exact line matches.
|
|
||||||
Note that this parameter will be ignored if the platform has onbox
|
|
||||||
diff support.
|
|
||||||
backup_options:
|
|
||||||
description:
|
|
||||||
- This is a dict object containing configurable options related to backup file path.
|
|
||||||
The value of this option is read only when C(backup) is set to I(yes), if C(backup) is set
|
|
||||||
to I(no) this option will be silently ignored.
|
|
||||||
suboptions:
|
|
||||||
filename:
|
|
||||||
description:
|
|
||||||
- The filename to be used to store the backup configuration. If the filename
|
|
||||||
is not given it will be generated based on the hostname, current time and date
|
|
||||||
in format defined by <hostname>_config.<current-date>@<current-time>
|
|
||||||
dir_path:
|
|
||||||
description:
|
|
||||||
- This option provides the path ending with directory name in which the backup
|
|
||||||
configuration file will be stored. If the directory does not exist it will be first
|
|
||||||
created and the filename is either the value of C(filename) or default filename
|
|
||||||
as described in C(filename) options description. If the path value is not given
|
|
||||||
in that case a I(backup) directory will be created in the current working directory
|
|
||||||
and backup configuration will be copied in C(filename) within I(backup) directory.
|
|
||||||
type: path
|
|
||||||
type: dict
|
|
||||||
version_added: "2.8"
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: configure device with config
|
|
||||||
cli_config:
|
|
||||||
config: "{{ lookup('template', 'basic/config.j2') }}"
|
|
||||||
|
|
||||||
- name: multiline config
|
|
||||||
cli_config:
|
|
||||||
config: |
|
|
||||||
hostname foo
|
|
||||||
feature nxapi
|
|
||||||
|
|
||||||
- name: configure device with config with defaults enabled
|
|
||||||
cli_config:
|
|
||||||
config: "{{ lookup('template', 'basic/config.j2') }}"
|
|
||||||
defaults: yes
|
|
||||||
|
|
||||||
- name: Use diff_match
|
|
||||||
cli_config:
|
|
||||||
config: "{{ lookup('file', 'interface_config') }}"
|
|
||||||
diff_match: none
|
|
||||||
|
|
||||||
- name: nxos replace config
|
|
||||||
cli_config:
|
|
||||||
replace: 'bootflash:nxoscfg'
|
|
||||||
|
|
||||||
- name: junos replace config
|
|
||||||
cli_config:
|
|
||||||
replace: '/var/home/ansible/junos01.cfg'
|
|
||||||
|
|
||||||
- name: commit with comment
|
|
||||||
cli_config:
|
|
||||||
config: set system host-name foo
|
|
||||||
commit_comment: this is a test
|
|
||||||
|
|
||||||
- name: configurable backup path
|
|
||||||
cli_config:
|
|
||||||
config: "{{ lookup('template', 'basic/config.j2') }}"
|
|
||||||
backup: yes
|
|
||||||
backup_options:
|
|
||||||
filename: backup.cfg
|
|
||||||
dir_path: /home/user
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
commands:
|
|
||||||
description: The set of commands that will be pushed to the remote device
|
|
||||||
returned: always
|
|
||||||
type: list
|
|
||||||
sample: ['interface Loopback999', 'no shutdown']
|
|
||||||
backup_path:
|
|
||||||
description: The full path to the backup file
|
|
||||||
returned: when backup is yes
|
|
||||||
type: str
|
|
||||||
sample: /playbooks/ansible/backup/hostname_config.2016-07-16@22:28:34
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
from ansible.module_utils.connection import Connection
|
|
||||||
from ansible.module_utils._text import to_text
|
|
||||||
|
|
||||||
|
|
||||||
def validate_args(module, device_operations):
|
|
||||||
"""validate param if it is supported on the platform
|
|
||||||
"""
|
|
||||||
feature_list = [
|
|
||||||
'replace', 'rollback', 'commit_comment', 'defaults', 'multiline_delimiter',
|
|
||||||
'diff_replace', 'diff_match', 'diff_ignore_lines',
|
|
||||||
]
|
|
||||||
|
|
||||||
for feature in feature_list:
|
|
||||||
if module.params[feature]:
|
|
||||||
supports_feature = device_operations.get('supports_%s' % feature)
|
|
||||||
if supports_feature is None:
|
|
||||||
module.fail_json(
|
|
||||||
"This platform does not specify whether %s is supported or not. "
|
|
||||||
"Please report an issue against this platform's cliconf plugin." % feature
|
|
||||||
)
|
|
||||||
elif not supports_feature:
|
|
||||||
module.fail_json(msg='Option %s is not supported on this platform' % feature)
|
|
||||||
|
|
||||||
|
|
||||||
def run(module, device_operations, connection, candidate, running, rollback_id):
|
|
||||||
result = {}
|
|
||||||
resp = {}
|
|
||||||
config_diff = []
|
|
||||||
banner_diff = {}
|
|
||||||
|
|
||||||
replace = module.params['replace']
|
|
||||||
commit_comment = module.params['commit_comment']
|
|
||||||
multiline_delimiter = module.params['multiline_delimiter']
|
|
||||||
diff_replace = module.params['diff_replace']
|
|
||||||
diff_match = module.params['diff_match']
|
|
||||||
diff_ignore_lines = module.params['diff_ignore_lines']
|
|
||||||
|
|
||||||
commit = not module.check_mode
|
|
||||||
|
|
||||||
if replace in ('yes', 'true', 'True'):
|
|
||||||
replace = True
|
|
||||||
elif replace in ('no', 'false', 'False'):
|
|
||||||
replace = False
|
|
||||||
|
|
||||||
if replace is not None and replace not in [True, False] and candidate is not None:
|
|
||||||
module.fail_json(msg="Replace value '%s' is a configuration file path already"
|
|
||||||
" present on the device. Hence 'replace' and 'config' options"
|
|
||||||
" are mutually exclusive" % replace)
|
|
||||||
|
|
||||||
if rollback_id is not None:
|
|
||||||
resp = connection.rollback(rollback_id, commit)
|
|
||||||
if 'diff' in resp:
|
|
||||||
result['changed'] = True
|
|
||||||
|
|
||||||
elif device_operations.get('supports_onbox_diff'):
|
|
||||||
if diff_replace:
|
|
||||||
module.warn('diff_replace is ignored as the device supports onbox diff')
|
|
||||||
if diff_match:
|
|
||||||
module.warn('diff_mattch is ignored as the device supports onbox diff')
|
|
||||||
if diff_ignore_lines:
|
|
||||||
module.warn('diff_ignore_lines is ignored as the device supports onbox diff')
|
|
||||||
|
|
||||||
if candidate and not isinstance(candidate, list):
|
|
||||||
candidate = candidate.strip('\n').splitlines()
|
|
||||||
|
|
||||||
kwargs = {'candidate': candidate, 'commit': commit, 'replace': replace,
|
|
||||||
'comment': commit_comment}
|
|
||||||
resp = connection.edit_config(**kwargs)
|
|
||||||
|
|
||||||
if 'diff' in resp:
|
|
||||||
result['changed'] = True
|
|
||||||
|
|
||||||
elif device_operations.get('supports_generate_diff'):
|
|
||||||
kwargs = {'candidate': candidate, 'running': running}
|
|
||||||
if diff_match:
|
|
||||||
kwargs.update({'diff_match': diff_match})
|
|
||||||
if diff_replace:
|
|
||||||
kwargs.update({'diff_replace': diff_replace})
|
|
||||||
if diff_ignore_lines:
|
|
||||||
kwargs.update({'diff_ignore_lines': diff_ignore_lines})
|
|
||||||
|
|
||||||
diff_response = connection.get_diff(**kwargs)
|
|
||||||
|
|
||||||
config_diff = diff_response.get('config_diff')
|
|
||||||
banner_diff = diff_response.get('banner_diff')
|
|
||||||
|
|
||||||
if config_diff:
|
|
||||||
if isinstance(config_diff, list):
|
|
||||||
candidate = config_diff
|
|
||||||
else:
|
|
||||||
candidate = config_diff.splitlines()
|
|
||||||
|
|
||||||
kwargs = {
|
|
||||||
'candidate': candidate,
|
|
||||||
'commit': commit,
|
|
||||||
'replace': replace,
|
|
||||||
'comment': commit_comment
|
|
||||||
}
|
|
||||||
if commit:
|
|
||||||
connection.edit_config(**kwargs)
|
|
||||||
result['changed'] = True
|
|
||||||
result['commands'] = config_diff.split('\n')
|
|
||||||
|
|
||||||
if banner_diff:
|
|
||||||
candidate = json.dumps(banner_diff)
|
|
||||||
|
|
||||||
kwargs = {'candidate': candidate, 'commit': commit}
|
|
||||||
if multiline_delimiter:
|
|
||||||
kwargs.update({'multiline_delimiter': multiline_delimiter})
|
|
||||||
if commit:
|
|
||||||
connection.edit_banner(**kwargs)
|
|
||||||
result['changed'] = True
|
|
||||||
|
|
||||||
if module._diff:
|
|
||||||
if 'diff' in resp:
|
|
||||||
result['diff'] = {'prepared': resp['diff']}
|
|
||||||
else:
|
|
||||||
diff = ''
|
|
||||||
if config_diff:
|
|
||||||
if isinstance(config_diff, list):
|
|
||||||
diff += '\n'.join(config_diff)
|
|
||||||
else:
|
|
||||||
diff += config_diff
|
|
||||||
if banner_diff:
|
|
||||||
diff += json.dumps(banner_diff)
|
|
||||||
result['diff'] = {'prepared': diff}
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""main entry point for execution
|
|
||||||
"""
|
|
||||||
backup_spec = dict(
|
|
||||||
filename=dict(),
|
|
||||||
dir_path=dict(type='path')
|
|
||||||
)
|
|
||||||
argument_spec = dict(
|
|
||||||
backup=dict(default=False, type='bool'),
|
|
||||||
backup_options=dict(type='dict', options=backup_spec),
|
|
||||||
config=dict(type='str'),
|
|
||||||
commit=dict(type='bool'),
|
|
||||||
replace=dict(type='str'),
|
|
||||||
rollback=dict(type='int'),
|
|
||||||
commit_comment=dict(type='str'),
|
|
||||||
defaults=dict(default=False, type='bool'),
|
|
||||||
multiline_delimiter=dict(type='str'),
|
|
||||||
diff_replace=dict(choices=['line', 'block', 'config']),
|
|
||||||
diff_match=dict(choices=['line', 'strict', 'exact', 'none']),
|
|
||||||
diff_ignore_lines=dict(type='list')
|
|
||||||
)
|
|
||||||
|
|
||||||
mutually_exclusive = [('config', 'rollback')]
|
|
||||||
required_one_of = [['backup', 'config', 'rollback']]
|
|
||||||
|
|
||||||
module = AnsibleModule(argument_spec=argument_spec,
|
|
||||||
mutually_exclusive=mutually_exclusive,
|
|
||||||
required_one_of=required_one_of,
|
|
||||||
supports_check_mode=True)
|
|
||||||
|
|
||||||
result = {'changed': False}
|
|
||||||
|
|
||||||
connection = Connection(module._socket_path)
|
|
||||||
capabilities = module.from_json(connection.get_capabilities())
|
|
||||||
|
|
||||||
if capabilities:
|
|
||||||
device_operations = capabilities.get('device_operations', dict())
|
|
||||||
validate_args(module, device_operations)
|
|
||||||
else:
|
|
||||||
device_operations = dict()
|
|
||||||
|
|
||||||
if module.params['defaults']:
|
|
||||||
if 'get_default_flag' in capabilities.get('rpc'):
|
|
||||||
flags = connection.get_default_flag()
|
|
||||||
else:
|
|
||||||
flags = 'all'
|
|
||||||
else:
|
|
||||||
flags = []
|
|
||||||
|
|
||||||
candidate = module.params['config']
|
|
||||||
candidate = to_text(candidate, errors='surrogate_then_replace') if candidate else None
|
|
||||||
running = connection.get_config(flags=flags)
|
|
||||||
rollback_id = module.params['rollback']
|
|
||||||
|
|
||||||
if module.params['backup']:
|
|
||||||
result['__backup__'] = running
|
|
||||||
|
|
||||||
if candidate or rollback_id or module.params['replace']:
|
|
||||||
try:
|
|
||||||
result.update(run(module, device_operations, connection, candidate, running, rollback_id))
|
|
||||||
except Exception as exc:
|
|
||||||
module.fail_json(msg=to_text(exc))
|
|
||||||
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,71 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2018, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: net_get
|
|
||||||
version_added: "2.6"
|
|
||||||
author: "Deepak Agrawal (@dagrawal)"
|
|
||||||
short_description: Copy a file from a network device to Ansible Controller
|
|
||||||
description:
|
|
||||||
- This module provides functionality to copy file from network device to
|
|
||||||
ansible controller.
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
src:
|
|
||||||
description:
|
|
||||||
- Specifies the source file. The path to the source file can either be
|
|
||||||
the full path on the network device or a relative path as per path
|
|
||||||
supported by destination network device.
|
|
||||||
required: true
|
|
||||||
protocol:
|
|
||||||
description:
|
|
||||||
- Protocol used to transfer file.
|
|
||||||
default: scp
|
|
||||||
choices: ['scp', 'sftp']
|
|
||||||
dest:
|
|
||||||
description:
|
|
||||||
- Specifies the destination file. The path to the destination file can
|
|
||||||
either be the full path on the Ansible control host or a relative
|
|
||||||
path from the playbook or role root directory.
|
|
||||||
default:
|
|
||||||
- Same filename as specified in I(src). The path will be playbook root
|
|
||||||
or role root directory if playbook is part of a role.
|
|
||||||
|
|
||||||
requirements:
|
|
||||||
- "scp"
|
|
||||||
|
|
||||||
notes:
|
|
||||||
- Some devices need specific configurations to be enabled before scp can work
|
|
||||||
These configuration should be pre-configured before using this module
|
|
||||||
e.g ios - C(ip scp server enable).
|
|
||||||
- User privilege to do scp on network device should be pre-configured
|
|
||||||
e.g. ios - need user privilege 15 by default for allowing scp.
|
|
||||||
- Default destination of source file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: copy file from the network device to Ansible controller
|
|
||||||
net_get:
|
|
||||||
src: running_cfg_ios1.txt
|
|
||||||
|
|
||||||
- name: copy file from ios to common location at /tmp
|
|
||||||
net_get:
|
|
||||||
src: running_cfg_sw1.txt
|
|
||||||
dest : /tmp/ios1.txt
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
"""
|
|
@ -1,81 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2018, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: net_put
|
|
||||||
version_added: "2.6"
|
|
||||||
author: "Deepak Agrawal (@dagrawal)"
|
|
||||||
short_description: Copy a file from Ansible Controller to a network device
|
|
||||||
description:
|
|
||||||
- This module provides functionality to copy file from Ansible controller to
|
|
||||||
network devices.
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
src:
|
|
||||||
description:
|
|
||||||
- Specifies the source file. The path to the source file can either be
|
|
||||||
the full path on the Ansible control host or a relative path from the
|
|
||||||
playbook or role root directory.
|
|
||||||
required: true
|
|
||||||
protocol:
|
|
||||||
description:
|
|
||||||
- Protocol used to transfer file.
|
|
||||||
default: scp
|
|
||||||
choices: ['scp', 'sftp']
|
|
||||||
dest:
|
|
||||||
description:
|
|
||||||
- Specifies the destination file. The path to destination file can
|
|
||||||
either be the full path or relative path as supported by network_os.
|
|
||||||
default:
|
|
||||||
- Filename from src and at default directory of user shell on
|
|
||||||
network_os.
|
|
||||||
required: no
|
|
||||||
mode:
|
|
||||||
description:
|
|
||||||
- Set the file transfer mode. If mode is set to I(text) then I(src)
|
|
||||||
file will go through Jinja2 template engine to replace any vars if
|
|
||||||
present in the src file. If mode is set to I(binary) then file will be
|
|
||||||
copied as it is to destination device.
|
|
||||||
default: binary
|
|
||||||
choices: ['binary', 'text']
|
|
||||||
version_added: "2.7"
|
|
||||||
|
|
||||||
requirements:
|
|
||||||
- "scp"
|
|
||||||
|
|
||||||
notes:
|
|
||||||
- Some devices need specific configurations to be enabled before scp can work
|
|
||||||
These configuration should be pre-configured before using this module
|
|
||||||
e.g ios - C(ip scp server enable).
|
|
||||||
- User privilege to do scp on network device should be pre-configured
|
|
||||||
e.g. ios - need user privilege 15 by default for allowing scp.
|
|
||||||
- Default destination of source file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: copy file from ansible controller to a network device
|
|
||||||
net_put:
|
|
||||||
src: running_cfg_ios1.txt
|
|
||||||
|
|
||||||
- name: copy file at root dir of flash in slot 3 of sw1(ios)
|
|
||||||
net_put:
|
|
||||||
src: running_cfg_sw1.txt
|
|
||||||
protocol: sftp
|
|
||||||
dest : flash3:/running_cfg_sw1.txt
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
"""
|
|
@ -1,144 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2017, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['deprecated'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: net_interface
|
|
||||||
version_added: "2.4"
|
|
||||||
author: "Ganesh Nalawade (@ganeshrn)"
|
|
||||||
short_description: Manage Interface on network devices
|
|
||||||
description:
|
|
||||||
- This module provides declarative management of Interfaces
|
|
||||||
on network devices.
|
|
||||||
deprecated:
|
|
||||||
removed_in: "2.13"
|
|
||||||
alternative: Use platform-specific "[netos]_interfaces" module
|
|
||||||
why: Updated modules released with more functionality
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
name:
|
|
||||||
description:
|
|
||||||
- Name of the Interface.
|
|
||||||
required: true
|
|
||||||
description:
|
|
||||||
description:
|
|
||||||
- Description of Interface.
|
|
||||||
enabled:
|
|
||||||
description:
|
|
||||||
- Configure interface link status.
|
|
||||||
speed:
|
|
||||||
description:
|
|
||||||
- Interface link speed.
|
|
||||||
mtu:
|
|
||||||
description:
|
|
||||||
- Maximum size of transmit packet.
|
|
||||||
duplex:
|
|
||||||
description:
|
|
||||||
- Interface link status
|
|
||||||
default: auto
|
|
||||||
choices: ['full', 'half', 'auto']
|
|
||||||
tx_rate:
|
|
||||||
description:
|
|
||||||
- Transmit rate in bits per second (bps).
|
|
||||||
- This is state check parameter only.
|
|
||||||
- Supports conditionals, see L(Conditionals in Networking Modules,../network/user_guide/network_working_with_command_output.html)
|
|
||||||
rx_rate:
|
|
||||||
description:
|
|
||||||
- Receiver rate in bits per second (bps).
|
|
||||||
- This is state check parameter only.
|
|
||||||
- Supports conditionals, see L(Conditionals in Networking Modules,../network/user_guide/network_working_with_command_output.html)
|
|
||||||
delay:
|
|
||||||
description:
|
|
||||||
- Time in seconds to wait before checking for the operational state on remote
|
|
||||||
device. This wait is applicable for operational state argument which are
|
|
||||||
I(state) with values C(up)/C(down), I(tx_rate) and I(rx_rate).
|
|
||||||
default: 10
|
|
||||||
aggregate:
|
|
||||||
description: List of Interfaces definitions.
|
|
||||||
purge:
|
|
||||||
description:
|
|
||||||
- Purge Interfaces not defined in the aggregate parameter.
|
|
||||||
This applies only for logical interface.
|
|
||||||
default: no
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- State of the Interface configuration, C(up) indicates present and
|
|
||||||
operationally up and C(down) indicates present and operationally C(down)
|
|
||||||
default: present
|
|
||||||
choices: ['present', 'absent', 'up', 'down']
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: configure interface
|
|
||||||
net_interface:
|
|
||||||
name: ge-0/0/1
|
|
||||||
description: test-interface
|
|
||||||
|
|
||||||
- name: remove interface
|
|
||||||
net_interface:
|
|
||||||
name: ge-0/0/1
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: make interface up
|
|
||||||
net_interface:
|
|
||||||
name: ge-0/0/1
|
|
||||||
description: test-interface
|
|
||||||
enabled: True
|
|
||||||
|
|
||||||
- name: make interface down
|
|
||||||
net_interface:
|
|
||||||
name: ge-0/0/1
|
|
||||||
description: test-interface
|
|
||||||
enabled: False
|
|
||||||
|
|
||||||
- name: Create interface using aggregate
|
|
||||||
net_interface:
|
|
||||||
aggregate:
|
|
||||||
- { name: ge-0/0/1, description: test-interface-1 }
|
|
||||||
- { name: ge-0/0/2, description: test-interface-2 }
|
|
||||||
speed: 1g
|
|
||||||
duplex: full
|
|
||||||
mtu: 512
|
|
||||||
|
|
||||||
- name: Delete interface using aggregate
|
|
||||||
net_interface:
|
|
||||||
aggregate:
|
|
||||||
- { name: ge-0/0/1 }
|
|
||||||
- { name: ge-0/0/2 }
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Check intent arguments
|
|
||||||
net_interface:
|
|
||||||
name: fxp0
|
|
||||||
state: up
|
|
||||||
tx_rate: ge(0)
|
|
||||||
rx_rate: le(0)
|
|
||||||
|
|
||||||
- name: Config + intent
|
|
||||||
net_interface:
|
|
||||||
name: fxp0
|
|
||||||
enabled: False
|
|
||||||
state: down
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
commands:
|
|
||||||
description: The list of configuration mode commands to send to the device.
|
|
||||||
returned: always, except for the platforms that use Netconf transport to manage the device.
|
|
||||||
type: list
|
|
||||||
sample:
|
|
||||||
- interface 20
|
|
||||||
- name test-interface
|
|
||||||
"""
|
|
@ -1,101 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2017, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['deprecated'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: net_linkagg
|
|
||||||
version_added: "2.4"
|
|
||||||
author: "Ricardo Carrillo Cruz (@rcarrillocruz)"
|
|
||||||
short_description: Manage link aggregation groups on network devices
|
|
||||||
description:
|
|
||||||
- This module provides declarative management of link aggregation groups
|
|
||||||
on network devices.
|
|
||||||
deprecated:
|
|
||||||
removed_in: "2.13"
|
|
||||||
alternative: Use platform-specific "[netos]_lag_interfaces" module
|
|
||||||
why: Updated modules released with more functionality
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
name:
|
|
||||||
description:
|
|
||||||
- Name of the link aggregation group.
|
|
||||||
required: true
|
|
||||||
mode:
|
|
||||||
description:
|
|
||||||
- Mode of the link aggregation group. A value of C(on) will enable LACP.
|
|
||||||
C(active) configures the link to actively information about the state of the link,
|
|
||||||
or it can be configured in C(passive) mode ie. send link state information only when
|
|
||||||
received them from another link.
|
|
||||||
default: on
|
|
||||||
choices: ['on', 'active', 'passive']
|
|
||||||
members:
|
|
||||||
description:
|
|
||||||
- List of members interfaces of the link aggregation group. The value can be
|
|
||||||
single interface or list of interfaces.
|
|
||||||
required: true
|
|
||||||
min_links:
|
|
||||||
description:
|
|
||||||
- Minimum members that should be up
|
|
||||||
before bringing up the link aggregation group.
|
|
||||||
aggregate:
|
|
||||||
description: List of link aggregation definitions.
|
|
||||||
purge:
|
|
||||||
description:
|
|
||||||
- Purge link aggregation groups not defined in the I(aggregate) parameter.
|
|
||||||
default: no
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- State of the link aggregation group.
|
|
||||||
default: present
|
|
||||||
choices: ['present', 'absent', 'up', 'down']
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: configure link aggregation group
|
|
||||||
net_linkagg:
|
|
||||||
name: bond0
|
|
||||||
members:
|
|
||||||
- eth0
|
|
||||||
- eth1
|
|
||||||
|
|
||||||
- name: remove configuration
|
|
||||||
net_linkagg:
|
|
||||||
name: bond0
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Create aggregate of linkagg definitions
|
|
||||||
net_linkagg:
|
|
||||||
aggregate:
|
|
||||||
- { name: bond0, members: [eth1] }
|
|
||||||
- { name: bond1, members: [eth2] }
|
|
||||||
|
|
||||||
- name: Remove aggregate of linkagg definitions
|
|
||||||
net_linkagg:
|
|
||||||
aggregate:
|
|
||||||
- name: bond0
|
|
||||||
- name: bond1
|
|
||||||
state: absent
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
commands:
|
|
||||||
description: The list of configuration mode commands to send to the device
|
|
||||||
returned: always, except for the platforms that use Netconf transport to manage the device.
|
|
||||||
type: list
|
|
||||||
sample:
|
|
||||||
- set interfaces bonding bond0
|
|
||||||
- set interfaces ethernet eth0 bond-group 'bond0'
|
|
||||||
- set interfaces ethernet eth1 bond-group 'bond0'
|
|
||||||
"""
|
|
@ -1,89 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2017, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['deprecated'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: net_lldp_interface
|
|
||||||
version_added: "2.4"
|
|
||||||
author: "Ganesh Nalawade (@ganeshrn)"
|
|
||||||
short_description: Manage LLDP interfaces configuration on network devices
|
|
||||||
description:
|
|
||||||
- This module provides declarative management of LLDP interfaces
|
|
||||||
configuration on network devices.
|
|
||||||
deprecated:
|
|
||||||
removed_in: "2.13"
|
|
||||||
alternative: Use platform-specific "[netos]_lldp_interfaces" module
|
|
||||||
why: Updated modules released with more functionality
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
name:
|
|
||||||
description:
|
|
||||||
- Name of the interface LLDP should be configured on.
|
|
||||||
aggregate:
|
|
||||||
description: List of interfaces LLDP should be configured on.
|
|
||||||
purge:
|
|
||||||
description:
|
|
||||||
- Purge interfaces not defined in the aggregate parameter.
|
|
||||||
default: no
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- State of the LLDP configuration.
|
|
||||||
default: present
|
|
||||||
choices: ['present', 'absent', 'enabled', 'disabled']
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: Configure LLDP on specific interfaces
|
|
||||||
net_lldp_interface:
|
|
||||||
name: eth1
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Disable LLDP on specific interfaces
|
|
||||||
net_lldp_interface:
|
|
||||||
name: eth1
|
|
||||||
state: disabled
|
|
||||||
|
|
||||||
- name: Enable LLDP on specific interfaces
|
|
||||||
net_lldp_interface:
|
|
||||||
name: eth1
|
|
||||||
state: enabled
|
|
||||||
|
|
||||||
- name: Delete LLDP on specific interfaces
|
|
||||||
net_lldp_interface:
|
|
||||||
name: eth1
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Create aggregate of LLDP interface configurations
|
|
||||||
net_lldp_interface:
|
|
||||||
aggregate:
|
|
||||||
- { name: eth1 }
|
|
||||||
- { name: eth2 }
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Delete aggregate of LLDP interface configurations
|
|
||||||
net_lldp_interface:
|
|
||||||
aggregate:
|
|
||||||
- { name: eth1 }
|
|
||||||
- { name: eth2 }
|
|
||||||
state: absent
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
commands:
|
|
||||||
description: The list of configuration mode commands to send to the device
|
|
||||||
returned: always, except for the platforms that use Netconf transport to manage the device.
|
|
||||||
type: list
|
|
||||||
sample:
|
|
||||||
- set service lldp eth1 disable
|
|
||||||
"""
|
|
@ -1,82 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2017, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['deprecated'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: net_l2_interface
|
|
||||||
version_added: "2.4"
|
|
||||||
author: "Ganesh Nalawade (@ganeshrn)"
|
|
||||||
short_description: Manage Layer-2 interface on network devices
|
|
||||||
description:
|
|
||||||
- This module provides declarative management of Layer-2 interface
|
|
||||||
on network devices.
|
|
||||||
deprecated:
|
|
||||||
removed_in: "2.13"
|
|
||||||
alternative: Use platform-specific "[netos]_l2_interfaces" module
|
|
||||||
why: Updated modules released with more functionality
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
name:
|
|
||||||
description:
|
|
||||||
- Name of the interface excluding any logical unit number.
|
|
||||||
aggregate:
|
|
||||||
description:
|
|
||||||
- List of Layer-2 interface definitions.
|
|
||||||
mode:
|
|
||||||
description:
|
|
||||||
- Mode in which interface needs to be configured.
|
|
||||||
default: access
|
|
||||||
choices: ['access', 'trunk']
|
|
||||||
access_vlan:
|
|
||||||
description:
|
|
||||||
- Configure given VLAN in access port.
|
|
||||||
trunk_vlans:
|
|
||||||
description:
|
|
||||||
- List of VLANs to be configured in trunk port.
|
|
||||||
native_vlan:
|
|
||||||
description:
|
|
||||||
- Native VLAN to be configured in trunk port.
|
|
||||||
trunk_allowed_vlans:
|
|
||||||
description:
|
|
||||||
- List of allowed VLAN's in a given trunk port.
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- State of the Layer-2 Interface configuration.
|
|
||||||
default: present
|
|
||||||
choices: ['present', 'absent',]
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: configure Layer-2 interface
|
|
||||||
net_l2_interface:
|
|
||||||
name: gigabitethernet0/0/1
|
|
||||||
mode: access
|
|
||||||
access_vlan: 30
|
|
||||||
|
|
||||||
- name: remove Layer-2 interface configuration
|
|
||||||
net_l2_interface:
|
|
||||||
name: gigabitethernet0/0/1
|
|
||||||
state: absent
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
commands:
|
|
||||||
description: The list of configuration mode commands to send to the device
|
|
||||||
returned: always, except for the platforms that use Netconf transport to manage the device.
|
|
||||||
type: list
|
|
||||||
sample:
|
|
||||||
- interface gigabitethernet0/0/1
|
|
||||||
- switchport mode access
|
|
||||||
- switchport access vlan 30
|
|
||||||
"""
|
|
@ -1,78 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2017, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['deprecated'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: net_vlan
|
|
||||||
version_added: "2.4"
|
|
||||||
author: "Ricardo Carrillo Cruz (@rcarrillocruz)"
|
|
||||||
short_description: Manage VLANs on network devices
|
|
||||||
description:
|
|
||||||
- This module provides declarative management of VLANs
|
|
||||||
on network devices.
|
|
||||||
deprecated:
|
|
||||||
removed_in: "2.13"
|
|
||||||
alternative: Use platform-specific "[netos]_vlans" module
|
|
||||||
why: Updated modules released with more functionality
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
name:
|
|
||||||
description:
|
|
||||||
- Name of the VLAN.
|
|
||||||
vlan_id:
|
|
||||||
description:
|
|
||||||
- ID of the VLAN.
|
|
||||||
interfaces:
|
|
||||||
description:
|
|
||||||
- List of interfaces the VLAN should be configured on.
|
|
||||||
aggregate:
|
|
||||||
description: List of VLANs definitions.
|
|
||||||
purge:
|
|
||||||
description:
|
|
||||||
- Purge VLANs not defined in the I(aggregate) parameter.
|
|
||||||
default: no
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- State of the VLAN configuration.
|
|
||||||
default: present
|
|
||||||
choices: ['present', 'absent', 'active', 'suspend']
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: configure VLAN ID and name
|
|
||||||
net_vlan:
|
|
||||||
vlan_id: 20
|
|
||||||
name: test-vlan
|
|
||||||
|
|
||||||
- name: remove configuration
|
|
||||||
net_vlan:
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: configure VLAN state
|
|
||||||
net_vlan:
|
|
||||||
vlan_id:
|
|
||||||
state: suspend
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
commands:
|
|
||||||
description: The list of configuration mode commands to send to the device
|
|
||||||
returned: always, except for the platforms that use Netconf transport to manage the device.
|
|
||||||
type: list
|
|
||||||
sample:
|
|
||||||
- vlan 20
|
|
||||||
- name test-vlan
|
|
||||||
"""
|
|
@ -1,85 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2017, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['deprecated'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: net_l3_interface
|
|
||||||
version_added: "2.4"
|
|
||||||
author: "Ricardo Carrillo Cruz (@rcarrillocruz)"
|
|
||||||
short_description: Manage L3 interfaces on network devices
|
|
||||||
description:
|
|
||||||
- This module provides declarative management of L3 interfaces
|
|
||||||
on network devices.
|
|
||||||
deprecated:
|
|
||||||
removed_in: "2.13"
|
|
||||||
alternative: Use platform-specific "[netos]_l3_interfaces" module
|
|
||||||
why: Updated modules released with more functionality
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
name:
|
|
||||||
description:
|
|
||||||
- Name of the L3 interface.
|
|
||||||
ipv4:
|
|
||||||
description:
|
|
||||||
- IPv4 of the L3 interface.
|
|
||||||
ipv6:
|
|
||||||
description:
|
|
||||||
- IPv6 of the L3 interface.
|
|
||||||
aggregate:
|
|
||||||
description: List of L3 interfaces definitions
|
|
||||||
purge:
|
|
||||||
description:
|
|
||||||
- Purge L3 interfaces not defined in the I(aggregate) parameter.
|
|
||||||
default: no
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- State of the L3 interface configuration.
|
|
||||||
default: present
|
|
||||||
choices: ['present', 'absent']
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: Set eth0 IPv4 address
|
|
||||||
net_l3_interface:
|
|
||||||
name: eth0
|
|
||||||
ipv4: 192.168.0.1/24
|
|
||||||
|
|
||||||
- name: Remove eth0 IPv4 address
|
|
||||||
net_l3_interface:
|
|
||||||
name: eth0
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Set IP addresses on aggregate
|
|
||||||
net_l3_interface:
|
|
||||||
aggregate:
|
|
||||||
- { name: eth1, ipv4: 192.168.2.10/24 }
|
|
||||||
- { name: eth2, ipv4: 192.168.3.10/24, ipv6: "fd5d:12c9:2201:1::1/64" }
|
|
||||||
|
|
||||||
- name: Remove IP addresses on aggregate
|
|
||||||
net_l3_interface:
|
|
||||||
aggregate:
|
|
||||||
- { name: eth1, ipv4: 192.168.2.10/24 }
|
|
||||||
- { name: eth2, ipv4: 192.168.3.10/24, ipv6: "fd5d:12c9:2201:1::1/64" }
|
|
||||||
state: absent
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
commands:
|
|
||||||
description: The list of configuration mode commands to send to the device
|
|
||||||
returned: always, except for the platforms that use Netconf transport to manage the device.
|
|
||||||
type: list
|
|
||||||
sample:
|
|
||||||
- set interfaces ethernet eth0 address '192.168.0.1/24'
|
|
||||||
"""
|
|
@ -1,85 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2017, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['deprecated'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: net_vrf
|
|
||||||
version_added: "2.4"
|
|
||||||
author: "Ricardo Carrillo Cruz (@rcarrillocruz)"
|
|
||||||
short_description: Manage VRFs on network devices
|
|
||||||
description:
|
|
||||||
- This module provides declarative management of VRFs
|
|
||||||
on network devices.
|
|
||||||
deprecated:
|
|
||||||
removed_in: "2.13"
|
|
||||||
alternative: Use platform-specific "[netos]_vrf" module
|
|
||||||
why: Updated modules released with more functionality
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
name:
|
|
||||||
description:
|
|
||||||
- Name of the VRF.
|
|
||||||
interfaces:
|
|
||||||
description:
|
|
||||||
- List of interfaces the VRF should be configured on.
|
|
||||||
aggregate:
|
|
||||||
description: List of VRFs definitions
|
|
||||||
purge:
|
|
||||||
description:
|
|
||||||
- Purge VRFs not defined in the I(aggregate) parameter.
|
|
||||||
default: no
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- State of the VRF configuration.
|
|
||||||
default: present
|
|
||||||
choices: ['present', 'absent']
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: Create VRF named MANAGEMENT
|
|
||||||
net_vrf:
|
|
||||||
name: MANAGEMENT
|
|
||||||
|
|
||||||
- name: remove VRF named MANAGEMENT
|
|
||||||
net_vrf:
|
|
||||||
name: MANAGEMENT
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Create aggregate of VRFs with purge
|
|
||||||
net_vrf:
|
|
||||||
aggregate:
|
|
||||||
- { name: test4, rd: "1:204" }
|
|
||||||
- { name: test5, rd: "1:205" }
|
|
||||||
state: present
|
|
||||||
purge: yes
|
|
||||||
|
|
||||||
- name: Delete aggregate of VRFs
|
|
||||||
net_vrf:
|
|
||||||
aggregate:
|
|
||||||
- name: test2
|
|
||||||
- name: test3
|
|
||||||
- name: test4
|
|
||||||
- name: test5
|
|
||||||
state: absent
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
commands:
|
|
||||||
description: The list of configuration mode commands to send to the device
|
|
||||||
returned: always, except for the platforms that use Netconf transport to manage the device.
|
|
||||||
type: list
|
|
||||||
sample:
|
|
||||||
- vrf definition MANAGEMENT
|
|
||||||
"""
|
|
@ -1,479 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
|
|
||||||
# (c) 2016, Leandro Lisboa Penz <lpenz at lpenz.org>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
|
||||||
---
|
|
||||||
module: netconf_config
|
|
||||||
version_added: "2.2"
|
|
||||||
author: "Leandro Lisboa Penz (@lpenz)"
|
|
||||||
short_description: netconf device configuration
|
|
||||||
description:
|
|
||||||
- Netconf is a network management protocol developed and standardized by
|
|
||||||
the IETF. It is documented in RFC 6241.
|
|
||||||
- This module allows the user to send a configuration XML file to a netconf
|
|
||||||
device, and detects if there was a configuration change.
|
|
||||||
extends_documentation_fragment:
|
|
||||||
- netconf
|
|
||||||
- network_agnostic
|
|
||||||
options:
|
|
||||||
content:
|
|
||||||
description:
|
|
||||||
- The configuration data as defined by the device's data models, the value can be either in
|
|
||||||
xml string format or text format. The format of the configuration should be supported by remote
|
|
||||||
Netconf server
|
|
||||||
aliases: ['xml']
|
|
||||||
target:
|
|
||||||
description:
|
|
||||||
Name of the configuration datastore to be edited.
|
|
||||||
- auto, uses candidate and fallback to running
|
|
||||||
- candidate, edit <candidate/> datastore and then commit
|
|
||||||
- running, edit <running/> datastore directly
|
|
||||||
default: auto
|
|
||||||
version_added: "2.4"
|
|
||||||
aliases: ['datastore']
|
|
||||||
source_datastore:
|
|
||||||
description:
|
|
||||||
- Name of the configuration datastore to use as the source to copy the configuration
|
|
||||||
to the datastore mentioned by C(target) option. The values can be either I(running), I(candidate),
|
|
||||||
I(startup) or a remote URL
|
|
||||||
version_added: "2.7"
|
|
||||||
aliases: ['source']
|
|
||||||
format:
|
|
||||||
description:
|
|
||||||
- The format of the configuration provided as value of C(content). Accepted values are I(xml) and I(text) and
|
|
||||||
the given configuration format should be supported by remote Netconf server.
|
|
||||||
default: xml
|
|
||||||
choices: ['xml', 'text']
|
|
||||||
version_added: "2.7"
|
|
||||||
lock:
|
|
||||||
description:
|
|
||||||
- Instructs the module to explicitly lock the datastore specified as C(target). By setting the option
|
|
||||||
value I(always) is will explicitly lock the datastore mentioned in C(target) option. It the value
|
|
||||||
is I(never) it will not lock the C(target) datastore. The value I(if-supported) lock the C(target)
|
|
||||||
datastore only if it is supported by the remote Netconf server.
|
|
||||||
default: always
|
|
||||||
choices: ['never', 'always', 'if-supported']
|
|
||||||
version_added: "2.7"
|
|
||||||
default_operation:
|
|
||||||
description:
|
|
||||||
- The default operation for <edit-config> rpc, valid values are I(merge), I(replace) and I(none).
|
|
||||||
If the default value is merge, the configuration data in the C(content) option is merged at the
|
|
||||||
corresponding level in the C(target) datastore. If the value is replace the data in the C(content)
|
|
||||||
option completely replaces the configuration in the C(target) datastore. If the value is none the C(target)
|
|
||||||
datastore is unaffected by the configuration in the config option, unless and until the incoming configuration
|
|
||||||
data uses the C(operation) operation to request a different operation.
|
|
||||||
choices: ['merge', 'replace', 'none']
|
|
||||||
version_added: "2.7"
|
|
||||||
confirm:
|
|
||||||
description:
|
|
||||||
- This argument will configure a timeout value for the commit to be confirmed before it is automatically
|
|
||||||
rolled back. If the C(confirm_commit) argument is set to False, this argument is silently ignored. If the
|
|
||||||
value of this argument is set to 0, the commit is confirmed immediately. The remote host MUST
|
|
||||||
support :candidate and :confirmed-commit capability for this option to .
|
|
||||||
default: 0
|
|
||||||
version_added: "2.7"
|
|
||||||
confirm_commit:
|
|
||||||
description:
|
|
||||||
- This argument will execute commit operation on remote device. It can be used to confirm a previous commit.
|
|
||||||
type: bool
|
|
||||||
default: 'no'
|
|
||||||
version_added: "2.7"
|
|
||||||
error_option:
|
|
||||||
description:
|
|
||||||
- This option controls the netconf server action after an error occurs while editing the configuration.
|
|
||||||
- If I(error_option=stop-on-error), abort the config edit on first error.
|
|
||||||
- If I(error_option=continue-on-error), continue to process configuration data on error.
|
|
||||||
The error is recorded and negative response is generated if any errors occur.
|
|
||||||
- If I(error_option=rollback-on-error), rollback to the original configuration if
|
|
||||||
any error occurs.
|
|
||||||
This requires the remote Netconf server to support the I(error_option=rollback-on-error) capability.
|
|
||||||
default: stop-on-error
|
|
||||||
choices: ['stop-on-error', 'continue-on-error', 'rollback-on-error']
|
|
||||||
version_added: "2.7"
|
|
||||||
save:
|
|
||||||
description:
|
|
||||||
- The C(save) argument instructs the module to save the configuration in C(target) datastore to the
|
|
||||||
startup-config if changed and if :startup capability is supported by Netconf server.
|
|
||||||
default: false
|
|
||||||
version_added: "2.4"
|
|
||||||
type: bool
|
|
||||||
backup:
|
|
||||||
description:
|
|
||||||
- This argument will cause the module to create a full backup of
|
|
||||||
the current C(running-config) from the remote device before any
|
|
||||||
changes are made. If the C(backup_options) value is not given,
|
|
||||||
the backup file is written to the C(backup) folder in the playbook
|
|
||||||
root directory or role root directory, if playbook is part of an
|
|
||||||
ansible role. If the directory does not exist, it is created.
|
|
||||||
type: bool
|
|
||||||
default: 'no'
|
|
||||||
version_added: "2.7"
|
|
||||||
delete:
|
|
||||||
description:
|
|
||||||
- It instructs the module to delete the configuration from value mentioned in C(target) datastore.
|
|
||||||
type: bool
|
|
||||||
default: 'no'
|
|
||||||
version_added: "2.7"
|
|
||||||
commit:
|
|
||||||
description:
|
|
||||||
- This boolean flag controls if the configuration changes should be committed or not after editing the
|
|
||||||
candidate datastore. This option is supported only if remote Netconf server supports :candidate
|
|
||||||
capability. If the value is set to I(False) commit won't be issued after edit-config operation
|
|
||||||
and user needs to handle commit or discard-changes explicitly.
|
|
||||||
type: bool
|
|
||||||
default: True
|
|
||||||
version_added: "2.7"
|
|
||||||
validate:
|
|
||||||
description:
|
|
||||||
- This boolean flag if set validates the content of datastore given in C(target) option.
|
|
||||||
For this option to work remote Netconf server should support :validate capability.
|
|
||||||
type: bool
|
|
||||||
default: False
|
|
||||||
version_added: "2.7"
|
|
||||||
src:
|
|
||||||
description:
|
|
||||||
- Specifies the source path to the xml file that contains the configuration or configuration template
|
|
||||||
to load. The path to the source file can either be the full path on the Ansible control host or
|
|
||||||
a relative path from the playbook or role root directory. This argument is mutually exclusive with I(xml).
|
|
||||||
version_added: "2.4"
|
|
||||||
backup_options:
|
|
||||||
description:
|
|
||||||
- This is a dict object containing configurable options related to backup file path.
|
|
||||||
The value of this option is read only when C(backup) is set to I(yes), if C(backup) is set
|
|
||||||
to I(no) this option will be silently ignored.
|
|
||||||
suboptions:
|
|
||||||
filename:
|
|
||||||
description:
|
|
||||||
- The filename to be used to store the backup configuration. If the filename
|
|
||||||
is not given it will be generated based on the hostname, current time and date
|
|
||||||
in format defined by <hostname>_config.<current-date>@<current-time>
|
|
||||||
dir_path:
|
|
||||||
description:
|
|
||||||
- This option provides the path ending with directory name in which the backup
|
|
||||||
configuration file will be stored. If the directory does not exist it will be first
|
|
||||||
created and the filename is either the value of C(filename) or default filename
|
|
||||||
as described in C(filename) options description. If the path value is not given
|
|
||||||
in that case a I(backup) directory will be created in the current working directory
|
|
||||||
and backup configuration will be copied in C(filename) within I(backup) directory.
|
|
||||||
type: path
|
|
||||||
type: dict
|
|
||||||
version_added: "2.8"
|
|
||||||
get_filter:
|
|
||||||
description:
|
|
||||||
- This argument specifies the XML string which acts as a filter to restrict the portions of
|
|
||||||
the data retrieved from the remote device when comparing the before and after state of the
|
|
||||||
device following calls to edit_config. When not specified, the entire configuration or
|
|
||||||
state data is returned for comparison depending on the value of C(source) option. The C(get_filter)
|
|
||||||
value can be either XML string or XPath, if the filter is in XPath format the NETCONF server
|
|
||||||
running on remote host should support xpath capability else it will result in an error.
|
|
||||||
version_added: "2.10"
|
|
||||||
requirements:
|
|
||||||
- "ncclient"
|
|
||||||
notes:
|
|
||||||
- This module requires the netconf system service be enabled on
|
|
||||||
the remote device being managed.
|
|
||||||
- This module supports devices with and without the candidate and
|
|
||||||
confirmed-commit capabilities. It will always use the safer feature.
|
|
||||||
- This module supports the use of connection=netconf
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = '''
|
|
||||||
- name: use lookup filter to provide xml configuration
|
|
||||||
netconf_config:
|
|
||||||
content: "{{ lookup('file', './config.xml') }}"
|
|
||||||
|
|
||||||
- name: set ntp server in the device
|
|
||||||
netconf_config:
|
|
||||||
content: |
|
|
||||||
<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
|
|
||||||
<system xmlns="urn:ietf:params:xml:ns:yang:ietf-system">
|
|
||||||
<ntp>
|
|
||||||
<enabled>true</enabled>
|
|
||||||
<server>
|
|
||||||
<name>ntp1</name>
|
|
||||||
<udp><address>127.0.0.1</address></udp>
|
|
||||||
</server>
|
|
||||||
</ntp>
|
|
||||||
</system>
|
|
||||||
</config>
|
|
||||||
|
|
||||||
- name: wipe ntp configuration
|
|
||||||
netconf_config:
|
|
||||||
content: |
|
|
||||||
<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
|
|
||||||
<system xmlns="urn:ietf:params:xml:ns:yang:ietf-system">
|
|
||||||
<ntp>
|
|
||||||
<enabled>false</enabled>
|
|
||||||
<server operation="remove">
|
|
||||||
<name>ntp1</name>
|
|
||||||
</server>
|
|
||||||
</ntp>
|
|
||||||
</system>
|
|
||||||
</config>
|
|
||||||
|
|
||||||
- name: configure interface while providing different private key file path (for connection=netconf)
|
|
||||||
netconf_config:
|
|
||||||
backup: yes
|
|
||||||
register: backup_junos_location
|
|
||||||
vars:
|
|
||||||
ansible_private_key_file: /home/admin/.ssh/newprivatekeyfile
|
|
||||||
|
|
||||||
- name: configurable backup path
|
|
||||||
netconf_config:
|
|
||||||
backup: yes
|
|
||||||
backup_options:
|
|
||||||
filename: backup.cfg
|
|
||||||
dir_path: /home/user
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = '''
|
|
||||||
server_capabilities:
|
|
||||||
description: list of capabilities of the server
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
sample: ['urn:ietf:params:netconf:base:1.1','urn:ietf:params:netconf:capability:confirmed-commit:1.0','urn:ietf:params:netconf:capability:candidate:1.0']
|
|
||||||
backup_path:
|
|
||||||
description: The full path to the backup file
|
|
||||||
returned: when backup is yes
|
|
||||||
type: str
|
|
||||||
sample: /playbooks/ansible/backup/config.2016-07-16@22:28:34
|
|
||||||
diff:
|
|
||||||
description: If --diff option in enabled while running, the before and after configuration change are
|
|
||||||
returned as part of before and after key.
|
|
||||||
returned: when diff is enabled
|
|
||||||
type: dict
|
|
||||||
sample:
|
|
||||||
"after": "<rpc-reply>\n<data>\n<configuration>\n<version>17.3R1.10</version>...<--snip-->"
|
|
||||||
"before": "<rpc-reply>\n<data>\n<configuration>\n <version>17.3R1.10</version>...<--snip-->"
|
|
||||||
'''
|
|
||||||
|
|
||||||
from ansible.module_utils._text import to_text
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
|
||||||
from ansible.module_utils.connection import Connection, ConnectionError
|
|
||||||
from ansible.module_utils.network.netconf.netconf import get_capabilities, get_config, sanitize_xml
|
|
||||||
|
|
||||||
import sys
|
|
||||||
try:
|
|
||||||
from lxml.etree import tostring, fromstring, XMLSyntaxError
|
|
||||||
except ImportError:
|
|
||||||
from xml.etree.ElementTree import tostring, fromstring
|
|
||||||
if sys.version_info < (2, 7):
|
|
||||||
from xml.parsers.expat import ExpatError as XMLSyntaxError
|
|
||||||
else:
|
|
||||||
from xml.etree.ElementTree import ParseError as XMLSyntaxError
|
|
||||||
|
|
||||||
|
|
||||||
def get_filter_type(filter):
|
|
||||||
if not filter:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
fromstring(filter)
|
|
||||||
return 'subtree'
|
|
||||||
except XMLSyntaxError:
|
|
||||||
return 'xpath'
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
""" main entry point for module execution
|
|
||||||
"""
|
|
||||||
backup_spec = dict(
|
|
||||||
filename=dict(),
|
|
||||||
dir_path=dict(type='path')
|
|
||||||
)
|
|
||||||
argument_spec = dict(
|
|
||||||
content=dict(aliases=['xml']),
|
|
||||||
target=dict(choices=['auto', 'candidate', 'running'], default='auto', aliases=['datastore']),
|
|
||||||
source_datastore=dict(aliases=['source']),
|
|
||||||
format=dict(choices=['xml', 'text'], default='xml'),
|
|
||||||
lock=dict(choices=['never', 'always', 'if-supported'], default='always'),
|
|
||||||
default_operation=dict(choices=['merge', 'replace', 'none']),
|
|
||||||
confirm=dict(type='int', default=0),
|
|
||||||
confirm_commit=dict(type='bool', default=False),
|
|
||||||
error_option=dict(choices=['stop-on-error', 'continue-on-error', 'rollback-on-error'], default='stop-on-error'),
|
|
||||||
backup=dict(type='bool', default=False),
|
|
||||||
backup_options=dict(type='dict', options=backup_spec),
|
|
||||||
save=dict(type='bool', default=False),
|
|
||||||
delete=dict(type='bool', default=False),
|
|
||||||
commit=dict(type='bool', default=True),
|
|
||||||
validate=dict(type='bool', default=False),
|
|
||||||
get_filter=dict(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# deprecated options
|
|
||||||
netconf_top_spec = {
|
|
||||||
'src': dict(type='path', removed_in_version=2.11),
|
|
||||||
'host': dict(removed_in_version=2.11),
|
|
||||||
'port': dict(removed_in_version=2.11, type='int', default=830),
|
|
||||||
'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME']), removed_in_version=2.11, no_log=True),
|
|
||||||
'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), removed_in_version=2.11, no_log=True),
|
|
||||||
'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), removed_in_version=2.11, type='path'),
|
|
||||||
'hostkey_verify': dict(removed_in_version=2.11, type='bool', default=True),
|
|
||||||
'look_for_keys': dict(removed_in_version=2.11, type='bool', default=True),
|
|
||||||
'timeout': dict(removed_in_version=2.11, type='int', default=10),
|
|
||||||
}
|
|
||||||
argument_spec.update(netconf_top_spec)
|
|
||||||
|
|
||||||
mutually_exclusive = [('content', 'src', 'source', 'delete', 'confirm_commit')]
|
|
||||||
required_one_of = [('content', 'src', 'source', 'delete', 'confirm_commit')]
|
|
||||||
|
|
||||||
module = AnsibleModule(argument_spec=argument_spec,
|
|
||||||
required_one_of=required_one_of,
|
|
||||||
mutually_exclusive=mutually_exclusive,
|
|
||||||
supports_check_mode=True)
|
|
||||||
|
|
||||||
if module.params['src']:
|
|
||||||
module.deprecate(msg="argument 'src' has been deprecated. Use file lookup plugin instead to read file contents.",
|
|
||||||
version="2.11")
|
|
||||||
|
|
||||||
config = module.params['content'] or module.params['src']
|
|
||||||
target = module.params['target']
|
|
||||||
lock = module.params['lock']
|
|
||||||
source = module.params['source_datastore']
|
|
||||||
delete = module.params['delete']
|
|
||||||
confirm_commit = module.params['confirm_commit']
|
|
||||||
confirm = module.params['confirm']
|
|
||||||
validate = module.params['validate']
|
|
||||||
save = module.params['save']
|
|
||||||
filter = module.params['get_filter']
|
|
||||||
filter_type = get_filter_type(filter)
|
|
||||||
|
|
||||||
conn = Connection(module._socket_path)
|
|
||||||
capabilities = get_capabilities(module)
|
|
||||||
operations = capabilities['device_operations']
|
|
||||||
|
|
||||||
supports_commit = operations.get('supports_commit', False)
|
|
||||||
supports_writable_running = operations.get('supports_writable_running', False)
|
|
||||||
supports_startup = operations.get('supports_startup', False)
|
|
||||||
|
|
||||||
# identify target datastore
|
|
||||||
if target == 'candidate' and not supports_commit:
|
|
||||||
module.fail_json(msg=':candidate is not supported by this netconf server')
|
|
||||||
elif target == 'running' and not supports_writable_running:
|
|
||||||
module.fail_json(msg=':writable-running is not supported by this netconf server')
|
|
||||||
elif target == 'auto':
|
|
||||||
if supports_commit:
|
|
||||||
target = 'candidate'
|
|
||||||
elif supports_writable_running:
|
|
||||||
target = 'running'
|
|
||||||
else:
|
|
||||||
module.fail_json(msg='neither :candidate nor :writable-running are supported by this netconf server')
|
|
||||||
|
|
||||||
# Netconf server capability validation against input options
|
|
||||||
if save and not supports_startup:
|
|
||||||
module.fail_json(msg='cannot copy <%s/> to <startup/>, while :startup is not supported' % target)
|
|
||||||
|
|
||||||
if confirm_commit and not operations.get('supports_confirm_commit', False):
|
|
||||||
module.fail_json(msg='confirm commit is not supported by Netconf server')
|
|
||||||
|
|
||||||
if (confirm > 0) and not operations.get('supports_confirm_commit', False):
|
|
||||||
module.fail_json(msg='confirm commit is not supported by this netconf server, given confirm timeout: %d' % confirm)
|
|
||||||
|
|
||||||
if validate and not operations.get('supports_validate', False):
|
|
||||||
module.fail_json(msg='validate is not supported by this netconf server')
|
|
||||||
|
|
||||||
if filter_type == 'xpath' and not operations.get('supports_xpath', False):
|
|
||||||
module.fail_json(msg="filter value '%s' of type xpath is not supported on this device" % filter)
|
|
||||||
|
|
||||||
filter_spec = (filter_type, filter) if filter_type else None
|
|
||||||
|
|
||||||
if lock == 'never':
|
|
||||||
execute_lock = False
|
|
||||||
elif target in operations.get('lock_datastore', []):
|
|
||||||
# lock is requested (always/if-support) and supported => lets do it
|
|
||||||
execute_lock = True
|
|
||||||
else:
|
|
||||||
# lock is requested (always/if-supported) but not supported => issue warning
|
|
||||||
module.warn("lock operation on '%s' source is not supported on this device" % target)
|
|
||||||
execute_lock = (lock == 'always')
|
|
||||||
|
|
||||||
result = {'changed': False, 'server_capabilities': capabilities.get('server_capabilities', [])}
|
|
||||||
before = None
|
|
||||||
after = None
|
|
||||||
locked = False
|
|
||||||
try:
|
|
||||||
if module.params['backup']:
|
|
||||||
response = get_config(module, target, filter_spec, lock=execute_lock)
|
|
||||||
before = to_text(tostring(response), errors='surrogate_then_replace').strip()
|
|
||||||
result['__backup__'] = before.strip()
|
|
||||||
if validate:
|
|
||||||
conn.validate(target)
|
|
||||||
if source:
|
|
||||||
if not module.check_mode:
|
|
||||||
conn.copy(source, target)
|
|
||||||
result['changed'] = True
|
|
||||||
elif delete:
|
|
||||||
if not module.check_mode:
|
|
||||||
conn.delete(target)
|
|
||||||
result['changed'] = True
|
|
||||||
elif confirm_commit:
|
|
||||||
if not module.check_mode:
|
|
||||||
conn.commit()
|
|
||||||
result['changed'] = True
|
|
||||||
elif config:
|
|
||||||
if module.check_mode and not supports_commit:
|
|
||||||
module.warn("check mode not supported as Netconf server doesn't support candidate capability")
|
|
||||||
result['changed'] = True
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
if execute_lock:
|
|
||||||
conn.lock(target=target)
|
|
||||||
locked = True
|
|
||||||
if before is None:
|
|
||||||
before = to_text(conn.get_config(source=target, filter=filter_spec), errors='surrogate_then_replace').strip()
|
|
||||||
|
|
||||||
kwargs = {
|
|
||||||
'config': config,
|
|
||||||
'target': target,
|
|
||||||
'default_operation': module.params['default_operation'],
|
|
||||||
'error_option': module.params['error_option'],
|
|
||||||
'format': module.params['format'],
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.edit_config(**kwargs)
|
|
||||||
|
|
||||||
if supports_commit and module.params['commit']:
|
|
||||||
after = to_text(conn.get_config(source='candidate', filter=filter_spec), errors='surrogate_then_replace').strip()
|
|
||||||
if not module.check_mode:
|
|
||||||
confirm_timeout = confirm if confirm > 0 else None
|
|
||||||
confirmed_commit = True if confirm_timeout else False
|
|
||||||
conn.commit(confirmed=confirmed_commit, timeout=confirm_timeout)
|
|
||||||
else:
|
|
||||||
conn.discard_changes()
|
|
||||||
|
|
||||||
if after is None:
|
|
||||||
after = to_text(conn.get_config(source='running', filter=filter_spec), errors='surrogate_then_replace').strip()
|
|
||||||
|
|
||||||
sanitized_before = sanitize_xml(before)
|
|
||||||
sanitized_after = sanitize_xml(after)
|
|
||||||
if sanitized_before != sanitized_after:
|
|
||||||
result['changed'] = True
|
|
||||||
|
|
||||||
if result['changed']:
|
|
||||||
if save and not module.check_mode:
|
|
||||||
conn.copy_config(target, 'startup')
|
|
||||||
if module._diff:
|
|
||||||
result['diff'] = {'before': sanitized_before, 'after': sanitized_after}
|
|
||||||
|
|
||||||
except ConnectionError as e:
|
|
||||||
module.fail_json(msg=to_text(e, errors='surrogate_then_replace').strip())
|
|
||||||
finally:
|
|
||||||
if locked:
|
|
||||||
conn.unlock(target=target)
|
|
||||||
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,261 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2018, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: netconf_get
|
|
||||||
version_added: "2.6"
|
|
||||||
author:
|
|
||||||
- "Ganesh Nalawade (@ganeshrn)"
|
|
||||||
- "Sven Wisotzky (@wisotzky)"
|
|
||||||
short_description: Fetch configuration/state data from NETCONF enabled network devices.
|
|
||||||
description:
|
|
||||||
- NETCONF is a network management protocol developed and standardized by
|
|
||||||
the IETF. It is documented in RFC 6241.
|
|
||||||
- This module allows the user to fetch configuration and state data from NETCONF
|
|
||||||
enabled network devices.
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
source:
|
|
||||||
description:
|
|
||||||
- This argument specifies the datastore from which configuration data should be fetched.
|
|
||||||
Valid values are I(running), I(candidate) and I(startup). If the C(source) value is not
|
|
||||||
set both configuration and state information are returned in response from running datastore.
|
|
||||||
choices: ['running', 'candidate', 'startup']
|
|
||||||
filter:
|
|
||||||
description:
|
|
||||||
- This argument specifies the XML string which acts as a filter to restrict the portions of
|
|
||||||
the data to be are retrieved from the remote device. If this option is not specified entire
|
|
||||||
configuration or state data is returned in result depending on the value of C(source)
|
|
||||||
option. The C(filter) value can be either XML string or XPath, if the filter is in
|
|
||||||
XPath format the NETCONF server running on remote host should support xpath capability
|
|
||||||
else it will result in an error.
|
|
||||||
display:
|
|
||||||
description:
|
|
||||||
- Encoding scheme to use when serializing output from the device. The option I(json) will
|
|
||||||
serialize the output as JSON data. If the option value is I(json) it requires jxmlease
|
|
||||||
to be installed on control node. The option I(pretty) is similar to received XML response
|
|
||||||
but is using human readable format (spaces, new lines). The option value I(xml) is similar
|
|
||||||
to received XML response but removes all XML namespaces.
|
|
||||||
choices: ['json', 'pretty', 'xml']
|
|
||||||
lock:
|
|
||||||
description:
|
|
||||||
- Instructs the module to explicitly lock the datastore specified as C(source). If no
|
|
||||||
I(source) is defined, the I(running) datastore will be locked. By setting the option
|
|
||||||
value I(always) is will explicitly lock the datastore mentioned in C(source) option.
|
|
||||||
By setting the option value I(never) it will not lock the C(source) datastore. The
|
|
||||||
value I(if-supported) allows better interworking with NETCONF servers, which do not
|
|
||||||
support the (un)lock operation for all supported datastores.
|
|
||||||
default: never
|
|
||||||
choices: ['never', 'always', 'if-supported']
|
|
||||||
requirements:
|
|
||||||
- ncclient (>=v0.5.2)
|
|
||||||
- jxmlease
|
|
||||||
|
|
||||||
notes:
|
|
||||||
- This module requires the NETCONF system service be enabled on
|
|
||||||
the remote device being managed.
|
|
||||||
- This module supports the use of connection=netconf
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: Get running configuration and state data
|
|
||||||
netconf_get:
|
|
||||||
|
|
||||||
- name: Get configuration and state data from startup datastore
|
|
||||||
netconf_get:
|
|
||||||
source: startup
|
|
||||||
|
|
||||||
- name: Get system configuration data from running datastore state (junos)
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
filter: <configuration><system></system></configuration>
|
|
||||||
|
|
||||||
- name: Get configuration and state data in JSON format
|
|
||||||
netconf_get:
|
|
||||||
display: json
|
|
||||||
|
|
||||||
- name: get schema list using subtree w/ namespaces
|
|
||||||
netconf_get:
|
|
||||||
display: json
|
|
||||||
filter: <netconf-state xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring"><schemas><schema/></schemas></netconf-state>
|
|
||||||
lock: never
|
|
||||||
|
|
||||||
- name: get schema list using xpath
|
|
||||||
netconf_get:
|
|
||||||
display: xml
|
|
||||||
filter: /netconf-state/schemas/schema
|
|
||||||
|
|
||||||
- name: get interface configuration with filter (iosxr)
|
|
||||||
netconf_get:
|
|
||||||
display: pretty
|
|
||||||
filter: <interface-configurations xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg"></interface-configurations>
|
|
||||||
lock: if-supported
|
|
||||||
|
|
||||||
- name: Get system configuration data from running datastore state (junos)
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
filter: <configuration><system></system></configuration>
|
|
||||||
lock: if-supported
|
|
||||||
|
|
||||||
- name: Get complete configuration data from running datastore (SROS)
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
filter: <configure xmlns="urn:nokia.com:sros:ns:yang:sr:conf"/>
|
|
||||||
|
|
||||||
- name: Get complete state data (SROS)
|
|
||||||
netconf_get:
|
|
||||||
filter: <state xmlns="urn:nokia.com:sros:ns:yang:sr:state"/>
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
stdout:
|
|
||||||
description: The raw XML string containing configuration or state data
|
|
||||||
received from the underlying ncclient library.
|
|
||||||
returned: always apart from low-level errors (such as action plugin)
|
|
||||||
type: str
|
|
||||||
sample: '...'
|
|
||||||
stdout_lines:
|
|
||||||
description: The value of stdout split into a list
|
|
||||||
returned: always apart from low-level errors (such as action plugin)
|
|
||||||
type: list
|
|
||||||
sample: ['...', '...']
|
|
||||||
output:
|
|
||||||
description: Based on the value of display option will return either the set of
|
|
||||||
transformed XML to JSON format from the RPC response with type dict
|
|
||||||
or pretty XML string response (human-readable) or response with
|
|
||||||
namespace removed from XML string.
|
|
||||||
returned: when the display format is selected as JSON it is returned as dict type, if the
|
|
||||||
display format is xml or pretty pretty it is returned as a string apart from low-level
|
|
||||||
errors (such as action plugin).
|
|
||||||
type: complex
|
|
||||||
contains:
|
|
||||||
formatted_output:
|
|
||||||
- Contains formatted response received from remote host as per the value in display format.
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
|
|
||||||
try:
|
|
||||||
from lxml.etree import tostring, fromstring, XMLSyntaxError
|
|
||||||
except ImportError:
|
|
||||||
from xml.etree.ElementTree import tostring, fromstring
|
|
||||||
if sys.version_info < (2, 7):
|
|
||||||
from xml.parsers.expat import ExpatError as XMLSyntaxError
|
|
||||||
else:
|
|
||||||
from xml.etree.ElementTree import ParseError as XMLSyntaxError
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
from ansible.module_utils.network.netconf.netconf import get_capabilities, get_config, get
|
|
||||||
from ansible.module_utils.network.common.netconf import remove_namespaces
|
|
||||||
from ansible.module_utils._text import to_text
|
|
||||||
|
|
||||||
try:
|
|
||||||
import jxmlease
|
|
||||||
HAS_JXMLEASE = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_JXMLEASE = False
|
|
||||||
|
|
||||||
|
|
||||||
def get_filter_type(filter):
|
|
||||||
if not filter:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
fromstring(filter)
|
|
||||||
return 'subtree'
|
|
||||||
except XMLSyntaxError:
|
|
||||||
return 'xpath'
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""entry point for module execution
|
|
||||||
"""
|
|
||||||
argument_spec = dict(
|
|
||||||
source=dict(choices=['running', 'candidate', 'startup']),
|
|
||||||
filter=dict(),
|
|
||||||
display=dict(choices=['json', 'pretty', 'xml']),
|
|
||||||
lock=dict(default='never', choices=['never', 'always', 'if-supported'])
|
|
||||||
)
|
|
||||||
|
|
||||||
module = AnsibleModule(argument_spec=argument_spec,
|
|
||||||
supports_check_mode=True)
|
|
||||||
|
|
||||||
capabilities = get_capabilities(module)
|
|
||||||
operations = capabilities['device_operations']
|
|
||||||
|
|
||||||
source = module.params['source']
|
|
||||||
filter = module.params['filter']
|
|
||||||
filter_type = get_filter_type(filter)
|
|
||||||
lock = module.params['lock']
|
|
||||||
display = module.params['display']
|
|
||||||
|
|
||||||
if source == 'candidate' and not operations.get('supports_commit', False):
|
|
||||||
module.fail_json(msg='candidate source is not supported on this device')
|
|
||||||
|
|
||||||
if source == 'startup' and not operations.get('supports_startup', False):
|
|
||||||
module.fail_json(msg='startup source is not supported on this device')
|
|
||||||
|
|
||||||
if filter_type == 'xpath' and not operations.get('supports_xpath', False):
|
|
||||||
module.fail_json(msg="filter value '%s' of type xpath is not supported on this device" % filter)
|
|
||||||
|
|
||||||
# If source is None, NETCONF <get> operation is issued, reading config/state data
|
|
||||||
# from the running datastore. The python expression "(source or 'running')" results
|
|
||||||
# in the value of source (if not None) or the value 'running' (if source is None).
|
|
||||||
|
|
||||||
if lock == 'never':
|
|
||||||
execute_lock = False
|
|
||||||
elif (source or 'running') in operations.get('lock_datastore', []):
|
|
||||||
# lock is requested (always/if-support) and supported => lets do it
|
|
||||||
execute_lock = True
|
|
||||||
else:
|
|
||||||
# lock is requested (always/if-supported) but not supported => issue warning
|
|
||||||
module.warn("lock operation on '%s' source is not supported on this device" % (source or 'running'))
|
|
||||||
execute_lock = (lock == 'always')
|
|
||||||
|
|
||||||
if display == 'json' and not HAS_JXMLEASE:
|
|
||||||
module.fail_json(msg='jxmlease is required to display response in json format'
|
|
||||||
'but does not appear to be installed. '
|
|
||||||
'It can be installed using `pip install jxmlease`')
|
|
||||||
|
|
||||||
filter_spec = (filter_type, filter) if filter_type else None
|
|
||||||
|
|
||||||
if source is not None:
|
|
||||||
response = get_config(module, source, filter_spec, execute_lock)
|
|
||||||
else:
|
|
||||||
response = get(module, filter_spec, execute_lock)
|
|
||||||
|
|
||||||
xml_resp = to_text(tostring(response))
|
|
||||||
output = None
|
|
||||||
|
|
||||||
if display == 'xml':
|
|
||||||
output = remove_namespaces(xml_resp)
|
|
||||||
elif display == 'json':
|
|
||||||
try:
|
|
||||||
output = jxmlease.parse(xml_resp)
|
|
||||||
except Exception:
|
|
||||||
raise ValueError(xml_resp)
|
|
||||||
elif display == 'pretty':
|
|
||||||
output = to_text(tostring(response, pretty_print=True))
|
|
||||||
|
|
||||||
result = {
|
|
||||||
'stdout': xml_resp,
|
|
||||||
'output': output
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,264 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2018, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: netconf_rpc
|
|
||||||
version_added: "2.6"
|
|
||||||
author:
|
|
||||||
- "Ganesh Nalawade (@ganeshrn)"
|
|
||||||
- "Sven Wisotzky (@wisotzky)"
|
|
||||||
short_description: Execute operations on NETCONF enabled network devices.
|
|
||||||
description:
|
|
||||||
- NETCONF is a network management protocol developed and standardized by
|
|
||||||
the IETF. It is documented in RFC 6241.
|
|
||||||
- This module allows the user to execute NETCONF RPC requests as defined
|
|
||||||
by IETF RFC standards as well as proprietary requests.
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
rpc:
|
|
||||||
description:
|
|
||||||
- This argument specifies the request (name of the operation) to be executed on
|
|
||||||
the remote NETCONF enabled device.
|
|
||||||
xmlns:
|
|
||||||
description:
|
|
||||||
- NETCONF operations not defined in rfc6241 typically require the appropriate
|
|
||||||
XML namespace to be set. In the case the I(request) option is not already
|
|
||||||
provided in XML format, the namespace can be defined by the I(xmlns)
|
|
||||||
option.
|
|
||||||
content:
|
|
||||||
description:
|
|
||||||
- This argument specifies the optional request content (all RPC attributes).
|
|
||||||
The I(content) value can either be provided as XML formatted string or as
|
|
||||||
dictionary.
|
|
||||||
display:
|
|
||||||
description:
|
|
||||||
- Encoding scheme to use when serializing output from the device. The option I(json) will
|
|
||||||
serialize the output as JSON data. If the option value is I(json) it requires jxmlease
|
|
||||||
to be installed on control node. The option I(pretty) is similar to received XML response
|
|
||||||
but is using human readable format (spaces, new lines). The option value I(xml) is similar
|
|
||||||
to received XML response but removes all XML namespaces.
|
|
||||||
choices: ['json', 'pretty', 'xml']
|
|
||||||
requirements:
|
|
||||||
- ncclient (>=v0.5.2)
|
|
||||||
- jxmlease
|
|
||||||
|
|
||||||
notes:
|
|
||||||
- This module requires the NETCONF system service be enabled on the remote device
|
|
||||||
being managed.
|
|
||||||
- This module supports the use of connection=netconf
|
|
||||||
- To execute C(get-config), C(get) or C(edit-config) requests it is recommended
|
|
||||||
to use the Ansible I(netconf_get) and I(netconf_config) modules.
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: lock candidate
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: lock
|
|
||||||
content:
|
|
||||||
target:
|
|
||||||
candidate:
|
|
||||||
|
|
||||||
- name: unlock candidate
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: unlock
|
|
||||||
xmlns: "urn:ietf:params:xml:ns:netconf:base:1.0"
|
|
||||||
content: "{'target': {'candidate': None}}"
|
|
||||||
|
|
||||||
- name: discard changes
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: discard-changes
|
|
||||||
|
|
||||||
- name: get-schema
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: get-schema
|
|
||||||
xmlns: urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring
|
|
||||||
content:
|
|
||||||
identifier: ietf-netconf
|
|
||||||
version: "2011-06-01"
|
|
||||||
|
|
||||||
- name: copy running to startup
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: copy-config
|
|
||||||
content:
|
|
||||||
source:
|
|
||||||
running:
|
|
||||||
target:
|
|
||||||
startup:
|
|
||||||
|
|
||||||
- name: get schema list with JSON output
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: get
|
|
||||||
content: |
|
|
||||||
<filter>
|
|
||||||
<netconf-state xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring">
|
|
||||||
<schemas/>
|
|
||||||
</netconf-state>
|
|
||||||
</filter>
|
|
||||||
display: json
|
|
||||||
|
|
||||||
- name: get schema using XML request
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: "get-schema"
|
|
||||||
xmlns: "urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring"
|
|
||||||
content: |
|
|
||||||
<identifier>ietf-netconf-monitoring</identifier>
|
|
||||||
<version>2010-10-04</version>
|
|
||||||
display: json
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
stdout:
|
|
||||||
description: The raw XML string containing configuration or state data
|
|
||||||
received from the underlying ncclient library.
|
|
||||||
returned: always apart from low-level errors (such as action plugin)
|
|
||||||
type: str
|
|
||||||
sample: '...'
|
|
||||||
stdout_lines:
|
|
||||||
description: The value of stdout split into a list
|
|
||||||
returned: always apart from low-level errors (such as action plugin)
|
|
||||||
type: list
|
|
||||||
sample: ['...', '...']
|
|
||||||
output:
|
|
||||||
description: Based on the value of display option will return either the set of
|
|
||||||
transformed XML to JSON format from the RPC response with type dict
|
|
||||||
or pretty XML string response (human-readable) or response with
|
|
||||||
namespace removed from XML string.
|
|
||||||
returned: when the display format is selected as JSON it is returned as dict type, if the
|
|
||||||
display format is xml or pretty pretty it is returned as a string apart from low-level
|
|
||||||
errors (such as action plugin).
|
|
||||||
type: complex
|
|
||||||
contains:
|
|
||||||
formatted_output:
|
|
||||||
- Contains formatted response received from remote host as per the value in display format.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import ast
|
|
||||||
|
|
||||||
try:
|
|
||||||
from lxml.etree import tostring
|
|
||||||
except ImportError:
|
|
||||||
from xml.etree.ElementTree import tostring
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
from ansible.module_utils.network.netconf.netconf import dispatch
|
|
||||||
from ansible.module_utils.network.common.netconf import remove_namespaces
|
|
||||||
|
|
||||||
try:
|
|
||||||
import jxmlease
|
|
||||||
HAS_JXMLEASE = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_JXMLEASE = False
|
|
||||||
|
|
||||||
|
|
||||||
def get_xml_request(module, request, xmlns, content):
|
|
||||||
if content is None:
|
|
||||||
if xmlns is None:
|
|
||||||
return '<%s/>' % request
|
|
||||||
else:
|
|
||||||
return '<%s xmlns="%s"/>' % (request, xmlns)
|
|
||||||
|
|
||||||
if isinstance(content, str):
|
|
||||||
content = content.strip()
|
|
||||||
|
|
||||||
if content.startswith('<') and content.endswith('>'):
|
|
||||||
# assumption content contains already XML payload
|
|
||||||
if xmlns is None:
|
|
||||||
return '<%s>%s</%s>' % (request, content, request)
|
|
||||||
else:
|
|
||||||
return '<%s xmlns="%s">%s</%s>' % (request, xmlns, content, request)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# trying if content contains dict
|
|
||||||
content = ast.literal_eval(content)
|
|
||||||
except Exception:
|
|
||||||
module.fail_json(msg='unsupported content value `%s`' % content)
|
|
||||||
|
|
||||||
if isinstance(content, dict):
|
|
||||||
if not HAS_JXMLEASE:
|
|
||||||
module.fail_json(msg='jxmlease is required to convert RPC content to XML '
|
|
||||||
'but does not appear to be installed. '
|
|
||||||
'It can be installed using `pip install jxmlease`')
|
|
||||||
|
|
||||||
payload = jxmlease.XMLDictNode(content).emit_xml(pretty=False, full_document=False)
|
|
||||||
if xmlns is None:
|
|
||||||
return '<%s>%s</%s>' % (request, payload, request)
|
|
||||||
else:
|
|
||||||
return '<%s xmlns="%s">%s</%s>' % (request, xmlns, payload, request)
|
|
||||||
|
|
||||||
module.fail_json(msg='unsupported content data-type `%s`' % type(content).__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""entry point for module execution
|
|
||||||
"""
|
|
||||||
argument_spec = dict(
|
|
||||||
rpc=dict(type="str", required=True),
|
|
||||||
xmlns=dict(type="str"),
|
|
||||||
content=dict(),
|
|
||||||
display=dict(choices=['json', 'pretty', 'xml'])
|
|
||||||
)
|
|
||||||
|
|
||||||
module = AnsibleModule(argument_spec=argument_spec,
|
|
||||||
supports_check_mode=True)
|
|
||||||
|
|
||||||
rpc = module.params['rpc']
|
|
||||||
xmlns = module.params['xmlns']
|
|
||||||
content = module.params['content']
|
|
||||||
display = module.params['display']
|
|
||||||
|
|
||||||
if rpc is None:
|
|
||||||
module.fail_json(msg='argument `rpc` must not be None')
|
|
||||||
|
|
||||||
rpc = rpc.strip()
|
|
||||||
if len(rpc) == 0:
|
|
||||||
module.fail_json(msg='argument `rpc` must not be empty')
|
|
||||||
|
|
||||||
if rpc in ['close-session']:
|
|
||||||
# explicit close-session is not allowed, as this would make the next
|
|
||||||
# NETCONF operation to the same host fail
|
|
||||||
module.fail_json(msg='unsupported operation `%s`' % rpc)
|
|
||||||
|
|
||||||
if display == 'json' and not HAS_JXMLEASE:
|
|
||||||
module.fail_json(msg='jxmlease is required to display response in json format'
|
|
||||||
'but does not appear to be installed. '
|
|
||||||
'It can be installed using `pip install jxmlease`')
|
|
||||||
|
|
||||||
xml_req = get_xml_request(module, rpc, xmlns, content)
|
|
||||||
response = dispatch(module, xml_req)
|
|
||||||
|
|
||||||
xml_resp = tostring(response)
|
|
||||||
output = None
|
|
||||||
|
|
||||||
if display == 'xml':
|
|
||||||
output = remove_namespaces(xml_resp)
|
|
||||||
elif display == 'json':
|
|
||||||
try:
|
|
||||||
output = jxmlease.parse(xml_resp)
|
|
||||||
except Exception:
|
|
||||||
raise ValueError(xml_resp)
|
|
||||||
elif display == 'pretty':
|
|
||||||
output = tostring(response, pretty_print=True)
|
|
||||||
|
|
||||||
result = {
|
|
||||||
'stdout': xml_resp,
|
|
||||||
'output': output
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,55 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2017, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['deprecated'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: net_lldp
|
|
||||||
version_added: "2.4"
|
|
||||||
author: "Ricardo Carrillo Cruz (@rcarrillocruz)"
|
|
||||||
short_description: Manage LLDP service configuration on network devices
|
|
||||||
description:
|
|
||||||
- This module provides declarative management of LLDP service configuration
|
|
||||||
on network devices.
|
|
||||||
deprecated:
|
|
||||||
removed_in: "2.13"
|
|
||||||
alternative: Use platform-specific "[netos]_lldp_global" module
|
|
||||||
why: Updated modules released with more functionality
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- State of the LLDP service configuration.
|
|
||||||
default: present
|
|
||||||
choices: ['present', 'absent']
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: Enable LLDP service
|
|
||||||
net_lldp:
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Disable LLDP service
|
|
||||||
net_lldp:
|
|
||||||
state: absent
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
commands:
|
|
||||||
description: The list of configuration mode commands to send to the device
|
|
||||||
returned: always, except for the platforms that use Netconf transport to manage the device.
|
|
||||||
type: list
|
|
||||||
sample:
|
|
||||||
- set service lldp
|
|
||||||
"""
|
|
@ -1,194 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# Copyright: Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
|
||||||
---
|
|
||||||
module: restconf_config
|
|
||||||
version_added: "2.8"
|
|
||||||
author: "Ganesh Nalawade (@ganeshrn)"
|
|
||||||
short_description: Handles create, update, read and delete of configuration data on RESTCONF enabled devices.
|
|
||||||
description:
|
|
||||||
- RESTCONF is a standard mechanisms to allow web applications to configure and manage
|
|
||||||
data. RESTCONF is a IETF standard and documented on RFC 8040.
|
|
||||||
- This module allows the user to configure data on RESTCONF enabled devices.
|
|
||||||
options:
|
|
||||||
path:
|
|
||||||
description:
|
|
||||||
- URI being used to execute API calls.
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
description:
|
|
||||||
- The configuration data in format as specififed in C(format) option. Required unless C(method) is
|
|
||||||
I(delete).
|
|
||||||
method:
|
|
||||||
description:
|
|
||||||
- The RESTCONF method to manage the configuration change on device. The value I(post) is used to
|
|
||||||
create a data resource or invoke an operation resource, I(put) is used to replace the target
|
|
||||||
data resource, I(patch) is used to modify the target resource, and I(delete) is used to delete
|
|
||||||
the target resource.
|
|
||||||
required: false
|
|
||||||
default: post
|
|
||||||
choices: ['post', 'put', 'patch', 'delete']
|
|
||||||
format:
|
|
||||||
description:
|
|
||||||
- The format of the configuration provided as value of C(content). Accepted values are I(xml) and I(json) and
|
|
||||||
the given configuration format should be supported by remote RESTCONF server.
|
|
||||||
default: json
|
|
||||||
choices: ['json', 'xml']
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = '''
|
|
||||||
- name: create l3vpn services
|
|
||||||
restconf_config:
|
|
||||||
path: /config/ietf-l3vpn-svc:l3vpn-svc/vpn-services
|
|
||||||
content: |
|
|
||||||
{
|
|
||||||
"vpn-service":[
|
|
||||||
{
|
|
||||||
"vpn-id": "red_vpn2",
|
|
||||||
"customer-name": "blue",
|
|
||||||
"vpn-service-topology": "ietf-l3vpn-svc:any-to-any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"vpn-id": "blue_vpn1",
|
|
||||||
"customer-name": "red",
|
|
||||||
"vpn-service-topology": "ietf-l3vpn-svc:any-to-any"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = '''
|
|
||||||
candidate:
|
|
||||||
description: The configuration sent to the device.
|
|
||||||
returned: When the method is not delete
|
|
||||||
type: dict
|
|
||||||
sample: |
|
|
||||||
{
|
|
||||||
"vpn-service": [
|
|
||||||
{
|
|
||||||
"customer-name": "red",
|
|
||||||
"vpn-id": "blue_vpn1",
|
|
||||||
"vpn-service-topology": "ietf-l3vpn-svc:any-to-any"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
running:
|
|
||||||
description: The current running configuration on the device.
|
|
||||||
returned: When the method is not delete
|
|
||||||
type: dict
|
|
||||||
sample: |
|
|
||||||
{
|
|
||||||
"vpn-service": [
|
|
||||||
{
|
|
||||||
"vpn-id": "red_vpn2",
|
|
||||||
"customer-name": "blue",
|
|
||||||
"vpn-service-topology": "ietf-l3vpn-svc:any-to-any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"vpn-id": "blue_vpn1",
|
|
||||||
"customer-name": "red",
|
|
||||||
"vpn-service-topology": "ietf-l3vpn-svc:any-to-any"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
from ansible.module_utils._text import to_text
|
|
||||||
from ansible.module_utils.connection import ConnectionError
|
|
||||||
from ansible.module_utils.network.common.utils import dict_diff
|
|
||||||
from ansible.module_utils.network.restconf import restconf
|
|
||||||
from ansible.module_utils.six import string_types
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""entry point for module execution
|
|
||||||
"""
|
|
||||||
argument_spec = dict(
|
|
||||||
path=dict(required=True),
|
|
||||||
content=dict(),
|
|
||||||
method=dict(choices=['post', 'put', 'patch', 'delete'], default='post'),
|
|
||||||
format=dict(choices=['json', 'xml'], default='json'),
|
|
||||||
)
|
|
||||||
required_if = [
|
|
||||||
['method', 'post', ['content']],
|
|
||||||
['method', 'put', ['content']],
|
|
||||||
['method', 'patch', ['content']],
|
|
||||||
]
|
|
||||||
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=argument_spec,
|
|
||||||
required_if=required_if,
|
|
||||||
supports_check_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
path = module.params['path']
|
|
||||||
candidate = module.params['content']
|
|
||||||
method = module.params['method']
|
|
||||||
format = module.params['format']
|
|
||||||
|
|
||||||
if isinstance(candidate, string_types):
|
|
||||||
candidate = json.loads(candidate)
|
|
||||||
|
|
||||||
warnings = list()
|
|
||||||
result = {'changed': False, 'warnings': warnings}
|
|
||||||
|
|
||||||
running = None
|
|
||||||
commit = not module.check_mode
|
|
||||||
try:
|
|
||||||
running = restconf.get(module, path, output=format)
|
|
||||||
except ConnectionError as exc:
|
|
||||||
if exc.code == 404:
|
|
||||||
running = None
|
|
||||||
else:
|
|
||||||
module.fail_json(msg=to_text(exc), code=exc.code)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if method == 'delete':
|
|
||||||
if running:
|
|
||||||
if commit:
|
|
||||||
restconf.edit_config(module, path=path, method='DELETE')
|
|
||||||
result['changed'] = True
|
|
||||||
else:
|
|
||||||
warnings.append("delete not executed as resource '%s' does not exist" % path)
|
|
||||||
else:
|
|
||||||
if running:
|
|
||||||
if method == 'post':
|
|
||||||
module.fail_json(msg="resource '%s' already exist" % path, code=409)
|
|
||||||
diff = dict_diff(running, candidate)
|
|
||||||
result['candidate'] = candidate
|
|
||||||
result['running'] = running
|
|
||||||
else:
|
|
||||||
method = 'POST'
|
|
||||||
diff = candidate
|
|
||||||
|
|
||||||
if diff:
|
|
||||||
if module._diff:
|
|
||||||
result['diff'] = {'prepared': diff, 'before': candidate, 'after': running}
|
|
||||||
|
|
||||||
if commit:
|
|
||||||
restconf.edit_config(module, path=path, content=diff, method=method.upper(), format=format)
|
|
||||||
result['changed'] = True
|
|
||||||
|
|
||||||
except ConnectionError as exc:
|
|
||||||
module.fail_json(msg=str(exc), code=exc.code)
|
|
||||||
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,110 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# Copyright: Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: restconf_get
|
|
||||||
version_added: "2.8"
|
|
||||||
author: "Ganesh Nalawade (@ganeshrn)"
|
|
||||||
short_description: Fetch configuration/state data from RESTCONF enabled devices.
|
|
||||||
description:
|
|
||||||
- RESTCONF is a standard mechanisms to allow web applications to access the
|
|
||||||
configuration data and state data developed and standardized by
|
|
||||||
the IETF. It is documented in RFC 8040.
|
|
||||||
- This module allows the user to fetch configuration and state data from RESTCONF
|
|
||||||
enabled devices.
|
|
||||||
options:
|
|
||||||
path:
|
|
||||||
description:
|
|
||||||
- URI being used to execute API calls.
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
description:
|
|
||||||
- The C(content) is a query parameter that controls how descendant nodes of the
|
|
||||||
requested data nodes in C(path) will be processed in the reply. If value is
|
|
||||||
I(config) return only configuration descendant data nodes of value in C(path).
|
|
||||||
If value is I(nonconfig) return only non-configuration descendant data nodes
|
|
||||||
of value in C(path). If value is I(all) return all descendant data nodes of
|
|
||||||
value in C(path)
|
|
||||||
required: false
|
|
||||||
choices: ['config', 'nonconfig', 'all']
|
|
||||||
output:
|
|
||||||
description:
|
|
||||||
- The output of response received.
|
|
||||||
required: false
|
|
||||||
default: json
|
|
||||||
choices: ['json', 'xml']
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: get l3vpn services
|
|
||||||
restconf_get:
|
|
||||||
path: /config/ietf-l3vpn-svc:l3vpn-svc/vpn-services
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
response:
|
|
||||||
description: A dictionary representing a JSON-formatted response
|
|
||||||
returned: when the device response is valid JSON
|
|
||||||
type: dict
|
|
||||||
sample: |
|
|
||||||
{
|
|
||||||
"vpn-services": {
|
|
||||||
"vpn-service": [
|
|
||||||
{
|
|
||||||
"customer-name": "red",
|
|
||||||
"vpn-id": "blue_vpn1",
|
|
||||||
"vpn-service-topology": "ietf-l3vpn-svc:any-to-any"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ansible.module_utils._text import to_text
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
from ansible.module_utils.connection import ConnectionError
|
|
||||||
from ansible.module_utils.network.restconf import restconf
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""entry point for module execution
|
|
||||||
"""
|
|
||||||
argument_spec = dict(
|
|
||||||
path=dict(required=True),
|
|
||||||
content=dict(choices=['config', 'nonconfig', 'all']),
|
|
||||||
output=dict(choices=['json', 'xml'], default='json'),
|
|
||||||
)
|
|
||||||
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=argument_spec,
|
|
||||||
supports_check_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
result = {'changed': False}
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = restconf.get(module, **module.params)
|
|
||||||
except ConnectionError as exc:
|
|
||||||
module.fail_json(msg=to_text(exc), code=exc.code)
|
|
||||||
|
|
||||||
result.update({
|
|
||||||
'response': response,
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,95 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2017, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['deprecated'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: net_static_route
|
|
||||||
version_added: "2.4"
|
|
||||||
author: "Ricardo Carrillo Cruz (@rcarrillocruz)"
|
|
||||||
short_description: Manage static IP routes on network appliances (routers, switches et. al.)
|
|
||||||
description:
|
|
||||||
- This module provides declarative management of static
|
|
||||||
IP routes on network appliances (routers, switches et. al.).
|
|
||||||
deprecated:
|
|
||||||
removed_in: "2.13"
|
|
||||||
alternative: Use platform-specific "[netos]_static_route" module
|
|
||||||
why: Updated modules released with more functionality
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
|
|
||||||
options:
|
|
||||||
prefix:
|
|
||||||
description:
|
|
||||||
- Network prefix of the static route.
|
|
||||||
required: true
|
|
||||||
mask:
|
|
||||||
description:
|
|
||||||
- Network prefix mask of the static route.
|
|
||||||
required: true
|
|
||||||
next_hop:
|
|
||||||
description:
|
|
||||||
- Next hop IP of the static route.
|
|
||||||
required: true
|
|
||||||
admin_distance:
|
|
||||||
description:
|
|
||||||
- Admin distance of the static route.
|
|
||||||
aggregate:
|
|
||||||
description: List of static route definitions
|
|
||||||
purge:
|
|
||||||
description:
|
|
||||||
- Purge static routes not defined in the I(aggregate) parameter.
|
|
||||||
default: no
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- State of the static route configuration.
|
|
||||||
default: present
|
|
||||||
choices: ['present', 'absent']
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: configure static route
|
|
||||||
net_static_route:
|
|
||||||
prefix: 192.168.2.0
|
|
||||||
mask: 255.255.255.0
|
|
||||||
next_hop: 10.0.0.1
|
|
||||||
|
|
||||||
- name: remove configuration
|
|
||||||
net_static_route:
|
|
||||||
prefix: 192.168.2.0
|
|
||||||
mask: 255.255.255.0
|
|
||||||
next_hop: 10.0.0.1
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: configure aggregates of static routes
|
|
||||||
net_static_route:
|
|
||||||
aggregate:
|
|
||||||
- { prefix: 192.168.2.0, mask: 255.255.255.0, next_hop: 10.0.0.1 }
|
|
||||||
- { prefix: 192.168.3.0, mask: 255.255.255.0, next_hop: 10.0.2.1 }
|
|
||||||
|
|
||||||
- name: Remove static route collections
|
|
||||||
net_static_route:
|
|
||||||
aggregate:
|
|
||||||
- { prefix: 172.24.1.0/24, next_hop: 192.168.42.64 }
|
|
||||||
- { prefix: 172.24.3.0/24, next_hop: 192.168.42.64 }
|
|
||||||
state: absent
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
commands:
|
|
||||||
description: The list of configuration mode commands to send to the device
|
|
||||||
returned: always
|
|
||||||
type: list
|
|
||||||
sample:
|
|
||||||
- ip route 192.168.2.0/24 10.0.0.1
|
|
||||||
"""
|
|
@ -1,82 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# Copyright: Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['deprecated'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: net_banner
|
|
||||||
version_added: "2.4"
|
|
||||||
author: "Ricardo Carrillo Cruz (@rcarrillocruz)"
|
|
||||||
short_description: Manage multiline banners on network devices
|
|
||||||
description:
|
|
||||||
- This will configure both login and motd banners on network devices.
|
|
||||||
It allows playbooks to add or remove
|
|
||||||
banner text from the active running configuration.
|
|
||||||
deprecated:
|
|
||||||
removed_in: "2.13"
|
|
||||||
alternative: Use platform-specific "[netos]_banner" module
|
|
||||||
why: Updated modules released with more functionality
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
banner:
|
|
||||||
description:
|
|
||||||
- Specifies which banner that should be
|
|
||||||
configured on the remote device.
|
|
||||||
required: true
|
|
||||||
choices: ['login', 'motd']
|
|
||||||
text:
|
|
||||||
description:
|
|
||||||
- The banner text that should be
|
|
||||||
present in the remote device running configuration. This argument
|
|
||||||
accepts a multiline string, with no empty lines. Requires I(state=present).
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- Specifies whether or not the configuration is
|
|
||||||
present in the current devices active running configuration.
|
|
||||||
default: present
|
|
||||||
choices: ['present', 'absent']
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: configure the login banner
|
|
||||||
net_banner:
|
|
||||||
banner: login
|
|
||||||
text: |
|
|
||||||
this is my login banner
|
|
||||||
that contains a multiline
|
|
||||||
string
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: remove the motd banner
|
|
||||||
net_banner:
|
|
||||||
banner: motd
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Configure banner from file
|
|
||||||
net_banner:
|
|
||||||
banner: motd
|
|
||||||
text: "{{ lookup('file', './config_partial/raw_banner.cfg') }}"
|
|
||||||
state: present
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
commands:
|
|
||||||
description: The list of configuration mode commands to send to the device
|
|
||||||
returned: always, except for the platforms that use Netconf transport to manage the device.
|
|
||||||
type: list
|
|
||||||
sample:
|
|
||||||
- banner login
|
|
||||||
- this is my login banner
|
|
||||||
- that contains a multiline
|
|
||||||
- string
|
|
||||||
"""
|
|
@ -1,107 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2017, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['deprecated'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: net_logging
|
|
||||||
version_added: "2.4"
|
|
||||||
author: "Ganesh Nalawade (@ganeshrn)"
|
|
||||||
short_description: Manage logging on network devices
|
|
||||||
description:
|
|
||||||
- This module provides declarative management of logging
|
|
||||||
on network devices.
|
|
||||||
deprecated:
|
|
||||||
removed_in: "2.13"
|
|
||||||
alternative: Use platform-specific "[netos]_logging" module
|
|
||||||
why: Updated modules released with more functionality
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
dest:
|
|
||||||
description:
|
|
||||||
- Destination of the logs.
|
|
||||||
choices: ['console', 'host']
|
|
||||||
name:
|
|
||||||
description:
|
|
||||||
- If value of C(dest) is I(host) it indicates file-name
|
|
||||||
the host name to be notified.
|
|
||||||
facility:
|
|
||||||
description:
|
|
||||||
- Set logging facility.
|
|
||||||
level:
|
|
||||||
description:
|
|
||||||
- Set logging severity levels.
|
|
||||||
aggregate:
|
|
||||||
description: List of logging definitions.
|
|
||||||
purge:
|
|
||||||
description:
|
|
||||||
- Purge logging not defined in the I(aggregate) parameter.
|
|
||||||
default: no
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- State of the logging configuration.
|
|
||||||
default: present
|
|
||||||
choices: ['present', 'absent']
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: configure console logging
|
|
||||||
net_logging:
|
|
||||||
dest: console
|
|
||||||
facility: any
|
|
||||||
level: critical
|
|
||||||
|
|
||||||
- name: remove console logging configuration
|
|
||||||
net_logging:
|
|
||||||
dest: console
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: configure host logging
|
|
||||||
net_logging:
|
|
||||||
dest: host
|
|
||||||
name: 192.0.2.1
|
|
||||||
facility: kernel
|
|
||||||
level: critical
|
|
||||||
|
|
||||||
- name: Configure file logging using aggregate
|
|
||||||
net_logging:
|
|
||||||
dest: file
|
|
||||||
aggregate:
|
|
||||||
- name: test-1
|
|
||||||
facility: pfe
|
|
||||||
level: critical
|
|
||||||
- name: test-2
|
|
||||||
facility: kernel
|
|
||||||
level: emergency
|
|
||||||
- name: Delete file logging using aggregate
|
|
||||||
net_logging:
|
|
||||||
dest: file
|
|
||||||
aggregate:
|
|
||||||
- name: test-1
|
|
||||||
facility: pfe
|
|
||||||
level: critical
|
|
||||||
- name: test-2
|
|
||||||
facility: kernel
|
|
||||||
level: emergency
|
|
||||||
state: absent
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
commands:
|
|
||||||
description: The list of configuration mode commands to send to the device
|
|
||||||
returned: always, except for the platforms that use Netconf transport to manage the device.
|
|
||||||
type: list
|
|
||||||
sample:
|
|
||||||
- logging console critical
|
|
||||||
"""
|
|
@ -1,108 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2017, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['deprecated'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: net_system
|
|
||||||
version_added: "2.4"
|
|
||||||
author: "Ricardo Carrillo Cruz (@rcarrillocruz)"
|
|
||||||
short_description: Manage the system attributes on network devices
|
|
||||||
description:
|
|
||||||
- This module provides declarative management of node system attributes
|
|
||||||
on network devices. It provides an option to configure host system
|
|
||||||
parameters or remove those parameters from the device active
|
|
||||||
configuration.
|
|
||||||
deprecated:
|
|
||||||
removed_in: "2.13"
|
|
||||||
alternative: Use platform-specific "[netos]_system" module
|
|
||||||
why: Updated modules released with more functionality
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
hostname:
|
|
||||||
description:
|
|
||||||
- Configure the device hostname parameter. This option takes an ASCII string value.
|
|
||||||
domain_name:
|
|
||||||
description:
|
|
||||||
- Configure the IP domain name
|
|
||||||
on the remote device to the provided value. Value
|
|
||||||
should be in the dotted name form and will be
|
|
||||||
appended to the C(hostname) to create a fully-qualified
|
|
||||||
domain name.
|
|
||||||
domain_search:
|
|
||||||
description:
|
|
||||||
- Provides the list of domain suffixes to
|
|
||||||
append to the hostname for the purpose of doing name resolution.
|
|
||||||
This argument accepts a name or list of names and will be reconciled
|
|
||||||
with the current active configuration on the running node.
|
|
||||||
lookup_source:
|
|
||||||
description:
|
|
||||||
- Provides one or more source
|
|
||||||
interfaces to use for performing DNS lookups. The interface
|
|
||||||
provided in C(lookup_source) must be a valid interface configured
|
|
||||||
on the device.
|
|
||||||
name_servers:
|
|
||||||
description:
|
|
||||||
- List of DNS name servers by IP address to use to perform name resolution
|
|
||||||
lookups. This argument accepts either a list of DNS servers See
|
|
||||||
examples.
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- State of the configuration
|
|
||||||
values in the device's current active configuration. When set
|
|
||||||
to I(present), the values should be configured in the device active
|
|
||||||
configuration and when set to I(absent) the values should not be
|
|
||||||
in the device active configuration
|
|
||||||
default: present
|
|
||||||
choices: ['present', 'absent']
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: configure hostname and domain name
|
|
||||||
net_system:
|
|
||||||
hostname: ios01
|
|
||||||
domain_name: test.example.com
|
|
||||||
domain_search:
|
|
||||||
- ansible.com
|
|
||||||
- redhat.com
|
|
||||||
- cisco.com
|
|
||||||
|
|
||||||
- name: domain search on single domain
|
|
||||||
net_system:
|
|
||||||
domain_search: ansible.com
|
|
||||||
|
|
||||||
- name: remove configuration
|
|
||||||
net_system:
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: configure DNS lookup sources
|
|
||||||
net_system:
|
|
||||||
lookup_source: MgmtEth0/0/CPU0/0
|
|
||||||
|
|
||||||
- name: configure name servers
|
|
||||||
net_system:
|
|
||||||
name_servers:
|
|
||||||
- 8.8.8.8
|
|
||||||
- 8.8.4.4
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
commands:
|
|
||||||
description: The list of configuration mode commands to send to the device
|
|
||||||
returned: always, except for the platforms that use Netconf transport to manage the device.
|
|
||||||
type: list
|
|
||||||
sample:
|
|
||||||
- hostname ios01
|
|
||||||
- ip domain name test.example.com
|
|
||||||
"""
|
|
@ -1,135 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2017, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['deprecated'],
|
|
||||||
'supported_by': 'network'}
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: net_user
|
|
||||||
version_added: "2.4"
|
|
||||||
author: "Trishna Guha (@trishnaguha)"
|
|
||||||
short_description: Manage the aggregate of local users on network device
|
|
||||||
description:
|
|
||||||
- This module provides declarative management of the local usernames
|
|
||||||
configured on network devices. It allows playbooks to manage
|
|
||||||
either individual usernames or the aggregate of usernames in the
|
|
||||||
current running config. It also supports purging usernames from the
|
|
||||||
configuration that are not explicitly defined.
|
|
||||||
deprecated:
|
|
||||||
removed_in: "2.13"
|
|
||||||
alternative: Use platform-specific "[netos]_user" module
|
|
||||||
why: Updated modules released with more functionality
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
aggregate:
|
|
||||||
description:
|
|
||||||
- The set of username objects to be configured on the remote
|
|
||||||
network device. The list entries can either be the username
|
|
||||||
or a hash of username and properties. This argument is mutually
|
|
||||||
exclusive with the C(name) argument.
|
|
||||||
name:
|
|
||||||
description:
|
|
||||||
- The username to be configured on the remote network device.
|
|
||||||
This argument accepts a string value and is mutually exclusive
|
|
||||||
with the C(aggregate) argument.
|
|
||||||
Please note that this option is not same as C(provider username).
|
|
||||||
configured_password:
|
|
||||||
description:
|
|
||||||
- The password to be configured on the remote network device. The
|
|
||||||
password needs to be provided in clear and it will be encrypted
|
|
||||||
on the device.
|
|
||||||
Please note that this option is not same as C(provider password).
|
|
||||||
update_password:
|
|
||||||
description:
|
|
||||||
- Since passwords are encrypted in the device running config, this
|
|
||||||
argument will instruct the module when to change the password. When
|
|
||||||
set to C(always), the password will always be updated in the device
|
|
||||||
and when set to C(on_create) the password will be updated only if
|
|
||||||
the username is created.
|
|
||||||
default: always
|
|
||||||
choices: ['on_create', 'always']
|
|
||||||
privilege:
|
|
||||||
description:
|
|
||||||
- The C(privilege) argument configures the privilege level of the
|
|
||||||
user when logged into the system. This argument accepts integer
|
|
||||||
values in the range of 1 to 15.
|
|
||||||
role:
|
|
||||||
description:
|
|
||||||
- Configures the role for the username in the
|
|
||||||
device running configuration. The argument accepts a string value
|
|
||||||
defining the role name. This argument does not check if the role
|
|
||||||
has been configured on the device.
|
|
||||||
sshkey:
|
|
||||||
description:
|
|
||||||
- Specifies the SSH public key to configure
|
|
||||||
for the given username. This argument accepts a valid SSH key value.
|
|
||||||
nopassword:
|
|
||||||
description:
|
|
||||||
- Defines the username without assigning
|
|
||||||
a password. This will allow the user to login to the system
|
|
||||||
without being authenticated by a password.
|
|
||||||
type: bool
|
|
||||||
purge:
|
|
||||||
description:
|
|
||||||
- Instructs the module to consider the
|
|
||||||
resource definition absolute. It will remove any previously
|
|
||||||
configured usernames on the device with the exception of the
|
|
||||||
`admin` user (the current defined set of users).
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- Configures the state of the username definition
|
|
||||||
as it relates to the device operational configuration. When set
|
|
||||||
to I(present), the username(s) should be configured in the device active
|
|
||||||
configuration and when set to I(absent) the username(s) should not be
|
|
||||||
in the device active configuration
|
|
||||||
default: present
|
|
||||||
choices: ['present', 'absent']
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
- name: create a new user
|
|
||||||
net_user:
|
|
||||||
name: ansible
|
|
||||||
sshkey: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: remove all users except admin
|
|
||||||
net_user:
|
|
||||||
purge: yes
|
|
||||||
|
|
||||||
- name: set multiple users to privilege level 15
|
|
||||||
net_user:
|
|
||||||
aggregate:
|
|
||||||
- { name: netop }
|
|
||||||
- { name: netend }
|
|
||||||
privilege: 15
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Change Password for User netop
|
|
||||||
net_user:
|
|
||||||
name: netop
|
|
||||||
configured_password: "{{ new_password }}"
|
|
||||||
update_password: always
|
|
||||||
state: present
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
commands:
|
|
||||||
description: The list of configuration mode commands to send to the device
|
|
||||||
returned: always
|
|
||||||
type: list
|
|
||||||
sample:
|
|
||||||
- username ansible secret password
|
|
||||||
- username admin secret admin
|
|
||||||
"""
|
|
@ -1,103 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2017, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = r'''
|
|
||||||
---
|
|
||||||
module: net_ping
|
|
||||||
version_added: "2.4"
|
|
||||||
author: "Jacob McGill (@jmcgill298)"
|
|
||||||
short_description: Tests reachability using ping from a network device
|
|
||||||
description:
|
|
||||||
- Tests reachability using ping from network device to a remote destination.
|
|
||||||
- For Windows targets, use the M(win_ping) module instead.
|
|
||||||
- For targets running Python, use the M(ping) module instead.
|
|
||||||
extends_documentation_fragment: network_agnostic
|
|
||||||
options:
|
|
||||||
count:
|
|
||||||
description:
|
|
||||||
- Number of packets to send.
|
|
||||||
default: 5
|
|
||||||
dest:
|
|
||||||
description:
|
|
||||||
- The IP Address or hostname (resolvable by switch) of the remote node.
|
|
||||||
required: true
|
|
||||||
source:
|
|
||||||
description:
|
|
||||||
- The source IP Address.
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- Determines if the expected result is success or fail.
|
|
||||||
choices: [ absent, present ]
|
|
||||||
default: present
|
|
||||||
vrf:
|
|
||||||
description:
|
|
||||||
- The VRF to use for forwarding.
|
|
||||||
default: default
|
|
||||||
notes:
|
|
||||||
- For Windows targets, use the M(win_ping) module instead.
|
|
||||||
- For targets running Python, use the M(ping) module instead.
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
EXAMPLES = r'''
|
|
||||||
- name: Test reachability to 10.10.10.10 using default vrf
|
|
||||||
net_ping:
|
|
||||||
dest: 10.10.10.10
|
|
||||||
|
|
||||||
- name: Test reachability to 10.20.20.20 using prod vrf
|
|
||||||
net_ping:
|
|
||||||
dest: 10.20.20.20
|
|
||||||
vrf: prod
|
|
||||||
|
|
||||||
- name: Test unreachability to 10.30.30.30 using default vrf
|
|
||||||
net_ping:
|
|
||||||
dest: 10.30.30.30
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Test reachability to 10.40.40.40 using prod vrf and setting count and source
|
|
||||||
net_ping:
|
|
||||||
dest: 10.40.40.40
|
|
||||||
source: loopback0
|
|
||||||
vrf: prod
|
|
||||||
count: 20
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = r'''
|
|
||||||
commands:
|
|
||||||
description: Show the command sent.
|
|
||||||
returned: always
|
|
||||||
type: list
|
|
||||||
sample: ["ping vrf prod 10.40.40.40 count 20 source loopback0"]
|
|
||||||
packet_loss:
|
|
||||||
description: Percentage of packets lost.
|
|
||||||
returned: always
|
|
||||||
type: str
|
|
||||||
sample: "0%"
|
|
||||||
packets_rx:
|
|
||||||
description: Packets successfully received.
|
|
||||||
returned: always
|
|
||||||
type: int
|
|
||||||
sample: 20
|
|
||||||
packets_tx:
|
|
||||||
description: Packets successfully transmitted.
|
|
||||||
returned: always
|
|
||||||
type: int
|
|
||||||
sample: 20
|
|
||||||
rtt:
|
|
||||||
description: Show RTT stats.
|
|
||||||
returned: always
|
|
||||||
type: dict
|
|
||||||
sample: {"avg": 2, "max": 8, "min": 1}
|
|
||||||
'''
|
|
@ -1,31 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2018 Red Hat Inc.
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.normal import ActionModule as _ActionModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
if self._play_context.connection.split('.')[-1] != 'network_cli':
|
|
||||||
return {'failed': True, 'msg': 'Connection type %s is not valid for this module' % self._play_context.connection}
|
|
||||||
|
|
||||||
return super(ActionModule, self).run(task_vars=task_vars)
|
|
@ -1,34 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2018 Red Hat Inc.
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.network import ActionModule as ActionNetworkModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(ActionNetworkModule):
|
|
||||||
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
|
|
||||||
self._config_module = True
|
|
||||||
if self._play_context.connection.split('.')[-1] != 'network_cli':
|
|
||||||
return {'failed': True, 'msg': 'Connection type %s is not valid for cli_config module' % self._play_context.connection}
|
|
||||||
|
|
||||||
return super(ActionModule, self).run(task_vars=task_vars)
|
|
@ -1,28 +0,0 @@
|
|||||||
# (c) 2017, Ansible Inc,
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.net_base import ActionModule as _ActionModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
result = super(ActionModule, self).run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
|
|
||||||
return result
|
|
@ -1,75 +0,0 @@
|
|||||||
# Copyright: (c) 2015, Ansible Inc,
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
import copy
|
|
||||||
|
|
||||||
from ansible.errors import AnsibleError
|
|
||||||
from ansible.plugins.action import ActionBase
|
|
||||||
from ansible.utils.display import Display
|
|
||||||
|
|
||||||
display = Display()
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(ActionBase):
|
|
||||||
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
|
|
||||||
result = {}
|
|
||||||
play_context = copy.deepcopy(self._play_context)
|
|
||||||
play_context.network_os = self._get_network_os(task_vars)
|
|
||||||
new_task = self._task.copy()
|
|
||||||
|
|
||||||
module = self._get_implementation_module(play_context.network_os, self._task.action)
|
|
||||||
if not module:
|
|
||||||
if self._task.args['fail_on_missing_module']:
|
|
||||||
result['failed'] = True
|
|
||||||
else:
|
|
||||||
result['failed'] = False
|
|
||||||
|
|
||||||
result['msg'] = ('Could not find implementation module %s for %s' %
|
|
||||||
(self._task.action, play_context.network_os))
|
|
||||||
return result
|
|
||||||
|
|
||||||
new_task.action = module
|
|
||||||
|
|
||||||
action = self._shared_loader_obj.action_loader.get(play_context.network_os,
|
|
||||||
task=new_task,
|
|
||||||
connection=self._connection,
|
|
||||||
play_context=play_context,
|
|
||||||
loader=self._loader,
|
|
||||||
templar=self._templar,
|
|
||||||
shared_loader_obj=self._shared_loader_obj)
|
|
||||||
display.vvvv('Running implementation module %s' % module)
|
|
||||||
return action.run(task_vars=task_vars)
|
|
||||||
|
|
||||||
def _get_network_os(self, task_vars):
|
|
||||||
if 'network_os' in self._task.args and self._task.args['network_os']:
|
|
||||||
display.vvvv('Getting network OS from task argument')
|
|
||||||
network_os = self._task.args['network_os']
|
|
||||||
elif self._play_context.network_os:
|
|
||||||
display.vvvv('Getting network OS from inventory')
|
|
||||||
network_os = self._play_context.network_os
|
|
||||||
elif 'network_os' in task_vars.get('ansible_facts', {}) and task_vars['ansible_facts']['network_os']:
|
|
||||||
display.vvvv('Getting network OS from fact')
|
|
||||||
network_os = task_vars['ansible_facts']['network_os']
|
|
||||||
else:
|
|
||||||
raise AnsibleError('ansible_network_os must be specified on this host to use platform agnostic modules')
|
|
||||||
|
|
||||||
return network_os
|
|
||||||
|
|
||||||
def _get_implementation_module(self, network_os, platform_agnostic_module):
|
|
||||||
module_name = network_os.split('.')[-1] + '_' + platform_agnostic_module.partition('_')[2]
|
|
||||||
if '.' in network_os:
|
|
||||||
fqcn_module = '.'.join(network_os.split('.')[0:-1])
|
|
||||||
implementation_module = fqcn_module + '.' + module_name
|
|
||||||
else:
|
|
||||||
implementation_module = module_name
|
|
||||||
|
|
||||||
if implementation_module not in self._shared_loader_obj.module_loader:
|
|
||||||
implementation_module = None
|
|
||||||
|
|
||||||
return implementation_module
|
|
@ -1,183 +0,0 @@
|
|||||||
# (c) 2018, Ansible Inc,
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import uuid
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from ansible.errors import AnsibleError
|
|
||||||
from ansible.module_utils._text import to_text, to_bytes
|
|
||||||
from ansible.module_utils.connection import Connection, ConnectionError
|
|
||||||
from ansible.plugins.action import ActionBase
|
|
||||||
from ansible.module_utils.six.moves.urllib.parse import urlsplit
|
|
||||||
from ansible.utils.display import Display
|
|
||||||
|
|
||||||
display = Display()
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(ActionBase):
|
|
||||||
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
socket_path = None
|
|
||||||
self._get_network_os(task_vars)
|
|
||||||
persistent_connection = self._play_context.connection.split('.')[-1]
|
|
||||||
|
|
||||||
result = super(ActionModule, self).run(task_vars=task_vars)
|
|
||||||
|
|
||||||
if persistent_connection != 'network_cli':
|
|
||||||
# It is supported only with network_cli
|
|
||||||
result['failed'] = True
|
|
||||||
result['msg'] = ('connection type %s is not valid for net_get module,'
|
|
||||||
' please use fully qualified name of network_cli connection type' % self._play_context.connection)
|
|
||||||
return result
|
|
||||||
|
|
||||||
try:
|
|
||||||
src = self._task.args['src']
|
|
||||||
except KeyError as exc:
|
|
||||||
return {'failed': True, 'msg': 'missing required argument: %s' % exc}
|
|
||||||
|
|
||||||
# Get destination file if specified
|
|
||||||
dest = self._task.args.get('dest')
|
|
||||||
|
|
||||||
if dest is None:
|
|
||||||
dest = self._get_default_dest(src)
|
|
||||||
else:
|
|
||||||
dest = self._handle_dest_path(dest)
|
|
||||||
|
|
||||||
# Get proto
|
|
||||||
proto = self._task.args.get('protocol')
|
|
||||||
if proto is None:
|
|
||||||
proto = 'scp'
|
|
||||||
|
|
||||||
if socket_path is None:
|
|
||||||
socket_path = self._connection.socket_path
|
|
||||||
|
|
||||||
conn = Connection(socket_path)
|
|
||||||
sock_timeout = conn.get_option('persistent_command_timeout')
|
|
||||||
|
|
||||||
try:
|
|
||||||
changed = self._handle_existing_file(conn, src, dest, proto, sock_timeout)
|
|
||||||
if changed is False:
|
|
||||||
result['changed'] = changed
|
|
||||||
result['destination'] = dest
|
|
||||||
return result
|
|
||||||
except Exception as exc:
|
|
||||||
result['msg'] = ('Warning: %s idempotency check failed. Check dest' % exc)
|
|
||||||
|
|
||||||
try:
|
|
||||||
conn.get_file(
|
|
||||||
source=src, destination=dest,
|
|
||||||
proto=proto, timeout=sock_timeout
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
result['failed'] = True
|
|
||||||
result['msg'] = 'Exception received: %s' % exc
|
|
||||||
|
|
||||||
result['changed'] = changed
|
|
||||||
result['destination'] = dest
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _handle_dest_path(self, dest):
|
|
||||||
working_path = self._get_working_path()
|
|
||||||
|
|
||||||
if os.path.isabs(dest) or urlsplit('dest').scheme:
|
|
||||||
dst = dest
|
|
||||||
else:
|
|
||||||
dst = self._loader.path_dwim_relative(working_path, '', dest)
|
|
||||||
|
|
||||||
return dst
|
|
||||||
|
|
||||||
def _get_src_filename_from_path(self, src_path):
|
|
||||||
filename_list = re.split('/|:', src_path)
|
|
||||||
return filename_list[-1]
|
|
||||||
|
|
||||||
def _get_default_dest(self, src_path):
|
|
||||||
dest_path = self._get_working_path()
|
|
||||||
src_fname = self._get_src_filename_from_path(src_path)
|
|
||||||
filename = '%s/%s' % (dest_path, src_fname)
|
|
||||||
return filename
|
|
||||||
|
|
||||||
def _handle_existing_file(self, conn, source, dest, proto, timeout):
|
|
||||||
"""
|
|
||||||
Determines whether the source and destination file match.
|
|
||||||
|
|
||||||
:return: False if source and dest both exist and have matching sha1 sums, True otherwise.
|
|
||||||
"""
|
|
||||||
if not os.path.exists(dest):
|
|
||||||
return True
|
|
||||||
|
|
||||||
cwd = self._loader.get_basedir()
|
|
||||||
filename = str(uuid.uuid4())
|
|
||||||
tmp_dest_file = os.path.join(cwd, filename)
|
|
||||||
try:
|
|
||||||
conn.get_file(
|
|
||||||
source=source, destination=tmp_dest_file,
|
|
||||||
proto=proto, timeout=timeout
|
|
||||||
)
|
|
||||||
except ConnectionError as exc:
|
|
||||||
error = to_text(exc)
|
|
||||||
if error.endswith("No such file or directory"):
|
|
||||||
if os.path.exists(tmp_dest_file):
|
|
||||||
os.remove(tmp_dest_file)
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(tmp_dest_file, 'r') as f:
|
|
||||||
new_content = f.read()
|
|
||||||
with open(dest, 'r') as f:
|
|
||||||
old_content = f.read()
|
|
||||||
except (IOError, OSError):
|
|
||||||
os.remove(tmp_dest_file)
|
|
||||||
raise
|
|
||||||
|
|
||||||
sha1 = hashlib.sha1()
|
|
||||||
old_content_b = to_bytes(old_content, errors='surrogate_or_strict')
|
|
||||||
sha1.update(old_content_b)
|
|
||||||
checksum_old = sha1.digest()
|
|
||||||
|
|
||||||
sha1 = hashlib.sha1()
|
|
||||||
new_content_b = to_bytes(new_content, errors='surrogate_or_strict')
|
|
||||||
sha1.update(new_content_b)
|
|
||||||
checksum_new = sha1.digest()
|
|
||||||
os.remove(tmp_dest_file)
|
|
||||||
if checksum_old == checksum_new:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _get_working_path(self):
|
|
||||||
cwd = self._loader.get_basedir()
|
|
||||||
if self._task._role is not None:
|
|
||||||
cwd = self._task._role._role_path
|
|
||||||
return cwd
|
|
||||||
|
|
||||||
def _get_network_os(self, task_vars):
|
|
||||||
if 'network_os' in self._task.args and self._task.args['network_os']:
|
|
||||||
display.vvvv('Getting network OS from task argument')
|
|
||||||
network_os = self._task.args['network_os']
|
|
||||||
elif self._play_context.network_os:
|
|
||||||
display.vvvv('Getting network OS from inventory')
|
|
||||||
network_os = self._play_context.network_os
|
|
||||||
elif 'network_os' in task_vars.get('ansible_facts', {}) and task_vars['ansible_facts']['network_os']:
|
|
||||||
display.vvvv('Getting network OS from fact')
|
|
||||||
network_os = task_vars['ansible_facts']['network_os']
|
|
||||||
else:
|
|
||||||
raise AnsibleError('ansible_network_os must be specified on this host')
|
|
||||||
|
|
||||||
return network_os
|
|
@ -1,27 +0,0 @@
|
|||||||
# (c) 2017, Ansible Inc,
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.net_base import ActionModule as _ActionModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
result = super(ActionModule, self).run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
return result
|
|
@ -1,27 +0,0 @@
|
|||||||
# (c) 2017, Ansible Inc,
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.net_base import ActionModule as _ActionModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
result = super(ActionModule, self).run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
return result
|
|
@ -1,28 +0,0 @@
|
|||||||
# (c) 2017, Ansible Inc,
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.net_base import ActionModule as _ActionModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
result = super(ActionModule, self).run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
|
|
||||||
return result
|
|
@ -1,27 +0,0 @@
|
|||||||
# (c) 2017, Ansible Inc,
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.net_base import ActionModule as _ActionModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
result = super(ActionModule, self).run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
return result
|
|
@ -1,28 +0,0 @@
|
|||||||
# (c) 2017, Ansible Inc,
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.net_base import ActionModule as _ActionModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
result = super(ActionModule, self).run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
|
|
||||||
return result
|
|
@ -1,28 +0,0 @@
|
|||||||
# (c) 2017, Ansible Inc,
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.net_base import ActionModule as _ActionModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
result = super(ActionModule, self).run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
|
|
||||||
return result
|
|
@ -1,27 +0,0 @@
|
|||||||
# (c) 2017, Ansible Inc,
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.net_base import ActionModule as _ActionModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
result = super(ActionModule, self).run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
return result
|
|
@ -1,16 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2017, Ansible by Red Hat, inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.net_base import ActionModule as _ActionModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
result = super(ActionModule, self).run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
return result
|
|
@ -1,212 +0,0 @@
|
|||||||
# (c) 2018, Ansible Inc,
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from ansible.errors import AnsibleError
|
|
||||||
from ansible.module_utils._text import to_text, to_bytes
|
|
||||||
from ansible.module_utils.connection import Connection, ConnectionError
|
|
||||||
from ansible.plugins.action import ActionBase
|
|
||||||
from ansible.module_utils.six.moves.urllib.parse import urlsplit
|
|
||||||
from ansible.utils.display import Display
|
|
||||||
|
|
||||||
display = Display()
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(ActionBase):
|
|
||||||
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
socket_path = None
|
|
||||||
network_os = self._get_network_os(task_vars).split('.')[-1]
|
|
||||||
persistent_connection = self._play_context.connection.split('.')[-1]
|
|
||||||
|
|
||||||
result = super(ActionModule, self).run(task_vars=task_vars)
|
|
||||||
|
|
||||||
if persistent_connection != 'network_cli':
|
|
||||||
# It is supported only with network_cli
|
|
||||||
result['failed'] = True
|
|
||||||
result['msg'] = ('connection type %s is not valid for net_put module,'
|
|
||||||
' please use fully qualified name of network_cli connection type' % self._play_context.connection)
|
|
||||||
return result
|
|
||||||
|
|
||||||
try:
|
|
||||||
src = self._task.args['src']
|
|
||||||
except KeyError as exc:
|
|
||||||
return {'failed': True, 'msg': 'missing required argument: %s' % exc}
|
|
||||||
|
|
||||||
src_file_path_name = src
|
|
||||||
|
|
||||||
# Get destination file if specified
|
|
||||||
dest = self._task.args.get('dest')
|
|
||||||
|
|
||||||
# Get proto
|
|
||||||
proto = self._task.args.get('protocol')
|
|
||||||
if proto is None:
|
|
||||||
proto = 'scp'
|
|
||||||
|
|
||||||
# Get mode if set
|
|
||||||
mode = self._task.args.get('mode')
|
|
||||||
if mode is None:
|
|
||||||
mode = 'binary'
|
|
||||||
|
|
||||||
if mode == 'text':
|
|
||||||
try:
|
|
||||||
self._handle_template(convert_data=False)
|
|
||||||
except ValueError as exc:
|
|
||||||
return dict(failed=True, msg=to_text(exc))
|
|
||||||
|
|
||||||
# Now src has resolved file write to disk in current diectory for scp
|
|
||||||
src = self._task.args.get('src')
|
|
||||||
filename = str(uuid.uuid4())
|
|
||||||
cwd = self._loader.get_basedir()
|
|
||||||
output_file = os.path.join(cwd, filename)
|
|
||||||
try:
|
|
||||||
with open(output_file, 'wb') as f:
|
|
||||||
f.write(to_bytes(src, encoding='utf-8'))
|
|
||||||
except Exception:
|
|
||||||
os.remove(output_file)
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
output_file = self._get_binary_src_file(src)
|
|
||||||
except ValueError as exc:
|
|
||||||
return dict(failed=True, msg=to_text(exc))
|
|
||||||
|
|
||||||
if socket_path is None:
|
|
||||||
socket_path = self._connection.socket_path
|
|
||||||
|
|
||||||
conn = Connection(socket_path)
|
|
||||||
sock_timeout = conn.get_option('persistent_command_timeout')
|
|
||||||
|
|
||||||
if dest is None:
|
|
||||||
dest = src_file_path_name
|
|
||||||
|
|
||||||
try:
|
|
||||||
changed = self._handle_existing_file(conn, output_file, dest, proto, sock_timeout)
|
|
||||||
if changed is False:
|
|
||||||
result['changed'] = changed
|
|
||||||
result['destination'] = dest
|
|
||||||
return result
|
|
||||||
except Exception as exc:
|
|
||||||
result['msg'] = ('Warning: %s idempotency check failed. Check dest' % exc)
|
|
||||||
|
|
||||||
try:
|
|
||||||
conn.copy_file(
|
|
||||||
source=output_file, destination=dest,
|
|
||||||
proto=proto, timeout=sock_timeout
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
if to_text(exc) == "No response from server":
|
|
||||||
if network_os == 'iosxr':
|
|
||||||
# IOSXR sometimes closes socket prematurely after completion
|
|
||||||
# of file transfer
|
|
||||||
result['msg'] = 'Warning: iosxr scp server pre close issue. Please check dest'
|
|
||||||
else:
|
|
||||||
result['failed'] = True
|
|
||||||
result['msg'] = 'Exception received: %s' % exc
|
|
||||||
|
|
||||||
if mode == 'text':
|
|
||||||
# Cleanup tmp file expanded wih ansible vars
|
|
||||||
os.remove(output_file)
|
|
||||||
|
|
||||||
result['changed'] = changed
|
|
||||||
result['destination'] = dest
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _handle_existing_file(self, conn, source, dest, proto, timeout):
|
|
||||||
"""
|
|
||||||
Determines whether the source and destination file match.
|
|
||||||
|
|
||||||
:return: False if source and dest both exist and have matching sha1 sums, True otherwise.
|
|
||||||
"""
|
|
||||||
cwd = self._loader.get_basedir()
|
|
||||||
filename = str(uuid.uuid4())
|
|
||||||
tmp_source_file = os.path.join(cwd, filename)
|
|
||||||
try:
|
|
||||||
conn.get_file(
|
|
||||||
source=dest, destination=tmp_source_file,
|
|
||||||
proto=proto, timeout=timeout
|
|
||||||
)
|
|
||||||
except ConnectionError as exc:
|
|
||||||
error = to_text(exc)
|
|
||||||
if error.endswith("No such file or directory"):
|
|
||||||
if os.path.exists(tmp_source_file):
|
|
||||||
os.remove(tmp_source_file)
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(source, 'r') as f:
|
|
||||||
new_content = f.read()
|
|
||||||
with open(tmp_source_file, 'r') as f:
|
|
||||||
old_content = f.read()
|
|
||||||
except (IOError, OSError):
|
|
||||||
os.remove(tmp_source_file)
|
|
||||||
raise
|
|
||||||
|
|
||||||
sha1 = hashlib.sha1()
|
|
||||||
old_content_b = to_bytes(old_content, errors='surrogate_or_strict')
|
|
||||||
sha1.update(old_content_b)
|
|
||||||
checksum_old = sha1.digest()
|
|
||||||
|
|
||||||
sha1 = hashlib.sha1()
|
|
||||||
new_content_b = to_bytes(new_content, errors='surrogate_or_strict')
|
|
||||||
sha1.update(new_content_b)
|
|
||||||
checksum_new = sha1.digest()
|
|
||||||
os.remove(tmp_source_file)
|
|
||||||
if checksum_old == checksum_new:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _get_binary_src_file(self, src):
|
|
||||||
working_path = self._get_working_path()
|
|
||||||
|
|
||||||
if os.path.isabs(src) or urlsplit('src').scheme:
|
|
||||||
source = src
|
|
||||||
else:
|
|
||||||
source = self._loader.path_dwim_relative(working_path, 'templates', src)
|
|
||||||
if not source:
|
|
||||||
source = self._loader.path_dwim_relative(working_path, src)
|
|
||||||
|
|
||||||
if not os.path.exists(source):
|
|
||||||
raise ValueError('path specified in src not found')
|
|
||||||
|
|
||||||
return source
|
|
||||||
|
|
||||||
def _get_working_path(self):
|
|
||||||
cwd = self._loader.get_basedir()
|
|
||||||
if self._task._role is not None:
|
|
||||||
cwd = self._task._role._role_path
|
|
||||||
return cwd
|
|
||||||
|
|
||||||
def _get_network_os(self, task_vars):
|
|
||||||
if 'network_os' in self._task.args and self._task.args['network_os']:
|
|
||||||
display.vvvv('Getting network OS from task argument')
|
|
||||||
network_os = self._task.args['network_os']
|
|
||||||
elif self._play_context.network_os:
|
|
||||||
display.vvvv('Getting network OS from inventory')
|
|
||||||
network_os = self._play_context.network_os
|
|
||||||
elif 'network_os' in task_vars.get('ansible_facts', {}) and task_vars['ansible_facts']['network_os']:
|
|
||||||
display.vvvv('Getting network OS from fact')
|
|
||||||
network_os = task_vars['ansible_facts']['network_os']
|
|
||||||
else:
|
|
||||||
raise AnsibleError('ansible_network_os must be specified on this host')
|
|
||||||
|
|
||||||
return network_os
|
|
@ -1,28 +0,0 @@
|
|||||||
# (c) 2017, Ansible Inc,
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.net_base import ActionModule as _ActionModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
result = super(ActionModule, self).run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
|
|
||||||
return result
|
|
@ -1,27 +0,0 @@
|
|||||||
# (c) 2017, Ansible Inc,
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.net_base import ActionModule as _ActionModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
result = super(ActionModule, self).run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
return result
|
|
@ -1,27 +0,0 @@
|
|||||||
# (c) 2017, Ansible Inc,
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.net_base import ActionModule as _ActionModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
result = super(ActionModule, self).run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
return result
|
|
@ -1,28 +0,0 @@
|
|||||||
# (c) 2017, Ansible Inc,
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.net_base import ActionModule as _ActionModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
result = super(ActionModule, self).run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
|
|
||||||
return result
|
|
@ -1,28 +0,0 @@
|
|||||||
# (c) 2017, Ansible Inc,
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from ansible.plugins.action.net_base import ActionModule as _ActionModule
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
result = super(ActionModule, self).run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
|
|
||||||
return result
|
|
@ -1,90 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2018 Red Hat Inc.
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
import copy
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from ansible.plugins.action.network import ActionModule as ActionNetworkModule
|
|
||||||
from ansible.utils.display import Display
|
|
||||||
|
|
||||||
display = Display()
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(ActionNetworkModule):
|
|
||||||
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
|
|
||||||
module_name = self._task.action.split('.')[-1]
|
|
||||||
self._config_module = True if module_name == 'netconf_config' else False
|
|
||||||
persistent_connection = self._play_context.connection.split('.')[-1]
|
|
||||||
warnings = []
|
|
||||||
|
|
||||||
if persistent_connection not in ['netconf', 'local'] and module_name == 'netconf_config':
|
|
||||||
return {'failed': True, 'msg': 'Connection type %s is not valid for netconf_config module. '
|
|
||||||
'Valid connection type is netconf or local (deprecated)' % self._play_context.connection}
|
|
||||||
elif persistent_connection not in ['netconf'] and module_name != 'netconf_config':
|
|
||||||
return {'failed': True, 'msg': 'Connection type %s is not valid for %s module. '
|
|
||||||
'Valid connection type is netconf.' % (self._play_context.connection, module_name)}
|
|
||||||
|
|
||||||
if self._play_context.connection == 'local' and module_name == 'netconf_config':
|
|
||||||
args = self._task.args
|
|
||||||
pc = copy.deepcopy(self._play_context)
|
|
||||||
pc.connection = 'ansible.netcommon.netconf'
|
|
||||||
pc.port = int(args.get('port') or self._play_context.port or 830)
|
|
||||||
|
|
||||||
pc.remote_user = args.get('username') or self._play_context.connection_user
|
|
||||||
pc.password = args.get('password') or self._play_context.password
|
|
||||||
pc.private_key_file = args.get('ssh_keyfile') or self._play_context.private_key_file
|
|
||||||
|
|
||||||
connection = self._shared_loader_obj.connection_loader.get('ansible.netcommon.persistent', pc, sys.stdin,
|
|
||||||
task_uuid=self._task._uuid)
|
|
||||||
|
|
||||||
# TODO: Remove below code after ansible minimal is cut out
|
|
||||||
if connection is None:
|
|
||||||
pc.connection = 'netconf'
|
|
||||||
connection = self._shared_loader_obj.connection_loader.get('persistent', pc, sys.stdin, task_uuid=self._task._uuid)
|
|
||||||
|
|
||||||
display.vvv('using connection plugin %s (was local)' % pc.connection, pc.remote_addr)
|
|
||||||
|
|
||||||
timeout = args.get('timeout')
|
|
||||||
command_timeout = int(timeout) if timeout else connection.get_option('persistent_command_timeout')
|
|
||||||
connection.set_options(direct={'persistent_command_timeout': command_timeout, 'look_for_keys': args.get('look_for_keys'),
|
|
||||||
'hostkey_verify': args.get('hostkey_verify'),
|
|
||||||
'allow_agent': args.get('allow_agent')})
|
|
||||||
|
|
||||||
socket_path = connection.run()
|
|
||||||
display.vvvv('socket_path: %s' % socket_path, pc.remote_addr)
|
|
||||||
if not socket_path:
|
|
||||||
return {'failed': True,
|
|
||||||
'msg': 'unable to open shell. Please see: ' +
|
|
||||||
'https://docs.ansible.com/ansible/network_debug_troubleshooting.html#unable-to-open-shell'}
|
|
||||||
|
|
||||||
task_vars['ansible_socket'] = socket_path
|
|
||||||
warnings.append(['connection local support for this module is deprecated and will be removed in version 2.14, use connection %s' % pc.connection])
|
|
||||||
|
|
||||||
result = super(ActionModule, self).run(task_vars=task_vars)
|
|
||||||
if warnings:
|
|
||||||
if 'warnings' in result:
|
|
||||||
result['warnings'].extend(warnings)
|
|
||||||
else:
|
|
||||||
result['warnings'] = warnings
|
|
||||||
return result
|
|
@ -1,178 +0,0 @@
|
|||||||
#
|
|
||||||
# (c) 2018 Red Hat Inc.
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import re
|
|
||||||
|
|
||||||
from ansible.errors import AnsibleError
|
|
||||||
from ansible.module_utils._text import to_text, to_bytes
|
|
||||||
from ansible.module_utils.six.moves.urllib.parse import urlsplit
|
|
||||||
from ansible.plugins.action.normal import ActionModule as _ActionModule
|
|
||||||
from ansible.utils.display import Display
|
|
||||||
|
|
||||||
display = Display()
|
|
||||||
|
|
||||||
PRIVATE_KEYS_RE = re.compile('__.+__')
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(_ActionModule):
|
|
||||||
|
|
||||||
def run(self, task_vars=None):
|
|
||||||
config_module = hasattr(self, '_config_module') and self._config_module
|
|
||||||
if config_module and self._task.args.get('src'):
|
|
||||||
try:
|
|
||||||
self._handle_src_option()
|
|
||||||
except AnsibleError as e:
|
|
||||||
return {'failed': True, 'msg': e.message, 'changed': False}
|
|
||||||
|
|
||||||
result = super(ActionModule, self).run(task_vars=task_vars)
|
|
||||||
|
|
||||||
if config_module and self._task.args.get('backup') and not result.get('failed'):
|
|
||||||
self._handle_backup_option(result, task_vars)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _handle_backup_option(self, result, task_vars):
|
|
||||||
|
|
||||||
filename = None
|
|
||||||
backup_path = None
|
|
||||||
try:
|
|
||||||
content = result['__backup__']
|
|
||||||
except KeyError:
|
|
||||||
raise AnsibleError('Failed while reading configuration backup')
|
|
||||||
|
|
||||||
backup_options = self._task.args.get('backup_options')
|
|
||||||
if backup_options:
|
|
||||||
filename = backup_options.get('filename')
|
|
||||||
backup_path = backup_options.get('dir_path')
|
|
||||||
|
|
||||||
if not backup_path:
|
|
||||||
cwd = self._get_working_path()
|
|
||||||
backup_path = os.path.join(cwd, 'backup')
|
|
||||||
if not filename:
|
|
||||||
tstamp = time.strftime("%Y-%m-%d@%H:%M:%S", time.localtime(time.time()))
|
|
||||||
filename = '%s_config.%s' % (task_vars['inventory_hostname'], tstamp)
|
|
||||||
|
|
||||||
dest = os.path.join(backup_path, filename)
|
|
||||||
backup_path = os.path.expanduser(os.path.expandvars(to_bytes(backup_path, errors='surrogate_or_strict')))
|
|
||||||
|
|
||||||
if not os.path.exists(backup_path):
|
|
||||||
os.makedirs(backup_path)
|
|
||||||
|
|
||||||
new_task = self._task.copy()
|
|
||||||
for item in self._task.args:
|
|
||||||
if not item.startswith('_'):
|
|
||||||
new_task.args.pop(item, None)
|
|
||||||
|
|
||||||
new_task.args.update(
|
|
||||||
dict(
|
|
||||||
content=content,
|
|
||||||
dest=dest,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
copy_action = self._shared_loader_obj.action_loader.get('copy',
|
|
||||||
task=new_task,
|
|
||||||
connection=self._connection,
|
|
||||||
play_context=self._play_context,
|
|
||||||
loader=self._loader,
|
|
||||||
templar=self._templar,
|
|
||||||
shared_loader_obj=self._shared_loader_obj)
|
|
||||||
copy_result = copy_action.run(task_vars=task_vars)
|
|
||||||
if copy_result.get('failed'):
|
|
||||||
result['failed'] = copy_result['failed']
|
|
||||||
result['msg'] = copy_result.get('msg')
|
|
||||||
return
|
|
||||||
|
|
||||||
result['backup_path'] = dest
|
|
||||||
if copy_result.get('changed', False):
|
|
||||||
result['changed'] = copy_result['changed']
|
|
||||||
|
|
||||||
if backup_options and backup_options.get('filename'):
|
|
||||||
result['date'] = time.strftime('%Y-%m-%d', time.gmtime(os.stat(result['backup_path']).st_ctime))
|
|
||||||
result['time'] = time.strftime('%H:%M:%S', time.gmtime(os.stat(result['backup_path']).st_ctime))
|
|
||||||
|
|
||||||
else:
|
|
||||||
result['date'] = tstamp.split('@')[0]
|
|
||||||
result['time'] = tstamp.split('@')[1]
|
|
||||||
result['shortname'] = result['backup_path'][::-1].split('.', 1)[1][::-1]
|
|
||||||
result['filename'] = result['backup_path'].split('/')[-1]
|
|
||||||
|
|
||||||
# strip out any keys that have two leading and two trailing
|
|
||||||
# underscore characters
|
|
||||||
for key in list(result.keys()):
|
|
||||||
if PRIVATE_KEYS_RE.match(key):
|
|
||||||
del result[key]
|
|
||||||
|
|
||||||
def _get_working_path(self):
|
|
||||||
cwd = self._loader.get_basedir()
|
|
||||||
if self._task._role is not None:
|
|
||||||
cwd = self._task._role._role_path
|
|
||||||
return cwd
|
|
||||||
|
|
||||||
def _handle_src_option(self, convert_data=True):
|
|
||||||
src = self._task.args.get('src')
|
|
||||||
working_path = self._get_working_path()
|
|
||||||
|
|
||||||
if os.path.isabs(src) or urlsplit('src').scheme:
|
|
||||||
source = src
|
|
||||||
else:
|
|
||||||
source = self._loader.path_dwim_relative(working_path, 'templates', src)
|
|
||||||
if not source:
|
|
||||||
source = self._loader.path_dwim_relative(working_path, src)
|
|
||||||
|
|
||||||
if not os.path.exists(source):
|
|
||||||
raise AnsibleError('path specified in src not found')
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(source, 'r') as f:
|
|
||||||
template_data = to_text(f.read())
|
|
||||||
except IOError as e:
|
|
||||||
raise AnsibleError("unable to load src file {0}, I/O error({1}): {2}".format(source, e.errno, e.strerror))
|
|
||||||
|
|
||||||
# Create a template search path in the following order:
|
|
||||||
# [working_path, self_role_path, dependent_role_paths, dirname(source)]
|
|
||||||
searchpath = [working_path]
|
|
||||||
if self._task._role is not None:
|
|
||||||
searchpath.append(self._task._role._role_path)
|
|
||||||
if hasattr(self._task, "_block:"):
|
|
||||||
dep_chain = self._task._block.get_dep_chain()
|
|
||||||
if dep_chain is not None:
|
|
||||||
for role in dep_chain:
|
|
||||||
searchpath.append(role._role_path)
|
|
||||||
searchpath.append(os.path.dirname(source))
|
|
||||||
with self._templar.set_temporary_context(searchpath=searchpath):
|
|
||||||
self._task.args['src'] = self._templar.template(template_data, convert_data=convert_data)
|
|
||||||
|
|
||||||
def _get_network_os(self, task_vars):
|
|
||||||
if 'network_os' in self._task.args and self._task.args['network_os']:
|
|
||||||
display.vvvv('Getting network OS from task argument')
|
|
||||||
network_os = self._task.args['network_os']
|
|
||||||
elif self._play_context.network_os:
|
|
||||||
display.vvvv('Getting network OS from inventory')
|
|
||||||
network_os = self._play_context.network_os
|
|
||||||
elif 'network_os' in task_vars.get('ansible_facts', {}) and task_vars['ansible_facts']['network_os']:
|
|
||||||
display.vvvv('Getting network OS from fact')
|
|
||||||
network_os = task_vars['ansible_facts']['network_os']
|
|
||||||
else:
|
|
||||||
raise AnsibleError('ansible_network_os must be specified on this host')
|
|
||||||
|
|
||||||
return network_os
|
|
@ -1,96 +0,0 @@
|
|||||||
# (c) 2017, Ansible Project
|
|
||||||
#
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
#
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
import telnetlib
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
from ansible.module_utils._text import to_native, to_bytes
|
|
||||||
from ansible.module_utils.six import text_type
|
|
||||||
from ansible.plugins.action import ActionBase
|
|
||||||
from ansible.utils.display import Display
|
|
||||||
|
|
||||||
display = Display()
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(ActionBase):
|
|
||||||
TRANSFERS_FILES = False
|
|
||||||
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
|
|
||||||
if self._task.environment and any(self._task.environment):
|
|
||||||
self._display.warning('The telnet task does not support the environment keyword')
|
|
||||||
|
|
||||||
result = super(ActionModule, self).run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
|
||||||
|
|
||||||
if self._play_context.check_mode:
|
|
||||||
# in --check mode, always skip this module execution
|
|
||||||
result['skipped'] = True
|
|
||||||
result['msg'] = 'The telnet task does not support check mode'
|
|
||||||
else:
|
|
||||||
result['changed'] = True
|
|
||||||
result['failed'] = False
|
|
||||||
|
|
||||||
host = self._task.args.get('host', self._play_context.remote_addr)
|
|
||||||
user = self._task.args.get('user', self._play_context.remote_user)
|
|
||||||
password = self._task.args.get('password', self._play_context.password)
|
|
||||||
|
|
||||||
# FIXME, default to play_context?
|
|
||||||
port = self._task.args.get('port', '23')
|
|
||||||
timeout = self._task.args.get('timeout', 120)
|
|
||||||
pause = self._task.args.get('pause', 1)
|
|
||||||
|
|
||||||
send_newline = self._task.args.get('send_newline', False)
|
|
||||||
|
|
||||||
login_prompt = self._task.args.get('login_prompt', "login: ")
|
|
||||||
password_prompt = self._task.args.get('password_prompt', "Password: ")
|
|
||||||
prompts = self._task.args.get('prompts', ["\\$ "])
|
|
||||||
commands = self._task.args.get('command') or self._task.args.get('commands')
|
|
||||||
|
|
||||||
if isinstance(commands, text_type):
|
|
||||||
commands = commands.split(',')
|
|
||||||
|
|
||||||
if isinstance(commands, list) and commands:
|
|
||||||
|
|
||||||
tn = telnetlib.Telnet(host, port, timeout)
|
|
||||||
|
|
||||||
output = []
|
|
||||||
try:
|
|
||||||
if send_newline:
|
|
||||||
tn.write(b'\n')
|
|
||||||
|
|
||||||
tn.read_until(to_bytes(login_prompt))
|
|
||||||
tn.write(to_bytes(user + "\n"))
|
|
||||||
|
|
||||||
if password:
|
|
||||||
tn.read_until(to_bytes(password_prompt))
|
|
||||||
tn.write(to_bytes(password + "\n"))
|
|
||||||
|
|
||||||
tn.expect(list(map(to_bytes, prompts)))
|
|
||||||
|
|
||||||
for cmd in commands:
|
|
||||||
display.vvvvv('>>> %s' % cmd)
|
|
||||||
tn.write(to_bytes(cmd + "\n"))
|
|
||||||
index, match, out = tn.expect(list(map(to_bytes, prompts)), timeout=timeout)
|
|
||||||
display.vvvvv('<<< %s' % cmd)
|
|
||||||
output.append(out)
|
|
||||||
sleep(pause)
|
|
||||||
|
|
||||||
tn.write(b"exit\n")
|
|
||||||
|
|
||||||
except EOFError as e:
|
|
||||||
result['failed'] = True
|
|
||||||
result['msg'] = 'Telnet action failed: %s' % to_native(e)
|
|
||||||
finally:
|
|
||||||
if tn:
|
|
||||||
tn.close()
|
|
||||||
result['output'] = output
|
|
||||||
else:
|
|
||||||
result['failed'] = True
|
|
||||||
result['msg'] = 'Telnet requires a command to execute'
|
|
||||||
|
|
||||||
return result
|
|
@ -1,41 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright: (c) 2018, Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
become: enable
|
|
||||||
short_description: Switch to elevated permissions on a network device
|
|
||||||
description:
|
|
||||||
- This become plugins allows elevated permissions on a remote network device.
|
|
||||||
author: ansible (@core)
|
|
||||||
version_added: "2.8"
|
|
||||||
options:
|
|
||||||
become_pass:
|
|
||||||
description: password
|
|
||||||
ini:
|
|
||||||
- section: enable_become_plugin
|
|
||||||
key: password
|
|
||||||
vars:
|
|
||||||
- name: ansible_become_password
|
|
||||||
- name: ansible_become_pass
|
|
||||||
- name: ansible_enable_pass
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_BECOME_PASS
|
|
||||||
- name: ANSIBLE_ENABLE_PASS
|
|
||||||
notes:
|
|
||||||
- enable is really implemented in the network connection handler and as such can only be used with network connections.
|
|
||||||
- This plugin ignores the 'become_exe' and 'become_user' settings as it uses an API and not an executable.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ansible.plugins.become import BecomeBase
|
|
||||||
|
|
||||||
|
|
||||||
class BecomeModule(BecomeBase):
|
|
||||||
|
|
||||||
name = 'enable'
|
|
||||||
|
|
||||||
def build_become_command(self, cmd, shell):
|
|
||||||
# enable is implemented inside the network connection plugins
|
|
||||||
return cmd
|
|
@ -1,301 +0,0 @@
|
|||||||
# (c) 2018 Red Hat Inc.
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
author: Ansible Networking Team
|
|
||||||
connection: httpapi
|
|
||||||
short_description: Use httpapi to run command on network appliances
|
|
||||||
description:
|
|
||||||
- This connection plugin provides a connection to remote devices over a
|
|
||||||
HTTP(S)-based api.
|
|
||||||
version_added: "2.6"
|
|
||||||
options:
|
|
||||||
host:
|
|
||||||
description:
|
|
||||||
- Specifies the remote device FQDN or IP address to establish the HTTP(S)
|
|
||||||
connection to.
|
|
||||||
default: inventory_hostname
|
|
||||||
vars:
|
|
||||||
- name: ansible_host
|
|
||||||
port:
|
|
||||||
type: int
|
|
||||||
description:
|
|
||||||
- Specifies the port on the remote device that listens for connections
|
|
||||||
when establishing the HTTP(S) connection.
|
|
||||||
- When unspecified, will pick 80 or 443 based on the value of use_ssl.
|
|
||||||
ini:
|
|
||||||
- section: defaults
|
|
||||||
key: remote_port
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_REMOTE_PORT
|
|
||||||
vars:
|
|
||||||
- name: ansible_httpapi_port
|
|
||||||
network_os:
|
|
||||||
description:
|
|
||||||
- Configures the device platform network operating system. This value is
|
|
||||||
used to load the correct httpapi plugin to communicate with the remote
|
|
||||||
device
|
|
||||||
vars:
|
|
||||||
- name: ansible_network_os
|
|
||||||
remote_user:
|
|
||||||
description:
|
|
||||||
- The username used to authenticate to the remote device when the API
|
|
||||||
connection is first established. If the remote_user is not specified,
|
|
||||||
the connection will use the username of the logged in user.
|
|
||||||
- Can be configured from the CLI via the C(--user) or C(-u) options.
|
|
||||||
ini:
|
|
||||||
- section: defaults
|
|
||||||
key: remote_user
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_REMOTE_USER
|
|
||||||
vars:
|
|
||||||
- name: ansible_user
|
|
||||||
password:
|
|
||||||
description:
|
|
||||||
- Configures the user password used to authenticate to the remote device
|
|
||||||
when needed for the device API.
|
|
||||||
vars:
|
|
||||||
- name: ansible_password
|
|
||||||
- name: ansible_httpapi_pass
|
|
||||||
- name: ansible_httpapi_password
|
|
||||||
use_ssl:
|
|
||||||
type: boolean
|
|
||||||
description:
|
|
||||||
- Whether to connect using SSL (HTTPS) or not (HTTP).
|
|
||||||
default: False
|
|
||||||
vars:
|
|
||||||
- name: ansible_httpapi_use_ssl
|
|
||||||
validate_certs:
|
|
||||||
type: boolean
|
|
||||||
version_added: '2.7'
|
|
||||||
description:
|
|
||||||
- Whether to validate SSL certificates
|
|
||||||
default: True
|
|
||||||
vars:
|
|
||||||
- name: ansible_httpapi_validate_certs
|
|
||||||
use_proxy:
|
|
||||||
type: boolean
|
|
||||||
version_added: "2.9"
|
|
||||||
description:
|
|
||||||
- Whether to use https_proxy for requests.
|
|
||||||
default: True
|
|
||||||
vars:
|
|
||||||
- name: ansible_httpapi_use_proxy
|
|
||||||
become:
|
|
||||||
type: boolean
|
|
||||||
description:
|
|
||||||
- The become option will instruct the CLI session to attempt privilege
|
|
||||||
escalation on platforms that support it. Normally this means
|
|
||||||
transitioning from user mode to C(enable) mode in the CLI session.
|
|
||||||
If become is set to True and the remote device does not support
|
|
||||||
privilege escalation or the privilege has already been elevated, then
|
|
||||||
this option is silently ignored.
|
|
||||||
- Can be configured from the CLI via the C(--become) or C(-b) options.
|
|
||||||
default: False
|
|
||||||
ini:
|
|
||||||
- section: privilege_escalation
|
|
||||||
key: become
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_BECOME
|
|
||||||
vars:
|
|
||||||
- name: ansible_become
|
|
||||||
become_method:
|
|
||||||
description:
|
|
||||||
- This option allows the become method to be specified in for handling
|
|
||||||
privilege escalation. Typically the become_method value is set to
|
|
||||||
C(enable) but could be defined as other values.
|
|
||||||
default: sudo
|
|
||||||
ini:
|
|
||||||
- section: privilege_escalation
|
|
||||||
key: become_method
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_BECOME_METHOD
|
|
||||||
vars:
|
|
||||||
- name: ansible_become_method
|
|
||||||
persistent_connect_timeout:
|
|
||||||
type: int
|
|
||||||
description:
|
|
||||||
- Configures, in seconds, the amount of time to wait when trying to
|
|
||||||
initially establish a persistent connection. If this value expires
|
|
||||||
before the connection to the remote device is completed, the connection
|
|
||||||
will fail.
|
|
||||||
default: 30
|
|
||||||
ini:
|
|
||||||
- section: persistent_connection
|
|
||||||
key: connect_timeout
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PERSISTENT_CONNECT_TIMEOUT
|
|
||||||
vars:
|
|
||||||
- name: ansible_connect_timeout
|
|
||||||
persistent_command_timeout:
|
|
||||||
type: int
|
|
||||||
description:
|
|
||||||
- Configures, in seconds, the amount of time to wait for a command to
|
|
||||||
return from the remote device. If this timer is exceeded before the
|
|
||||||
command returns, the connection plugin will raise an exception and
|
|
||||||
close.
|
|
||||||
default: 30
|
|
||||||
ini:
|
|
||||||
- section: persistent_connection
|
|
||||||
key: command_timeout
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PERSISTENT_COMMAND_TIMEOUT
|
|
||||||
vars:
|
|
||||||
- name: ansible_command_timeout
|
|
||||||
persistent_log_messages:
|
|
||||||
type: boolean
|
|
||||||
description:
|
|
||||||
- This flag will enable logging the command executed and response received from
|
|
||||||
target device in the ansible log file. For this option to work 'log_path' ansible
|
|
||||||
configuration option is required to be set to a file path with write access.
|
|
||||||
- Be sure to fully understand the security implications of enabling this
|
|
||||||
option as it could create a security vulnerability by logging sensitive information in log file.
|
|
||||||
default: False
|
|
||||||
ini:
|
|
||||||
- section: persistent_connection
|
|
||||||
key: log_messages
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PERSISTENT_LOG_MESSAGES
|
|
||||||
vars:
|
|
||||||
- name: ansible_persistent_log_messages
|
|
||||||
"""
|
|
||||||
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
from ansible.errors import AnsibleConnectionFailure
|
|
||||||
from ansible.module_utils._text import to_bytes
|
|
||||||
from ansible.module_utils.six import PY3
|
|
||||||
from ansible.module_utils.six.moves import cPickle
|
|
||||||
from ansible.module_utils.six.moves.urllib.error import HTTPError, URLError
|
|
||||||
from ansible.module_utils.urls import open_url
|
|
||||||
from ansible.playbook.play_context import PlayContext
|
|
||||||
from ansible.plugins.loader import httpapi_loader
|
|
||||||
from ansible.plugins.connection import NetworkConnectionBase, ensure_connect
|
|
||||||
|
|
||||||
|
|
||||||
class Connection(NetworkConnectionBase):
|
|
||||||
'''Network API connection'''
|
|
||||||
|
|
||||||
transport = 'httpapi'
|
|
||||||
has_pipelining = True
|
|
||||||
|
|
||||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
|
||||||
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
|
||||||
|
|
||||||
self._url = None
|
|
||||||
self._auth = None
|
|
||||||
|
|
||||||
if self._network_os:
|
|
||||||
|
|
||||||
self.httpapi = httpapi_loader.get(self._network_os, self)
|
|
||||||
if self.httpapi:
|
|
||||||
self._sub_plugin = {'type': 'httpapi', 'name': self.httpapi._load_name, 'obj': self.httpapi}
|
|
||||||
self.queue_message('vvvv', 'loaded API plugin %s from path %s for network_os %s' %
|
|
||||||
(self.httpapi._load_name, self.httpapi._original_path, self._network_os))
|
|
||||||
else:
|
|
||||||
raise AnsibleConnectionFailure('unable to load API plugin for network_os %s' % self._network_os)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise AnsibleConnectionFailure(
|
|
||||||
'Unable to automatically determine host network os. Please '
|
|
||||||
'manually configure ansible_network_os value for this host'
|
|
||||||
)
|
|
||||||
self.queue_message('log', 'network_os is set to %s' % self._network_os)
|
|
||||||
|
|
||||||
def update_play_context(self, pc_data):
|
|
||||||
"""Updates the play context information for the connection"""
|
|
||||||
pc_data = to_bytes(pc_data)
|
|
||||||
if PY3:
|
|
||||||
pc_data = cPickle.loads(pc_data, encoding='bytes')
|
|
||||||
else:
|
|
||||||
pc_data = cPickle.loads(pc_data)
|
|
||||||
play_context = PlayContext()
|
|
||||||
play_context.deserialize(pc_data)
|
|
||||||
|
|
||||||
self.queue_message('vvvv', 'updating play_context for connection')
|
|
||||||
if self._play_context.become ^ play_context.become:
|
|
||||||
self.set_become(play_context)
|
|
||||||
if play_context.become is True:
|
|
||||||
self.queue_message('vvvv', 'authorizing connection')
|
|
||||||
else:
|
|
||||||
self.queue_message('vvvv', 'deauthorizing connection')
|
|
||||||
|
|
||||||
self._play_context = play_context
|
|
||||||
|
|
||||||
def _connect(self):
|
|
||||||
if not self.connected:
|
|
||||||
protocol = 'https' if self.get_option('use_ssl') else 'http'
|
|
||||||
host = self.get_option('host')
|
|
||||||
port = self.get_option('port') or (443 if protocol == 'https' else 80)
|
|
||||||
self._url = '%s://%s:%s' % (protocol, host, port)
|
|
||||||
|
|
||||||
self.queue_message('vvv', "ESTABLISH HTTP(S) CONNECTFOR USER: %s TO %s" %
|
|
||||||
(self._play_context.remote_user, self._url))
|
|
||||||
self.httpapi.set_become(self._play_context)
|
|
||||||
self._connected = True
|
|
||||||
|
|
||||||
self.httpapi.login(self.get_option('remote_user'), self.get_option('password'))
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
'''
|
|
||||||
Close the active session to the device
|
|
||||||
'''
|
|
||||||
# only close the connection if its connected.
|
|
||||||
if self._connected:
|
|
||||||
self.queue_message('vvvv', "closing http(s) connection to device")
|
|
||||||
self.logout()
|
|
||||||
|
|
||||||
super(Connection, self).close()
|
|
||||||
|
|
||||||
@ensure_connect
|
|
||||||
def send(self, path, data, **kwargs):
|
|
||||||
'''
|
|
||||||
Sends the command to the device over api
|
|
||||||
'''
|
|
||||||
url_kwargs = dict(
|
|
||||||
timeout=self.get_option('persistent_command_timeout'),
|
|
||||||
validate_certs=self.get_option('validate_certs'),
|
|
||||||
use_proxy=self.get_option("use_proxy"),
|
|
||||||
headers={},
|
|
||||||
)
|
|
||||||
url_kwargs.update(kwargs)
|
|
||||||
if self._auth:
|
|
||||||
# Avoid modifying passed-in headers
|
|
||||||
headers = dict(kwargs.get('headers', {}))
|
|
||||||
headers.update(self._auth)
|
|
||||||
url_kwargs['headers'] = headers
|
|
||||||
else:
|
|
||||||
url_kwargs['force_basic_auth'] = True
|
|
||||||
url_kwargs['url_username'] = self.get_option('remote_user')
|
|
||||||
url_kwargs['url_password'] = self.get_option('password')
|
|
||||||
|
|
||||||
try:
|
|
||||||
url = self._url + path
|
|
||||||
self._log_messages("send url '%s' with data '%s' and kwargs '%s'" % (url, data, url_kwargs))
|
|
||||||
response = open_url(url, data=data, **url_kwargs)
|
|
||||||
except HTTPError as exc:
|
|
||||||
is_handled = self.handle_httperror(exc)
|
|
||||||
if is_handled is True:
|
|
||||||
return self.send(path, data, **kwargs)
|
|
||||||
elif is_handled is False:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
response = is_handled
|
|
||||||
except URLError as exc:
|
|
||||||
raise AnsibleConnectionFailure('Could not connect to {0}: {1}'.format(self._url + path, exc.reason))
|
|
||||||
|
|
||||||
response_buffer = BytesIO()
|
|
||||||
resp_data = response.read()
|
|
||||||
self._log_messages("received response: '%s'" % resp_data)
|
|
||||||
response_buffer.write(resp_data)
|
|
||||||
|
|
||||||
# Try to assign a new auth token if one is given
|
|
||||||
self._auth = self.update_auth(response, response_buffer) or self._auth
|
|
||||||
|
|
||||||
response_buffer.seek(0)
|
|
||||||
|
|
||||||
return response, response_buffer
|
|
@ -1,195 +0,0 @@
|
|||||||
# (c) 2018 Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
author: Ansible Networking Team
|
|
||||||
connection: napalm
|
|
||||||
short_description: Provides persistent connection using NAPALM
|
|
||||||
description:
|
|
||||||
- This connection plugin provides connectivity to network devices using
|
|
||||||
the NAPALM network device abstraction library. This library requires
|
|
||||||
certain features to be enabled on network devices depending on the
|
|
||||||
destination device operating system. The connection plugin requires
|
|
||||||
C(napalm) to be installed locally on the Ansible controller.
|
|
||||||
version_added: "2.8"
|
|
||||||
requirements:
|
|
||||||
- napalm
|
|
||||||
options:
|
|
||||||
host:
|
|
||||||
description:
|
|
||||||
- Specifies the remote device FQDN or IP address to establish the SSH
|
|
||||||
connection to.
|
|
||||||
default: inventory_hostname
|
|
||||||
vars:
|
|
||||||
- name: ansible_host
|
|
||||||
port:
|
|
||||||
type: int
|
|
||||||
description:
|
|
||||||
- Specifies the port on the remote device that listens for connections
|
|
||||||
when establishing the SSH connection.
|
|
||||||
default: 22
|
|
||||||
ini:
|
|
||||||
- section: defaults
|
|
||||||
key: remote_port
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_REMOTE_PORT
|
|
||||||
vars:
|
|
||||||
- name: ansible_port
|
|
||||||
network_os:
|
|
||||||
description:
|
|
||||||
- Configures the device platform network operating system. This value is
|
|
||||||
used to load a napalm device abstraction.
|
|
||||||
vars:
|
|
||||||
- name: ansible_network_os
|
|
||||||
remote_user:
|
|
||||||
description:
|
|
||||||
- The username used to authenticate to the remote device when the SSH
|
|
||||||
connection is first established. If the remote_user is not specified,
|
|
||||||
the connection will use the username of the logged in user.
|
|
||||||
- Can be configured from the CLI via the C(--user) or C(-u) options.
|
|
||||||
ini:
|
|
||||||
- section: defaults
|
|
||||||
key: remote_user
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_REMOTE_USER
|
|
||||||
vars:
|
|
||||||
- name: ansible_user
|
|
||||||
password:
|
|
||||||
description:
|
|
||||||
- Configures the user password used to authenticate to the remote device
|
|
||||||
when first establishing the SSH connection.
|
|
||||||
vars:
|
|
||||||
- name: ansible_password
|
|
||||||
- name: ansible_ssh_pass
|
|
||||||
- name: ansible_ssh_password
|
|
||||||
private_key_file:
|
|
||||||
description:
|
|
||||||
- The private SSH key or certificate file used to authenticate to the
|
|
||||||
remote device when first establishing the SSH connection.
|
|
||||||
ini:
|
|
||||||
- section: defaults
|
|
||||||
key: private_key_file
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PRIVATE_KEY_FILE
|
|
||||||
vars:
|
|
||||||
- name: ansible_private_key_file
|
|
||||||
timeout:
|
|
||||||
type: int
|
|
||||||
description:
|
|
||||||
- Sets the connection time, in seconds, for communicating with the
|
|
||||||
remote device. This timeout is used as the default timeout value for
|
|
||||||
commands when issuing a command to the network CLI. If the command
|
|
||||||
does not return in timeout seconds, an error is generated.
|
|
||||||
default: 120
|
|
||||||
host_key_auto_add:
|
|
||||||
type: boolean
|
|
||||||
description:
|
|
||||||
- By default, Ansible will prompt the user before adding SSH keys to the
|
|
||||||
known hosts file. By enabling this option, unknown host keys will
|
|
||||||
automatically be added to the known hosts file.
|
|
||||||
- Be sure to fully understand the security implications of enabling this
|
|
||||||
option on production systems as it could create a security vulnerability.
|
|
||||||
default: False
|
|
||||||
ini:
|
|
||||||
- section: paramiko_connection
|
|
||||||
key: host_key_auto_add
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_HOST_KEY_AUTO_ADD
|
|
||||||
persistent_connect_timeout:
|
|
||||||
type: int
|
|
||||||
description:
|
|
||||||
- Configures, in seconds, the amount of time to wait when trying to
|
|
||||||
initially establish a persistent connection. If this value expires
|
|
||||||
before the connection to the remote device is completed, the connection
|
|
||||||
will fail.
|
|
||||||
default: 30
|
|
||||||
ini:
|
|
||||||
- section: persistent_connection
|
|
||||||
key: connect_timeout
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PERSISTENT_CONNECT_TIMEOUT
|
|
||||||
vars:
|
|
||||||
- name: ansible_connect_timeout
|
|
||||||
persistent_command_timeout:
|
|
||||||
type: int
|
|
||||||
description:
|
|
||||||
- Configures, in seconds, the amount of time to wait for a command to
|
|
||||||
return from the remote device. If this timer is exceeded before the
|
|
||||||
command returns, the connection plugin will raise an exception and
|
|
||||||
close.
|
|
||||||
default: 30
|
|
||||||
ini:
|
|
||||||
- section: persistent_connection
|
|
||||||
key: command_timeout
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PERSISTENT_COMMAND_TIMEOUT
|
|
||||||
vars:
|
|
||||||
- name: ansible_command_timeout
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ansible.errors import AnsibleConnectionFailure, AnsibleError
|
|
||||||
from ansible.plugins.connection import NetworkConnectionBase
|
|
||||||
|
|
||||||
try:
|
|
||||||
from napalm import get_network_driver
|
|
||||||
from napalm.base import ModuleImportError
|
|
||||||
HAS_NAPALM = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_NAPALM = False
|
|
||||||
|
|
||||||
|
|
||||||
class Connection(NetworkConnectionBase):
|
|
||||||
"""Napalm connections"""
|
|
||||||
|
|
||||||
transport = 'napalm'
|
|
||||||
has_pipelining = False
|
|
||||||
|
|
||||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
|
||||||
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
|
||||||
|
|
||||||
self.napalm = None
|
|
||||||
|
|
||||||
def _connect(self):
|
|
||||||
if not HAS_NAPALM:
|
|
||||||
raise AnsibleError('The "napalm" python library is required to use the napalm connection type.\n')
|
|
||||||
|
|
||||||
super(Connection, self)._connect()
|
|
||||||
|
|
||||||
if not self.connected:
|
|
||||||
if not self._network_os:
|
|
||||||
raise AnsibleConnectionFailure(
|
|
||||||
'Unable to automatically determine host network os. Please '
|
|
||||||
'manually configure ansible_network_os value for this host'
|
|
||||||
)
|
|
||||||
self.queue_message('log', 'network_os is set to %s' % self._network_os)
|
|
||||||
|
|
||||||
try:
|
|
||||||
driver = get_network_driver(self._network_os)
|
|
||||||
except ModuleImportError:
|
|
||||||
raise AnsibleConnectionFailure('Failed to import napalm driver for {0}'.format(self._network_os))
|
|
||||||
|
|
||||||
host = self.get_option('host')
|
|
||||||
self.napalm = driver(
|
|
||||||
hostname=host,
|
|
||||||
username=self.get_option('remote_user'),
|
|
||||||
password=self.get_option('password'),
|
|
||||||
timeout=self.get_option('persistent_command_timeout'),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.napalm.open()
|
|
||||||
|
|
||||||
self._sub_plugin = {'name': 'napalm', 'obj': self.napalm}
|
|
||||||
self.queue_message('vvvv', 'created napalm device for network_os %s' % self._network_os)
|
|
||||||
self._connected = True
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if self.napalm:
|
|
||||||
self.napalm.close()
|
|
||||||
self.napalm = None
|
|
||||||
|
|
||||||
super(Connection, self).close()
|
|
@ -1,337 +0,0 @@
|
|||||||
# (c) 2016 Red Hat Inc.
|
|
||||||
# (c) 2017 Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
author: Ansible Networking Team
|
|
||||||
connection: netconf
|
|
||||||
short_description: Provides a persistent connection using the netconf protocol
|
|
||||||
description:
|
|
||||||
- This connection plugin provides a connection to remote devices over the
|
|
||||||
SSH NETCONF subsystem. This connection plugin is typically used by
|
|
||||||
network devices for sending and receiving RPC calls over NETCONF.
|
|
||||||
- Note this connection plugin requires ncclient to be installed on the
|
|
||||||
local Ansible controller.
|
|
||||||
version_added: "2.3"
|
|
||||||
requirements:
|
|
||||||
- ncclient
|
|
||||||
options:
|
|
||||||
host:
|
|
||||||
description:
|
|
||||||
- Specifies the remote device FQDN or IP address to establish the SSH
|
|
||||||
connection to.
|
|
||||||
default: inventory_hostname
|
|
||||||
vars:
|
|
||||||
- name: ansible_host
|
|
||||||
port:
|
|
||||||
type: int
|
|
||||||
description:
|
|
||||||
- Specifies the port on the remote device that listens for connections
|
|
||||||
when establishing the SSH connection.
|
|
||||||
default: 830
|
|
||||||
ini:
|
|
||||||
- section: defaults
|
|
||||||
key: remote_port
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_REMOTE_PORT
|
|
||||||
vars:
|
|
||||||
- name: ansible_port
|
|
||||||
network_os:
|
|
||||||
description:
|
|
||||||
- Configures the device platform network operating system. This value is
|
|
||||||
used to load a device specific netconf plugin. If this option is not
|
|
||||||
configured (or set to C(auto)), then Ansible will attempt to guess the
|
|
||||||
correct network_os to use.
|
|
||||||
If it can not guess a network_os correctly it will use C(default).
|
|
||||||
vars:
|
|
||||||
- name: ansible_network_os
|
|
||||||
remote_user:
|
|
||||||
description:
|
|
||||||
- The username used to authenticate to the remote device when the SSH
|
|
||||||
connection is first established. If the remote_user is not specified,
|
|
||||||
the connection will use the username of the logged in user.
|
|
||||||
- Can be configured from the CLI via the C(--user) or C(-u) options.
|
|
||||||
ini:
|
|
||||||
- section: defaults
|
|
||||||
key: remote_user
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_REMOTE_USER
|
|
||||||
vars:
|
|
||||||
- name: ansible_user
|
|
||||||
password:
|
|
||||||
description:
|
|
||||||
- Configures the user password used to authenticate to the remote device
|
|
||||||
when first establishing the SSH connection.
|
|
||||||
vars:
|
|
||||||
- name: ansible_password
|
|
||||||
- name: ansible_ssh_pass
|
|
||||||
- name: ansible_ssh_password
|
|
||||||
- name: ansible_netconf_password
|
|
||||||
private_key_file:
|
|
||||||
description:
|
|
||||||
- The private SSH key or certificate file used to authenticate to the
|
|
||||||
remote device when first establishing the SSH connection.
|
|
||||||
ini:
|
|
||||||
- section: defaults
|
|
||||||
key: private_key_file
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PRIVATE_KEY_FILE
|
|
||||||
vars:
|
|
||||||
- name: ansible_private_key_file
|
|
||||||
look_for_keys:
|
|
||||||
default: True
|
|
||||||
description:
|
|
||||||
- Enables looking for ssh keys in the usual locations for ssh keys (e.g. :file:`~/.ssh/id_*`).
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
|
|
||||||
ini:
|
|
||||||
- section: paramiko_connection
|
|
||||||
key: look_for_keys
|
|
||||||
type: boolean
|
|
||||||
host_key_checking:
|
|
||||||
description: 'Set this to "False" if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host'
|
|
||||||
type: boolean
|
|
||||||
default: True
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_HOST_KEY_CHECKING
|
|
||||||
- name: ANSIBLE_SSH_HOST_KEY_CHECKING
|
|
||||||
- name: ANSIBLE_NETCONF_HOST_KEY_CHECKING
|
|
||||||
ini:
|
|
||||||
- section: defaults
|
|
||||||
key: host_key_checking
|
|
||||||
- section: paramiko_connection
|
|
||||||
key: host_key_checking
|
|
||||||
vars:
|
|
||||||
- name: ansible_host_key_checking
|
|
||||||
- name: ansible_ssh_host_key_checking
|
|
||||||
- name: ansible_netconf_host_key_checking
|
|
||||||
persistent_connect_timeout:
|
|
||||||
type: int
|
|
||||||
description:
|
|
||||||
- Configures, in seconds, the amount of time to wait when trying to
|
|
||||||
initially establish a persistent connection. If this value expires
|
|
||||||
before the connection to the remote device is completed, the connection
|
|
||||||
will fail.
|
|
||||||
default: 30
|
|
||||||
ini:
|
|
||||||
- section: persistent_connection
|
|
||||||
key: connect_timeout
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PERSISTENT_CONNECT_TIMEOUT
|
|
||||||
vars:
|
|
||||||
- name: ansible_connect_timeout
|
|
||||||
persistent_command_timeout:
|
|
||||||
type: int
|
|
||||||
description:
|
|
||||||
- Configures, in seconds, the amount of time to wait for a command to
|
|
||||||
return from the remote device. If this timer is exceeded before the
|
|
||||||
command returns, the connection plugin will raise an exception and
|
|
||||||
close.
|
|
||||||
default: 30
|
|
||||||
ini:
|
|
||||||
- section: persistent_connection
|
|
||||||
key: command_timeout
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PERSISTENT_COMMAND_TIMEOUT
|
|
||||||
vars:
|
|
||||||
- name: ansible_command_timeout
|
|
||||||
netconf_ssh_config:
|
|
||||||
description:
|
|
||||||
- This variable is used to enable bastion/jump host with netconf connection. If set to
|
|
||||||
True the bastion/jump host ssh settings should be present in ~/.ssh/config file,
|
|
||||||
alternatively it can be set to custom ssh configuration file path to read the
|
|
||||||
bastion/jump host settings.
|
|
||||||
ini:
|
|
||||||
- section: netconf_connection
|
|
||||||
key: ssh_config
|
|
||||||
version_added: '2.7'
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_NETCONF_SSH_CONFIG
|
|
||||||
vars:
|
|
||||||
- name: ansible_netconf_ssh_config
|
|
||||||
version_added: '2.7'
|
|
||||||
persistent_log_messages:
|
|
||||||
type: boolean
|
|
||||||
description:
|
|
||||||
- This flag will enable logging the command executed and response received from
|
|
||||||
target device in the ansible log file. For this option to work 'log_path' ansible
|
|
||||||
configuration option is required to be set to a file path with write access.
|
|
||||||
- Be sure to fully understand the security implications of enabling this
|
|
||||||
option as it could create a security vulnerability by logging sensitive information in log file.
|
|
||||||
default: False
|
|
||||||
ini:
|
|
||||||
- section: persistent_connection
|
|
||||||
key: log_messages
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PERSISTENT_LOG_MESSAGES
|
|
||||||
vars:
|
|
||||||
- name: ansible_persistent_log_messages
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
|
|
||||||
from ansible.errors import AnsibleConnectionFailure, AnsibleError
|
|
||||||
from ansible.module_utils._text import to_bytes, to_native, to_text
|
|
||||||
from ansible.module_utils.basic import missing_required_lib
|
|
||||||
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE, BOOLEANS_FALSE
|
|
||||||
from ansible.plugins.loader import netconf_loader
|
|
||||||
from ansible.plugins.connection import NetworkConnectionBase, ensure_connect
|
|
||||||
|
|
||||||
try:
|
|
||||||
from ncclient import manager
|
|
||||||
from ncclient.operations import RPCError
|
|
||||||
from ncclient.transport.errors import SSHUnknownHostError
|
|
||||||
from ncclient.xml_ import to_ele, to_xml
|
|
||||||
HAS_NCCLIENT = True
|
|
||||||
NCCLIENT_IMP_ERR = None
|
|
||||||
except (ImportError, AttributeError) as err: # paramiko and gssapi are incompatible and raise AttributeError not ImportError
|
|
||||||
HAS_NCCLIENT = False
|
|
||||||
NCCLIENT_IMP_ERR = err
|
|
||||||
|
|
||||||
logging.getLogger('ncclient').setLevel(logging.INFO)
|
|
||||||
|
|
||||||
|
|
||||||
class Connection(NetworkConnectionBase):
|
|
||||||
"""NetConf connections"""
|
|
||||||
|
|
||||||
transport = 'netconf'
|
|
||||||
has_pipelining = False
|
|
||||||
|
|
||||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
|
||||||
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
|
||||||
|
|
||||||
# If network_os is not specified then set the network os to auto
|
|
||||||
# This will be used to trigger the use of guess_network_os when connecting.
|
|
||||||
self._network_os = self._network_os or 'auto'
|
|
||||||
|
|
||||||
self.netconf = netconf_loader.get(self._network_os, self)
|
|
||||||
if self.netconf:
|
|
||||||
self._sub_plugin = {'type': 'netconf', 'name': self.netconf._load_name, 'obj': self.netconf}
|
|
||||||
self.queue_message('vvvv', 'loaded netconf plugin %s from path %s for network_os %s' %
|
|
||||||
(self.netconf._load_name, self.netconf._original_path, self._network_os))
|
|
||||||
else:
|
|
||||||
self.netconf = netconf_loader.get("default", self)
|
|
||||||
self._sub_plugin = {'type': 'netconf', 'name': 'default', 'obj': self.netconf}
|
|
||||||
self.queue_message('display', 'unable to load netconf plugin for network_os %s, falling back to default plugin' % self._network_os)
|
|
||||||
|
|
||||||
self.queue_message('log', 'network_os is set to %s' % self._network_os)
|
|
||||||
self._manager = None
|
|
||||||
self.key_filename = None
|
|
||||||
self._ssh_config = None
|
|
||||||
|
|
||||||
def exec_command(self, cmd, in_data=None, sudoable=True):
|
|
||||||
"""Sends the request to the node and returns the reply
|
|
||||||
The method accepts two forms of request. The first form is as a byte
|
|
||||||
string that represents xml string be send over netconf session.
|
|
||||||
The second form is a json-rpc (2.0) byte string.
|
|
||||||
"""
|
|
||||||
if self._manager:
|
|
||||||
# to_ele operates on native strings
|
|
||||||
request = to_ele(to_native(cmd, errors='surrogate_or_strict'))
|
|
||||||
|
|
||||||
if request is None:
|
|
||||||
return 'unable to parse request'
|
|
||||||
|
|
||||||
try:
|
|
||||||
reply = self._manager.rpc(request)
|
|
||||||
except RPCError as exc:
|
|
||||||
error = self.internal_error(data=to_text(to_xml(exc.xml), errors='surrogate_or_strict'))
|
|
||||||
return json.dumps(error)
|
|
||||||
|
|
||||||
return reply.data_xml
|
|
||||||
else:
|
|
||||||
return super(Connection, self).exec_command(cmd, in_data, sudoable)
|
|
||||||
|
|
||||||
@property
|
|
||||||
@ensure_connect
|
|
||||||
def manager(self):
|
|
||||||
return self._manager
|
|
||||||
|
|
||||||
def _connect(self):
|
|
||||||
if not HAS_NCCLIENT:
|
|
||||||
raise AnsibleError("%s: %s" % (missing_required_lib("ncclient"), to_native(NCCLIENT_IMP_ERR)))
|
|
||||||
|
|
||||||
self.queue_message('log', 'ssh connection done, starting ncclient')
|
|
||||||
|
|
||||||
allow_agent = True
|
|
||||||
if self._play_context.password is not None:
|
|
||||||
allow_agent = False
|
|
||||||
setattr(self._play_context, 'allow_agent', allow_agent)
|
|
||||||
|
|
||||||
self.key_filename = self._play_context.private_key_file or self.get_option('private_key_file')
|
|
||||||
if self.key_filename:
|
|
||||||
self.key_filename = str(os.path.expanduser(self.key_filename))
|
|
||||||
|
|
||||||
self._ssh_config = self.get_option('netconf_ssh_config')
|
|
||||||
if self._ssh_config in BOOLEANS_TRUE:
|
|
||||||
self._ssh_config = True
|
|
||||||
elif self._ssh_config in BOOLEANS_FALSE:
|
|
||||||
self._ssh_config = None
|
|
||||||
|
|
||||||
# Try to guess the network_os if the network_os is set to auto
|
|
||||||
if self._network_os == 'auto':
|
|
||||||
for cls in netconf_loader.all(class_only=True):
|
|
||||||
network_os = cls.guess_network_os(self)
|
|
||||||
if network_os:
|
|
||||||
self.queue_message('vvv', 'discovered network_os %s' % network_os)
|
|
||||||
self._network_os = network_os
|
|
||||||
|
|
||||||
# If we have tried to detect the network_os but were unable to i.e. network_os is still 'auto'
|
|
||||||
# then use default as the network_os
|
|
||||||
|
|
||||||
if self._network_os == 'auto':
|
|
||||||
# Network os not discovered. Set it to default
|
|
||||||
self.queue_message('vvv', 'Unable to discover network_os. Falling back to default.')
|
|
||||||
self._network_os = 'default'
|
|
||||||
try:
|
|
||||||
ncclient_device_handler = self.netconf.get_option('ncclient_device_handler')
|
|
||||||
except KeyError:
|
|
||||||
ncclient_device_handler = 'default'
|
|
||||||
self.queue_message('vvv', 'identified ncclient device handler: %s.' % ncclient_device_handler)
|
|
||||||
device_params = {'name': ncclient_device_handler}
|
|
||||||
|
|
||||||
try:
|
|
||||||
port = self._play_context.port or 830
|
|
||||||
self.queue_message('vvv', "ESTABLISH NETCONF SSH CONNECTION FOR USER: %s on PORT %s TO %s WITH SSH_CONFIG = %s" %
|
|
||||||
(self._play_context.remote_user, port, self._play_context.remote_addr, self._ssh_config))
|
|
||||||
self._manager = manager.connect(
|
|
||||||
host=self._play_context.remote_addr,
|
|
||||||
port=port,
|
|
||||||
username=self._play_context.remote_user,
|
|
||||||
password=self._play_context.password,
|
|
||||||
key_filename=self.key_filename,
|
|
||||||
hostkey_verify=self.get_option('host_key_checking'),
|
|
||||||
look_for_keys=self.get_option('look_for_keys'),
|
|
||||||
device_params=device_params,
|
|
||||||
allow_agent=self._play_context.allow_agent,
|
|
||||||
timeout=self.get_option('persistent_connect_timeout'),
|
|
||||||
ssh_config=self._ssh_config
|
|
||||||
)
|
|
||||||
|
|
||||||
self._manager._timeout = self.get_option('persistent_command_timeout')
|
|
||||||
except SSHUnknownHostError as exc:
|
|
||||||
raise AnsibleConnectionFailure(to_native(exc))
|
|
||||||
except ImportError:
|
|
||||||
raise AnsibleError("connection=netconf is not supported on {0}".format(self._network_os))
|
|
||||||
|
|
||||||
if not self._manager.connected:
|
|
||||||
return 1, b'', b'not connected'
|
|
||||||
|
|
||||||
self.queue_message('log', 'ncclient manager object created successfully')
|
|
||||||
|
|
||||||
self._connected = True
|
|
||||||
|
|
||||||
super(Connection, self)._connect()
|
|
||||||
|
|
||||||
return 0, to_bytes(self._manager.session_id, errors='surrogate_or_strict'), b''
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if self._manager:
|
|
||||||
self._manager.close_session()
|
|
||||||
super(Connection, self).close()
|
|
@ -1,759 +0,0 @@
|
|||||||
# (c) 2016 Red Hat Inc.
|
|
||||||
# (c) 2017 Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
author: Ansible Networking Team
|
|
||||||
connection: network_cli
|
|
||||||
short_description: Use network_cli to run command on network appliances
|
|
||||||
description:
|
|
||||||
- This connection plugin provides a connection to remote devices over the
|
|
||||||
SSH and implements a CLI shell. This connection plugin is typically used by
|
|
||||||
network devices for sending and receiving CLi commands to network devices.
|
|
||||||
version_added: "2.3"
|
|
||||||
options:
|
|
||||||
host:
|
|
||||||
description:
|
|
||||||
- Specifies the remote device FQDN or IP address to establish the SSH
|
|
||||||
connection to.
|
|
||||||
default: inventory_hostname
|
|
||||||
vars:
|
|
||||||
- name: ansible_host
|
|
||||||
port:
|
|
||||||
type: int
|
|
||||||
description:
|
|
||||||
- Specifies the port on the remote device that listens for connections
|
|
||||||
when establishing the SSH connection.
|
|
||||||
default: 22
|
|
||||||
ini:
|
|
||||||
- section: defaults
|
|
||||||
key: remote_port
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_REMOTE_PORT
|
|
||||||
vars:
|
|
||||||
- name: ansible_port
|
|
||||||
network_os:
|
|
||||||
description:
|
|
||||||
- Configures the device platform network operating system. This value is
|
|
||||||
used to load the correct terminal and cliconf plugins to communicate
|
|
||||||
with the remote device.
|
|
||||||
vars:
|
|
||||||
- name: ansible_network_os
|
|
||||||
remote_user:
|
|
||||||
description:
|
|
||||||
- The username used to authenticate to the remote device when the SSH
|
|
||||||
connection is first established. If the remote_user is not specified,
|
|
||||||
the connection will use the username of the logged in user.
|
|
||||||
- Can be configured from the CLI via the C(--user) or C(-u) options.
|
|
||||||
ini:
|
|
||||||
- section: defaults
|
|
||||||
key: remote_user
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_REMOTE_USER
|
|
||||||
vars:
|
|
||||||
- name: ansible_user
|
|
||||||
password:
|
|
||||||
description:
|
|
||||||
- Configures the user password used to authenticate to the remote device
|
|
||||||
when first establishing the SSH connection.
|
|
||||||
vars:
|
|
||||||
- name: ansible_password
|
|
||||||
- name: ansible_ssh_pass
|
|
||||||
- name: ansible_ssh_password
|
|
||||||
private_key_file:
|
|
||||||
description:
|
|
||||||
- The private SSH key or certificate file used to authenticate to the
|
|
||||||
remote device when first establishing the SSH connection.
|
|
||||||
ini:
|
|
||||||
- section: defaults
|
|
||||||
key: private_key_file
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PRIVATE_KEY_FILE
|
|
||||||
vars:
|
|
||||||
- name: ansible_private_key_file
|
|
||||||
become:
|
|
||||||
type: boolean
|
|
||||||
description:
|
|
||||||
- The become option will instruct the CLI session to attempt privilege
|
|
||||||
escalation on platforms that support it. Normally this means
|
|
||||||
transitioning from user mode to C(enable) mode in the CLI session.
|
|
||||||
If become is set to True and the remote device does not support
|
|
||||||
privilege escalation or the privilege has already been elevated, then
|
|
||||||
this option is silently ignored.
|
|
||||||
- Can be configured from the CLI via the C(--become) or C(-b) options.
|
|
||||||
default: False
|
|
||||||
ini:
|
|
||||||
- section: privilege_escalation
|
|
||||||
key: become
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_BECOME
|
|
||||||
vars:
|
|
||||||
- name: ansible_become
|
|
||||||
become_method:
|
|
||||||
description:
|
|
||||||
- This option allows the become method to be specified in for handling
|
|
||||||
privilege escalation. Typically the become_method value is set to
|
|
||||||
C(enable) but could be defined as other values.
|
|
||||||
default: sudo
|
|
||||||
ini:
|
|
||||||
- section: privilege_escalation
|
|
||||||
key: become_method
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_BECOME_METHOD
|
|
||||||
vars:
|
|
||||||
- name: ansible_become_method
|
|
||||||
host_key_auto_add:
|
|
||||||
type: boolean
|
|
||||||
description:
|
|
||||||
- By default, Ansible will prompt the user before adding SSH keys to the
|
|
||||||
known hosts file. Since persistent connections such as network_cli run
|
|
||||||
in background processes, the user will never be prompted. By enabling
|
|
||||||
this option, unknown host keys will automatically be added to the
|
|
||||||
known hosts file.
|
|
||||||
- Be sure to fully understand the security implications of enabling this
|
|
||||||
option on production systems as it could create a security vulnerability.
|
|
||||||
default: False
|
|
||||||
ini:
|
|
||||||
- section: paramiko_connection
|
|
||||||
key: host_key_auto_add
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_HOST_KEY_AUTO_ADD
|
|
||||||
persistent_connect_timeout:
|
|
||||||
type: int
|
|
||||||
description:
|
|
||||||
- Configures, in seconds, the amount of time to wait when trying to
|
|
||||||
initially establish a persistent connection. If this value expires
|
|
||||||
before the connection to the remote device is completed, the connection
|
|
||||||
will fail.
|
|
||||||
default: 30
|
|
||||||
ini:
|
|
||||||
- section: persistent_connection
|
|
||||||
key: connect_timeout
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PERSISTENT_CONNECT_TIMEOUT
|
|
||||||
vars:
|
|
||||||
- name: ansible_connect_timeout
|
|
||||||
persistent_command_timeout:
|
|
||||||
type: int
|
|
||||||
description:
|
|
||||||
- Configures, in seconds, the amount of time to wait for a command to
|
|
||||||
return from the remote device. If this timer is exceeded before the
|
|
||||||
command returns, the connection plugin will raise an exception and
|
|
||||||
close.
|
|
||||||
default: 30
|
|
||||||
ini:
|
|
||||||
- section: persistent_connection
|
|
||||||
key: command_timeout
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PERSISTENT_COMMAND_TIMEOUT
|
|
||||||
vars:
|
|
||||||
- name: ansible_command_timeout
|
|
||||||
persistent_buffer_read_timeout:
|
|
||||||
type: float
|
|
||||||
description:
|
|
||||||
- Configures, in seconds, the amount of time to wait for the data to be read
|
|
||||||
from Paramiko channel after the command prompt is matched. This timeout
|
|
||||||
value ensures that command prompt matched is correct and there is no more data
|
|
||||||
left to be received from remote host.
|
|
||||||
default: 0.1
|
|
||||||
ini:
|
|
||||||
- section: persistent_connection
|
|
||||||
key: buffer_read_timeout
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PERSISTENT_BUFFER_READ_TIMEOUT
|
|
||||||
vars:
|
|
||||||
- name: ansible_buffer_read_timeout
|
|
||||||
persistent_log_messages:
|
|
||||||
type: boolean
|
|
||||||
description:
|
|
||||||
- This flag will enable logging the command executed and response received from
|
|
||||||
target device in the ansible log file. For this option to work 'log_path' ansible
|
|
||||||
configuration option is required to be set to a file path with write access.
|
|
||||||
- Be sure to fully understand the security implications of enabling this
|
|
||||||
option as it could create a security vulnerability by logging sensitive information in log file.
|
|
||||||
default: False
|
|
||||||
ini:
|
|
||||||
- section: persistent_connection
|
|
||||||
key: log_messages
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PERSISTENT_LOG_MESSAGES
|
|
||||||
vars:
|
|
||||||
- name: ansible_persistent_log_messages
|
|
||||||
terminal_stdout_re:
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
version_added: '2.9'
|
|
||||||
description:
|
|
||||||
- A single regex pattern or a sequence of patterns along with optional flags
|
|
||||||
to match the command prompt from the received response chunk. This option
|
|
||||||
accepts C(pattern) and C(flags) keys. The value of C(pattern) is a python
|
|
||||||
regex pattern to match the response and the value of C(flags) is the value
|
|
||||||
accepted by I(flags) argument of I(re.compile) python method to control
|
|
||||||
the way regex is matched with the response, for example I('re.I').
|
|
||||||
vars:
|
|
||||||
- name: ansible_terminal_stdout_re
|
|
||||||
terminal_stderr_re:
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
version_added: '2.9'
|
|
||||||
description:
|
|
||||||
- This option provides the regex pattern and optional flags to match the
|
|
||||||
error string from the received response chunk. This option
|
|
||||||
accepts C(pattern) and C(flags) keys. The value of C(pattern) is a python
|
|
||||||
regex pattern to match the response and the value of C(flags) is the value
|
|
||||||
accepted by I(flags) argument of I(re.compile) python method to control
|
|
||||||
the way regex is matched with the response, for example I('re.I').
|
|
||||||
vars:
|
|
||||||
- name: ansible_terminal_stderr_re
|
|
||||||
terminal_initial_prompt:
|
|
||||||
type: list
|
|
||||||
version_added: '2.9'
|
|
||||||
description:
|
|
||||||
- A single regex pattern or a sequence of patterns to evaluate the expected
|
|
||||||
prompt at the time of initial login to the remote host.
|
|
||||||
vars:
|
|
||||||
- name: ansible_terminal_initial_prompt
|
|
||||||
terminal_initial_answer:
|
|
||||||
type: list
|
|
||||||
version_added: '2.9'
|
|
||||||
description:
|
|
||||||
- The answer to reply with if the C(terminal_initial_prompt) is matched. The value can be a single answer
|
|
||||||
or a list of answers for multiple terminal_initial_prompt. In case the login menu has
|
|
||||||
multiple prompts the sequence of the prompt and excepted answer should be in same order and the value
|
|
||||||
of I(terminal_prompt_checkall) should be set to I(True) if all the values in C(terminal_initial_prompt) are
|
|
||||||
expected to be matched and set to I(False) if any one login prompt is to be matched.
|
|
||||||
vars:
|
|
||||||
- name: ansible_terminal_initial_answer
|
|
||||||
terminal_initial_prompt_checkall:
|
|
||||||
type: boolean
|
|
||||||
version_added: '2.9'
|
|
||||||
description:
|
|
||||||
- By default the value is set to I(False) and any one of the prompts mentioned in C(terminal_initial_prompt)
|
|
||||||
option is matched it won't check for other prompts. When set to I(True) it will check for all the prompts
|
|
||||||
mentioned in C(terminal_initial_prompt) option in the given order and all the prompts
|
|
||||||
should be received from remote host if not it will result in timeout.
|
|
||||||
default: False
|
|
||||||
vars:
|
|
||||||
- name: ansible_terminal_initial_prompt_checkall
|
|
||||||
terminal_inital_prompt_newline:
|
|
||||||
type: boolean
|
|
||||||
version_added: '2.9'
|
|
||||||
description:
|
|
||||||
- This boolean flag, that when set to I(True) will send newline in the response if any of values
|
|
||||||
in I(terminal_initial_prompt) is matched.
|
|
||||||
default: True
|
|
||||||
vars:
|
|
||||||
- name: ansible_terminal_initial_prompt_newline
|
|
||||||
network_cli_retries:
|
|
||||||
description:
|
|
||||||
- Number of attempts to connect to remote host. The delay time between the retires increases after
|
|
||||||
every attempt by power of 2 in seconds till either the maximum attempts are exhausted or any of the
|
|
||||||
C(persistent_command_timeout) or C(persistent_connect_timeout) timers are triggered.
|
|
||||||
default: 3
|
|
||||||
version_added: '2.9'
|
|
||||||
type: integer
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_NETWORK_CLI_RETRIES
|
|
||||||
ini:
|
|
||||||
- section: persistent_connection
|
|
||||||
key: network_cli_retries
|
|
||||||
vars:
|
|
||||||
- name: ansible_network_cli_retries
|
|
||||||
"""
|
|
||||||
|
|
||||||
from functools import wraps
|
|
||||||
import getpass
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
import signal
|
|
||||||
import socket
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
from ansible.errors import AnsibleConnectionFailure
|
|
||||||
from ansible.module_utils.six import PY3
|
|
||||||
from ansible.module_utils.six.moves import cPickle
|
|
||||||
from ansible.module_utils.network.common.utils import to_list
|
|
||||||
from ansible.module_utils._text import to_bytes, to_text
|
|
||||||
from ansible.playbook.play_context import PlayContext
|
|
||||||
from ansible.plugins.connection import NetworkConnectionBase
|
|
||||||
from ansible.plugins.loader import cliconf_loader, terminal_loader, connection_loader
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_connect(func):
|
|
||||||
@wraps(func)
|
|
||||||
def wrapped(self, *args, **kwargs):
|
|
||||||
if not self._connected:
|
|
||||||
self._connect()
|
|
||||||
self.update_cli_prompt_context()
|
|
||||||
return func(self, *args, **kwargs)
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
class AnsibleCmdRespRecv(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Connection(NetworkConnectionBase):
|
|
||||||
''' CLI (shell) SSH connections on Paramiko '''
|
|
||||||
|
|
||||||
transport = 'network_cli'
|
|
||||||
has_pipelining = True
|
|
||||||
|
|
||||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
|
||||||
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
|
||||||
self._ssh_shell = None
|
|
||||||
|
|
||||||
self._matched_prompt = None
|
|
||||||
self._matched_cmd_prompt = None
|
|
||||||
self._matched_pattern = None
|
|
||||||
self._last_response = None
|
|
||||||
self._history = list()
|
|
||||||
self._command_response = None
|
|
||||||
self._last_recv_window = None
|
|
||||||
|
|
||||||
self._terminal = None
|
|
||||||
self.cliconf = None
|
|
||||||
self._paramiko_conn = None
|
|
||||||
|
|
||||||
# Managing prompt context
|
|
||||||
self._check_prompt = False
|
|
||||||
self._task_uuid = to_text(kwargs.get('task_uuid', ''))
|
|
||||||
|
|
||||||
if self._play_context.verbosity > 3:
|
|
||||||
logging.getLogger('paramiko').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
if self._network_os:
|
|
||||||
self._terminal = terminal_loader.get(self._network_os, self)
|
|
||||||
if not self._terminal:
|
|
||||||
raise AnsibleConnectionFailure('network os %s is not supported' % self._network_os)
|
|
||||||
|
|
||||||
self.cliconf = cliconf_loader.get(self._network_os, self)
|
|
||||||
if self.cliconf:
|
|
||||||
self._sub_plugin = {'type': 'cliconf', 'name': self.cliconf._load_name, 'obj': self.cliconf}
|
|
||||||
self.queue_message('vvvv', 'loaded cliconf plugin %s from path %s for network_os %s' %
|
|
||||||
(self.cliconf._load_name, self.cliconf._original_path, self._network_os))
|
|
||||||
else:
|
|
||||||
self.queue_message('vvvv', 'unable to load cliconf for network_os %s' % self._network_os)
|
|
||||||
else:
|
|
||||||
raise AnsibleConnectionFailure(
|
|
||||||
'Unable to automatically determine host network os. Please '
|
|
||||||
'manually configure ansible_network_os value for this host'
|
|
||||||
)
|
|
||||||
self.queue_message('log', 'network_os is set to %s' % self._network_os)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def paramiko_conn(self):
|
|
||||||
if self._paramiko_conn is None:
|
|
||||||
self._paramiko_conn = connection_loader.get('paramiko', self._play_context, '/dev/null')
|
|
||||||
self._paramiko_conn.set_options(direct={'look_for_keys': not bool(self._play_context.password and not self._play_context.private_key_file)})
|
|
||||||
return self._paramiko_conn
|
|
||||||
|
|
||||||
def _get_log_channel(self):
|
|
||||||
name = "p=%s u=%s | " % (os.getpid(), getpass.getuser())
|
|
||||||
name += "paramiko [%s]" % self._play_context.remote_addr
|
|
||||||
return name
|
|
||||||
|
|
||||||
@ensure_connect
|
|
||||||
def get_prompt(self):
|
|
||||||
"""Returns the current prompt from the device"""
|
|
||||||
return self._matched_prompt
|
|
||||||
|
|
||||||
def exec_command(self, cmd, in_data=None, sudoable=True):
|
|
||||||
# this try..except block is just to handle the transition to supporting
|
|
||||||
# network_cli as a toplevel connection. Once connection=local is gone,
|
|
||||||
# this block can be removed as well and all calls passed directly to
|
|
||||||
# the local connection
|
|
||||||
if self._ssh_shell:
|
|
||||||
try:
|
|
||||||
cmd = json.loads(to_text(cmd, errors='surrogate_or_strict'))
|
|
||||||
kwargs = {'command': to_bytes(cmd['command'], errors='surrogate_or_strict')}
|
|
||||||
for key in ('prompt', 'answer', 'sendonly', 'newline', 'prompt_retry_check'):
|
|
||||||
if cmd.get(key) is True or cmd.get(key) is False:
|
|
||||||
kwargs[key] = cmd[key]
|
|
||||||
elif cmd.get(key) is not None:
|
|
||||||
kwargs[key] = to_bytes(cmd[key], errors='surrogate_or_strict')
|
|
||||||
return self.send(**kwargs)
|
|
||||||
except ValueError:
|
|
||||||
cmd = to_bytes(cmd, errors='surrogate_or_strict')
|
|
||||||
return self.send(command=cmd)
|
|
||||||
|
|
||||||
else:
|
|
||||||
return super(Connection, self).exec_command(cmd, in_data, sudoable)
|
|
||||||
|
|
||||||
def update_play_context(self, pc_data):
|
|
||||||
"""Updates the play context information for the connection"""
|
|
||||||
pc_data = to_bytes(pc_data)
|
|
||||||
if PY3:
|
|
||||||
pc_data = cPickle.loads(pc_data, encoding='bytes')
|
|
||||||
else:
|
|
||||||
pc_data = cPickle.loads(pc_data)
|
|
||||||
play_context = PlayContext()
|
|
||||||
play_context.deserialize(pc_data)
|
|
||||||
|
|
||||||
self.queue_message('vvvv', 'updating play_context for connection')
|
|
||||||
if self._play_context.become ^ play_context.become:
|
|
||||||
if play_context.become is True:
|
|
||||||
auth_pass = play_context.become_pass
|
|
||||||
self._terminal.on_become(passwd=auth_pass)
|
|
||||||
self.queue_message('vvvv', 'authorizing connection')
|
|
||||||
else:
|
|
||||||
self._terminal.on_unbecome()
|
|
||||||
self.queue_message('vvvv', 'deauthorizing connection')
|
|
||||||
|
|
||||||
self._play_context = play_context
|
|
||||||
|
|
||||||
if hasattr(self, 'reset_history'):
|
|
||||||
self.reset_history()
|
|
||||||
if hasattr(self, 'disable_response_logging'):
|
|
||||||
self.disable_response_logging()
|
|
||||||
|
|
||||||
def set_check_prompt(self, task_uuid):
|
|
||||||
self._check_prompt = task_uuid
|
|
||||||
|
|
||||||
def update_cli_prompt_context(self):
|
|
||||||
# set cli prompt context at the start of new task run only
|
|
||||||
if self._check_prompt and self._task_uuid != self._check_prompt:
|
|
||||||
self._task_uuid, self._check_prompt = self._check_prompt, False
|
|
||||||
self.set_cli_prompt_context()
|
|
||||||
|
|
||||||
def _connect(self):
|
|
||||||
'''
|
|
||||||
Connects to the remote device and starts the terminal
|
|
||||||
'''
|
|
||||||
if not self.connected:
|
|
||||||
self.paramiko_conn._set_log_channel(self._get_log_channel())
|
|
||||||
self.paramiko_conn.force_persistence = self.force_persistence
|
|
||||||
|
|
||||||
command_timeout = self.get_option('persistent_command_timeout')
|
|
||||||
max_pause = min([self.get_option('persistent_connect_timeout'), command_timeout])
|
|
||||||
retries = self.get_option('network_cli_retries')
|
|
||||||
total_pause = 0
|
|
||||||
|
|
||||||
for attempt in range(retries + 1):
|
|
||||||
try:
|
|
||||||
ssh = self.paramiko_conn._connect()
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
pause = 2 ** (attempt + 1)
|
|
||||||
if attempt == retries or total_pause >= max_pause:
|
|
||||||
raise AnsibleConnectionFailure(to_text(e, errors='surrogate_or_strict'))
|
|
||||||
else:
|
|
||||||
msg = (u"network_cli_retry: attempt: %d, caught exception(%s), "
|
|
||||||
u"pausing for %d seconds" % (attempt + 1, to_text(e, errors='surrogate_or_strict'), pause))
|
|
||||||
|
|
||||||
self.queue_message('vv', msg)
|
|
||||||
time.sleep(pause)
|
|
||||||
total_pause += pause
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.queue_message('vvvv', 'ssh connection done, setting terminal')
|
|
||||||
self._connected = True
|
|
||||||
|
|
||||||
self._ssh_shell = ssh.ssh.invoke_shell()
|
|
||||||
self._ssh_shell.settimeout(command_timeout)
|
|
||||||
|
|
||||||
self.queue_message('vvvv', 'loaded terminal plugin for network_os %s' % self._network_os)
|
|
||||||
|
|
||||||
terminal_initial_prompt = self.get_option('terminal_initial_prompt') or self._terminal.terminal_initial_prompt
|
|
||||||
terminal_initial_answer = self.get_option('terminal_initial_answer') or self._terminal.terminal_initial_answer
|
|
||||||
newline = self.get_option('terminal_inital_prompt_newline') or self._terminal.terminal_inital_prompt_newline
|
|
||||||
check_all = self.get_option('terminal_initial_prompt_checkall') or False
|
|
||||||
|
|
||||||
self.receive(prompts=terminal_initial_prompt, answer=terminal_initial_answer, newline=newline, check_all=check_all)
|
|
||||||
|
|
||||||
if self._play_context.become:
|
|
||||||
self.queue_message('vvvv', 'firing event: on_become')
|
|
||||||
auth_pass = self._play_context.become_pass
|
|
||||||
self._terminal.on_become(passwd=auth_pass)
|
|
||||||
|
|
||||||
self.queue_message('vvvv', 'firing event: on_open_shell()')
|
|
||||||
self._terminal.on_open_shell()
|
|
||||||
|
|
||||||
self.queue_message('vvvv', 'ssh connection has completed successfully')
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
'''
|
|
||||||
Close the active connection to the device
|
|
||||||
'''
|
|
||||||
# only close the connection if its connected.
|
|
||||||
if self._connected:
|
|
||||||
self.queue_message('debug', "closing ssh connection to device")
|
|
||||||
if self._ssh_shell:
|
|
||||||
self.queue_message('debug', "firing event: on_close_shell()")
|
|
||||||
self._terminal.on_close_shell()
|
|
||||||
self._ssh_shell.close()
|
|
||||||
self._ssh_shell = None
|
|
||||||
self.queue_message('debug', "cli session is now closed")
|
|
||||||
|
|
||||||
self.paramiko_conn.close()
|
|
||||||
self._paramiko_conn = None
|
|
||||||
self.queue_message('debug', "ssh connection has been closed successfully")
|
|
||||||
super(Connection, self).close()
|
|
||||||
|
|
||||||
def receive(self, command=None, prompts=None, answer=None, newline=True, prompt_retry_check=False, check_all=False):
|
|
||||||
'''
|
|
||||||
Handles receiving of output from command
|
|
||||||
'''
|
|
||||||
self._matched_prompt = None
|
|
||||||
self._matched_cmd_prompt = None
|
|
||||||
recv = BytesIO()
|
|
||||||
handled = False
|
|
||||||
command_prompt_matched = False
|
|
||||||
matched_prompt_window = window_count = 0
|
|
||||||
|
|
||||||
# set terminal regex values for command prompt and errors in response
|
|
||||||
self._terminal_stderr_re = self._get_terminal_std_re('terminal_stderr_re')
|
|
||||||
self._terminal_stdout_re = self._get_terminal_std_re('terminal_stdout_re')
|
|
||||||
|
|
||||||
cache_socket_timeout = self._ssh_shell.gettimeout()
|
|
||||||
command_timeout = self.get_option('persistent_command_timeout')
|
|
||||||
self._validate_timeout_value(command_timeout, "persistent_command_timeout")
|
|
||||||
if cache_socket_timeout != command_timeout:
|
|
||||||
self._ssh_shell.settimeout(command_timeout)
|
|
||||||
|
|
||||||
buffer_read_timeout = self.get_option('persistent_buffer_read_timeout')
|
|
||||||
self._validate_timeout_value(buffer_read_timeout, "persistent_buffer_read_timeout")
|
|
||||||
|
|
||||||
self._log_messages("command: %s" % command)
|
|
||||||
while True:
|
|
||||||
if command_prompt_matched:
|
|
||||||
try:
|
|
||||||
signal.signal(signal.SIGALRM, self._handle_buffer_read_timeout)
|
|
||||||
signal.setitimer(signal.ITIMER_REAL, buffer_read_timeout)
|
|
||||||
data = self._ssh_shell.recv(256)
|
|
||||||
signal.alarm(0)
|
|
||||||
self._log_messages("response-%s: %s" % (window_count + 1, data))
|
|
||||||
# if data is still received on channel it indicates the prompt string
|
|
||||||
# is wrongly matched in between response chunks, continue to read
|
|
||||||
# remaining response.
|
|
||||||
command_prompt_matched = False
|
|
||||||
|
|
||||||
# restart command_timeout timer
|
|
||||||
signal.signal(signal.SIGALRM, self._handle_command_timeout)
|
|
||||||
signal.alarm(command_timeout)
|
|
||||||
|
|
||||||
except AnsibleCmdRespRecv:
|
|
||||||
# reset socket timeout to global timeout
|
|
||||||
self._ssh_shell.settimeout(cache_socket_timeout)
|
|
||||||
return self._command_response
|
|
||||||
else:
|
|
||||||
data = self._ssh_shell.recv(256)
|
|
||||||
self._log_messages("response-%s: %s" % (window_count + 1, data))
|
|
||||||
# when a channel stream is closed, received data will be empty
|
|
||||||
if not data:
|
|
||||||
break
|
|
||||||
|
|
||||||
recv.write(data)
|
|
||||||
offset = recv.tell() - 256 if recv.tell() > 256 else 0
|
|
||||||
recv.seek(offset)
|
|
||||||
|
|
||||||
window = self._strip(recv.read())
|
|
||||||
self._last_recv_window = window
|
|
||||||
window_count += 1
|
|
||||||
|
|
||||||
if prompts and not handled:
|
|
||||||
handled = self._handle_prompt(window, prompts, answer, newline, False, check_all)
|
|
||||||
matched_prompt_window = window_count
|
|
||||||
elif prompts and handled and prompt_retry_check and matched_prompt_window + 1 == window_count:
|
|
||||||
# check again even when handled, if same prompt repeats in next window
|
|
||||||
# (like in the case of a wrong enable password, etc) indicates
|
|
||||||
# value of answer is wrong, report this as error.
|
|
||||||
if self._handle_prompt(window, prompts, answer, newline, prompt_retry_check, check_all):
|
|
||||||
raise AnsibleConnectionFailure("For matched prompt '%s', answer is not valid" % self._matched_cmd_prompt)
|
|
||||||
|
|
||||||
if self._find_prompt(window):
|
|
||||||
self._last_response = recv.getvalue()
|
|
||||||
resp = self._strip(self._last_response)
|
|
||||||
self._command_response = self._sanitize(resp, command)
|
|
||||||
if buffer_read_timeout == 0.0:
|
|
||||||
# reset socket timeout to global timeout
|
|
||||||
self._ssh_shell.settimeout(cache_socket_timeout)
|
|
||||||
return self._command_response
|
|
||||||
else:
|
|
||||||
command_prompt_matched = True
|
|
||||||
|
|
||||||
@ensure_connect
|
|
||||||
def send(self, command, prompt=None, answer=None, newline=True, sendonly=False, prompt_retry_check=False, check_all=False):
|
|
||||||
'''
|
|
||||||
Sends the command to the device in the opened shell
|
|
||||||
'''
|
|
||||||
if check_all:
|
|
||||||
prompt_len = len(to_list(prompt))
|
|
||||||
answer_len = len(to_list(answer))
|
|
||||||
if prompt_len != answer_len:
|
|
||||||
raise AnsibleConnectionFailure("Number of prompts (%s) is not same as that of answers (%s)" % (prompt_len, answer_len))
|
|
||||||
try:
|
|
||||||
cmd = b'%s\r' % command
|
|
||||||
self._history.append(cmd)
|
|
||||||
self._ssh_shell.sendall(cmd)
|
|
||||||
self._log_messages('send command: %s' % cmd)
|
|
||||||
if sendonly:
|
|
||||||
return
|
|
||||||
response = self.receive(command, prompt, answer, newline, prompt_retry_check, check_all)
|
|
||||||
return to_text(response, errors='surrogate_then_replace')
|
|
||||||
except (socket.timeout, AttributeError):
|
|
||||||
self.queue_message('error', traceback.format_exc())
|
|
||||||
raise AnsibleConnectionFailure("timeout value %s seconds reached while trying to send command: %s"
|
|
||||||
% (self._ssh_shell.gettimeout(), command.strip()))
|
|
||||||
|
|
||||||
def _handle_buffer_read_timeout(self, signum, frame):
|
|
||||||
self.queue_message('vvvv', "Response received, triggered 'persistent_buffer_read_timeout' timer of %s seconds" %
|
|
||||||
self.get_option('persistent_buffer_read_timeout'))
|
|
||||||
raise AnsibleCmdRespRecv()
|
|
||||||
|
|
||||||
def _handle_command_timeout(self, signum, frame):
|
|
||||||
msg = 'command timeout triggered, timeout value is %s secs.\nSee the timeout setting options in the Network Debug and Troubleshooting Guide.'\
|
|
||||||
% self.get_option('persistent_command_timeout')
|
|
||||||
self.queue_message('log', msg)
|
|
||||||
raise AnsibleConnectionFailure(msg)
|
|
||||||
|
|
||||||
def _strip(self, data):
|
|
||||||
'''
|
|
||||||
Removes ANSI codes from device response
|
|
||||||
'''
|
|
||||||
for regex in self._terminal.ansi_re:
|
|
||||||
data = regex.sub(b'', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _handle_prompt(self, resp, prompts, answer, newline, prompt_retry_check=False, check_all=False):
|
|
||||||
'''
|
|
||||||
Matches the command prompt and responds
|
|
||||||
|
|
||||||
:arg resp: Byte string containing the raw response from the remote
|
|
||||||
:arg prompts: Sequence of byte strings that we consider prompts for input
|
|
||||||
:arg answer: Sequence of Byte string to send back to the remote if we find a prompt.
|
|
||||||
A carriage return is automatically appended to this string.
|
|
||||||
:param prompt_retry_check: Bool value for trying to detect more prompts
|
|
||||||
:param check_all: Bool value to indicate if all the values in prompt sequence should be matched or any one of
|
|
||||||
given prompt.
|
|
||||||
:returns: True if a prompt was found in ``resp``. If check_all is True
|
|
||||||
will True only after all the prompt in the prompts list are matched. False otherwise.
|
|
||||||
'''
|
|
||||||
single_prompt = False
|
|
||||||
if not isinstance(prompts, list):
|
|
||||||
prompts = [prompts]
|
|
||||||
single_prompt = True
|
|
||||||
if not isinstance(answer, list):
|
|
||||||
answer = [answer]
|
|
||||||
prompts_regex = [re.compile(to_bytes(r), re.I) for r in prompts]
|
|
||||||
for index, regex in enumerate(prompts_regex):
|
|
||||||
match = regex.search(resp)
|
|
||||||
if match:
|
|
||||||
self._matched_cmd_prompt = match.group()
|
|
||||||
self._log_messages("matched command prompt: %s" % self._matched_cmd_prompt)
|
|
||||||
|
|
||||||
# if prompt_retry_check is enabled to check if same prompt is
|
|
||||||
# repeated don't send answer again.
|
|
||||||
if not prompt_retry_check:
|
|
||||||
prompt_answer = answer[index] if len(answer) > index else answer[0]
|
|
||||||
self._ssh_shell.sendall(b'%s' % prompt_answer)
|
|
||||||
if newline:
|
|
||||||
self._ssh_shell.sendall(b'\r')
|
|
||||||
prompt_answer += b'\r'
|
|
||||||
self._log_messages("matched command prompt answer: %s" % prompt_answer)
|
|
||||||
if check_all and prompts and not single_prompt:
|
|
||||||
prompts.pop(0)
|
|
||||||
answer.pop(0)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _sanitize(self, resp, command=None):
|
|
||||||
'''
|
|
||||||
Removes elements from the response before returning to the caller
|
|
||||||
'''
|
|
||||||
cleaned = []
|
|
||||||
for line in resp.splitlines():
|
|
||||||
if command and line.strip() == command.strip():
|
|
||||||
continue
|
|
||||||
|
|
||||||
for prompt in self._matched_prompt.strip().splitlines():
|
|
||||||
if prompt.strip() in line:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
cleaned.append(line)
|
|
||||||
return b'\n'.join(cleaned).strip()
|
|
||||||
|
|
||||||
def _find_prompt(self, response):
|
|
||||||
'''Searches the buffered response for a matching command prompt
|
|
||||||
'''
|
|
||||||
errored_response = None
|
|
||||||
is_error_message = False
|
|
||||||
|
|
||||||
for regex in self._terminal_stderr_re:
|
|
||||||
if regex.search(response):
|
|
||||||
is_error_message = True
|
|
||||||
|
|
||||||
# Check if error response ends with command prompt if not
|
|
||||||
# receive it buffered prompt
|
|
||||||
for regex in self._terminal_stdout_re:
|
|
||||||
match = regex.search(response)
|
|
||||||
if match:
|
|
||||||
errored_response = response
|
|
||||||
self._matched_pattern = regex.pattern
|
|
||||||
self._matched_prompt = match.group()
|
|
||||||
self._log_messages("matched error regex '%s' from response '%s'" % (self._matched_pattern, errored_response))
|
|
||||||
break
|
|
||||||
|
|
||||||
if not is_error_message:
|
|
||||||
for regex in self._terminal_stdout_re:
|
|
||||||
match = regex.search(response)
|
|
||||||
if match:
|
|
||||||
self._matched_pattern = regex.pattern
|
|
||||||
self._matched_prompt = match.group()
|
|
||||||
self._log_messages("matched cli prompt '%s' with regex '%s' from response '%s'" % (self._matched_prompt, self._matched_pattern, response))
|
|
||||||
if not errored_response:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if errored_response:
|
|
||||||
raise AnsibleConnectionFailure(errored_response)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _validate_timeout_value(self, timeout, timer_name):
|
|
||||||
if timeout < 0:
|
|
||||||
raise AnsibleConnectionFailure("'%s' timer value '%s' is invalid, value should be greater than or equal to zero." % (timer_name, timeout))
|
|
||||||
|
|
||||||
def transport_test(self, connect_timeout):
|
|
||||||
"""This method enables wait_for_connection to work.
|
|
||||||
|
|
||||||
As it is used by wait_for_connection, it is called by that module's action plugin,
|
|
||||||
which is on the controller process, which means that nothing done on this instance
|
|
||||||
should impact the actual persistent connection... this check is for informational
|
|
||||||
purposes only and should be properly cleaned up.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Force a fresh connect if for some reason we have connected before.
|
|
||||||
self.close()
|
|
||||||
self._connect()
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def _get_terminal_std_re(self, option):
|
|
||||||
terminal_std_option = self.get_option(option)
|
|
||||||
terminal_std_re = []
|
|
||||||
|
|
||||||
if terminal_std_option:
|
|
||||||
for item in terminal_std_option:
|
|
||||||
if "pattern" not in item:
|
|
||||||
raise AnsibleConnectionFailure("'pattern' is a required key for option '%s',"
|
|
||||||
" received option value is %s" % (option, item))
|
|
||||||
pattern = br"%s" % to_bytes(item['pattern'])
|
|
||||||
flag = item.get('flags', 0)
|
|
||||||
if flag:
|
|
||||||
flag = getattr(re, flag.split('.')[1])
|
|
||||||
terminal_std_re.append(re.compile(pattern, flag))
|
|
||||||
else:
|
|
||||||
# To maintain backward compatibility
|
|
||||||
terminal_std_re = getattr(self._terminal, option)
|
|
||||||
|
|
||||||
return terminal_std_re
|
|
@ -1,82 +0,0 @@
|
|||||||
# 2017 Red Hat Inc.
|
|
||||||
# (c) 2017 Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
author: Ansible Core Team
|
|
||||||
connection: persistent
|
|
||||||
short_description: Use a persistent unix socket for connection
|
|
||||||
description:
|
|
||||||
- This is a helper plugin to allow making other connections persistent.
|
|
||||||
version_added: "2.3"
|
|
||||||
options:
|
|
||||||
persistent_command_timeout:
|
|
||||||
type: int
|
|
||||||
description:
|
|
||||||
- Configures, in seconds, the amount of time to wait for a command to
|
|
||||||
return from the remote device. If this timer is exceeded before the
|
|
||||||
command returns, the connection plugin will raise an exception and
|
|
||||||
close
|
|
||||||
default: 10
|
|
||||||
ini:
|
|
||||||
- section: persistent_connection
|
|
||||||
key: command_timeout
|
|
||||||
env:
|
|
||||||
- name: ANSIBLE_PERSISTENT_COMMAND_TIMEOUT
|
|
||||||
vars:
|
|
||||||
- name: ansible_command_timeout
|
|
||||||
"""
|
|
||||||
from ansible.executor.task_executor import start_connection
|
|
||||||
from ansible.plugins.connection import ConnectionBase
|
|
||||||
from ansible.module_utils._text import to_text
|
|
||||||
from ansible.module_utils.connection import Connection as SocketConnection
|
|
||||||
from ansible.utils.display import Display
|
|
||||||
|
|
||||||
display = Display()
|
|
||||||
|
|
||||||
|
|
||||||
class Connection(ConnectionBase):
|
|
||||||
''' Local based connections '''
|
|
||||||
|
|
||||||
transport = 'persistent'
|
|
||||||
has_pipelining = False
|
|
||||||
|
|
||||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
|
||||||
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
|
||||||
self._task_uuid = to_text(kwargs.get('task_uuid', ''))
|
|
||||||
|
|
||||||
def _connect(self):
|
|
||||||
self._connected = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def exec_command(self, cmd, in_data=None, sudoable=True):
|
|
||||||
display.vvvv('exec_command(), socket_path=%s' % self.socket_path, host=self._play_context.remote_addr)
|
|
||||||
connection = SocketConnection(self.socket_path)
|
|
||||||
out = connection.exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
|
||||||
return 0, out, ''
|
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self._connected = False
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""Returns the path of the persistent connection socket.
|
|
||||||
|
|
||||||
Attempts to ensure (within playcontext.timeout seconds) that the
|
|
||||||
socket path exists. If the path exists (or the timeout has expired),
|
|
||||||
returns the socket path.
|
|
||||||
"""
|
|
||||||
display.vvvv('starting connection from persistent connection plugin', host=self._play_context.remote_addr)
|
|
||||||
variables = {'ansible_command_timeout': self.get_option('persistent_command_timeout')}
|
|
||||||
socket_path = start_connection(self._play_context, variables, self._task_uuid)
|
|
||||||
display.vvvv('local domain socket path is %s' % socket_path, host=self._play_context.remote_addr)
|
|
||||||
setattr(self, '_socket_path', socket_path)
|
|
||||||
return socket_path
|
|
@ -1,67 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleDocFragment(object):
|
|
||||||
|
|
||||||
# Standard files documentation fragment
|
|
||||||
DOCUMENTATION = r'''
|
|
||||||
options:
|
|
||||||
host:
|
|
||||||
description:
|
|
||||||
- Specifies the DNS host name or address for connecting to the remote
|
|
||||||
device over the specified transport. The value of host is used as
|
|
||||||
the destination address for the transport.
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
port:
|
|
||||||
description:
|
|
||||||
- Specifies the port to use when building the connection to the remote
|
|
||||||
device. The port value will default to port 830.
|
|
||||||
type: int
|
|
||||||
default: 830
|
|
||||||
username:
|
|
||||||
description:
|
|
||||||
- Configures the username to use to authenticate the connection to
|
|
||||||
the remote device. This value is used to authenticate
|
|
||||||
the SSH session. If the value is not specified in the task, the
|
|
||||||
value of environment variable C(ANSIBLE_NET_USERNAME) will be used instead.
|
|
||||||
type: str
|
|
||||||
password:
|
|
||||||
description:
|
|
||||||
- Specifies the password to use to authenticate the connection to
|
|
||||||
the remote device. This value is used to authenticate
|
|
||||||
the SSH session. If the value is not specified in the task, the
|
|
||||||
value of environment variable C(ANSIBLE_NET_PASSWORD) will be used instead.
|
|
||||||
type: str
|
|
||||||
timeout:
|
|
||||||
description:
|
|
||||||
- Specifies the timeout in seconds for communicating with the network device
|
|
||||||
for either connecting or sending commands. If the timeout is
|
|
||||||
exceeded before the operation is completed, the module will error.
|
|
||||||
type: int
|
|
||||||
default: 10
|
|
||||||
ssh_keyfile:
|
|
||||||
description:
|
|
||||||
- Specifies the SSH key to use to authenticate the connection to
|
|
||||||
the remote device. This value is the path to the key
|
|
||||||
used to authenticate the SSH session. If the value is not specified in
|
|
||||||
the task, the value of environment variable C(ANSIBLE_NET_SSH_KEYFILE)
|
|
||||||
will be used instead.
|
|
||||||
type: path
|
|
||||||
hostkey_verify:
|
|
||||||
description:
|
|
||||||
- If set to C(yes), the ssh host key of the device must match a ssh key present on
|
|
||||||
the host if set to C(no), the ssh host key of the device is not checked.
|
|
||||||
type: bool
|
|
||||||
default: yes
|
|
||||||
look_for_keys:
|
|
||||||
description:
|
|
||||||
- Enables looking in the usual locations for the ssh keys (e.g. :file:`~/.ssh/id_*`)
|
|
||||||
type: bool
|
|
||||||
default: yes
|
|
||||||
notes:
|
|
||||||
- For information on using netconf see the :ref:`Platform Options guide using Netconf<netconf_enabled_platform_options>`
|
|
||||||
- For more information on using Ansible to manage network devices see the :ref:`Ansible Network Guide <network_guide>`
|
|
||||||
'''
|
|
@ -1,15 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright: (c) 2019 Ansible, Inc
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleDocFragment(object):
|
|
||||||
|
|
||||||
# Standard files documentation fragment
|
|
||||||
DOCUMENTATION = r'''
|
|
||||||
options: {}
|
|
||||||
notes:
|
|
||||||
- This module is supported on C(ansible_network_os) network platforms. See
|
|
||||||
the :ref:`Network Platform Options <platform_options>` for details.
|
|
||||||
'''
|
|
File diff suppressed because it is too large
Load Diff
@ -1,481 +0,0 @@
|
|||||||
#
|
|
||||||
# {c) 2017 Red Hat, Inc.
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
# Make coding more python3-ish
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
import traceback
|
|
||||||
import string
|
|
||||||
|
|
||||||
from xml.etree.ElementTree import fromstring
|
|
||||||
|
|
||||||
from ansible.module_utils._text import to_native, to_text
|
|
||||||
from ansible.module_utils.network.common.utils import Template
|
|
||||||
from ansible.module_utils.six import iteritems, string_types
|
|
||||||
from ansible.module_utils.common._collections_compat import Mapping
|
|
||||||
from ansible.errors import AnsibleError, AnsibleFilterError
|
|
||||||
from ansible.utils.display import Display
|
|
||||||
from ansible.utils.encrypt import passlib_or_crypt, random_password
|
|
||||||
|
|
||||||
try:
|
|
||||||
import yaml
|
|
||||||
HAS_YAML = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_YAML = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
import textfsm
|
|
||||||
HAS_TEXTFSM = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_TEXTFSM = False
|
|
||||||
|
|
||||||
display = Display()
|
|
||||||
|
|
||||||
|
|
||||||
def re_matchall(regex, value):
|
|
||||||
objects = list()
|
|
||||||
for match in re.findall(regex.pattern, value, re.M):
|
|
||||||
obj = {}
|
|
||||||
if regex.groupindex:
|
|
||||||
for name, index in iteritems(regex.groupindex):
|
|
||||||
if len(regex.groupindex) == 1:
|
|
||||||
obj[name] = match
|
|
||||||
else:
|
|
||||||
obj[name] = match[index - 1]
|
|
||||||
objects.append(obj)
|
|
||||||
return objects
|
|
||||||
|
|
||||||
|
|
||||||
def re_search(regex, value):
|
|
||||||
obj = {}
|
|
||||||
match = regex.search(value, re.M)
|
|
||||||
if match:
|
|
||||||
items = list(match.groups())
|
|
||||||
if regex.groupindex:
|
|
||||||
for name, index in iteritems(regex.groupindex):
|
|
||||||
obj[name] = items[index - 1]
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
def parse_cli(output, tmpl):
|
|
||||||
if not isinstance(output, string_types):
|
|
||||||
raise AnsibleError("parse_cli input should be a string, but was given a input of %s" % (type(output)))
|
|
||||||
|
|
||||||
if not os.path.exists(tmpl):
|
|
||||||
raise AnsibleError('unable to locate parse_cli template: %s' % tmpl)
|
|
||||||
|
|
||||||
try:
|
|
||||||
template = Template()
|
|
||||||
except ImportError as exc:
|
|
||||||
raise AnsibleError(to_native(exc))
|
|
||||||
|
|
||||||
with open(tmpl) as tmpl_fh:
|
|
||||||
tmpl_content = tmpl_fh.read()
|
|
||||||
|
|
||||||
spec = yaml.safe_load(tmpl_content)
|
|
||||||
obj = {}
|
|
||||||
|
|
||||||
for name, attrs in iteritems(spec['keys']):
|
|
||||||
value = attrs['value']
|
|
||||||
|
|
||||||
try:
|
|
||||||
variables = spec.get('vars', {})
|
|
||||||
value = template(value, variables)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if 'start_block' in attrs and 'end_block' in attrs:
|
|
||||||
start_block = re.compile(attrs['start_block'])
|
|
||||||
end_block = re.compile(attrs['end_block'])
|
|
||||||
|
|
||||||
blocks = list()
|
|
||||||
lines = None
|
|
||||||
block_started = False
|
|
||||||
|
|
||||||
for line in output.split('\n'):
|
|
||||||
match_start = start_block.match(line)
|
|
||||||
match_end = end_block.match(line)
|
|
||||||
|
|
||||||
if match_start:
|
|
||||||
lines = list()
|
|
||||||
lines.append(line)
|
|
||||||
block_started = True
|
|
||||||
|
|
||||||
elif match_end:
|
|
||||||
if lines:
|
|
||||||
lines.append(line)
|
|
||||||
blocks.append('\n'.join(lines))
|
|
||||||
block_started = False
|
|
||||||
|
|
||||||
elif block_started:
|
|
||||||
if lines:
|
|
||||||
lines.append(line)
|
|
||||||
|
|
||||||
regex_items = [re.compile(r) for r in attrs['items']]
|
|
||||||
objects = list()
|
|
||||||
|
|
||||||
for block in blocks:
|
|
||||||
if isinstance(value, Mapping) and 'key' not in value:
|
|
||||||
items = list()
|
|
||||||
for regex in regex_items:
|
|
||||||
match = regex.search(block)
|
|
||||||
if match:
|
|
||||||
item_values = match.groupdict()
|
|
||||||
item_values['match'] = list(match.groups())
|
|
||||||
items.append(item_values)
|
|
||||||
else:
|
|
||||||
items.append(None)
|
|
||||||
|
|
||||||
obj = {}
|
|
||||||
for k, v in iteritems(value):
|
|
||||||
try:
|
|
||||||
obj[k] = template(v, {'item': items}, fail_on_undefined=False)
|
|
||||||
except Exception:
|
|
||||||
obj[k] = None
|
|
||||||
objects.append(obj)
|
|
||||||
|
|
||||||
elif isinstance(value, Mapping):
|
|
||||||
items = list()
|
|
||||||
for regex in regex_items:
|
|
||||||
match = regex.search(block)
|
|
||||||
if match:
|
|
||||||
item_values = match.groupdict()
|
|
||||||
item_values['match'] = list(match.groups())
|
|
||||||
items.append(item_values)
|
|
||||||
else:
|
|
||||||
items.append(None)
|
|
||||||
|
|
||||||
key = template(value['key'], {'item': items})
|
|
||||||
values = dict([(k, template(v, {'item': items})) for k, v in iteritems(value['values'])])
|
|
||||||
objects.append({key: values})
|
|
||||||
|
|
||||||
return objects
|
|
||||||
|
|
||||||
elif 'items' in attrs:
|
|
||||||
regexp = re.compile(attrs['items'])
|
|
||||||
when = attrs.get('when')
|
|
||||||
conditional = "{%% if %s %%}True{%% else %%}False{%% endif %%}" % when
|
|
||||||
|
|
||||||
if isinstance(value, Mapping) and 'key' not in value:
|
|
||||||
values = list()
|
|
||||||
|
|
||||||
for item in re_matchall(regexp, output):
|
|
||||||
entry = {}
|
|
||||||
|
|
||||||
for item_key, item_value in iteritems(value):
|
|
||||||
entry[item_key] = template(item_value, {'item': item})
|
|
||||||
|
|
||||||
if when:
|
|
||||||
if template(conditional, {'item': entry}):
|
|
||||||
values.append(entry)
|
|
||||||
else:
|
|
||||||
values.append(entry)
|
|
||||||
|
|
||||||
obj[name] = values
|
|
||||||
|
|
||||||
elif isinstance(value, Mapping):
|
|
||||||
values = dict()
|
|
||||||
|
|
||||||
for item in re_matchall(regexp, output):
|
|
||||||
entry = {}
|
|
||||||
|
|
||||||
for item_key, item_value in iteritems(value['values']):
|
|
||||||
entry[item_key] = template(item_value, {'item': item})
|
|
||||||
|
|
||||||
key = template(value['key'], {'item': item})
|
|
||||||
|
|
||||||
if when:
|
|
||||||
if template(conditional, {'item': {'key': key, 'value': entry}}):
|
|
||||||
values[key] = entry
|
|
||||||
else:
|
|
||||||
values[key] = entry
|
|
||||||
|
|
||||||
obj[name] = values
|
|
||||||
|
|
||||||
else:
|
|
||||||
item = re_search(regexp, output)
|
|
||||||
obj[name] = template(value, {'item': item})
|
|
||||||
|
|
||||||
else:
|
|
||||||
obj[name] = value
|
|
||||||
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
def parse_cli_textfsm(value, template):
|
|
||||||
if not HAS_TEXTFSM:
|
|
||||||
raise AnsibleError('parse_cli_textfsm filter requires TextFSM library to be installed')
|
|
||||||
|
|
||||||
if not isinstance(value, string_types):
|
|
||||||
raise AnsibleError("parse_cli_textfsm input should be a string, but was given a input of %s" % (type(value)))
|
|
||||||
|
|
||||||
if not os.path.exists(template):
|
|
||||||
raise AnsibleError('unable to locate parse_cli_textfsm template: %s' % template)
|
|
||||||
|
|
||||||
try:
|
|
||||||
template = open(template)
|
|
||||||
except IOError as exc:
|
|
||||||
raise AnsibleError(to_native(exc))
|
|
||||||
|
|
||||||
re_table = textfsm.TextFSM(template)
|
|
||||||
fsm_results = re_table.ParseText(value)
|
|
||||||
|
|
||||||
results = list()
|
|
||||||
for item in fsm_results:
|
|
||||||
results.append(dict(zip(re_table.header, item)))
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_param(template, root, attrs, value):
|
|
||||||
|
|
||||||
key = None
|
|
||||||
when = attrs.get('when')
|
|
||||||
conditional = "{%% if %s %%}True{%% else %%}False{%% endif %%}" % when
|
|
||||||
param_to_xpath_map = attrs['items']
|
|
||||||
|
|
||||||
if isinstance(value, Mapping):
|
|
||||||
key = value.get('key', None)
|
|
||||||
if key:
|
|
||||||
value = value['values']
|
|
||||||
|
|
||||||
entries = dict() if key else list()
|
|
||||||
|
|
||||||
for element in root.findall(attrs['top']):
|
|
||||||
entry = dict()
|
|
||||||
item_dict = dict()
|
|
||||||
for param, param_xpath in iteritems(param_to_xpath_map):
|
|
||||||
fields = None
|
|
||||||
try:
|
|
||||||
fields = element.findall(param_xpath)
|
|
||||||
except Exception:
|
|
||||||
display.warning("Failed to evaluate value of '%s' with XPath '%s'.\nUnexpected error: %s." % (param, param_xpath, traceback.format_exc()))
|
|
||||||
|
|
||||||
tags = param_xpath.split('/')
|
|
||||||
|
|
||||||
# check if xpath ends with attribute.
|
|
||||||
# If yes set attribute key/value dict to param value in case attribute matches
|
|
||||||
# else if it is a normal xpath assign matched element text value.
|
|
||||||
if len(tags) and tags[-1].endswith(']'):
|
|
||||||
if fields:
|
|
||||||
if len(fields) > 1:
|
|
||||||
item_dict[param] = [field.attrib for field in fields]
|
|
||||||
else:
|
|
||||||
item_dict[param] = fields[0].attrib
|
|
||||||
else:
|
|
||||||
item_dict[param] = {}
|
|
||||||
else:
|
|
||||||
if fields:
|
|
||||||
if len(fields) > 1:
|
|
||||||
item_dict[param] = [field.text for field in fields]
|
|
||||||
else:
|
|
||||||
item_dict[param] = fields[0].text
|
|
||||||
else:
|
|
||||||
item_dict[param] = None
|
|
||||||
|
|
||||||
if isinstance(value, Mapping):
|
|
||||||
for item_key, item_value in iteritems(value):
|
|
||||||
entry[item_key] = template(item_value, {'item': item_dict})
|
|
||||||
else:
|
|
||||||
entry = template(value, {'item': item_dict})
|
|
||||||
|
|
||||||
if key:
|
|
||||||
expanded_key = template(key, {'item': item_dict})
|
|
||||||
if when:
|
|
||||||
if template(conditional, {'item': {'key': expanded_key, 'value': entry}}):
|
|
||||||
entries[expanded_key] = entry
|
|
||||||
else:
|
|
||||||
entries[expanded_key] = entry
|
|
||||||
else:
|
|
||||||
if when:
|
|
||||||
if template(conditional, {'item': entry}):
|
|
||||||
entries.append(entry)
|
|
||||||
else:
|
|
||||||
entries.append(entry)
|
|
||||||
|
|
||||||
return entries
|
|
||||||
|
|
||||||
|
|
||||||
def parse_xml(output, tmpl):
|
|
||||||
if not os.path.exists(tmpl):
|
|
||||||
raise AnsibleError('unable to locate parse_xml template: %s' % tmpl)
|
|
||||||
|
|
||||||
if not isinstance(output, string_types):
|
|
||||||
raise AnsibleError('parse_xml works on string input, but given input of : %s' % type(output))
|
|
||||||
|
|
||||||
root = fromstring(output)
|
|
||||||
try:
|
|
||||||
template = Template()
|
|
||||||
except ImportError as exc:
|
|
||||||
raise AnsibleError(to_native(exc))
|
|
||||||
|
|
||||||
with open(tmpl) as tmpl_fh:
|
|
||||||
tmpl_content = tmpl_fh.read()
|
|
||||||
|
|
||||||
spec = yaml.safe_load(tmpl_content)
|
|
||||||
obj = {}
|
|
||||||
|
|
||||||
for name, attrs in iteritems(spec['keys']):
|
|
||||||
value = attrs['value']
|
|
||||||
|
|
||||||
try:
|
|
||||||
variables = spec.get('vars', {})
|
|
||||||
value = template(value, variables)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if 'items' in attrs:
|
|
||||||
obj[name] = _extract_param(template, root, attrs, value)
|
|
||||||
else:
|
|
||||||
obj[name] = value
|
|
||||||
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
def type5_pw(password, salt=None):
|
|
||||||
if not isinstance(password, string_types):
|
|
||||||
raise AnsibleFilterError("type5_pw password input should be a string, but was given a input of %s" % (type(password).__name__))
|
|
||||||
|
|
||||||
salt_chars = u''.join((
|
|
||||||
to_text(string.ascii_letters),
|
|
||||||
to_text(string.digits),
|
|
||||||
u'./'
|
|
||||||
))
|
|
||||||
if salt is not None and not isinstance(salt, string_types):
|
|
||||||
raise AnsibleFilterError("type5_pw salt input should be a string, but was given a input of %s" % (type(salt).__name__))
|
|
||||||
elif not salt:
|
|
||||||
salt = random_password(length=4, chars=salt_chars)
|
|
||||||
elif not set(salt) <= set(salt_chars):
|
|
||||||
raise AnsibleFilterError("type5_pw salt used inproper characters, must be one of %s" % (salt_chars))
|
|
||||||
|
|
||||||
encrypted_password = passlib_or_crypt(password, "md5_crypt", salt=salt)
|
|
||||||
|
|
||||||
return encrypted_password
|
|
||||||
|
|
||||||
|
|
||||||
def hash_salt(password):
|
|
||||||
|
|
||||||
split_password = password.split("$")
|
|
||||||
if len(split_password) != 4:
|
|
||||||
raise AnsibleFilterError('Could not parse salt out password correctly from {0}'.format(password))
|
|
||||||
else:
|
|
||||||
return split_password[2]
|
|
||||||
|
|
||||||
|
|
||||||
def comp_type5(unencrypted_password, encrypted_password, return_original=False):
|
|
||||||
|
|
||||||
salt = hash_salt(encrypted_password)
|
|
||||||
if type5_pw(unencrypted_password, salt) == encrypted_password:
|
|
||||||
if return_original is True:
|
|
||||||
return encrypted_password
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def vlan_parser(vlan_list, first_line_len=48, other_line_len=44):
|
|
||||||
|
|
||||||
'''
|
|
||||||
Input: Unsorted list of vlan integers
|
|
||||||
Output: Sorted string list of integers according to IOS-like vlan list rules
|
|
||||||
|
|
||||||
1. Vlans are listed in ascending order
|
|
||||||
2. Runs of 3 or more consecutive vlans are listed with a dash
|
|
||||||
3. The first line of the list can be first_line_len characters long
|
|
||||||
4. Subsequent list lines can be other_line_len characters
|
|
||||||
'''
|
|
||||||
|
|
||||||
# Sort and remove duplicates
|
|
||||||
sorted_list = sorted(set(vlan_list))
|
|
||||||
|
|
||||||
if sorted_list[0] < 1 or sorted_list[-1] > 4094:
|
|
||||||
raise AnsibleFilterError('Valid VLAN range is 1-4094')
|
|
||||||
|
|
||||||
parse_list = []
|
|
||||||
idx = 0
|
|
||||||
while idx < len(sorted_list):
|
|
||||||
start = idx
|
|
||||||
end = start
|
|
||||||
while end < len(sorted_list) - 1:
|
|
||||||
if sorted_list[end + 1] - sorted_list[end] == 1:
|
|
||||||
end += 1
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
if start == end:
|
|
||||||
# Single VLAN
|
|
||||||
parse_list.append(str(sorted_list[idx]))
|
|
||||||
elif start + 1 == end:
|
|
||||||
# Run of 2 VLANs
|
|
||||||
parse_list.append(str(sorted_list[start]))
|
|
||||||
parse_list.append(str(sorted_list[end]))
|
|
||||||
else:
|
|
||||||
# Run of 3 or more VLANs
|
|
||||||
parse_list.append(str(sorted_list[start]) + '-' + str(sorted_list[end]))
|
|
||||||
idx = end + 1
|
|
||||||
|
|
||||||
line_count = 0
|
|
||||||
result = ['']
|
|
||||||
for vlans in parse_list:
|
|
||||||
# First line (" switchport trunk allowed vlan ")
|
|
||||||
if line_count == 0:
|
|
||||||
if len(result[line_count] + vlans) > first_line_len:
|
|
||||||
result.append('')
|
|
||||||
line_count += 1
|
|
||||||
result[line_count] += vlans + ','
|
|
||||||
else:
|
|
||||||
result[line_count] += vlans + ','
|
|
||||||
|
|
||||||
# Subsequent lines (" switchport trunk allowed vlan add ")
|
|
||||||
else:
|
|
||||||
if len(result[line_count] + vlans) > other_line_len:
|
|
||||||
result.append('')
|
|
||||||
line_count += 1
|
|
||||||
result[line_count] += vlans + ','
|
|
||||||
else:
|
|
||||||
result[line_count] += vlans + ','
|
|
||||||
|
|
||||||
# Remove trailing orphan commas
|
|
||||||
for idx in range(0, len(result)):
|
|
||||||
result[idx] = result[idx].rstrip(',')
|
|
||||||
|
|
||||||
# Sometimes text wraps to next line, but there are no remaining VLANs
|
|
||||||
if '' in result:
|
|
||||||
result.remove('')
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class FilterModule(object):
|
|
||||||
"""Filters for working with output from network devices"""
|
|
||||||
|
|
||||||
filter_map = {
|
|
||||||
'parse_cli': parse_cli,
|
|
||||||
'parse_cli_textfsm': parse_cli_textfsm,
|
|
||||||
'parse_xml': parse_xml,
|
|
||||||
'type5_pw': type5_pw,
|
|
||||||
'hash_salt': hash_salt,
|
|
||||||
'comp_type5': comp_type5,
|
|
||||||
'vlan_parser': vlan_parser
|
|
||||||
}
|
|
||||||
|
|
||||||
def filters(self):
|
|
||||||
return self.filter_map
|
|
@ -1,86 +0,0 @@
|
|||||||
# Copyright (c) 2018 Cisco and/or its affiliates.
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
author: Ansible Networking Team
|
|
||||||
httpapi: restconf
|
|
||||||
short_description: HttpApi Plugin for devices supporting Restconf API
|
|
||||||
description:
|
|
||||||
- This HttpApi plugin provides methods to connect to Restconf API
|
|
||||||
endpoints.
|
|
||||||
version_added: "2.8"
|
|
||||||
options:
|
|
||||||
root_path:
|
|
||||||
type: str
|
|
||||||
description:
|
|
||||||
- Specifies the location of the Restconf root.
|
|
||||||
default: '/restconf'
|
|
||||||
vars:
|
|
||||||
- name: ansible_httpapi_restconf_root
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from ansible.module_utils._text import to_text
|
|
||||||
from ansible.module_utils.connection import ConnectionError
|
|
||||||
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
|
||||||
from ansible.plugins.httpapi import HttpApiBase
|
|
||||||
|
|
||||||
|
|
||||||
CONTENT_TYPE = 'application/yang-data+json'
|
|
||||||
|
|
||||||
|
|
||||||
class HttpApi(HttpApiBase):
|
|
||||||
def send_request(self, data, **message_kwargs):
|
|
||||||
if data:
|
|
||||||
data = json.dumps(data)
|
|
||||||
|
|
||||||
path = '/'.join([self.get_option('root_path').rstrip('/'), message_kwargs.get('path', '').lstrip('/')])
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'Content-Type': message_kwargs.get('content_type') or CONTENT_TYPE,
|
|
||||||
'Accept': message_kwargs.get('accept') or CONTENT_TYPE,
|
|
||||||
}
|
|
||||||
response, response_data = self.connection.send(path, data, headers=headers, method=message_kwargs.get('method'))
|
|
||||||
|
|
||||||
return handle_response(response, response_data)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_response(response, response_data):
|
|
||||||
try:
|
|
||||||
response_data = json.loads(response_data.read())
|
|
||||||
except ValueError:
|
|
||||||
response_data = response_data.read()
|
|
||||||
|
|
||||||
if isinstance(response, HTTPError):
|
|
||||||
if response_data:
|
|
||||||
if 'errors' in response_data:
|
|
||||||
errors = response_data['errors']['error']
|
|
||||||
error_text = '\n'.join((error['error-message'] for error in errors))
|
|
||||||
else:
|
|
||||||
error_text = response_data
|
|
||||||
|
|
||||||
raise ConnectionError(error_text, code=response.code)
|
|
||||||
raise ConnectionError(to_text(response), code=response.code)
|
|
||||||
|
|
||||||
return response_data
|
|
@ -1,68 +0,0 @@
|
|||||||
#
|
|
||||||
# (c) 2017 Red Hat Inc.
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
author: Ansible Networking Team
|
|
||||||
netconf: default
|
|
||||||
short_description: Use default netconf plugin to run standard netconf commands as per RFC
|
|
||||||
description:
|
|
||||||
- This default plugin provides low level abstraction apis for
|
|
||||||
sending and receiving netconf commands as per Netconf RFC specification.
|
|
||||||
version_added: "2.9"
|
|
||||||
options:
|
|
||||||
ncclient_device_handler:
|
|
||||||
type: str
|
|
||||||
default: default
|
|
||||||
description:
|
|
||||||
- Specifies the ncclient device handler name for network os that support default netconf
|
|
||||||
implementation as per Netconf RFC specification. To identify the ncclient device handler
|
|
||||||
name refer ncclient library documentation.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
|
|
||||||
from ansible.module_utils._text import to_text
|
|
||||||
from ansible.plugins.netconf import NetconfBase
|
|
||||||
|
|
||||||
|
|
||||||
class Netconf(NetconfBase):
|
|
||||||
|
|
||||||
def get_text(self, ele, tag):
|
|
||||||
try:
|
|
||||||
return to_text(ele.find(tag).text, errors='surrogate_then_replace').strip()
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_device_info(self):
|
|
||||||
device_info = dict()
|
|
||||||
device_info['network_os'] = 'default'
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
def get_capabilities(self):
|
|
||||||
result = dict()
|
|
||||||
result['rpc'] = self.get_base_rpc()
|
|
||||||
result['network_api'] = 'netconf'
|
|
||||||
result['device_info'] = self.get_device_info()
|
|
||||||
result['server_capabilities'] = [c for c in self.m.server_capabilities]
|
|
||||||
result['client_capabilities'] = [c for c in self.m.client_capabilities]
|
|
||||||
result['session_id'] = self.m.session_id
|
|
||||||
result['device_operations'] = self.get_device_operations(result['server_capabilities'])
|
|
||||||
return json.dumps(result)
|
|
@ -1,3 +0,0 @@
|
|||||||
shippable/posix/group2
|
|
||||||
skip/python2.6 # filters are controller only, and we no longer support Python 2.6 on the controller
|
|
||||||
skip/aix
|
|
@ -1,12 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -eux
|
|
||||||
|
|
||||||
source virtualenv.sh
|
|
||||||
|
|
||||||
# Requirements have to be installed prior to running ansible-playbook
|
|
||||||
# because plugins and requirements are loaded before the task runs
|
|
||||||
|
|
||||||
pip install netaddr
|
|
||||||
|
|
||||||
ANSIBLE_ROLES_PATH=../ ansible-playbook runme.yml "$@"
|
|
@ -1,3 +0,0 @@
|
|||||||
- hosts: localhost
|
|
||||||
roles:
|
|
||||||
- { role: filter_ipaddr }
|
|
@ -1,23 +0,0 @@
|
|||||||
- name: Test ipaddr filter
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'192.168.0.1/32' | ipaddr('netmask') == '255.255.255.255'"
|
|
||||||
- "'192.168.0.1/24' | ipaddr('netmask') == '255.255.255.0'"
|
|
||||||
- "not '192.168.0.1/31' | ipaddr('broadcast')"
|
|
||||||
- "'192.168.0.1/24' | ipaddr('broadcast') == '192.168.0.255'"
|
|
||||||
- "'192.168.0.1/24' | ipaddr('prefix') == 24"
|
|
||||||
- "'192.168.0.1/24' | ipaddr('address') == '192.168.0.1'"
|
|
||||||
- "'192.168.0.1/24' | ipaddr('network') == '192.168.0.0'"
|
|
||||||
- "'fe80::dead:beef/64' | ipaddr('broadcast') == 'fe80::ffff:ffff:ffff:ffff'"
|
|
||||||
- "'::1/120' | ipaddr('netmask') == 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00'"
|
|
||||||
- "{{ subnets | ipaddr(1) }} == ['10.1.1.1/24', '10.1.2.1/24']"
|
|
||||||
- "{{ subnets | ipaddr('1') }} == ['10.1.1.1/24', '10.1.2.1/24']"
|
|
||||||
- "{{ subnets | ipaddr(-1) }} == ['10.1.1.255/24', '10.1.2.255/24']"
|
|
||||||
- "{{ subnets | ipaddr('-1') }} == ['10.1.1.255/24', '10.1.2.255/24']"
|
|
||||||
- "'{{ prefix | ipaddr(1) }}' == '10.1.1.1/24'"
|
|
||||||
- "'{{ prefix | ipaddr('1') }}' == '10.1.1.1/24'"
|
|
||||||
- "'{{ prefix | ipaddr('network') }}' == '10.1.1.0'"
|
|
||||||
- "'{{ prefix | ipaddr('-1') }}' == '10.1.1.255/24'"
|
|
||||||
vars:
|
|
||||||
subnets: ['10.1.1.0/24', '10.1.2.0/24']
|
|
||||||
prefix: '10.1.1.0/24'
|
|
@ -1,2 +0,0 @@
|
|||||||
---
|
|
||||||
testcase: "*"
|
|
@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
dependencies:
|
|
||||||
- { role: prepare_junos_tests, when: ansible_network_os == 'junos' }
|
|
||||||
- { role: prepare_iosxr_tests, when: ansible_network_os == 'iosxr' }
|
|
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
- name: collect all netconf test cases
|
|
||||||
find:
|
|
||||||
paths: "{{ role_path }}/tests/iosxr"
|
|
||||||
patterns: "{{ testcase }}.yaml"
|
|
||||||
register: test_cases
|
|
||||||
connection: local
|
|
||||||
|
|
||||||
- name: set test_items
|
|
||||||
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
|
|
||||||
|
|
||||||
- name: run test case (connection=netconf)
|
|
||||||
include: "{{ test_case_to_run }}"
|
|
||||||
with_items: "{{ test_items }}"
|
|
||||||
loop_control:
|
|
||||||
loop_var: test_case_to_run
|
|
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
- name: collect all netconf test cases
|
|
||||||
find:
|
|
||||||
paths: "{{ role_path }}/tests/junos"
|
|
||||||
patterns: "{{ testcase }}.yaml"
|
|
||||||
register: test_cases
|
|
||||||
connection: local
|
|
||||||
|
|
||||||
- name: set test_items
|
|
||||||
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
|
|
||||||
|
|
||||||
- name: run test case (connection=netconf)
|
|
||||||
include: "{{ test_case_to_run }} ansible_connection=netconf"
|
|
||||||
with_items: "{{ test_items }}"
|
|
||||||
loop_control:
|
|
||||||
loop_var: test_case_to_run
|
|
@ -1,3 +0,0 @@
|
|||||||
---
|
|
||||||
- { include: junos.yaml, when: ansible_network_os == 'junos', tags: ['netconf'] }
|
|
||||||
- { include: iosxr.yaml, when: ansible_network_os == 'iosxr', tags: ['netconf'] }
|
|
@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
- debug: msg="START netconf_config iosxr/basic.yaml on connection={{ ansible_connection }}"
|
|
||||||
|
|
||||||
- name: save config test
|
|
||||||
netconf_config:
|
|
||||||
backup: yes
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'backup_path' in result"
|
|
||||||
|
|
||||||
- debug: msg="END netconf_config iosxr/basic.yaml on connection={{ ansible_connection }}"
|
|
@ -1,75 +0,0 @@
|
|||||||
---
|
|
||||||
- debug: msg="START netconf_config junos/basic.yaml on connection={{ ansible_connection }}"
|
|
||||||
|
|
||||||
- include_vars: "{{playbook_dir }}/targets/netconf_config/tests/junos/fixtures/config.yml"
|
|
||||||
|
|
||||||
- name: syslog file config- setup
|
|
||||||
junos_config:
|
|
||||||
lines:
|
|
||||||
- delete system syslog file test_netconf_config
|
|
||||||
|
|
||||||
- name: configure syslog file
|
|
||||||
netconf_config:
|
|
||||||
content: "{{ syslog_config }}"
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "result.changed == true"
|
|
||||||
- "'<name>test_netconf_config</name>' in result.diff.after"
|
|
||||||
|
|
||||||
- name: configure syslog file (idempotent)
|
|
||||||
netconf_config:
|
|
||||||
content: "{{ syslog_config }}"
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "result.changed == false"
|
|
||||||
|
|
||||||
- name: replace default operation fail
|
|
||||||
netconf_config:
|
|
||||||
content: "{{ syslog_config_replace }}"
|
|
||||||
default_operation: 'replace'
|
|
||||||
register: result
|
|
||||||
ignore_errors: yes
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "result.failed == true"
|
|
||||||
- "'Missing mandatory statement' in result.msg"
|
|
||||||
|
|
||||||
- name: replace syslog config with operation key in content
|
|
||||||
netconf_config:
|
|
||||||
content: "{{ syslog_config_replace }}"
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "result.changed == true"
|
|
||||||
|
|
||||||
- name: test backup
|
|
||||||
netconf_config:
|
|
||||||
content: "{{ syslog_config }}"
|
|
||||||
backup: True
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'backup_path' in result"
|
|
||||||
|
|
||||||
- name: syslog file config- teardown
|
|
||||||
junos_config:
|
|
||||||
lines:
|
|
||||||
- delete system syslog file test_netconf_config
|
|
||||||
|
|
||||||
- name: save config
|
|
||||||
netconf_config:
|
|
||||||
backup: yes
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'backup_path' in result"
|
|
||||||
|
|
||||||
- debug: msg="END netconf_config junos/basic.yaml on connection={{ ansible_connection }}"
|
|
@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
syslog_config: |
|
|
||||||
<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
|
|
||||||
<configuration>
|
|
||||||
<system>
|
|
||||||
<syslog>
|
|
||||||
<file>
|
|
||||||
<name>test_netconf_config</name>
|
|
||||||
<contents>
|
|
||||||
<name>any</name>
|
|
||||||
<any/>
|
|
||||||
</contents>
|
|
||||||
<contents>
|
|
||||||
<name>kernel</name>
|
|
||||||
<critical/>
|
|
||||||
</contents>
|
|
||||||
</file>
|
|
||||||
</syslog>
|
|
||||||
</system>
|
|
||||||
</configuration>
|
|
||||||
</config>
|
|
||||||
|
|
||||||
syslog_config_replace: |
|
|
||||||
<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
|
|
||||||
<configuration>
|
|
||||||
<system>
|
|
||||||
<syslog operation="replace">
|
|
||||||
<file>
|
|
||||||
<name>test_netconf_config</name>
|
|
||||||
<contents>
|
|
||||||
<name>any</name>
|
|
||||||
<any/>
|
|
||||||
</contents>
|
|
||||||
</file>
|
|
||||||
</syslog>
|
|
||||||
</system>
|
|
||||||
</configuration>
|
|
||||||
</config>
|
|
@ -1,2 +0,0 @@
|
|||||||
---
|
|
||||||
testcase: "*"
|
|
@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
dependencies:
|
|
||||||
- { role: prepare_junos_tests, when: ansible_network_os == 'junos' }
|
|
||||||
- { role: prepare_iosxr_tests, when: ansible_network_os == 'iosxr' }
|
|
||||||
- { role: prepare_sros_tests, when: ansible_network_os == 'sros' }
|
|
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
- name: collect all netconf test cases
|
|
||||||
find:
|
|
||||||
paths: "{{ role_path }}/tests/iosxr"
|
|
||||||
patterns: "{{ testcase }}.yaml"
|
|
||||||
register: test_cases
|
|
||||||
connection: local
|
|
||||||
|
|
||||||
- name: set test_items
|
|
||||||
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
|
|
||||||
|
|
||||||
- name: run test case (connection=netconf)
|
|
||||||
include: "{{ test_case_to_run }}"
|
|
||||||
with_items: "{{ test_items }}"
|
|
||||||
loop_control:
|
|
||||||
loop_var: test_case_to_run
|
|
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
- name: collect all netconf test cases
|
|
||||||
find:
|
|
||||||
paths: "{{ role_path }}/tests/junos"
|
|
||||||
patterns: "{{ testcase }}.yaml"
|
|
||||||
register: test_cases
|
|
||||||
connection: local
|
|
||||||
|
|
||||||
- name: set test_items
|
|
||||||
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
|
|
||||||
|
|
||||||
- name: run test case (connection=netconf)
|
|
||||||
include: "{{ test_case_to_run }} ansible_connection=netconf"
|
|
||||||
with_items: "{{ test_items }}"
|
|
||||||
loop_control:
|
|
||||||
loop_var: test_case_to_run
|
|
@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
- { include: junos.yaml, when: ansible_network_os == 'junos', tags: ['netconf'] }
|
|
||||||
- { include: iosxr.yaml, when: ansible_network_os == 'iosxr', tags: ['netconf'] }
|
|
||||||
- { include: sros.yaml, when: ansible_network_os == 'sros', tags: ['netconf'] }
|
|
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
- name: collect all netconf test cases
|
|
||||||
find:
|
|
||||||
paths: "{{ role_path }}/tests/sros"
|
|
||||||
patterns: "{{ testcase }}.yaml"
|
|
||||||
register: test_cases
|
|
||||||
connection: local
|
|
||||||
|
|
||||||
- name: set test_items
|
|
||||||
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
|
|
||||||
|
|
||||||
- name: run test case (connection=netconf)
|
|
||||||
include: "{{ test_case_to_run }} ansible_connection=netconf"
|
|
||||||
with_items: "{{ test_items }}"
|
|
||||||
loop_control:
|
|
||||||
loop_var: test_case_to_run
|
|
@ -1,163 +0,0 @@
|
|||||||
---
|
|
||||||
- debug: msg="START netconf_get iosxr/basic.yaml on connection={{ ansible_connection }}"
|
|
||||||
|
|
||||||
- name: setup interface
|
|
||||||
iosxr_config:
|
|
||||||
commands:
|
|
||||||
- description this is test interface Loopback999
|
|
||||||
- no shutdown
|
|
||||||
parents:
|
|
||||||
- interface Loopback999
|
|
||||||
match: none
|
|
||||||
connection: network_cli
|
|
||||||
|
|
||||||
- name: get running interface confiugration with filter
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
filter: <interface-configurations xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg"></interface-configurations>
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'<description>this is test interface Loopback999</description>' in result.stdout"
|
|
||||||
- "'<usernames>' not in result.stdout"
|
|
||||||
|
|
||||||
- name: test lock=never, get-config, running interface confiugration with filter without lock
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
lock: never
|
|
||||||
filter: <interface-configurations xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg"></interface-configurations>
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'<description>this is test interface Loopback999</description>' in result.stdout"
|
|
||||||
- "'<usernames>' not in result.stdout"
|
|
||||||
|
|
||||||
- name: test lock=if-supported, get-config, running interface confiugration with filter without lock
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
lock: if-supported
|
|
||||||
filter: <interface-configurations xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg"></interface-configurations>
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'<description>this is test interface Loopback999</description>' in result.stdout"
|
|
||||||
- "'<usernames>' not in result.stdout"
|
|
||||||
|
|
||||||
- name: Failure scenario, get-config information with lock
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
lock: always
|
|
||||||
register: result
|
|
||||||
ignore_errors: True
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'<bad-element>running</bad-element>' in result.msg"
|
|
||||||
|
|
||||||
- name: Failure scenario, fetch config from startup
|
|
||||||
netconf_get:
|
|
||||||
source: startup
|
|
||||||
register: result
|
|
||||||
ignore_errors: True
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'startup source is not supported' in result.msg"
|
|
||||||
|
|
||||||
- name: test get, information from running datastore without lock
|
|
||||||
netconf_get:
|
|
||||||
lock: never
|
|
||||||
filter: <interface-configurations xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg"></interface-configurations>
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'<description>this is test interface Loopback999</description>' in result.stdout"
|
|
||||||
|
|
||||||
- name: test get, information from running datastore with lock if supported
|
|
||||||
netconf_get:
|
|
||||||
lock: if-supported
|
|
||||||
filter: <interface-configurations xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg"></interface-configurations>
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'<description>this is test interface Loopback999</description>' in result.stdout"
|
|
||||||
|
|
||||||
- name: Failure scenario, get information from running with lock
|
|
||||||
netconf_get:
|
|
||||||
lock: always
|
|
||||||
register: result
|
|
||||||
ignore_errors: True
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'<bad-element>running</bad-element>' in result.msg"
|
|
||||||
|
|
||||||
- name: get configuration and state data in json format
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
display: json
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "{{ result['output']['data']['aaa'] is defined}}"
|
|
||||||
|
|
||||||
- name: get configuration data in xml pretty format
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
display: pretty
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "{{ result['output'] is defined}}"
|
|
||||||
|
|
||||||
- name: get configuration data in xml with namespace stripped
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
display: xml
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "{{ result['output'] is defined}}"
|
|
||||||
- "{{ 'xmlns' not in result.output }}"
|
|
||||||
|
|
||||||
- name: Failure scenario, unsupported filter
|
|
||||||
netconf_get:
|
|
||||||
filter: configuration/state
|
|
||||||
register: result
|
|
||||||
ignore_errors: True
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'filter value \\'configuration/state\\' of type xpath is not supported' in result.msg"
|
|
||||||
|
|
||||||
- name: setup - teardown
|
|
||||||
iosxr_config:
|
|
||||||
commands:
|
|
||||||
- no description
|
|
||||||
- shutdown
|
|
||||||
parents:
|
|
||||||
- interface Loopback999
|
|
||||||
match: none
|
|
||||||
connection: network_cli
|
|
||||||
|
|
||||||
- debug: msg="END netconf_get iosxr/basic.yaml on connection={{ ansible_connection }}"
|
|
@ -1,126 +0,0 @@
|
|||||||
---
|
|
||||||
- debug: msg="START netconf_get junos/basic.yaml on connection={{ ansible_connection }}"
|
|
||||||
|
|
||||||
- name: Configure syslog file - setup
|
|
||||||
junos_config:
|
|
||||||
lines:
|
|
||||||
- set system syslog file test1 any any
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- name: Get system configuration data from running datastore state
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
filter: <configuration><system><syslog></syslog></system></configuration>
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'<name>test1</name>' in result.stdout"
|
|
||||||
- "'<name>any</name>' in result.stdout"
|
|
||||||
- "'<any/>' in result.stdout"
|
|
||||||
- "'<login>' not in result.stdout"
|
|
||||||
- "'<interface>' not in result.stdout"
|
|
||||||
|
|
||||||
- name: Failure scenario, fetch config from startup
|
|
||||||
netconf_get:
|
|
||||||
source: startup
|
|
||||||
register: result
|
|
||||||
ignore_errors: True
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'startup source is not supported' in result.msg"
|
|
||||||
|
|
||||||
- name: Failure scenario, fetch config from running with lock
|
|
||||||
netconf_get:
|
|
||||||
lock: always
|
|
||||||
source: running
|
|
||||||
register: result
|
|
||||||
ignore_errors: True
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'syntax error' in result.msg"
|
|
||||||
|
|
||||||
- name: Get system configuration data from running datastore state and lock if-supported
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
filter: <configuration><system><syslog></syslog></system></configuration>
|
|
||||||
lock: if-supported
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'<name>test1</name>' in result.stdout"
|
|
||||||
- "'<name>any</name>' in result.stdout"
|
|
||||||
- "'<any/>' in result.stdout"
|
|
||||||
- "'<login>' not in result.stdout"
|
|
||||||
- "'<interface>' not in result.stdout"
|
|
||||||
|
|
||||||
- name: get configuration and state data in json format
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
display: json
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "{{ result['output']['rpc-reply']['data']['configuration'] is defined}}"
|
|
||||||
|
|
||||||
- name: get configuration and state data in xml pretty format
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
display: pretty
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "{{ result['output'] is defined}}"
|
|
||||||
|
|
||||||
- name: get configuration data in xml with namespace stripped
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
display: xml
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "{{ result['output'] is defined}}"
|
|
||||||
- "{{ 'xmlns' not in result.output }}"
|
|
||||||
|
|
||||||
- name: get configuration and state data without datastore lock
|
|
||||||
netconf_get:
|
|
||||||
lock: never
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'<database-status-information>' in result.stdout"
|
|
||||||
- "'</configuration>' in result.stdout"
|
|
||||||
|
|
||||||
- name: get configuration and state data and lock data-store if supported
|
|
||||||
netconf_get:
|
|
||||||
lock: if-supported
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'<database-status-information>' in result.stdout"
|
|
||||||
- "'</configuration>' in result.stdout"
|
|
||||||
|
|
||||||
- name: Failure scenario, unsupported filter
|
|
||||||
netconf_get:
|
|
||||||
filter: configuration/state
|
|
||||||
register: result
|
|
||||||
ignore_errors: True
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'filter value \\'configuration/state\\' of type xpath is not supported' in result.msg"
|
|
||||||
|
|
||||||
- name: Configure syslog file - teardown
|
|
||||||
junos_config:
|
|
||||||
lines:
|
|
||||||
- delete system syslog file test1 any any
|
|
||||||
|
|
||||||
- debug: msg="END netconf_get junos/basic.yaml on connection={{ ansible_connection }}"
|
|
@ -1,56 +0,0 @@
|
|||||||
---
|
|
||||||
- debug: msg="START netconf_get sros/basic.yaml on connection={{ ansible_connection }}"
|
|
||||||
|
|
||||||
- name: Get complete configuration data (SROS)
|
|
||||||
netconf_get:
|
|
||||||
filter: <configure xmlns="urn:nokia.com:sros:ns:yang:sr:conf"/>
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'urn:nokia.com:sros:ns:yang:sr:conf' in result.stdout"
|
|
||||||
- "'urn:nokia.com:sros:ns:yang:sr:state' not in result.stdout"
|
|
||||||
|
|
||||||
- name: Get complete state data (SROS)
|
|
||||||
netconf_get:
|
|
||||||
filter: <state xmlns="urn:nokia.com:sros:ns:yang:sr:state"/>
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'urn:nokia.com:sros:ns:yang:sr:state' in result.stdout"
|
|
||||||
- "'urn:nokia.com:sros:ns:yang:sr:conf' not in result.stdout"
|
|
||||||
|
|
||||||
- name: Get service configuration data from candidate datastore (SROS)
|
|
||||||
netconf_get:
|
|
||||||
source: candidate
|
|
||||||
filter: <configure xmlns="urn:nokia.com:sros:ns:yang:sr:conf"><service/></configure>
|
|
||||||
display: json
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'<service>' in result.stdout"
|
|
||||||
|
|
||||||
- name: Get system configuration data from running datastore (SROS)
|
|
||||||
netconf_get:
|
|
||||||
source: running
|
|
||||||
filter: <configure xmlns="urn:nokia.com:sros:ns:yang:sr:conf"><system/></configure>
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'<system>' in result.stdout"
|
|
||||||
|
|
||||||
- name: Get complete configuration and state data (SROS)
|
|
||||||
netconf_get:
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'<service>' in result.stdout"
|
|
||||||
- "'<system>' in result.stdout"
|
|
||||||
- "'urn:nokia.com:sros:ns:yang:sr:conf' in result.stdout"
|
|
||||||
- "'urn:nokia.com:sros:ns:yang:sr:state' in result.stdout"
|
|
||||||
|
|
||||||
- debug: msg="END netconf_get sros/basic.yaml on connection={{ ansible_connection }}"
|
|
@ -1,2 +0,0 @@
|
|||||||
---
|
|
||||||
testcase: "*"
|
|
@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
dependencies:
|
|
||||||
- { role: prepare_junos_tests, when: ansible_network_os == 'junos' }
|
|
||||||
- { role: prepare_iosxr_tests, when: ansible_network_os == 'iosxr' }
|
|
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
- name: collect all netconf test cases
|
|
||||||
find:
|
|
||||||
paths: "{{ role_path }}/tests/iosxr"
|
|
||||||
patterns: "{{ testcase }}.yaml"
|
|
||||||
register: test_cases
|
|
||||||
connection: local
|
|
||||||
|
|
||||||
- name: set test_items
|
|
||||||
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
|
|
||||||
|
|
||||||
- name: run test case (connection=netconf)
|
|
||||||
include: "{{ test_case_to_run }} ansible_connection=netconf"
|
|
||||||
with_items: "{{ test_items }}"
|
|
||||||
loop_control:
|
|
||||||
loop_var: test_case_to_run
|
|
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
- name: collect all netconf test cases
|
|
||||||
find:
|
|
||||||
paths: "{{ role_path }}/tests/junos"
|
|
||||||
patterns: "{{ testcase }}.yaml"
|
|
||||||
register: test_cases
|
|
||||||
connection: local
|
|
||||||
|
|
||||||
- name: set test_items
|
|
||||||
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
|
|
||||||
|
|
||||||
- name: run test case (connection=netconf)
|
|
||||||
include: "{{ test_case_to_run }} ansible_connection=netconf"
|
|
||||||
with_items: "{{ test_items }}"
|
|
||||||
loop_control:
|
|
||||||
loop_var: test_case_to_run
|
|
@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
- { include: junos.yaml, when: ansible_network_os == 'junos', tags: ['netconf'] }
|
|
||||||
- { include: iosxr.yaml, when: ansible_network_os == 'iosxr', tags: ['netconf'] }
|
|
||||||
- { include: sros.yaml, when: ansible_network_os == 'sros', tags: ['netconf'] }
|
|
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
- name: collect all netconf test cases
|
|
||||||
find:
|
|
||||||
paths: "{{ role_path }}/tests/sros"
|
|
||||||
patterns: "{{ testcase }}.yaml"
|
|
||||||
register: test_cases
|
|
||||||
connection: local
|
|
||||||
|
|
||||||
- name: set test_items
|
|
||||||
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
|
|
||||||
|
|
||||||
- name: run test case (connection=netconf)
|
|
||||||
include: "{{ test_case_to_run }} ansible_connection=netconf"
|
|
||||||
with_items: "{{ test_items }}"
|
|
||||||
loop_control:
|
|
||||||
loop_var: test_case_to_run
|
|
@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
- debug: msg="START netconf_rpc iosxr/basic.yaml on connection={{ ansible_connection }}"
|
|
||||||
|
|
||||||
- name: discard changes
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: discard-changes
|
|
||||||
|
|
||||||
- debug: msg="END netconf_rpc iosxr/basic.yaml on connection={{ ansible_connection }}"
|
|
@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
- debug: msg="START netconf_rpc junos/basic.yaml on connection={{ ansible_connection }}"
|
|
||||||
|
|
||||||
- name: discard changes
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: discard-changes
|
|
||||||
|
|
||||||
- debug: msg="END netconf_rpc junos/basic.yaml on connection={{ ansible_connection }}"
|
|
@ -1,188 +0,0 @@
|
|||||||
---
|
|
||||||
- debug: msg="START netconf_rpc sros/basic.yaml on connection={{ ansible_connection }}"
|
|
||||||
|
|
||||||
- name: lock candidate (content is dict)
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: lock
|
|
||||||
content:
|
|
||||||
target:
|
|
||||||
candidate:
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- name: discard changes (w/o content)
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: discard-changes
|
|
||||||
display: xml
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- name: unlock candidate (content is dict as json)
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: unlock
|
|
||||||
xmlns: "urn:ietf:params:xml:ns:netconf:base:1.0"
|
|
||||||
content: "{'target': {'candidate': None}}"
|
|
||||||
display: json
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "{{ result['output']['rpc-reply'] is defined}}"
|
|
||||||
- "{{ result['output']['rpc-reply']['ok'] is defined}}"
|
|
||||||
|
|
||||||
- name: validate candidate (content is single line of XML)
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: validate
|
|
||||||
content: "<source><candidate/></source>"
|
|
||||||
display: json
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "{{ result['output']['rpc-reply'] is defined}}"
|
|
||||||
- "{{ result['output']['rpc-reply']['ok'] is defined}}"
|
|
||||||
|
|
||||||
- name: copy running to startup
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: copy-config
|
|
||||||
content:
|
|
||||||
source:
|
|
||||||
running:
|
|
||||||
target:
|
|
||||||
startup:
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- name: get schema list (content is multiple lines of XML)
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: get
|
|
||||||
content: |
|
|
||||||
<filter>
|
|
||||||
<netconf-state xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring">
|
|
||||||
<schemas/>
|
|
||||||
</netconf-state>
|
|
||||||
</filter>
|
|
||||||
display: json
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "{{ result['output']['data'] is defined}}"
|
|
||||||
- "{{ result['output']['data']['netconf-state'] is defined}}"
|
|
||||||
- "{{ result['output']['data']['netconf-state']['schemas'] is defined}}"
|
|
||||||
- "{{ result['output']['data']['netconf-state']['schemas']['schema'] is defined}}"
|
|
||||||
|
|
||||||
# The following two test-cases have been validated against a pre-release implementation.
|
|
||||||
# To make this playbook work with the regular Nokia SROS 16.0 release, those test-cases
|
|
||||||
# have been commented out. As soon the <get-schema> operation is supported by SROS
|
|
||||||
# those test-cases shall be included.
|
|
||||||
|
|
||||||
#- name: get-schema
|
|
||||||
# netconf_rpc:
|
|
||||||
# rpc: get-schema
|
|
||||||
# xmlns: urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring
|
|
||||||
# content:
|
|
||||||
# identifier: ietf-netconf
|
|
||||||
# version: "2011-06-01"
|
|
||||||
# register: result
|
|
||||||
# connection: netconf
|
|
||||||
|
|
||||||
#- name: get schema using XML request
|
|
||||||
# netconf_rpc:
|
|
||||||
# rpc: "get-schema"
|
|
||||||
# xmlns: "urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring"
|
|
||||||
# content: |
|
|
||||||
# <identifier>ietf-netconf-monitoring</identifier>
|
|
||||||
# <version>2010-10-04</version>
|
|
||||||
# display: pretty
|
|
||||||
# register: result
|
|
||||||
# connection: netconf
|
|
||||||
|
|
||||||
- name: Failure scenario, unsupported content (xpath value)
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: get
|
|
||||||
content: schemas/schema[identifier=ietf-netconf-monitoring]
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
ignore_errors: True
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'unsupported content value' in result.msg"
|
|
||||||
|
|
||||||
- name: Failure scenario, unsupported content type (list)
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: get
|
|
||||||
content:
|
|
||||||
- value1
|
|
||||||
- value2
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
ignore_errors: True
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'unsupported content data-type' in result.msg"
|
|
||||||
|
|
||||||
- name: Failure scenario, RPC is close-session
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: close-session
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
ignore_errors: True
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'unsupported operation' in result.msg"
|
|
||||||
|
|
||||||
- name: Failure scenario, attribute rpc missing
|
|
||||||
netconf_rpc:
|
|
||||||
display: json
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
ignore_errors: True
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'missing required arguments' in result.msg"
|
|
||||||
|
|
||||||
- name: Failure scenario, attribute rpc is None
|
|
||||||
netconf_rpc:
|
|
||||||
rpc:
|
|
||||||
display: json
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
ignore_errors: True
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'must not be None' in result.msg"
|
|
||||||
|
|
||||||
- name: Failure scenario, attribute rpc is zero-length string
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: ""
|
|
||||||
display: json
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
ignore_errors: True
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'must not be empty' in result.msg"
|
|
||||||
|
|
||||||
- name: Failure scenario, attribute rpc only contains white-spaces
|
|
||||||
netconf_rpc:
|
|
||||||
rpc: " "
|
|
||||||
display: json
|
|
||||||
register: result
|
|
||||||
connection: netconf
|
|
||||||
ignore_errors: True
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "'must not be empty' in result.msg"
|
|
||||||
|
|
||||||
- debug: msg="END netconf_rpc sros/basic.yaml on connection={{ ansible_connection }}"
|
|
@ -1,46 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# (c) 2017 Red Hat, Inc.
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible 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 General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from units.compat import unittest
|
|
||||||
from ansible.module_utils.network.common.parsing import Conditional
|
|
||||||
|
|
||||||
test_results = ['result_1', 'result_2', 'result_3']
|
|
||||||
c1 = Conditional('result[1] == result_2')
|
|
||||||
c2 = Conditional('result[2] not == result_2')
|
|
||||||
c3 = Conditional('result[0] neq not result_1')
|
|
||||||
|
|
||||||
|
|
||||||
class TestNotKeyword(unittest.TestCase):
|
|
||||||
def test_negate_instance_variable_assignment(self):
|
|
||||||
assert c1.negate is False and c2.negate is True
|
|
||||||
|
|
||||||
def test_key_value_instance_variable_assignment(self):
|
|
||||||
c1_assignments = c1.key == 'result[1]' and c1.value == 'result_2'
|
|
||||||
c2_assignments = c2.key == 'result[2]' and c2.value == 'result_2'
|
|
||||||
assert c1_assignments and c2_assignments
|
|
||||||
|
|
||||||
def test_conditionals_w_not_keyword(self):
|
|
||||||
assert c1(test_results) and c2(test_results) and c3(test_results)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue