diff --git a/lib/ansible/modules/network/ios/ios_ping.py b/lib/ansible/modules/network/ios/ios_ping.py new file mode 100644 index 00000000000..a61d78c6ae7 --- /dev/null +++ b/lib/ansible/modules/network/ios/ios_ping.py @@ -0,0 +1,212 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: ios_ping +short_description: Tests reachability using ping from IOS switch +description: +- Tests reachability using ping from switch to a remote destination. +author: +- Jacob McGill (@jmcgill298) +version_added: '2.4' +extends_documentation_fragment: ios +options: + count: + description: + - Number of packets to send. + required: false + default: 5 + dest: + description: + - The IP Address or hostname (resolvable by switch) of the remote node. + required: true + source: + description: + - The source IP Address. + required: false + default: null + state: + description: + - Determines if the expected result is success or fail. + choices: [ absent, present ] + default: present + vrf: + description: + - The VRF to use for forwarding. + required: false + default: default +''' + +EXAMPLES = r''' +- provider: + host: "{{ ansible_host }}" + username: "{{ username }}" + password: "{{ password }}" + +- name: Test reachability to 10.10.10.10 using default vrf + ios_ping: + provider: "{{ provider }}" + dest: 10.10.10.10 + +- name: Test reachability to 10.20.20.20 using prod vrf + ios_ping: + provider: "{{ provider }}" + dest: 10.20.20.20 + vrf: prod + +- name: Test unreachability to 10.30.30.30 using default vrf + ios_ping: + provider: "{{ provider }}" + dest: 10.30.30.30 + state: absent + +- name: Test reachability to 10.40.40.40 using prod vrf and setting count and source + ios_ping: + provider: "{{ provider }}" + dest: 10.40.40.40 + source: loopback0 + vrf: prod + count: 20 +''' + +RETURN = ''' +commands: + description: Show the command sent. + returned: always + type: list + sample: ["ping vrf prod 10.40.40.40 count 20 source loopback0"] +packet_loss: + description: Percentage of packets lost. + returned: always + type: str + sample: "0%" +packets_rx: + description: Packets successfully received. + returned: always + type: int + sample: 20 +packets_tx: + description: Packets successfully transmitted. + returned: always + type: int + sample: 20 +rtt: + description: Show RTT stats. + returned: always + type: dict + sample: {"avg": 2, "max": 8, "min": 1} +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ios import run_commands +from ansible.module_utils.ios import ios_argument_spec, check_args +import re + + +def main(): + """ main entry point for module execution + """ + argument_spec = dict( + count=dict(type="int"), + dest=dict(type="str", required=True), + source=dict(type="str"), + state=dict(type="str", choices=["absent", "present"], default="present"), + vrf=dict(type="str") + ) + + argument_spec.update(ios_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec) + + count = module.params["count"] + dest = module.params["dest"] + source = module.params["source"] + vrf = module.params["vrf"] + + warnings = list() + check_args(module, warnings) + + results = {} + if warnings: + results["warnings"] = warnings + + results["commands"] = [build_ping(dest, count, source, vrf)] + + ping_results = run_commands(module, commands=results["commands"]) + ping_results_list = ping_results[0].split("\n") + + success, rx, tx, rtt = parse_ping(ping_results_list[3]) + loss = abs(100 - int(success)) + results["packet_loss"] = str(loss) + "%" + results["packets_rx"] = int(rx) + results["packets_tx"] = int(tx) + + # Convert rtt values to int + for k, v in rtt.items(): + if rtt[k] is not None: + rtt[k] = int(v) + + results["rtt"] = rtt + + validate_results(module, loss, results) + + module.exit_json(**results) + + +def build_ping(dest, count=None, source=None, vrf=None): + """ + Function to build the command to send to the terminal for the switch + to execute. All args come from the module's unique params. + """ + if vrf is not None: + cmd = "ping {0} {1}".format(vrf, dest) + else: + cmd = "ping {0}".format(dest) + + if count is not None: + cmd += " repeat {0}".format(str(count)) + + if source is not None: + cmd += " source {0}".format(source) + + return cmd + + +def parse_ping(ping_stats): + """ + Function used to parse the statistical information from the ping response. + Example: "Success rate is 100 percent (5/5), round-trip min/avg/max = 1/2/8 ms" + Returns the percent of packet loss, recieved packets, transmitted packets, and RTT dict. + """ + rate_re = re.compile("^\w+\s+\w+\s+\w+\s+(?P\d+)\s+\w+\s+\((?P\d+)\/(?P\d+)\)") + rtt_re = re.compile(".*,\s+\S+\s+\S+\s+=\s+(?P\d+)\/(?P\d+)\/(?P\d+)\s+\w+\s*$|.*\s*$") + + rate = rate_re.match(ping_stats) + rtt = rtt_re.match(ping_stats) + + return rate.group("pct"), rate.group("rx"), rate.group("tx"), rtt.groupdict() + + +def validate_results(module, loss, results): + """ + This function is used to validate whether the ping results were unexpected per "state" param. + """ + state = module.params["state"] + if state == "present" and loss == 100: + module.fail_json(msg="Ping failed unexpectedly", **results) + elif state == "absent" and loss < 100: + module.fail_json(msg="Ping succeeded unexpectedly", **results) + + +if __name__ == "__main__": + main() diff --git a/test/integration/ios.yaml b/test/integration/ios.yaml index 21db7a0c7ad..2ef6f3f52cf 100644 --- a/test/integration/ios.yaml +++ b/test/integration/ios.yaml @@ -94,6 +94,15 @@ failed_modules: "{{ failed_modules }} + [ 'ios_logging' ]" test_failed: true + - block: + - include_role: + name: ios_ping + when: "limit_to in ['*', 'ios_ping']" + rescue: + - set_fact: + failed_modules: "{{ failed_modules }} + [ 'ios_ping' ]" + test_failed: true + ########### - debug: var=failed_modules when: test_failed diff --git a/test/integration/targets/ios_ping/defaults/main.yaml b/test/integration/targets/ios_ping/defaults/main.yaml new file mode 100644 index 00000000000..9ef5ba51651 --- /dev/null +++ b/test/integration/targets/ios_ping/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "*" +test_items: [] diff --git a/test/integration/targets/ios_ping/meta/main.yaml b/test/integration/targets/ios_ping/meta/main.yaml new file mode 100644 index 00000000000..159cea8d383 --- /dev/null +++ b/test/integration/targets/ios_ping/meta/main.yaml @@ -0,0 +1,2 @@ +dependencies: + - prepare_ios_tests diff --git a/test/integration/targets/ios_ping/tasks/cli.yaml b/test/integration/targets/ios_ping/tasks/cli.yaml new file mode 100644 index 00000000000..46d86dd6988 --- /dev/null +++ b/test/integration/targets/ios_ping/tasks/cli.yaml @@ -0,0 +1,16 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + register: test_cases + delegate_to: localhost + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test case + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/ios_ping/tasks/main.yaml b/test/integration/targets/ios_ping/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/ios_ping/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/ios_ping/tests/cli/ping.yaml b/test/integration/targets/ios_ping/tests/cli/ping.yaml new file mode 100644 index 00000000000..0ac5fa86620 --- /dev/null +++ b/test/integration/targets/ios_ping/tests/cli/ping.yaml @@ -0,0 +1,31 @@ +--- +- debug: msg="START cli/ping.yaml" + +- name: expected successful ping + ios_ping: &valid_ip + dest: '8.8.8.8' + register: esp + +- name: unexpected unsuccessful ping + ios_ping: &invalid_ip + dest: '10.255.255.250' + timeout: 45 + register: uup + +- name: unexpected successful ping + ios_ping: + <<: *valid_ip + state: 'absent' + register: usp + +- name: expected unsuccessful ping + ios_ping: + <<: *invalid_ip + state: 'absent' + register: eup + +- name: assert + assert: + that: + - esp.failed == eup.failed == false + - usp.failed == uup.failed == true diff --git a/test/units/modules/network/ios/fixtures/ios_ping_ping_10.255.255.250_repeat_2 b/test/units/modules/network/ios/fixtures/ios_ping_ping_10.255.255.250_repeat_2 new file mode 100644 index 00000000000..9b25d64549f --- /dev/null +++ b/test/units/modules/network/ios/fixtures/ios_ping_ping_10.255.255.250_repeat_2 @@ -0,0 +1,4 @@ +Type escape sequence to abort. +Sending 2, 100-byte ICMP Echos to 10.255.255.250, timeout is 2 seconds: +.. +Success rate is 0 percent (0/2) diff --git a/test/units/modules/network/ios/fixtures/ios_ping_ping_8.8.8.8_repeat_2 b/test/units/modules/network/ios/fixtures/ios_ping_ping_8.8.8.8_repeat_2 new file mode 100644 index 00000000000..4dddd76b0f6 --- /dev/null +++ b/test/units/modules/network/ios/fixtures/ios_ping_ping_8.8.8.8_repeat_2 @@ -0,0 +1,4 @@ +Type escape sequence to abort. +Sending 2, 100-byte ICMP Echos to 8.8.8.8, timeout is 2 seconds: +!! +Success rate is 100 percent (2/2), round-trip min/avg/max = 25/25/25 ms diff --git a/test/units/modules/network/ios/test_ios_ping.py b/test/units/modules/network/ios/test_ios_ping.py new file mode 100644 index 00000000000..a045fcc6261 --- /dev/null +++ b/test/units/modules/network/ios/test_ios_ping.py @@ -0,0 +1,70 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.compat.tests.mock import patch +from ansible.modules.network.ios import ios_ping +from .ios_module import TestIosModule, load_fixture, set_module_args + + +class TestIosPingModule(TestIosModule): + ''' Class used for Unit Tests agains ios_ping module ''' + module = ios_ping + + def setUp(self): + self.mock_run_commands = patch('ansible.modules.network.ios.ios_ping.run_commands') + self.run_commands = self.mock_run_commands.start() + + def tearDown(self): + self.mock_run_commands.stop() + + def load_fixtures(self, commands=None): + def load_from_file(*args, **kwargs): + module = args + commands = kwargs['commands'] + output = list() + + for command in commands: + filename = str(command).split(' | ')[0].replace(' ', '_') + output.append(load_fixture('ios_ping_%s' % filename)) + return output + + self.run_commands.side_effect = load_from_file + + def test_ios_ping_expected_success(self): + ''' Test for successful pings when destination should be reachable ''' + set_module_args(dict(count=2, dest="8.8.8.8")) + self.execute_module() + + def test_ios_ping_expected_failure(self): + ''' Test for unsuccessful pings when destination should not be reachable ''' + set_module_args(dict(count=2, dest="10.255.255.250", state="absent", timeout=45)) + self.execute_module() + + def test_ios_ping_unexpected_success(self): + ''' Test for successful pings when destination should not be reachable - FAIL. ''' + set_module_args(dict(count=2, dest="8.8.8.8", state="absent")) + self.execute_module(failed=True) + + def test_ios_ping_unexpected_failure(self): + ''' Test for unsuccessful pings when destination should be reachable - FAIL. ''' + set_module_args(dict(count=2, dest="10.255.255.250", timeout=45)) + self.execute_module(failed=True)