#!/usr/bin/python
#
# Copyright 2008-2011 VMware, Inc.  All rights reserved.
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
from __future__ import print_function

import getopt
import libxml2
import os
import pdb
import re
import subprocess
import sys
import tempfile
import time

sys.path.append("opt/vmware/share/vami/")
import vami_log

log = None

import gettext

__t = gettext.translation('vami_ovf_process', '/opt/vmware/lib/locale', fallback=True)
_ = __t.ugettext if sys.version_info[0] < 3 else __t.gettext


#
# Constants
#
STUDIO_ROOT = os.path.join(os.sep, 'opt', 'vmware')
VAMI_ROOT = os.path.join(STUDIO_ROOT, "share", "vami")
SET_DNS = os.path.join(VAMI_ROOT, "vami_set_dns")
SET_NETWORK = os.path.join(VAMI_ROOT, "vami_set_network")
ENSURE_CONFIG = os.path.join(VAMI_ROOT, "vami_ensure_network_configuration")
SET_HOSTNAME = os.path.join(VAMI_ROOT, "vami_set_hostname")
SET_TIMEZONE = os.path.join(VAMI_ROOT, "vami_set_timezone_cmd")
UDEV_UPDATE = os.path.join(VAMI_ROOT, "vami_udev_update")

OVFENV_CMD = os.path.join(STUDIO_ROOT, "bin", "ovfenv")

OVF_INFO = os.path.join(STUDIO_ROOT, "etc", "vami", "vami_ovf_info.xml")
OVF_LOG = os.path.join(STUDIO_ROOT, "var", "log", "vami", "vami-ovf.log")

INTERFACES = os.path.join(VAMI_ROOT, "vami_interfaces")
DEFAULT_GATEWAY = "default"

OSP_TOOLS_CONFIG_FILE = os.path.join(os.sep, 'etc', 'vmware-tools', 'tools.conf')
OSP_TOOLS_DISABLE_VERSION = 'disable-tools-version'


class OvfSettings:
    def __init__(self):
        """
        Constructor to create the xml doc to manage the ovf settings xml file
        """
        self.file = OVF_INFO
        self.doc = libxml2.parseFile(self.file)
        self.ctxt = self.doc.xpathNewContext()

    def __del__(self):
        """
        Destructor, free the xml doc and context
        """
        self.ctxt.xpathFreeContext()
        self.doc.freeDoc()

    def saveDoc(self):
        """
        Save Doc to file
        """
        f = open(self.file, 'w')
        self.doc.saveTo(f)
        f.close

    def setNetworkManagedExternally(self, device):
        """
        Set the Ovf flag to true, indicates that ovf properties are present
        """
        result = self.ctxt.xpathEval("//ovf-info/network")
        result[0].setProp("ovf" + device, "true")
        self.saveDoc()

    def setNetworkManagedInternally(self, device):
        """
        Set the ovf flag to false, indicates that ovf properties are no longer
        present or that ip address was changed outside of OVF properties
        """
        result = self.ctxt.xpathEval("//ovf-info/network")
        result[0].setProp("ovf" + device, "false")
        self.saveDoc()

    def isNetworkManagedExternally(self, device):
        """
        Get the ovf flag
        """
        result = self.ctxt.xpathEval("//ovf-info/network")
        if result[0].prop("ovf" + device) == "false":
            return False
        else:
            return True


    def setEulaFlag(self):
        """
        set eula flag indicates that ovf properties are present
        """
        result = self.ctxt.xpathEval("//ovf-info/eula")
        result[0].setProp("ovf", "true")
        self.saveDoc()

    def getEulaFlag(self):
        """
        get eula flag
        """
        result = self.ctxt.xpathEval("//ovf-info/eula")
        return result[0].prop("ovf")


def getContext():
    nsdict = {
        'default': "http://schemas.dmtf.org/ovf/environment/1",
        'xsi': "http://www.w3.org/2001/XMLSchema-instance",
        'oe': "http://schemas.dmtf.org/ovf/environment/1",
        've': "http://www.vmware.com/schema/ovfenv"}

    doc = getOvfDoc()
    if (doc == None):
        return None

    xp = doc.xpathNewContext()

    for k, v in nsdict.items():
        xp.xpathRegisterNs(k, v)

    return xp


def getMacAddressList(xp):
    """
    Given an xpath context, return the list of Adapter mac addresses
    """
    alist = []
    node = xp.xpathEval("//default:Environment/ve:EthernetAdapterSection/ve:Adapter")

    for elem in node:
        alist.append(elem.prop("mac"))

    return alist


def getPropValue(xp, key):
    """
    Given an xpath context and a key for a property, return the value for that key
    """
    value = ""
    node = xp.xpathEval("//default:Environment/default:PropertySection/default:Property")

    for elem in node:
        if (key == elem.prop("key")):
            value = elem.prop("value")

    return value


def hasPropKey(xp, key):
    """
    Given an xpath context and a key for a property, return if the key exists
    """
    value = ""
    node = xp.xpathEval("//default:Environment/default:PropertySection/default:Property")

    for elem in node:
        if (key == elem.prop("key")):
            return True

    return False


def getApplianceName(xp):
    return getPropValue(xp, "vm.vmname")


def setDns(dns, domain, searchpath):
    cmd = [SET_DNS]

    if domain:
        cmd += ["-d", domain]

    # search path can be space or comma separated
    if searchpath:
        cmd += ["-s"] + [searchpath.replace(',', ' ')]

    # retrieve at most first two dns server entries from the dns list
    # (represented as a string with , as the separator)
    dnsList = dns.split(',')

    # remove whitespace from each address
    dnsList = map(lambda x: x.strip(), dnsList)

    if len(dnsList) > 2:
        log.message(vami_log.LOG_WARNING, "VAMI can configure a maximum of two DNS servers. Ignoring the rest.");

    cmd += dnsList
    log.message(vami_log.LOG_INFO, "Setting DNS to: " + ','.join(dnsList));
    buf = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=False).communicate()[0].strip()
    log.message(vami_log.LOG_INFO, "Output:" + buf)


def convertPrefixToNetmask(prefix):
    canonicalMask = ''
    prefix_num = int(prefix)
    mask = [0, 0, 0, 0]
    for i in range(prefix_num):
        mask[int(i/8)] = mask[int(i/8)] + (1 << (7 - i % 8))
    for i in range(4):
        canonicalMask += str(mask[i])
        if ( i < 3):
           canonicalMask += "."
    return canonicalMask


def validateNetMask(netmask):
    canonicalMask = "";
    if netmask.find('.') > 0:
        canonicalMask = netmask
    else:
        #prefix
        if (int(netmask) >= 8  and int(netmask) <= 31):
            canonicalMask = convertPrefixToNetmask(netmask)
            log.message(vami_log.LOG_INFO, "Convert prefix " + netmask + " to " + canonicalMask)
        else:
            log.message(vami_log.LOG_ERROR, "Error: wrong net prefix: " + netmask)
    return canonicalMask


def setIp(ifc, ip, netmask, gateway):
    log.message(vami_log.LOG_INFO,
                "Setting network information for " + ifc + " ip: " + ip + " netmask: " + netmask + " gateway: " + gateway)
    if ip.find(':') == -1:
        netmask=validateNetMask(netmask)
        buf = \
        subprocess.Popen([SET_NETWORK, ifc, 'STATICV4', ip, netmask, gateway], stdout=subprocess.PIPE).communicate()[
            0].strip()
    else:
        buf = \
        subprocess.Popen([SET_NETWORK, ifc, 'STATICV6', ip, netmask, gateway], stdout=subprocess.PIPE).communicate()[
            0].strip()
    log.message(vami_log.LOG_INFO, "Output:" + buf)


def unsetIp(ifc):
    log.message(vami_log.LOG_INFO, "Setting network information for " + ifc)
    buf = subprocess.Popen([SET_NETWORK, ifc, 'DHCPV4'], stdout=subprocess.PIPE).communicate()[0].strip()
    log.message(vami_log.LOG_INFO, "Output:" + buf)


def setHostName(hostname):
    if ( len(hostname) > 0 and len(hostname) < 254 and re.match('^\w[\w\.\-]*$', hostname) ):
        log.message(vami_log.LOG_INFO, "Setting Hostname to " + hostname)
        buf = subprocess.Popen([SET_HOSTNAME, hostname], stdout=subprocess.PIPE).communicate()[0].strip()
        log.message(vami_log.LOG_INFO, "Output:" + buf)
    else:
        if len(hostname) > 0 :
            log.message(vami_log.LOG_INFO, "Hostname property " + hostname + " is invalid, querying and setting hostname.")
        else:
            log.message(vami_log.LOG_INFO, "Hostname property is not present, querying and setting hostname.")
        buf = subprocess.Popen(SET_HOSTNAME, stdout=subprocess.PIPE).communicate()[0].strip()
        log.message(vami_log.LOG_INFO, "Output:" + buf)


def setTimezone():
    xp = getContext()
    if (xp == None):
        return 1

    timezone = getPropValue(xp, "vamitimezone")
    if (len(timezone) > 0):
        log.message(vami_log.LOG_INFO, _("Setting timezone to ") + timezone, True)
        buf = subprocess.Popen([SET_TIMEZONE, timezone], stdout=subprocess.PIPE).communicate()[0].strip()
        log.message(vami_log.LOG_INFO, "Output:" + buf)
        return 0
    else:
        log.message(vami_log.LOG_INFO, "Timezone string is empty.")

    return 1


def getOvfBuf():
    #
    # Dump the entire XML using ovfenv. It will
    # get it from a cdrom or guestinfo, as
    # appropriate
    #
    pobj = subprocess.Popen([OVFENV_CMD, '--dump'], shell=False,
                            universal_newlines=True,
                            stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT)
    buf = pobj.stdout.read()
    ret = pobj.wait()

    if ret:
        log.message(vami_log.LOG_INFO, "Unable to retrieve the OVF environment: " + buf)
        return None
    return buf


def prependWithDev(cdDev):
    return "/dev/" + cdDev


def execute(args, errorFile='/opt/vmware/var/log/vami/vami-ovf.log'):
    stderrFile = open(errorFile, 'a')
    try:
        return not subprocess.call(args, stderr=stderrFile)
    finally:
        stderrFile.close()


def mount(device):
    mntDir = tempfile.mkdtemp()
    args = ["mount", "-r", device, mntDir]
    retValue = execute(args)
    if not retValue:
        os.rmdir(mntDir)
        raise Exception("Error mounting " + device + " on " + mntDir)
    return mntDir


def unmount(mntDir):
    args = ["umount", mntDir]
    execute(args)
    os.rmdir(mntDir)


def getOvfDoc():
    try:
        buf = getOvfBuf()
        if buf:
            doc = libxml2.parseMemory(buf, len(buf))
            log.message(vami_log.LOG_INFO, "Ovf properties found.")
            return doc
        else:
            log.message(vami_log.LOG_INFO, "Ovf properties not present.")
            return None
    except:
        log.message(vami_log.LOG_ERROR, "Error while reading Ovf properties xml.")
        return None


def reorderUdevRules():
    xp = getContext()
    if (xp == None):
        return 1

    macs = getMacAddressList(xp)
    macs_file = None
    try:
        macs_file = tempfile.NamedTemporaryFile(delete=False)

        for item in macs:
            print (item, end="", file=macs_file)

        log.message(vami_log.LOG_INFO, "Reset udev rules with supplied MAC addresses")
        buf = subprocess.Popen([UDEV_UPDATE, macs_file.name], stdout=subprocess.PIPE).communicate()[0].strip()
        log.message(vami_log.LOG_INFO, "Output:" + buf)

        os.remove(macs_file.name)
    except Exception as msg:
        log.message(vami_log.LOG_ERROR, "Error while reading Ovf Adapter MAC addresses: " + str(msg))
        if macs_file != None:
            os.remove(macs_file.name)


def setNetwork():
    domain = ""
    searchpath = ""
    o = OvfSettings()
    xp = getContext()
    if (xp == None):
        return 1

    appName = getApplianceName(xp)
    hostname = getPropValue(xp, "vami.hostname")
    log.message(vami_log.LOG_INFO, "Using the following properties to configure the network settings:", False)
    log.message(vami_log.LOG_INFO, _("Appliance Name - ") + appName, True)

    # Grab the list of devices
    try:
        devices_output = subprocess.Popen(INTERFACES, stdout=subprocess.PIPE)
        devices = []
        for device in devices_output.stdout:
            device = device.strip()
            devices.append(device)
            answer = subprocess.Popen([ENSURE_CONFIG, device], stdout=subprocess.PIPE).communicate()[0].strip()
            log.message(vami_log.LOG_INFO, answer, True)
    except:
        log.message(vami_log.LOG_ERROR, "Unable to determine the presence of networking configurations")
        devices = []

    anyInterfacesManagedExternally = False

    for index in range(len(devices)):
        interface = devices[index]

        if (not o.isNetworkManagedExternally(interface)):
            log.message(vami_log.LOG_INFO, _("The network for interface ") + interface + _(
                " is managed internally, networking properties ignored"), True)
            continue

        anyInterfacesManagedExternally = True

        if (hasPropKey(xp, "vami.ip" + str(index) + "." + appName) or
                (hasPropKey(xp, "vCloud_ip_" + str(index)) and ospToolsPresent())):

            device = interface.strip()

            ip = getPropValue(xp, "vami.ip" + str(index) + "." + appName)

            netmask = getPropValue(xp, "vami.netmask" + str(index) + "." + appName)

            gateway = getPropValue(xp, "vami.gateway" + str(index) + "." + appName)

            # The vCloud_bootproto is used to find out if we are under vCloud.

            if hasPropKey(xp, "vCloud_bootproto_" + str(index)):
                ip = getPropValue(xp, "vCloud_ip_" + str(index))
                netmask = getPropValue(xp, "vCloud_netmask_" + str(index))
                gateway = getPropValue(xp, "vCloud_gateway_" + str(index))

            # if the gateway<n> property isn't here, then try the
            # sole "gateway" property instead
            if not gateway:
                gateway = getPropValue(xp, "vami.gateway." + appName)
                if not gateway:
                    gateway = DEFAULT_GATEWAY

            # IPv6 Gateways are not always needed
            # VC permits an empty IPv6 Gateway
            if ip.find(':') > -1:
                if (len(gateway) == 0):
                    gateway = DEFAULT_GATEWAY

            log.message(vami_log.LOG_INFO, _("Device    : ") + device, True)
            if (len(ip) > 0) and (len(netmask) > 0) and (len(gateway) > 0):
                log.message(vami_log.LOG_INFO, _("  Ip      : ") + ip, True)
                log.message(vami_log.LOG_INFO, _("  Netmask : ") + netmask, True)
                log.message(vami_log.LOG_INFO, _("  Gateway : ") + gateway, True)

                setIp(device, ip, netmask, gateway)
                o.setNetworkManagedExternally(device)
            else:
                if (o.isNetworkManagedExternally(device)):
                    unsetIp(device)
                    o.setNetworkManagedInternally(device)
                    log.message(vami_log.LOG_INFO, _("Previous external properties removed"), True)
                else:
                    log.message(vami_log.LOG_INFO, _("Properties managed internally."), True)

            #
            # set the DNS after programming the network, because if we were
            # using dhcp, it could overwrite the resolv.conf before we had
            # a chance to change the IP address.
            #
            allDns = []
            dnsDict = {}

            dns = getPropValue(xp, "vami.DNS" + str(index) + "." + appName)

            if (len(dns) == 0):
                second_index = 1
                while (hasPropKey(xp, "vCloud_dns" + str(index) + "_" + str(second_index))):
                    dns = getPropValue(xp, "vCloud_dns" + str(index) + "_" + str(second_index))
                    if not dnsDict.has_key(dns):
                        dnsDict[dns] = "true"
                        allDns.append(dns)
                    second_index = second_index + 1
            else:
                allDns.append(dns)

            # No need to remove existing nameservers if there are none to add
            if len(allDns) > 0:
                log.message(vami_log.LOG_INFO, _("DNS     : ") + ','.join(allDns), True)
                setDns(','.join(allDns), domain, searchpath)

    if (anyInterfacesManagedExternally):
        # If there is one solitary DNS attribute, override the above individual ones
        dns = getPropValue(xp, "vami.DNS." + appName)

        searchpath = getPropValue(xp, "vami.searchpath." + appName)
        domain = getPropValue(xp, "vami.domain." + appName)

        if len(dns):
            log.message(vami_log.LOG_INFO, _("DNS     : ") + dns, True)
            setDns(dns, domain, searchpath)

        if len(hostname) > 0:
            log.message(vami_log.LOG_INFO, _("Hostname: ") + hostname, True)

        # The script setHostName is calling is satisfied if there is
        # a hostname string or not.
        setHostName(hostname)

    xp.xpathFreeContext()
    return 0


def ignoreEula():
    doc = getOvfDoc()
    # if the doc is present then the ovf properties were present
    # and we could assume that the EULA was accepted by the end user
    if (doc == None):
        return 1
    o = OvfSettings()
    o.setEulaFlag()
    log.message(vami_log.LOG_INFO, "EULA was already accepted, the prompt for EULA can now be bypassed.")
    return 0


def setNetworkManagedInternally(device):
    o = OvfSettings()
    o.setNetworkManagedInternally(device)
    log.message(vami_log.LOG_INFO, device + " is now managed internally.")
    return 0


def setNetworkManagedExternally(device):
    o = OvfSettings()
    o.setNetworkManagedExternally(device)
    log.message(vami_log.LOG_INFO, device + " is now managed externally.")
    return 0


def isNetworkManagedExternally(device):
    o = OvfSettings()
    return o.isNetworkManagedExternally(device)


def getNetworkManagedDescription(device):
    if isNetworkManagedExternally(device):
        log.message(vami_log.LOG_INFO, _("Managed Externally"), True)
    else:
        log.message(vami_log.LOG_INFO, _("Managed Internally"), True)
    return 0


def printOvfEnv():
    buf = getOvfBuf()
    if (buf == None):
        return 1
    print (buf)
    return 0


#
# Determine if OSP tools are present. If they are,
# it is assumed that these OSP tools cannot program
# the network from any existing vcloud properties, so we
# have to program the network from any existing vcloud
# network properties ourselves.
#
# Implementation note: a named initialized list argument
# is persistent across calls; basically, a static variable.
#
def ospToolsPresent(result=[None]):
    if result[0] is None:
        result[0] = False
        #
        # OSP tools have a configuration file that contains
        # a key that states that the version number should
        # be disabled; this indicates to the platform that
        # the tools are "unmanaged", i.e. not the version of
        # tools that are provided by the platform.
        #
        # Since the vCloud network properties mechanism ("Guest
        # Customization") depends on a version of tools, disabling
        # the version effectlively disables Guest Customization
        # mechanism. So, any vcloud properties we find must be used
        # by us to program the network, as the Guest Customization
        # mechansim won't.
        #
        try:
            fd = open(OSP_TOOLS_CONFIG_FILE, 'r')
            for line in fd.readlines():
                if not line.startswith(OSP_TOOLS_DISABLE_VERSION):
                    continue
                key, equals, bool = line.partition('=')
                if bool.strip() == 'true':
                    result[0] = True
            fd.close()
        except:
            pass  # result[0] is already set to False

    return result[0]


def usage():
    print (_("Usage:"), sys.argv[0], "[-h|--help]", "[-u|--reorderudevs]", "[-n|--setnetwork]", "[-e|--ignoreeula]", \
        "[-r|--resetnetworkinternallymanaged device]", "[-g|--getnetworkmanageddesc device]", "[-p|--printovfenv]", "[-t|--settimezone]", "[-s|--setnetworkexternallymanaged device]")


def main():
    global log
    log = vami_log.logfile(OVF_LOG)

    try:
        opts, args = getopt.getopt(sys.argv[1:],
                                   "huner:g:pts:", ["help", "reorderudevs", "setnetwork",
                                                    "ignoreeula", "resetnetworkinternallymanaged=",
                                                    "getnetworkmanageddesc=", "printovfenv", "settimezone",
                                                    "setnetworkexternallymanaged="])
    except getopt.GetoptError as msg:
        log.message(vami_log.LOG_ERROR, _("Invalid option"), True)
        usage()
        sys.exit(1)

    for o, a in opts:
        if o in ("-h", "--help"):
            usage()
            sys.exit(0)
        if o in ("-u", "--reorderudevs"):
            ret = reorderUdevRules()
            sys.exit(ret)
        if o in ("-n", "--setnetwork"):
            ret = setNetwork()
            sys.exit(ret)
        if o in ("-e", "--ignoreeula"):
            ret = ignoreEula()
            sys.exit(ret)
        if o in ("-r", "--resetnetworkinternallymanaged"):
            ret = setNetworkManagedInternally(a)
            sys.exit(ret)
        if o in ("-s", "--setnetworkexternallymanaged"):
            ret = setNetworkManagedExternally(a)
            sys.exit(ret)
        if o in ("-g", "--getnetworkmanageddesc"):
            ret = getNetworkManagedDescription(a)
            sys.exit(ret)
        if o in ("-p", "--printovfenv"):
            ret = printOvfEnv()
            sys.exit(ret)
        if o in ("-t", "--settimezone"):
            ret = setTimezone()
            sys.exit(ret)

    print (_("Invalid option"))
    usage()
    sys.exit(1)


if __name__ == "__main__":
    main()

