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))