#!/usr/bin/python3
'''
cpbx-trunk-ip configures IP addresses on the requested interface and
modifies the Kamalio listeners accordingly.
This is usefull in the cases ofr the IP-based authentication for the SIP trunks.
Written: Leonid Fainshtein <leonid.fainshtein@xorcom.com>
Xorcom Ltd
'''
import argparse
import yaml
import sys
import os
import datetime
import shutil
import re
import ipaddress

VERSION="1.0.1"

# The YAML Python module works very different in Python < 3.9.x
# We need to adjust the code accordingly because Python 3.6.9 is used in Ubuntu 18.04
MODERN_PYTHON = True
pmajor, pminor, __,__,__ = sys.version_info
if pmajor ==3 and pminor < 9:
    MODERN_PYTHON = False
#print(f'Pyton is modern? {MODERN_PYTHON}')

def configInterface(fileName, interfaceName, firstIP, ipNum, applyChanges):
    """
    Replace an array in a YAML file.
    Parameters:
        fileName : the YAML file name.
        key_path: list of keys representing the path to the array within the YAML file.
        new_array : the new array to replace the existing one.
        applyChanges: (boolean) apply or not the chnages 
    """
    print(f'I: -- Configuring the IP addresses in the {fileName} for interface {interfaceName}')

    try:
        with open(fileName, 'r') as f:
            # We use the 'FullLoader' in order to be able to show a more clear error message
            # when file parsing failed.
            if MODERN_PYTHON:
                data = yaml.load(f,Loader=yaml.FullLoader) 
            else:
                data = yaml.load(f) 
    except yaml.YAMLError as e:
      print(f'E: Failed to parse the {fileName} file', e)
      exit(1)

    key_path = ['network', 'ethernets', interfaceName,'addresses']

    # Make sure that the required path in the YAML file exists.
    current_dict = data
    for key in key_path[:-1]:
        if key in current_dict:
            current_dict = current_dict[key]
        else:
            die(f"E: Key '{key}' not found in the YAML file.", 1)

    # Replace the array
    last_key = key_path[-1]
    if last_key in current_dict:
        # Prepare a list of IP address that will replace the currently configured list.
        # The new list must include the requested range of IPs and the old IPs from other subnets.

        ip,net = firstIP.split('/')
        parts = ip.split('.')
        # Create a list of IP addresses that will be applied:
        requested_range = []
        lastpart = int(parts[3])
        for i in range(0, ipNum):
            requested_range.append(f'{".".join(parts[:3])}.{str(lastpart+i)}/{net}')

        new_ip_list =[]
        
        # We should replace all current IPs that are from the requsted subnet.
        # For example, the curremntly configured 172.16.200.5/24 must be deleted if the requested range
        # is "10 addresses starting from 172.16.200.25/24"
        
        # Build the full list of IP addresses in the requested subnet
        ip_subnet = ipaddress.ip_interface(firstIP).network
        
        # Create new_ip_list list that includes the old not related to the requested range addresses
        # plus the requested addresses
        for cidr in current_dict[last_key]:
            # cidr - is currently configured IP in the CIDR format
            addr,__ = cidr.split('/')
            if ipaddress.ip_address(addr) not in ip_subnet:
                new_ip_list.append(cidr)
        new_ip_list.extend(requested_range)

        # Replace the currently configured IPs with the new list.
        current_dict[last_key] = new_ip_list
    else:
        die(f"E: Key '{last_key}' not found in the YAML file.", 1)

    if applyChanges:
        # Write the modified data back to the file
        create_backup(fileName)
        with open(fileName, 'w') as f:
            if MODERN_PYTHON:
                yaml.dump(data, f, sort_keys=False)
            else:
                yaml.dump(data, f)
        print(f'I: the {fileName} file has been successfully updated.')
    else:
        print(f'The updated {fileName} will look as the following:\n')
        if MODERN_PYTHON:
            print(yaml.dump(data,sort_keys=False))
        else:
            print(yaml.dump(data, default_flow_style=False))

def create_backup(fileName):
    now = datetime.datetime.now()
    backupFileName = fileName + "_" + now.strftime("%Y%m%d-%H%M%S")
    shutil.copy2(fileName, backupFileName)
    print(f'I: created the backup file {backupFileName}')

def configProxy(fileName, firstIP, ipNum, applyChanges):
    """
    Configure the listeners for the SIP proxy
    Each address will ba added as 2 lines:
        socket_workers=2 
        listen=udp:172.25.109.17:5060
    Parameters:
        fileName: the SIP proxy configuration file that will be updated.
                  The file will be created if it doesn't exist.
        firstIP: first IPv4 address in CIDR in the range to be configured
        ipNum: number of IPs to be configured
        applyChanges: (boolean) apply or not the chnages 
    """
    print(f'I: -- Preparing the SIP proxy configuration the {fileName}.')
    ip,net = firstIP.split('/')
    parts = ip.split('.')

    # Create a list of IP addresses that will be applied:
    newAddresses = []
    lastpart = int(parts[3])
    for i in range(0, ipNum):
        newAddresses.append('socket_workers=2')
        newAddresses.append(f'listen=udp:{".".join(parts[:3])}.{str(lastpart+i)}:5060')
    if not applyChanges:
        print(f'The updated SIP proxy configuration {fileName} file will look as the following:\n')
        for l in newAddresses:
            print(l)
    else:
        if os.path.isfile(fileName):
            create_backup(fileName)
        with open(fileName, 'w') as f:
            for l in newAddresses:
                f.write(l + '\n')
        print(f'I: the {fileName} file has been successfully updated.')


# Checks that 'address' is a valid IPv4 address in the CIDR format
def validate_address(address):
    try:
        ip = address.split('/')[0]
    except Exception:
        return False
    
    regex = r'^(25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$'
    match = re.match(regex, ip)
    
    return bool(match)

def die(msg, rc):
	print(msg)
	sys.exit(rc)

if __name__ == "__main__":
    print(f'Version: {VERSION}')
    parser = argparse.ArgumentParser(description='Configure IP addresses for the MT Manager SIP trunks.')
    parser.add_argument('-f', '--file', type=str, required=True, help='The netplan configuration YAML file to be changed.')
    parser.add_argument('-i', '--interface', type=str, required=True, metavar='NET_INTERFACE', help='The network interface name the IP addresses to be configured for.')
    parser.add_argument('-a', '--address', type=str, required=True, metavar='IP/NET', help='The initial IP address in CIDR format (e.g., 1.2.3.4/24).')
    parser.add_argument('-n', '--number', type=int, required=True, choices=range(1, 253), metavar='NUM', help='Number of the IP addresses to be configured.')
    parser.add_argument('-p', '--proxy', action='store_true', help='Modify the SIP Proxy settings accordingly.(default: %(default)s)')
    parser.add_argument('--apply', action='store_true', help='Apply the new settings.(default: %(default)s)')
    args = parser.parse_args()

    # SIP Proxy configuration file that will be updated.
    proxyCfgFile = '/etc/cpbxmt/sip-proxy/conf.d/listen-trunks.cfg'

    #Validate IP address
    if not validate_address(args.address):
        die(f'Wrong IP address {args.address} is requested. The address must be a valid IPv4 in the CIDR format.', 1)
    
    # Check that the requested netplan's configuration file exists and writable
    if not os.access(args.file, os.W_OK):
        die(f'The {args.file} doesn\'t exist or is not writable.', 1)
    
    configInterface(args.file, args.interface, args.address, args.number, args.apply)

    if args.proxy:
        configProxy(proxyCfgFile, args.address, args.number, args.apply)
    
    if args.apply:
        print('\nNOTE! The configuration file(s) have been updated.')
        print('Run "netplan try" to apply the IP addresses chenges.')
        if args.proxy:
            print('Run "systemctl restart kamailio" to apply the SIP proxy changes.')
    else:
        print('\nNOTE! The configuration files were not updated! Please run the same command with option --apply to update the files.\n')

    print('*** SUCCESS ***')
