Add python parsing library for network settings file.
[apex.git] / lib / python / apex / ip_utils.py
index 680ce7e..d7099db 100644 (file)
@@ -1,4 +1,3 @@
-
 ##############################################################################
 # Copyright (c) 2016 Feng Pan (fpan@redhat.com) and others.
 #
 
 
 import ipaddress
+import subprocess
+import re
+import logging
+
+
+def get_ip_range(start_offset=None, count=None, end_offset=None,
+                 cidr=None, interface=None):
+    """
+    Generate IP range for a network (cidr) or an interface.
+
+    If CIDR is provided, it will take precedence over interface. In this case,
+    The entire CIDR IP address space is considered usable. start_offset will be
+    calculated from the network address, and end_offset will be calculated from
+    the last address in subnet.
+
+    If interface is provided, the interface IP will be used to calculate
+    offsets:
+        - If the interface IP is in the first half of the address space,
+        start_offset will be calculated from the interface IP, and end_offset
+        will be calculated from end of address space.
+        - If the interface IP is in the second half of the address space,
+        start_offset will be calculated from the network address in the address
+        space, and end_offset will be calculated from the interface IP.
+
+    2 of start_offset, end_offset and count options must be provided:
+        - If start_offset and end_offset are provided, a range from start_offset
+        to end_offset will be returned.
+        - If count is provided, a range from either start_offset to (start_offset
+        +count) or (end_offset-count) to end_offset will be returned. The
+        IP range returned will be of size <count>.
+    Both start_offset and end_offset must be greater than 0.
+
+    Returns IP range in the format of "first_addr,second_addr" or exception
+    is raised.
+    """
+    if cidr:
+        if count and start_offset and not end_offset:
+            start_index = start_offset
+            end_index = start_offset + count -1
+        elif count and end_offset and not start_offset:
+            end_index = -1 - end_offset
+            start_index = -1 - end_index - count + 1
+        elif start_offset and end_offset and not count:
+            start_index = start_offset
+            end_index = -1 - end_offset
+        else:
+            raise IPUtilsException("Argument error: must pass in exactly 2 of"
+                                   "start_offset, end_offset and count")
+
+        start_ip = cidr[start_index]
+        end_ip = cidr[end_index]
+        network = cidr
+    elif interface:
+        network = interface.network
+        number_of_addr = network.num_addresses
+        if interface.ip < network[int(number_of_addr / 2)]:
+            if count and start_offset and not end_offset:
+                start_ip = interface.ip + start_offset
+                end_ip = start_ip + count - 1
+            elif count and end_offset and not start_offset:
+                end_ip = network[-1 - end_offset]
+                start_ip = end_ip - count + 1
+            elif start_offset and end_offset and not count:
+                start_ip = interface.ip + start_offset
+                end_ip = network[-1 - end_offset]
+            else:
+                raise IPUtilsException(
+                    "Argument error: must pass in exactly 2 of"
+                    "start_offset, end_offset and count")
+        else:
+            if count and start_offset and not end_offset:
+                start_ip = network[start_offset]
+                end_ip = start_ip + count -1
+            elif count and end_offset and not start_offset:
+                end_ip = interface.ip - end_offset
+                start_ip = end_ip - count + 1
+            elif start_offset and end_offset and not count:
+                start_ip = network[start_offset]
+                end_ip = interface.ip - end_offset
+            else:
+                raise IPUtilsException(
+                    "Argument error: must pass in exactly 2 of"
+                    "start_offset, end_offset and count")
+
+    else:
+        raise IPUtilsException("Must pass in cidr or interface to generate"
+                               "ip range")
+
+    range_result = _validate_ip_range(start_ip, end_ip, network)
+    if range_result:
+        ip_range = "{},{}".format(start_ip, end_ip)
+        return ip_range
+    else:
+        raise IPUtilsException("Invalid IP range: {},{} for network {}"
+                               .format(start_ip, end_ip, network))
+
+
+def get_ip(offset, cidr=None, interface=None):
+    """
+    Returns an IP in a network given an offset.
+
+    Either cidr or interface must be provided, cidr takes precedence.
+
+    If cidr is provided, offset is calculated from network address.
+    If interface is provided, offset is calculated from interface IP.
+
+    offset can be positive or negative, but the resulting IP address must also
+    be contained in the same subnet, otherwise an exception will be raised.
+
+    returns a IP address object.
+    """
+    if cidr:
+        ip = cidr[0 + offset]
+        network = cidr
+    elif interface:
+        ip = interface.ip + offset
+        network = interface.network
+    else:
+        raise IPUtilsException("Must pass in cidr or interface to generate IP")
+
+    if ip not in network:
+        raise IPUtilsException("IP {} not in network {}".format(ip, network))
+    else:
+        return str(ip)
 
 
 def generate_ip_range(args):
@@ -22,7 +145,8 @@ def generate_ip_range(args):
     start_position: starting index, default to first address in subnet (1)
     end_position:  ending index, default to last address in subnet (-1)
 
-    Returns IP range in string format. A single IP is returned if start and end IPs are identical.
+    Returns IP range in string format. A single IP is returned if start and
+    end IPs are identical.
     """
     cidr = ipaddress.ip_network(args.CIDR)
     (start_index, end_index) = (args.start_position, args.end_position)
@@ -32,23 +156,95 @@ def generate_ip_range(args):
         return ','.join(sorted([str(cidr[start_index]), str(cidr[end_index])]))
 
 
-def main():
-    import argparse
-    import sys
+def get_interface(nic, address_family=4):
+    """
+    Returns interface object for a given NIC name in the system
+
+    Only global address will be returned at the moment.
+
+    Returns interface object if an address is found for the given nic,
+    otherwise returns None.
+    """
+    if not nic.strip():
+        logging.error("empty nic name specified")
+        return None
+    output = subprocess.getoutput("ip -{} addr show {} scope global"
+                                  .format(address_family, nic))
+    if address_family == 4:
+        pattern = re.compile("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}")
+    elif address_family == 6:
+        pattern = re.compile("([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}/\d{1,3}")
+    else:
+        raise IPUtilsException("Invalid address family: {}"
+                               .format(address_family))
+    match = re.search(pattern, output)
+    if match:
+        logging.info("found interface {} ip: {}".format(nic, match.group()))
+        return ipaddress.ip_interface(match.group())
+    else:
+        logging.info("interface ip not found! ip address output:\n{}"
+                        .format(output))
+        return None
+
+
+def find_gateway(interface):
+    """
+    Validate gateway on the system
+
+    Ensures that the provided interface object is in fact configured as default
+    route on the system.
+
+    Returns gateway IP (reachable from interface) if default route is found,
+    otherwise returns None.
+    """
+
+    address_family = interface.version
+    output = subprocess.getoutput("ip -{} route".format(address_family))
 
-    parser = argparse.ArgumentParser()
-    subparsers = parser.add_subparsers()
+    pattern = re.compile("default\s+via\s+(\S+)\s+")
+    match = re.search(pattern, output)
 
-    parser_gen_ip_range = subparsers.add_parser('generate_ip_range', help='Generate IP Range given CIDR')
-    parser_gen_ip_range.add_argument('CIDR', help='Network in CIDR notation')
-    parser_gen_ip_range.add_argument('start_position', type=int, help='Starting index')
-    parser_gen_ip_range.add_argument('end_position', type=int, help='Ending index')
-    parser_gen_ip_range.set_defaults(func=generate_ip_range)
+    if match:
+        gateway_ip = match.group(1)
+        reverse_route_output = subprocess.getoutput("ip route get {}"
+                                                    .format(gateway_ip))
+        pattern = re.compile("{}.+src\s+{}".format(gateway_ip, interface.ip))
+        if not re.search(pattern, reverse_route_output):
+            logging.warning("Default route doesn't match interface specified: "
+                            "{}".format(reverse_route_output))
+            return None
+        else:
+            return gateway_ip
+    else:
+        logging.warning("Can't find gateway address on system")
+        return None
+
+
+def _validate_ip_range(start_ip, end_ip, cidr):
+    """
+    Validates an IP range is in good order and the range is part of cidr.
+
+    Returns True if validation succeeds, False otherwise.
+    """
+    ip_range = "{},{}".format(start_ip, end_ip)
+    if end_ip <= start_ip:
+        logging.warning("IP range {} is invalid: end_ip should be greater than "
+                        "starting ip".format(ip_range))
+        return False
+    if start_ip not in ipaddress.ip_network(cidr):
+        logging.warning('start_ip {} is not in network {}'
+                        .format(start_ip, cidr))
+        return False
+    if end_ip not in ipaddress.ip_network(cidr):
+        logging.warning('end_ip {} is not in network {}'.format(end_ip, cidr))
+        return False
 
-    args = parser.parse_args(sys.argv[1:])
-    print(args.func(args))
+    return True
 
 
-if __name__ == '__main__':
-    main()
+class IPUtilsException(Exception):
+    def __init__(self, value):
+        self.value = value
 
+    def __str__(self):
+        return self.value