#!/usr/bin/python

#
# Copyright (c) 2017-2020 Virtuozzo International GmbH
#

import prlsdkapi
from prlsdkapi import consts as pc
import subprocess
import syslog
import time
import json
import sys
from string import ascii_uppercase
from xml.dom import minidom
import os
import re
import fcntl
import tempfile

flags_running = [pc.VMS_STARTING, pc.VMS_RUNNING, pc.VMS_SUSPENDING, pc.VMS_SNAPSHOTING, pc.VMS_RESETTING, pc.VMS_PAUSING, pc.VMS_CONTINUING, pc.VMS_MOUNTED]

VE_DB = '/var/lib/vz-guest-tools-updater.json'
SUPPORTED_GUESTS = ['Linux', 'Windows']
DEBUG = False
GUEST_ISO_LIN = '/usr/share/vz-guest-tools/vz-guest-tools-lin.iso'
GUEST_ISO_WIN = '/usr/share/vz-guest-tools/vz-guest-tools-win.iso'
GUEST_MNT_POINT = '/tmp/vz-guest-tools-iso'
GUEST_WIN_LNK_FILE = '/usr/share/vz-guest-tools-updater/vz-guest-tools-install.lnk'
LOCKFILE = '/var/lock/vz-guest-tools-updater.lock'
LOCK_TIMEOUT = 600
LOCK_WAIT = 10

def log_debug(msg):
    if DEBUG:
        syslog.syslog(msg)

log_debug('Tools autoupdate started')

try:
    import libvirt
    import guestfs
    prlsdkapi.init_server_sdk()
    _server = prlsdkapi.Server()
    _server.login_local().wait()
except Exception, err:
    log_debug(str(err))
    sys.exit(1)

# We have to manually compare version of installed guest tools with version of guest tools package
# until we fix PSBM-60158
try:
    win_guest_ver = subprocess.check_output(['/bin/rpm', '-q', '--qf', '%{VERSION}-%{RELEASE}', 'vz-guest-tools-win'])
    lin_guest_ver = subprocess.check_output(['/bin/rpm', '-q', '--qf', '%{VERSION}-%{RELEASE}', 'vz-guest-tools-lin'])
except Exception, err:
    log_debug(str(err))
    sys.exit(1)


def get_actual_tools_ver(ve_type):
    """
    Should we use version of Lin or Win guest tools?
    """
    if ve_type == 'Linux':
        return lin_guest_ver
    else:
        return win_guest_ver


def check_bad_udev_guest(ve_uuid, ve_db):
    """
    In some guests our udev magic doesn't work
    """
    log_debug("'Bad Udev' check")

    # Even in 'bad' guests installer can be sometimes launched automagically
    try:
        if ve_db[ve_uuid]['type'] == 'Linux':
            if subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, 'pgrep', '/install$']) == 0:
                log_debug("Running installer detected!")
                return False
    except:
        log_debug("Can't check if installer is already running")
        pass

    FNULL = open(os.devnull, 'w')
    try:
        check_centos = subprocess.check_output(['/bin/virsh', 'x-exec', ve_uuid, '/bin/cat', '/etc/centos-release'], stderr=FNULL, close_fds=True)
        if "CentOS release 6" in check_centos:
            log_debug("CentOS 6 detected")
            FNULL.close()
            return True
    except:
        log_debug("Centos check failed")
        pass

    try:
        check_debian = subprocess.check_output(['/bin/virsh', 'x-exec', ve_uuid, '/bin/cat', '/etc/debian_version'], stderr=FNULL, close_fds=True)
        if check_debian.startswith("7"):
            log_debug("Debian 7 detected")
            FNULL.close()
            return True
    except:
        log_debug("Debian check failed")
        pass

    FNULL.close()
    return False


def check_tools_ver(tools_ver):
    """
    Check if the installed tools version looks like Vz one
    """
    return 'vz7' in tools_ver or 'vz8' in tools_ver or 'rv7' in tools_ver


def analyze_ves():
    """
    'Heavy' function - analyze all VMs and decide which of them should be updated
    """
    # Load cache of VMs processed during the last launch
    try:
        ve_db = json.load(open(VE_DB))
    except:
        ve_db = {}

    # Get list of all VMs
    try:
        ves = _server.get_vm_list_ex(nFlags=pc.PVTF_VM).wait()
    except:
        return

    processed_ves = []
    for ve in ves:
        # prlctl accepts uuids with brackets, but virsh doesn't
        ve_uuid = ve.get_uuid().lstrip('{').rstrip('}')
        log_debug("Processing " + ve_uuid)
        if ve_uuid not in ve_db:
            ve_db[ve_uuid] = {}
        processed_ves.append(ve_uuid)
        if ve.is_template():
            log_debug('Template, skipping')
            ve_db[ve_uuid]['need_update'] = False
            continue
        # 'type' can be missing in the db if autoupdate was previously disabled
        # so we didn't populate the db with all data
        if ve_uuid in ve_db and 'type' in ve_db[ve_uuid]:
            ve_type = ve_db[ve_uuid]['type']
            if ve_type not in SUPPORTED_GUESTS:
                continue
            if 'current' not in ve_db[ve_uuid]:
                ve_db[ve_uuid]['current'] = ''
            current_ver = ve_db[ve_uuid]['current']
            new_ver = get_actual_tools_ver(ve_db[ve_uuid]['type'])

            if current_ver == new_ver:
                if 'last_update_from' in ve_db[ve_uuid]:
                    syslog.syslog('{"name": "ToolsAutoUpdateResult", "actors": {"VEs": [{"%s"}]}, "from_version": "%s", "to_version": "%s", "result": "success"}' \
                            % (ve_uuid, ve_db[ve_uuid]['last_update_from'], ve_db[ve_uuid]['last_update_to']))
                ve_db[ve_uuid]['need_update'] = False
                ve_db[ve_uuid]['failed'] = ''
                continue
        else:
            ve_db[ve_uuid] = {}

        # Either we don't have VM in db or we want to refresh info about it
        conf = ve.get_config()

        guest_os_name = prlsdkapi.call_sdk_function('PrlApi_GuestToString', conf.get_os_type())
        ve_db[ve_uuid]['type'] = guest_os_name

        try:
            r = ve.get_tools_state().wait()
            pcount = r.get_params_count()
        except:
            log_debug('Failed to get tools stat')
            continue

        if pcount > 0:
            tools_info = r.get_param()
            tools_ver = tools_info.get_version()
            if not check_tools_ver(tools_ver):
                log_debug('Strange tools detected')

                with open(os.devnull, 'w') as FNULL:
                    if subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '/bin/ls', '/'], stdout=FNULL, stderr=subprocess.STDOUT) == 255:
                        log_debug('Will try to install our tools from scratch')
                        tools_ver = None
                        continue

        if pcount == 0 or not tools_ver:
            if conf.is_tools_auto_update_enabled():
                log_debug('Tools are not installed, will try to install from scratch')
                ve_db[ve_uuid]['need_update'] = True
            else:
                log_debug('Tools are not installed and auto-update is disabled')
                ve_db[ve_uuid]['need_update'] = False
            continue

        ve_db[ve_uuid]['current'] = tools_ver

        if not conf.is_tools_auto_update_enabled():
            ve_db[ve_uuid]['need_update'] = False
        elif guest_os_name == "Linux":
            ve_db[ve_uuid]['need_update'] = (tools_ver != lin_guest_ver)
        elif guest_os_name == "Windows":
            ve_db[ve_uuid]['need_update'] = (tools_ver != win_guest_ver)
        else:
            ve_db[ve_uuid]['need_update'] = False

        if 'last_update_from' in ve_db[ve_uuid]:
            if ve_db[ve_uuid]['last_update_from'] == current_ver \
                    and ve_db[ve_uuid]['last_update_to'] == new_ver:
                if 'failed' in ve_db[ve_uuid] and ve_db[ve_uuid]['failed'] == 'once':
                    ve_db[ve_uuid]['failed'] = 'failed'
                    syslog.syslog('{"name": "ToolsAutoUpdateResult", "actors": {"VEs": [{"%s"}]}, "from_version": "%s", "to_version": "%s", "result": "failed"}' \
                            % (ve_uuid, ve_db[ve_uuid]['last_update_from'], ve_db[ve_uuid]['last_update_to']))
                    ve_db[ve_uuid]['need_update'] = False
                    continue
                elif 'failed' in ve_db[ve_uuid] and ve_db[ve_uuid]['failed'] == 'failed':
                    ve_db[ve_uuid]['failed'] = ''
                    ve_db[ve_uuid]['need_update'] = True
                    continue
                else:
                    syslog.syslog('{"name": "ToolsAutoUpdateResult", "actors": {"VEs": [{"%s"}]}, "from_version": "%s", "to_version": "%s", "result": "failedOnce"}' \
                            % (ve_uuid, ve_db[ve_uuid]['last_update_from'], ve_db[ve_uuid]['last_update_to']))
                    ve_db[ve_uuid]['failed'] = 'once'

    # Clean VE_DB - drop entries that don't exist anymore
    to_drop = [ve for ve in ve_db if ve not in processed_ves]
    for ve in to_drop:
        ve_db.pop(ve, None)

    open(VE_DB, 'w').write(json.dumps(ve_db, indent = 2))


def get_ves_to_update():
    """
    Read the database and return list of VMs to be updated
    """
    try:
        ve_db = json.load(open(VE_DB))
    except:
        log_debug("Failed to load json")
        ve_db = {}

    ve_list = [ve_uuid for ve_uuid in ve_db if 'need_update' in ve_db[ve_uuid] and ve_db[ve_uuid]['need_update']]
    log_debug("Will auto_select " + ", ".join(ve_list))

    return ve_list


def get_max_vms_per_time():
    """
    Get maximum number of VMs that can be processed at once
    """
    # Limit number of VMs that can be processed at once
    try:
        conf = json.load(open('/etc/vz/tools-update.conf'))
        max_vms = conf['MaxVMs']
    except:
        # Just a hardcoded value - we use it in our config by default
        max_vms = 5
    return max_vms


def install_tools_enabled():
    """
    Check if config allows us installing tools if they are missing
    """
    try:
        conf = json.load(open('/etc/vz/tools-update.conf'))
        return conf['InstallTools']
    except:
        # By default, we allow tools installation
        return True


def update_udev_script(ve_uuid):
    """
    Update install-tools script inside the guest before connecting CD with guest tools
    In old guest tools, that script could be killed by timeout.
    """
    log_debug("Updating install-tools script")
    if not os.path.exists(GUEST_MNT_POINT):
        os.mkdir(GUEST_MNT_POINT)
    else:
        subprocess.call(['/bin/umount', GUEST_MNT_POINT])

    if subprocess.call(['/bin/mount', '-o', 'loop', GUEST_ISO_LIN, GUEST_MNT_POINT]) != 0:
        log_debug("Failed to mount guest cd on host")
    if not os.path.isfile(GUEST_MNT_POINT + "/install-tools"):
        log_debug("Guest cd not mounted, can't proceed")
        return False

    try:
        newf = open(GUEST_MNT_POINT + "/install-tools")
        subprocess.call(['/bin/virsh', 'x-exec', '--shell', ve_uuid, "/bin/cat /dev/null > /usr/bin/install-tools.new"])
        for l in newf.readlines():
            l = re.escape(l).rstrip()
            subprocess.call(['/bin/virsh', 'x-exec', '--shell', ve_uuid, "/bin/echo " + l + " >> /usr/bin/install-tools.new"])
        subprocess.call(['/bin/virsh', 'x-exec', '--shell', ve_uuid, "/bin/cat /usr/bin/install-tools.new > /usr/bin/install-tools"])
        subprocess.call(['/bin/virsh', 'x-exec', '--shell', ve_uuid, "/bin/rm -f /usr/bin/install-tools.new"])
        subprocess.call(['/bin/virsh', 'x-exec', '--shell', ve_uuid, "/bin/chmod 755 /usr/bin/install-tools"])
        newf.close()

        newf = open(GUEST_MNT_POINT + "/90-guest_iso.rules")
        subprocess.call(['/bin/virsh', 'x-exec', '--shell', ve_uuid, "/bin/cat /dev/null > /etc/udev/rules.d/90-guest_iso.rules.new"])
        for l in newf.readlines():
            l = re.escape(l).rstrip()
            subprocess.call(['/bin/virsh', 'x-exec', '--shell', ve_uuid, "/bin/echo " + l + " >> /etc/udev/rules.d/90-guest_iso.rules.new"])
        subprocess.call(['/bin/virsh', 'x-exec', '--shell', ve_uuid, "/bin/cat /etc/udev/rules.d/90-guest_iso.rules.new > /etc/udev/rules.d/90-guest_iso.rules"])
        subprocess.call(['/bin/virsh', 'x-exec', '--shell', ve_uuid, "/bin/rm -f /etc/udev/rules.d/90-guest_iso.rules.new"])
        subprocess.call(['/bin/virsh', 'x-exec', '--shell', ve_uuid, "/bin/chmod 755 /usr/bin/install-tools"])
        newf.close()
        subprocess.call(['/bin/virsh', 'x-exec', '--shell', ve_uuid, "udevadm control --reload"])

        # Mainly for test - will this help to fix #PSBM-76001?
        time.sleep(5)
        log_debug("Successfully copied/updated install-tools script")
    except:
        log_debug("Smth went wrong during update")
        pass

    subprocess.call(['/bin/umount', GUEST_MNT_POINT])
    return True


def update_via_repo(ve_uuid):
    """
    Update the tools inside given VM using tools repo.
    For the supported guests, we temporary add repo with the tools corresponding
    to the host package version and the create one-time cron task to launch update from that repo

    Return True if crontask was created, False otherwise
    """

    if 'vz7' in lin_guest_ver:
        repo = 'http://repo.virtuozzo.com/vz-guest-tools-lin/vz7/' + lin_guest_ver
    elif 'vz8' in lin_guest_ver:
        repo = 'http://repo.virtuozzo.com/vz-guest-tools-lin/vz8/' + lin_guest_ver
    else:
        # If some strange (non-Vz) guest tools package is installed - just do nothing
        return False

    repo_name = 'vz-tools-' + lin_guest_ver
    # Cron task will be launch in 1 minute after we finish our work
    cron_date = subprocess.check_output(['/bin/virsh', 'x-exec', ve_uuid, 'date', '-d', '1mins', '+%M %H %d %m'])

    # Try to guess OS id / version from /etc/os/release
    os_id = None
    os_ver = None
    if subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '/bin/ls', '/etc/os-release']) == 0:
        proc = subprocess.Popen(['/bin/virsh', 'x-exec', ve_uuid, 'cat', '/etc/os-release'], stdout=subprocess.PIPE)
        for line in proc.stdout:
            if line.startswith("ID="):
                os_id = line.strip().split('=')[1].replace('"','')
            elif line.startswith("VERSION_ID="):
                os_ver = line.strip().split('=')[1].replace('"','')

    # We have three major cases - RH-like guest, Ubuntu/Debian and SUSE
    if subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '/bin/ls', '/etc/redhat-release']) == 0:
        # There was no os-releae in CentOS 6
        if not os_ver or os_ver.startswith("6"):
            repo += "/centos6"
        elif os_ver.startswith("7"):
            repo += "/centos7"
        elif os_ver.startswith("8"):
            repo += "/centos8"
        else:
            return False

        # Create repo file - we will remove it in the cron task, but disable just in case
        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '--shell', '/bin/echo "[' + repo_name + ']" > /etc/yum.repos.d/' + repo_name + '.repo'])
        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '--shell', '/bin/echo "name=Vz Guest Tools" >> /etc/yum.repos.d/' + repo_name + '.repo'])
        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '--shell', '/bin/echo "baseurl=' + repo + '" >> /etc/yum.repos.d/' + repo_name + '.repo'])
        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '--shell', '/bin/echo "enabled=0" >> /etc/yum.repos.d/' + repo_name + '.repo'])
        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '--shell', '/bin/echo "gpgcheck=0" >> /etc/yum.repos.d/' + repo_name + '.repo'])

        cmd = '/usr/bin/yum -y --disablerepo=* --enablerepo=' + repo_name + ' update > /var/log/vz_tools_update && echo ' + lin_guest_ver + '> /usr/share/qemu-ga/VERSION; '
        cmd += '/sbin/service qemu-guest-agent condrestart; '
        cmd += 'rm -f /etc/cron.d/vz_tools_up; rm -f /etc/yum.repos.d/' + repo_name + '.repo'
        cronjob = cron_date.strip() + " * root " + cmd
        log_debug("Using cronjob: " + cronjob)
        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '--shell', '/bin/echo "' + cronjob + '" > /etc/cron.d/vz_tools_up'])
    elif (os_id and ('debian' in os_id or 'ubuntu' in os_id)) or subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '/bin/ls', '/etc/debian-version']) == 0:
        if not os_ver or not os_id:
            return False
        if 'ubuntu' in os_id:
            if '14' in os_ver:
                repo_str = 'deb [trusted=yes] http://repo.virtuozzo.com/vz-guest-tools-lin/vz7/' + lin_guest_ver + '/public/ubuntu-14.04/ trusty main'
            elif '16' in os_ver:
                repo_str = 'deb [trusted=yes] http://repo.virtuozzo.com/vz-guest-tools-lin/vz7/' + lin_guest_ver + '/public/ubuntu-16.04/ xenial main'
            elif '18' in os_ver:
                repo_str = 'deb [trusted=yes] http://repo.virtuozzo.com/vz-guest-tools-lin/vz7/' + lin_guest_ver + '/public/ubuntu-18.04/ bionic main'
            else:
                repo_str = 'deb [trusted=yes] http://repo.virtuozzo.com/vz-guest-tools-lin/vz7/' + lin_guest_ver + '/public/ubuntu-20.04/ focal main'
        else:
            if '7' in os_ver:
                repo_str = 'deb [trusted=yes] http://repo.virtuozzo.com/vz-guest-tools-lin/vz7/' + lin_guest_ver + '/public/debian-7/ wheezy main'
            elif '8' in os_ver:
                repo_str = 'deb [trusted=yes] http://repo.virtuozzo.com/vz-guest-tools-lin/vz7/' + lin_guest_ver + '/public/debian-8/ jessie main'
            elif '9' in os_ver:
                repo_str = 'deb [trusted=yes] http://repo.virtuozzo.com/vz-guest-tools-lin/vz7/' + lin_guest_ver + '/public/debian-9/ squeeze main'
            elif '10' in os_ver:
                repo_str = 'deb [trusted=yes] http://repo.virtuozzo.com/vz-guest-tools-lin/vz7/' + lin_guest_ver + '/public/debian-10/ buster main'
            else:
                repo_str = 'deb [trusted=yes] http://repo.virtuozzo.com/vz-guest-tools-lin/vz7/' + lin_guest_ver + '/public/debian-11/ bullseye main'

        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '--shell', '/bin/echo "' + repo_str + '" > /etc/apt/sources.list.d/' + repo_name + '.list'])

        # Dunno how to ask apt-get to update packages from particular repo only, let's just list them
        cmd = 'DEBIAN_FRONTEND=noninteractive apt-get update > /var/log/vz_tools_update; PATH=/bin:/sbin:/usr/bin:/usr/sbin DEBIAN_FRONTEND=noninteractive apt-get -y install prl-nettool qemu-guest-agent-vz vz-guest-udev vz-guest-prl-backup >> /var/log/vz_tools_update && echo ' + lin_guest_ver + '> /usr/share/qemu-ga/VERSION; '
        cmd += '/etc/init.d/qemu-guest-agent restart; '
        cmd += 'rm -f /etc/cron.d/vz_tools_up; rm -f /etc/apt/sources.list.d/' + repo_name + '.list'
        cronjob = cron_date.strip() + " * root " + cmd
        log_debug("Using cronjob: " + cronjob)
        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '--shell', '/bin/echo "' + cronjob + '" > /etc/cron.d/vz_tools_up'])

    elif (os_id and 'suse' in os_id) or subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '/bin/ls', '/etc/SuSE-release']) == 0:
        repo += "/suse"

        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '--shell', '/bin/echo "[' + repo_name + ']" > /etc/zypp/repos.d/' + repo_name + '.repo'])
        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '--shell', '/bin/echo "name=Vz Guest Tools" >> /etc/zypp/repos.d/' + repo_name + '.repo'])
        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '--shell', '/bin/echo "baseurl=' + repo + '" >> /etc/zypp/.repos.d/' + repo_name + '.repo'])
        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '--shell', '/bin/echo "enabled=1" >> /etc/zypp/repos.d/' + repo_name + '.repo'])
        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '--shell', '/bin/echo "gpgcheck=0" >> /etc/zypp/repos.d/' + repo_name + '.repo'])

        cmd = 'zypper --non-interactive update prl_nettool qemu-guest-agent-vz vz-guest-udev vz-guest-prl_backup > /var/log/vz_tools_update && echo ' + lin_guest_ver + ' > /usr/share/qemu-ga/VERSION; '
        cmd += '/sbin/service qemu-guest-agent condrestart; '
        cmd += 'rm -f /etc/cron.d/vz_tools_up; rm -f /etc/zypp/repos.d/' + repo_name + '.repo'
        cronjob = cron_date.strip() + " * root " + cmd
        log_debug("Using cronjob: " + cronjob)
        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '--shell', '/bin/echo "' + cronjob + '" > /etc/cron.d/vz_tools_up'])

    return True


def update_exec(ve, ve_db, ve_uuid, linux_skip_run):
    """
    Update guest tools inside VM using direct execution of necessary actions
    through 'virsh exec'
    """
    guest_dev = ""
    if ve_db[ve_uuid]['type'] == 'Linux':
        log_debug("Looking for cd device in Lin guest")
        # Let guest some time to detect a new device
        time.sleep(5)
        proc = subprocess.Popen(['/bin/virsh', 'x-exec', ve_uuid, 'blkid'], stdout=subprocess.PIPE)
        for line in proc.stdout:
            if "vz-tools-lin" in line:
                guest_dev = line.split(":")[0]
                break
        if not guest_dev:
            log_debug("Failed to detect guest cd")
            return False
        log_debug("Detected: " + guest_dev)
        tmpdir = subprocess.check_output(['/bin/virsh', 'x-exec', ve_uuid, '/bin/mktemp', '-d'])
        log_debug("Will mount to temp dir " + tmpdir.rstrip())
        if subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '/bin/mount', guest_dev, tmpdir.rstrip()]) != 0:
            log_debug("Failed to mount guest cd")
            return False
        log_debug("Copying install-tools...")
        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '/bin/cp', '-f', tmpdir.rstrip() + "/install-tools", "/usr/bin"])
        if not linux_skip_run:
            log_debug("Launching install-tools...")
            subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, "/usr/bin/install-tools"])
        else:
            if subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '/bin/umount', guest_dev]) != 0:
                log_debug("Failed to umount guest cd")
                return False
        log_debug("Done!")
    elif ve_db[ve_uuid]['type'] == 'Windows':
        log_debug("Looking for cd device in Win guest")
        # Iterate through all possible drive letters
        for drive in ascii_uppercase:
            proc = subprocess.Popen(['/bin/virsh', 'x-exec', ve_uuid, 'vol', drive], stdout=subprocess.PIPE)
            for line in proc.stdout:
                if "vz-tools-win" in line:
                    guest_dev = drive
                    break
            if guest_dev:
                break
        if not guest_dev:
            return False
        log_debug("Detected: " + guest_dev)
        subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, guest_dev + ":/setup.exe"])

    return True


def disconnect_tools_cd(conn, ve, ve_uuid):
    """
    Disconnect iso with guest tools from VM if connected.
    If iso is already connected then 'installtools' does nothing, we have
    no events inside VM guest so update is not triggered.

    We have to use libvirt API here since our SDK "hides" guest tools CD
    """
    uuid = ve_uuid.replace("{", "").replace("}","")
    try:
        dom = conn.lookupByUUIDString(uuid)
    except:
        log_debug('Failed to find the domain ' + ve_uuid)
        return False
    if dom is None:
        log_debug('Failed to find the domain ' + ve_uuid)
        return False

    raw_xml = dom.XMLDesc(0)
    xml = minidom.parseString(raw_xml)
    diskTypes = xml.getElementsByTagName('disk')
    iso_found = False
    for diskType in diskTypes:
        if diskType.getAttribute('type') != 'file' or diskType.getAttribute('device') != 'cdrom':
            continue
        diskNodes = diskType.childNodes
        for diskNode in diskNodes:
            if diskNode.nodeName != 'source' and not iso_found:
                continue
            if not iso_found:
                if not 'file' in diskNode.attributes.keys():
                    continue
                if diskNode.getAttribute('file').endswith('vz-guest-tools-lin.iso') or diskNode.getAttribute('file').endswith('vz-guest-tools-win.iso'):
                    iso_found = True
                continue
            if diskNode.nodeName == 'target':
                iso_dev = diskNode.getAttribute('dev')
                break
        if iso_found:
            if not iso_dev:
                log_debug("Found connected guest tools CD, but failed to connect its parameters")
                return False
            break

    if not iso_found:
        # Guest tools cd is not mounted yet
        return True

    log_debug("Will disconnect " + iso_dev)
    # Dunno how to do this using Python API
    if subprocess.call(['/bin/virsh', 'change-media', '--eject', '--force', uuid, iso_dev]) != 0:
        log_debug("Failed to eject media using virsh")
        return False

    time.sleep(5)
    return True


def check_update(ve_names):
    """
    Check if tools in given VEs are up-to-date
    """
    ves = _server.get_vm_list_ex(nFlags=pc.PVTF_VM).wait()
    for ve in ves:
        ve_uuid = ve.get_uuid().lstrip('{').rstrip('}')
        if ve_uuid not in ve_names and ve.get_name() not in ve_names:
            continue
        try:
            conf = ve.get_config()
            vm_info = ve.get_vm_info()
            ve_desc = ve_uuid + " (" + ve.get_name() + ")"
            ve_type = prlsdkapi.call_sdk_function('PrlApi_GuestToString', conf.get_os_type())
        except:
            log_debug('Fail to get VM info for %s' % ve_uuid)
            continue
        if ve_type not in SUPPORTED_GUESTS:
            print('Unsupported guest type in %s' % ve_desc)
            continue

        try:
            r = ve.get_tools_state().wait()
        except:
            print('%s: Failed to get tools info' % ve_desc)
            continue

        if r.get_params_count() == 0:
            print('%s: Tools are not installed' % ve_desc)
            continue

        tools_info = r.get_param()
        current_ver = tools_info.get_version()
        new_ver = get_actual_tools_ver(ve_type)

        if not current_ver:
            print('%s: Tools are not installed' % ve_desc)
            continue

        if not check_tools_ver(current_ver):
            log_debug('Strange tools detected')
            code = subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '/bin/ls', '/'])
            if code == 255:
                print('%s: Tools are reported to be installed but behaves incorrectly' % ve_desc)
                continue

        if not current_ver:
            print('%s: Tools are not installed' % ve_desc)
            continue

        if current_ver == new_ver:
            print('%s: Tools are up to date' % ve_desc)
        else:
            print('%s: Tools are outdated (%s vs %s)' % (ve_desc, current_ver, new_ver))


def process_ve(ve, ve_db, conn, force=False):
    """
    Check if tools should be updated in a given VE and trigger update, if yes
    'Force' argument forces tools update for VE with 'auto_update' set to false
    Return True if update was triggered, False otherwise
    """
    ve_uuid = ve.get_uuid().lstrip('{').rstrip('}')
    log_debug('Processing %s ' % (ve_uuid))
    conf = ve.get_config()
    # Just for sure, check that user didn't turn off autoupdate
    if not force and not conf.is_tools_auto_update_enabled():
        log_debug('Autoupdate disabled, skipping')
        return False
    # Check that VM is running

    vm_info = ve.get_vm_info()

    # It is possible that we don't have this VM into the db
    # if VM was specified in command line explicitly
    # or have not 'type' set (e.g., if autoupdate was disabled during 'analyze' phase)
    if ve_uuid not in ve_db or 'type' not in ve_db[ve_uuid]:
        ve_db[ve_uuid] = {}
        ve_db[ve_uuid]['type'] = prlsdkapi.call_sdk_function('PrlApi_GuestToString', conf.get_os_type())
        if ve_db[ve_uuid]['type'] not in SUPPORTED_GUESTS:
            log_debug('Unsupported guest type')
            return False

        tools_ver = ""
        if vm_info.get_state() in flags_running:
            try:
                r = ve.get_tools_state().wait()
                pcount = r.get_params_count() 
            except:
                log_debug('Failed to get tools info')
                return False
        else: 
            pcount = 0

        if pcount > 0:
            tools_info = r.get_param()
            tools_ver = tools_info.get_version()
            if tools_ver and not check_tools_ver(tools_ver):
                log_debug('Strange tools detected')
                code = subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '/bin/ls', '/'])
                if code == 255:
                    log_debug('Will try to install our tools from scratch')
                    tools_ver = None

        if pcount == 0 or not tools_ver:
            ve_db[ve_uuid]['need_update'] = True
            log_debug('Tools are not installed, will try to install from scratch')
        ve_db[ve_uuid]['current'] = tools_ver

    if 'current' in ve_db[ve_uuid] and ve_db[ve_uuid]['current'] and not check_tools_ver(ve_db[ve_uuid]['current']):
        log_debug('Strange tools detected')
        code = subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '/bin/ls', '/'])
        if code == 255:
            log_debug('Will try to install our tools from scratch')
            ve_db[ve_uuid]['current'] = None

    if 'current' not in ve_db[ve_uuid] or not ve_db[ve_uuid]['current']:
        if not install_tools_enabled():
            log_debug('Tools installation is disabled by config, skipping')
            return False
        if vm_info.get_state() in flags_running:
            log_debug('VM is running, cannot install tools from scratch, skipping')
            return False
        try:
            res = install_tools(ve, conn)
        except:
            res = False
        return res

    if vm_info.get_state() not in flags_running:
        log_debug('VM is not running, skipping')
        return False

    log_debug("Update has been triggered!")
    # JSON record for the CEP collector
    new_ver = get_actual_tools_ver(ve_db[ve_uuid]['type'])
    syslog.syslog('{"name": "ToolsAutoUpdate", "actors": {"VEs": [{"%s"}]}, "start_time": "%s", "from_version": "%s", "to_version": "%s"}' \
                    % (ve.get_uuid(), time.time(), ve_db[ve_uuid]['current'], new_ver))

    # If there was no failed update attempt for VM and we can access our repo from inside it, then try
    # to update from repo
    if ve_db[ve_uuid]['type'] == 'Linux' and ('failed' not in ve_db[ve_uuid] or not ve_db[ve_uuid]['failed']) and subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, 'ping', '-c1', 'repo.virtuozzo.com']) == 0:
        log_debug('Triggering update using repo')
        if update_via_repo(ve_uuid):
            return True

    # Check and fix missing install-tools file bug (u3 legacy, Linux guests only)
    if ve_db[ve_uuid]['type'] == 'Linux':
        if subprocess.call(['/bin/virsh', 'x-exec', ve_uuid, '/bin/ls', '/usr/bin/install-tools']) != 0:
            # File is missing - copy it from CD
            log_debug('install-tools is missing, copying it to guest')
            update_udev_script(ve_uuid)
        else:
            try:
                log_debug('updating install-tools script')
                update_udev_script(ve_uuid)
            except:
                pass

    # Disconnect tools iso, if connected, otherwise 'installtools'
    # will have no effect
    disconnect_tools_cd(conn, ve, ve_uuid)

    # We need to call installtools in either case
    ve.install_tools()
    bad_guest =  check_bad_udev_guest(ve_uuid, ve_db)
    if 'failed' in ve_db[ve_uuid] and ve_db[ve_uuid]['failed'] != "" or bad_guest:
        # If update was failed once, then likely installtools by itself is not enough
        # let's try to launch update through exec
        if 'failed' in ve_db[ve_uuid] and ve_db[ve_uuid]['failed'] != 'once' and not force and not bad_guest:
            log_debug('Unexpected value of "failed", refusing to trigger update')
            return False
        else:
            log_debug('Triggering update using exec')
            try:
                update_exec(ve, ve_db, ve_uuid, False)
                ve_db[ve_uuid]['failed'] = ""
            except:
                log_debug('Failed to start update - possibly update is already in progress or the tools are not installed correctly')
    return True


def guestfs_log_cb(event, event_handle, buf, array):
    """
    Callback for guestfs events to save messages to syslog
    """
    msg = "Guestfs: " + str(event)
    if buf:
        msg += " "
        msg += str(buf)
    if array:
        msg += " "
        msg += str(array)
    log_debug(msg)


def install_tools(ve, conn):
    """
    Install tools to VM which is completely missing them
    We use libguestfs to put content of guest tools iso into VM
    and configure cron to launch installer on reboot.
    """
    ve_uuid = ve.get_uuid().lstrip('{').rstrip('}')
    guest = guestfs.GuestFS()
    if DEBUG:
        guest.set_trace(1)
        guest.set_verbose(1)
#        guest.set_event_callback(guestfs_log_cb, guestfs.EVENT_TRACE|guestfs.EVENT_APPLIANCE|guestfs.EVENT_LIBRARY|guestfs.EVENT_ENTER)

    uuid = ve_uuid.replace("{", "").replace("}","")
    dom = conn.lookupByUUIDString(uuid)
    if (dom.isActive() != 0):
        syslog.syslog('Failed to install tools to "%s": VM is running' % (ve_uuid))
        return False

    guest.add_libvirt_dom(dom, readonlydisk = "ignore")
    guest.launch()

    roots = []
    try:
        roots = guest.inspect_os()
    except:
        syslog.syslog('Failed to use guestfs to inspect "%s"' % (ve_uuid))
        return False

    if len(roots) == 0:
        syslog.syslog('Failed to install tools to "%s": inspect_vm: no operating systems found' % (ve_uuid))
        return False

    if not os.path.exists(GUEST_MNT_POINT):
        os.mkdir(GUEST_MNT_POINT)
    else:
        subprocess.call(['/bin/umount', GUEST_MNT_POINT])

    is_windows = False
    guest_iso = GUEST_ISO_LIN
    for root in roots:
        if guest.inspect_get_type(root) == 'windows':
            is_windows = True
            guest_iso = GUEST_ISO_WIN
            break

    if subprocess.call(['/bin/mount', '-o', 'ro,loop', guest_iso, GUEST_MNT_POINT]) != 0:
        log_debug("Failed to mount guest cd on host")
        return False

    for root in roots:
        guest.mount(root, '/')
        if is_windows:
            sysroot = guest.inspect_get_windows_systemroot(root)
            win_drive = filter(lambda x: x[1] == root, guest.inspect_get_drive_mappings(root))[0][0]
            win_sysroot = win_drive + ':' + sysroot.replace('/', '\\')
            guest.upload(GUEST_MNT_POINT + '/setup.exe', guest.case_sensitive_path(sysroot + '/vz-install-tools.exe'))

            # Need to free the handles to use virt-sysprep
            guest.sync()
            guest.umount('/')
            guest.shutdown()

            (fd, fpath) = tempfile.mkstemp(suffix = '.cmd', prefix = 'vz-guest-tools-install-')
            os.write(fd, "@echo off\r\n%(root)s\\vz-install-tools.exe\r\ndel /q %(root)s\\vz-install-tools.exe\r\n" % {'root': win_sysroot})
            os.close(fd)
            retcode = subprocess.call(['/bin/virt-sysprep', '-d', uuid, '--firstboot', fpath])
            os.unlink(fpath)
            if retcode != 0:
                log_debug('Failed to call virt-sysprep, returned code: %d' % retcode)
            ve.install_tools()
        else:
            # We expect a partition with /etc and /var/lib folders
            # We also need /etc/crontab since we use it to initiate tools installation on boot
            if not guest.exists('/etc') or not guest.exists('/var/lib'):
                guest.sync()
                guest.umount('/')
                continue
            if not guest.exists('/etc'):
                log_debug("Can't find /etc/crontab...")
                guest.sync()
                guest.umount('/')
                continue

            # Check for supported guest version
            supported_guest = False
            try:
                os_release = guest.cat('/etc/os-release')
                for distr in os_release.split("\n"):
                    if re.match("(ID=altlinux|ID=fedora|ID=virtuozzo|ID=centos|ID=rhel|ID=debian|ID=ubuntu|ID=opensuse|ID=sles|ID=cloudlinux)", distr.replace('"','')):
                        supported_guest = True
                        break
                if not supported_guest:
                    log_debug("Guest system is not supported by tools")
                    return False
            except:
                pass

            try:
                redhat_release = guest.cat('/etc/redhat-release')
                for distr in redhat_release.split("\n"):
                    rel = re.findall(r'release (\d+)', distr)
                    if float(rel[0]) < 6:
                        log_debug("CentOS-based systems with version %s are not supported by guest tools" % rel[0])
                        return False
                    supported_guest = True
                    break
            except:
                pass

            try:
                debian_version = guest.cat('/etc/debian_version')
                supported_guest = True
#                for distr in debian_version:
#                    ver = re.findall(r'\d+', distr)
#                    if int(ver[0]) < 7:
#                        log_debug("Debian %s not suported" %(ver[0]))
#                        return False
            except:
                pass

            if not supported_guest:
                log_debug("Guest system is not supported by tools - can't get system type and version")
                return False

            INTERNAL_GUEST_TOOLS_DIR = "/var/lib/vz-guest-tools"
            if not guest.exists(INTERNAL_GUEST_TOOLS_DIR):
                guest.mkdir(INTERNAL_GUEST_TOOLS_DIR)

            for root, dirs, files in os.walk(GUEST_MNT_POINT):
                g_root = root.replace(GUEST_MNT_POINT, INTERNAL_GUEST_TOOLS_DIR)
                for d in dirs:
                    if not guest.exists(g_root + "/" + d):
                        guest.mkdir(g_root + "/" + d)
                for f in files:
                    guest.upload(root + "/" + f, g_root + "/" + f)

            # Cron one-liner to launch install script on reboot; one-liner drops itself at the end from crontab
            # independently of installation result, to avoid unexpected launch on the next boot.
            script = guest.cat('/etc/crontab')
            script = script + "\n@reboot  root cd %s && bash ./install && cd /var/lib && rm -rf %s ; sed -i '/vz-guest-tools/d' /etc/crontab\n" \
                    % (INTERNAL_GUEST_TOOLS_DIR, INTERNAL_GUEST_TOOLS_DIR)
            guest.write_file('/etc/crontab', script, 0)
            guest.sync()
            guest.umount('/')
            guest.shutdown()


def print_help():
    prog_name = sys.argv[0]
    print("usage: %s [-h|--help] [-d|--debug] [--analyze|vm1, vm2, ...|--get-state vm1, vm2, ...]\n" % prog_name)
    print("%s - a tool for automated update of guest tools inside Virtual Machines.\n" % prog_name)
    print("If launched with '--analyze' option, the tool analyzes the state of every VM ")
    print("  registered in the system with 'GuestTools autoupdate' parameter set to 'on' and chooses VMs ")
    print("  with outdated guest tools. This information is stored in %s file.\n" % VE_DB)
    print("If launched without any arguments, %s triggers guest tools update in VMs" % prog_name)
    print("  from the %s list. " % VE_DB)
    print("  Maximum number of VMs where update can be triggered during a single guest tools updater")
    print("  invocation is limited by 'MaxVMs' parameter in the /etc/vz/tools-update.conf configuration file.\n")
    print("Alternatively, one can explicitly specify names or UUIDs of VMs where guest tools update ")
    print("  should be triggered. In this case, update is launched in all specified VMs at once regardless")
    print("  of their parameters and version of installed tools.\n")
    print("--get-state option can be used to check if guest tools are up-to-date in given VMs\n")


def get_lock(f):
    """
    It is possible that we are launched in parallel
    """
    locked = False
    waiting = 0
    while waiting < LOCK_TIMEOUT:
        try:
            fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
            locked = True
            break
        except:
            log_debug("Waiting for a lock...")
            time.sleep(LOCK_WAIT)
            waiting += LOCK_WAIT

    if not locked:
        print("Failed to obtain lock, exiting...")
        sys.exit(1)


def main():
    global DEBUG

    try:
        f = open(LOCKFILE, 'w')
    except:
        print("Failed to obtain lock, exiting...")
        sys.exit(1)
    get_lock(f)
    ve_names = []
    # For manually specified VEs, we will force update even if
    # auto_update parameter is set to False
    force_update = False
    if len(sys.argv) > 1:
        if sys.argv[1] == "--analyze":
            if "-d" in sys.argv or "--debug" in sys.argv:
                DEBUG = True
            analyze_ves()
            sys.exit(0)
        if sys.argv[1] == "--get-state":
            if len(sys.argv) <= 2:
                print_help()
                sys.exit(0)
            ve_names = sys.argv[2:]
            check_update(ve_names)
            sys.exit(0)
        if sys.argv[1] in ["-h", "--help"]:
            print_help()
            sys.exit(0)
        if sys.argv[1] in ["-d", "--debug"]:
            DEBUG = True
            if len(sys.argv) > 2:
                ve_names = sys.argv[2:]
            else:
                try:
                    ve_names = get_ves_to_update()
                except:
                    log_debug('Failed to get VMs for update')
                    ve_names = []
        else:
            ve_names = sys.argv[1:]
        force_update = True
    else:
        try:
            ve_names = get_ves_to_update()
        except:
            log_debug('Failed to get VMs for update')
            ve_names = []

    if len(ve_names) == 0:
        sys.exit(0)

    try:
        ves = _server.get_vm_list_ex(nFlags=pc.PVTF_VM).wait()
    except:
        log_debug('Failed to get list of active VMs')
        sys.exit(0)

    max_vms = get_max_vms_per_time()
    processed_vms = 0

    try:
        ve_db = json.load(open(VE_DB))
    except:
        ve_db = {}

    try:
        conn = libvirt.open('qemu:///system')
    except:
        print("Failed to open connection to qemu:///system")
        sys.exit(1)

    if conn is None:
        print("Failed to open connection to qemu:///system")
        sys.exit(1)

    for ve in ves:
        if processed_vms >= max_vms:
            log_debug("Reached max number of VMs to be processed per launch!")
            break

        ve_uuid = ve.get_uuid().lstrip('{').rstrip('}')
        if ve_uuid not in ve_names and ve.get_name() not in ve_names:
            continue
        if not process_ve(ve, ve_db, conn, force_update):
            continue
        ve_db[ve_uuid]['last_update_from'] = ve_db[ve_uuid]['current']
        new_ver = get_actual_tools_ver(ve_db[ve_uuid]['type'])
        ve_db[ve_uuid]['last_update_to'] = new_ver
        ve_db[ve_uuid]['need_update'] = False

        processed_vms += 1

    conn.close()
    open(VE_DB, 'w').write(json.dumps(ve_db, indent = 2))

    fcntl.flock(f, fcntl.LOCK_UN)


if __name__ == '__main__':
    main()
