#!/usr/bin/env python
### Copyright 1999-2014. Parallels IP Holdings GmbH. All Rights Reserved.

import os
import sys
from optparse import OptionParser
import logging as log
import fileinput
import plesk_subprocess as subprocess
import re

PLESK_LIBEXEC_DIR = '/usr/lib64/plesk-9.0'

# http://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
def valid_hostname(hostname):
    """ Check that provided hostname is valid. """
    if len(hostname) > 255:
        return False
    if hostname.endswith("."): # A single trailing dot is legal
        hostname = hostname[:-1] # strip exactly one dot from the right, if present
    disallowed = re.compile("[^A-Z0-9-]", re.IGNORECASE) # The RFC mandate that component hostname labels may contain only the ASCII letters 'a' through 'z' (in a case-insensitive manner), the digits '0' through '9', and the hyphen ('-').
    return all( # Split by labels and verify individually
        (label and len(label) <= 63 # length is within proper range
         and not label.startswith("-") and not label.endswith("-") # no bordering hyphens
         and not disallowed.search(label)) # contains only legal characters
        for label in hostname.split("."))

def update_hostname_file(filename, fqdn):
    """ Function to write new hostname into files like /etc/hostname, /etc/mailname and so on """
    if not os.path.exists(filename):
        log.debug("%s not updated, as it doesn't exists", filename)
        return False

    with open(filename, 'w') as f:
        f.write("%s\n" % fqdn)

def _prepare_hosts_line(line, old_fqdn, hostname, domainname):
    """ Insert new entry in head of hosts line (after ip address) and remove old FQDN entries """
    chunks = line.split()

    hostnames = ["%s.%s" % (hostname, domainname), hostname]
    hostnames.extend(filter(lambda x: (x != old_fqdn and x != hostname), chunks[1:]))

    return "%s\t%s\n" % (chunks[0], " ".join(hostnames))

def update_hosts_file(hosts_file, old_fqdn, hostname, domainname):
    """ Replace old entries in hosts file, or append new one """
    new_fqdn = "%s.%s" % (hostname, domainname)

    with open(hosts_file, 'r+') as f:
        contents = f.readlines()
        found_old_fqdn = False
        found_localhost = False

        for line in contents:
            # Fully commented line, ignoring
            if line.strip().startswith("#"):
                continue

            have_comment = line.find('#')
            canonical_line = None
            if have_comment != -1:
                canonical_line = line[:have_comment].strip()
            else:
                canonical_line = line.strip()

            if canonical_line.startswith("127.0.0.1") or canonical_line.startswith("::1"):
                found_localhost = True

            if old_fqdn and canonical_line.find(old_fqdn) != -1:
                found_old_fqdn = True

        if not found_old_fqdn and not found_localhost:
            log.debug("Appending new entry to hosts file, as neither loopback entry nor old_fqdn not found")
            f.write("127.0.0.1\t%s %s" % (new_fqdn, hostname))
            return

        # Truncate file inplace
        f.seek(0)
        f.truncate()

        for line in contents:
            if found_old_fqdn and line.find(old_fqdn) != -1:
                log.debug("Replace line '%s' in hosts, as old FQDN found", line.strip())
                f.write(_prepare_hosts_line(line, old_fqdn, hostname, domainname))
                continue

            if (not found_old_fqdn) and found_localhost and (line.startswith("127.0.0.1") or line.startswith("::1")):
                log.debug("Replace loopback line '%s' in hosts, as no old FQDN found", line.strip())
                f.write(_prepare_hosts_line(line, old_fqdn, hostname, domainname))
                continue

            f.write(line)

def update_network_file(filename, new_fqdn):
    """ Replace hostname in /etc/sysconfig/network file """
    if not os.path.exists(filename):
        log.debug("Do not update '%s', as it doesn't exist", filename)
        return False

    for line in fileinput.input(filename, inplace=True):
        if line.strip().startswith("HOSTNAME="):
            sys.stdout.write("HOSTNAME=%s" % new_fqdn)
        else:
            sys.stdout.write(line)

def get_hostname():
    """ Obtain current hostname from system """
    try:
        return subprocess.check_output(["/bin/hostname", "--fqdn"]).rstrip()
    except subprocess.CalledProcessError:
        log.debug("Cannot get FQDN hostname")

    return None

def set_hostname(hostname, domainname):
    """ Set hostname on Linux host """
    new_fqdn = "%s.%s" % (hostname, domainname)

    old_fqdn = get_hostname()
    log.debug("Got current hostname %s", old_fqdn)

    log.debug("Update hostname in system")
    subprocess.check_call(["/bin/hostname", new_fqdn])

    for hostname_file in ["/etc/hostname", "/etc/HOSTNAME"]:
        log.debug("Update hostname in %s, if exists", hostname_file)
        update_hostname_file(hostname_file, new_fqdn)

    log.debug("Update hostname in /etc/hosts")
    update_hosts_file("/etc/hosts", old_fqdn, hostname, domainname)

    log.debug("Update hostname in /etc/sysconfig/network file")
    update_network_file("/etc/sysconfig/network", new_fqdn)

    log.debug("Update hostname via mailsrv_set_hostname")
    toolpath = PLESK_LIBEXEC_DIR + "/mailsrv_set_hostname"
    if not os.path.exists(toolpath):
        log.debug("%s not updated, as utility doesn't exists")
    else:
        subprocess.check_call([toolpath, new_fqdn])

    log.debug("Update hostname in /etc/mailname")
    update_hostname_file("/etc/mailname", new_fqdn)

def reconfig():
    """ Parse command line argument and do the work """
    usage = "usage: %prog [-v] hostname domainname"
    parser = OptionParser(usage=usage)
    parser.description = "Utility set hostname"
    parser.add_option('-v', '--verbose', action='count', dest='verbose',
                      help='Increase verbosity (can be specified multiple times)')

    (options, args) = parser.parse_args()
    log_level = log.WARNING

    if options.verbose == 1:
        log_level = log.INFO
    elif options.verbose >= 2:
        log_level = log.DEBUG

    log.basicConfig(format='%(levelname)s:%(message)s', level=log_level)

    log.debug("Options: %s", options)
    log.debug("Args: %s", args)

    if len(args) < 2:
        parser.error("Not enough arguments")

    hostname = args[0]
    domainname = args[1]

    if not hostname or not domainname:
        parser.error("Not enough arguments")

    if not valid_hostname("%s.%s" % (hostname, domainname)):
        parser.error("Specified hostname doesn't valid")

    if os.geteuid() != 0:
        raise RuntimeError("You need to have root privileges to run this utility")

    return set_hostname(hostname, domainname)

def main():
    """ reconfig main entry point """
    try:
        return reconfig()
    except SystemExit:
        raise
    except Exception, ex:
        sys.stderr.write('%s\n' % ex)
        log.debug('This exception happened at:', exc_info=sys.exc_info())
        sys.exit(1)

if __name__ == '__main__':
    main()

# vim: ts=4 sts=4 sw=4 et :

