summaryrefslogtreecommitdiffstats
path: root/test/support/network-integration/collections/ansible_collections/cisco
diff options
context:
space:
mode:
Diffstat (limited to 'test/support/network-integration/collections/ansible_collections/cisco')
-rw-r--r--test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/action/ios.py133
-rw-r--r--test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/cliconf/ios.py466
-rw-r--r--test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/doc_fragments/ios.py81
-rw-r--r--test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/module_utils/network/ios/ios.py197
-rw-r--r--test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_command.py229
-rw-r--r--test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py596
-rw-r--r--test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/terminal/ios.py115
7 files changed, 1817 insertions, 0 deletions
diff --git a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/action/ios.py b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/action/ios.py
new file mode 100644
index 0000000..e3605d0
--- /dev/null
+++ b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/action/ios.py
@@ -0,0 +1,133 @@
+#
+# (c) 2016 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 sys
+import copy
+
+from ansible_collections.ansible.netcommon.plugins.action.network import (
+ ActionModule as ActionNetworkModule,
+)
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
+ load_provider,
+)
+from ansible_collections.cisco.ios.plugins.module_utils.network.ios.ios import (
+ ios_provider_spec,
+)
+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 == "ios_config" else False
+ persistent_connection = self._play_context.connection.split(".")[-1]
+ warnings = []
+
+ if persistent_connection == "network_cli":
+ provider = self._task.args.get("provider", {})
+ if any(provider.values()):
+ display.warning(
+ "provider is unnecessary when using network_cli and will be ignored"
+ )
+ del self._task.args["provider"]
+ elif self._play_context.connection == "local":
+ provider = load_provider(ios_provider_spec, self._task.args)
+ pc = copy.deepcopy(self._play_context)
+ pc.connection = "ansible.netcommon.network_cli"
+ pc.network_os = "cisco.ios.ios"
+ pc.remote_addr = provider["host"] or self._play_context.remote_addr
+ pc.port = int(provider["port"] or self._play_context.port or 22)
+ pc.remote_user = (
+ provider["username"] or self._play_context.connection_user
+ )
+ pc.password = provider["password"] or self._play_context.password
+ pc.private_key_file = (
+ provider["ssh_keyfile"] or self._play_context.private_key_file
+ )
+ pc.become = provider["authorize"] or False
+ if pc.become:
+ pc.become_method = "enable"
+ pc.become_pass = provider["auth_pass"]
+
+ 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 = "network_cli"
+ pc.network_os = "ios"
+ 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,
+ )
+
+ command_timeout = (
+ int(provider["timeout"])
+ if provider["timeout"]
+ else connection.get_option("persistent_command_timeout")
+ )
+ connection.set_options(
+ direct={"persistent_command_timeout": command_timeout}
+ )
+
+ 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/latest/network/user_guide/network_debug_troubleshooting.html#category-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
+ ]
+ )
+ else:
+ return {
+ "failed": True,
+ "msg": "Connection type %s is not valid for this module"
+ % self._play_context.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
diff --git a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/cliconf/ios.py b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/cliconf/ios.py
new file mode 100644
index 0000000..feba971
--- /dev/null
+++ b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/cliconf/ios.py
@@ -0,0 +1,466 @@
+#
+# (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
+cliconf: ios
+short_description: Use ios cliconf to run command on Cisco IOS platform
+description:
+ - This ios plugin provides low level abstraction apis for
+ sending and receiving CLI commands from Cisco IOS network devices.
+version_added: "2.4"
+"""
+
+import re
+import time
+import json
+
+from collections.abc import Mapping
+
+from ansible.errors import AnsibleConnectionFailure
+from ansible.module_utils._text import to_text
+from ansible.module_utils.six import iteritems
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import (
+ NetworkConfig,
+ dumps,
+)
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
+ to_list,
+)
+from ansible.plugins.cliconf import CliconfBase, enable_mode
+
+
+class Cliconf(CliconfBase):
+ @enable_mode
+ def get_config(self, source="running", flags=None, format=None):
+ if source not in ("running", "startup"):
+ raise ValueError(
+ "fetching configuration from %s is not supported" % source
+ )
+
+ if format:
+ raise ValueError(
+ "'format' value %s is not supported for get_config" % format
+ )
+
+ if not flags:
+ flags = []
+ if source == "running":
+ cmd = "show running-config "
+ else:
+ cmd = "show startup-config "
+
+ cmd += " ".join(to_list(flags))
+ cmd = cmd.strip()
+
+ return self.send_command(cmd)
+
+ def get_diff(
+ self,
+ candidate=None,
+ running=None,
+ diff_match="line",
+ diff_ignore_lines=None,
+ path=None,
+ diff_replace="line",
+ ):
+ """
+ Generate diff between candidate and running configuration. If the
+ remote host supports onbox diff capabilities ie. supports_onbox_diff in that case
+ candidate and running configurations are not required to be passed as argument.
+ In case if onbox diff capability is not supported candidate argument is mandatory
+ and running argument is optional.
+ :param candidate: The configuration which is expected to be present on remote host.
+ :param running: The base configuration which is used to generate diff.
+ :param diff_match: Instructs how to match the candidate configuration with current device configuration
+ Valid values are 'line', 'strict', 'exact', 'none'.
+ 'line' - commands are matched line by line
+ 'strict' - command lines are matched with respect to position
+ 'exact' - command lines must be an equal match
+ 'none' - will not compare the candidate configuration with the running configuration
+ :param diff_ignore_lines: 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.
+ :param path: The ordered set of parents that uniquely identify the section or hierarchy
+ the commands should be checked against. If the parents argument
+ is omitted, the commands are checked against the set of top
+ level or global commands.
+ :param diff_replace: Instructs on the way to perform the configuration on the device.
+ If the replace argument is set to I(line) then the modified lines are
+ pushed to the device in configuration mode. If the replace 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.
+ :return: Configuration diff in json format.
+ {
+ 'config_diff': '',
+ 'banner_diff': {}
+ }
+
+ """
+ diff = {}
+ device_operations = self.get_device_operations()
+ option_values = self.get_option_values()
+
+ if candidate is None and device_operations["supports_generate_diff"]:
+ raise ValueError(
+ "candidate configuration is required to generate diff"
+ )
+
+ if diff_match not in option_values["diff_match"]:
+ raise ValueError(
+ "'match' value %s in invalid, valid values are %s"
+ % (diff_match, ", ".join(option_values["diff_match"]))
+ )
+
+ if diff_replace not in option_values["diff_replace"]:
+ raise ValueError(
+ "'replace' value %s in invalid, valid values are %s"
+ % (diff_replace, ", ".join(option_values["diff_replace"]))
+ )
+
+ # prepare candidate configuration
+ candidate_obj = NetworkConfig(indent=1)
+ want_src, want_banners = self._extract_banners(candidate)
+ candidate_obj.load(want_src)
+
+ if running and diff_match != "none":
+ # running configuration
+ have_src, have_banners = self._extract_banners(running)
+ running_obj = NetworkConfig(
+ indent=1, contents=have_src, ignore_lines=diff_ignore_lines
+ )
+ configdiffobjs = candidate_obj.difference(
+ running_obj, path=path, match=diff_match, replace=diff_replace
+ )
+
+ else:
+ configdiffobjs = candidate_obj.items
+ have_banners = {}
+
+ diff["config_diff"] = (
+ dumps(configdiffobjs, "commands") if configdiffobjs else ""
+ )
+ banners = self._diff_banners(want_banners, have_banners)
+ diff["banner_diff"] = banners if banners else {}
+ return diff
+
+ @enable_mode
+ def edit_config(
+ self, candidate=None, commit=True, replace=None, comment=None
+ ):
+ resp = {}
+ operations = self.get_device_operations()
+ self.check_edit_config_capability(
+ operations, candidate, commit, replace, comment
+ )
+
+ results = []
+ requests = []
+ if commit:
+ self.send_command("configure terminal")
+ for line in to_list(candidate):
+ if not isinstance(line, Mapping):
+ line = {"command": line}
+
+ cmd = line["command"]
+ if cmd != "end" and cmd[0] != "!":
+ results.append(self.send_command(**line))
+ requests.append(cmd)
+
+ self.send_command("end")
+ else:
+ raise ValueError("check mode is not supported")
+
+ resp["request"] = requests
+ resp["response"] = results
+ return resp
+
+ def edit_macro(
+ self, candidate=None, commit=True, replace=None, comment=None
+ ):
+ """
+ ios_config:
+ lines: "{{ macro_lines }}"
+ parents: "macro name {{ macro_name }}"
+ after: '@'
+ match: line
+ replace: block
+ """
+ resp = {}
+ operations = self.get_device_operations()
+ self.check_edit_config_capability(
+ operations, candidate, commit, replace, comment
+ )
+
+ results = []
+ requests = []
+ if commit:
+ commands = ""
+ self.send_command("config terminal")
+ time.sleep(0.1)
+ # first item: macro command
+ commands += candidate.pop(0) + "\n"
+ multiline_delimiter = candidate.pop(-1)
+ for line in candidate:
+ commands += " " + line + "\n"
+ commands += multiline_delimiter + "\n"
+ obj = {"command": commands, "sendonly": True}
+ results.append(self.send_command(**obj))
+ requests.append(commands)
+
+ time.sleep(0.1)
+ self.send_command("end", sendonly=True)
+ time.sleep(0.1)
+ results.append(self.send_command("\n"))
+ requests.append("\n")
+
+ resp["request"] = requests
+ resp["response"] = results
+ return resp
+
+ def get(
+ self,
+ command=None,
+ prompt=None,
+ answer=None,
+ sendonly=False,
+ output=None,
+ newline=True,
+ check_all=False,
+ ):
+ if not command:
+ raise ValueError("must provide value of command to execute")
+ if output:
+ raise ValueError(
+ "'output' value %s is not supported for get" % output
+ )
+
+ return self.send_command(
+ command=command,
+ prompt=prompt,
+ answer=answer,
+ sendonly=sendonly,
+ newline=newline,
+ check_all=check_all,
+ )
+
+ def get_device_info(self):
+ device_info = {}
+
+ device_info["network_os"] = "ios"
+ reply = self.get(command="show version")
+ data = to_text(reply, errors="surrogate_or_strict").strip()
+
+ match = re.search(r"Version (\S+)", data)
+ if match:
+ device_info["network_os_version"] = match.group(1).strip(",")
+
+ model_search_strs = [
+ r"^[Cc]isco (.+) \(revision",
+ r"^[Cc]isco (\S+).+bytes of .*memory",
+ ]
+ for item in model_search_strs:
+ match = re.search(item, data, re.M)
+ if match:
+ version = match.group(1).split(" ")
+ device_info["network_os_model"] = version[0]
+ break
+
+ match = re.search(r"^(.+) uptime", data, re.M)
+ if match:
+ device_info["network_os_hostname"] = match.group(1)
+
+ match = re.search(r'image file is "(.+)"', data)
+ if match:
+ device_info["network_os_image"] = match.group(1)
+
+ return device_info
+
+ def get_device_operations(self):
+ return {
+ "supports_diff_replace": True,
+ "supports_commit": False,
+ "supports_rollback": False,
+ "supports_defaults": True,
+ "supports_onbox_diff": False,
+ "supports_commit_comment": False,
+ "supports_multiline_delimiter": True,
+ "supports_diff_match": True,
+ "supports_diff_ignore_lines": True,
+ "supports_generate_diff": True,
+ "supports_replace": False,
+ }
+
+ def get_option_values(self):
+ return {
+ "format": ["text"],
+ "diff_match": ["line", "strict", "exact", "none"],
+ "diff_replace": ["line", "block"],
+ "output": [],
+ }
+
+ def get_capabilities(self):
+ result = super(Cliconf, self).get_capabilities()
+ result["rpc"] += [
+ "edit_banner",
+ "get_diff",
+ "run_commands",
+ "get_defaults_flag",
+ ]
+ result["device_operations"] = self.get_device_operations()
+ result.update(self.get_option_values())
+ return json.dumps(result)
+
+ def edit_banner(
+ self, candidate=None, multiline_delimiter="@", commit=True
+ ):
+ """
+ Edit banner on remote device
+ :param banners: Banners to be loaded in json format
+ :param multiline_delimiter: Line delimiter for banner
+ :param commit: Boolean value that indicates if the device candidate
+ configuration should be pushed in the running configuration or discarded.
+ :param diff: Boolean flag to indicate if configuration that is applied on remote host should
+ generated and returned in response or not
+ :return: Returns response of executing the configuration command received
+ from remote host
+ """
+ resp = {}
+ banners_obj = json.loads(candidate)
+ results = []
+ requests = []
+ if commit:
+ for key, value in iteritems(banners_obj):
+ key += " %s" % multiline_delimiter
+ self.send_command("config terminal", sendonly=True)
+ for cmd in [key, value, multiline_delimiter]:
+ obj = {"command": cmd, "sendonly": True}
+ results.append(self.send_command(**obj))
+ requests.append(cmd)
+
+ self.send_command("end", sendonly=True)
+ time.sleep(0.1)
+ results.append(self.send_command("\n"))
+ requests.append("\n")
+
+ resp["request"] = requests
+ resp["response"] = results
+
+ return resp
+
+ def run_commands(self, commands=None, check_rc=True):
+ if commands is None:
+ raise ValueError("'commands' value is required")
+
+ responses = list()
+ for cmd in to_list(commands):
+ if not isinstance(cmd, Mapping):
+ cmd = {"command": cmd}
+
+ output = cmd.pop("output", None)
+ if output:
+ raise ValueError(
+ "'output' value %s is not supported for run_commands"
+ % output
+ )
+
+ try:
+ out = self.send_command(**cmd)
+ except AnsibleConnectionFailure as e:
+ if check_rc:
+ raise
+ out = getattr(e, "err", to_text(e))
+
+ responses.append(out)
+
+ return responses
+
+ def get_defaults_flag(self):
+ """
+ The method identifies the filter that should be used to fetch running-configuration
+ with defaults.
+ :return: valid default filter
+ """
+ out = self.get("show running-config ?")
+ out = to_text(out, errors="surrogate_then_replace")
+
+ commands = set()
+ for line in out.splitlines():
+ if line.strip():
+ commands.add(line.strip().split()[0])
+
+ if "all" in commands:
+ return "all"
+ else:
+ return "full"
+
+ def set_cli_prompt_context(self):
+ """
+ Make sure we are in the operational cli mode
+ :return: None
+ """
+ if self._connection.connected:
+ out = self._connection.get_prompt()
+
+ if out is None:
+ raise AnsibleConnectionFailure(
+ message=u"cli prompt is not identified from the last received"
+ u" response window: %s"
+ % self._connection._last_recv_window
+ )
+
+ if re.search(
+ r"config.*\)#",
+ to_text(out, errors="surrogate_then_replace").strip(),
+ ):
+ self._connection.queue_message(
+ "vvvv", "wrong context, sending end to device"
+ )
+ self._connection.send_command("end")
+
+ def _extract_banners(self, config):
+ banners = {}
+ banner_cmds = re.findall(r"^banner (\w+)", config, re.M)
+ for cmd in banner_cmds:
+ regex = r"banner %s \^C(.+?)(?=\^C)" % cmd
+ match = re.search(regex, config, re.S)
+ if match:
+ key = "banner %s" % cmd
+ banners[key] = match.group(1).strip()
+
+ for cmd in banner_cmds:
+ regex = r"banner %s \^C(.+?)(?=\^C)" % cmd
+ match = re.search(regex, config, re.S)
+ if match:
+ config = config.replace(str(match.group(1)), "")
+
+ config = re.sub(r"banner \w+ \^C\^C", "!! banner removed", config)
+ return config, banners
+
+ def _diff_banners(self, want, have):
+ candidate = {}
+ for key, value in iteritems(want):
+ if value != have.get(key):
+ candidate[key] = value
+ return candidate
diff --git a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/doc_fragments/ios.py b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/doc_fragments/ios.py
new file mode 100644
index 0000000..ff22d27
--- /dev/null
+++ b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/doc_fragments/ios.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Peter Sprygada <psprygada@ansible.com>
+# 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:
+ provider:
+ description:
+ - B(Deprecated)
+ - 'Starting with Ansible 2.5 we recommend using C(connection: network_cli).'
+ - For more information please see the L(IOS Platform Options guide, ../network/user_guide/platform_ios.html).
+ - HORIZONTALLINE
+ - A dict object containing connection details.
+ type: dict
+ suboptions:
+ 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.
+ type: int
+ default: 22
+ 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
+ authorize:
+ description:
+ - Instructs the module to enter privileged mode on the remote device before
+ sending any commands. If not specified, the device will attempt to execute
+ all commands in non-privileged mode. If the value is not specified in the
+ task, the value of environment variable C(ANSIBLE_NET_AUTHORIZE) will be
+ used instead.
+ type: bool
+ default: false
+ auth_pass:
+ description:
+ - Specifies the password to use if required to enter privileged mode on the
+ remote device. If I(authorize) is false, then this argument does nothing.
+ If the value is not specified in the task, the value of environment variable
+ C(ANSIBLE_NET_AUTH_PASS) will be used instead.
+ type: str
+notes:
+- For more information on using Ansible to manage network devices see the :ref:`Ansible
+ Network Guide <network_guide>`
+- For more information on using Ansible to manage Cisco devices see the `Cisco integration
+ page <https://www.ansible.com/integrations/networks/cisco>`_.
+"""
diff --git a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/module_utils/network/ios/ios.py b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/module_utils/network/ios/ios.py
new file mode 100644
index 0000000..6818a0c
--- /dev/null
+++ b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/module_utils/network/ios/ios.py
@@ -0,0 +1,197 @@
+# 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 json
+
+from ansible.module_utils._text import to_text
+from ansible.module_utils.basic import env_fallback
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
+ to_list,
+)
+from ansible.module_utils.connection import Connection, ConnectionError
+
+_DEVICE_CONFIGS = {}
+
+ios_provider_spec = {
+ "host": dict(),
+ "port": dict(type="int"),
+ "username": dict(fallback=(env_fallback, ["ANSIBLE_NET_USERNAME"])),
+ "password": dict(
+ fallback=(env_fallback, ["ANSIBLE_NET_PASSWORD"]), no_log=True
+ ),
+ "ssh_keyfile": dict(
+ fallback=(env_fallback, ["ANSIBLE_NET_SSH_KEYFILE"]), type="path"
+ ),
+ "authorize": dict(
+ fallback=(env_fallback, ["ANSIBLE_NET_AUTHORIZE"]), type="bool"
+ ),
+ "auth_pass": dict(
+ fallback=(env_fallback, ["ANSIBLE_NET_AUTH_PASS"]), no_log=True
+ ),
+ "timeout": dict(type="int"),
+}
+ios_argument_spec = {
+ "provider": dict(
+ type="dict", options=ios_provider_spec, removed_in_version=2.14
+ )
+}
+
+
+def get_provider_argspec():
+ return ios_provider_spec
+
+
+def get_connection(module):
+ if hasattr(module, "_ios_connection"):
+ return module._ios_connection
+
+ capabilities = get_capabilities(module)
+ network_api = capabilities.get("network_api")
+ if network_api == "cliconf":
+ module._ios_connection = Connection(module._socket_path)
+ else:
+ module.fail_json(msg="Invalid connection type %s" % network_api)
+
+ return module._ios_connection
+
+
+def get_capabilities(module):
+ if hasattr(module, "_ios_capabilities"):
+ return module._ios_capabilities
+ try:
+ capabilities = Connection(module._socket_path).get_capabilities()
+ except ConnectionError as exc:
+ module.fail_json(msg=to_text(exc, errors="surrogate_then_replace"))
+ module._ios_capabilities = json.loads(capabilities)
+ return module._ios_capabilities
+
+
+def get_defaults_flag(module):
+ connection = get_connection(module)
+ try:
+ out = connection.get_defaults_flag()
+ except ConnectionError as exc:
+ module.fail_json(msg=to_text(exc, errors="surrogate_then_replace"))
+ return to_text(out, errors="surrogate_then_replace").strip()
+
+
+def get_config(module, flags=None):
+ flags = to_list(flags)
+
+ section_filter = False
+ if flags and "section" in flags[-1]:
+ section_filter = True
+
+ flag_str = " ".join(flags)
+
+ try:
+ return _DEVICE_CONFIGS[flag_str]
+ except KeyError:
+ connection = get_connection(module)
+ try:
+ out = connection.get_config(flags=flags)
+ except ConnectionError as exc:
+ if section_filter:
+ # Some ios devices don't understand `| section foo`
+ out = get_config(module, flags=flags[:-1])
+ else:
+ module.fail_json(
+ msg=to_text(exc, errors="surrogate_then_replace")
+ )
+ cfg = to_text(out, errors="surrogate_then_replace").strip()
+ _DEVICE_CONFIGS[flag_str] = cfg
+ return cfg
+
+
+def run_commands(module, commands, check_rc=True):
+ connection = get_connection(module)
+ try:
+ return connection.run_commands(commands=commands, check_rc=check_rc)
+ except ConnectionError as exc:
+ module.fail_json(msg=to_text(exc))
+
+
+def load_config(module, commands):
+ connection = get_connection(module)
+
+ try:
+ resp = connection.edit_config(commands)
+ return resp.get("response")
+ except ConnectionError as exc:
+ module.fail_json(msg=to_text(exc))
+
+
+def normalize_interface(name):
+ """Return the normalized interface name
+ """
+ if not name:
+ return
+
+ def _get_number(name):
+ digits = ""
+ for char in name:
+ if char.isdigit() or char in "/.":
+ digits += char
+ return digits
+
+ if name.lower().startswith("gi"):
+ if_type = "GigabitEthernet"
+ elif name.lower().startswith("te"):
+ if_type = "TenGigabitEthernet"
+ elif name.lower().startswith("fa"):
+ if_type = "FastEthernet"
+ elif name.lower().startswith("fo"):
+ if_type = "FortyGigabitEthernet"
+ elif name.lower().startswith("et"):
+ if_type = "Ethernet"
+ elif name.lower().startswith("vl"):
+ if_type = "Vlan"
+ elif name.lower().startswith("lo"):
+ if_type = "loopback"
+ elif name.lower().startswith("po"):
+ if_type = "port-channel"
+ elif name.lower().startswith("nv"):
+ if_type = "nve"
+ elif name.lower().startswith("twe"):
+ if_type = "TwentyFiveGigE"
+ elif name.lower().startswith("hu"):
+ if_type = "HundredGigE"
+ else:
+ if_type = None
+
+ number_list = name.split(" ")
+ if len(number_list) == 2:
+ if_number = number_list[-1].strip()
+ else:
+ if_number = _get_number(name)
+
+ if if_type:
+ proper_interface = if_type + if_number
+ else:
+ proper_interface = name
+
+ return proper_interface
diff --git a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_command.py b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_command.py
new file mode 100644
index 0000000..ef383fc
--- /dev/null
+++ b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_command.py
@@ -0,0 +1,229 @@
+#!/usr/bin/python
+#
+# 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/>.
+#
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "network",
+}
+
+
+DOCUMENTATION = """module: ios_command
+author: Peter Sprygada (@privateip)
+short_description: Run commands on remote devices running Cisco IOS
+description:
+- Sends arbitrary commands to an ios node and returns the results read from the device.
+ This module includes an argument that will cause the module to wait for a specific
+ condition before returning or timing out if the condition is not met.
+- This module does not support running commands in configuration mode. Please use
+ M(ios_config) to configure IOS devices.
+extends_documentation_fragment:
+- cisco.ios.ios
+notes:
+- Tested against IOS 15.6
+options:
+ commands:
+ description:
+ - List of commands to send to the remote ios device over the configured provider.
+ The resulting output from the command is returned. If the I(wait_for) argument
+ is provided, the module is not returned until the condition is satisfied or
+ the number of retries has expired. If a command sent to the device requires
+ answering a prompt, it is possible to pass a dict containing I(command), I(answer)
+ and I(prompt). Common answers are 'y' or "\r" (carriage return, must be double
+ quotes). See examples.
+ required: true
+ wait_for:
+ description:
+ - List of conditions to evaluate against the output of the command. The task will
+ wait for each condition to be true before moving forward. If the conditional
+ is not true within the configured number of retries, the task fails. See examples.
+ aliases:
+ - waitfor
+ match:
+ description:
+ - The I(match) argument is used in conjunction with the I(wait_for) argument to
+ specify the match policy. Valid values are C(all) or C(any). If the value
+ is set to C(all) then all conditionals in the wait_for must be satisfied. If
+ the value is set to C(any) then only one of the values must be satisfied.
+ default: all
+ choices:
+ - any
+ - all
+ retries:
+ description:
+ - Specifies the number of retries a command should by tried before it is considered
+ failed. The command is run on the target device every retry and evaluated against
+ the I(wait_for) conditions.
+ default: 10
+ interval:
+ description:
+ - Configures the interval in seconds to wait between retries of the command. If
+ the command does not pass the specified conditions, the interval indicates how
+ long to wait before trying the command again.
+ default: 1
+"""
+
+EXAMPLES = r"""
+tasks:
+ - name: run show version on remote devices
+ ios_command:
+ commands: show version
+
+ - name: run show version and check to see if output contains IOS
+ ios_command:
+ commands: show version
+ wait_for: result[0] contains IOS
+
+ - name: run multiple commands on remote nodes
+ ios_command:
+ commands:
+ - show version
+ - show interfaces
+
+ - name: run multiple commands and evaluate the output
+ ios_command:
+ commands:
+ - show version
+ - show interfaces
+ wait_for:
+ - result[0] contains IOS
+ - result[1] contains Loopback0
+
+ - name: run commands that require answering a prompt
+ ios_command:
+ commands:
+ - command: 'clear counters GigabitEthernet0/1'
+ prompt: 'Clear "show interface" counters on this interface \[confirm\]'
+ answer: 'y'
+ - command: 'clear counters GigabitEthernet0/2'
+ prompt: '[confirm]'
+ answer: "\r"
+"""
+
+RETURN = """
+stdout:
+ description: The set of responses from the commands
+ returned: always apart from low level errors (such as action plugin)
+ type: list
+ 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: [['...', '...'], ['...'], ['...']]
+failed_conditions:
+ description: The list of conditionals that have failed
+ returned: failed
+ type: list
+ sample: ['...', '...']
+"""
+import time
+
+from ansible.module_utils._text import to_text
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.parsing import (
+ Conditional,
+)
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
+ transform_commands,
+ to_lines,
+)
+from ansible_collections.cisco.ios.plugins.module_utils.network.ios.ios import (
+ run_commands,
+)
+from ansible_collections.cisco.ios.plugins.module_utils.network.ios.ios import (
+ ios_argument_spec,
+)
+
+
+def parse_commands(module, warnings):
+ commands = transform_commands(module)
+
+ if module.check_mode:
+ for item in list(commands):
+ if not item["command"].startswith("show"):
+ warnings.append(
+ "Only show commands are supported when using check mode, not "
+ "executing %s" % item["command"]
+ )
+ commands.remove(item)
+
+ return commands
+
+
+def main():
+ """main entry point for module execution
+ """
+ argument_spec = dict(
+ commands=dict(type="list", required=True),
+ wait_for=dict(type="list", aliases=["waitfor"]),
+ match=dict(default="all", choices=["all", "any"]),
+ retries=dict(default=10, type="int"),
+ interval=dict(default=1, type="int"),
+ )
+
+ argument_spec.update(ios_argument_spec)
+
+ module = AnsibleModule(
+ argument_spec=argument_spec, supports_check_mode=True
+ )
+
+ warnings = list()
+ result = {"changed": False, "warnings": warnings}
+ commands = parse_commands(module, warnings)
+ wait_for = module.params["wait_for"] or list()
+
+ try:
+ conditionals = [Conditional(c) for c in wait_for]
+ except AttributeError as exc:
+ module.fail_json(msg=to_text(exc))
+
+ retries = module.params["retries"]
+ interval = module.params["interval"]
+ match = module.params["match"]
+
+ while retries > 0:
+ responses = run_commands(module, commands)
+
+ for item in list(conditionals):
+ if item(responses):
+ if match == "any":
+ conditionals = list()
+ break
+ conditionals.remove(item)
+
+ if not conditionals:
+ break
+
+ time.sleep(interval)
+ retries -= 1
+
+ if conditionals:
+ failed_conditions = [item.raw for item in conditionals]
+ msg = "One or more conditional statements have not been satisfied"
+ module.fail_json(msg=msg, failed_conditions=failed_conditions)
+
+ result.update(
+ {"stdout": responses, "stdout_lines": list(to_lines(responses))}
+ )
+
+ module.exit_json(**result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py
new file mode 100644
index 0000000..beec5b8
--- /dev/null
+++ b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py
@@ -0,0 +1,596 @@
+#!/usr/bin/python
+#
+# 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/>.
+#
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "network",
+}
+
+
+DOCUMENTATION = """module: ios_config
+author: Peter Sprygada (@privateip)
+short_description: Manage Cisco IOS configuration sections
+description:
+- Cisco IOS configurations use a simple block indent file syntax for segmenting configuration
+ into sections. This module provides an implementation for working with IOS configuration
+ sections in a deterministic way.
+extends_documentation_fragment:
+- cisco.ios.ios
+notes:
+- Tested against IOS 15.6
+- Abbreviated commands are NOT idempotent, see L(Network FAQ,../network/user_guide/faq.html#why-do-the-config-modules-always-return-changed-true-with-abbreviated-commands).
+options:
+ lines:
+ description:
+ - The ordered set of commands that should be configured in the section. The commands
+ must be the exact same commands as found in the device running-config. Be sure
+ to note the configuration command syntax as some commands are automatically
+ modified by the device config parser.
+ aliases:
+ - commands
+ parents:
+ description:
+ - The ordered set of parents that uniquely identify the section or hierarchy the
+ commands should be checked against. If the parents argument is omitted, the
+ commands are checked against the set of top level or global commands.
+ src:
+ description:
+ - Specifies the source path to the 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(lines), I(parents).
+ before:
+ description:
+ - The ordered set of commands to push on to the command stack if a change needs
+ to be made. This allows the playbook designer the opportunity to perform configuration
+ commands prior to pushing any changes without affecting how the set of commands
+ are matched against the system.
+ after:
+ description:
+ - The ordered set of commands to append to the end of the command stack if a change
+ needs to be made. Just like with I(before) this allows the playbook designer
+ to append a set of commands to be executed after the command set.
+ match:
+ description:
+ - Instructs the module on the way to perform the matching of the set of commands
+ against the current device config. If match is set to I(line), commands are
+ matched line by line. If match is set to I(strict), command lines are matched
+ with respect to position. If match is set to I(exact), command lines must be
+ an equal match. Finally, if match is set to I(none), the module will not attempt
+ to compare the source configuration with the running configuration on the remote
+ device.
+ choices:
+ - line
+ - strict
+ - exact
+ - none
+ default: line
+ replace:
+ description:
+ - Instructs the module on the way to perform the configuration on the device.
+ If the replace argument is set to I(line) then the modified lines are pushed
+ to the device in configuration mode. If the replace 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.
+ default: line
+ choices:
+ - line
+ - block
+ multiline_delimiter:
+ description:
+ - This argument is used when pushing a multiline configuration element to the
+ IOS device. It specifies the character to use as the delimiting character. This
+ only applies to the configuration action.
+ default: '@'
+ 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'
+ running_config:
+ description:
+ - The module, by default, will connect to the remote device and retrieve the current
+ running-config to use as a base for comparing against the contents of source.
+ There are times when it is not desirable to have the task get the current running-config
+ for every task in a playbook. The I(running_config) argument allows the implementer
+ to pass in the configuration to use as the base config for comparison.
+ aliases:
+ - config
+ defaults:
+ description:
+ - This argument specifies whether or not to collect all defaults when getting
+ the remote device running config. When enabled, the module will get the current
+ config by issuing the command C(show running-config all).
+ type: bool
+ default: 'no'
+ save_when:
+ description:
+ - When changes are made to the device running-configuration, the changes are not
+ copied to non-volatile storage by default. Using this argument will change
+ that before. If the argument is set to I(always), then the running-config will
+ always be copied to the startup-config and the I(modified) flag will always
+ be set to True. If the argument is set to I(modified), then the running-config
+ will only be copied to the startup-config if it has changed since the last save
+ to startup-config. If the argument is set to I(never), the running-config will
+ never be copied to the startup-config. If the argument is set to I(changed),
+ then the running-config will only be copied to the startup-config if the task
+ has made a change. I(changed) was added in Ansible 2.5.
+ default: never
+ choices:
+ - always
+ - never
+ - modified
+ - changed
+ diff_against:
+ description:
+ - When using the C(ansible-playbook --diff) command line argument the module can
+ generate diffs against different sources.
+ - When this option is configure as I(startup), the module will return the diff
+ of the running-config against the startup-config.
+ - When this option is configured as I(intended), the module will return the diff
+ of the running-config against the configuration provided in the C(intended_config)
+ argument.
+ - When this option is configured as I(running), the module will return the before
+ and after diff of the running-config with respect to any changes made to the
+ device configuration.
+ choices:
+ - running
+ - startup
+ - intended
+ 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.
+ intended_config:
+ description:
+ - The C(intended_config) provides the master configuration that the node should
+ conform to and is used to check the final running-config against. This argument
+ will not modify any settings on the remote device and is strictly used to check
+ the compliance of the current device's configuration against. When specifying
+ this argument, the task should also modify the C(diff_against) value and set
+ it to I(intended).
+ 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
+"""
+
+EXAMPLES = """
+- name: configure top level configuration
+ ios_config:
+ lines: hostname {{ inventory_hostname }}
+
+- name: configure interface settings
+ ios_config:
+ lines:
+ - description test interface
+ - ip address 172.31.1.1 255.255.255.0
+ parents: interface Ethernet1
+
+- name: configure ip helpers on multiple interfaces
+ ios_config:
+ lines:
+ - ip helper-address 172.26.1.10
+ - ip helper-address 172.26.3.8
+ parents: "{{ item }}"
+ with_items:
+ - interface Ethernet1
+ - interface Ethernet2
+ - interface GigabitEthernet1
+
+- name: configure policer in Scavenger class
+ ios_config:
+ lines:
+ - conform-action transmit
+ - exceed-action drop
+ parents:
+ - policy-map Foo
+ - class Scavenger
+ - police cir 64000
+
+- name: load new acl into device
+ ios_config:
+ lines:
+ - 10 permit ip host 192.0.2.1 any log
+ - 20 permit ip host 192.0.2.2 any log
+ - 30 permit ip host 192.0.2.3 any log
+ - 40 permit ip host 192.0.2.4 any log
+ - 50 permit ip host 192.0.2.5 any log
+ parents: ip access-list extended test
+ before: no ip access-list extended test
+ match: exact
+
+- name: check the running-config against master config
+ ios_config:
+ diff_against: intended
+ intended_config: "{{ lookup('file', 'master.cfg') }}"
+
+- name: check the startup-config against the running-config
+ ios_config:
+ diff_against: startup
+ diff_ignore_lines:
+ - ntp clock .*
+
+- name: save running to startup when modified
+ ios_config:
+ save_when: modified
+
+- name: for idempotency, use full-form commands
+ ios_config:
+ lines:
+ # - shut
+ - shutdown
+ # parents: int gig1/0/11
+ parents: interface GigabitEthernet1/0/11
+
+# Set boot image based on comparison to a group_var (version) and the version
+# that is returned from the `ios_facts` module
+- name: SETTING BOOT IMAGE
+ ios_config:
+ lines:
+ - no boot system
+ - boot system flash bootflash:{{new_image}}
+ host: "{{ inventory_hostname }}"
+ when: ansible_net_version != version
+
+- name: render a Jinja2 template onto an IOS device
+ ios_config:
+ backup: yes
+ src: ios_template.j2
+
+- name: configurable backup path
+ ios_config:
+ src: ios_template.j2
+ backup: yes
+ backup_options:
+ filename: backup.cfg
+ dir_path: /home/user
+"""
+
+RETURN = """
+updates:
+ description: The set of commands that will be pushed to the remote device
+ returned: always
+ type: list
+ sample: ['hostname foo', 'router ospf 1', 'router-id 192.0.2.1']
+commands:
+ description: The set of commands that will be pushed to the remote device
+ returned: always
+ type: list
+ sample: ['hostname foo', 'router ospf 1', 'router-id 192.0.2.1']
+backup_path:
+ description: The full path to the backup file
+ returned: when backup is yes
+ type: str
+ sample: /playbooks/ansible/backup/ios_config.2016-07-16@22:28:34
+filename:
+ description: The name of the backup file
+ returned: when backup is yes and filename is not specified in backup options
+ type: str
+ sample: ios_config.2016-07-16@22:28:34
+shortname:
+ description: The full path to the backup file excluding the timestamp
+ returned: when backup is yes and filename is not specified in backup options
+ type: str
+ sample: /playbooks/ansible/backup/ios_config
+date:
+ description: The date extracted from the backup file name
+ returned: when backup is yes
+ type: str
+ sample: "2016-07-16"
+time:
+ description: The time extracted from the backup file name
+ returned: when backup is yes
+ type: str
+ sample: "22:28:34"
+"""
+import json
+
+from ansible.module_utils._text import to_text
+from ansible.module_utils.connection import ConnectionError
+from ansible_collections.cisco.ios.plugins.module_utils.network.ios.ios import (
+ run_commands,
+ get_config,
+)
+from ansible_collections.cisco.ios.plugins.module_utils.network.ios.ios import (
+ get_defaults_flag,
+ get_connection,
+)
+from ansible_collections.cisco.ios.plugins.module_utils.network.ios.ios import (
+ ios_argument_spec,
+)
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import (
+ NetworkConfig,
+ dumps,
+)
+
+
+def check_args(module, warnings):
+ if module.params["multiline_delimiter"]:
+ if len(module.params["multiline_delimiter"]) != 1:
+ module.fail_json(
+ msg="multiline_delimiter value can only be a "
+ "single character"
+ )
+
+
+def edit_config_or_macro(connection, commands):
+ # only catch the macro configuration command,
+ # not negated 'no' variation.
+ if commands[0].startswith("macro name"):
+ connection.edit_macro(candidate=commands)
+ else:
+ connection.edit_config(candidate=commands)
+
+
+def get_candidate_config(module):
+ candidate = ""
+ if module.params["src"]:
+ candidate = module.params["src"]
+
+ elif module.params["lines"]:
+ candidate_obj = NetworkConfig(indent=1)
+ parents = module.params["parents"] or list()
+ candidate_obj.add(module.params["lines"], parents=parents)
+ candidate = dumps(candidate_obj, "raw")
+
+ return candidate
+
+
+def get_running_config(module, current_config=None, flags=None):
+ running = module.params["running_config"]
+ if not running:
+ if not module.params["defaults"] and current_config:
+ running = current_config
+ else:
+ running = get_config(module, flags=flags)
+
+ return running
+
+
+def save_config(module, result):
+ result["changed"] = True
+ if not module.check_mode:
+ run_commands(module, "copy running-config startup-config\r")
+ else:
+ module.warn(
+ "Skipping command `copy running-config startup-config` "
+ "due to check_mode. Configuration not copied to "
+ "non-volatile storage"
+ )
+
+
+def main():
+ """ main entry point for module execution
+ """
+ backup_spec = dict(filename=dict(), dir_path=dict(type="path"))
+ argument_spec = dict(
+ src=dict(type="path"),
+ lines=dict(aliases=["commands"], type="list"),
+ parents=dict(type="list"),
+ before=dict(type="list"),
+ after=dict(type="list"),
+ match=dict(
+ default="line", choices=["line", "strict", "exact", "none"]
+ ),
+ replace=dict(default="line", choices=["line", "block"]),
+ multiline_delimiter=dict(default="@"),
+ running_config=dict(aliases=["config"]),
+ intended_config=dict(),
+ defaults=dict(type="bool", default=False),
+ backup=dict(type="bool", default=False),
+ backup_options=dict(type="dict", options=backup_spec),
+ save_when=dict(
+ choices=["always", "never", "modified", "changed"], default="never"
+ ),
+ diff_against=dict(choices=["startup", "intended", "running"]),
+ diff_ignore_lines=dict(type="list"),
+ )
+
+ argument_spec.update(ios_argument_spec)
+
+ mutually_exclusive = [("lines", "src"), ("parents", "src")]
+
+ required_if = [
+ ("match", "strict", ["lines"]),
+ ("match", "exact", ["lines"]),
+ ("replace", "block", ["lines"]),
+ ("diff_against", "intended", ["intended_config"]),
+ ]
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ mutually_exclusive=mutually_exclusive,
+ required_if=required_if,
+ supports_check_mode=True,
+ )
+
+ result = {"changed": False}
+
+ warnings = list()
+ check_args(module, warnings)
+ result["warnings"] = warnings
+
+ diff_ignore_lines = module.params["diff_ignore_lines"]
+ config = None
+ contents = None
+ flags = get_defaults_flag(module) if module.params["defaults"] else []
+ connection = get_connection(module)
+
+ if module.params["backup"] or (
+ module._diff and module.params["diff_against"] == "running"
+ ):
+ contents = get_config(module, flags=flags)
+ config = NetworkConfig(indent=1, contents=contents)
+ if module.params["backup"]:
+ result["__backup__"] = contents
+
+ if any((module.params["lines"], module.params["src"])):
+ match = module.params["match"]
+ replace = module.params["replace"]
+ path = module.params["parents"]
+
+ candidate = get_candidate_config(module)
+ running = get_running_config(module, contents, flags=flags)
+ try:
+ response = connection.get_diff(
+ candidate=candidate,
+ running=running,
+ diff_match=match,
+ diff_ignore_lines=diff_ignore_lines,
+ path=path,
+ diff_replace=replace,
+ )
+ except ConnectionError as exc:
+ module.fail_json(msg=to_text(exc, errors="surrogate_then_replace"))
+
+ config_diff = response["config_diff"]
+ banner_diff = response["banner_diff"]
+
+ if config_diff or banner_diff:
+ commands = config_diff.split("\n")
+
+ if module.params["before"]:
+ commands[:0] = module.params["before"]
+
+ if module.params["after"]:
+ commands.extend(module.params["after"])
+
+ result["commands"] = commands
+ result["updates"] = commands
+ result["banners"] = banner_diff
+
+ # send the configuration commands to the device and merge
+ # them with the current running config
+ if not module.check_mode:
+ if commands:
+ edit_config_or_macro(connection, commands)
+ if banner_diff:
+ connection.edit_banner(
+ candidate=json.dumps(banner_diff),
+ multiline_delimiter=module.params[
+ "multiline_delimiter"
+ ],
+ )
+
+ result["changed"] = True
+
+ running_config = module.params["running_config"]
+ startup_config = None
+
+ if module.params["save_when"] == "always":
+ save_config(module, result)
+ elif module.params["save_when"] == "modified":
+ output = run_commands(
+ module, ["show running-config", "show startup-config"]
+ )
+
+ running_config = NetworkConfig(
+ indent=1, contents=output[0], ignore_lines=diff_ignore_lines
+ )
+ startup_config = NetworkConfig(
+ indent=1, contents=output[1], ignore_lines=diff_ignore_lines
+ )
+
+ if running_config.sha1 != startup_config.sha1:
+ save_config(module, result)
+ elif module.params["save_when"] == "changed" and result["changed"]:
+ save_config(module, result)
+
+ if module._diff:
+ if not running_config:
+ output = run_commands(module, "show running-config")
+ contents = output[0]
+ else:
+ contents = running_config
+
+ # recreate the object in order to process diff_ignore_lines
+ running_config = NetworkConfig(
+ indent=1, contents=contents, ignore_lines=diff_ignore_lines
+ )
+
+ if module.params["diff_against"] == "running":
+ if module.check_mode:
+ module.warn(
+ "unable to perform diff against running-config due to check mode"
+ )
+ contents = None
+ else:
+ contents = config.config_text
+
+ elif module.params["diff_against"] == "startup":
+ if not startup_config:
+ output = run_commands(module, "show startup-config")
+ contents = output[0]
+ else:
+ contents = startup_config.config_text
+
+ elif module.params["diff_against"] == "intended":
+ contents = module.params["intended_config"]
+
+ if contents is not None:
+ base_config = NetworkConfig(
+ indent=1, contents=contents, ignore_lines=diff_ignore_lines
+ )
+
+ if running_config.sha1 != base_config.sha1:
+ if module.params["diff_against"] == "intended":
+ before = running_config
+ after = base_config
+ elif module.params["diff_against"] in ("startup", "running"):
+ before = base_config
+ after = running_config
+
+ result.update(
+ {
+ "changed": True,
+ "diff": {"before": str(before), "after": str(after)},
+ }
+ )
+
+ module.exit_json(**result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/terminal/ios.py b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/terminal/ios.py
new file mode 100644
index 0000000..29f31b0
--- /dev/null
+++ b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/terminal/ios.py
@@ -0,0 +1,115 @@
+#
+# (c) 2016 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 json
+import re
+
+from ansible.errors import AnsibleConnectionFailure
+from ansible.module_utils._text import to_text, to_bytes
+from ansible.plugins.terminal import TerminalBase
+from ansible.utils.display import Display
+
+display = Display()
+
+
+class TerminalModule(TerminalBase):
+
+ terminal_stdout_re = [
+ re.compile(br"[\r\n]?[\w\+\-\.:\/\[\]]+(?:\([^\)]+\)){0,3}(?:[>#]) ?$")
+ ]
+
+ terminal_stderr_re = [
+ re.compile(br"% ?Error"),
+ # re.compile(br"^% \w+", re.M),
+ re.compile(br"% ?Bad secret"),
+ re.compile(br"[\r\n%] Bad passwords"),
+ re.compile(br"invalid input", re.I),
+ re.compile(br"(?:incomplete|ambiguous) command", re.I),
+ re.compile(br"connection timed out", re.I),
+ re.compile(br"[^\r\n]+ not found"),
+ re.compile(br"'[^']' +returned error code: ?\d+"),
+ re.compile(br"Bad mask", re.I),
+ re.compile(br"% ?(\S+) ?overlaps with ?(\S+)", re.I),
+ re.compile(br"[%\S] ?Error: ?[\s]+", re.I),
+ re.compile(br"[%\S] ?Informational: ?[\s]+", re.I),
+ re.compile(br"Command authorization failed"),
+ ]
+
+ def on_open_shell(self):
+ try:
+ self._exec_cli_command(b"terminal length 0")
+ except AnsibleConnectionFailure:
+ raise AnsibleConnectionFailure("unable to set terminal parameters")
+
+ try:
+ self._exec_cli_command(b"terminal width 512")
+ try:
+ self._exec_cli_command(b"terminal width 0")
+ except AnsibleConnectionFailure:
+ pass
+ except AnsibleConnectionFailure:
+ display.display(
+ "WARNING: Unable to set terminal width, command responses may be truncated"
+ )
+
+ def on_become(self, passwd=None):
+ if self._get_prompt().endswith(b"#"):
+ return
+
+ cmd = {u"command": u"enable"}
+ if passwd:
+ # Note: python-3.5 cannot combine u"" and r"" together. Thus make
+ # an r string and use to_text to ensure it's text on both py2 and py3.
+ cmd[u"prompt"] = to_text(
+ r"[\r\n]?(?:.*)?[Pp]assword: ?$", errors="surrogate_or_strict"
+ )
+ cmd[u"answer"] = passwd
+ cmd[u"prompt_retry_check"] = True
+ try:
+ self._exec_cli_command(
+ to_bytes(json.dumps(cmd), errors="surrogate_or_strict")
+ )
+ prompt = self._get_prompt()
+ if prompt is None or not prompt.endswith(b"#"):
+ raise AnsibleConnectionFailure(
+ "failed to elevate privilege to enable mode still at prompt [%s]"
+ % prompt
+ )
+ except AnsibleConnectionFailure as e:
+ prompt = self._get_prompt()
+ raise AnsibleConnectionFailure(
+ "unable to elevate privilege to enable mode, at prompt [%s] with error: %s"
+ % (prompt, e.message)
+ )
+
+ def on_unbecome(self):
+ prompt = self._get_prompt()
+ if prompt is None:
+ # if prompt is None most likely the terminal is hung up at a prompt
+ return
+
+ if b"(config" in prompt:
+ self._exec_cli_command(b"end")
+ self._exec_cli_command(b"disable")
+
+ elif prompt.endswith(b"#"):
+ self._exec_cli_command(b"disable")