diff --git a/README.md b/README.md
index 8d8456157..35c71fce3 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,7 @@ Name | Description
[cisco.ios.ios_lacp](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_lacp_module.rst)|Resource module to configure LACP.
[cisco.ios.ios_lacp_interfaces](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_lacp_interfaces_module.rst)|Resource module to configure LACP interfaces.
[cisco.ios.ios_lag_interfaces](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_lag_interfaces_module.rst)|Resource module to configure LAG interfaces.
+[cisco.ios.ios_line](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_line_module.rst)|Resource module to configure line
[cisco.ios.ios_linkagg](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_linkagg_module.rst)|Module to configure link aggregation groups.
[cisco.ios.ios_lldp](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_lldp_module.rst)|(deprecated, removed after 2024-06-01) Manage LLDP configuration on Cisco IOS network devices.
[cisco.ios.ios_lldp_global](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_lldp_global_module.rst)|Resource module to configure LLDP.
diff --git a/changelogs/fragments/new_ios_line.yml b/changelogs/fragments/new_ios_line.yml
new file mode 100644
index 000000000..2954ee29d
--- /dev/null
+++ b/changelogs/fragments/new_ios_line.yml
@@ -0,0 +1,3 @@
+---
+minor_changes:
+ - ios_line - Add a new module to manage the line console and line vty on Cisco IOS
diff --git a/docs/cisco.ios.ios_facts_module.rst b/docs/cisco.ios.ios_facts_module.rst
index 39b210f19..5abc4a2b5 100644
--- a/docs/cisco.ios.ios_facts_module.rst
+++ b/docs/cisco.ios.ios_facts_module.rst
@@ -65,7 +65,7 @@ Parameters
|
- When supplied, this argument will restrict the facts collected to a given subset. Possible values for this argument include all and the resources like interfaces, vlans etc. Can specify a list of values to include a larger subset. Values can also be used with an initial ! to specify that a specific subset should not be collected. Valid subsets are 'bgp_global', 'l3_interfaces', 'lag_interfaces', 'ntp_global', 'acls', 'hostname', 'interfaces', 'lldp_interfaces', 'logging_global', 'ospf_interfaces', 'ospfv2', 'prefix_lists', 'static_routes', 'acl_interfaces', 'all', 'bgp_address_family', 'l2_interfaces', 'lacp', 'lacp_interfaces', 'lldp_global', 'ospfv3', 'snmp_server', 'vlans', 'service'.
+ When supplied, this argument will restrict the facts collected to a given subset. Possible values for this argument include all and the resources like interfaces, vlans etc. Can specify a list of values to include a larger subset. Values can also be used with an initial ! to specify that a specific subset should not be collected. Valid subsets are 'bgp_global', 'l3_interfaces', 'lag_interfaces', 'ntp_global', 'acls', 'hostname', 'interfaces', 'lldp_interfaces', 'logging_global', 'ospf_interfaces', 'ospfv2', 'prefix_lists', 'static_routes', 'acl_interfaces', 'all', 'bgp_address_family', 'l2_interfaces', 'lacp', 'lacp_interfaces', 'lldp_global', 'ospfv3', 'snmp_server', 'vlans', 'service', 'line'.
|
diff --git a/meta/runtime.yml b/meta/runtime.yml
index c7d437cb8..b36396021 100644
--- a/meta/runtime.yml
+++ b/meta/runtime.yml
@@ -41,6 +41,8 @@ plugin_routing:
redirect: cisco.ios.ios_lacp_interfaces
lag_interfaces:
redirect: cisco.ios.ios_lag_interfaces
+ line:
+ redirect: cisco.ios.ios_line
linkagg:
deprecation:
removal_date: "2024-06-01"
diff --git a/plugins/action/line.py b/plugins/action/line.py
new file mode 120000
index 000000000..7747aa9dd
--- /dev/null
+++ b/plugins/action/line.py
@@ -0,0 +1 @@
+ios.py
\ No newline at end of file
diff --git a/plugins/module_utils/network/ios/argspec/line/__init__.py b/plugins/module_utils/network/ios/argspec/line/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/plugins/module_utils/network/ios/argspec/line/line.py b/plugins/module_utils/network/ios/argspec/line/line.py
new file mode 100644
index 000000000..ba50b7d6c
--- /dev/null
+++ b/plugins/module_utils/network/ios/argspec/line/line.py
@@ -0,0 +1,229 @@
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+#############################################
+# WARNING #
+#############################################
+#
+# This file is auto generated by the
+# ansible.content_builder.
+#
+# Manually editing this file is not advised.
+#
+# To update the argspec make the desired changes
+# in the documentation in the module file and re-run
+# ansible.content_builder commenting out
+# the path to external 'docstring' in build.yaml.
+#
+##############################################
+
+"""
+The arg spec for the ios_line module
+"""
+
+
+class LineArgs(object): # pylint: disable=R0903
+ """The arg spec for the ios_line module"""
+
+ argument_spec = {
+ "config": {
+ "type": "dict",
+ "options": {
+ "lines": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "access_classes_in": {
+ "type": "dict",
+ "options": {
+ "name": {"type": "str"},
+ "vrf_also": {"type": "bool"},
+ "vrfname": {"type": "str"},
+ },
+ },
+ "access_classes_out": {"type": "str"},
+ "accounting": {
+ "type": "dict",
+ "options": {
+ "arap": {"type": "str", "default": "default"},
+ "commands": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "level": {"type": "int"},
+ "command": {
+ "type": "str",
+ "default": "default",
+ },
+ },
+ },
+ "connection": {
+ "type": "str",
+ "default": "default",
+ },
+ "exec": {"type": "str", "default": "default"},
+ "resource": {"type": "str", "default": "default"},
+ },
+ },
+ "authorization": {
+ "type": "dict",
+ "options": {
+ "arap": {"type": "str", "default": "default"},
+ "commands": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "level": {"type": "int"},
+ "command": {
+ "type": "str",
+ "default": "default",
+ },
+ },
+ },
+ "exec": {"type": "str", "default": "default"},
+ "reverse_access": {
+ "type": "str",
+ "default": "default",
+ },
+ },
+ },
+ "escape_character": {
+ "type": "dict",
+ "options": {
+ "soft": {"type": "bool"},
+ "value": {"type": "str"},
+ },
+ },
+ "exec": {
+ "type": "dict",
+ "options": {
+ "banner": {"type": "bool"},
+ "character_bits": {
+ "type": "int",
+ "choices": [7, 8],
+ },
+ "prompt": {
+ "type": "dict",
+ "options": {
+ "expand": {"type": "bool"},
+ "timestamp": {"type": "bool"},
+ },
+ },
+ "timeout": {"type": "int"},
+ },
+ },
+ "length": {"type": "int"},
+ "location": {"type": "str"},
+ "logging": {
+ "type": "dict",
+ "options": {
+ "enable": {"type": "bool"},
+ "level": {
+ "type": "str",
+ "choices": [
+ "0",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "all",
+ ],
+ },
+ "limit": {"type": "int"},
+ },
+ },
+ "login": {"type": "str", "default": "default"},
+ "logout_warning": {"type": "int"},
+ "motd": {"type": "bool", "default": True},
+ "name": {"type": "str", "required": True},
+ "notify": {"type": "bool"},
+ "padding": {"type": "str"},
+ "parity": {
+ "type": "str",
+ "choices": ["even", "mark", "none", "odd", "space"],
+ },
+ "password": {
+ "type": "dict",
+ "options": {
+ "hash": {"type": "int", "choices": [0, 7]},
+ "value": {"type": "str", "no_log": True},
+ },
+ "no_log": False,
+ },
+ "privilege": {
+ "type": "int",
+ "choices": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 12,
+ 13,
+ 14,
+ 15,
+ ],
+ },
+ "session": {
+ "type": "dict",
+ "options": {
+ "disconnect_warning": {"type": "int"},
+ "limit": {"type": "int"},
+ "timeout": {"type": "int"},
+ },
+ },
+ "speed": {"type": "int"},
+ "stopbits": {"type": "str", "choices": ["1", "1.5", "2"]},
+ "transport": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "all": {"type": "bool"},
+ "name": {
+ "type": "str",
+ "choices": ["input", "output", "preferred"],
+ },
+ "none": {"type": "bool"},
+ "pad": {"type": "bool"},
+ "rlogin": {"type": "bool"},
+ "ssh": {"type": "bool"},
+ "telnet": {"type": "bool"},
+ },
+ },
+ },
+ },
+ },
+ },
+ "running_config": {"type": "str"},
+ "state": {
+ "type": "str",
+ "choices": [
+ "merged",
+ "overridden",
+ "replaced",
+ "deleted",
+ "rendered",
+ "parsed",
+ "gathered",
+ ],
+ "default": "merged",
+ },
+ } # pylint: disable=C0301
diff --git a/plugins/module_utils/network/ios/config/line/__init__.py b/plugins/module_utils/network/ios/config/line/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/plugins/module_utils/network/ios/config/line/line.py b/plugins/module_utils/network/ios/config/line/line.py
new file mode 100644
index 000000000..3bf1a2e87
--- /dev/null
+++ b/plugins/module_utils/network/ios/config/line/line.py
@@ -0,0 +1,310 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The ios_line config file.
+It is in this file where the current configuration (as dict)
+is compared to the provided configuration (as dict) and the command set
+necessary to bring the current configuration to its desired end-state is
+created.
+"""
+
+from copy import deepcopy
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import (
+ ResourceModule,
+)
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
+ dict_merge,
+ to_list,
+)
+
+from ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.facts import Facts
+from ansible_collections.cisco.ios.plugins.module_utils.network.ios.rm_templates.line import (
+ LineTemplate,
+)
+
+
+class Line(ResourceModule):
+ """
+ The ios_line config class
+ """
+
+ def __init__(self, module):
+ super(Line, self).__init__(
+ empty_fact_val={},
+ facts_module=Facts(module),
+ module=module,
+ resource="line",
+ tmplt=LineTemplate(),
+ )
+ self.parsers = {
+ "line": ["line"],
+ "access_classes_in": ["access_classes_in"],
+ "access_classes_out": ["access_classes_out"],
+ "accounting": [
+ "accounting.arap",
+ "accounting.commands",
+ "accounting.connection",
+ "accounting.exec",
+ "accounting.resource",
+ ],
+ "authorization": [
+ "authorization.arap",
+ "authorization.commands",
+ "authorization.exec",
+ "authorization.reverse_access",
+ ],
+ "escape_character": ["escape_character"],
+ "exec": [
+ "exec.banner",
+ "exec.character_bits",
+ "exec.prompt.expand",
+ "exec.prompt.timestamp",
+ "exec.timeout",
+ ],
+ "length": ["length"],
+ "location": ["location"],
+ "logging": ["logging"],
+ "login": ["login"],
+ "logout_warning": ["logout_warning"],
+ "motd": ["motd"],
+ "notify": ["notify"],
+ "padding": ["padding"],
+ "parity": ["parity"],
+ "password": ["password"],
+ "privilege": ["privilege"],
+ "session": [
+ "session.disconnect_warning",
+ "session.limit",
+ "session.timeout",
+ ],
+ "speed": ["speed"],
+ "stopbits": ["stopbits"],
+ "transport": ["transport"],
+ }
+
+ def execute_module(self):
+ """Execute the module
+
+ :rtype: A dictionary
+ :returns: The result from module execution
+ """
+ if self.state not in ["parsed", "gathered"]:
+ self.generate_commands()
+ self.run_commands()
+ return self.result
+
+ def generate_commands(self):
+ """Generate configuration commands to send based on
+ want, have and desired state.
+ """
+ wantd = deepcopy(self.want)
+ wantd["lines"] = self._list_to_dict(data=wantd.get("lines", []))
+ haved = deepcopy(self.have)
+ haved["lines"] = self._list_to_dict(data=haved.get("lines", []))
+
+ # if state is merged, merge want onto have and then compare
+ if self.state == "merged":
+ wantd = dict_merge(haved, wantd)
+
+ # if state is deleted, empty out wantd and set haved to wantd
+ if self.state == "deleted" and wantd["lines"] != {}:
+ haved["lines"] = {k: v for k, v in haved["lines"].items() if k in wantd["lines"]}
+
+ # remove superfluous config
+ if self.state in ["overridden", "deleted"]:
+ for k, have in haved["lines"].items():
+ if k not in wantd["lines"]:
+ if k == "con 0" or k == "vty 0 4" or k.startswith("aux"):
+ self._compare(want={"name": k}, have=have)
+ else:
+ self._compare(want={}, have=have)
+ elif self.state in ["replaced"]:
+ haved["lines"] = {k: v for k, v in haved["lines"].items() if k in wantd["lines"]}
+
+ for k, want in wantd["lines"].items():
+ self._compare(want=want, have=haved["lines"].pop(k, {}))
+
+ # Workaround: if we change the length command, we need to
+ # force to reset the length for the terminal
+ if any(cmd for cmd in self.commands if "length" in cmd):
+ self.commands.extend(to_list("do terminal length 0"))
+
+ def _compare(self, want, have):
+ """Leverages the base class `compare()` method and
+ populates the list of commands to be run by comparing
+ the `want` and `have` data with the `parsers` defined
+ for the Line network resource.
+ """
+ config_default = {
+ "accounting": {
+ "arap": "default",
+ "connection": "default",
+ "exec": "default",
+ "resource": "default",
+ },
+ "authorization": {
+ "arap": "default",
+ "exec": "default",
+ "reverse_access": "default",
+ },
+ "escape_character": {
+ "value": "DEFAULT",
+ },
+ "exec": {
+ "banner": True,
+ "character_bits": 7,
+ "timeout": "10",
+ },
+ "login": "default",
+ "logout_warning": 20,
+ "motd": True,
+ "privilege": 1,
+ }
+ config_default_transport = {
+ "transport": {
+ "input": {
+ "name": "input",
+ "ssh": True,
+ },
+ },
+ }
+ begin = len(self.commands)
+ if want != {}:
+ want = dict_merge(config_default, want)
+ have = dict_merge(config_default, have)
+ if "vty 0" in want["name"] or want["name"].startswith("aux"):
+ want = dict_merge(config_default_transport, want)
+ self._compare_lists(want=want, have=have)
+ if len(self.commands) != begin:
+ self.commands.insert(begin, self._tmplt.render(want or have, "line", False))
+ else:
+ self.commands.insert(begin, self._tmplt.render(have, "line", True))
+
+ def _compare_lists(self, want, have):
+ p_lvl_1 = [
+ "accounting",
+ "authorization",
+ "exec",
+ ]
+ # Take commands that have a subdict
+ for l1 in p_lvl_1:
+ l1_want = want.pop(l1, {})
+ l1_have = have.pop(l1, {})
+ for l1_key, l1_w_entry in l1_want.items():
+ if l1_key == "prompt":
+ l1_h_entry = l1_have.pop(l1_key, {})
+ for p_key, p_w_entry in l1_w_entry.items():
+ p_h_entry = l1_h_entry.pop(p_key, {})
+ if p_w_entry != p_h_entry:
+ self.addcmd(
+ data={p_key: p_w_entry},
+ tmplt="{0}.{1}.{2}".format(l1, l1_key, p_key),
+ negate=False,
+ )
+ for p_key, p_h_entry in l1_h_entry.items():
+ self.addcmd(data={p_key: p_h_entry}, tmplt=self.parsers[l1], negate=True)
+ elif l1_key == "commands":
+ l1_h_entry = l1_have.pop(l1_key, {})
+ for c_key, c_w_entry in l1_w_entry.items():
+ c_h_entry = l1_h_entry.pop(
+ c_key,
+ {"level": c_w_entry["level"], "command": "default"},
+ )
+ self.compare(
+ parsers=self.parsers[l1],
+ want={l1_key: c_w_entry},
+ have={l1_key: c_h_entry},
+ )
+ for c_key, cc_h_entry in l1_h_entry.items():
+ self.compare(
+ parsers=self.parsers[l1],
+ want={},
+ have={l1_key: c_h_entry},
+ )
+ else:
+ l1_h_entry = l1_have.pop(l1_key, "")
+ self.compare(
+ parsers=self.parsers[l1],
+ want={l1: {l1_key: l1_w_entry}},
+ have={l1: {l1_key: l1_h_entry}},
+ )
+ for l1_key, l1_h_entry in l1_have.items():
+ if l1_key == "prompt":
+ for p_key, p_h_entry in l1_h_entry.items():
+ self.addcmd(
+ data={p_key: p_h_entry},
+ tmplt="{0}.{1}.{2}".format(l1, l1_key, p_key),
+ negate=True,
+ )
+ elif l1_key == "commands":
+ for c_key, c_h_entry in l1_h_entry.items():
+ self.compare(
+ parsers=self.parsers[l1],
+ want={},
+ have={l1_key: c_h_entry},
+ )
+ else:
+ self.compare(
+ parsers=self.parsers[l1],
+ want={},
+ have={l1_key: l1_h_entry},
+ )
+
+ # Take commands that didn't have a subdict
+ for key, w_entry in want.items():
+ if key == "name":
+ continue
+ if key == "transport":
+ h_entry = have.get(key, {})
+ for t_key, t_w_entry in w_entry.items():
+ t_h_entry = h_entry.pop(t_key, {})
+ self.compare(
+ parsers=self.parsers[key],
+ want={key: t_w_entry},
+ have={key: t_h_entry},
+ )
+ else:
+ h_entry = have.pop(key, {})
+ self.compare(parsers=self.parsers[key], want={key: w_entry}, have={key: h_entry})
+ for key, h_entry in have.items():
+ if key == "name":
+ continue
+ if key == "transport":
+ for t_key, t_h_entry in h_entry.items():
+ self.compare(
+ parsers=self.parsers[key],
+ want={},
+ have={key: t_h_entry},
+ )
+ else:
+ self.compare(parsers=self.parsers[key], want={}, have={key: h_entry})
+
+ def _convert_list_to_dict(self, data, key="name"):
+ return {_k.get(key, ""): _k for _k in data} if data else {}
+
+ def _list_to_dict(self, data):
+ l_result = self._convert_list_to_dict(data=data)
+
+ for _name, _line in l_result.items():
+ for k, v in _line.items():
+ if k == "transport":
+ l_result[_name][k] = self._convert_list_to_dict(data=v)
+ elif k in ["accounting", "authorization", "exec"]:
+ for _sk, _sv in v.items():
+ if _sk == "commands":
+ l_result[_name][k][_sk] = self._convert_list_to_dict(
+ data=_sv,
+ key="level",
+ )
+ return l_result
diff --git a/plugins/module_utils/network/ios/facts/facts.py b/plugins/module_utils/network/ios/facts/facts.py
index 583c86b51..f26e31d8c 100644
--- a/plugins/module_utils/network/ios/facts/facts.py
+++ b/plugins/module_utils/network/ios/facts/facts.py
@@ -60,6 +60,7 @@
Hardware,
Interfaces,
)
+from ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.line.line import LineFacts
from ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.lldp_global.lldp_global import (
Lldp_globalFacts,
)
@@ -118,6 +119,7 @@
lag_interfaces=Lag_interfacesFacts,
lacp=LacpFacts,
lacp_interfaces=Lacp_InterfacesFacts,
+ line=LineFacts,
lldp_global=Lldp_globalFacts,
lldp_interfaces=Lldp_InterfacesFacts,
l3_interfaces=L3_InterfacesFacts,
diff --git a/plugins/module_utils/network/ios/facts/line/__init__.py b/plugins/module_utils/network/ios/facts/line/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/plugins/module_utils/network/ios/facts/line/line.py b/plugins/module_utils/network/ios/facts/line/line.py
new file mode 100644
index 000000000..9696f8b7f
--- /dev/null
+++ b/plugins/module_utils/network/ios/facts/line/line.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The ios line fact class
+It is in this file the configuration is collected from the device
+for a given resource, parsed, and the facts tree is populated
+based on the configuration.
+"""
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils
+
+from ansible_collections.cisco.ios.plugins.module_utils.network.ios.argspec.line.line import (
+ LineArgs,
+)
+from ansible_collections.cisco.ios.plugins.module_utils.network.ios.rm_templates.line import (
+ LineTemplate,
+)
+
+
+class LineFacts(object):
+ """The ios line facts class"""
+
+ def __init__(self, module, subspec="config", options="options"):
+ self._module = module
+ self.argument_spec = LineArgs.argument_spec
+
+ def get_line_data(self, connection):
+ return connection.get("show running-config | sec ^line")
+
+ def populate_facts(self, connection, ansible_facts, data=None):
+ """Populate the facts for Line network resource
+
+ :param connection: the device connection
+ :param ansible_facts: Facts dictionary
+ :param data: previously collected conf
+
+ :rtype: dictionary
+ :returns: facts
+ """
+ facts = {}
+ objs = []
+ params = {}
+
+ if not data:
+ data = self.get_line_data(connection)
+
+ # parse native config using the Line template
+ line_parser = LineTemplate(lines=data.splitlines(), module=self._module)
+ objs = line_parser.parse()
+ objs["lines"] = list(objs["lines"].values())
+
+ for obj in objs["lines"]:
+ if "authorization" in obj and "commands" in obj["authorization"]:
+ obj["authorization"]["commands"] = list(obj["authorization"]["commands"].values())
+ elif "accounting" in obj and "commands" in obj["accounting"]:
+ obj["accounting"]["commands"] = list(obj["accounting"]["commands"].values())
+
+ ansible_facts["ansible_network_resources"].pop("line", None)
+ params = utils.remove_empties(
+ line_parser.validate_config(self.argument_spec, {"config": objs}, redact=True),
+ )
+
+ facts["line"] = params.get("config", {})
+ ansible_facts["ansible_network_resources"].update(facts)
+
+ return ansible_facts
diff --git a/plugins/module_utils/network/ios/rm_templates/line.py b/plugins/module_utils/network/ios/rm_templates/line.py
new file mode 100644
index 000000000..74ada608c
--- /dev/null
+++ b/plugins/module_utils/network/ios/rm_templates/line.py
@@ -0,0 +1,713 @@
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The Line parser templates file. This contains
+a list of parser definitions and associated functions that
+facilitates both facts gathering and native command generation for
+the given network resource.
+"""
+
+import re
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import (
+ NetworkTemplate,
+)
+
+
+class LineTemplate(NetworkTemplate):
+ def __init__(self, lines=None, module=None):
+ super(LineTemplate, self).__init__(lines=lines, tmplt=self, module=module)
+
+ def render(self, data, parser_name, negate=False):
+ """render"""
+ if negate:
+ tmplt = (
+ self.get_parser(parser_name).get("remval") or self.get_parser(parser_name)["setval"]
+ )
+ else:
+ tmplt = self.get_parser(parser_name)["setval"]
+ command = self._render(tmplt, data, negate and not tmplt.startswith("default"))
+ return command
+
+ # fmt: off
+ PARSERS = [
+ {
+ "name": "line",
+ "getval": re.compile(
+ r"""
+ ^line\s+(?Pcon\s+[0-9]+|aux\s+[0-9]+|vty\s+[0-9]+\s+[0-9]*)
+ """, re.VERBOSE,
+ ),
+ "setval": "line {{ name }}",
+ "result": {
+ "lines": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ },
+ },
+ },
+ "shared": True,
+ },
+ {
+ "name": "access_classes_in",
+ "getval": re.compile(
+ r"""
+ ^\s+access-class\s+(?P\S+)\s+in
+ (\svrfname(?P\S+))?
+ (\s(?Pvrf-also))?
+ """, re.VERBOSE,
+ ),
+ "setval": "access-class {{ access_classes_in.name }} in"
+ "{{ ' vrfname' + access_classes_in.vrfname if 'vrfname' in access_classes_in }}"
+ "{{ ' vrf-also' if 'vrf_also' in access_classes_in and access_classes_in.vrf_also }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "access_classes_in": {
+ "name": "{{ access_classes_in }}",
+ "vrfname": "{{ vrfname }}",
+ "vrf_also": "{{ not not vrf_also }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "access_classes_out",
+ "getval": re.compile(
+ r"""
+ ^\s+access-class\s+(?P\S+)\s+out
+ """, re.VERBOSE,
+ ),
+ "setval": "access-class {{ access_classes_out }} out",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "access_classes_out": "{{ access_classes_out }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "accounting.arap",
+ "getval": re.compile(
+ r"""
+ ^\s+accounting\s+arap\s+(?P\S+)
+ """, re.VERBOSE,
+ ),
+ "setval": "accounting arap {{ accounting.arap }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "accounting": {
+ "arap": "{{ arap|d(default) }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "accounting.commands",
+ "compval": "commands",
+ "getval": re.compile(
+ r"""
+ ^\s+accounting\s+commands\s+(?P\d+)\s+(?P\S+)
+ """, re.VERBOSE,
+ ),
+ "setval": "accounting commands {{ commands.level }} {{ commands.command }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "accounting": {
+ "commands": {
+ "{{ level|d() }}": {
+ "level": "{{ level }}",
+ "command": "{{ command }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "accounting.connection",
+ "getval": re.compile(
+ r"""
+ ^\s+accounting\s+connection\s+(?P\S+)
+ """, re.VERBOSE,
+ ),
+ "setval": "accounting connection {{ accounting.connection }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "accounting": {
+ "connection": "{{ connection|d(default) }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "accounting.exec",
+ "getval": re.compile(
+ r"""
+ ^\s+accounting\s+exec\s+(?P\S+)
+ """, re.VERBOSE,
+ ),
+ "setval": "accounting exec {{ accounting.exec }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "accounting": {
+ "exec": "{{ exec|d(default) }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "accounting.resource",
+ "getval": re.compile(
+ r"""
+ ^\s+accounting\s+resource\s+(?P\S+)
+ """, re.VERBOSE,
+ ),
+ "setval": "accounting resource {{ accounting.resource }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "accounting": {
+ "resource": "{{ resource|d(default) }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authorization.arap",
+ "getval": re.compile(
+ r"""
+ ^\s+authorization\s+arap\s+(?P\S+)
+ """, re.VERBOSE,
+ ),
+ "setval": "authorization arap {{ authorization.arap }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "authorization": {
+ "arap": "{{ arap|d(default) }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authorization.commands",
+ "compval": "commands",
+ "getval": re.compile(
+ r"""
+ ^\s+authorization\s+commands\s+(?P\d+)\s+(?P\S+)
+ """, re.VERBOSE,
+ ),
+ "setval": "authorization commands {{ commands.level }} {{ commands.command }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "authorization": {
+ "commands": {
+ "{{ level|d() }}": {
+ "level": "{{ level }}",
+ "command": "{{ command }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authorization.exec",
+ "getval": re.compile(
+ r"""
+ ^\s+authorization\s+exec\s+(?P\S+)
+ """, re.VERBOSE,
+ ),
+ "setval": "authorization exec {{ authorization.exec }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "authorization": {
+ "exec": "{{ exec|d(default) }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authorization.reverse_access",
+ "getval": re.compile(
+ r"""
+ ^\s+authorization\s+reverse-access\s+(?P\S+)
+ """, re.VERBOSE,
+ ),
+ "setval": "authorization reverse-access {{ authorization.reverse_access }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "authorization": {
+ "reverse-access": "{{ reverse-access|d(default) }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "escape_character",
+ "getval": re.compile(
+ r"""
+ ^\s+escape-character
+ (\s+(?Psoft))?
+ \s+(?P\S+)
+ """, re.VERBOSE,
+ ),
+ "setval": "escape-character"
+ "{{ ' soft' if escape_character.soft|d(False) else '' }}"
+ "{{ ' ' + escape_character.value }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "escape_character": {
+ "soft": "{{ True if soft }}",
+ "value": "{{ value }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "exec.banner",
+ "getval": re.compile(
+ r"""
+ ^\s+exec-banner
+ """, re.VERBOSE,
+ ),
+ "setval": "exec-banner",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "exec": {
+ "banner": True,
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "exec.character_bits",
+ "getval": re.compile(
+ r"""
+ ^\s+exec-character-bits\s+(?P7|8)
+ """, re.VERBOSE,
+ ),
+ "setval": "exec-character-bits {{ exec.character_bits }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "exec": {
+ "character_bits": "{{ character_bits }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "exec.prompt.expand",
+ "getval": re.compile(
+ r"""
+ ^\s+exec\s+prompt\s+expand
+ """, re.VERBOSE,
+ ),
+ "setval": "{{ ' exec prompt expand' if expand|d(False) }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "exec": {
+ "prompt": {
+ "expand": True,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "exec.prompt.timestamp",
+ "getval": re.compile(
+ r"""
+ ^\s+exec\s+prompt\s+timestamp
+ """, re.VERBOSE,
+ ),
+ "setval": "{{ ' exec prompt timestamp' if timestamp|d(False) }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "exec": {
+ "prompt": {
+ "timestamp": True,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "exec.timeout",
+ "getval": re.compile(
+ r"""
+ ^\s+exec-timeout\s+(?P\d+)
+ """, re.VERBOSE,
+ ),
+ "setval": "exec-timeout {{ exec.timeout }} 0",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "exec": {
+ "timeout": "{{ timeout }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "length",
+ "getval": re.compile(
+ r"""
+ ^\s+length\s(?P\d+)
+ """, re.VERBOSE,
+ ),
+ "setval": "length {{ length|string }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "length": "{{ length }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "location",
+ "getval": re.compile(
+ r"""
+ ^\s+location\s+(?P.+)$
+ """, re.VERBOSE,
+ ),
+ "setval": "location {{ location }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "location": "{{ location }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "logging",
+ "getval": re.compile(
+ r"""
+ ^\s+logging\s+synchronous
+ (\s+(?P\S+))?
+ (\s+(?P\d+))?
+ """, re.VERBOSE,
+ ),
+ "setval": "logging synchronous"
+ "{{ ' level ' + logging.level if logging.level is defined else '' }}"
+ "{{ ' limit ' + logging.limit if logging.limit is defined else '' }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "logging": {
+ "enable": True,
+ "level": "{{ level }}",
+ "limit": "{{ limit }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "login",
+ "getval": re.compile(
+ r"""
+ ^\s+login\s+authentication\s+(?P\S+)
+ """, re.VERBOSE,
+ ),
+ "setval": "login authentication {{ login }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "login": "{{ login|d('default') }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "logout_warning",
+ "getval": re.compile(
+ r"""
+ ^\s+logout-warning\s+(?P\d+)
+ """, re.VERBOSE,
+ ),
+ "setval": "logout-warning {{ logout_warning }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "logout_warning": "{{ logout_warning }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "motd",
+ "getval": re.compile(
+ r"""
+ ^\s+motd-banner
+ """, re.VERBOSE,
+ ),
+ "setval": "motd-banner",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "motd": True,
+ },
+ },
+ },
+ },
+ {
+ "name": "notify",
+ "getval": re.compile(
+ r"""
+ ^\s+notify
+ """, re.VERBOSE,
+ ),
+ "setval": "notify",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "notify": True,
+ },
+ },
+ },
+ },
+ {
+ "name": "padding",
+ "getval": re.compile(
+ r"""
+ ^\s+padding\s+(?P\S+)
+ """, re.VERBOSE,
+ ),
+ "setval": "padding {{ padding }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "padding": "{{ padding }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "parity",
+ "getval": re.compile(
+ r"""
+ ^\s+parity\s+(?Peven|mark|none|odd|space)
+ """, re.VERBOSE,
+ ),
+ "setval": "parity {{ parity }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "parity": "{{ parity }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "password",
+ "getval": re.compile(
+ r"""
+ ^\s+password
+ (\s+(?P0|7))?
+ (\s+(?P\S+))?
+ """, re.VERBOSE,
+ ),
+ "setval": "{% if 'value' in password and password.value is defined %}"
+ "password"
+ "{{ ' ' + password.hash|string if password.hash is defined else '' }}"
+ "{{ ' ' + password.value }}"
+ "{% endif %}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "password": {
+ "hash": "{{ hash }}",
+ "value": "{{ value }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "privilege",
+ "getval": re.compile(
+ r"""
+ ^\s+privilege\s+level\s+(?P\d+)
+ """, re.VERBOSE,
+ ),
+ "setval": "privilege level {{ privilege }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "privilege": "{{ privilege }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "session.disconnect_warning",
+ "getval": re.compile(
+ r"""
+ ^\s+session-disconnect-warning\s+(?P\d+)
+ """, re.VERBOSE,
+ ),
+ "setval": "session-disconnect-warning {{ session.disconnect_warning }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "session": {
+ "disconnect_warning": "{{ disconnect_warning }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "session.limit",
+ "getval": re.compile(
+ r"""
+ ^\s+session-limit\s+(?P\d+)
+ """, re.VERBOSE,
+ ),
+ "setval": "session-limit {{ session.limit }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "session": {
+ "limit": "{{ limit }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "session.timeout",
+ "getval": re.compile(
+ r"""
+ ^\s+session-timeout\s+(?P\d+)
+ """, re.VERBOSE,
+ ),
+ "setval": "session-timeout {{ session.timeout }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "session": {
+ "timeout": "{{ timeout }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "speed",
+ "getval": re.compile(
+ r"""
+ ^\s+speed\s+(?P\d+)
+ """, re.VERBOSE,
+ ),
+ "setval": "speed {{ speed }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "speed": "{{ speed }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "stopbits",
+ "getval": re.compile(
+ r"""
+ ^\s+stopbits\s+(?P1|1.5|2)
+ """, re.VERBOSE,
+ ),
+ "setval": "stopbits {{ stopbits }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "stopbits": "{{ stopbits }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "transport",
+ "getval": re.compile(
+ r"""
+ ^\s+transport\s+(?Pinput|output|preferred)
+ (\s+(?Pall))?
+ (\s+(?Pnone))?
+ (\s+(?Ppas))?
+ (\s+(?Ptelnet))?
+ (\s+(?Prlogin))?
+ (\s+(?Pssh))?
+ """, re.VERBOSE,
+ ),
+ "setval": "transport {{ transport.name }}"
+ "{% if transport.all|d(False) %}"
+ " all"
+ "{% elif transport.none|d(False) %}"
+ " none"
+ "{% else %}"
+ "{{ ' pad' if transport.pad|d(False) }}"
+ "{{ ' telnet' if transport.telnet|d(False) }}"
+ "{{ ' rlogin' if transport.rlogin|d(False) }}"
+ "{{ ' ssh' if transport.ssh|d(False) }}"
+ "{% endif %}",
+ "remval": "default transport {{ transport.name }}",
+ "result": {
+ "lines": {
+ "{{ name|d() }}": {
+ "transport": [
+ {
+ "name": "{{ transport_name }}",
+ "all": "{{ True if t_all is defined }}",
+ "none": "{{ True if t_none is defined }}",
+ "pad": "{{ True if t_pad is defined }}",
+ "telnet": "{{ True if t_telnet is defined }}",
+ "rlogin": "{{ True if t_rlogin is defined }}",
+ "ssh": "{{ True if t_ssh is defined }}",
+ },
+ ],
+ },
+ },
+ },
+ },
+ ]
+ # fmt: on
diff --git a/plugins/modules/ios_facts.py b/plugins/modules/ios_facts.py
index 93b7b446a..087cf0ee9 100644
--- a/plugins/modules/ios_facts.py
+++ b/plugins/modules/ios_facts.py
@@ -61,7 +61,7 @@
'ntp_global', 'acls', 'hostname', 'interfaces', 'lldp_interfaces', 'logging_global',
'ospf_interfaces', 'ospfv2', 'prefix_lists', 'static_routes', 'acl_interfaces',
'all', 'bgp_address_family', 'l2_interfaces', 'lacp', 'lacp_interfaces', 'lldp_global',
- 'ospfv3', 'snmp_server', 'vlans', 'service'.
+ 'ospfv3', 'snmp_server', 'vlans', 'service', 'line'.
type: list
elements: str
available_network_resources:
diff --git a/plugins/modules/ios_line.py b/plugins/modules/ios_line.py
new file mode 100644
index 000000000..a42f18124
--- /dev/null
+++ b/plugins/modules/ios_line.py
@@ -0,0 +1,868 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""
+The module file for ios_line
+"""
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+---
+module: ios_line
+short_description: Resource module to configure line
+description:
+ - This module provides declarative management of the lines I(console), I(vty)
+version_added: 4.7.0
+author:
+ - Ambroise Rosset (@earendilfr)
+notes:
+ - Tested against Cisco IOSXE Version 17.3 on CML.
+ - This module works with connection C(network_cli).
+options:
+ config:
+ description: The provided configurations.
+ type: dict
+ suboptions:
+ lines:
+ description: Configuration for each lines
+ type: list
+ elements: dict
+ suboptions:
+ access_classes_in:
+ description: Access-list used to filter inbound connections
+ type: dict
+ suboptions:
+ name:
+ description: Name or ID of the ACL to use
+ type: str
+ vrf_also:
+ description: Enable also this filtring on VRF traffic
+ type: bool
+ vrfname:
+ description: Apply this ACL only for this VRF
+ type: str
+ access_classes_out:
+ description: ID of the access-list used to filter outbound connections
+ type: str
+ accounting:
+ description: Accounting parameters
+ type: dict
+ suboptions:
+ arap:
+ description:
+ - For Appletalk Remote Access Protocol
+ - Use an accounting list with this name
+ - I(default) is the default name
+ type: str
+ default: default
+ commands:
+ description:
+ - For exec (shell) commands
+ type: list
+ elements: dict
+ suboptions:
+ level:
+ description:
+ - Enable level
+ type: int
+ command:
+ description:
+ - Use an accounting list with this name
+ - I(default) is the default name
+ type: str
+ default: default
+ connection:
+ description:
+ - For connection accounting
+ - Use an accounting list with this name
+ - I(default) is the default name
+ type: str
+ default: default
+ exec:
+ description:
+ - For starting an exec (shell)
+ - Use an accounting list with this name
+ - I(default) is the default name
+ type: str
+ default: default
+ resource:
+ description:
+ - For resource accounting
+ - Use an accounting list with this name
+ - I(default) is the default name
+ type: str
+ default: default
+ authorization:
+ description: Authorization parameters
+ type: dict
+ suboptions:
+ arap:
+ description:
+ - For Appletalk Remote Access Protocol
+ - Use an authorization list with this name
+ - I(default) is the default name
+ type: str
+ default: default
+ commands:
+ description:
+ - For exec (shell) commands
+ type: list
+ elements: dict
+ suboptions:
+ level:
+ description:
+ - Enable level
+ type: int
+ command:
+ description:
+ - Use an authorization list with this name
+ - I(default) is the default name
+ type: str
+ default: default
+ exec:
+ description:
+ - For starting an exec (shell)
+ - Use an authorization list with this name
+ - I(default) is the default name
+ type: str
+ default: default
+ reverse_access:
+ description:
+ - For reverse telnet connections
+ - Use an authorization list with this name
+ - I(default) is the default name
+ type: str
+ default: default
+ escape_character:
+ description: Change the current line's escape character
+ type: dict
+ suboptions:
+ soft:
+ description: Set the soft escape character for this line
+ type: bool
+ value:
+ description:
+ - Escape character configured
+ - I(BREAK) - Cause escape on BREAK
+ - I(DEFAULT) - Use default escape character
+ - I(NONE) - Disable escape entirely
+ - I(CHAR) or I(<0-255>) - Escape character or its ASCII decimal equivalent
+ type: str
+ exec:
+ description: Configure EXEC
+ type: dict
+ suboptions:
+ banner:
+ description: Enable the display of the EXEC banner
+ type: bool
+ character_bits:
+ description: Size of characters to the command exec
+ type: int
+ choices:
+ - 7
+ - 8
+ prompt:
+ description: EXEC prompt
+ type: dict
+ suboptions:
+ expand:
+ description: Prints expanded command for show commands
+ type: bool
+ timestamp:
+ description: Print timestamps for show commands
+ type: bool
+ timeout:
+ description:
+ - Timeout in minutes (Value between C(<0-35791>))
+ type: int
+ length:
+ description:
+ - Set number of lines on a screen (Value between C(<0-512>))
+ - C(0) for no pausing
+ type: int
+ location:
+ description:
+ - Enter terminal location description
+ type: str
+ logging:
+ description: Modify message logging facilities for synchronous
+ type: dict
+ suboptions:
+ enable:
+ description: Enable logging synchronous
+ type: bool
+ level:
+ description: Severity level to output asynchronously
+ type: str
+ choices: [ '0', '1', '2', '3', '4', '5', '6', '7', 'all']
+ limit:
+ description:
+ - Messages queue size (Value between C(<0-2147483647>))
+ type: int
+ login:
+ description:
+ - Enable password checking for authentication
+ - Use an authentication list with this name
+ - I(default) is the default name
+ type: str
+ default: default
+ logout_warning:
+ description:
+ - Set Warning countdown for absolute timeout of line
+ - (Value between C(<0-4294967295>))
+ type: int
+ motd:
+ description: Enable the display of the MOTD banner
+ type: bool
+ default: true
+ name:
+ description:
+ - Define the type of line to configure
+ - Should be the same form than C(line ....) indicated in the cisco running configuration
+ - By example, I(con 0) or I(vty 0 4)
+ type: str
+ required: true
+ notify:
+ description: Inform users of output from concurrent sessions
+ type: bool
+ padding:
+ description: Set padding for a specified output character
+ type: str
+ parity:
+ description: Set terminal parity
+ type: str
+ choices:
+ - even
+ - mark
+ - none
+ - odd
+ - space
+ password:
+ description: Password to connect to the line
+ type: dict
+ suboptions:
+ hash:
+ description:
+ - I(0) - Specifies an UNENCRYPTED password will follow
+ - I(7) - Specifies a HIDDEN password will follow
+ type: int
+ choices: [0, 7]
+ value:
+ description: The actual hashed password to be configured
+ type: str
+ privilege:
+ description:
+ - Change privilege level for line
+ - The I(privilege) valu should be between C(0) and C(15)
+ type: int
+ choices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 12, 13, 14, 15]
+ session:
+ description: Configure for session
+ type: dict
+ suboptions:
+ disconnect_warning:
+ description: Set warning countdown for session-timeout (Between C(<0-4294967295>))
+ type: int
+ limit:
+ description: Set maximum number of sessions (Between C(<0-4294967295>))
+ type: int
+ timeout:
+ description:
+ - Set interval for closing connection when there is no input traffic
+ - (Between C(<0-35791>))
+ type: int
+ speed:
+ description: Set the transmit and receive speeds (Between C(<0-4294967295>))
+ type: int
+ stopbits:
+ description:
+ - Set async line stop bits
+ - I(1) - One stop bit
+ - I(1.5) - One and one-half stop bits
+ - I(2) - Two stop bits
+ type: str
+ choices: ['1', '1.5', '2']
+ transport:
+ description: Define transport protocols for line
+ type: list
+ elements: dict
+ suboptions:
+ all:
+ description:
+ - All protocols are allowed
+ - Not used if I(name) is configured at I(preferred)
+ type: bool
+ name:
+ description:
+ - Type of transport to configure
+ - I(input) - Configure incomming connections
+ - I(output) - Configure outgoing connections
+ - I(preferred) - Configure preferred protocol to use
+ type: str
+ choices:
+ - input
+ - output
+ - preferred
+ none:
+ description: No protocols are allowed
+ type: bool
+ pad:
+ description: Allow X.3 PAD
+ type: bool
+ rlogin:
+ description: Allow Unix rlogin protocol
+ type: bool
+ ssh:
+ description: Allow TCP/IP SSH protocol
+ type: bool
+ telnet:
+ description: Allow TCP/IP Telnet protocol
+ type: bool
+ running_config:
+ description:
+ - This option is used only with state I(parsed).
+ - The value of this option should be the output received from the IOS device by
+ executing the command B(show lacp sys-id).
+ - The state I(parsed) reads the configuration from C(running_config) option and
+ transforms it into Ansible structured data as per the resource module's argspec
+ and the value is then returned in the I(parsed) key within the result.
+ type: str
+ state:
+ description:
+ - The state the configuration should be left in
+ - The states I(rendered), I(gathered) and I(parsed) does not perform any change
+ on the device.
+ - The state I(rendered) will transform the configuration in C(config) option to
+ platform specific CLI commands which will be returned in the I(rendered) key
+ within the result. For state I(rendered) active connection to remote host is
+ not required.
+ - The state I(overridden) modify/add the lines defined, deleted all other lines.
+ - The state I(replaced) will only override the configuration part of the defined lines.
+ - The state I(gathered) will fetch the running configuration from device and transform
+ it into structured data in the format as per the resource module argspec and
+ the value is returned in the I(gathered) key within the result.
+ - The state I(parsed) reads the configuration from C(running_config) option and
+ transforms it into JSON format as per the resource module parameters and the
+ value is returned in the I(parsed) key within the result. The value of C(running_config)
+ option should be the same format as the output of command I(show running-config
+ | sec ^line) executed on device. For state I(parsed) active connection to
+ remote host is not required.
+ - The state I(deleted), deletes only the specified lines, or all if not specified.
+ type: str
+ choices:
+ - merged
+ - overridden
+ - replaced
+ - deleted
+ - rendered
+ - parsed
+ - gathered
+ default: merged
+"""
+
+EXAMPLES = """
+# Using merged
+
+# Before state:
+# -------------
+#
+# sh run | sec ^line
+# line con 0
+# session-timeout 15
+# stopbits 1
+# line aux 0
+# line vty 0 4
+# transport input ssh
+
+- name: Merge provided configuration with device configuration
+ cisco.ios.ios_line:
+ config:
+ line:
+ - name: "con 0"
+ escape_character:
+ value: "3"
+ login: "console"
+ exec:
+ timeout: "60"
+ - name: "vty 0 4"
+ escape_character:
+ value: "3"
+ login: "default"
+ transport:
+ - name: preferred
+ none: true
+ - name: input
+ ssh: true
+ - name: output
+ ssh: true
+ state: "merged"
+
+# Task Output
+# -----------
+#
+# before:
+# lines:
+# - login: default
+# motd: true
+# name: con 0
+# session:
+# timeout: 15
+# stopbits: '1'
+# - login: default
+# motd: true
+# name: vty 0 4
+# transport:
+# - name: input
+# ssh: true
+# commands:
+# - line con 0
+# - ' exec-timeout 60 0'
+# - ' escape-character 3'
+# - ' login authentication console'
+# - line vty 0 4
+# - ' escape-character 3'
+# - ' transport preferred none'
+# - ' transport output ssh'
+# after:
+# lines:
+# - escape_character:
+# value: '3'
+# exec:
+# timeout: 60
+# login: console
+# motd: true
+# name: con 0
+# session:
+# timeout: 15
+# stopbits: '1'
+# - escape_character:
+# value: '3'
+# login: default
+# motd: true
+# name: vty 0 4
+# transport:
+# - name: preferred
+# none: true
+# - name: input
+# ssh: true
+# - name: output
+# ssh: true
+
+# After state:
+# ------------
+#
+# router-ios#sh run | sec ^line
+# line con 0
+# session-timeout 15
+# exec-timeout 60 0
+# login authentication console
+# escape-character 3
+# stopbits 1
+# line aux 0
+# line vty 0 4
+# transport preferred none
+# transport input ssh
+# transport output ssh
+# escape-character 3
+
+# Using overriden
+
+# router-ios#sh run | sec ^line
+# line con 0
+# session-timeout 15
+# stopbits 1
+# line aux 0
+# line vty 0 4
+# transport input ssh
+
+- name: Merge provided configuration with device configuration
+ cisco.ios.ios_line:
+ config:
+ line:
+ - name: "con 0"
+ escape_character:
+ value: "3"
+ login: "console"
+ exec:
+ timeout: "60"
+ - name: "vty 0 4"
+ escape_character:
+ value: "3"
+ login: "default"
+ transport:
+ - name: preferred
+ none: true
+ - name: input
+ ssh: true
+ - name: output
+ ssh: true
+ state: "overridden"
+
+# Task Output
+# -----------
+#
+# before:
+# lines:
+# - login: default
+# motd: true
+# name: con 0
+# session:
+# timeout: 15
+# stopbits: '1'
+# - login: default
+# motd: true
+# name: vty 0 4
+# transport:
+# - name: input
+# ssh: true
+# commands:
+# - line con 0
+# - ' exec-timeout 60 0'
+# - ' escape-character 3'
+# - ' login authentication console'
+# - ' no stopbits 1'
+# - ' no session-timeout 15'
+# - line vty 0 4
+# - ' escape-character 3'
+# - ' transport preferred none'
+# - ' transport output ssh'
+# after:
+# lines:
+# - escape_character:
+# value: '3'
+# exec:
+# timeout: 60
+# login: console
+# motd: true
+# name: con 0
+# - escape_character:
+# value: '3'
+# login: default
+# motd: true
+# name: vty 0 4
+# transport:
+# - name: preferred
+# none: true
+# - name: input
+# ssh: true
+# - name: output
+# ssh: true
+
+# After state:
+# ------------
+#
+# router-ios#sh run | sec ^line
+# line con 0
+# exec-timeout 60 0
+# login authentication console
+# escape-character 3
+# line aux 0
+# line vty 0 4
+# transport preferred none
+# transport input ssh
+# transport output ssh
+# escape-character 3
+
+# Using deleted
+
+# Before state:
+# -------------
+#
+# router-ios#sh run | sec ^line
+# line con 0
+# exec-timeout 60 0
+# login authentication console
+# escape-character 3
+# line aux 0
+# line vty 0 4
+# transport preferred none
+# transport input ssh
+# transport output ssh
+# escape-character 3
+
+- name: Merge provided configuration with device configuration
+ cisco.ios.ios_line:
+ config:
+ state: deleted
+
+# Task Output
+# -----------
+#
+# before:
+# lines:
+# - escape_character:
+# value: '3'
+# exec:
+# timeout: 60
+# login: console
+# motd: true
+# name: con 0
+# session:
+# timeout: 15
+# stopbits: '1'
+# - escape_character:
+# value: '3'
+# login: default
+# motd: true
+# name: vty 0 4
+# transport:
+# - name: preferred
+# none: true
+# - name: input
+# ssh: true
+# - name: output
+# ssh: true
+# commands:
+# - line con 0
+# - exec-timeout 10 0
+# - escape-character DEFAULT
+# - login authentication default
+# - no session-timeout 15
+# - no stopbits 1
+# - line vty 0 4
+# - escape-character DEFAULT
+# - no transport preferred
+# - no transport input
+# - no transport output
+# after:
+# lines:
+# - login: default
+# motd: true
+# name: con 0
+# - login: default
+# motd: true
+# name: vty 0 4
+# transport:
+# - name: preferred
+# none: true
+# - name: input
+# ssh: true
+# - name: output
+# none: true
+
+# After state:
+# ------------
+#
+# line con 0
+# line aux 0
+# line vty 0 4
+# transport preferred none
+# transport input ssh
+# transport output none
+
+# Using gathered
+
+# router-ios#sh run | sec ^line
+# line con 0
+# exec-timeout 60 0
+# login authentication console
+# escape-character 3
+# line aux 0
+# line vty 0 4
+# transport preferred none
+# transport input ssh
+# transport output ssh
+# escape-character 3
+
+- name: Gather ACLs configuration from target device
+ cisco.ios.ios_line:
+ state: gathered
+
+# Module Execution Result:
+# ------------------------
+#
+# before:
+# lines:
+# - escape_character:
+# value: '3'
+# exec:
+# timeout: 60
+# login: console
+# motd: true
+# name: con 0
+# - escape_character:
+# value: '3'
+# login: default
+# motd: true
+# name: vty 0 4
+# transport:
+# - name: preferred
+# none: true
+# - name: input
+# ssh: true
+# - name: output
+# ssh: true
+
+# Using rendered
+
+- name: Render the provided configuration into platform specific configuration lines
+ cisco.ios.ios_line:
+ config:
+ lines:
+ - name: "con 0"
+ escape_character:
+ value: "3"
+ login: "console"
+ exec:
+ timeout: "60"
+ - name: "vty 0 4"
+ escape_character:
+ value: "3"
+ login: "default"
+ transport:
+ - name: preferred
+ none: true
+ - name: input
+ ssh: true
+ - name: output
+ ssh: true
+ state: rendered
+
+# Module Execution Result:
+# ------------------------
+#
+# rendered:
+# - line con 0
+# - exec-timeout 60 0
+# - escape-character 3
+# - login authentication console
+# - line vty 0 4
+# - escape-character 3
+# - transport preferred none
+# - transport input ssh
+# - transport output ssh
+
+# Using Parsed
+
+# File: parsed.cfg
+# ----------------
+#
+# line con 0
+# exec-timeout 60 0
+# login authentication console
+# escape-character 3
+# line aux 0
+# line vty 0 4
+# transport preferred none
+# transport input ssh
+# transport output ssh
+# escape-character 3
+
+- name: Parse the commands for provided configuration
+ cisco.ios.ios_line:
+ running_config: "{{ lookup('file', 'parsed.cfg') }}"
+ state: parsed
+
+# Module Execution Result:
+# ------------------------
+#
+# parsed:
+# lines:
+# - escape_character:
+# value: '3'
+# exec:
+# timeout: 60
+# login: console
+# motd: true
+# name: con 0
+# - escape_character:
+# value: '3'
+# login: default
+# motd: true
+# name: vty 0 4
+# transport:
+# - name: preferred
+# none: true
+# - name: input
+# ssh: true
+# - name: output
+# ssh: true
+"""
+
+RETURN = """
+before:
+ description: The configuration prior to the module execution.
+ returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged)
+ type: dict
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+after:
+ description: The resulting configuration after module execution.
+ returned: when changed
+ type: dict
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+commands:
+ description: The set of commands pushed to the remote device.
+ returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged)
+ type: list
+ sample:
+ - sample command 1
+ - sample command 2
+ - sample command 3
+rendered:
+ description: The provided configuration in the task rendered in device-native format (offline).
+ returned: when I(state) is C(rendered)
+ type: list
+ sample:
+ - sample command 1
+ - sample command 2
+ - sample command 3
+gathered:
+ description: Facts about the network resource gathered from the remote device as structured data.
+ returned: when I(state) is C(gathered)
+ type: list
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+parsed:
+ description: The device native config provided in I(running_config) option parsed into structured data as per module argspec.
+ returned: when I(state) is C(parsed)
+ type: list
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible_collections.cisco.ios.plugins.module_utils.network.ios.argspec.line.line import (
+ LineArgs,
+)
+from ansible_collections.cisco.ios.plugins.module_utils.network.ios.config.line.line import Line
+
+
+def main():
+ """
+ Main entry point for module execution
+
+ :returns: the result form module invocation
+ """
+ module = AnsibleModule(
+ argument_spec=LineArgs.argument_spec,
+ mutually_exclusive=[["config", "running_config"]],
+ required_if=[
+ ["state", "merged", ["config"]],
+ ["state", "replaced", ["config"]],
+ ["state", "overridden", ["config"]],
+ ["state", "rendered", ["config"]],
+ ["state", "parsed", ["running_config"]],
+ ],
+ supports_check_mode=True,
+ )
+
+ result = Line(module).execute_module()
+ module.exit_json(**result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/integration/targets/ios_line/defaults/main.yaml b/tests/integration/targets/ios_line/defaults/main.yaml
new file mode 100644
index 000000000..164afead2
--- /dev/null
+++ b/tests/integration/targets/ios_line/defaults/main.yaml
@@ -0,0 +1,3 @@
+---
+testcase: "[^_].*"
+test_items: []
diff --git a/tests/integration/targets/ios_line/meta/main.yaml b/tests/integration/targets/ios_line/meta/main.yaml
new file mode 100644
index 000000000..23d65c7ef
--- /dev/null
+++ b/tests/integration/targets/ios_line/meta/main.yaml
@@ -0,0 +1,2 @@
+---
+dependencies: []
diff --git a/tests/integration/targets/ios_line/tasks/cli.yaml b/tests/integration/targets/ios_line/tasks/cli.yaml
new file mode 100644
index 000000000..6f505600c
--- /dev/null
+++ b/tests/integration/targets/ios_line/tasks/cli.yaml
@@ -0,0 +1,21 @@
+---
+- name: Collect all CLI test cases
+ ansible.builtin.find:
+ paths: "{{ role_path }}/tests/cli"
+ patterns: "{{ testcase }}.yaml"
+ use_regex: true
+ register: test_cases
+ delegate_to: localhost
+
+- name: Set test_items
+ ansible.builtin.set_fact:
+ test_items: "{{ test_cases.files | map(attribute='path') | list }}"
+ delegate_to: localhost
+
+- name: Run test case (connection=ansible.netcommon.network_cli)
+ ansible.builtin.include_tasks: "{{ test_case_to_run }}"
+ vars:
+ ansible_connection: ansible.netcommon.network_cli
+ with_items: "{{ test_items }}"
+ loop_control:
+ loop_var: test_case_to_run
diff --git a/tests/integration/targets/ios_line/tasks/main.yaml b/tests/integration/targets/ios_line/tasks/main.yaml
new file mode 100644
index 000000000..ba7233b9e
--- /dev/null
+++ b/tests/integration/targets/ios_line/tasks/main.yaml
@@ -0,0 +1,5 @@
+---
+- name: Main task for lines
+ ansible.builtin.include_tasks: cli.yaml
+ tags:
+ - network_cli
diff --git a/tests/integration/targets/ios_line/tests/cli/_parsed.cfg b/tests/integration/targets/ios_line/tests/cli/_parsed.cfg
new file mode 100644
index 000000000..9de97b17b
--- /dev/null
+++ b/tests/integration/targets/ios_line/tests/cli/_parsed.cfg
@@ -0,0 +1,27 @@
+line con 0
+ session-timeout 15
+ exec-timeout 60 0
+ authorization commands 1 console
+ authorization commands 15 console
+ authorization exec console
+ login authentication console
+ escape-character 3
+ stopbits 1
+line aux 0
+ stopbits 1
+line vty 0 4
+ session-timeout 15
+ exec-timeout 60 0
+ logging synchronous
+ length 0
+ transport preferred none
+ transport input ssh
+ transport output ssh
+ escape-character 3
+line vty 5 15
+ session-timeout 15
+ exec-timeout 60 0
+ transport preferred none
+ transport input ssh
+ transport output ssh
+ escape-character 3
diff --git a/tests/integration/targets/ios_line/tests/cli/_populate_config.yaml b/tests/integration/targets/ios_line/tests/cli/_populate_config.yaml
new file mode 100644
index 000000000..9262a662e
--- /dev/null
+++ b/tests/integration/targets/ios_line/tests/cli/_populate_config.yaml
@@ -0,0 +1,9 @@
+---
+- name: Populate configuration
+ vars:
+ lines:
+ "line con 0\n session-timeout 15\n login authentication limited\n escape-character 3\n stopbits 1\n
+ line aux 0\n stopbits 1\n
+ line vty 0 4\n session-timeout 15\n exec-timeout 60 0\n password 7 02050D480809\n login authentication remote\n logging synchronous\n transport input ssh telnet"
+ ansible.netcommon.cli_config:
+ config: "{{ lines }}"
diff --git a/tests/integration/targets/ios_line/tests/cli/_populate_config_replaced.yaml b/tests/integration/targets/ios_line/tests/cli/_populate_config_replaced.yaml
new file mode 100644
index 000000000..b72720ecb
--- /dev/null
+++ b/tests/integration/targets/ios_line/tests/cli/_populate_config_replaced.yaml
@@ -0,0 +1,9 @@
+---
+- name: Populate configuration
+ vars:
+ lines:
+ "line con 0\n session-timeout 15\n login authentication limited\n escape-character 3\n stopbits 1\n
+ line aux 0\n stopbits 1\n no exec\n transport input ssh\n stopbits 1\n
+ line vty 0 4\n session-timeout 15\n exec-timeout 60 0\n password 7 02050D480809\n login authentication remote\n logging synchronous\n transport input ssh telnet"
+ ansible.netcommon.cli_config:
+ config: "{{ lines }}"
diff --git a/tests/integration/targets/ios_line/tests/cli/_remove_config.yaml b/tests/integration/targets/ios_line/tests/cli/_remove_config.yaml
new file mode 100644
index 000000000..bcb6e9339
--- /dev/null
+++ b/tests/integration/targets/ios_line/tests/cli/_remove_config.yaml
@@ -0,0 +1,5 @@
+---
+- name: Remove all configuration line
+ cisco.ios.ios_line:
+ config:
+ state: deleted
diff --git a/tests/integration/targets/ios_line/tests/cli/deleted.yaml b/tests/integration/targets/ios_line/tests/cli/deleted.yaml
new file mode 100644
index 000000000..dc2dabb85
--- /dev/null
+++ b/tests/integration/targets/ios_line/tests/cli/deleted.yaml
@@ -0,0 +1,86 @@
+---
+- ansible.builtin.debug:
+ msg: Start Deleted integration state for ios_line ansible_connection={{ ansible_connection }}
+
+- ansible.builtin.include_tasks: _remove_config.yaml
+
+- ansible.builtin.include_tasks: _populate_config.yaml
+
+- block:
+ - name: Delete attributes of provided configured line
+ register: result
+ cisco.ios.ios_line: &id001
+ config:
+ lines:
+ - name: "con 0"
+ - name: "vty 5 15"
+ state: deleted
+
+ - ansible.builtin.assert:
+ that:
+ - result.commands|length == 5
+ - result.changed == true
+ - result.commands|symmetric_difference(deleted.commands) == []
+
+ - name: Delete configured lines (idempotent)
+ register: result
+ cisco.ios.ios_line: *id001
+ - name: Assert that the previous task was idempotent
+ ansible.builtin.assert:
+ that:
+ - result.commands|length == 0
+ - result.changed == false
+
+ - ansible.builtin.include_tasks: _remove_config.yaml
+
+ - ansible.builtin.include_tasks: _populate_config.yaml
+
+ - name: Delete line attributes base on line number
+ register: result
+ cisco.ios.ios_line: &id002
+ config:
+ lines:
+ - name: "vty 0 4"
+ state: deleted
+
+ - ansible.builtin.assert:
+ that:
+ - result.commands|length == 7
+ - result.changed == true
+ - result.commands|symmetric_difference(deleted_line.commands) == []
+
+ - name: Delete line attributes base on line number (idempotent)
+ register: result
+ cisco.ios.ios_line: *id002
+ - name: Assert that the previous task was idempotent
+ ansible.builtin.assert:
+ that:
+ - result.commands|length == 0
+ - result.changed == false
+
+ - ansible.builtin.include_tasks: _remove_config.yaml
+
+ - ansible.builtin.include_tasks: _populate_config.yaml
+
+ - name: Delete all configured lines
+ register: result
+ cisco.ios.ios_line: &id003
+ state: deleted
+
+ - ansible.builtin.assert:
+ that:
+ - result.commands|length == 12
+ - result.changed == true
+ - result.commands|symmetric_difference(deleted_all.commands) == []
+
+ - name: Delete all configured lines (idempotent)
+ register: result
+ cisco.ios.ios_line: *id003
+ - name: Assert that the previous task was idempotent
+ ansible.builtin.assert:
+ that:
+ - result.commands|length == 0
+ - result.changed == false
+
+ always:
+ - ansible.builtin.include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/ios_line/tests/cli/empty_config.yaml b/tests/integration/targets/ios_line/tests/cli/empty_config.yaml
new file mode 100644
index 000000000..ba00d6353
--- /dev/null
+++ b/tests/integration/targets/ios_line/tests/cli/empty_config.yaml
@@ -0,0 +1,47 @@
+---
+- ansible.builtin.debug:
+ msg: START ios_line empty_config.yaml integration tests on connection={{ ansible_connection }}
+
+- name: Merged with empty configuration should give appropriate error message
+ register: result
+ ignore_errors: true
+ cisco.ios.ios_line:
+ config:
+ state: merged
+
+- ansible.builtin.assert:
+ that:
+ - result.msg == 'value of config parameter must not be empty for state merged'
+
+- name: Overridden with empty configuration should give appropriate error message
+ register: result
+ ignore_errors: true
+ cisco.ios.ios_line:
+ config:
+ state: overridden
+
+- ansible.builtin.assert:
+ that:
+ - result.msg == 'value of config parameter must not be empty for state overridden'
+
+- name: Rendered with empty configuration should give appropriate error message
+ register: result
+ ignore_errors: true
+ cisco.ios.ios_line:
+ config:
+ state: rendered
+
+- ansible.builtin.assert:
+ that:
+ - result.msg == 'value of config parameter must not be empty for state rendered'
+
+- name: Parsed with empty configuration should give appropriate error message
+ register: result
+ ignore_errors: true
+ cisco.ios.ios_line:
+ running_config:
+ state: parsed
+
+- ansible.builtin.assert:
+ that:
+ - result.msg == 'value of running_config parameter must not be empty for state parsed'
diff --git a/tests/integration/targets/ios_line/tests/cli/gathered.yaml b/tests/integration/targets/ios_line/tests/cli/gathered.yaml
new file mode 100644
index 000000000..04507df9d
--- /dev/null
+++ b/tests/integration/targets/ios_line/tests/cli/gathered.yaml
@@ -0,0 +1,21 @@
+---
+- ansible.builtin.debug:
+ msg: START ios_line gathered integration tests on connection={{ ansible_connection }}
+
+- ansible.builtin.include_tasks: _remove_config.yaml
+
+- ansible.builtin.include_tasks: _populate_config.yaml
+
+- block:
+ - name: Gather the provided configuration with the existing running configuration
+ register: result
+ cisco.ios.ios_line:
+ config:
+ state: gathered
+
+ - ansible.builtin.assert:
+ that:
+ - gathered['config'] == result.gathered
+ - result['changed'] == false
+ always:
+ - ansible.builtin.include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/ios_line/tests/cli/merged.yaml b/tests/integration/targets/ios_line/tests/cli/merged.yaml
new file mode 100644
index 000000000..309576844
--- /dev/null
+++ b/tests/integration/targets/ios_line/tests/cli/merged.yaml
@@ -0,0 +1,84 @@
+---
+- ansible.builtin.debug:
+ msg: START Merged ios_line state for integration tests on connection={{ ansible_connection }}
+
+- ansible.builtin.include_tasks: _remove_config.yaml
+
+- block:
+ - name: Merge initial configuration with device configuration
+ cisco.ios.ios_line:
+ config:
+ lines:
+ - name: "con 0"
+ escape_character:
+ value: "3"
+ stopbits: 1
+ session:
+ timeout: 5
+ - name: "vty 0 4"
+ escape_character:
+ value: "3"
+ stopbits: 1
+ session:
+ timeout: 5
+
+ - name: Merge new configuration with existing device configuration
+ register: result
+ cisco.ios.ios_line: &id001
+ config:
+ lines:
+ - name: "con 0"
+ session:
+ timeout: "5"
+ exec:
+ timeout: "60"
+ authorization:
+ exec: "console"
+ - name: "vty 0 4"
+ escape_character:
+ value: "3"
+ stopbits: 1
+ session:
+ timeout: "15"
+ exec:
+ timeout: "60"
+ transport:
+ - name: preferred
+ none: true
+ - name: input
+ ssh: true
+ - name: output
+ ssh: true
+ - name: "vty 5 15"
+ escape_character:
+ value: "3"
+ stopbits: 1
+ session:
+ timeout: "15"
+ exec:
+ timeout: "60"
+ transport:
+ - name: preferred
+ none: true
+ - name: input
+ ssh: true
+ - name: output
+ ssh: true
+ state: merged
+
+ - ansible.builtin.assert:
+ that:
+ - result.commands|length == 16
+ - result.changed == true
+ - result.commands|symmetric_difference(merged.commands) == []
+
+ - name: Merge provided configuration with device configuration (idempotent)
+ register: result
+ cisco.ios.ios_line: *id001
+ - name: Assert that the previous task was idempotent
+ ansible.builtin.assert:
+ that:
+ - result.commands|length == 0
+ - result['changed'] == false
+ always:
+ - ansible.builtin.include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/ios_line/tests/cli/overridden.yaml b/tests/integration/targets/ios_line/tests/cli/overridden.yaml
new file mode 100644
index 000000000..8e2b69361
--- /dev/null
+++ b/tests/integration/targets/ios_line/tests/cli/overridden.yaml
@@ -0,0 +1,82 @@
+---
+- ansible.builtin.debug:
+ msg: START Overridden ios_line state for integration tests on connection={{ ansible_connection }}
+
+- ansible.builtin.include_tasks: _remove_config.yaml
+
+- ansible.builtin.include_tasks: _populate_config.yaml
+
+- block:
+ - name: Override device configuration of all interfaces with provided configuration
+ register: result
+ cisco.ios.ios_line: &id001
+ config:
+ lines:
+ - name: "con 0"
+ exec:
+ timeout: "60"
+ session:
+ timeout: "15"
+ escape_character:
+ value: "3"
+ stopbits: 1
+ authorization:
+ exec: "console"
+ commands:
+ - level: 1
+ command: console
+ - level: 15
+ command: console
+ login: "console"
+ - name: "vty 0 4"
+ authorization:
+ exec: "default"
+ login: "default"
+ session:
+ timeout: "{{ line_vty.session_timeout|d(15) }}"
+ exec:
+ timeout: "{{ line_vty.exec_timeout|d(60) }}"
+ escape_character:
+ value: "3"
+ transport:
+ - name: preferred
+ none: true
+ - name: input
+ ssh: true
+ - name: output
+ ssh: true
+ - name: "vty 5 15"
+ authorization:
+ exec: "default"
+ login: "default"
+ session:
+ timeout: "{{ line_vty.session_timeout|d(15) }}"
+ exec:
+ timeout: "{{ line_vty.exec_timeout|d(60) }}"
+ escape_character:
+ value: "3"
+ transport:
+ - name: preferred
+ none: true
+ - name: input
+ ssh: true
+ - name: output
+ ssh: true
+ state: overridden
+
+ - ansible.builtin.assert:
+ that:
+ - result.commands|length == 21
+ - result.changed == true
+ - result.commands|symmetric_difference(overridden.commands) == []
+
+ - name: Override device configuration of all interfaces with provided configuration (idempotent)
+ register: result
+ cisco.ios.ios_line: *id001
+ - name: Assert that task was idempotent
+ ansible.builtin.assert:
+ that:
+ - result.commands|length == 0
+ - result['changed'] == false
+ always:
+ - ansible.builtin.include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/ios_line/tests/cli/parsed.yaml b/tests/integration/targets/ios_line/tests/cli/parsed.yaml
new file mode 100644
index 000000000..88c82a8ef
--- /dev/null
+++ b/tests/integration/targets/ios_line/tests/cli/parsed.yaml
@@ -0,0 +1,20 @@
+---
+- ansible.builtin.debug:
+ msg: START ios_line parsed integration tests on connection={{ ansible_connection }}
+
+- name: Parse the commands for provided configuration
+ become: true
+ register: result
+ cisco.ios.ios_line:
+ running_config: "{{ lookup('file', '_parsed.cfg') }}"
+ state: parsed
+
+- ansible.builtin.debug:
+ var: parsed['config']
+- ansible.builtin.debug:
+ var: result.parsed
+
+- ansible.builtin.assert:
+ that:
+ - result.changed == false
+ - parsed['config'] == result.parsed
diff --git a/tests/integration/targets/ios_line/tests/cli/rendered.yaml b/tests/integration/targets/ios_line/tests/cli/rendered.yaml
new file mode 100644
index 000000000..2c2221b8d
--- /dev/null
+++ b/tests/integration/targets/ios_line/tests/cli/rendered.yaml
@@ -0,0 +1,22 @@
+---
+- ansible.builtin.debug:
+ msg: Start ios_line rendered integration tests ansible_connection={{ ansible_connection }}
+
+- block:
+ - name: Rendered the provided configuration with the existing running configuration
+ register: result
+ cisco.ios.ios_line:
+ config:
+ lines:
+ - name: "con 0"
+ stopbits: "1"
+ escape_character:
+ value: "3"
+ - name: "aux 0"
+ stopbits: "1"
+ state: rendered
+
+ - ansible.builtin.assert:
+ that:
+ - result.changed == false
+ - result.rendered|symmetric_difference(rendered.commands) == []
diff --git a/tests/integration/targets/ios_line/tests/cli/replaced.yaml b/tests/integration/targets/ios_line/tests/cli/replaced.yaml
new file mode 100644
index 000000000..e9f03f62c
--- /dev/null
+++ b/tests/integration/targets/ios_line/tests/cli/replaced.yaml
@@ -0,0 +1,65 @@
+---
+- ansible.builtin.debug:
+ msg: START Replaced ios_line state for integration tests on connection={{ ansible_connection }}
+
+- ansible.builtin.include_tasks: _remove_config.yaml
+
+- ansible.builtin.include_tasks: _populate_config_replaced.yaml
+
+- block:
+ - name: Replaced device configuration of all lines listed with provided configuration
+ register: result
+ cisco.ios.ios_line: &id001
+ config:
+ lines:
+ - name: "con 0"
+ exec:
+ timeout: "60"
+ session:
+ timeout: "15"
+ escape_character:
+ value: "3"
+ stopbits: 1
+ authorization:
+ exec: "console"
+ commands:
+ - level: 1
+ command: console
+ - level: 15
+ command: console
+ login: "console"
+ - name: "vty 0 4"
+ authorization:
+ exec: "default"
+ login: "default"
+ session:
+ timeout: "{{ line_vty.session_timeout|d(15) }}"
+ exec:
+ timeout: "{{ line_vty.exec_timeout|d(60) }}"
+ escape_character:
+ value: "3"
+ transport:
+ - name: preferred
+ none: true
+ - name: input
+ ssh: true
+ - name: output
+ ssh: true
+ state: replaced
+
+ - ansible.builtin.assert:
+ that:
+ - result.commands|length == 14
+ - result.changed == true
+ - result.commands|symmetric_difference(replaced.commands) == []
+
+ - name: Replaced device configuration of all lines listed with provided configuration (idempotent)
+ register: result
+ cisco.ios.ios_line: *id001
+ - name: Assert that task was idempotent
+ ansible.builtin.assert:
+ that:
+ - result.commands|length == 0
+ - result['changed'] == false
+ always:
+ - ansible.builtin.include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/ios_line/vars/main.yml b/tests/integration/targets/ios_line/vars/main.yml
new file mode 100644
index 000000000..4e9e3e984
--- /dev/null
+++ b/tests/integration/targets/ios_line/vars/main.yml
@@ -0,0 +1,196 @@
+---
+deleted:
+ commands:
+ - "line con 0"
+ - "escape-character DEFAULT"
+ - "login authentication default"
+ - "no stopbits 1"
+ - "no session-timeout 15"
+deleted_line:
+ commands:
+ - "line vty 0 4"
+ - "exec-timeout 10 0"
+ - "transport input ssh"
+ - "login authentication default"
+ - "no logging synchronous"
+ - "no password 7 ********"
+ - "no session-timeout 15"
+deleted_all:
+ commands:
+ - "line con 0"
+ - "escape-character DEFAULT"
+ - "login authentication default"
+ - "no session-timeout 15"
+ - "no stopbits 1"
+ - "line vty 0 4"
+ - "exec-timeout 10 0"
+ - "transport input ssh"
+ - "login authentication default"
+ - "no password 7 ********"
+ - "no logging synchronous"
+ - "no session-timeout 15"
+
+gathered:
+ config:
+ lines:
+ - name: "con 0"
+ escape_character:
+ value: "3"
+ login: "limited"
+ motd: true
+ session:
+ timeout: 15
+ stopbits: "1"
+ - name: "aux 0"
+ login: "default"
+ motd: true
+ transport:
+ - name: input
+ ssh: true
+ - name: "vty 0 4"
+ exec:
+ timeout: 60
+ logging:
+ enable: true
+ login: "remote"
+ motd: true
+ password:
+ hash: 7
+ value: "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ session:
+ timeout: 15
+ transport:
+ - name: input
+ ssh: true
+ telnet: true
+
+merged:
+ commands:
+ - "line con 0"
+ - "authorization exec console"
+ - "exec-timeout 60 0"
+ - "line vty 0 4"
+ - "exec-timeout 60 0"
+ - "transport output ssh"
+ - "transport preferred none"
+ - "session-timeout 15"
+ - "line vty 5 15"
+ - "exec-timeout 60 0"
+ - "escape-character 3"
+ - "session-timeout 15"
+ - "stopbits 1"
+ - "transport preferred none"
+ - "transport input ssh"
+ - "transport output ssh"
+
+overridden:
+ commands:
+ - "line con 0"
+ - "authorization exec console"
+ - "authorization commands 1 console"
+ - "authorization commands 15 console"
+ - "exec-timeout 60 0"
+ - "login authentication console"
+ - "line vty 0 4"
+ - "transport input ssh"
+ - "transport output ssh"
+ - "transport preferred none"
+ - "escape-character 3"
+ - "login authentication default"
+ - "no logging synchronous"
+ - "no password 7 ********"
+ - "line vty 5 15"
+ - "exec-timeout 60 0"
+ - "escape-character 3"
+ - "session-timeout 15"
+ - "transport preferred none"
+ - "transport input ssh"
+ - "transport output ssh"
+
+replaced:
+ commands:
+ - "line con 0"
+ - "authorization exec console"
+ - "authorization commands 1 console"
+ - "authorization commands 15 console"
+ - "exec-timeout 60 0"
+ - "login authentication console"
+ - "line vty 0 4"
+ - "escape-character 3"
+ - "login authentication default"
+ - "transport preferred none"
+ - "transport input ssh"
+ - "transport output ssh"
+ - "no password 7 ********"
+ - "no logging synchronous"
+
+parsed:
+ config:
+ lines:
+ - name: "con 0"
+ authorization:
+ arap: "default"
+ exec: "console"
+ commands:
+ - level: 1
+ command: console
+ - level: 15
+ command: console
+ reverse_access: "default"
+ login: "console"
+ motd: true
+ session:
+ timeout: 15
+ exec:
+ timeout: 60
+ escape_character:
+ value: "3"
+ stopbits: "1"
+ - name: "aux 0"
+ motd: true
+ login: "default"
+ stopbits: "1"
+ - name: "vty 0 4"
+ motd: true
+ login: "default"
+ logging:
+ enable: true
+ length: 0
+ session:
+ timeout: 15
+ exec:
+ timeout: 60
+ escape_character:
+ value: "3"
+ transport:
+ - name: preferred
+ none: true
+ - name: input
+ ssh: true
+ - name: output
+ ssh: true
+ - name: "vty 5 15"
+ motd: true
+ login: "default"
+ session:
+ timeout: 15
+ exec:
+ timeout: 60
+ escape_character:
+ value: "3"
+ transport:
+ - name: preferred
+ none: true
+ - name: input
+ ssh: true
+ - name: output
+ ssh: true
+
+rendered:
+ commands:
+ - "line con 0"
+ - "escape-character 3"
+ - "stopbits 1"
+ - "line aux 0"
+ - "transport input ssh"
+ - "stopbits 1"
diff --git a/tests/unit/modules/network/ios/fixtures/ios_line_config.cfg b/tests/unit/modules/network/ios/fixtures/ios_line_config.cfg
new file mode 100644
index 000000000..481374820
--- /dev/null
+++ b/tests/unit/modules/network/ios/fixtures/ios_line_config.cfg
@@ -0,0 +1,18 @@
+!
+line con 0
+ session-timeout 5
+ exec-timeout 60 0
+ authorization exec CON
+ login authentication CON
+ escape-character 3
+ stopbits 1
+!
+line vty 0 4
+ session-timeout 5
+ exec-timeout 60 0
+ logging synchronous
+ transport preferred none
+ transport input ssh
+ transport output ssh
+ escape-character 3
+!
diff --git a/tests/unit/modules/network/ios/test_ios_line.py b/tests/unit/modules/network/ios/test_ios_line.py
new file mode 100644
index 000000000..6152839db
--- /dev/null
+++ b/tests/unit/modules/network/ios/test_ios_line.py
@@ -0,0 +1,903 @@
+#
+# (c) 2021, Ansible by Red Hat, inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from textwrap import dedent
+
+from ansible_collections.cisco.ios.plugins.modules import ios_line
+from ansible_collections.cisco.ios.tests.unit.compat.mock import patch
+from ansible_collections.cisco.ios.tests.unit.modules.utils import set_module_args
+
+from .ios_module import TestIosModule
+
+
+class TestIosLineModule(TestIosModule):
+ module = ios_line
+
+ def setUp(self):
+ super(TestIosLineModule, self).setUp()
+ self.mock_get_resource_connection_facts = patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module_base."
+ "get_resource_connection",
+ )
+ self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start()
+
+ self.mock_execute_show_command = patch(
+ "ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.line.line."
+ "LineFacts.get_line_data",
+ )
+ self.execute_show_command = self.mock_execute_show_command.start()
+
+ def tearDown(self):
+ super(TestIosLineModule, self).tearDown()
+ self.mock_get_resource_connection_facts.stop()
+ self.mock_execute_show_command.stop()
+
+ def test_ios_line_parsed(self):
+ set_module_args(
+ dict(
+ running_config=dedent(
+ """\
+ line con 0
+ session-timeout 5
+ exec-timeout 60 0
+ authorization exec CON
+ login authentication CON
+ escape-character 3
+ stopbits 1
+ line vty 0 4
+ session-timeout 5
+ exec-timeout 60 0
+ logging synchronous
+ transport preferred none
+ transport input telnet ssh
+ transport output ssh
+ escape-character 3
+ """,
+ ),
+ state="parsed",
+ ),
+ )
+ parsed = {
+ "lines": [
+ {
+ "name": "con 0",
+ "authorization": {
+ "arap": "default",
+ "exec": "CON",
+ "reverse_access": "default",
+ },
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "login": "CON",
+ "motd": True,
+ "session": {
+ "timeout": 5,
+ },
+ "stopbits": "1",
+ },
+ {
+ "name": "vty 0 4",
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "logging": {
+ "enable": True,
+ },
+ "login": "default",
+ "motd": True,
+ "session": {
+ "timeout": 5,
+ },
+ "transport": [
+ {
+ "name": "preferred",
+ "none": True,
+ },
+ {
+ "name": "input",
+ "telnet": True,
+ "ssh": True,
+ },
+ {
+ "name": "output",
+ "ssh": True,
+ },
+ ],
+ },
+ ],
+ }
+
+ result = self.execute_module(changed=False)
+ self.assertEqual(parsed, result["parsed"])
+
+ def test_ios_line_gathered(self):
+ self.execute_show_command.return_value = dedent(
+ """\
+ line con 0
+ session-timeout 5
+ exec-timeout 60 0
+ authorization exec CON
+ login authentication CON
+ escape-character 3
+ stopbits 1
+ line vty 0 4
+ session-timeout 5
+ exec-timeout 60 0
+ logging synchronous
+ transport preferred none
+ transport input telnet ssh
+ transport output ssh
+ escape-character 3
+ """,
+ )
+ set_module_args(dict(state="gathered"))
+ gathered = {
+ "lines": [
+ {
+ "name": "con 0",
+ "authorization": {
+ "arap": "default",
+ "exec": "CON",
+ "reverse_access": "default",
+ },
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "login": "CON",
+ "motd": True,
+ "session": {
+ "timeout": 5,
+ },
+ "stopbits": "1",
+ },
+ {
+ "name": "vty 0 4",
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "logging": {
+ "enable": True,
+ },
+ "login": "default",
+ "motd": True,
+ "session": {
+ "timeout": 5,
+ },
+ "transport": [
+ {
+ "name": "preferred",
+ "none": True,
+ },
+ {
+ "name": "input",
+ "telnet": True,
+ "ssh": True,
+ },
+ {
+ "name": "output",
+ "ssh": True,
+ },
+ ],
+ },
+ ],
+ }
+
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+ self.assertEqual(gathered, result["gathered"])
+
+ def test_ios_line_rendered(self):
+ set_module_args(
+ {
+ "config": {
+ "lines": [
+ {
+ "name": "con 0",
+ "authorization": {
+ "arap": "default",
+ "exec": "CON",
+ "reverse_access": "default",
+ },
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "login": "CON",
+ "motd": True,
+ "session": {
+ "timeout": 5,
+ },
+ "stopbits": "1",
+ },
+ {
+ "name": "vty 0 4",
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "logging": {
+ "enable": True,
+ },
+ "login": "default",
+ "motd": True,
+ "session": {
+ "timeout": 5,
+ },
+ "transport": [
+ {
+ "name": "preferred",
+ "none": True,
+ },
+ {
+ "name": "input",
+ "telnet": True,
+ "ssh": True,
+ },
+ {
+ "name": "output",
+ "ssh": True,
+ },
+ ],
+ },
+ ],
+ },
+ "state": "rendered",
+ },
+ )
+ rendered = [
+ "line con 0",
+ "session-timeout 5",
+ "exec-timeout 60 0",
+ "authorization exec CON",
+ "login authentication CON",
+ "escape-character 3",
+ "stopbits 1",
+ "line vty 0 4",
+ "session-timeout 5",
+ "exec-timeout 60 0",
+ "logging synchronous",
+ "transport preferred none",
+ "transport input telnet ssh",
+ "transport output ssh",
+ "escape-character 3",
+ ]
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+
+ self.assertEqual(sorted(result["rendered"]), sorted(rendered))
+
+ def test_ios_line_merged_idempotent(self):
+ self.execute_show_command.return_value = dedent(
+ """\
+ line con 0
+ session-timeout 5
+ exec-timeout 60 0
+ authorization exec CON
+ login authentication CON
+ escape-character 3
+ stopbits 1
+ line vty 0 4
+ session-timeout 5
+ exec-timeout 60 0
+ logging synchronous
+ transport preferred none
+ transport input telnet ssh
+ transport output ssh
+ escape-character 3
+ """,
+ )
+
+ playbook = {
+ "config": {
+ "lines": [
+ {
+ "name": "con 0",
+ "authorization": {
+ "exec": "CON",
+ },
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "login": "CON",
+ "session": {
+ "timeout": 5,
+ },
+ "stopbits": "1",
+ },
+ {
+ "name": "vty 0 4",
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "logging": {
+ "enable": True,
+ },
+ "login": "default",
+ "session": {
+ "timeout": 5,
+ },
+ "transport": [
+ {
+ "name": "preferred",
+ "none": True,
+ },
+ {
+ "name": "input",
+ "telnet": True,
+ "ssh": True,
+ },
+ {
+ "name": "output",
+ "ssh": True,
+ },
+ ],
+ },
+ ],
+ },
+ }
+
+ merged = []
+ playbook["state"] = "merged"
+ set_module_args(playbook)
+ result = self.execute_module()
+
+ self.assertEqual(sorted(result["commands"]), sorted(merged))
+
+ def test_ios_line_merged(self):
+ self.execute_show_command.return_value = dedent(
+ """\
+ line con 0
+ session-timeout 5
+ exec-timeout 60 0
+ stopbits 1
+ line vty 0 4
+ session-timeout 5
+ exec-timeout 60 0
+ transport input ssh
+ """,
+ )
+
+ playbook = {
+ "config": {
+ "lines": [
+ {
+ "name": "con 0",
+ "authorization": {
+ "exec": "CON",
+ },
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "login": "CON",
+ "session": {
+ "timeout": 5,
+ },
+ "stopbits": "1",
+ },
+ {
+ "name": "vty 0 4",
+ "access_classes_in": {
+ "name": "mgmt",
+ "vrf_also": True,
+ },
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "logging": {
+ "enable": True,
+ },
+ "login": "default",
+ "session": {
+ "timeout": 5,
+ },
+ "transport": [
+ {
+ "name": "preferred",
+ "none": True,
+ },
+ {
+ "name": "input",
+ "telnet": True,
+ "ssh": True,
+ },
+ {
+ "name": "output",
+ "ssh": True,
+ },
+ ],
+ },
+ ],
+ },
+ }
+
+ merged = [
+ "line con 0",
+ "authorization exec CON",
+ "login authentication CON",
+ "escape-character 3",
+ "line vty 0 4",
+ "access-class mgmt in vrf-also",
+ "logging synchronous",
+ "transport preferred none",
+ "transport input telnet ssh",
+ "transport output ssh",
+ "escape-character 3",
+ ]
+ playbook["state"] = "merged"
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+
+ self.assertEqual(sorted(result["commands"]), sorted(merged))
+
+ def test_ios_line_overridden(self):
+ self.execute_show_command.return_value = dedent(
+ """\
+ line con 0
+ session-timeout 5
+ exec-timeout 60 0
+ authorization commands 15 CON
+ password 7 02050D480809
+ length 0
+ stopbits 1
+ line vty 0 4
+ access-class filter in
+ password 7 02050D480809
+ transport input telnet
+ line vty 5 15
+ password 7 02050D480809
+ length 0
+ transport input telnet
+ """,
+ )
+ playbook = {
+ "config": {
+ "lines": [
+ {
+ "name": "con 0",
+ "authorization": {
+ "exec": "CON",
+ },
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "login": "CON",
+ "session": {
+ "timeout": 5,
+ },
+ "stopbits": "1",
+ },
+ {
+ "name": "vty 0 4",
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "logging": {
+ "enable": True,
+ },
+ "login": "default",
+ "session": {
+ "timeout": 5,
+ },
+ "transport": [
+ {
+ "name": "preferred",
+ "none": True,
+ },
+ {
+ "name": "input",
+ "telnet": True,
+ "ssh": True,
+ },
+ {
+ "name": "output",
+ "ssh": True,
+ },
+ ],
+ },
+ ],
+ },
+ }
+ overridden = [
+ "line con 0",
+ "authorization exec CON",
+ "login authentication CON",
+ "escape-character 3",
+ "no authorization commands 15 CON",
+ "no length 0",
+ "no password 7 02050D480809",
+ "line vty 0 4",
+ "exec-timeout 60 0",
+ "logging synchronous",
+ "transport preferred none",
+ "transport input telnet ssh",
+ "transport output ssh",
+ "escape-character 3",
+ "session-timeout 5",
+ "no access-class filter in",
+ "no password 7 02050D480809",
+ "no line vty 5 15",
+ "do terminal length 0",
+ ]
+ playbook["state"] = "overridden"
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+
+ self.assertEqual(sorted(result["commands"]), sorted(overridden))
+
+ def test_ios_line_overridden_idempotent(self):
+ self.execute_show_command.return_value = dedent(
+ """\
+ line con 0
+ session-timeout 5
+ exec-timeout 60 0
+ authorization exec CON
+ login authentication CON
+ escape-character 3
+ stopbits 1
+ line vty 0 4
+ session-timeout 5
+ exec prompt expand
+ exec-timeout 60 0
+ logging synchronous
+ transport preferred none
+ transport input telnet ssh
+ transport output ssh
+ escape-character 3
+ """,
+ )
+ playbook = {
+ "config": {
+ "lines": [
+ {
+ "name": "con 0",
+ "authorization": {
+ "exec": "CON",
+ },
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "login": "CON",
+ "session": {
+ "timeout": 5,
+ },
+ "stopbits": "1",
+ },
+ {
+ "name": "vty 0 4",
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "prompt": {
+ "expand": True,
+ },
+ "timeout": 60,
+ },
+ "logging": {
+ "enable": True,
+ },
+ "login": "default",
+ "session": {
+ "timeout": 5,
+ },
+ "transport": [
+ {
+ "name": "preferred",
+ "none": True,
+ },
+ {
+ "name": "input",
+ "telnet": True,
+ "ssh": True,
+ },
+ {
+ "name": "output",
+ "ssh": True,
+ },
+ ],
+ },
+ ],
+ },
+ }
+ overridden = []
+ playbook["state"] = "overridden"
+ set_module_args(playbook)
+ result = self.execute_module(changed=False)
+
+ self.assertEqual(sorted(result["commands"]), sorted(overridden))
+
+ def test_ios_line_replaced(self):
+ self.execute_show_command.return_value = dedent(
+ """\
+ line con 0
+ session-timeout 5
+ exec-timeout 60 0
+ authorization commands 15 CON
+ password 7 02050D480809
+ length 0
+ stopbits 1
+ line aux 0
+ no exec
+ transport input none
+ stopbits 1
+ line vty 0 4
+ access-class filter in
+ password 7 02050D480809
+ transport input telnet
+ line vty 5 15
+ password 7 02050D480809
+ length 0
+ transport input telnet
+ """,
+ )
+ playbook = {
+ "config": {
+ "lines": [
+ {
+ "name": "con 0",
+ "authorization": {
+ "exec": "CON",
+ },
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "login": "CON",
+ "session": {
+ "timeout": 5,
+ },
+ "stopbits": "1",
+ },
+ {
+ "name": "vty 0 4",
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "logging": {
+ "enable": True,
+ },
+ "login": "default",
+ "session": {
+ "timeout": 5,
+ },
+ "transport": [
+ {
+ "name": "preferred",
+ "none": True,
+ },
+ {
+ "name": "input",
+ "telnet": True,
+ "ssh": True,
+ },
+ {
+ "name": "output",
+ "ssh": True,
+ },
+ ],
+ },
+ ],
+ },
+ }
+ replaced = [
+ "line con 0",
+ "authorization exec CON",
+ "login authentication CON",
+ "escape-character 3",
+ "no authorization commands 15 CON",
+ "no length 0",
+ "no password 7 02050D480809",
+ "line vty 0 4",
+ "exec-timeout 60 0",
+ "logging synchronous",
+ "transport preferred none",
+ "transport input telnet ssh",
+ "transport output ssh",
+ "escape-character 3",
+ "session-timeout 5",
+ "no access-class filter in",
+ "no password 7 02050D480809",
+ "do terminal length 0",
+ ]
+ playbook["state"] = "replaced"
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+
+ self.assertEqual(sorted(result["commands"]), sorted(replaced))
+
+ def test_ios_line_replaced_idempotent(self):
+ self.execute_show_command.return_value = dedent(
+ """\
+ line con 0
+ session-timeout 5
+ exec-timeout 60 0
+ authorization exec CON
+ login authentication CON
+ escape-character 3
+ stopbits 1
+ line aux 0
+ no exec
+ transport input none
+ stopbits 1
+ line vty 0 4
+ session-timeout 5
+ exec prompt expand
+ exec-timeout 60 0
+ logging synchronous
+ transport preferred none
+ transport input telnet ssh
+ transport output ssh
+ escape-character 3
+ """,
+ )
+ playbook = {
+ "config": {
+ "lines": [
+ {
+ "name": "con 0",
+ "authorization": {
+ "exec": "CON",
+ },
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "timeout": 60,
+ },
+ "login": "CON",
+ "session": {
+ "timeout": 5,
+ },
+ "stopbits": "1",
+ },
+ {
+ "name": "vty 0 4",
+ "escape_character": {
+ "value": "3",
+ },
+ "exec": {
+ "prompt": {
+ "expand": True,
+ },
+ "timeout": 60,
+ },
+ "logging": {
+ "enable": True,
+ },
+ "login": "default",
+ "session": {
+ "timeout": 5,
+ },
+ "transport": [
+ {
+ "name": "preferred",
+ "none": True,
+ },
+ {
+ "name": "input",
+ "telnet": True,
+ "ssh": True,
+ },
+ {
+ "name": "output",
+ "ssh": True,
+ },
+ ],
+ },
+ ],
+ },
+ }
+ replaced = []
+ playbook["state"] = "replaced"
+ set_module_args(playbook)
+ result = self.execute_module(changed=False)
+
+ self.assertEqual(sorted(result["commands"]), sorted(replaced))
+
+ def test_ios_line_deleted(self):
+ self.execute_show_command.return_value = dedent(
+ """\
+ line con 0
+ session-timeout 5
+ exec-timeout 60 0
+ authorization exec CON
+ login authentication CON
+ escape-character 3
+ stopbits 1
+ line aux 0
+ no exec
+ transport input ssh
+ line vty 0 4
+ session-timeout 5
+ exec-timeout 60 0
+ logging synchronous
+ transport preferred none
+ transport input telnet ssh
+ transport output ssh
+ escape-character 3
+ line vty 5 15
+ session-timeout 5
+ exec-timeout 60 0
+ logging synchronous
+ transport preferred none
+ transport input telnet ssh
+ transport output ssh
+ escape-character 3
+ """,
+ )
+
+ playbook = {"config": {}}
+ deleted = [
+ "line con 0",
+ "no session-timeout 5",
+ "exec-timeout 10 0",
+ "authorization exec default",
+ "login authentication default",
+ "escape-character DEFAULT",
+ "no stopbits 1",
+ "line vty 0 4",
+ "no session-timeout 5",
+ "exec-timeout 10 0",
+ "no logging synchronous",
+ "default transport preferred",
+ "transport input ssh",
+ "default transport output",
+ "escape-character DEFAULT",
+ "no line vty 5 15",
+ ]
+ playbook["state"] = "deleted"
+ set_module_args(playbook)
+ self.maxDiff = None
+ result = self.execute_module(changed=True)
+
+ self.assertEqual(sorted(result["commands"]), sorted(deleted))