# Sample code to parse an image XML description and
# spit out libvirt XML
#
# Copyright 2007, 2013, 2014 Red Hat, Inc.
# David Lutterkort <dlutter@redhat.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301 USA.

import logging
import os

import urlgrabber

from virtinst import CapabilitiesParser
from virtinst import Installer
from virtinst import VirtualDisk
from virtinst import util


class Image(object):
    """The toplevel object representing a VM image"""
    def __init__(self, node=None, base=None, filename=None):
        self.storage = {}
        self.domain = None
        if filename is None:
            self.filename = None
        else:
            self.filename = os.path.abspath(filename)
        if base is None:
            if filename is not None:
                self.base = os.path.dirname(filename)
                if self.base == '':
                    self.base = "."
            else:
                self.base = "."
        else:
            self.base = base
        self.name = None
        self.label = None
        self.descr = None
        self.version = None
        self.release = None
        if node is not None:
            self.parseXML(node)

    def abspath(self, p):
        """Turn P into an absolute path. Relative paths are taken relative
           to self.BASE"""
        return os.path.abspath(os.path.join(self.base, p))

    def parseXML(self, node):
        self.name = xpathString(node, "name")
        self.label = xpathString(node, "label")
        self.descr = xpathString(node, "description")
        self.version = xpathString(node, "name/@version")
        self.release = xpathString(node, "name/@release")
        for d in node.xpathEval("storage/disk"):
            disk = Disk(d)
            if disk.file is None:
                disk.id = "disk%d.img" % len(self.storage)
                disk.file = "disk%d.img" % (len(self.storage) + 1)
            if disk.id in self.storage:
                raise RuntimeError("Disk file '%s' defined twice" % disk.file)
            self.storage[disk.id] = disk
        lm = node.xpathEval("domain")
        if len(lm) == 1:
            self.domain = Domain(lm[0])
        else:
            raise RuntimeError(_("Expected exactly one 'domain' element"))
        # Connect the disk maps to the disk definitions
        for boot in self.domain.boots:
            for d in boot.drives:
                if d.disk_id not in self.storage:
                    raise RuntimeError(_("Disk entry for '%s' not found")
                                       % d.disk_id)
                d.disk = self.storage[d.disk_id]


class Domain(object):
    """The description of a virtual domain as part of an image"""
    def __init__(self, node=None):
        self.boots = []
        self.vcpu = None
        self.memory = None
        self.interface = 0
        self.graphics = None
        if node is not None:
            self.parseXML(node)

    def parseXML(self, node):
        self.boots = [Boot(b) for b in node.xpathEval("boot")]
        self.vcpu = xpathString(node, "devices/vcpu", 1)
        tmpmem = xpathString(node, "devices/memory")
        self.interface = int(node.xpathEval("count(devices/interface)"))
        self.graphics = node.xpathEval("count(devices/graphics)") > 0

        if tmpmem is not None:
            try:
                self.memory = int(tmpmem)
            except ValueError:
                raise RuntimeError(_("Memory must be an integer, "
                                     "but is '%s'") % self.memory)
        else:
            tmpmem = 0


class ImageFeatures(CapabilitiesParser.Features):
    def __init__(self, node=None):
        CapabilitiesParser.Features.__init__(self, node)

    def _extractFeature(self, feature, d, n):
        state = xpathString(n, "@state", "on")

        if state == "on":
            d[feature] = CapabilitiesParser.FEATURE_ON
        elif state == "off":
            d[feature] = CapabilitiesParser.FEATURE_OFF
        else:
            raise RuntimeError("The state for feature %s must be "
                               "either 'on' or 'off', but is '%s'" %
                               (feature, state))


class Boot(object):
    """The overall description of how the image can be booted, including
    required capabilities of the host and mapping of disks into the VM"""
    def __init__(self, node=None):
        # 'xen' or 'hvm'
        self.type = None
        # Either 'pygrub' or nothing; might have others in the future
        # For HVM, figure outhte right loader based on the guest
        self.loader = None
        # Only used for hvm
        self.bootdev = None
        self.kernel = None
        self.initrd = None
        self.cmdline = None
        self.drives = []
        self.arch = None
        self.features = ImageFeatures()
        if node is not None:
            self.parseXML(node)

    def parseXML(self, node):
        self.type = xpathString(node, "@type")
        self.loader = xpathString(node, "os/loader")
        self.bootdev = xpathString(node, "os/loader/@dev")
        self.kernel = xpathString(node, "os/kernel")
        self.initrd = xpathString(node, "os/initrd")
        self.cmdline = xpathString(node, "os/cmdline")
        self.arch = util.sanitize_arch(xpathString(node, "guest/arch"))

        fl = node.xpathEval("guest/features")
        if len(fl) > 1:
            raise RuntimeError("Expected at most one <features> element "
                               "in %s boot descriptor for %s" %
                               (self.type, self.arch))
        elif len(fl) == 1:
            self.features = ImageFeatures(fl[0])

        for d in node.xpathEval("drive"):
            self.drives.append(Drive(d))

        validate(self.type is not None,
           "The boot type must be provided")
        validate(self.type == "hvm" or self.type == "xen",
           "Boot type must be 'xen' or 'hvm', but is %s" % self.type)
        validate(self.arch is not None, "Missing guest arch")
        validate(self.loader is None or self.loader == "pygrub",
                 "Invalid loader %s" % self.loader)
        validate([None, "hd", "cdrom"].count(self.bootdev) > 0,
                 "Invalid bootdev %s" % self.bootdev)
        # We should make sure that kernel/initrd/cmdline are only used for pv
        # and without a loader


class Drive(object):
    """The mapping of a disk from the storage section to a virtual drive
    in a guest"""
    def __init__(self, node=None):
        self.disk_id = None
        self.target = None
        self.disk = None   # Will point to the underlying Disk object
        if node:
            self.parseXML(node)

    def parseXML(self, node):
        self.disk_id = xpathString(node, "@disk")
        self.target = xpathString(node, "@target")


class Disk(object):
    FORMAT_RAW = "raw"
    FORMAT_ISO = "iso"
    FORMAT_QCOW = "qcow"
    FORMAT_QCOW2 = "qcow2"
    FORMAT_VMDK = "vmdk"
    FORMAT_VDI = "vdi"

    USE_SYSTEM = "system"
    USE_USER = "user"
    USE_SCRATCH = "scratch"

    def __init__(self, node=None):
        self.id = None
        self.file = None
        self.format = None
        self.size = None
        self.use = None
        self.csum = {}
        if node is not None:
            self.parseXML(node)

    def parseXML(self, node):
        self.file = xpathString(node, "@file")
        self.id = xpathString(node, "@id", self.file)
        self.format = xpathString(node, "@format", Disk.FORMAT_RAW)
        self.size = xpathString(node, "@size")
        self.use = xpathString(node, "@use", Disk.USE_SYSTEM)

        for d in node.xpathEval("checksum"):
            csumtype = xpathString(d, "@type")
            csumvalue = xpathString(d, "")
            self.csum[csumtype] = csumvalue
        formats = [Disk.FORMAT_RAW,
                   Disk.FORMAT_QCOW,
                   Disk.FORMAT_QCOW2,
                   Disk.FORMAT_VMDK,
                   Disk.FORMAT_ISO,
                   Disk.FORMAT_VDI]
        validate(formats.count(self.format) > 0,
                 _("The format for disk %s must be one of %s") %
                 (self.file, ",".join(formats)))

    def check_disk_signature(self, meter=None):
        try:
            import hashlib
            sha = None
        except:
            import sha
            hashlib = None

        if meter is None:
            meter = urlgrabber.progress.BaseMeter()

        m = None
        if hashlib:
            if "sha256" in self.csum:
                csumvalue = self.csum["sha256"]
                m = hashlib.sha256()

            elif "sha1" in self.csum:
                csumvalue = self.csum["sha1"]
                m = hashlib.sha1()
        else:
            if "sha1" in self.csum:
                csumvalue = self.csum["sha1"]
                m = sha.new()

        if not m:
            return

        meter_ct = 0
        disk_size = os.path.getsize(self.file)
        meter.start(size=disk_size,
                    text=_("Checking disk signature for %s" % self.file))

        f = file(self.file)
        while 1:
            chunk = f.read(65536)
            if not chunk:
                break
            meter.update(meter_ct)
            meter_ct = meter_ct + 65536
            m.update(chunk)
        checksum = m.hexdigest()
        if checksum != csumvalue:
            logging.debug(_("Disk signature for %s does not match "
                            "Expected: %s  Received: %s" % (self.file,
                             csumvalue, checksum)))
            raise ValueError(_("Disk signature for %s does not "
                               "match" % self.file))


def validate(cond, msg):
    if not cond:
        raise RuntimeError(msg)


def xpathString(node, path, default=None):
    result = node.xpathEval("string(%s)" % path)
    if len(result) == 0:
        result = default
    return result


def parse(xml, filename):
    """Parse the XML description of a VM image into a data structure. Returns
    an object of class Image. BASE should be the directory where the disk
    image files for this image can be found"""
    def cb(x):
        return Image(x, filename=filename)
    return util.parse_node_helper(xml, "image", cb, RuntimeError)


def parse_file(filename):
    f = open(filename, "r")
    xml = f.read()
    f.close()
    return parse(xml, filename=filename)


class ImageInstaller(Installer):
    """
    Installer for virt-image-based guests
    """
    _has_install_phase = False

    def __init__(self, conn, image, boot_index=None):
        Installer.__init__(self, conn)

        self._image = image

        # Set boot _boot_caps/_boot_parameters
        if boot_index is None:
            self._boot_caps = match_boots(self.conn.caps,
                                     self.image.domain.boots)
            if self._boot_caps is None:
                raise RuntimeError(_("Could not find suitable boot "
                                     "descriptor for this host"))
        else:
            if (boot_index < 0 or
                (boot_index + 1) > len(image.domain.boots)):
                raise ValueError(_("boot_index out of range."))
            self._boot_caps = image.domain.boots[boot_index]

        # Set up internal caps.guest object
        self._guest, self._domain = self.conn.caps.guest_lookup(
            os_type=self.boot_caps.type, arch=self.boot_caps.arch)


    # Custom ImageInstaller methods
    def get_caps_guest(self):
        return self._guest, self._domain

    def get_image(self):
        return self._image
    image = property(get_image)

    def get_boot_caps(self):
        return self._boot_caps
    boot_caps = property(get_boot_caps)


    # General Installer methods
    def _prepare(self, guest, meter, scratchdir):
        ignore = scratchdir
        ignore = meter

        self._make_disks()

        for f in ['pae', 'acpi', 'apic']:
            if self.boot_caps.features[f] & CapabilitiesParser.FEATURE_ON:
                setattr(guest.features, f, True)
            elif self.boot_caps.features[f] & CapabilitiesParser.FEATURE_OFF:
                setattr(guest.features, f, False)

        guest.os.kernel = self.boot_caps.kernel
        guest.os.initrd = self.boot_caps.initrd
        guest.os.kernel_args = self.boot_caps.cmdline

    # Private methods
    def _get_bootdev(self, isinstall, guest):
        return self.boot_caps.bootdev

    def _make_disks(self):
        for drive in self.boot_caps.drives:
            path = self.image.abspath(drive.disk.file)
            size = None
            if drive.disk.size is not None:
                size = float(drive.disk.size) / 1024

            # FIXME: This is awkward; the image should be able to express
            # whether the disk is expected to be there or not independently
            # of its classification, especially for user disks
            # FIXME: We ignore the target for the mapping in m.target
            if (drive.disk.use == Disk.USE_SYSTEM and
                not os.path.exists(path)):
                raise RuntimeError(_("System disk %s does not exist") % path)

            device = VirtualDisk.DEVICE_DISK
            if drive.disk.format == Disk.FORMAT_ISO:
                device = VirtualDisk.DEVICE_CDROM

            disk = VirtualDisk(self.conn)
            disk.path = path
            disk.device = device
            disk.target = drive.target

            disk.set_create_storage(size=size, fmt=drive.disk.format)
            disk.validate()
            self.install_devices.append(disk)


def match_boots(capabilities, boots):
    for b in boots:
        for g in capabilities.guests:
            if b.type == g.os_type and b.arch == g.arch:
                found = True
                for bf in b.features.names():
                    if not b.features[bf] & g.features[bf]:
                        found = False
                        break
                if found:
                    return b
    return None
