summaryrefslogtreecommitdiffstats
path: root/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common
diff options
context:
space:
mode:
Diffstat (limited to 'test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common')
-rw-r--r--test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/cfg/base.py27
-rw-r--r--test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/config.py473
-rw-r--r--test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py162
-rw-r--r--test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/netconf.py179
-rw-r--r--test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/network.py275
-rw-r--r--test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/parsing.py316
-rw-r--r--test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py686
7 files changed, 2118 insertions, 0 deletions
diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/cfg/base.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/cfg/base.py
new file mode 100644
index 0000000..68608d1
--- /dev/null
+++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/cfg/base.py
@@ -0,0 +1,27 @@
+#
+# -*- 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_collections.ansible.netcommon.plugins.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)
diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/config.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/config.py
new file mode 100644
index 0000000..bc458eb
--- /dev/null
+++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/config.py
@@ -0,0 +1,473 @@
+# 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)
diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py
new file mode 100644
index 0000000..477d318
--- /dev/null
+++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py
@@ -0,0 +1,162 @@
+#
+# -*- 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_collections.ansible.netcommon.plugins.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
diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/netconf.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/netconf.py
new file mode 100644
index 0000000..53a91e8
--- /dev/null
+++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/netconf.py
@@ -0,0 +1,179 @@
+# 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
diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/network.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/network.py
new file mode 100644
index 0000000..555fc71
--- /dev/null
+++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/network.py
@@ -0,0 +1,275 @@
+# 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_collections.ansible.netcommon.plugins.module_utils.network.common.netconf import (
+ NetconfConnection,
+)
+from ansible_collections.ansible.netcommon.plugins.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."
+ )
diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/parsing.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/parsing.py
new file mode 100644
index 0000000..2dd1de9
--- /dev/null
+++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/parsing.py
@@ -0,0 +1,316 @@
+# 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
diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py
new file mode 100644
index 0000000..64eca15
--- /dev/null
+++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py
@@ -0,0 +1,686 @@
+# 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