1#   Copyright 2016 - The Android Open Source Project
2#
3#   Licensed under the Apache License, Version 2.0 (the "License");
4#   you may not use this file except in compliance with the License.
5#   You may obtain a copy of the License at
6#
7#       http://www.apache.org/licenses/LICENSE-2.0
8#
9#   Unless required by applicable law or agreed to in writing, software
10#   distributed under the License is distributed on an "AS IS" BASIS,
11#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12#   See the License for the specific language governing permissions and
13#   limitations under the License.
14
15import ipaddress
16import re
17
18from acts.controllers.utils_lib.ssh import connection
19
20
21class Error(Exception):
22    """Exception thrown when a valid ip command experiences errors."""
23
24
25class NetworkInterfaceDown(Error):
26    """Exception thrown when a network interface is down."""
27
28
29class LinuxRouteCommand(object):
30    """Interface for doing standard ip route commands on a linux system."""
31
32    DEFAULT_ROUTE = 'default'
33
34    def __init__(self, runner):
35        """
36        Args:
37            runner: Object that can take unix commands and run them in an
38                    enviroment.
39        """
40        self._runner = runner
41
42    def add_route(self, net_interface, address):
43        """Add an entry to the ip routing table.
44
45        Will add a route for either a specific ip address, or a network.
46
47        Args:
48            net_interface: string, Any packet that sends through this route
49                           will be sent using this network interface
50                           (eg. wlan0).
51            address: ipaddress.IPv4Address, ipaddress.IPv4Network,
52                     or DEFAULT_ROUTE. The address to use. If a network
53                     is given then the entire subnet will be routed.
54                     If DEFAULT_ROUTE is given then this will set the
55                     default route.
56
57        Raises:
58            NetworkInterfaceDown: Raised when the network interface is down.
59        """
60        try:
61            self._runner.run('ip route add %s dev %s' %
62                             (address, net_interface))
63        except connection.CommandError as e:
64            if 'File exists' in e.result.stderr:
65                raise Error('Route already exists.')
66            if 'Network is down' in e.result.stderr:
67                raise NetworkInterfaceDown(
68                    'Device must be up for adding a route.')
69            raise
70
71    def get_routes(self, net_interface=None):
72        """Get the routes in the ip routing table.
73
74        Args:
75            net_interface: string, If given, only retrive routes that have
76                           been registered to go through this network
77                           interface (eg. wlan0).
78
79        Returns: An iterator that returns a tuple of (address, net_interface).
80                 If it is the default route then address
81                 will be the DEFAULT_ROUTE. If the route is a subnet then
82                 it will be a ipaddress.IPv4Network otherwise it is a
83                 ipaddress.IPv4Address.
84        """
85        result = self._runner.run('ip route show')
86
87        lines = result.stdout.splitlines()
88
89        # Scan through each line for valid route entries
90        # Example output:
91        # default via 192.168.1.254 dev eth0  proto static
92        # 192.168.1.0/24 dev eth0  proto kernel  scope link  src 172.22.100.19  metric 1
93        # 192.168.2.1 dev eth2 proto kernel scope link metric 1
94        for line in lines:
95            if not 'dev' in line:
96                continue
97
98            if line.startswith(self.DEFAULT_ROUTE):
99                # The default route entry is formatted differently.
100                match = re.search('dev (?P<net_interface>.*)', line)
101                pair = None
102                if match:
103                    # When there is a match for the route entry pattern create
104                    # A pair to hold the info.
105                    pair = (self.DEFAULT_ROUTE,
106                            match.groupdict()['net_interface'])
107            else:
108                # Test the normal route entry pattern.
109                match = re.search(
110                    '(?P<address>[^\s]*) dev (?P<net_interface>[^\s]*)', line)
111                pair = None
112                if match:
113                    # When there is a match for the route entry pattern create
114                    # A pair to hold the info.
115                    d = match.groupdict()
116                    # Route can be either a network or specific address
117                    try:
118                        address = ipaddress.IPv4Address(d['address'])
119                    except ipaddress.AddressValueError:
120                        address = ipaddress.IPv4Network(d['address'])
121
122                    pair = (address, d['net_interface'])
123
124            # No pair means no pattern was found.
125            if not pair:
126                continue
127
128            if net_interface:
129                # If a net_interface was passed in then only give the pair when it is
130                # The correct net_interface.
131                if pair[1] == net_interface:
132                    yield pair
133            else:
134                # No net_interface given give all valid route entries.
135                yield pair
136
137    def is_route(self, address, net_interface=None):
138        """Checks to see if a route exists.
139
140        Args:
141            address: ipaddress.IPv4Address, ipaddress.IPv4Network,
142                     or DEFAULT_ROUTE, The address to use.
143            net_interface: string, If specified, the route must be
144                           registered to go through this network interface
145                           (eg. wlan0).
146
147        Returns: True if the route is found, False otherwise.
148        """
149        for route, _ in self.get_routes(net_interface):
150            if route == address:
151                return True
152
153        return False
154
155    def remove_route(self, address, net_interface=None):
156        """Removes a route from the ip routing table.
157
158        Removes a route from the ip routing table. If the route does not exist
159        nothing is done.
160
161        Args:
162            address: ipaddress.IPv4Address, ipaddress.IPv4Network,
163                     or DEFAULT_ROUTE, The address of the route to remove.
164            net_interface: string, If specified the route being removed is
165                           registered to go through this network interface
166                           (eg. wlan0)
167        """
168        try:
169            if net_interface:
170                self._runner.run('ip route del %s dev %s' %
171                                 (address, net_interface))
172            else:
173                self._runner.run('ip route del %s' % address)
174        except connection.CommandError as e:
175            if 'No such process' in e.result.stderr:
176                # The route didn't exist.
177                return
178            raise
179
180    def clear_routes(self, net_interface=None):
181        """Clears all routes.
182
183        Args:
184            net_interface: The network interface to clear routes on.
185            If not given then all routes will be removed on all network
186            interfaces (eg. wlan0).
187        """
188        routes = self.get_routes(net_interface)
189
190        for a, d in routes:
191            self.remove_route(a, d)
192