Mini Shell

Direktori : /opt/sharedrads/
Upload File :
Current File : //opt/sharedrads/rotate_ip_addresses.py

#!/opt/imh-python/bin/python3

import argparse
import collections
import functools
import logging
import shlex
import os
import pp_api
import re
import shutil
import sys
import textwrap
import multiprocessing
import glob
import subprocess
import rads
import netaddr

runcmd = functools.partial(
    subprocess.run, check=True, capture_output=True, encoding='utf-8'
)

description = textwrap.dedent(
    """
    This script will change all users of the target IP to use one of the
    provided new IP addresses.

    Download and read this first: https://imhsc.imhadmin.net/modules/Fi"""
    """les/Setup%20Checklists/?download=MainIPChangeChecklist.ods

    Tasks you should already have performed:
    - Let support/systems know what you're about to do.
    - If the target is the main IP of the box
        - You've chosen one from the Shared block.
        - You've licensed cpanel/softaculous on the new IP.
        - Ensured that this script is being run over the backlan.
        - Ensured that this script is being run in a screen.
    - You've chosen a pool of dedicated IPs to use as the new shared IPs.
    - Chosen IPs are marked in system center.
    - A and PTR records are updated for the IPs in question.

    Tasks this script will perform:
    - Reconfigure networking, including the main IP if specified
    - Notify customers of their new IP via power panel
    - Configure cPanel for the new IP
        - /var/cpanel/mainip
        - /etc/wwwacct.conf
        - /var/cpanel/mainips/{root,inmotion,tier2s}
        - /etc/mailips - only if the target IP is here
        - /etc/reservedips
        - /etc/reservedipreasons
    - If the main IP is the target, change inmotion/tier2s/secure IP
    - Change all users on the target IP to one of the new IPs

    Tasks you should perform after user rotation begins:
    - Revoke the cPanel and Softaculous licenses.
    - Start the HUD updating from /root/ip_change_status
    - Double-check that the DNS for the secure hostname updated correctly
    - Review the checklist and confirm all steps manually
    """
)


def get_users_on(ip):
    """Returns a list of users on ip"""
    user_dir = "/var/cpanel/users"
    ip_matcher = re.compile("IP={}$".format(str(ip.ip).replace(".", r"\.")))
    user_list = []
    special_users = {'tier2s', 'inmotion', 'hubhost', rads.SECURE_USER}
    for user in os.listdir(user_dir):
        if user in special_users:
            continue
        with open(os.path.join(user_dir, user), encoding='ascii') as f:
            for line in f:
                if ip_matcher.match(line) is not None:
                    user_list.append(user)
                    break
    return user_list


def generate_work_list(destinations, user_list: list[str]):
    """Generates a dict of ip -> userlist mappings"""
    users = user_list.copy()
    mapping = collections.defaultdict(list)
    while len(users) > 0:
        for dest in destinations:
            if len(users) <= 0:
                break
            mapping[str(dest.ip)].append(users.pop())
    return mapping


def strip_quotes(string):
    """Strips quotes and whitespace from a string"""
    return string.strip("""'" \n\t""")


def get_main_ip():
    """Gets the current main IP of the server."""
    with open("/var/cpanel/mainip", encoding='ascii') as f:
        cpanel_main_ip = strip_quotes(f.read())
    ifcfg_addr = None
    ifcfg_mask = None
    with open(
        "/etc/sysconfig/network-scripts/ifcfg-eth0", encoding='ascii'
    ) as f:
        for line in f:
            if line.startswith("IPADDR="):
                ifcfg_addr = strip_quotes(line.split('=')[1])
            if line.startswith("NETMASK="):
                ifcfg_mask = strip_quotes(line.split('=')[1])
    if ifcfg_addr is None or ifcfg_mask is None:
        logging.critical(
            "Could not read eth0 address information, check IPADDR and NETMASK!"
        )
        sys.exit(1)
    if ifcfg_addr != cpanel_main_ip:
        logging.critical(
            "cPanel IP (%s) and eth0 IP (%s) do not match, fix this!",
            cpanel_main_ip,
            ifcfg_addr,
        )
        sys.exit(1)
    logging.info("Determined main IP to be %s", ifcfg_addr)
    return netaddr.IPNetwork(f"{ifcfg_addr}/{ifcfg_mask}")


def set_main_ip(ip):
    """Sets the main IP of the server"""
    # ifcfg-eth0
    new_ifcfg = []
    ifcfg_file = "/etc/sysconfig/network-scripts/ifcfg-eth0"
    with open(ifcfg_file, encoding='ascii') as f:
        for line in f:
            if line.startswith("IPADDR="):
                new_ifcfg.append(f"IPADDR={ip.ip}\n")
            elif line.startswith("NETMASK="):
                new_ifcfg.append(f"NETMASK={ip.netmask}\n")
            else:
                new_ifcfg.append(line)
    output = "".join(new_ifcfg)
    with open(ifcfg_file, 'w', encoding='ascii') as f:
        f.write(output)
    logging.info("Wrote new ifcfg-eth0")

    # /etc/sysconfig/network
    new_network = []
    network_file = "/etc/sysconfig/network"
    with open(network_file, encoding='ascii') as f:
        for line in f:
            if line.startswith("GATEWAY="):
                new_network.append(f"GATEWAY={ip.network + 1}\n")
            else:
                new_network.append(line)
    output = "".join(new_network)
    with open(network_file, 'w', encoding='ascii') as f:
        f.write(output)
    logging.info("Wrote new network config")

    # /var/cpanel/mainip
    with open("/var/cpanel/mainip", 'w', encoding='ascii') as f:
        f.write(str(ip.ip))
    logging.info("Wrote new /var/cpanel/mainip")

    # /etc/wwwacct.conf
    www_file = "/etc/wwwacct.conf"
    new_www = []
    with open(www_file, encoding='ascii') as f:
        for line in f:
            if line.startswith("ADDR "):
                new_www.append(f"ADDR {ip.ip}\n")
            else:
                new_www.append(line)
    output = "".join(new_www)
    with open(www_file, 'w', encoding='ascii') as f:
        f.write(output)
    logging.info("Wrote new wwwacct.conf")
    try:
        runcmd(['ifdown', 'eth0'])
        runcmd(['ifup', 'eth0'])
        runcmd(['service', 'ipaliases', 'restart'])
    except subprocess.CalledProcessError as exc:
        logging.critical(
            "Failed to configure eth0 when executing %s! STDOUT: %r STDERR: %r",
            shlex.join(exc.args),
            exc.stdout,
            exc.stderr,
        )
        sys.exit(1)

    logging.info("Completed reload of eth0 networking.")
    input(
        f"Please ping the new main IP {ip} and press ENTER when connectivity "
        "has been confirmed.  If the IP does not begin to ping within 2 "
        "minutes, press Ctrl+C to abort and check main IP configuration."
    )


def add_new_ips(destinations):
    """Adds a list of new shared IPs to the server"""
    mainips_root = "/var/cpanel/mainips"
    if 'hub' in os.uname()[1]:
        reseller = 'hubhost'
    else:
        reseller = 'inmotion'
    if not os.path.isdir(mainips_root):
        os.mkdir(mainips_root)
    with open(os.path.join(mainips_root, "root"), 'a', encoding='ascii') as f:
        for ip in destinations:
            f.write(f"{ip.ip}\n")
            logging.info("Added %s to mainips/root", ip.ip)
    shutil.copy(
        os.path.join(mainips_root, "root"), os.path.join(mainips_root, "tier2s")
    )
    logging.info("Copied mainips/root to mainips/tier2s")
    shutil.copy(
        os.path.join(mainips_root, "root"), os.path.join(mainips_root, reseller)
    )
    logging.info("Copied mainips/root to mainips/%s", reseller)
    with open("/etc/reservedips", 'a', encoding='ascii') as f:
        for ip in destinations:
            f.write(f"{ip.ip}\n")
            logging.info("Added %s to reservedips", ip.ip)
    with open("/etc/reservedipreasons", 'a', encoding='ascii') as f:
        for ip in destinations:
            f.write(f"{ip.ip}=backup mail ip\n")
            logging.info("Added %s to reservedipreasons", ip.ip)
    with open("/etc/ips", 'a', encoding='ascii') as f:
        for ip in destinations:
            f.write(f"{ip.ip}:{ip.netmask}:{ip.broadcast}\n")
            logging.info("Added %s to /etc/ips", ip)
    try:
        runcmd(['service', 'ipaliases', 'restart'])
        logging.info("Added IPs to the server successfully")
    except subprocess.CalledProcessError as exc:
        logging.critical(
            "Error restarting ipaliases after adding IPs. "
            "STDOUT: %s STDERR: %s",
            exc.stdout,
            exc.stderr,
        )


def remove_from_server(ip):
    """Removes an IP from the server (not a main ip)"""
    mainips_root = "/var/cpanel/mainips"
    if 'hub' in os.uname()[1]:
        reseller = 'hubhost'
    else:
        reseller = 'inmotion'
    if os.path.isdir(mainips_root):
        new_mainips = []
        with open(os.path.join(mainips_root, "root"), encoding='ascii') as f:
            for line in f:
                if line != f"{ip.ip}\n":
                    new_mainips.append(line)
        with open(
            os.path.join(mainips_root, "root"), 'w', encoding='ascii'
        ) as f:
            f.write("".join(new_mainips))
        shutil.copy(
            os.path.join(mainips_root, "root"),
            os.path.join(mainips_root, "tier2s"),
        )
        logging.info("Copied mainips/root to mainips/tier2s")
        shutil.copy(
            os.path.join(mainips_root, "root"),
            os.path.join(mainips_root, reseller),
        )
        logging.info("Copied mainips/root to mainips/%s", reseller)
    new_reservedips = []
    with open("/etc/reservedips", encoding='ascii') as f:
        for line in f:
            if line != f"{ip.ip}\n":
                new_reservedips.append(line)
    with open("/etc/reservedips", 'w', encoding='ascii') as f:
        f.write("".join(new_reservedips))
    new_reasons = []
    with open("/etc/reservedipreasons", encoding='ascii') as f:
        for line in f:
            if not line.startswith(f"{ip.ip}="):
                new_reasons.append(line)
    with open("/etc/reservedipreasons", 'w', encoding='ascii') as f:
        f.write("".join(new_reasons))
    new_ips = []
    with open("/etc/ips", encoding='ascii') as f:
        for line in f:
            if not line.startswith(f"{ip.ip}:"):
                new_ips.append(line)
    with open("/etc/ips", 'w', encoding='ascii') as f:
        f.write("".join(new_ips))


def validate_ip(address):
    """Validates an IP and exits on failure."""
    try:
        ip = netaddr.IPNetwork(address)
    except netaddr.core.AddrFormatError:
        logging.critical("The specified address '%s' is not valid!", address)
        sys.exit(1)
    if ip.size <= 2:
        logging.critical(
            "The prefix of specified address '%s' is too short to be a usable "
            "address. You must enter a valid IP with CIDR netmask.",
            ip,
        )
        sys.exit(1)
    return ip


def get_args(args=None):
    """Gets the command line arguments or parses args if set."""
    parser = argparse.ArgumentParser(
        description=description,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        'target_ip',
        help="The IP to be changed.  Should currently be black-holed. Must be "
        "specified in CIDR notation.",
    )
    parser.add_argument(
        'shared_ip',
        nargs='+',
        help="The shared IPs over which users will be distributed. These must "
        "be specified in CIDR notation.",
    )
    parser.add_argument(
        '-m',
        '--mainip',
        dest='main_ip',
        help="The new main IP. If the target is not the main IP, this is "
        "optional. Must be specified in CIDR notation.",
    )
    parser.add_argument(
        '--loglevel',
        default="INFO",
        help="Specify a log level, defaults to INFO.",
    )

    if args is not None:
        return parser.parse_args(args)
    return parser.parse_args()


def mail_users(work_dict):
    """Sends mail to each user with their new IP"""
    pp = pp_api.PowerPanel()
    if 'hub' in os.uname()[1]:
        template_id = 539
    else:
        template_id = 538
    data = {'template': template_id, 'cpanelUser': None, 'variable_1': None}

    for ip in work_dict:
        for user in work_dict[ip]:
            data['cpanelUser'] = user
            data['variable_1'] = ip
            req = pp.call("notification.send", **data)
            if req.status != 0:
                logging.error(
                    "Failed to send notification to %s with IP %s because %s.",
                    user,
                    ip,
                    req.message,
                )
            else:
                logging.info("Success: %s notified of IP change %s", user, ip)


def change_user_ips(work_dict):
    """Changes the IP of each user per the work provided"""
    total_users = sum(len(work_dict[i]) for i in work_dict)
    count = 0.0
    setsiteip = ['/usr/local/cpanel/bin/setsiteip', '-u']
    for ip in work_dict:
        for user in work_dict[ip]:
            try:
                output = runcmd(setsiteip + [user, ip]).stdout
                if len(output) > 5:
                    logging.warning(
                        "Possible issue setting %s to %s: %r", user, ip, output
                    )
                else:
                    logging.info("Success: %s set to %s", user, ip)
            except subprocess.CalledProcessError as exc:
                logging.error(
                    "Failed to change IP for %s to %s.  STDOUT: %r STDERR: %r",
                    user,
                    ip,
                    exc.stdout,
                    exc.stderr,
                )
            count += 1
            with open('/root/ip_change_status', 'w', encoding='ascii') as f:
                f.write(f"{count / total_users * 100:0.2f}%\n")


def change_main_users(ip):
    """Changes the "main" users to use the new main IP"""
    if 'hub' in os.uname()[1]:
        reseller = 'hubhost'
    else:
        reseller = 'inmotion'
    change_user_ips({ip: ['tier2s', rads.SECURE_USER, reseller]})


def parse_userdomains():
    dom_owners = {}
    with open('/etc/userdomains', encoding='ascii') as handle:
        for line in handle:
            domain, user = line.split(':')
            domain = domain.strip()
            user = user.strip()
            if domain != '*':
                dom_owners[domain] = user
    return dom_owners


def update_dns(work_dict, target):
    """Manually updates zone files for records that setsiteip
    does not touch. (SPF)"""
    try:
        dnscluster = runcmd(["/scripts/dnscluster"])
    except FileNotFoundError:
        logging.error("Unable to find cPanel tools!")
        return
    for zone_file in glob.glob("/var/named/*.db"):
        zone_name = zone_file.split("/")[-1].split(".db")[0]
        try:
            owner = DOM_OWNERS[zone_name]
        except KeyError:
            logging.error(
                'Unable to determine owner of %s from /etc/userdomains',
                zone_name,
            )
            owner = None

        new_ip = None
        for ip, users in work_dict.items():
            for user in users:
                if user.rstrip() == owner:
                    new_ip = ip
                    break

        if new_ip is None:
            logging.warning("Unable to determine new IP of %s", zone_name)
            continue

        lines = []
        try:
            with open(zone_file, encoding='ascii') as infile:
                for line in infile:
                    line = line.replace(str(target.ip), new_ip)
                    lines.append(line)
            if len(lines) <= 10:
                logging.warning(
                    "Zone file for %s is suspiciously short! Skipping...",
                    zone_name,
                )
                continue
            with open(zone_file, 'w', encoding='ascii') as outfile:
                for line in lines:
                    outfile.write(line)
        except OSError:
            logging.error("Unable to read/write %s", zone_file)
            continue

        dnscluster(synczone=zone_name)
        logging.info("Replaced DNS records for %s", zone_name)


def main():
    args = get_args()
    rads.setup_logging(
        path="/var/log/ip_address_rotation.log",
        fmt="[%(asctime)s] %(levelname)s: %(message)s",
        loglevel=getattr(logging, args.loglevel.upper(), logging.INFO),
        print_out=sys.stderr,
    )
    target = validate_ip(args.target_ip)
    logging.info("Validated target IP '%s'", target)
    destinations = [validate_ip(addr) for addr in args.shared_ip]
    logging.info(
        "Validated destination IP(s) '[%s]'", ", ".join(map(str, destinations))
    )
    current_main_ip = get_main_ip()
    if args.main_ip is not None:
        main_ip = validate_ip(args.main_ip)
        logging.info("Validated main IP '%s'", main_ip)
    else:
        main_ip = current_main_ip
        logging.info("Not changing main IP '%s'", main_ip)

    if target.ip == main_ip.ip:
        logging.critical(
            "The target IP (%s) is the current main IP of the server!  You "
            "must specify a new main IP!",
            target,
        )
        sys.exit(1)

    affected_users = get_users_on(target)

    logging.info("Pre-flight checks complete.  Confirming...")
    print(description)
    print("The following changes will be made to the server:")
    print(
        "- All {} users on {} will be migrated to one of the following:".format(
            len(affected_users), target.ip
        )
    )
    for ip in destinations:
        print(f"    - {ip.ip} with netmask {ip.netmask}")
    print(
        f"- The main IP of the server will be '{main_ip.ip}',",
        f"the netmask will be '{main_ip.netmask}',",
        f"and the gateway will be '{main_ip.network}'.",
    )
    print()
    print("The current status percentage will be in /root/ip_change_status")
    print()
    confirmation = input(
        "Please take a moment to thoroughly read and ensure you understand "
        "all of the information above.  If you have confirmed that this script"
        " is about to do what you want, type 'YES' to continue: "
    )
    if confirmation != 'YES':
        logging.critical("User did not enter 'YES' when prompted.  Aborting.")
        sys.exit(1)
    logging.info("User has confirmed all changes.  Proceeding.")

    work = generate_work_list(destinations, affected_users)

    if target.ip == current_main_ip.ip:
        set_main_ip(main_ip)
        change_main_users(str(main_ip.ip))
    else:
        remove_from_server(target)

    add_new_ips(destinations)

    rebuild = ["/scripts/rebuildhttpdconf"]

    mailer = multiprocessing.Process(target=mail_users, args=(work,))
    mailer.start()

    ip_changer = multiprocessing.Process(target=change_user_ips, args=(work,))
    ip_changer.start()

    dns_updater = multiprocessing.Process(
        target=update_dns, args=(work, target)
    )
    dns_updater.start()

    try:
        runcmd(rebuild)
    except subprocess.CalledProcessError:
        logging.error('rebuildhttpdconf failed')

    mailer.join()
    logging.info("Finished mailing")
    ip_changer.join()
    logging.info("Finished changing IPs")
    dns_updater.join()
    logging.info("Finished updating extra DNS records")

    try:
        runcmd(rebuild)
    except subprocess.CalledProcessError:
        logging.error('rebuildhttpdconf failed')

    try:
        runcmd(["/scripts/restartsrv_httpd", "--graceful"])
    except subprocess.CalledProcessError:
        logging.error('failed to reload httpd')


if __name__ == "__main__":
    DOM_OWNERS = parse_userdomains()
    main()

Zerion Mini Shell 1.0