#!/usr/bin/python
# vi: ts=4 expandtab syntax=python
##############################################################################
# Copyright (c) 2008 IBM Corporation
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the Eclipse Public License v1.0
# which accompanies this distribution, and is available at
# http://www.eclipse.org/legal/epl-v10.html
#
# Contributors:
# David Leskovec (IBM) - initial implementation
# Eric Casler    (IBM)
##############################################################################
"""
This file implements the ova command.  The functions provided so far are:
manifest - create a manifest file
pack - package an appliance into an ova file
unpack - un-packages an ova file into a set of files comprising the appliance
validate - validate the package, currently only checks the the file digests
environment - extract the appliance parameters from product sections and
              generate the ovf-env.xml
xport - prepare the environment file for the given transport method
runtime - deploy ovf using libvirt
"""

import glob
import os
import sys
import libvirt

from ovf.commands import cli
from ovf.commands import VERSION_STR
from ovf import Ovf
from ovf.env import EnvironmentSection
from ovf.OvfFile import OvfFile
from ovf import OvfPlatform
from ovf import OvfProperty
from ovf.OvfReferencedFile import OvfReferencedFile
from ovf.OvfSet import *
from ovf.OvfManifest import writeManifestFromReferencedFilesList
from ovf import OvfTransport
from ovf.env import PlatformSection

def makeManifest(options, args):
    """
    Handle ova command to create a manifest file
    @type options : options object returned by parse_args
    @param options: ovfFile is required
    @type args    : list of positional arguments returned by parse_args
    @param args   : manifest file name is first (and only) argument if
                    specified.  if not, file name is formed from ovf file name
    """
    manifestFile = options.manifestFile
    if manifestFile == None:
        manifestFile = os.path.splitext(options.ovfFile)[0] + '.mf'

    ovfFileObj = OvfFile(options.ovfFile)

    fileList = ovfFileObj.files
    # writeManifest expects the ovf as the first file.  insert it there
    # create a referenced file object for the ovf
    ovfRefFile = OvfReferencedFile(ovfFileObj.path,
                                   os.path.basename(ovfFileObj.path))
    fileList.insert(0, ovfRefFile)
    writeManifestFromReferencedFilesList(manifestFile, fileList)

def validateAppliance(options, args):
    """
    Validate an appliance pacakge
    @type options : object returned by parseArgs
    @param options: ovfFile is required
    @type args    : List of Strings
    @param args   : positional arguments returned by parseArgs

    @rtype: Boolean
    @return: True - all tests passed, False - one or more tests failed
    """
    # Call method to verify the manifest sums
    ovfSet = OvfSet(options.ovfFile)

    if ovfSet.manifest == None and options.manifestFile == None:
        print 'No manifest file for package, skipping sum verification'
        return

    result = ovfSet.verifyManifest(options.manifestFile)

    if result == False:
        print "checkFileDigests detected a mismatch"

    # TODO: validate the certificate

    print 'All tests passed'

def packOva(options, args):
    """
    Package an appliance into an archive file using tar.
    @type options : options object returned by parse_args
    @param options: ovfFile is required
    @type args    : list of positional arguments returned by parse_args
    @param args   : manifest file name is first (and only) argument if
                    specified.  if not, file name is formed from ovf file name
    """
    outFile = options.output
    if outFile == None:
        # Base the output file name on the ovf file name
        outFile = os.path.splitext(options.ovfFile)[0] + '.ova'

    ovfSet = OvfSet(outFile, "w", FORMAT_TAR)

    if options.noManifest == False:
        manifestFile = options.manifestFile
        if manifestFile == None:
            # Base the manifest file name on the ovf file name
            manifestFile = os.path.splitext(options.ovfFile)[0] + '.mf'
        if os.path.isfile(manifestFile) == False:
            raise ValueError('Specified manifest file does not exist')
        ovfSet.manifest = manifestFile
    else:
        ovfSet.manifest = None

    # add the certificate file if needed
    if options.noCertificate == False:
        certificateFile = options.certificateFile
        if certificateFile == None:
            # Base the manifest file name on the ovf file name
            certificateFile = os.path.splitext(options.ovfFile)[0] + '.cert'
        if os.path.isfile(certificateFile) == False:
            raise ValueError('Specified certificate file does not exist')
        ovfSet.certificate = certificateFile
    else:
        ovfSet.certificate = None

    ovfSet.writeAsTar(outFile)

def unpackOva(options, args):
    """
    Unpackage an appliance from an archive file
    @type options : object returned by parse_args
    @param options: appliance archive file is required
    @type args    : list of positional arguments returned by parse_args
    @param args   : target directory path to extract the appliance archive file
    """
    if options.ovfFile != None and os.path.isfile(options.ovfFile):
        ovaSet = OvfSet(options.ovfFile, "r", FORMAT_TAR)
        if not os.path.isdir(options.targetDir):
            os.mkdir(options.targetDir)
        ovaSet.writeAsDir(options.targetDir)
    else:
        raise IOError("Specified appliance archive " + options.ovfFile + \
                      " does not exist")

def promptToSelectNode(nodes):
    """
    Prompt the user to select a node from a list.
    @type node: List of DOM nodes
    @param node: List of nodes to select from

    @rtype: DOM node
    @return: node that user selected
    """
    if nodes == []:
        return None
    elif len(nodes) == 1:
        return nodes[0]

    print 'Please select a node:'
    nodeNumber = 1
    for node in nodes:
        ovfId = node.getAttribute('ovf:id')
        infoNodes = Ovf.getChildNodes(node, (Ovf.hasTagName, 'Info'))
        infoString = ''
        if infoNodes:
            if len(infoNodes[0].childNodes):
                infoString =  infoNodes[0].childNodes[0].data
        nodeNumberString = str(nodeNumber).rjust(3, ' ')
        nodeString = nodeNumberString + ' ' + node.tagName
        if ovfId:
            nodeString = nodeString + ':' + ovfId
        if infoString:
            nodeString = nodeString + ' (' + infoString + ')'
        print nodeString
        nodeNumber = nodeNumber + 1

    nodeSelected = None
    while nodeSelected == None:
        sys.stdout.write("Enter number for node --> ")
        response = int(raw_input())

        if response > 0 and response < len(nodes) + 1:
            nodeSelected = response

    return nodes[nodeSelected - 1]

def promptForValue(node, properties, defaultValue):
    """
    Prompt the user for a given property value.
    @type node: DOM node
    @param node: Property node
    @type properties: dict
    @param properties: Property attribute dictionary
    @type defaultValue: String
    @param defaultValue: Default value for property

    @rtype: String
    @return: Value for property.  User input or default
    """
    if defaultValue == None:
        # user didn't specify a default, make it an empty string
        defaultValue = ''

    # Get the Description for the property
    descriptions = Ovf.getNodes(node, (Ovf.hasTagName, 'Description'))
    print "Enter value for property " + properties['ovf:key'] + '('\
          + properties['ovf:type'] + ')[' + defaultValue + ']'
    for description in descriptions:
        for child in description.childNodes:
            print child.data

    response = ''
    try:
        while response == '':
            sys.stdout.write("--> ")

            response = raw_input()

            if response == "":
                # user wants the default
                response = defaultValue

    except EOFError:
        # use CTRL-D to specify an empty value
        response = ''

    return response

globalPropertyDict = {}

def getAndPromptPropertiesForNode(node, options):
    """
    Get the properties via getPropertiesForNode and prompt for anything as
    needed.
    @type node: DOM node
    @param node: Virtual System or Virtual System Collection node
    @type options: object returned by parse_args
    @param options: queries noPrompt and noValue

    @rtype: dict
    @return: dictionary of properties {key, value}
    """
    # call function to get environment information for this node
    nodeEnvironment = OvfProperty.getPropertiesForNode(node,
                                                       options.configuration)

    # go through the properties and prompt for anything as needed
    propertyDict = {}
    for (propName, propNode, propValue) in nodeEnvironment:
        if not options.noPrompt:
            attributes = Ovf.getAttributes(propNode)
            if propValue == None or\
               (attributes.has_key('ovf:userConfigurable') and\
                attributes['ovf:userConfigurable'] == 'true'):
                propValue = promptForValue(propNode, attributes, propValue)
        else:
            if propValue == None:
                if options.noValue:
                    propValue = ''
                else:
                    # this is an error, or should it force a prompt?
                    raise RuntimeError, \
                          '--no-prompt specified but value has no default'

        if propValue.startswith('${'):
            subKey = propValue.strip('}{$ \n')
            if globalPropertyDict.has_key(subKey):
                propValue = globalPropertyDict[subKey]

        propertyDict[propName] = propValue
        attributes = Ovf.getAttributes(propNode)
        globalPropertyDict[attributes['ovf:key']] = propValue

    return propertyDict

def createEnvSection(node, propertyDict, options, parentEnv=None):
    """
    Create an EnvironmentSection object for the node, property dictionary
    and optionally parent environment
    @type node: DOM node
    @param node: Virtual System node
    @type node: dict
    @param node: Dictionary of properties for node
    @type options: object returned by parse_args
    @param options: Uses virtPlatform
    @type node: dict
    @param node: Optional dictionary of properties for parent node

    @rtype: EnvironmentSection
    @return: object containing environment for Virtual System
    """
    envSection = EnvironmentSection.EnvironmentSection()
    nodeId = node.getAttribute('ovf:id')
    envSection.createHeader(nodeId)
    platformDict = OvfPlatform.getPlatformDict(node, options.virtPlatform)
    platformSec = PlatformSection.PlatformSection(platformDict)
    envSection.createSection(None, 'PlatformSection', platformSec)
    localPropertyDict = {}
    if parentEnv:
        localPropertyDict.update(parentEnv)
    localPropertyDict.update(propertyDict)
    envSection.createSection(None, 'PropertySection', localPropertyDict)

    return envSection

def createEnvSectionsWithSibs(options, vsList, siblingList, parentEnv):
    """
    Create an EnvironmentSection object for each virtual system passed in
    vsList.  The entity section of each object will be populated with the
    invormation passed in siblingList.  Each object will also get the
    parentEnv.
    @type options: object returned by parse_args
    @param options: passed to functions
    @type vsList: List of (dict, DOM node) tuples
    @param vsList: List of tuples containing a property dictionary
                   and virtual system node
    @type siblingList: List of (dict, DOM node) tuples
    @param siblingList: List of siblings for nodes in vsList.  This list may
                        have elements common with vsList
    @type node: dict
    @param node: Optional dictionary of properties for parent node

    @rtype: List of EnvironmentSection objects
    @return: List of filled in Environment sections, one for each
             node in vsList
    """
    returnList = []

    for (propertyDict, vsNode) in vsList:
        targetEnvObj = createEnvSection(vsNode, propertyDict, options,
                                        parentEnv)
        for (entityDict, entityNode) in siblingList:
            if vsNode != entityNode:
                subjectId = entityNode.getAttribute('ovf:id')
                targetEnvObj.createSection(subjectId, 'Entity')
                setDict = {}
                if parentEnv:
                    setDict = setDict.update(parentEnv)
                setDict.update(entityDict)
                targetEnvObj.createSection(subjectId, 'PropertySection',
                                           setDict)

        returnList.append(targetEnvObj)

    return returnList

def getEnvSectionForVs(node, options):
    """
    Generate the EnvironmentSection for a single virtual system.
    @type node: DOM node
    @param node: Virtual System node
    @type options: object returned by parse_args
    @param options: passed to functions

    @rtype: EnvironmentSection
    @return: object containing environment for Virtual System
    """
    vsList = []
    siblingList = []
    returnList = []

    parentEnv = None
    parentNode = node.parentNode
    if parentNode.tagName == 'VirtualSystemCollection':
        parentEnv = getAndPromptPropertiesForNode(parentNode, options)
        for child in parentNode.childNodes:
            if Ovf.hasTagName(child, 'VirtualSystem'):
                childDict = getAndPromptPropertiesForNode(child, options)
                siblingList.append((childDict, child))
            elif Ovf.hasTagName(child, 'VirtualSystemCollection'):
                childDict = getAndPromptPropertiesForNode(child, options)
                siblingList.append((childDict, child))

    propertyDict = getAndPromptPropertiesForNode(node, options)
    vsList.append((propertyDict, node))

    returnList = createEnvSectionsWithSibs(options, vsList, siblingList,
                                           parentEnv)
    return returnList[0]

def getEnvSectionsForVsc(node, options):
    """
    Take a vsc and gather the environment for each of it's virtual systems
    @type node: DOM node
    @param node: Virtual System node
    @type options: object returned by parse_args
    @param options: passed to functions

    @rtype: List of EnvironmentSection objects
    @return: objects containing environment for Virtual Systems
    """
    vsList = []
    siblingList = []
    returnList = []

    # Get the environment for this node
    parentEnv = getAndPromptPropertiesForNode(node, options)

    for child in node.childNodes:
        if Ovf.hasTagName(child, 'VirtualSystem'):
            childDict = getAndPromptPropertiesForNode(child, options)
            vsList.append((childDict, child))
            siblingList.append((childDict, child))
        elif Ovf.hasTagName(child, 'VirtualSystemCollection'):
            childDict = getAndPromptPropertiesForNode(child, options)
            siblingList.append((childDict, child))

    returnList = createEnvSectionsWithSibs(options, vsList, siblingList,
                                           parentEnv)

    return returnList

def getEnvSectionsForId(options):
    """
    Generate the environment file(s) for the entity with a given ovf id
    @type options: object returned by parse_args
    @param options: ffile is required and ovfId is used

    @raise ValueError: ovf file not found, content with ovf id not found
    @raise RuntimeError: ovf file contains no entities with ovf id

    @rtype: List of EnvironmentSection objects
    @return: objects containing environment for Virtual Systems
    """
    try:
        ovfFile = OvfFile(options.ovfFile)
    except:
        raise ValueError, "Ovf file not found"

    envList = []

    # Get the virtual system collection associated with this id
    nodes = Ovf.getContentEntities(ovfFile.envelope, options.ovfId,
                                   True, True)
    if nodes == []:
        # no, error out
        raise ValueError, "Element with ovf id " + options.ovfId + " not found"
    elif len(nodes) > 1:
        node = promptToSelectNode(nodes)
    else:
        node = nodes[0]

    if Ovf.hasTagName(node, 'VirtualSystem'):
        # handle environment for a single vs
        vsEnv = getEnvSectionForVs(nodes[0], options)
        envList.append(vsEnv)
    else:
        # handle environment for a vsc (and it's children but not grand)
        envList.extend(getEnvSectionsForVsc(node, options))

    return envList

def getAllEnvSections(options):
    """
    Generate the environment file(s) for all virtual systems in an ovf file
    @type options: object returned by parse_args
    @param options: ffile is required

    @raise ValueError: ovf file not found, content with ovf id not found
    @raise RuntimeError: ovf file contains no virtual systems
    @raise RuntimeError: ovf file contains multiple virtual systems but
                         no virtual system collection

    @rtype: List of EnvironmentSection objects
    @return: objects containing environment for Virtual Systems
    """
    try:
        ovfFile = OvfFile(options.ovfFile)
    except:
        raise ValueError, "Ovf file not found"

    envList = []

    # User wants all vs and vsc handled
    # Get any virtual system collections in the ovf
    nodes = Ovf.getNodes(ovfFile.envelope, (Ovf.hasTagName,
                         'VirtualSystemCollection'))
    if nodes != []:
        for vsc in nodes:
            envList.extend(getEnvSectionsForVsc(vsc, options))

    else:
        # no collections, get the single virtual system
        vsList = Ovf.getChildNodes(ovfFile.envelope, (Ovf.hasTagName,
                                                      'VirtualSystem'))
        if vsList == []:
            raise RuntimeError, "ovf contains no virtual systems"
        elif len(vsList) > 1:
            raise RuntimeError, "invalid, ovf multiple virtual systems " +\
                                "without a collection"
        else:
            vsEnv = getEnvSectionForVs(vsList[0], options)
            envList.append(vsEnv)

    return envList

def guestEnvironment(options, args):
    """
    Generate the environment file(s) as requested by the user
    @type options: object returned by parse_args
    @param options: passed through
    @type args: List of Strings
    @param args: positional arguments returned by parse_args
    """
    envList = []

    if options.ovfId == None:
        envList.extend(getAllEnvSections(options))

    else:
        envList.extend(getEnvSectionsForId(options))

    if options.xmlFile:
        targetFileName = options.xmlFile
        for env in envList:
            env.generateXML(targetFileName, "a")
    else:
        for env in envList:
            # write the environment information out to file
            # If there's only one vs, the file name is ovf-env.xml
            if len(envList) == 1:
                targetFileName = 'ovf-env.xml'
            else:
                # The file name will be the vs ovf:id plus ovf-env.xml
                targetFileName = env.environment.getAttribute('ovfenv:id')
                targetFileName = targetFileName + '_ovf-env.xml'

            env.generateXML(targetFileName, "w")

def envTransport(options, args):
    """
    Format the environment file(s) for the given transport mechanism.  This
    currently supports the following formats:
    iso - creates an iso file containing the environment file
    @type options: object returned by parse_args
    @param options: ovfFile - input environment files
                    outFile - optional output file name
    @type args: List of Strings
    @param args: positional arguments returned by parse_args
    """
    # Put together a list of files that we want to package.  This allows
    # specifying multiple files such as *.xml
    fileList = glob.glob(options.ovfFile)

    # Make a list of tuples like this [(output file, [input files])]
    # This command doesn't provide the options yet to include multiple files
    # on an ISO however the library is setup to allow this.  The requirement
    # is that the first file in the list is the environment file as defined
    # in the OVF spec.
    processList = []
    if options.outFile:
        processList = [(options.outFile, fileList)]
    else:
        for envFile in fileList:
            processList.append((None, [envFile]))

    OvfTransport.makeISOTransport(processList)

def run(options, args):
    """Deploy a vm"""

    if os.path.isfile(options.ovfFile):
        # Instantiate OvfSet instance for OVF file
        ovf = OvfSet(options.ovfFile)

        installLoc = options.installLoc
        if options.ovfFile.endswith(".ova") and installLoc == None:
            installLoc = os.path.dirname(options.ovfFile)
        if installLoc != None and os.path.isdir(installLoc):
            installLoc = os.path.abspath(installLoc)
            ovf.writeAsDir(installLoc)

        # Boot Virtual Machines
        ovf.boot(options.virtPlatform, options.ofile, None, installLoc,
                 options.envDir)
        sys.exit(0)

    else:
        raise IOError(MISSING_FILE)

def main():
    """
    main routine for this program
    """
    cmdUsage = "ova command -f <file> [options]"
    cliParser = cli.CLI(COMMANDS, COMMON_OPTS, cmdUsage, VERSION_STR)
    command, options, args = cliParser.parseArgs()

    try:
        COMMANDS[command]['function'](options, args)

    except ValueError, inst:
        print 'Parameter error: ' + inst.__str__() + "\n"
        #subParser.parse_args(['--help'])
    except Exception:
        excTuple = sys.exc_info()
        print 'Command failed: ',  excTuple[0].__name__, '-', excTuple[1]

MISSING_FILE = "Ovf file not found."

COMMANDS = {

    "manifest" :
    {
        'function' : makeManifest,
        'help' : "Create a manifest file with SHA-1 sum for each "
                 "referenced file",
        'args' : (
        {
            'flags' : ['-o', '--ofile'],
            'parms' : {'dest' : 'manifestFile',
                       'help' : "Output manifest file"}
        },
        )
    },

    "validate" :
    {
        'function' : validateAppliance,
        'help' : "Validate the virtual appliance pakage",
        'args' : (
        {
            'flags' : ['-m', '--manifest'],
            'parms' : {'dest' : 'manifestFile', 'help' : "Manifest file"}
        },
        {
            'flags' : ['-c', '--cert'],
            'parms' : {'dest' : 'certFile', 'help' : "Certificate file"}
        }
        )
    },

    "pack" :
    {
        'function' : packOva,
        'help' : "Packs a set of files comprising a virtual appliance into "
                 "a single file in the tar format",
        'args' : (
        {
            'flags' : ['-o', '--output'],
            'parms' : {'dest' : 'output',
                       'help' : "Output package file (- stdout)"}
        },
        {
            'flags' : ['-m', '--manifestfile'],
            'parms' : {'dest' : 'manifestFile',
                       'help' : "Manifest file to include in archive"}
        },
        {
            'flags' : ['-c', '--certificatefile'],
            'parms' : {'dest' : 'certificateFile',
                       'help' : "Certificate file to include in archive"}
        },
        {
            'flags' : ['-n', '--no-manifest'],
            'parms' : {'dest' : 'noManifest', 'action' : "store_true",
                       'default' : False,
                       'help' : "Do not store manifest file in the archive."}
        },
        {
            'flags' : ['-r', '--no-certificate'],
            'parms' : {'dest' : 'noCertificate', 'action' : "store_true",
                       'default' : False,
                       'help' :
                           "Do not store a certificate file in the archive."}
        }
        )
    },

    "unpack" :
    {
        'function' : unpackOva,
        'help' : "Unpack an ova package",
        'args' : (
        {
            'flags' : ['-d', '--directory'],
            'parms' : {'dest' : 'targetDir', 'default' : '.',
                       'help' : "Target directory where files will be stored"}
        },
        )
    },

    "runtime" :
    {
        "function" : run,
        "help" : "Deploy the virtual systems of an OVF file as "
                 "libvirt domains",
        "args" : (
        {
            'flags' : ['-v', '--virt'],
            'parms': {'dest' : 'virtPlatform',
                      'help' : "Virtualization Platform"}
        },
        {
            'flags' : ['-o', '--ofile'],
            'parms': {'dest' : 'ofile',
                      'help' : "Output file for libvirt XML"}
        },
        {
            "flags" : ["-e", "--environment"],
            "parms" : {"dest"    : "envDir", 'default' : None,
                       "help"    : "Path to environment files"}
        },
        {
            "flags" : ["-i", "--install-to"],
            "parms" : {"dest"    : "installLoc", 'default' : None,
                       "help"    : "Directory location to install ova package contents"}
        },
        )
    },


    "environment" :
    {
        'function' : guestEnvironment,
        'help' : "Generate the guest environment",
        'args' : (
        {
            'flags' : ['-n', '--no-prompt'],
            'parms' : {'dest' : 'noPrompt', 'action' : "store_true",
                       'default' : False,
                       'help' : "Don't prompt for values, take all defaults"}
        },
        {
            'flags' : ['-z', '--no-value'],
            'parms' : {'dest' : 'noValue', 'action' : "store_true",
                       'default' : False,
                       'help' : "With no-prompt, values without defaults "
                       "are listed but left unset"}
        },
        {
            'flags' : ['-i', '--id'],
            'parms' : {'dest' : 'ovfId', 'help' : "Virtual System id"}
        },
        {
            'flags' : ['-o', '--outfile'],
            'parms': {'dest' : 'xmlFile', 'default' : None,
                      'help' : "xml output file"}
        },
        {
            'flags' : ['-c', '--configuration'],
            'parms': {'dest' : 'configuration',
                      'help' : "Configuration to use"}
        },
        {
            'flags' : ['-v', '--virt'],
            'parms': {'dest' : 'virtPlatform',
                      'help' : "Virtualization Platform"}
        },
        {
            'flags' : ['-d', '--directory'],
            'parms' : {'dest' : 'pkgDirectory',
                       'help' : "Directory containing the package"}
        }
        ),
    },

    "xport" :
    {
        'function' : envTransport,
        'help' : "Generate the guest environment",
        'args' : (
        {
            'flags' : ['-t', '--format'],
            'parms' : {'dest' : 'format', 'default' : 'iso',
                       'help' : "Transport format.  Valid values: 'iso'. "
                                "Default is 'iso'."}
        },
        {
            'flags' : ['-o', '--outfile'],
            'parms': {'dest' : 'outFile', 'default' : None,
                      'help' : "Output file.  All environment files will be "
                               "put on a single iso with this name."}
        }
        ),

    }
}

COMMON_OPTS = (
    {
        "flags" : [ "-f", "--file" ],
        "parms" : { "dest"    : "ovfFile",
                    "help"    : "path to OVF file.",
                    "metavar" : "PATH"},
        "required": True
    },
)

if __name__ == "__main__":
    main()

