diff --git a/lib/ansible/modules/network/junos/junos_ping.py b/lib/ansible/modules/network/junos/junos_ping.py new file mode 100644 index 0000000000..4df358ac41 --- /dev/null +++ b/lib/ansible/modules/network/junos/junos_ping.py @@ -0,0 +1,231 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2019, 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + +DOCUMENTATION = """ +--- +module: junos_ping +short_description: Tests reachability using ping from devices running Juniper JUNOS +description: + - Tests reachability using ping from devices running Juniper JUNOS to a remote destination. + - Tested against Junos (17.3R1.10) + - For a general purpose network module, see the M(net_ping) module. + - For Windows targets, use the M(win_ping) module instead. + - For targets running Python, use the M(ping) module instead. +author: + - Nilashish Chakraborty (@nilashishc) +version_added: '2.8' +options: + dest: + description: + - The IP Address or hostname (resolvable by the device) of the remote node. + required: true + count: + description: + - Number of packets to send to check reachability. + type: int + default: 5 + source: + description: + - The IP Address to use while sending the ping packet(s). + interface: + description: + - The source interface to use while sending the ping packet(s). + ttl: + description: + - The time-to-live value for the ICMP packet(s). + type: int + size: + description: + - Determines the size (in bytes) of the ping packet(s). + type: int + interval: + description: + - Determines the interval (in seconds) between consecutive pings. + type: int + state: + description: + - Determines if the expected result is success or fail. + choices: [ absent, present ] + default: present +notes: + - For a general purpose network module, see the M(net_ping) module. + - For Windows targets, use the M(win_ping) module instead. + - For targets running Python, use the M(ping) module instead. +extends_documentation_fragment: junos +""" + +EXAMPLES = """ +- name: Test reachability to 10.10.10.10 + junos_ping: + dest: 10.10.10.10 + +- name: Test reachability to 10.20.20.20 using source and size set + junos_ping: + dest: 10.20.20.20 + size: 1024 + ttl: 128 + +- name: Test unreachability to 10.30.30.30 using interval + junos_ping: + dest: 10.30.30.30 + interval: 3 + state: absent + +- name: Test reachability to 10.40.40.40 setting count and interface + junos_ping: + dest: 10.40.40.40 + interface: fxp0 + count: 20 + size: 512 +""" + +RETURN = """ +commands: + description: List of commands sent. + returned: always + type: list + sample: ["ping 10.8.38.44 count 10 source 10.8.38.38 ttl 128"] +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: The round trip time (RTT) stats. + returned: when ping succeeds + type: dict + sample: {"avg": 2, "max": 8, "min": 1, "stddev": 24} +""" + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.junos.junos import junos_argument_spec, get_connection + + +def main(): + """ main entry point for module execution + """ + argument_spec = dict( + count=dict(type="int", default=5), + dest=dict(type="str", required=True), + source=dict(), + interface=dict(), + ttl=dict(type='int'), + size=dict(type='int'), + interval=dict(type='int'), + state=dict(type="str", choices=["absent", "present"], default="present"), + ) + + argument_spec.update(junos_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec) + + count = module.params["count"] + dest = module.params["dest"] + source = module.params["source"] + size = module.params["size"] + ttl = module.params["ttl"] + interval = module.params["interval"] + interface = module.params['interface'] + warnings = list() + + results = {'changed': False} + if warnings: + results["warnings"] = warnings + + results["commands"] = build_ping(dest, count, size, interval, source, ttl, interface) + conn = get_connection(module) + + ping_results = conn.get(results["commands"]) + + rtt_info, rate_info = None, None + for line in ping_results.split("\n"): + if line.startswith('round-trip'): + rtt_info = line + if line.startswith('%s packets transmitted' % count): + rate_info = line + + if rtt_info: + rtt = parse_rtt(rtt_info) + for k, v in rtt.items(): + if rtt[k] is not None: + rtt[k] = float(v) + results["rtt"] = rtt + + pkt_loss, rx, tx = parse_rate(rate_info) + results["packet_loss"] = str(pkt_loss) + "%" + results["packets_rx"] = int(rx) + results["packets_tx"] = int(tx) + + validate_results(module, pkt_loss, results) + + module.exit_json(**results) + + +def build_ping(dest, count, size=None, interval=None, source=None, ttl=None, interface=None): + cmd = "ping {0} count {1}".format(dest, str(count)) + + if source: + cmd += " source {0}".format(source) + + if interface: + cmd += " interface {0}".format(interface) + + if ttl: + cmd += " ttl {0}".format(str(ttl)) + + if size: + cmd += " size {0}".format(str(size)) + + if interval: + cmd += " interval {0}".format(str(interval)) + + return cmd + + +def parse_rate(rate_info): + rate_re = re.compile( + r"(?P\d*) packets transmitted,(?:\s*)(?P\d*) packets received,(?:\s*)(?P\d*)% packet loss") + rate = rate_re.match(rate_info) + + return rate.group("pkt_loss"), rate.group("rx"), rate.group("tx") + + +def parse_rtt(rtt_info): + rtt_re = re.compile( + r"round-trip (?:.*)=(?:\s*)(?P\d+\.\d+).(?:\d*)/(?P\d+\.\d+).(?:\d*)/(?P\d*\.\d*).(?:\d*)/(?P\d*\.\d*)") + rtt = rtt_re.match(rtt_info) + + return rtt.groupdict() + + +def validate_results(module, loss, results): + state = module.params["state"] + if state == "present" and int(loss) == 100: + module.fail_json(msg="Ping failed unexpectedly", **results) + elif state == "absent" and int(loss) < 100: + module.fail_json(msg="Ping succeeded unexpectedly", **results) + + +if __name__ == "__main__": + main() diff --git a/test/units/modules/network/junos/fixtures/junos_ping_ping_10.10.10.10_count_2 b/test/units/modules/network/junos/fixtures/junos_ping_ping_10.10.10.10_count_2 new file mode 100644 index 0000000000..9fcc94a6c3 --- /dev/null +++ b/test/units/modules/network/junos/fixtures/junos_ping_ping_10.10.10.10_count_2 @@ -0,0 +1,7 @@ +PING 10.10.10.10 (10.10.10.10): 56 data bytes +64 bytes from 10.10.10.10: icmp_seq=0 ttl=64 time=18.041 ms +64 bytes from 10.10.10.10: icmp_seq=1 ttl=64 time=15.710 ms + +--- 10.10.10.10 ping statistics --- +2 packets transmitted, 2 packets received, 0% packet loss +round-trip min/avg/max/stddev = 15.710/16.876/18.041/1.165 ms diff --git a/test/units/modules/network/junos/fixtures/junos_ping_ping_10.10.10.11_count_5_size_512_interval_2 b/test/units/modules/network/junos/fixtures/junos_ping_ping_10.10.10.11_count_5_size_512_interval_2 new file mode 100644 index 0000000000..68d44d7f57 --- /dev/null +++ b/test/units/modules/network/junos/fixtures/junos_ping_ping_10.10.10.11_count_5_size_512_interval_2 @@ -0,0 +1,11 @@ +PING 10.10.10.11 (10.10.10.11): 56 data bytes +64 bytes from 10.10.10.11: icmp_seq=0 ttl=64 time=18.041 ms +64 bytes from 10.10.10.11: icmp_seq=1 ttl=64 time=15.710 ms +64 bytes from 10.10.10.11: icmp_seq=0 ttl=64 time=16.051 ms +64 bytes from 10.10.10.11: icmp_seq=0 ttl=64 time=17.024 ms +64 bytes from 10.10.10.11: icmp_seq=0 ttl=64 time=20.090 ms + + +--- 10.10.10.11 ping statistics --- +5 packets transmitted, 5 packets received, 0% packet loss +round-trip min/avg/max/stddev = 18.710/17.876/20.041/2.165 ms diff --git a/test/units/modules/network/junos/fixtures/junos_ping_ping_10.10.10.20_count_4 b/test/units/modules/network/junos/fixtures/junos_ping_ping_10.10.10.20_count_4 new file mode 100644 index 0000000000..70cff3bc96 --- /dev/null +++ b/test/units/modules/network/junos/fixtures/junos_ping_ping_10.10.10.20_count_4 @@ -0,0 +1,4 @@ +PING 10.10.10.20 (10.10.10.20): 56 data bytes + +--- 10.10.10.20 ping statistics --- +4 packets transmitted, 0 packets received, 100% packet loss diff --git a/test/units/modules/network/junos/test_junos_ping.py b/test/units/modules/network/junos/test_junos_ping.py new file mode 100644 index 0000000000..b0af7b33e9 --- /dev/null +++ b/test/units/modules/network/junos/test_junos_ping.py @@ -0,0 +1,101 @@ +# (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 units.compat.mock import patch, MagicMock +from ansible.modules.network.junos import junos_ping +from units.modules.utils import set_module_args +from .junos_module import TestJunosModule, load_fixture + + +class TestJunosPingModule(TestJunosModule): + module = junos_ping + + def setUp(self): + super(TestJunosPingModule, self).setUp() + + self.mock_get_connection = patch('ansible.modules.network.junos.junos_ping.get_connection') + self.get_connection = self.mock_get_connection.start() + + self.conn = self.get_connection() + self.conn.get = MagicMock() + + def tearDown(self): + super(TestJunosPingModule, self).tearDown() + self.mock_get_connection.stop() + + def test_junos_ping_expected_success(self): + set_module_args(dict(count=2, dest="10.10.10.10")) + self.conn.get = MagicMock(return_value=load_fixture('junos_ping_ping_10.10.10.10_count_2', content='str')) + result = self.execute_module() + self.assertEqual(result['commands'], 'ping 10.10.10.10 count 2') + + def test_junos_ping_expected_failure(self): + set_module_args(dict(count=4, dest="10.10.10.20", state="absent")) + self.conn.get = MagicMock(return_value=load_fixture('junos_ping_ping_10.10.10.20_count_4', content='str')) + result = self.execute_module() + self.assertEqual(result['commands'], 'ping 10.10.10.20 count 4') + + def test_junos_ping_unexpected_success(self): + ''' Test for successful pings when destination should not be reachable - FAIL. ''' + set_module_args(dict(count=2, dest="10.10.10.10", state="absent")) + self.conn.get = MagicMock(return_value=load_fixture('junos_ping_ping_10.10.10.10_count_2', content='str')) + self.execute_module(failed=True) + + def test_junos_ping_unexpected_failure(self): + ''' Test for unsuccessful pings when destination should be reachable - FAIL. ''' + set_module_args(dict(count=4, dest="10.10.10.20")) + self.conn.get = MagicMock(return_value=load_fixture('junos_ping_ping_10.10.10.20_count_4', content='str')) + self.execute_module(failed=True) + + def test_junos_ping_failure_stats(self): + '''Test for asserting stats when ping fails''' + set_module_args(dict(count=4, dest="10.10.10.20")) + self.conn.get = MagicMock(return_value=load_fixture('junos_ping_ping_10.10.10.20_count_4', content='str')) + result = self.execute_module(failed=True) + self.assertEqual(result['packet_loss'], '100%') + self.assertEqual(result['packets_rx'], 0) + self.assertEqual(result['packets_tx'], 4) + + def test_junos_ping_success_stats(self): + set_module_args(dict(count=2, dest="10.10.10.10")) + self.conn.get = MagicMock(return_value=load_fixture('junos_ping_ping_10.10.10.10_count_2', content='str')) + result = self.execute_module() + self.assertEqual(result['commands'], 'ping 10.10.10.10 count 2') + self.assertEqual(result['packet_loss'], '0%') + self.assertEqual(result['packets_rx'], 2) + self.assertEqual(result['packets_tx'], 2) + self.assertEqual(result['rtt']['min'], 15.71) + self.assertEqual(result['rtt']['avg'], 16.87) + self.assertEqual(result['rtt']['max'], 18.04) + self.assertEqual(result['rtt']['stddev'], 1.165) + + def test_junos_ping_success_stats_with_options(self): + set_module_args(dict(count=5, size=512, interval=2, dest="10.10.10.11")) + self.conn.get = MagicMock(return_value=load_fixture('junos_ping_ping_10.10.10.11_count_5_size_512_interval_2', content='str')) + result = self.execute_module() + self.assertEqual(result['commands'], 'ping 10.10.10.11 count 5 size 512 interval 2') + self.assertEqual(result['packet_loss'], '0%') + self.assertEqual(result['packets_rx'], 5) + self.assertEqual(result['packets_tx'], 5) + self.assertEqual(result['rtt']['min'], 18.71) + self.assertEqual(result['rtt']['avg'], 17.87) + self.assertEqual(result['rtt']['max'], 20.04) + self.assertEqual(result['rtt']['stddev'], 2.165)