1# Copyright (c) 2016 RIPE NCC
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16from ..ipdetails import IP
17from .base import Renderer as BaseRenderer
18
19
20class Renderer(BaseRenderer):
21
22    RENDERS = [BaseRenderer.TYPE_TRACEROUTE]
23
24    DEFAULT_RADIUS = 2
25
26    @staticmethod
27    def add_arguments(parser):
28        group = parser.add_argument_group(
29            title="Optional arguments for traceroute_aspath renderer"
30        )
31        group.add_argument(
32            "--traceroute-aspath-radius",
33            type=int,
34            help="Number of different ASs starting from the end of the "
35                 "traceroute path. "
36                 "Default: {}.".format(Renderer.DEFAULT_RADIUS),
37            metavar="RADIUS",
38            default=Renderer.DEFAULT_RADIUS
39        )
40
41    def __init__(self, *args, **kwargs):
42        BaseRenderer.__init__(self, *args, **kwargs)
43        self.paths = {}
44
45        # Number of different ASs starting from the end of the traceroute path.
46        if "arguments" in kwargs:
47            self.RADIUS = kwargs["arguments"].traceroute_aspath_radius
48        else:
49            self.RADIUS = Renderer.DEFAULT_RADIUS
50
51    @staticmethod
52    def _get_asns_for_output(asns, radius):
53        asns_with_padding = [""] * radius + asns
54        asns_with_padding = asns_with_padding[-radius:]
55        return " ".join(
56            ["{:>8}".format("AS{}".format(asn) if asn else "") for asn in asns_with_padding]
57        )
58
59    def on_start(self):
60        return "For each traceroute path toward the target, the " \
61               "last {} ASNs will be shown\n\n".format(self.RADIUS)
62
63    def on_result(self, result):
64
65        ip_hops = []
66
67        for hop in result.hops:
68            for packet in hop.packets:
69                if packet.origin:
70                    ip_hops.append(packet.origin)
71                    break
72
73        asns = []
74
75        # starting from the last hop's IP, get up to <RADIUS> ASNs
76        for address in reversed(ip_hops):
77            ip = IP(address)
78            if ip.asn and ip.asn not in asns:
79                asns.append(ip.asn)
80            if len(asns) == self.RADIUS:
81                break
82
83        as_path = self._get_asns_for_output(list(reversed(asns)), self.RADIUS)
84
85        if as_path not in self.paths:
86            self.paths[as_path] = {}
87            self.paths[as_path]['cnt'] = 0
88            self.paths[as_path]['responded'] = 0
89        self.paths[as_path]['cnt'] += 1
90        if result.destination_ip_responded:
91            self.paths[as_path]['responded'] += 1
92
93        return "Probe #{:<5}: {}, {}completed\n".format(
94            result.probe_id, as_path,
95            "NOT " if not result.destination_ip_responded else ""
96        )
97
98    def additional(self, results):
99        s = "\nNumber of probes for each AS path:\n\n"
100
101        for as_path in self.paths:
102            s += "  {}: {} probe{}, {} completed\n".format(
103                as_path,
104                self.paths[as_path]['cnt'],
105                "s" if self.paths[as_path]['cnt'] > 1 else "",
106                self.paths[as_path]['responded']
107            )
108
109        return s
110