#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# #
# Copyright 2016-2017 VMware Inc.
# This file is part of ETSI OSM
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact:  osslegalrouting@vmware.com
# #

import logging
import os
import subprocess
import yaml
from lxml import etree as ET

# file paths
MODULE_DIR = os.path.dirname(__file__)
OVF_TEMPLATE_PATH = os.path.join(MODULE_DIR,
                                 "ovf_template/template.xml")
IDE_CDROM_XML_PATH = os.path.join(MODULE_DIR,
                                  "ovf_template/ide_cdrom.xml")
OS_INFO_FILE_PATH = os.path.join(MODULE_DIR,
                                 "config/os_type.yaml")
DISK_CONTROLLER_INFO_FILE_PATH = os.path.join(MODULE_DIR,
                                              "config/disk_controller.yaml")

# Set logger
LOG_FILE = os.path.join(MODULE_DIR, "logs/ovf_converter.log")
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
logger = logging.getLogger(__name__)
hdlr = logging.FileHandler(LOG_FILE)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
hdlr.setFormatter(formatter)
logger.addHandler(hdlr)
logger.setLevel(10)

__version__ = "1.2"
__description__ = "OVF Hardware Version 14 compatible"


def get_version(*args, **kwargs):
    """ get version of this application"""
    version = str(__version__) + " - " + str(__description__)
    return version


# converter class
class OVFConverter(object):
    """ Class to convert input image into OVF format """

    def __init__(self, source_img_path, output_location=None, output_ovf_name=None,
                 memory=None, cpu=None, disk=None, os_type=None,
                 disk_controller=None, cdrom=None, hwversion=14):
        """
            Constructor to initialize object of class OVFConverter
            Args:
            source_img_path - absolute path to source image which will get convert into ovf
            output_location - location where created OVF will be kept. This location
                              should have write access. If not given file will get
                              created at source location  (optional)
            output_ovf_name - name of output ovf.If not given source image name will
                              be used (optional)
            memory -          required memory for VM in MB (optional)
            cpu -             required number of virtual cpus for VM (optional)
            disk -            required size of disk for VM in GB (optional)
            os_type-          required operating system type as specified in user document
                              (default os type other 32 bit) (optional)
            disk_controller - required disk controller type
                              (default controller SCSI with lsilogicsas)
                              (SATA, IDE, Paravirtual, Buslogic, Lsilogic, Lsilogicsas) (optional)
            hwversion -       VMware ESXi hardware family version (optional)

            Returns:
                Nothing.
        """
        self.logger = logger
        self.ovf_template_path = OVF_TEMPLATE_PATH

        self.source_img_path = source_img_path
        self.source_img_filename, file_extension = os.path.splitext(os.path.basename(self.source_img_path))
        self.source_img_location = os.path.dirname(self.source_img_path)
        self.source_format = file_extension[1:]

        self.output_format = "ovf"
        self.output_ovf_name = output_ovf_name.split('.')[0] if output_ovf_name else self.source_img_filename
        self.output_location = output_location if output_location else "."
        self.output_ovf_name_ext = self.output_ovf_name + "." + self.output_format
        self.output_path = os.path.join(self.output_location, self.output_ovf_name_ext)

        self.output_diskimage_format = "vmdk"
        self.output_diskimage_name = self.source_img_filename + "." + self.output_diskimage_format
        self.output_diskimage_path = os.path.join(self.output_location, self.output_diskimage_name)

        self.logger.info("Input parameters to Converter: \n ovf_template_path = {}, \n source_img_path = {}, \n"
                         "source_img_location ={} , \n source_format = {}, \n source_img_filename = {}".format(
                             self.ovf_template_path,
                             self.source_img_path, self.source_img_location,
                             self.source_format, self.source_img_filename))

        self.logger.info("Output parameters to Converter: \n output_format = {}, \n output_ovf_name = {}, \n"
                         "output_location ={} , \n output_path = {}, \n output_diskimage_name = {} , \n"
                         " output_diskimage_path = {} ".format(self.output_format, self.output_ovf_name,
                                                               self.output_location, self.output_path,
                                                               self.output_diskimage_name, self.output_diskimage_path))

        self.disk_capacity = 1
        self.disk_populated_size = 0

        self.vm_name = self.output_ovf_name
        self.memory = str(memory) if memory is not None else None
        self.cpu = str(cpu) if cpu is not None else None
        self.os_type = str(os_type).strip() if os_type else None
        self.cdrom = cdrom
        self.hwversion = hwversion

        if self.os_type:
            self.osID, self.osType = self.__get_osType()
            if self.osID is None or self.osType is None:
                error_msg = "ERROR: Invalid input can not find OS type {} ".format(self.os_type)
                self.__raise_exception(error_msg)

        self.disk_controller = str(disk_controller).strip() if disk_controller else None

        if self.disk_controller:
            self.disk_controller_info = self.__get_diskcontroller()

            if not self.disk_controller_info:
                error_msg = "ERROR: Invalid input can not find Disk Controller {} ".format(self.disk_controller)
                self.__raise_exception(error_msg)

        if disk is not None:
            # convert disk size from GB to bytes
            self.disk_size = int(disk) * 1024 * 1024 * 1024
        else:
            self.disk_size = None

        self.logger.info("Other input parameters to Converter: \n vm_name = {}, \n memory = {}, \n"
                         "disk_size ={} \n os type = {} \n disk controller = {}".format(
                             self.vm_name, self.memory, self.disk_size, self.os_type, self.disk_controller))

        # check access for read input location and write output location return none if no access
        if not os.access(self.source_img_path, os.F_OK):
            error_msg = "ERROR: Source image file {} not present".format(self.source_img_path)
            self.__raise_exception(error_msg, exception_type="IO")

        elif not os.access(self.source_img_path, os.R_OK):
            error_msg = "ERROR: Cannot read source image file {}".format(self.source_img_path)
            self.__raise_exception(error_msg, exception_type="IO")

        if not os.access(self.output_location, os.W_OK):
            error_msg = "ERROR: No write access to location {} to write output OVF ".format(self.output_location)
            self.__raise_exception(error_msg, exception_type="IO")

    def __get_image_info(self):
        """
            Private method to get information about source imager.
            Args  : None
            Return : True on success else False
        """
        try:
            print("Getting source image information")
            command = "qemu-img info \t " + self.source_img_path
            output, error, returncode = self.__execute_command(command)

            if error or returncode:
                self.logger.error("ERROR: Error occurred while getting information about source image : {} \n "
                                  "return code : {} ".format(error, returncode))
                return False

            elif output:
                self.logger.info("Get Image Info Output : {} \n ".format(output))
                split_output = output.decode().split("\n")
                for line in split_output:
                    line = line.strip()
                    if "virtual size" in line:
                        virtual_size_info = line.split(":")[1].split()
                        if len(virtual_size_info) == 3 and virtual_size_info[2].strip(")") == "bytes":
                            self.disk_capacity = int(virtual_size_info[1].strip("("))
                        else:
                            self.disk_capacity = self.__convert_size(virtual_size_info[0])

                    elif "disk size" in line:
                        size = line.split(":")[1].split()[0]
                        self.disk_populated_size = self.__convert_size(size)
                    elif "file format" in line:
                        self.source_format = line.split(":")[1]

                self.logger.info("Updated source image virtual disk capacity : {} ,"
                                 "Updated source image populated size: {}".format(self.disk_capacity,
                                                                                  self.disk_populated_size))
                return True
        except Exception as exp:
            error_msg = "ERROR: Error occurred while getting information about source image : {}".format(exp)
            self.logger.error(error_msg)
            print(error_msg)
            return False

    def __convert_image(self):
        """
            Private method to convert source disk image into .vmdk disk image.
            Args  : None
            Return : True on success else False
        """

        print("Converting source disk image to .vmdk ")

        command = "qemu-img convert -p -f " + self.source_format + " -O " + self.output_diskimage_format + \
            " -o subformat=streamOptimized " + self.source_img_path + " " + self.output_diskimage_path

        _, error, returncode = self.__execute_command(command, show_output=True)

        if error or returncode:
            error_msg = "ERROR: Error occurred while converting source disk image into vmdk: {}\n" + \
                "return code : {} ".format(error, returncode)
            self.logger.error(error_msg)
            print(error_msg)
            return False
        else:
            if os.path.isfile(self.output_diskimage_path):
                self.logger.info("Successfully converted source image {} into {} \n "
                                 "return code : {} ".format(self.source_img_path,
                                                            self.output_diskimage_path,
                                                            returncode))
                result = self.__make_image_bootable()
                if result:
                    self.logger.info("Made {} bootable".format(self.output_diskimage_path))
                    print("Output VMDK is at: {}".format(self.output_diskimage_path))
                    return True
                else:
                    self.logger.error("Cannot make {} bootable".format(self.output_diskimage_path))
                    print("ERROR: Fail to convert source image into .vmdk")
                    return False
            else:
                self.logger.error("Converted vmdk disk file {} is not present \n ".format(
                    self.output_diskimage_path))
                print("Fail to convert source image into .vmdk")
                return False

    def __make_image_bootable(self):
        """
            Private method to make source disk image bootable.
            Args  : None
            Return : True on success else False
        """
        command = "printf '\x03' | dd conv=notrunc of=" + self.output_diskimage_path + "\t bs=1 seek=$((0x4))"
        output, error, returncode = self.__execute_command(command)

        if error and returncode:
            error_msg = "ERROR:Error occurred while making source disk image bootable : {} \n "\
                "return code : {} ".format(error, returncode)
            self.logger.error(error_msg)
            print(error_msg)
            return False
        else:
            self.logger.info("Make Image Bootable Output : {} ".format(output))
            return True

    def __edit_ovf_template(self):
        """
            Private method to create new OVF file by editing OVF template
            Args  : None
            Return : True on success else False
        """
        try:
            print("Creating OVF")
            # Read OVF template file
            OVF_tree = ET.parse(self.ovf_template_path)
            root = OVF_tree.getroot()

            # Collect namespaces
            nsmap = {k: v for k, v in root.nsmap.items() if k}
            nsmap["xmlns"] = "http://schemas.dmtf.org/ovf/envelope/1"

            # Edit OVF template
            references = root.find('xmlns:References', nsmap)
            if references is not None:
                file_tag = references.find('xmlns:File', nsmap)
                if file_tag is not None:
                    file_tag.attrib['{' + nsmap['ovf'] + '}href'] = self.output_diskimage_name

            disksection = root.find('xmlns:DiskSection', nsmap)
            if disksection is not None:
                diak_tag = disksection.find('xmlns:Disk', nsmap)
                if diak_tag is not None:
                    if self.disk_size and self.disk_size > self.disk_capacity:
                        self.disk_capacity = self.disk_size

                    diak_tag.attrib['{' + nsmap['ovf'] + '}capacity'] = str(self.disk_capacity)
                    diak_tag.attrib['{' + nsmap['ovf'] + '}populatedSize'] = str(self.disk_populated_size)

            virtuasystem = root.find('xmlns:VirtualSystem', nsmap)
            if virtuasystem is not None:
                name_tag = virtuasystem.find('xmlns:Name', nsmap)
                if name_tag is not None:
                    name_tag.text = self.vm_name

                if self.os_type is not None:
                    operatingSystemSection = virtuasystem.find('xmlns:OperatingSystemSection', nsmap)
                    if self.osID and self.osType:
                        operatingSystemSection.attrib['{' + nsmap['ovf'] + '}id'] = self.osID
                        os_discription_tag = operatingSystemSection.find('xmlns:Description', nsmap)
                        os_discription_tag.text = self.osType

                virtualHardwareSection = virtuasystem.find('xmlns:VirtualHardwareSection', nsmap)
                system = virtualHardwareSection.find('xmlns:System', nsmap)
                virtualSystemIdentifier = system.find('vssd:VirtualSystemIdentifier', nsmap)
                if virtualSystemIdentifier is not None:
                    virtualSystemIdentifier.text = self.vm_name
                VirtualSystemType = system.find('vssd:VirtualSystemType', nsmap)
                if VirtualSystemType is not None:
                    VirtualSystemType.text = "vmx-{}".format(self.hwversion)

                if self.memory is not None or self.cpu is not None or self.disk_controller is not None:
                    for item in virtualHardwareSection.iterfind('xmlns:Item', nsmap):
                        description = item.find("rasd:Description", nsmap)

                        if self.cpu is not None:
                            if description is not None and description.text == "Number of Virtual CPUs":
                                cpu_item = item.find("rasd:VirtualQuantity", nsmap)
                                name_item = item.find("rasd:ElementName", nsmap)
                                if cpu_item is not None:
                                    cpu_item.text = self.cpu
                                    name_item.text = self.cpu + " virtual CPU(s)"

                        if self.memory is not None:
                            if description is not None and description.text == "Memory Size":
                                mem_item = item.find("rasd:VirtualQuantity", nsmap)
                                name_item = item.find("rasd:ElementName", nsmap)
                                if mem_item is not None:
                                    mem_item.text = self.memory
                                    name_item.text = self.memory + " MB of memory"

                        if self.disk_controller is not None:
                            if description is not None and description.text == "SCSI Controller":
                                if self.disk_controller_info is not None:
                                    name_item = item.find("rasd:ElementName", nsmap)
                                    name_item.text = str(self.disk_controller_info["controllerName"]) + "0"

                                    resource_type = item.find("rasd:ResourceType", nsmap)
                                    resource_type.text = self.disk_controller_info["resourceType"]

                                    description.text = self.disk_controller_info["controllerName"]
                                    resource_subtype = item.find("rasd:ResourceSubType", nsmap)
                                    if self.disk_controller_info["controllerName"] == "IDE Controller":
                                        # Remove resource subtype item
                                        resource_subtype.getparent().remove(resource_subtype)
                                    if "resourceSubType" in self.disk_controller_info:
                                        resource_subtype.text = self.disk_controller_info["resourceSubType"]
                if self.cdrom:
                    last_item = list(virtualHardwareSection.iterfind('xmlns:Item', nsmap))[-1]
                    ide_cdrom_items_etree = ET.parse(IDE_CDROM_XML_PATH)
                    ide_cdrom_items = list(ide_cdrom_items_etree.iterfind('Item'))
                    for item in ide_cdrom_items:
                        last_item.addnext(item)

            # Save output OVF
            OVF_tree.write(self.output_path, xml_declaration=True, encoding='utf-8',
                           method="xml")

            if os.path.isfile(self.output_path):
                logger.info("Successfully written output OVF at {}".format(self.output_path))
                print("Output OVF is at: {}".format(self.output_path))
                return self.output_path
            else:
                error_msg = "ERROR: Error occurred while creating OVF file"
                print(error_msg)
                return False

        except Exception as exp:
            error_msg = "ERROR: Error occurred while editing OVF template : {}".format(exp)
            self.logger.error(error_msg)
            print(error_msg)
            return False

    def __convert_size(self, size):
        """
            Private method to convert disk size from GB,MB to bytes.
            Args :
                size  : disk size with prefix 'G' for GB and 'M' for MB
            Return :  disk size in bytes
        """
        byte_size = 0
        try:
            if not size:
                self.logger.error("No size {} to convert in bytes".format(size))
            else:
                size = str(size)
                disk_size = float(size[:-1])
                input_type = size[-1].strip()

                self.logger.info("Disk size : {} , size type : {} ".format(disk_size, input_type))

                if input_type == "G":
                    byte_size = disk_size * 1024 * 1024 * 1024
                elif input_type == "M":
                    byte_size = disk_size * 1024 * 1024

                self.logger.info("Disk size in bytes: {} ".format(byte_size))

            return int(byte_size)

        except Exception as exp:
            error_msg = "ERROR:Error occurred while converting disk size in bytes : {}".format(exp)
            self.logger.error(error_msg)
            print(error_msg)
            return False

    def __get_osType(self):
        """
            Private method to get OS ID and Type
            Args :
                None
            Return :
                osID : OS ID
                osType: OS Type
        """
        osID = None
        osType = None
        os_info = self.__read_yaml_file(OS_INFO_FILE_PATH)

        try:
            if self.os_type and os_info:
                for os_id, os_type in os_info.items():
                    if self.os_type.lower() == os_type.lower():
                        osID = os_id
                        osType = os_type
                        break
        except Exception as exp:
            error_msg = "ERROR:Error occurred while getting OS details : {}".format(exp)
            self.logger.error(error_msg)
            print(error_msg)

        return osID, osType

    def __get_diskcontroller(self):
        """
            Private method to get details of Disk Controller
            Args :
                None
            Return :
                disk_controller : dict with details of Disk Controller
        """
        disk_controller = {}
        scsi_subtype = None
        if self.disk_controller.lower() in ["paravirtual", "lsilogic", "buslogic", "lsilogicsas"]:
            scsi_subtype = self.disk_controller
            self.disk_controller = "SCSI"

        disk_controller_info = self.__read_yaml_file(DISK_CONTROLLER_INFO_FILE_PATH)
        try:
            if self.disk_controller and disk_controller_info:
                for key, value in disk_controller_info.items():
                    if self.disk_controller.lower() in key.lower():
                        disk_controller['controllerName'] = key
                        disk_controller['resourceType'] = str(value["ResourceType"])
                        resourceSubTypes = value["ResourceSubTypes"] if "ResourceSubTypes" in value else None
                        if key == "SATA Controller":
                            disk_controller["resourceSubType"] = resourceSubTypes[0]
                        elif key == "SCSI Controller":
                            if scsi_subtype:
                                if scsi_subtype.lower() == "paravirtual":
                                    scsi_subtype = "VirtualSCSI"
                                for subtype in resourceSubTypes:
                                    if scsi_subtype.lower() == subtype.lower():
                                        disk_controller["resourceSubType"] = subtype
                                        break
                                else:
                                    error_msg = "ERROR: Invalid inputs can not "\
                                        "find SCSI subtype {}".format(scsi_subtype)
                                    self.__raise_exception(error_msg)

        except KeyError as exp:
            error_msg = "ERROR:Error occurred while getting Disk Controller details : {}".format(exp)
            self.logger.error(error_msg)
            print(error_msg)

        return disk_controller

    def __read_yaml_file(self, file_path):
        """
            Private method to execute command
            Args :
                command  : command to execute
            Return :
                Dict of yaml data
        """
        with open(file_path) as data_file:
            data = yaml.load(data_file, Loader=yaml.SafeLoader)
        return data

    def __raise_exception(self, error_msg, exception_type="Generic"):
        """
            Private method to execute command
            Args :
                command  : command to execute
            Return :
               None
        """
        if error_msg:
            self.logger.debug(error_msg)
            print(error_msg)
            if exception_type == "Generic":
                raise Exception(error_msg)
            elif exception_type == "IO":
                raise Exception(error_msg)

    def __execute_command(self, command, show_output=False):
        """
            Private method to execute command
            Args :
                command  : command to execute
            Return :
                stdout : output of command
                stderr: error occurred while executing command if any
                returncode : return code of command execution
        """
        try:
            self.logger.info("Execute command: {} ".format(command))

            proc = subprocess.Popen(command, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
                                    stderr=subprocess.PIPE, shell=True, bufsize=1)

            stdout = b''
            stderr = b''

            while True:
                output = proc.stdout.read(1)
                stdout += output
                if show_output:
                    print(output.decode(), end='')
                returncode = proc.poll()
                if returncode is not None:
                    for output in proc.stdout.readlines():
                        stdout += output
                        if show_output:
                            print(output.decode(), end='')
                    break

            for output in proc.stderr.readlines():
                stderr += output

        except Exception as exp:
            self.logger.error("Error {} occurred while executing command {} ".format(exp, command))

        return stdout, stderr, returncode

    def create_ovf(self):
        """
            Method to convert source image into OVF
            Args : None
            Return : True on success else False
        """
        # check output format
        if self.source_format == self.output_format:
            self.logger.info("Source format is OVF. No need to convert: {} ")
            return self.source_img_path

        # Get source img properties
        img_info = self.__get_image_info()
        if img_info:

            # Create vmdk disk image
            disk_img = self.__convert_image()
            if disk_img:

                # Edit OVF tempalte
                ovf_path = self.__edit_ovf_template()
                return ovf_path
        else:
            self.logger.error("Error in getting image information cannot convert image")
            raise Exception("Error in getting image information cannot convert image")
            return False
