f51f227ae96c42aa98025784b7e1f27d0fddd7ef
[apex.git] / lib / python / apex / ip_utils.py
1 ##############################################################################
2 # Copyright (c) 2016 Feng Pan (fpan@redhat.com) and others.
3 #
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 ##############################################################################
9
10
11 import ipaddress
12 import subprocess
13 import re
14 import logging
15
16
17 def get_ip_range(start_offset=None, count=None, end_offset=None,
18                  cidr=None, interface=None):
19     """
20     Generate IP range for a network (cidr) or an interface.
21
22     If CIDR is provided, it will take precedence over interface. In this case,
23     The entire CIDR IP address space is considered usable. start_offset will be
24     calculated from the network address, and end_offset will be calculated from
25     the last address in subnet.
26
27     If interface is provided, the interface IP will be used to calculate
28     offsets:
29         - If the interface IP is in the first half of the address space,
30         start_offset will be calculated from the interface IP, and end_offset
31         will be calculated from end of address space.
32         - If the interface IP is in the second half of the address space,
33         start_offset will be calculated from the network address in the address
34         space, and end_offset will be calculated from the interface IP.
35
36     2 of start_offset, end_offset and count options must be provided:
37         - If start_offset and end_offset are provided, a range from start_offset
38         to end_offset will be returned.
39         - If count is provided, a range from either start_offset to (start_offset
40         +count) or (end_offset-count) to end_offset will be returned. The
41         IP range returned will be of size <count>.
42     Both start_offset and end_offset must be greater than 0.
43
44     Returns IP range in the format of "first_addr,second_addr" or exception
45     is raised.
46     """
47     if cidr:
48         if count and start_offset and not end_offset:
49             start_index = start_offset
50             end_index = start_offset + count -1
51         elif count and end_offset and not start_offset:
52             end_index = -1 - end_offset
53             start_index = -1 - end_index - count + 1
54         elif start_offset and end_offset and not count:
55             start_index = start_offset
56             end_index = -1 - end_offset
57         else:
58             raise IPUtilsException("Argument error: must pass in exactly 2 of"
59                                    " start_offset, end_offset and count")
60
61         start_ip = cidr[start_index]
62         end_ip = cidr[end_index]
63         network = cidr
64     elif interface:
65         network = interface.network
66         number_of_addr = network.num_addresses
67         if interface.ip < network[int(number_of_addr / 2)]:
68             if count and start_offset and not end_offset:
69                 start_ip = interface.ip + start_offset
70                 end_ip = start_ip + count - 1
71             elif count and end_offset and not start_offset:
72                 end_ip = network[-1 - end_offset]
73                 start_ip = end_ip - count + 1
74             elif start_offset and end_offset and not count:
75                 start_ip = interface.ip + start_offset
76                 end_ip = network[-1 - end_offset]
77             else:
78                 raise IPUtilsException(
79                     "Argument error: must pass in exactly 2 of"
80                     " start_offset, end_offset and count")
81         else:
82             if count and start_offset and not end_offset:
83                 start_ip = network[start_offset]
84                 end_ip = start_ip + count -1
85             elif count and end_offset and not start_offset:
86                 end_ip = interface.ip - end_offset
87                 start_ip = end_ip - count + 1
88             elif start_offset and end_offset and not count:
89                 start_ip = network[start_offset]
90                 end_ip = interface.ip - end_offset
91             else:
92                 raise IPUtilsException(
93                     "Argument error: must pass in exactly 2 of"
94                     " start_offset, end_offset and count")
95
96     else:
97         raise IPUtilsException("Must pass in cidr or interface to generate"
98                                "ip range")
99
100     range_result = _validate_ip_range(start_ip, end_ip, network)
101     if range_result:
102         ip_range = "{},{}".format(start_ip, end_ip)
103         return ip_range
104     else:
105         raise IPUtilsException("Invalid IP range: {},{} for network {}"
106                                .format(start_ip, end_ip, network))
107
108
109 def get_ip(offset, cidr=None, interface=None):
110     """
111     Returns an IP in a network given an offset.
112
113     Either cidr or interface must be provided, cidr takes precedence.
114
115     If cidr is provided, offset is calculated from network address.
116     If interface is provided, offset is calculated from interface IP.
117
118     offset can be positive or negative, but the resulting IP address must also
119     be contained in the same subnet, otherwise an exception will be raised.
120
121     returns a IP address object.
122     """
123     if cidr:
124         ip = cidr[0 + offset]
125         network = cidr
126     elif interface:
127         ip = interface.ip + offset
128         network = interface.network
129     else:
130         raise IPUtilsException("Must pass in cidr or interface to generate IP")
131
132     if ip not in network:
133         raise IPUtilsException("IP {} not in network {}".format(ip, network))
134     else:
135         return str(ip)
136
137
138 def generate_ip_range(args):
139     """
140     Generate IP range in string format for given CIDR.
141     This function works for both IPv4 and IPv6.
142
143     args is expected to contain the following members:
144     CIDR: any valid CIDR representation.
145     start_position: starting index, default to first address in subnet (1)
146     end_position:  ending index, default to last address in subnet (-1)
147
148     Returns IP range in string format. A single IP is returned if start and
149     end IPs are identical.
150     """
151     cidr = ipaddress.ip_network(args.CIDR)
152     (start_index, end_index) = (args.start_position, args.end_position)
153     if cidr[start_index] == cidr[end_index]:
154         return str(cidr[start_index])
155     else:
156         return ','.join(sorted([str(cidr[start_index]), str(cidr[end_index])]))
157
158
159 def get_interface(nic, address_family=4):
160     """
161     Returns interface object for a given NIC name in the system
162
163     Only global address will be returned at the moment.
164
165     Returns interface object if an address is found for the given nic,
166     otherwise returns None.
167     """
168     if not nic.strip():
169         logging.error("empty nic name specified")
170         return None
171     output = subprocess.getoutput("ip -{} addr show {} scope global"
172                                   .format(address_family, nic))
173     if address_family == 4:
174         pattern = re.compile("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}")
175     elif address_family == 6:
176         pattern = re.compile("([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}/\d{1,3}")
177     else:
178         raise IPUtilsException("Invalid address family: {}"
179                                .format(address_family))
180     match = re.search(pattern, output)
181     if match:
182         logging.info("found interface {} ip: {}".format(nic, match.group()))
183         return ipaddress.ip_interface(match.group())
184     else:
185         logging.info("interface ip not found! ip address output:\n{}"
186                         .format(output))
187         return None
188
189
190 def find_gateway(interface):
191     """
192     Validate gateway on the system
193
194     Ensures that the provided interface object is in fact configured as default
195     route on the system.
196
197     Returns gateway IP (reachable from interface) if default route is found,
198     otherwise returns None.
199     """
200
201     address_family = interface.version
202     output = subprocess.getoutput("ip -{} route".format(address_family))
203
204     pattern = re.compile("default\s+via\s+(\S+)\s+")
205     match = re.search(pattern, output)
206
207     if match:
208         gateway_ip = match.group(1)
209         reverse_route_output = subprocess.getoutput("ip route get {}"
210                                                     .format(gateway_ip))
211         pattern = re.compile("{}.+src\s+{}".format(gateway_ip, interface.ip))
212         if not re.search(pattern, reverse_route_output):
213             logging.warning("Default route doesn't match interface specified: "
214                             "{}".format(reverse_route_output))
215             return None
216         else:
217             return gateway_ip
218     else:
219         logging.warning("Can't find gateway address on system")
220         return None
221
222
223 def _validate_ip_range(start_ip, end_ip, cidr):
224     """
225     Validates an IP range is in good order and the range is part of cidr.
226
227     Returns True if validation succeeds, False otherwise.
228     """
229     ip_range = "{},{}".format(start_ip, end_ip)
230     if end_ip <= start_ip:
231         logging.warning("IP range {} is invalid: end_ip should be greater than "
232                         "starting ip".format(ip_range))
233         return False
234     if start_ip not in ipaddress.ip_network(cidr):
235         logging.warning('start_ip {} is not in network {}'
236                         .format(start_ip, cidr))
237         return False
238     if end_ip not in ipaddress.ip_network(cidr):
239         logging.warning('end_ip {} is not in network {}'.format(end_ip, cidr))
240         return False
241
242     return True
243
244
245 class IPUtilsException(Exception):
246     def __init__(self, value):
247         self.value = value
248
249     def __str__(self):
250         return self.value