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