#!/usr/bin/env python
#
# VMEncryption extension
#
# Copyright 2015 Microsoft Corporation
#
# 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.

import subprocess
import os
import os.path
import shlex
import sys
from subprocess import *
import shutil
import uuid
import glob
from common import DeviceItem
import Utils.HandlerUtil
import traceback
try:
        import ConfigParser as ConfigParsers
except ImportError:
        import configparser as ConfigParsers

class DiskUtil(object):
    __instance__ = None
    patching = None
    logger = None
    mount_output = None


    def __init__(self, patching, logger):

        if DiskUtil.__instance__ is None:
            self.patching = patching
            self.logger = logger
            self.mount_output = None
            DiskUtil.__instance__ = self
        else:
            return DiskUtil.__instance__

    @staticmethod
    def get_instance(patching, logger):
        if not DiskUtil.__instance__:
            DiskUtil(patching, logger)

        return DiskUtil.__instance__

    def get_device_items_property(self, lsblk_path, dev_name, property_name):
        get_property_cmd = lsblk_path + " /dev/" + dev_name + " -b -nl -o NAME," + property_name
        get_property_cmd_args = shlex.split(get_property_cmd)
        get_property_cmd_p = Popen(get_property_cmd_args,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
        output,err = get_property_cmd_p.communicate()
        output= str(output)
        lines = output.splitlines()
        for i in range(0,len(lines)):
            item_value_str = lines[i].strip()
            if(item_value_str != ""):
                disk_info_item_array = item_value_str.split()
                if(dev_name == disk_info_item_array[0]):
                    if(len(disk_info_item_array) > 1):
                        return disk_info_item_array[1]
        return None

    def get_device_items_sles(self,dev_path):
        self.logger.log("get_device_items_sles : getting the blk info from " + str(dev_path), True)
        device_items = []
        #first get all the device names
        if(dev_path is None):
            get_device_cmd = self.patching.lsblk_path + " -b -nl -o NAME"
        else:
            get_device_cmd = self.patching.lsblk_path + " -b -nl -o NAME " + dev_path
        get_device_cmd_args = shlex.split(get_device_cmd)
        p = Popen(get_device_cmd_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        out_lsblk_output, err = p.communicate()
        out_lsblk_output = str(out_lsblk_output)
        lines = out_lsblk_output.splitlines()
        for i in range(0,len(lines)):
            item_value_str = lines[i].strip()
            if(item_value_str != ""):
                disk_info_item_array = item_value_str.split()
                device_item = DeviceItem()
                device_item.name = disk_info_item_array[0]
                device_items.append(device_item)

        for i in range(0,len(device_items)):
            device_item = device_items[i]
            device_item.file_system = self.get_device_items_property(lsblk_path=self.patching.lsblk_path,dev_name=device_item.name,property_name='FSTYPE')
            device_item.mount_point = self.get_device_items_property(lsblk_path=self.patching.lsblk_path,dev_name=device_item.name,property_name='MOUNTPOINT')
            device_item.label = self.get_device_items_property(lsblk_path=self.patching.lsblk_path,dev_name=device_item.name,property_name='LABEL')
            device_item.uuid = self.get_device_items_property(lsblk_path=self.patching.lsblk_path,dev_name=device_item.name,property_name='UUID')
            #get the type of device
            model_file_path = '/sys/block/' + device_item.name + '/device/model'
            if(os.path.exists(model_file_path)):
                with open(model_file_path,'r') as f:
                    device_item.model = f.read().strip()
            if(device_item.model == 'Virtual Disk'):
                self.logger.log("model is virtual disk", True)
                device_item.type = 'disk'
            if(device_item.type != 'disk'):
                partition_files = glob.glob('/sys/block/*/' + device_item.name + '/partition')
                if(partition_files is not None and len(partition_files) > 0):
                    self.logger.log("partition files exists", True)
                    device_item.type = 'part'
        return device_items

    def get_device_items_from_lsblk_list(self, lsblk_path, dev_path):
        self.logger.log("get_device_items_from_lsblk_list : getting the blk info from " + str(dev_path), True)
        device_items = []
        #first get all the device names
        if(dev_path is None):
            get_device_cmd = lsblk_path + " -b -nl -o NAME"
        else:
            get_device_cmd = lsblk_path + " -b -nl -o NAME " + dev_path
        get_device_cmd_args = shlex.split(get_device_cmd)
        p = Popen(get_device_cmd_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        out_lsblk_output, err = p.communicate()
        if sys.version_info > (3,):
            out_lsblk_output =str(out_lsblk_output, encoding='utf-8', errors="backslashreplace")
        else:
            out_lsblk_output =str(out_lsblk_output)
        lines = out_lsblk_output.splitlines()
        device_items_temp = []
        for i in range(0,len(lines)):
            item_value_str = lines[i].strip()
            if(item_value_str != ""):
                disk_info_item_array = item_value_str.split()
                device_item = DeviceItem()
                device_item.name = disk_info_item_array[0]
                device_items_temp.append(device_item)

        for i in range(0,len(device_items_temp)):
            device_item = device_items_temp[i]
            device_item.mount_point = self.get_device_items_property(lsblk_path=lsblk_path,dev_name=device_item.name,property_name='MOUNTPOINT')
            if (device_item.mount_point is not None):
                device_item.file_system = self.get_device_items_property(lsblk_path=lsblk_path,dev_name=device_item.name,property_name='FSTYPE')
                device_item.label = self.get_device_items_property(lsblk_path=lsblk_path,dev_name=device_item.name,property_name='LABEL')
                device_item.uuid = self.get_device_items_property(lsblk_path=lsblk_path,dev_name=device_item.name,property_name='UUID')
                device_item.type = self.get_device_items_property(lsblk_path=lsblk_path,dev_name=device_item.name,property_name='TYPE')
                device_items.append(device_item)
                self.logger.log("lsblk MOUNTPOINT=" + str(device_item.mount_point) + ", NAME=" + str(device_item.name) + ", TYPE=" + str(device_item.type) + ", FSTYPE=" + str(device_item.file_system) + ", LABEL=" + str(device_item.label) + ", UUID=" + str(device_item.uuid) + ", MODEL=" + str(device_item.model), True)
        return device_items

    def get_lsblk_pairs_output(self, lsblk_path, dev_path):
        self.logger.log("get_lsblk_pairs_output : getting the blk info from " + str(dev_path) + " using lsblk_path " + str(lsblk_path), True)
        
        # If an alternate user is specified  in vmbackup.conf, run lsblk command through that user, not with root access. 
        # Fixes issues found in some SUSE-related distros where lsblk command gets stuck with root access
        # Sample vmbackup.conf file with such alternate user setting:
        # [lsblkUser]
        # username: vmadmin

        configfile = '/etc/azure/vmbackup.conf'
        command_user = ''
        alternate_user = False

        try :
            if os.path.exists(configfile):
                config = ConfigParsers.ConfigParser()
                config.read(configfile)
                if config.has_option('lsblkUser','username'):
                    lsblk_user = config.get('lsblkUser','username')
                    command_user = "su - " + lsblk_user + " -c"
                    if (dev_path is None):
                        command_user = command_user + ' \'' + 'lsblk -b -n -P -o NAME,TYPE,FSTYPE,MOUNTPOINT,LABEL,UUID,MODEL,SIZE' + '\''
                    else:
                        command_user = command_user + ' \'' + 'lsblk -b -n -P -o NAME,TYPE,FSTYPE,MOUNTPOINT,LABEL,UUID,MODEL,SIZE' + ' ' + dev_path + '\''
                    alternate_user = True
        except Exception as e:
            pass

        out_lsblk_output = None
        error_msg = None
        is_lsblk_path_wrong = False
        try:
            if (alternate_user):
                self.logger.log("Switching to alternate user to run this lsblk command: " + str(command_user), True)
                p = Popen(command_user, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
            elif(dev_path is None):
                p = Popen([str(lsblk_path), '-b', '-n','-P','-o','NAME,TYPE,FSTYPE,MOUNTPOINT,LABEL,UUID,MODEL,SIZE'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            else:
                p = Popen([str(lsblk_path), '-b', '-n','-P','-o','NAME,TYPE,FSTYPE,MOUNTPOINT,LABEL,UUID,MODEL,SIZE',dev_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        except Exception as e:
            errMsg = 'Exception in lsblk command, error: %s, stack trace: %s' % (str(e), traceback.format_exc())
            self.logger.log(errMsg, True, 'Error')
            is_lsblk_path_wrong = True
        if is_lsblk_path_wrong == False :
            out_lsblk_output, err = p.communicate()
            if sys.version_info > (3,):
                out_lsblk_output = str(out_lsblk_output, encoding='utf-8', errors="backslashreplace")
            else:
                out_lsblk_output = str(out_lsblk_output)    
            error_msg = str(err)
            if(error_msg is not None and error_msg.strip() != ""):
                self.logger.log(str(err), True)
        return is_lsblk_path_wrong, out_lsblk_output, error_msg
    
    def get_which_command_result(self, program_to_locate):
        self.logger.log("getting the which info for  " + str(program_to_locate), True)
        out_which_output = None
        error_msg = None
        try:
            p = Popen(['which', str(program_to_locate)], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            out_which_output, err = p.communicate()
            if sys.version_info > (3,):
                out_which_output = str(out_which_output, encoding='utf-8', errors="backslashreplace")
            else:
                out_which_output = str(out_which_output)
            error_msg = str(err)
            if(error_msg is not None and error_msg.strip() != ""):
                self.logger.log(str(err), True)
            self.logger.log("which command result :" + str(out_which_output), True)
            if (out_which_output is not None):
                out_which_output = out_which_output.splitlines()[0]
        except Exception as e:
            errMsg = 'Exception in which command, error: %s, stack trace: %s' % (str(e), traceback.format_exc())
            self.logger.log(errMsg, True, 'Error')
        return out_which_output, error_msg

    def get_device_items(self, dev_path):
        if(self.patching.distro_info[0].lower() == 'suse' and self.patching.distro_info[1] == '11'):
            return self.get_device_items_sles(dev_path)
        else:
            self.logger.log("getting the blk info from " + str(dev_path), True)
            device_items = []
            lsblk_path = self.patching.lsblk_path
            # Get lsblk command output using lsblk_path as self.patching.lsblk_path
            is_lsblk_path_wrong, out_lsblk_output, error_msg = self.get_lsblk_pairs_output(lsblk_path, dev_path)
            # if lsblk_path was wrong, use /bin/lsblk or usr/bin/lsblk based on self.patching.usr_flag to get lsblk command output again for centos/redhat distros
            if (is_lsblk_path_wrong == True) and (self.patching.distro_info[0].lower() == 'centos' or self.patching.distro_info[0].lower() == 'redhat'):
                if self.patching.usr_flag == 1:
                    self.logger.log("lsblk path is wrong, removing /usr prefix", True, 'Warning')
                    lsblk_path = "/bin/lsblk"
                else:
                    self.logger.log("lsblk path is wrong, adding /usr prefix", True, 'Warning')
                    lsblk_path = "/usr/bin/lsblk"
                is_lsblk_path_wrong, out_lsblk_output, error_msg = self.get_lsblk_pairs_output(lsblk_path, dev_path)
            # if lsblk_path was still wrong, lsblk_path using "which" command
            if (is_lsblk_path_wrong == True):
                self.logger.log("lsblk path is wrong. finding path using which command", True, 'Warning')
                out_which_output, which_error_msg = self.get_which_command_result('lsblk')
                # get lsblk command output
                if (out_which_output is not None):
                     lsblk_path = str(out_which_output)
                     is_lsblk_path_wrong, out_lsblk_output, error_msg = self.get_lsblk_pairs_output(lsblk_path, dev_path)
            # if error_msg contains "invalid option" or "P" (rely on only "-P" optiont in error to handle non-English locales), then get device_items using method get_device_items_from_lsblk_list
            if (error_msg is not None and error_msg.strip() != "" and ('invalid option' in error_msg or 'P' in error_msg)):
                device_items = self.get_device_items_from_lsblk_list(lsblk_path, dev_path)
            # else get device_items from parsing the lsblk command output
            elif (out_lsblk_output is not None):
                lines = out_lsblk_output.splitlines()
                for i in range(0,len(lines)):
                    item_value_str = lines[i].strip()
                    if(item_value_str != ""):
                        disk_info_item_array = item_value_str.split()
                        device_item = DeviceItem()
                        disk_info_item_array_length = len(disk_info_item_array)
                        for j in range(0, disk_info_item_array_length):
                            disk_info_property = disk_info_item_array[j]
                            property_item_pair = disk_info_property.split('=')

                            if(property_item_pair[0] == 'NAME'):
                                device_item.name = property_item_pair[1].strip('"')

                            if(property_item_pair[0] == 'TYPE'):
                                device_item.type = property_item_pair[1].strip('"')

                            if(property_item_pair[0] == 'FSTYPE'):
                                device_item.file_system = property_item_pair[1].strip('"')
                        
                            if(property_item_pair[0] == 'MOUNTPOINT'):
                                device_item.mount_point = property_item_pair[1].strip('"')

                            if(property_item_pair[0] == 'LABEL'):
                                device_item.label = property_item_pair[1].strip('"')

                            if(property_item_pair[0] == 'UUID'):
                                device_item.uuid = property_item_pair[1].strip('"')

                            if(property_item_pair[0] == 'MODEL'):
                                device_item.model = property_item_pair[1].strip('"')

                        self.logger.log("lsblk MOUNTPOINT=" + str(device_item.mount_point) + ", NAME=" + str(device_item.name) + ", TYPE=" + str(device_item.type) + ", FSTYPE=" + str(device_item.file_system) + ", LABEL=" + str(device_item.label) + ", UUID=" + str(device_item.uuid) + ", MODEL=" + str(device_item.model), True)
                        
                        if(device_item.mount_point is not None and device_item.mount_point != "" and device_item.mount_point != " "):
                            device_items.append(device_item)
            return device_items

    def get_mount_command_output(self, mount_path):
        self.logger.log("getting the mount info using mount_path " + str(mount_path), True)
        out_mount_output = None
        error_msg = None
        is_mount_path_wrong = False
        try:
            p = Popen([str(mount_path)], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        except Exception as e:
            errMsg = 'Exception in mount command, error: %s, stack trace: %s' % (str(e), traceback.format_exc())
            self.logger.log(errMsg, True, 'Error')
            is_mount_path_wrong = True
        if is_mount_path_wrong == False :
            out_mount_output, err = p.communicate()
            if sys.version_info > (3,):
                out_mount_output = str(out_mount_output, encoding='utf-8', errors="backslashreplace")
            else:
                out_mount_output = str(out_mount_output)
            self.logger.log("getting the mount info using mount_path " + out_mount_output, True)
            error_msg = str(err)
            if(error_msg is not None and error_msg.strip() != ""):
                self.logger.log(str(err), True)
        return is_mount_path_wrong, out_mount_output, error_msg

    def get_mount_points(self):
        mount_points_info = []
        mount_points = []
        fs_types = []
        out_mount_output = self.get_mount_output()
        if (out_mount_output is not None):
            #Extract the list of mnt_point in order
            lines = out_mount_output.splitlines()
            #Reverse the mount command output to go-through from last-to-first mounts in output
            lines.reverse()
            for line in lines: