#!/usr/bin/python

#
# Copyright (c) 2017-2019 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
import libvirt
from xml.dom import minidom
import os
import re
import guestfs
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:
    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)

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

'''
In some guests our udev magic doesn't work
'''
#def check_bad_udev_guest(ve_uuid, tools_ver):
def check_bad_udev_guest(ve_uuid, ve_db):
    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/prlctl', '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/prlctl', '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")
            return True
    except:
        log_debug("Centos check failed")
        pass

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

    return False

'''
'Heavy' function - analyze all VMs and decide which of them should be updated
'''
def analyze_ves():
    # 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
    ves = _server.get_vm_list_ex(nFlags=pc.PVTF_VM).wait()

    processed_ves = []
    for ve in ves:
        ve_uuid = ve.get_uuid()
        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 'vz7' not in tools_ver and 'rv7' not in tools_ver: 
                log_debug('Strange tools detected')
                code = subprocess.call(['/bin/prlctl', 'exec', ve_uuid, '/bin/ls', '/'])
                if subprocess.call(['/bin/prlctl', 'exec', ve_uuid, '/bin/ls', '/']) == 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 deisabled')
                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
                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))

'''
Read the database and return list of VMs to be updated
'''
def get_ves_to_update():
    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

'''
Get maximum number of VMs that can be processed at once
'''
def get_max_vms_per_time():
    # 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

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

'''
Update install-tools script inside the guest before connecting CD with guest tools
In old guest tools, that script could be killed by timeout.
'''
def update_udev_script(ve_uuid):
    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/prlctl', 'exec', ve_uuid, "/bin/cat /dev/null > /usr/bin/install-tools.new"])
        for l in newf.readlines():
            l = re.escape(l).rstrip()
            subprocess.call(['/bin/prlctl', 'exec', ve_uuid, "/bin/echo " + l + " >> /usr/bin/install-tools.new"])
        subprocess.call(['/bin/prlctl', 'exec', ve_uuid, "/bin/cat /usr/bin/install-tools.new > /usr/bin/install-tools"])
        subprocess.call(['/bin/prlctl', 'exec', ve_uuid, "/bin/rm -f /usr/bin/install-tools.new"])
        subprocess.call(['/bin/prlctl', 'exec', ve_uuid, "/bin/chmod 755 /usr/bin/install-tools"])
        newf.close()

        newf = open(GUEST_MNT_POINT + "/90-guest_iso.rules")
        subprocess.call(['/bin/prlctl', 'exec', 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/prlctl', 'exec', ve_uuid, "/bin/echo " + l + " >> /etc/udev/rules.d/90-guest_iso.rules.new"])
        subprocess.call(['/bin/prlctl', 'exec', ve_uuid, "/bin/cat /etc/udev/rules.d/90-guest_iso.rules.new > /etc/udev/rules.d/90-guest_iso.rules"])
        subprocess.call(['/bin/prlctl', 'exec', ve_uuid, "/bin/rm -f /etc/udev/rules.d/90-guest_iso.rules.new"])
        subprocess.call(['/bin/prlctl', 'exec', ve_uuid, "/bin/chmod 755 /usr/bin/install-tools"])
        newf.close()
        subprocess.call(['/bin/prlctl', 'exec', 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

'''
Update guest tools inside VM using direct execution of necessary actions
through 'prlctl exec'
'''
def update_exec(ve, ve_db, ve_uuid, linux_skip_run):
    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/prlctl', '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/prlctl', 'exec', ve_uuid, '/bin/mktemp', '-d'])
        log_debug("Will mount to temp dir " + tmpdir.rstrip())
        if subprocess.call(['/bin/prlctl', '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/prlctl', '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/prlctl', 'exec', ve_uuid, "/usr/bin/install-tools"])
        else:
            if subprocess.call(['/bin/prlctl', '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/prlctl', '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/prlctl', 'exec', ve_uuid, guest_dev + ":/setup.exe"])

    return True

'''
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
'''
def disconnect_tools_cd(conn, ve, ve_uuid):
    uuid = ve_uuid.replace("{", "").replace("}","")
    try:
        dom = conn.lookupByUUIDString(uuid)
    except:
        log_debug('Failed to find the domain ' + ve_uuid)
        return False
    if dom == 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 not diskNode.getAttribute('file').endswith('vz-guest-tools-lin.iso'):
                    continue
                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

'''
Check if tools in given VEs are up-to-date
'''
def check_update(ve_names):
    ves = _server.get_vm_list_ex(nFlags=pc.PVTF_VM).wait()
    for ve in ves:
        ve_uuid = ve.get_uuid()
        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 'vz7' not in current_ver and 'rv7' not in current_ver:
            log_debug('Strange tools detected')
            code = subprocess.call(['/bin/prlctl', 'exec', ve_uuid, '/bin/ls', '/'])
            if subprocess.call(['/bin/prlctl', 'exec', ve_uuid, '/bin/ls', '/']) == 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))


'''
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
'''
def process_ve(ve, ve_db, conn, force=False):
    ve_uuid = ve.get_uuid()
    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 'vz7' not in tools_ver and 'rv7' not in tools_ver:
                log_debug('Strange tools detected')
                code = subprocess.call(['/bin/prlctl', 'exec', ve_uuid, '/bin/ls', '/'])
                if subprocess.call(['/bin/prlctl', 'exec', ve_uuid, '/bin/ls', '/']) == 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 'vz7' not in ve_db[ve_uuid]['current'] and 'rv7' not in ve_db[ve_uuid]['current']:
        log_debug('Strange tools detected')
        code = subprocess.call(['/bin/prlctl', 'exec', ve_uuid, '/bin/ls', '/'])
        if subprocess.call(['/bin/prlctl', 'exec', ve_uuid, '/bin/ls', '/']) == 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))

    # Check and fix missing install-tools file bug (u3 legacy, Linux guests only)
    if ve_db[ve_uuid]['type'] == 'Linux':
        if subprocess.call(['/bin/prlctl', '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


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


'''
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.
'''
def install_tools(ve, conn):
    ve_uuid = ve.get_uuid()
    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_error('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" %(ver[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")

'''
It is possible that we are launched in parallel
'''
def get_lock(f):
    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)


if __name__ == '__main__':
    f = open(LOCKFILE, 'w')
    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:
                ve_names = get_ves_to_update()
        else:
            ve_names = sys.argv[1:]
        force_update = True
    else:
        ve_names = get_ves_to_update()

    if len(ve_names) == 0:
        sys.exit(0)
    ves = _server.get_vm_list_ex(nFlags=pc.PVTF_VM).wait()

    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 == 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() 
        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)

