diff --git a/README.md b/README.md index 6ac06df519..f174b22bb1 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Name | Description [cisco.ios.ios_ping](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_ping_module.rst)|Tests reachability using ping from IOS switch. [cisco.ios.ios_prefix_lists](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_prefix_lists_module.rst)|Resource module to configure prefix lists. [cisco.ios.ios_route_maps](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_route_maps_module.rst)|Resource module to configure route maps. +[cisco.ios.ios_service](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_service_module.rst)|Resource module to configure service. [cisco.ios.ios_snmp_server](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_snmp_server_module.rst)|Resource module to configure snmp server. [cisco.ios.ios_static_routes](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_static_routes_module.rst)|Resource module to configure static routes. [cisco.ios.ios_system](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_system_module.rst)|Module to manage the system attributes. diff --git a/changelogs/fragments/ios_service_changes.yml b/changelogs/fragments/ios_service_changes.yml new file mode 100644 index 0000000000..5cd5546421 --- /dev/null +++ b/changelogs/fragments/ios_service_changes.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - ios_service - Create module to manage service configuration on IOS switches diff --git a/meta/runtime.yml b/meta/runtime.yml index 7d4fea2dea..e7c99024e5 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -99,6 +99,8 @@ plugin_routing: redirect: cisco.ios.ios route_maps: redirect: cisco.ios.ios + service: + redirect: cisco.ios.ios snmp_server: redirect: cisco.ios.ios static_routes: @@ -201,6 +203,8 @@ plugin_routing: redirect: cisco.ios.ios_prefix_lists route_maps: redirect: cisco.ios.ios_route_maps + service: + redirect: cisco.ios.ios_service snmp_server: redirect: cisco.ios.ios_snmp_server static_routes: diff --git a/plugins/action/service.py b/plugins/action/service.py new file mode 120000 index 0000000000..7747aa9dd1 --- /dev/null +++ b/plugins/action/service.py @@ -0,0 +1 @@ +ios.py \ No newline at end of file diff --git a/plugins/module_utils/network/ios/argspec/service/__init__.py b/plugins/module_utils/network/ios/argspec/service/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/module_utils/network/ios/argspec/service/service.py b/plugins/module_utils/network/ios/argspec/service/service.py new file mode 100644 index 0000000000..76d2a3e346 --- /dev/null +++ b/plugins/module_utils/network/ios/argspec/service/service.py @@ -0,0 +1,121 @@ +# -*- 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_service module +""" + + +class ServiceArgs(object): # pylint: disable=R0903 + """The arg spec for the ios_service module""" + + argument_spec = { + "config": { + "options": { + "call_home": {"type": "bool"}, + "compress_config": {"type": "bool"}, + "config": {"type": "bool"}, + "counters": {"type": "int", "default": 0}, + "dhcp": {"type": "bool", "default": True}, + "disable_ip_fast_frag": {"type": "bool"}, + "exec_callback": {"type": "bool"}, + "exec_wait": {"type": "bool"}, + "hide_telnet_addresses": {"type": "bool"}, + "internal": {"type": "bool"}, + "linenumber": {"type": "bool"}, + "log": {"type": "bool"}, + "log_hidden": {"type": "bool"}, + "nagle": {"type": "bool"}, + "old_slip_prompts": {"type": "bool"}, + "pad": {"type": "bool"}, + "pad_cmns": {"type": "bool"}, + "pad_from_xot": {"type": "bool"}, + "pad_to_xot": {"type": "bool"}, + "password_encryption": {"type": "bool"}, + "password_recovery": {"type": "bool", "default": True}, + "prompt": {"type": "bool", "default": True}, + "private_config_encryption": {"type": "bool"}, + "pt_vty_logging": {"type": "bool"}, + "scripting": {"type": "bool"}, + "sequence_numbers": {"type": "bool"}, + "slave_coredump": {"type": "bool"}, + "slave_log": {"type": "bool", "default": True}, + "tcp_keepalives_in": {"type": "bool"}, + "tcp_keepalives_out": {"type": "bool"}, + "tcp_small_servers": { + "options": { + "enable": {"type": "bool"}, + "max_servers": {"type": "str"}, + }, + "type": "dict", + }, + "telnet_zeroidle": {"type": "bool"}, + "timestamps": { + "elements": "dict", + "options": { + "msg": {"choices": ["debug", "log"], "type": "str"}, + "enable": {"type": "bool"}, + "timestamp": { + "choices": ["datetime", "uptime"], + "type": "str", + }, + "datetime_options": { + "options": { + "localtime": {"type": "bool"}, + "msec": {"type": "bool"}, + "show_timezone": {"type": "bool"}, + "year": {"type": "bool"}, + }, + "type": "dict", + }, + }, + "type": "list", + }, + "udp_small_servers": { + "options": { + "enable": {"type": "bool"}, + "max_servers": {"type": "str"}, + }, + "type": "dict", + }, + "unsupported_transceiver": {"type": "bool"}, + }, + "type": "dict", + }, + "running_config": {"type": "str"}, + "state": { + "choices": [ + "merged", + "replaced", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + "type": "str", + }, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/ios/config/service/__init__.py b/plugins/module_utils/network/ios/config/service/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/module_utils/network/ios/config/service/service.py b/plugins/module_utils/network/ios/config/service/service.py new file mode 100644 index 0000000000..23e3329be4 --- /dev/null +++ b/plugins/module_utils/network/ios/config/service/service.py @@ -0,0 +1,152 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The ios_service class +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 __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from copy import deepcopy + +from ansible.module_utils.six import iteritems +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, +) + +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.service import ( + ServiceTemplate, +) + + +class Service(ResourceModule): + """ + The ios_service class + """ + + def __init__(self, module): + super(Service, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="service", + tmplt=ServiceTemplate(), + ) + self.parsers = [ + "call_home", + "compress_config", + "config", + "counters", + "dhcp", + "disable_ip_fast_frag", + "exec_callback", + "exec_wait", + "hide_telnet_addresses", + "internal", + "linenumber", + "log", + "log_hidden", + "nagle", + "old_slip_prompts", + "pad", + "pad_cmns", + "pad_from_xot", + "pad_to_xot", + "password_encryption", + "password_recovery", + "private_config_encryption", + "prompt", + "pt_vty_logging", + "scripting", + "sequence_numbers", + "slave_coredump", + "slave_log", + "tcp_keepalives_in", + "tcp_keepalives_out", + "telnet_zeroidle", + "unsupported_transceiver", + ] + + 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 = self._service_list_to_dict(self.want) + haved = self._service_list_to_dict(self.have) + + service_default = { + "counters": 0, + "dhcp": True, + "prompt": True, + "slave_log": True, + "password_recovery": True, + "private_config_encryption": True, + } + + # 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 + elif self.state == "deleted": + wantd = self._service_list_to_dict(service_default) + + # if state is replaced + elif self.state == "replaced": + wantd = dict_merge(self._service_list_to_dict(service_default), wantd) + + self._compare(want=wantd, have=haved) + + 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 Service network resource. + """ + self.compare(parsers=self.parsers, want=want, have=have) + self._compare_lists_attrs(want, have) + + def _compare_lists_attrs(self, want, have): + """Compare list of dict""" + i_want = want.get("timestamps", {}) + i_have = have.get("timestamps", {}) + for key, wanting in iteritems(i_want): + haveing = i_have.pop(key, {}) + if wanting != haveing: + self.addcmd(wanting, "timestamps") + for key, haveing in iteritems(i_have): + self.addcmd(haveing, "timestamps", negate=True) + + def _service_list_to_dict(self, data): + """Convert all list of dicts to dicts of dicts""" + p_key = { + "timestamps": "msg", + } + tmp_data = deepcopy(data) + for k, _v in p_key.items(): + if k in tmp_data: + tmp_data[k] = {str(i[p_key.get(k)]): i for i in tmp_data[k]} + return tmp_data diff --git a/plugins/module_utils/network/ios/facts/facts.py b/plugins/module_utils/network/ios/facts/facts.py index 9b82862460..726a2de955 100644 --- a/plugins/module_utils/network/ios/facts/facts.py +++ b/plugins/module_utils/network/ios/facts/facts.py @@ -81,6 +81,9 @@ from ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.route_maps.route_maps import ( Route_mapsFacts, ) +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.service.service import ( + ServiceFacts, +) from ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.snmp_server.snmp_server import ( Snmp_serverFacts, ) @@ -116,6 +119,7 @@ route_maps=Route_mapsFacts, prefix_lists=Prefix_listsFacts, ntp_global=Ntp_globalFacts, + service=ServiceFacts, snmp_server=Snmp_serverFacts, hostname=HostnameFacts, ) diff --git a/plugins/module_utils/network/ios/facts/service/__init__.py b/plugins/module_utils/network/ios/facts/service/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/module_utils/network/ios/facts/service/service.py b/plugins/module_utils/network/ios/facts/service/service.py new file mode 100644 index 0000000000..df2dcf9934 --- /dev/null +++ b/plugins/module_utils/network/ios/facts/service/service.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 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_service 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.service.service import ( + ServiceArgs, +) +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.rm_templates.service import ( + ServiceTemplate, +) + + +class ServiceFacts(object): + """The ios service facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = ServiceArgs.argument_spec + + def get_service_data(self, connection): + return connection.get("show running-config all | section ^service ") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Service 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_service_data(connection) + + # parse native config using the ServiceTemplate + service_parser = ServiceTemplate(lines=data.splitlines(), module=self._module) + objs = service_parser.parse() + + ansible_facts["ansible_network_resources"].pop("service", None) + + params = utils.remove_empties( + service_parser.validate_config(self.argument_spec, {"config": objs}, redact=True), + ) + + facts["service"] = params.get("config", {}) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/plugins/module_utils/network/ios/rm_templates/service.py b/plugins/module_utils/network/ios/rm_templates/service.py new file mode 100644 index 0000000000..992892db8b --- /dev/null +++ b/plugins/module_utils/network/ios/rm_templates/service.py @@ -0,0 +1,457 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 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 Service 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 ServiceTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + super(ServiceTemplate, self).__init__(lines=lines, tmplt=self, module=module) + + # fmt: off + PARSERS = [ + { + "name": "call_home", + "getval": re.compile( + r""" + ^service\s(?Pcall-home) + """, re.VERBOSE, + ), + "setval": "service call-home", + "result": { + "call_home": "{{ not not call_home }}", + }, + }, + { + "name": "compress_config", + "getval": re.compile( + r""" + ^service\s(?Pcompress-config) + """, re.VERBOSE, + ), + "setval": "service compress-config", + "result": { + "compress_config": "{{ not not compress_config }}", + }, + }, + { + "name": "config", + "getval": re.compile( + r""" + ^service\s(?Pconfig) + """, re.VERBOSE, + ), + "setval": "service config", + "result": { + "config": "{{ not not config }}", + }, + }, + { + "name": "counters", + "getval": re.compile( + r""" + ^service\scounters\smax\sage(\s(?P\d+))? + """, re.VERBOSE, + ), + "setval": "service counters max age {{ counters }}", + "result": { + "counters": "{{ max_age|int }}", + }, + }, + { + "name": "dhcp", + "getval": re.compile( + r""" + ^service\sdhcp + """, re.VERBOSE, + ), + "setval": "service dhcp", + "result": { + "dhcp": True, + }, + }, + { + "name": "disable_ip_fast_frag", + "getval": re.compile( + r""" + ^service\s(?Pdisable-ip-fast-frag) + """, re.VERBOSE, + ), + "setval": "service disable-ip-fast-frag", + "result": { + "disable_ip_fast_frag": "{{ not not disable_ip_fast_frag }}", + }, + }, + { + "name": "exec_callback", + "getval": re.compile( + r""" + ^service\s(?Pexec-callback) + """, re.VERBOSE, + ), + "setval": "service exec-callback", + "result": { + "exec_callback": "{{ not not exec_callback }}", + }, + }, + { + "name": "exec_wait", + "getval": re.compile( + r""" + ^service\s(?Pexec-wait) + """, re.VERBOSE, + ), + "setval": "service exec-wait", + "return": { + "exec_wait": "{{ not not exec_wait }}", + }, + }, + { + "name": "hide_telnet_addresses", + "getval": re.compile( + r""" + ^service\s(?Phide-telnet-addresses) + """, re.VERBOSE, + ), + "setval": "service hide-telnet-addresses", + "result": { + "hide_telnet_addresses": "{{ not not hide_telnet_addresses }}", + }, + }, + { + "name": "internal", + "getval": re.compile( + r""" + ^service\s(?Pinternal) + """, re.VERBOSE, + ), + "setval": "service internal", + "result": { + "internal": "{{ not not internal }}", + }, + }, + { + "name": "linenumber", + "getval": re.compile( + r""" + ^service\s(?Plinenumber) + """, re.VERBOSE, + ), + "setval": "service linenumber", + "result": { + "linenumber": "{{ not not linenumber }}", + }, + }, + { + "name": "log", + "getval": re.compile( + r""" + ^service\slog(\s(?Pbacktrace))? + """, re.VERBOSE, + ), + "setval": "service log backtrace", + "result": { + "log": "{{ not not backtrace }}", + }, + }, + { + "name": "log_hidden", + "getval": re.compile( + r""" + ^service\s(?Plog-hidden) + """, re.VERBOSE, + ), + "setval": "service log-hidden", + "result": { + "log_hidden": "{{ not not log_hidden }}", + }, + }, + { + "name": "nagle", + "getval": re.compile( + r""" + ^service\s(?Pnagle) + """, re.VERBOSE, + ), + "setval": "service nagle", + "result": { + "nagle": "{{ not not nagle }}", + }, + }, + { + "name": "old_slip_prompts", + "getval": re.compile( + r""" + ^service\s(?Pold-slip-prompts) + """, re.VERBOSE, + ), + "setval": "service old-slip-prompts", + "result": { + "old_slip_prompts": "{{ not not old_slip_prompts }}", + }, + }, + { + "name": "pad", + "getval": re.compile( + r""" + ^service\spad$ + """, re.VERBOSE, + ), + "setval": "service pad", + "result": { + "pad": True, + }, + }, + { + "name": "pad_cmns", + "getval": re.compile( + r""" + ^service\spad\scmns + """, re.VERBOSE, + ), + "setval": "service pad cmns", + "result": { + "pad_cmns": True, + }, + }, + { + "name": "pad_from_xot", + "getval": re.compile( + r""" + ^service\spad\sfrom-xot + """, re.VERBOSE, + ), + "setval": "service pad from-xot", + "result": { + "pad_from_xot": True, + }, + }, + { + "name": "pad_to_xot", + "getval": re.compile( + r""" + ^service\spad\sto-xot + """, re.VERBOSE, + ), + "setval": "service pad to-xot", + "result": { + "pad_to_xot": True, + }, + }, + { + "name": "password_encryption", + "getval": re.compile( + r""" + ^service\s(?Ppassword-encryption) + """, re.VERBOSE, + ), + "setval": "service password-encryption", + "result": { + "password_encryption": "{{ not not password_encryption }}", + }, + }, + { + "name": "password_recovery", + "getval": re.compile( + r""" + ^service\spassword-recovery + """, re.VERBOSE, + ), + "setval": "service password-recovery", + "remval": "service password-recovery\nyes", + "result": { + "password_recovery": True, + }, + }, + { + "name": "prompt", + "getval": re.compile( + r""" + ^service\sprompt\sconfig + """, re.VERBOSE, + ), + "setval": "service prompt config", + "result": { + "prompt": True, + }, + }, + { + "name": "private_config_encryption", + "getval": re.compile( + r""" + ^service\sprivate-config-encryption + """, re.VERBOSE, + ), + "setval": "service private-config-encryption", + "result": { + "private_config_encryption": True, + }, + }, + { + "name": "pt_vty_logging", + "getval": re.compile( + r""" + ^service\s(?Ppt-vty-logging) + """, re.VERBOSE, + ), + "setval": "service pt-vty-logging", + "result": { + "pt_vty_logging": "{{ not not pt_vty_logging }}", + }, + }, + { + "name": "scripting", + "getval": re.compile( + r""" + ^service\s(?Pscripting) + """, re.VERBOSE, + ), + "setval": "service scripting", + "result": { + "scripting": "{{ not not scripting }}", + }, + }, + { + "name": "sequence_numbers", + "getval": re.compile( + r""" + ^service\s(?Psequence-numbers) + """, re.VERBOSE, + ), + "setval": "service sequence-numbers", + "result": { + "sequence_numbers": "{{ not not sequence_numbers }}", + }, + }, + { + "name": "slave_coredump", + "getval": re.compile( + r""" + ^service\s(?Pslave-coredump) + """, re.VERBOSE, + ), + "setval": "service slave-coredump", + "result": { + "slave_coredump": "{{ not not slave_coredump }}", + }, + }, + { + "name": "slave_log", + "getval": re.compile( + r""" + ^service\sslave-log + """, re.VERBOSE, + ), + "setval": "service slave-log", + "result": { + "slave_log": True, + }, + }, + { + "name": "tcp_keepalives_in", + "getval": re.compile( + r""" + ^service\stcp-keepalives-in + """, re.VERBOSE, + ), + "setval": "service tcp-keepalives-in", + "result": { + "tcp_keepalives_in": True, + }, + }, + { + "name": "tcp_keepalives_out", + "getval": re.compile( + r""" + ^service\stcp-keepalives-out + """, re.VERBOSE, + ), + "setval": "service tcp-keepalives-out", + "result": { + "tcp_keepalives_out": True, + }, + }, + { + "name": "telnet_zeroidle", + "getval": re.compile( + r""" + ^service\stelnet-zeroidle + """, re.VERBOSE, + ), + "setval": "service telnet-zeroidle", + "result": { + "telnet_zeroidle": True, + }, + }, + { + "name": "timestamps", + "getval": re.compile( + r""" + ^service\stimestamps + (\s(?P\S+))? + (\s(?P\S+))? + (\s(?Pmsec))? + (\s(?Plocaltime))? + (\s(?Pshow-timezone))? + (\s(?Pyear))? + """, re.VERBOSE, + ), + "remval": "service timestamps{{ (' ' + msg) if msg is defined else '' }}", + "setval": "service timestamps" + "{{ (' ' + msg) if msg is defined else '' }}" + "{% if msg is defined %}" + "{{ (' ' + timestamp) if timestamp is defined else '' }}" + "{% if timestamp == 'datetime' and datetime_options is defined %}" + "{{ ' msec' if datetime_options.msec else '' }}" + "{{ ' localtime' if datetime_options.localtime else '' }}" + "{{ ' show-timezone' if datetime_options.show_timezone else '' }}" + "{{ ' year' if datetime_options.year else '' }}" + "{% endif %}" + "{% endif %}" + "", + "result": { + "timestamps": [ + { + "msg": "{{ msg if msg is defined else 'debug' }}", + "timestamp": "{{ timestamp if timestamp is defined else 'uptime' }}", + "datetime_options": { + "msec": "{{ True if msec else False}}", + "localtime": "{{ True if localtime else False }}", + "show_timezone": "{{ True if show_timezone else False }}", + "year": "{{ True if year else False }}", + }, + }, + ], + }, + }, + { + "name": "unsupported_transceiver", + "getval": re.compile( + r""" + ^service\sunsupported-transceiver + """, re.VERBOSE, + ), + "setval": "service unsupported-transceiver", + "result": { + "unsupported_transceiver": True, + }, + }, + ] diff --git a/plugins/modules/ios_service.py b/plugins/modules/ios_service.py new file mode 100644 index 0000000000..89fe2ed479 --- /dev/null +++ b/plugins/modules/ios_service.py @@ -0,0 +1,690 @@ +#!/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_service +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: ios_service +short_description: Resource module to configure service. +description: + - This module configures and manages service attributes on IOS platforms +version_added: 4.6.0 +author: + - Ambroise Rosset (@earendilfr) +notes: + - Tested against Cisco IOSXE Version 16.9 + - This module works with connection C(network_cli). + See U(https://docs.ansible.com/ansible/latest/network/user_guide/platform_ios.html) +options: + config: + description: A dictionnary of service configuration + suboptions: + call_home: + description: Cisco call-home service + type: bool + compress_config: + description: Compress the nvram configuration file + type: bool + config: + description: TFTP load config files + type: bool + counters: + description: + - Control aging of interface counters by setting the maximum counter aging threshold + type: int + default: 0 + dhcp: + description: Enable DHCP server and relay agent + type: bool + default: true + disable_ip_fast_frag: + description: Disable IP particle-based fast fragmentation + type: bool + exec_callback: + description: Enable exec callback + type: bool + exec_wait: + description: Delay EXEC startup on noisy lines + type: bool + hide_telnet_addresses: + description: Hide destination addresses in telnet command + type: bool + internal: + description: Enable/Disable Internal commands + type: bool + linenumber: + description: enable line number banner for each exec + type: bool + log: + description: log backtrace + type: bool + log_hidden: + description: Enable syslog msgs for hidden/internal commands + type: bool + nagle: + description: Enable Nagle's congestion control algorithm + type: bool + old_slip_prompts: + description: Allow old scripts to operate with slip/ppp + type: bool + pad: + description: Enable PAD commands + type: bool + pad_cmns: + description: Enable PAD over CMNS connections + type: bool + pad_from_xot: + description: Accept XOT to PAD connections + type: bool + pad_to_xot: + description: Allow outgoing PAD over XOT connections + type: bool + password_encryption: + description: Encrypt system passwords + type: bool + password_recovery: + description: Password recovery + type: bool + default: true + prompt: + description: Enable mode specific prompt + type: bool + default: true + private_config_encryption: + description: Enable private config file encryption + type: bool + pt_vty_logging: + description: Log significant VTY-Async events + type: bool + scripting: + description: scripting + type: bool + sequence_numbers: + description: Stamp logger messages with a sequence number + type: bool + slave_coredump: + description: slave-coredump + type: bool + slave_log: + description: Enable log capability of slave IPs + type: bool + default: true + tcp_keepalives_in: + description: Generate keepalives on idle incoming network connections + type: bool + tcp_keepalives_out: + description: Generate keepalives on idle outgoing network connections + type: bool + tcp_small_servers: + description: + - TCP and UDP small servers are servers (daemons, in Unix parlance) that run in the + router which are useful for diagnostics. + suboptions: + enable: + description: Enable small TCP servers (e.g., ECHO) + type: bool + max_servers: + description: + - Set number of allowable TCP small servers + - 1 to 2147483647 or no-limit + type: str + type: dict + telnet_zeroidle: + description: Set TCP window 0 when connection is idle + type: bool + timestamps: + description: Timestamp debug/log messages + elements: dict + suboptions: + msg: + description: Timestamp log or debug messages + choices: + - debug + - log + type: str + enable: + description: Enable timestamp for the choosen message + type: bool + timestamp: + description: Timestamp with date and time or with system uptime + choices: + - datetime + - uptime + type: str + datetime_options: + description: Options for date and time timestamp + suboptions: + localtime: + description: Use local time zone for timestamps + type: bool + msec: + description: Include milliseconds in timestamp + type: bool + show_timezone: + description: Add time zone information to timestamp + type: bool + year: + description: Include year in timestam + type: bool + type: dict + type: list + udp_small_servers: + description: + - TCP and UDP small servers are servers (daemons, in Unix parlance) that run in the + router which are useful for diagnostics. + suboptions: + enable: + description: Enable small UDP servers (e.g., ECHO) + type: bool + max_servers: + description: + - Set number of allowable TCP small servers + - 1 to 2147483647 or no-limit + type: str + type: dict + unsupported_transceiver: + description: enable support for third-party transceivers + type: bool + type: dict + 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 running-config | section ^service|^no service). + - 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: + choices: + - merged + - replaced + - deleted + - gathered + - rendered + - parsed + default: merged + description: + - The state the configuration should be left in. + - Refer to examples for more details. + type: str +""" + +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- + +# router-ios#show running-config all | section ^service +# service slave-log +# service timestamps debug datetime msec +# service timestamps log datetime msec +# service private-config-encryption +# service prompt config +# service counters max age 0 +# service dhcp +# service call-home +# service password-recovery + +- name: Merge provided configuration with device configuration + cisco.ios.ios_service: + config: + tcp_keepalives_in: true + tcp_keepalives_out: true + timestamps: + - msg: debug + enable: true + timestamp: datetime + - msg: log + enable: true + timestamp: datetime + pad: false + password_encryption: true + state: merged + +# Task Output +# ----------- +# +# before: +# call_home: true +# counters: 0 +# dhcp: true +# password_recovery: true +# private_config_encryption: true +# prompt: true +# slave_log: true +# timestamps: +# - datetime_options: +# msec: true +# msg: debug +# timestamp: datetime +# - datetime_options: +# msec: true +# msg: log +# timestamp: datetime +# commands: +# - service password-encryption +# - service tcp-keepalives-in +# - service tcp-keepalives-out +# after: +# call_home: true +# counters: 0 +# dhcp: true +# password_encryption: true +# password_recovery: true +# private_config_encryption: true +# prompt: true +# slave_log: true +# tcp_keepalives_in: true +# tcp_keepalives_out: true +# timestamps: +# - datetime_options: +# msec: true +# msg: debug +# timestamp: datetime +# - datetime_options: +# msec: true +# msg: log +# timestamp: datetime + +# After state: +# ------------ + +# router-ios#show running-config all | section ^service +# service slave-log +# service tcp-keepalives-in +# service tcp-keepalives-out +# service timestamps debug datetime msec +# service timestamps log datetime msec +# service password-encryption +# service private-config-encryption +# service prompt config +# service counters max age 0 +# service dhcp +# service call-home +# service password-recovery + +# Using replaced + +# Before state: +# ------------- + +# router-ios#show running-config all | section ^service +# service slave-log +# service tcp-keepalives-in +# service tcp-keepalives-out +# service timestamps debug datetime msec +# service timestamps log datetime msec +# service password-encryption +# service private-config-encryption +# service prompt config +# service counters max age 0 +# service dhcp +# service call-home +# service password-recovery + +- name: Replaces device configuration of services with provided configuration + cisco.ios.ios_service: + config: + timestamps: + - msg: log + enable: true + timestamp: datetime + datetime_options: + localtime: true + msec: true + show_timezone: true + year: true + - msg: debug + enable: true + timestamp: datetime + pad: false + password_encryption: true + state: "replaced" + +# Task Output +# ----------- +# +# before: +# call_home: true +# counters: 0 +# dhcp: true +# password_encryption: true +# password_recovery: true +# private_config_encryption: true +# prompt: true +# slave_log: true +# tcp_keepalives_in: true +# tcp_keepalives_out: true +# timestamps: +# - datetime_options: +# msec: true +# msg: debug +# timestamp: datetime +# - datetime_options: +# msec: true +# msg: log +# timestamp: datetime +# commands: +# - no service call-home +# - no service tcp-keepalives-in +# - no service tcp-keepalives-out +# - no service timestamps log +# - service timestamps log datetime msec localtime show-timezone year +# - no service timestamps debug +# - service timestamps debug datetime +# after: +# counters: 0 +# dhcp: true +# password_encryption: true +# password_recovery: true +# private_config_encryption: true +# prompt: true +# slave_log: true +# timestamps: +# - msg: debug +# timestamp: datetime +# - datetime_options: +# localtime: true +# msec: true +# show_timezone: true +# year: true +# msg: log +# timestamp: datetime + +# After state: +# ------------ + +# router-ios#show running-config all | section ^service +# service slave-log +# service timestamps debug datetime +# service timestamps log datetime msec localtime show-timezone year +# service password-encryption +# service private-config-encryption +# service prompt config +# service counters max age 0 +# service dhcp +# service password-recovery + +# Using Deleted + +# Before state: +# ------------- + +# router-ios#show running-config all | section ^service +# service slave-log +# service timestamps debug datetime +# service timestamps log datetime msec localtime show-timezone year +# service password-encryption +# service private-config-encryption +# service prompt config +# service counters max age 0 +# service dhcp +# service password-recovery + +- name: "Delete service configuration and restore default configuration for some importants service (those with a default value in module)" + cisco.ios.ios_service: + state: deleted + +# Task Output +# ----------- +# +# before: +# counters: 0 +# dhcp: true +# password_encryption: true +# password_recovery: true +# private_config_encryption: true +# prompt: true +# slave_log: true +# timestamps: +# - msg: debug +# timestamp: datetime +# - datetime_options: +# localtime: true +# msec: true +# show_timezone: true +# year: true +# msg: log +# timestamp: datetime +# commands: +# - no service password-encryption +# - no service timestamps debug +# - no service timestamps log +# after: +# counters: 0 +# dhcp: true +# password_recovery: true +# private_config_encryption: true +# prompt: true +# slave_log: true + +#·After·state: +#·------------ +# +# router-ios#show running-config all | section ^service +# service slave-log +# service private-config-encryption +# service prompt config +# service counters max age 0 +# service dhcp +# service password-recovery + +# Using gathered + +# Before state: +# ------------- +# +# router-ios#show running-config all | section ^service +# service slave-log +# service timestamps debug datetime +# service timestamps log datetime msec localtime show-timezone year +# service password-encryption +# service private-config-encryption +# service prompt config +# service counters max age 0 +# service dhcp +# service password-recovery + +- name: Gather facts of interfaces + cisco.ios.ios_service: + config: + state: gathered + +# Task Output +# ----------- +# +# gathered: +# counters: 0 +# dhcp: true +# password_encryption: true +# password_recovery: true +# private_config_encryption: true +# prompt: true +# slave_log: true +# timestamps: +# - msg: debug +# timestamp: datetime +# - datetime_options: +# localtime: true +# msec: true +# show_timezone: true +# year: true +# msg: log +# timestamp: datetime + +# Using rendered + +- name: Render the commands for provided configuration + cisco.ios.ios_service: + config: + timestamps: + - msg: log + enable: true + timestamp: datetime + datetime_options: + localtime: true + msec: true + show_timezone: true + year: true + - msg: debug + enable: true + timestamp: datetime + pad: false + password_encryption: true + state: rendered + +# ·Task·Output +# ----------- +# +# rendered: +# - service dhcp +# - service password-encryption +# - service password-recovery +# - service prompt config +# - service slave-log +# - service timestamps log datetime msec localtime show-timezone year +# - service timestamps debug datetime + +# Using parsed + +# File: parsed.cfg +# ---------------- +# +# no service pad +# service password-encryption +# service tcp-keepalives-in +# service tcp-keepalives-out +# service timestamps debug datetime msec localtime show-timezone year +# service timestamps log datetime msec localtime show-timezone year +# service counters max age 5 + +- name: Parse the provided configuration + cisco.ios.ios_service: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task Output +# ----------- +# +# parsed: +# counters: 5 +# dhcp: true +# password_encryption: true +# password_recovery: true +# prompt: true +# slave_log: true +# tcp_keepalives_in: true +# tcp_keepalives_out: true +# timestamps: +# - datetime_options: +# localtime: true +# msec: true +# show_timezone: true +# year: true +# msg: debug +# timestamp: datetime +# - datetime_options: +# localtime: true +# msec: true +# show_timezone: true +# year: true +# msg: log +# timestamp: datetime +""" + +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: + - no service config + - service tcp-keepalives-in + - service tcp-keepalives-out +rendered: + description: The provided configuration in the task rendered in device-native format (offline). + returned: when I(state) is C(rendered) + type: list + sample: + - service dhcp + - service password-encryption + - service password-recovery + - service prompt config + - service slave-log +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.service.service import ( + ServiceArgs, +) +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.config.service.service import ( + Service, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=ServiceArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Service(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/modules/network/ios/test_ios_service.py b/tests/unit/modules/network/ios/test_ios_service.py new file mode 100644 index 0000000000..b77be44f20 --- /dev/null +++ b/tests/unit/modules/network/ios/test_ios_service.py @@ -0,0 +1,435 @@ +# +# (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_service +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 TestIosServiceModule(TestIosModule): + module = ios_service + + def setUp(self): + super(TestIosServiceModule, self).setUp() + + self.mock_get_config = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network.Config.get_config", + ) + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network.Config.load_config", + ) + self.load_config = self.mock_load_config.start() + + self.mock_get_resource_connection_config = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base." + "get_resource_connection", + ) + self.get_resource_connection_config = self.mock_get_resource_connection_config.start() + + 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_edit_config = patch( + "ansible_collections.cisco.ios.plugins.module_utils.network.ios.providers.providers.CliProvider.edit_config", + ) + self.edit_config = self.mock_edit_config.start() + + self.mock_execute_show_command = patch( + "ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.service.service." + "ServiceFacts.get_service_data", + ) + self.execute_show_command = self.mock_execute_show_command.start() + + def tearDown(self): + super(TestIosServiceModule, self).tearDown() + self.mock_get_resource_connection_config.stop() + self.mock_get_resource_connection_facts.stop() + self.mock_edit_config.stop() + self.mock_get_config.stop() + self.mock_load_config.stop() + self.mock_execute_show_command.stop() + + def test_ios_service_merged_idempotent(self): + self.execute_show_command.return_value = dedent( + """\ + service slave-log + service tcp-keepalives-in + service tcp-keepalives-out + service timestamps debug datetime msec + service timestamps log datetime msec + service private-config-encryption + service prompt config + service counters max age 0 + service dhcp + service call-home + service password-recovery + """, + ) + + playbook = { + "config": { + "call_home": True, + "tcp_keepalives_in": True, + "tcp_keepalives_out": True, + "timestamps": [ + { + "msg": "log", + "timestamp": "datetime", + "datetime_options": { + "msec": True, + }, + }, + { + "msg": "debug", + "timestamp": "datetime", + "datetime_options": { + "msec": True, + }, + }, + ], + }, + } + merged = [] + playbook["state"] = "merged" + set_module_args(playbook) + result = self.execute_module() + + self.assertEqual(sorted(result["commands"]), sorted(merged)) + + def test_ios_service_merged(self): + self.execute_show_command.return_value = dedent( + """\ + service slave-log + service tcp-keepalives-in + service tcp-keepalives-out + service timestamps debug datetime msec + service timestamps log datetime msec + service private-config-encryption + service prompt config + service counters max age 0 + service dhcp + service call-home + service password-recovery + """, + ) + + playbook = { + "config": { + "tcp_keepalives_in": True, + "tcp_keepalives_out": True, + "timestamps": [ + { + "msg": "log", + "timestamp": "datetime", + "datetime_options": { + "localtime": True, + "msec": True, + "show_timezone": True, + "year": True, + }, + }, + { + "msg": "debug", + "timestamp": "uptime", + }, + ], + "pad": False, + "password_encryption": True, + }, + } + merged = [ + "service timestamps debug uptime", + "service timestamps log datetime msec localtime show-timezone year", + "service password-encryption", + ] + playbook["state"] = "merged" + set_module_args(playbook) + result = self.execute_module(changed=True) + + self.assertEqual(sorted(result["commands"]), sorted(merged)) + + def test_ios_snm_server_deleted(self): + self.execute_show_command.return_value = dedent( + """\ + service slave-log + service tcp-keepalives-in + service tcp-keepalives-out + service timestamps debug datetime msec + service timestamps log datetime msec localtime show-timezone year + service password-encryption + service private-config-encryption + service prompt config + service counters max age 0 + service dhcp + service call-home + service password-recovery + """, + ) + playbook = {"config": {}} + deleted = [ + "no service tcp-keepalives-in", + "no service tcp-keepalives-out", + "no service timestamps debug", + "no service timestamps log", + "no service password-encryption", + "no service call-home", + ] + playbook["state"] = "deleted" + set_module_args(playbook) + self.maxDiff = None + result = self.execute_module(changed=True) + + self.assertEqual(sorted(result["commands"]), sorted(deleted)) + + def test_ios_service_replaced(self): + self.execute_show_command.return_value = dedent( + """\ + service call-home + service config + service counters max age 0 + service dhcp + service pad + service password-recovery + service private-config-encryption + service prompt config + service slave-log + service timestamps log datetime msec + """, + ) + playbook = { + "config": { + "timestamps": [ + { + "msg": "log", + "timestamp": "datetime", + "datetime_options": { + "localtime": True, + "msec": True, + "show_timezone": True, + "year": True, + }, + }, + { + "msg": "debug", + "timestamp": "datetime", + }, + ], + "tcp_keepalives_in": True, + "tcp_keepalives_out": True, + "password_encryption": True, + "counters": 5, + }, + } + replaced = [ + "no service call-home", + "no service config", + "no service pad", + "service counters max age 5", + "service password-encryption", + "service tcp-keepalives-in", + "service tcp-keepalives-out", + "service timestamps debug datetime", + "service timestamps log datetime msec localtime show-timezone year", + ] + playbook["state"] = "replaced" + set_module_args(playbook) + result = self.execute_module(changed=True) + + self.assertEqual(sorted(result["commands"]), sorted(replaced)) + + def test_ios_service_replaced_idempotent(self): + self.execute_show_command.return_value = dedent( + """\ + service slave-log + service timestamps debug datetime msec + service timestamps log datetime msec + service private-config-encryption + service prompt config + service counters max age 0 + service dhcp + service call-home + service password-recovery + """, + ) + playbook = { + "config": { + "call_home": True, + "private_config_encryption": True, + "timestamps": [ + { + "msg": "debug", + "timestamp": "datetime", + "datetime_options": { + "msec": True, + }, + }, + { + "msg": "log", + "timestamp": "datetime", + "datetime_options": { + "msec": True, + }, + }, + ], + }, + } + replaced = [] + playbook["state"] = "replaced" + set_module_args(playbook) + result = self.execute_module(changed=False) + self.maxDiff = None + + self.assertEqual(sorted(result["commands"]), sorted(replaced)) + + #################### + + def test_ios_service_parsed(self): + set_module_args( + dict( + running_config=dedent( + """\ + service slave-log + service timestamps debug datetime msec + service timestamps log datetime msec + service private-config-encryption + service prompt config + service counters max age 0 + service dhcp + service call-home + service password-recovery + """, + ), + state="parsed", + ), + ) + + parsed = { + "timestamps": [ + { + "msg": "debug", + "timestamp": "datetime", + "datetime_options": { + "msec": True, + }, + }, + { + "msg": "log", + "timestamp": "datetime", + "datetime_options": { + "msec": True, + }, + }, + ], + "prompt": True, + "private_config_encryption": True, + "counters": 0, + "dhcp": True, + "call_home": True, + "password_recovery": True, + "slave_log": True, + } + result = self.execute_module(changed=False) + self.maxDiff = None + + self.assertEqual(result["parsed"], parsed) + + def test_ios_service_gathered(self): + self.execute_show_command.return_value = dedent( + """\ + service timestamps log datetime msec localtime show-timezone year + service timestamps debug uptime + service call-home + """, + ) + set_module_args(dict(state="gathered")) + gathered = { + "timestamps": [ + { + "msg": "debug", + "timestamp": "uptime", + }, + { + "msg": "log", + "timestamp": "datetime", + "datetime_options": { + "localtime": True, + "msec": True, + "show_timezone": True, + "year": True, + }, + }, + ], + "call_home": True, + "dhcp": True, + "counters": 0, + "password_recovery": True, + "prompt": True, + "slave_log": True, + } + result = self.execute_module(changed=False) + self.maxDiff = None + + self.assertEqual(sorted(result["gathered"]), sorted(gathered)) + + def test_ios_service_rendered(self): + set_module_args( + { + "config": { + "call_home": True, + "counters": 5, + "config": False, + "pad": False, + "tcp_keepalives_in": True, + "tcp_keepalives_out": True, + "timestamps": [ + { + "msg": "debug", + "timestamp": "uptime", + }, + { + "msg": "log", + "timestamp": "datetime", + "datetime_options": { + "localtime": True, + "msec": True, + "show_timezone": True, + "year": True, + }, + }, + ], + }, + "state": "rendered", + }, + ) + rendered = [ + "service call-home", + "service counters max age 5", + "service dhcp", + "service password-recovery", + "service prompt config", + "service slave-log", + "service tcp-keepalives-in", + "service tcp-keepalives-out", + "service timestamps debug uptime", + "service timestamps log datetime msec localtime show-timezone year", + ] + result = self.execute_module(changed=False) + self.maxDiff = None + + self.assertEqual(sorted(result["rendered"]), sorted(rendered))