#!/usr/bin/python
#
# AbstractPatching is the base patching class of all the linux distros
#
# Copyright 2014 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 os
import sys
import re
import json
import random
import shutil
import time
import datetime
import logging
import logging.handlers

from Utils.WAAgentUtil import waagent
from ConfigOptions import ConfigOptions

mfile = os.path.join(os.getcwd(), 'HandlerManifest.json')
with open(mfile,'r') as f:
    manifest = json.loads(f.read())[0]
    Version = manifest['version']

StatusTest = {
    "Scheduled" : {
        "Idle" : None,
        "Healthy" : None
    },
    "Oneoff" : {
        "Idle" : None,
        "Healthy" : None
    }
}

try:
    from scheduled.idleTest import is_vm_idle
    StatusTest["Scheduled"]["Idle"] = is_vm_idle
except:
    pass

try:
    from oneoff.idleTest import is_vm_idle
    StatusTest["Oneoff"]["Idle"] = is_vm_idle
except:
    pass

try:
    from scheduled.healthyTest import is_vm_healthy
    StatusTest["Scheduled"]["Healthy"] = is_vm_healthy
except:
    pass

try:
    from oneoff.healthyTest import is_vm_healthy
    StatusTest["Oneoff"]["Healthy"] = is_vm_healthy
except:
    pass


class AbstractPatching(object):
    """
    AbstractPatching defines a skeleton neccesary for a concrete Patching class.
    """
    def __init__(self, hutil):
        self.hutil = hutil
        self.syslogger = None

        self.patched = []
        self.to_patch = []
        self.downloaded = []
        self.download_retry_queue = []

        # Patching Configuration
        self.disabled = None
        self.stop = None
        self.reboot_after_patch = None
        self.category = None
        self.install_duration = None
        self.oneoff = None
        self.interval_of_weeks = None
        self.day_of_week = None
        self.start_time = None
        self.download_time = None
        self.download_duration = 3600
        self.gap_between_stage = 60
        self.current_configs = dict()

        self.category_required = ConfigOptions.category["required"]
        self.category_all = ConfigOptions.category["all"]

        # Crontab Variables
        self.crontab = '/etc/crontab'
        self.cron_restart_cmd = 'service cron restart'
        self.cron_chkconfig_cmd = 'chkconfig cron on'

        # Path Variables
        self.cwd = os.getcwd()
        self.package_downloaded_path = os.path.join(self.cwd, 'package.downloaded')
        self.package_patched_path = os.path.join(self.cwd, 'package.patched')
        self.stop_flag_path = os.path.join(self.cwd, 'StopOSPatching')
        self.history_scheduled = os.path.join(self.cwd, 'scheduled/history')
        self.scheduled_configs_file = os.path.join(self.cwd, 'scheduled/configs')
        self.dist_upgrade_list = None
        self.dist_upgrade_list_key = 'distUpgradeList'
        self.dist_upgrade_all = False
        self.dist_upgrade_all_key = 'distUpgradeAll'

        # Reboot Requirements
        self.reboot_required = False
        self.open_deleted_files_before = list()
        self.open_deleted_files_after = list()
        self.needs_restart = list()

    def is_string_none_or_empty(self, str):
        if str is None or len(str) < 1:
            return True
        return False
    
    def parse_settings(self, settings):
        disabled = settings.get("disabled")
        if disabled is None or str(disabled).lower() not in ConfigOptions.disabled:
            msg = "The value of parameter \"disabled\" is empty or invalid. Set it False by default."
            self.log_and_syslog(logging.WARNING, msg)
            self.disabled = False
        else:
            if str(disabled).lower() == "true":
                self.disabled = True
            else:
                self.disabled = False
        self.current_configs["disabled"] = str(self.disabled)
        if self.disabled:
            msg = "The extension is disabled."
            self.log_and_syslog(logging.WARNING, msg)
            return

        stop = settings.get("stop")
        if stop is None or str(stop).lower() not in ConfigOptions.stop:
            msg = "The value of parameter \"stop\" is empty or invalid. Set it False by default."
            self.log_and_syslog(logging.WARNING, msg)
            self.stop = False
        else:
            if str(stop).lower() == 'true':
                self.stop = True
            else:
                self.stop = False
        self.current_configs["stop"] = str(self.stop)

        reboot_after_patch = settings.get("rebootAfterPatch")
        if reboot_after_patch is None or reboot_after_patch.lower() not in ConfigOptions.reboot_after_patch:
            msg = "The value of parameter \"rebootAfterPatch\" is empty or invalid. Set it \"rebootifneed\" by default."
            self.log_and_syslog(logging.WARNING, msg)
            self.reboot_after_patch = ConfigOptions.reboot_after_patch[0]
        else:
            self.reboot_after_patch = reboot_after_patch.lower()
        waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                  op=waagent.WALAEventOperation.Enable,
                                  isSuccess=True,
                                  version=Version,
                                  message="rebootAfterPatch="+self.reboot_after_patch)
        self.current_configs["rebootAfterPatch"] = self.reboot_after_patch

        category = settings.get('category')
        if category is None or category.lower() not in ConfigOptions.category.values():
            msg = "The value of parameter \"category\" is empty or invalid. Set it " + self.category_required + " by default."
            self.log_and_syslog(logging.WARNING, msg)
            self.category = self.category_required
        else:
            self.category = category.lower()
        waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                  op=waagent.WALAEventOperation.Enable,
                                  isSuccess=True,
                                  version=Version,
                                  message="category="+self.category)
        self.current_configs["category"] =  self.category
        
        self.dist_upgrade_list = settings.get(self.dist_upgrade_list_key)
        if not self.is_string_none_or_empty(self.dist_upgrade_list):
            self.current_configs[self.dist_upgrade_list_key] = self.dist_upgrade_list

        dist_upgrade_all = settings.get(self.dist_upgrade_all_key)
        if dist_upgrade_all is None:
            msg = "The value of parameter \"{0}\" is empty or invalid. Set it false by default.".format(self.dist_upgrade_all_key)
            self.log_and_syslog(logging.INFO, msg)
            self.dist_upgrade_all = False
        elif str(dist_upgrade_all).lower() == 'true':
            self.dist_upgrade_all = True
        else:
            self.dist_upgrade_all = False
        self.current_configs[self.dist_upgrade_all_key] = str(self.dist_upgrade_all)
        
        check_hrmin = re.compile(r'^[0-9]{1,2}:[0-9]{1,2}$')
        install_duration = settings.get('installDuration')
        if install_duration is None or not re.match(check_hrmin, install_duration):
            msg = "The value of parameter \"installDuration\" is empty or invalid. Set it 1 hour by default."
            self.log_and_syslog(logging.WARNING, msg)
            self.install_duration = 3600
            self.current_configs["installDuration"] = "01:00"
        else:
            hr_min = install_duration.split(':')
            self.install_duration = int(hr_min[0]) * 3600 + int(hr_min[1]) * 60
            self.current_configs["installDuration"] = install_duration
        if self.install_duration <= 300:
            msg = "The value of parameter \"installDuration\" is smaller than 5 minutes. The extension will not reserve 5 minutes for reboot. It is recommended to set \"installDuration\" more than 30 minutes."
            self.log_and_syslog(logging.WARNING, msg)
        else:
            msg = "The extension will reserve 5 minutes for reboot."
            # 5 min for reboot
            self.install_duration -= 300
            self.log_and_syslog(logging.INFO, msg)

        # The parameter "downloadDuration" is not exposed to users. So there's no log.
        download_duration = settings.get('downloadDuration')
        if download_duration is not None and re.match(check_hrmin, download_duration):
            hr_min = download_duration.split(':')
            self.download_duration = int(hr_min[0]) * 3600 + int(hr_min[1]) * 60

        oneoff = settings.get('oneoff')
        if oneoff is None or str(oneoff).lower() not in ConfigOptions.oneoff:
            msg = "The value of parameter \"oneoff\" is empty or invalid. Set it False by default."
            self.log_and_syslog(logging.WARNING, msg)
            self.oneoff = False
        else:
            if str(oneoff).lower() == "true":
                self.oneoff = True
                msg = "The extension will run in one-off mode."
            else:
                self.oneoff = False
                msg = "The extension will run in scheduled task mode."
            self.log_and_syslog(logging.INFO, msg)
        self.current_configs["oneoff"] = str(self.oneoff)

        if not self.oneoff:
            start_time = settings.get('startTime')
            if start_time is None or not re.match(check_hrmin, start_time):
                msg = "The parameter \"startTime\" is empty or invalid. It defaults to 03:00."
                self.log_and_syslog(logging.WARNING, msg)
                start_time = "03:00"
            try:
                start_time_dt = datetime.datetime.strptime(start_time, '%H:%M')
                self.start_time = datetime.time(start_time_dt.hour, start_time_dt.minute)
            except ValueError:
                msg = "The parameter \"startTime\" is invalid. It defaults to 03:00."
                self.log_and_syslog(logging.WARNING, msg)
                self.start_time = datetime.time(3)
            download_time_dt = start_time_dt - datetime.timedelta(seconds=self.download_duration)
            self.download_time = datetime.time(download_time_dt.hour, download_time_dt.minute)
            self.current_configs["startTime"] = start_time
 
            day_of_week = settings.get("dayOfWeek")
            if day_of_week is None or day_of_week == "":
                msg = "The parameter \"dayOfWeek\" is empty. dayOfWeek defaults to Everyday."
                self.log_and_syslog(logging.WARNING, msg)
                day_of_week = "everyday"
                self.day_of_week = ConfigOptions.day_of_week["everyday"]
            else:
                for day in day_of_week.split('|'):
                    day = day.strip().lower()
                    if day not in ConfigOptions.day_of_week:
                        msg = "The parameter \"dayOfWeek\" is invalid. dayOfWeek defaults to Everyday."
                        self.log_and_syslog(logging.WARNING, msg)
                        day_of_week = "everyday"
                        break
                if "everyday" in day_of_week:
                    self.day_of_week = ConfigOptions.day_of_week["everyday"]
                else:
                    self.day_of_week = [ConfigOptions.day_of_week[day.strip().lower()] for day in day_of_week.split('|')]
            waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                      op=waagent.WALAEventOperation.Enable,
                                      isSuccess=True,
                                      version=Version,
                                      message="dayOfWeek=" + day_of_week)
            self.current_configs["dayOfWeek"] = day_of_week

            interval_of_weeks = settings.get('intervalOfWeeks')
            if interval_of_weeks is None or interval_of_weeks not in ConfigOptions.interval_of_weeks:
                msg = "The parameter \"intervalOfWeeks\" is empty or invalid. intervalOfWeeks defaults to 1."
                self.log_and_syslog(logging.WARNING, msg)
                self.interval_of_weeks = '1'
            else:
                self.interval_of_weeks = interval_of_weeks
            waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                      op=waagent.WALAEventOperation.Enable,
                                      isSuccess=True,
                                      version=Version,
                                      message="intervalOfWeeks="+self.interval_of_weeks)
            self.current_configs["intervalOfWeeks"] = self.interval_of_weeks

            # Save the latest configuration for scheduled task to avoid one-off mode's affection
            waagent.SetFileContents(self.scheduled_configs_file, json.dumps(self.current_configs))

        msg = "Current Configuration: " + self.get_current_config()
        self.log_and_syslog(logging.INFO, msg)

    def install(self):
        pass

    def enable(self):
        if self.stop:
            self.stop_download()
            self.create_stop_flag()
            return
        self.delete_stop_flag()
        if not self.disabled and self.oneoff:
            script_file_path = os.path.realpath(sys.argv[0])
            os.system(' '.join(['python', script_file_path, '-oneoff', '>/dev/null 2>&1 &']))
        else:
            waagent.SetFileContents(self.history_scheduled, '')
            self.set_download_cron()
            self.set_patch_cron()
            self.restart_cron()

    def disable(self):
        self.disabled = True
        self.enable()

    def stop_download(self):
        '''
        kill the process of downloading and its subprocess.
        return code:
            100  - There are no downloading process to stop
            0    - The downloading process is stopped
        '''
        script_file_path = os.path.realpath(sys.argv[0])
        script_file = os.path.basename(script_file_path)
        retcode, output = waagent.RunGetOutput('ps -ef | grep "' + script_file + ' -download" | grep -v grep | grep -v sh | awk \'{print $2}\'')
        if retcode > 0:
            self.log_and_syslog(logging.ERROR, output)
        if output != '':
            retcode, output2 = waagent.RunGetOutput("ps -ef | awk '{if($3==" + output.strip() + ") {print $2}}'")
            if retcode > 0:
                self.log_and_syslog(logging.ERROR, output2)
            if output2 != '':
                waagent.Run('kill -9 ' + output2.strip())
            waagent.Run('kill -9 ' + output.strip())
            return 0
        return 100

    def set_download_cron(self):
        script_file_path = os.path.realpath(sys.argv[0])
        script_dir = os.path.dirname(script_file_path)
        script_file = os.path.basename(script_file_path)
        old_line_end = ' '.join([script_file, '-download'])
        if self.disabled:
            new_line = '\n'
        else:
            if self.download_time > self.start_time:
                dow = ','.join([str((day - 1) % 7) for day in self.day_of_week])
            else:
                dow = ','.join([str(day % 7) for day in self.day_of_week])
            hr = str(self.download_time.hour)
            minute = str(self.download_time.minute)
            new_line = ' '.join(['\n' + minute, hr, '* *', dow, 'root cd', script_dir, '&& python check.py', self.interval_of_weeks, '&& python', script_file, '-download > /dev/null 2>&1\n'])
        waagent.ReplaceFileContentsAtomic(self.crontab, '\n'.join(filter(lambda a: a and (old_line_end not in a), waagent.GetFileContents(self.crontab).split('\n'))) + new_line)

    def set_patch_cron(self):
        script_file_path = os.path.realpath(sys.argv[0])
        script_dir = os.path.dirname(script_file_path)
        script_file = os.path.basename(script_file_path)
        old_line_end = ' '.join([script_file, '-patch'])
        if self.disabled:
            new_line = '\n'
        else:
            start_time_dt = datetime.datetime(100, 1, 1, self.start_time.hour, self.start_time.minute)
            start_hr = str(self.start_time.hour)
            start_minute = str(self.start_time.minute)
            start_dow = ','.join([str(day % 7) for day in self.day_of_week])
            cleanup_time_dt = start_time_dt + datetime.timedelta(minutes=1)
            cleanup_hr = str(cleanup_time_dt.hour)
            cleanup_minute = str(cleanup_time_dt.minute)
            if start_time_dt.day < cleanup_time_dt.day:
                cleanup_dow = ','.join([str((day + 1) % 7) for day in self.day_of_week])
            else:
                cleanup_dow = ','.join([str(day % 7) for day in self.day_of_week])
            new_line = ' '.join(['\n' + start_minute, start_hr, '* *', start_dow, 'root cd', script_dir, '&& python check.py', self.interval_of_weeks, '&& python', script_file, '-patch >/dev/null 2>&1\n'])
            new_line += ' '.join([cleanup_minute, cleanup_hr, '* *', cleanup_dow, 'root rm -f', self.stop_flag_path, '\n'])
        waagent.ReplaceFileContentsAtomic(self.crontab, "\n".join(filter(lambda a: a and (old_line_end not in a) and (self.stop_flag_path not in a), waagent.GetFileContents(self.crontab).split('\n'))) + new_line)

    def restart_cron(self):
        retcode,output = waagent.RunGetOutput(self.cron_restart_cmd)
        if retcode > 0:
            self.log_and_syslog(logging.ERROR, output)

    def download(self):
        # Read the latest configuration for scheduled task
        settings = json.loads(waagent.GetFileContents(self.scheduled_configs_file))
        self.parse_settings(settings)

        self.provide_vm_status_test(StatusTest["Scheduled"])
        if not self.check_vm_idle(StatusTest["Scheduled"]):
            return

        if self.exists_stop_flag():
            self.log_and_syslog(logging.INFO, "Downloading patches is stopped/canceled")
            return

        waagent.SetFileContents(self.package_downloaded_path, '')
        waagent.SetFileContents(self.package_patched_path, '')

        start_download_time = time.time()
        # Installing security patches is mandatory
        self._download(self.category_required)
        if self.category == self.category_all:
            self._download(self.category_all)
        self.retry_download()
        end_download_time = time.time()
        waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                  op=waagent.WALAEventOperation.Download,
                                  isSuccess=True,
                                  version=Version,
                                  message=" ".join(["Real downloading time is", str(round(end_download_time-start_download_time,3)), "s"]))

    def _download(self, category):
        self.log_and_syslog(logging.INFO, "Start to check&download patches (Category:" + category + ")")
        retcode, downloadlist = self.check(category)
        if retcode > 0:
            msg = "Failed to check valid upgrades"
            self.log_and_syslog(logging.ERROR, msg)
            self.hutil.do_exit(1, 'Enable', 'error', '0', msg)
        if 'walinuxagent' in downloadlist:
            downloadlist.remove('walinuxagent')
        if not downloadlist:
            self.log_and_syslog(logging.INFO, "No packages are available for update.")
            return
        self.log_and_syslog(logging.INFO, "There are " + str(len(downloadlist)) + " packages to upgrade.")
        self.log_and_syslog(logging.INFO, "Download list: " + ' '.join(downloadlist))
        for pkg_name in downloadlist:
            if pkg_name in self.downloaded:
                continue
            retcode = self.download_package(pkg_name)
            if retcode != 0:
                self.log_and_syslog(logging.ERROR, "Failed to download the package: " + pkg_name)
                self.log_and_syslog(logging.INFO, "Put {0} into a retry queue".format(pkg_name))
                self.download_retry_queue.append((pkg_name, category))
                continue
            self.downloaded.append(pkg_name)
            self.log_and_syslog(logging.INFO, "Package " + pkg_name + " is downloaded.")
            waagent.AppendFileContents(self.package_downloaded_path, pkg_name + ' ' + category + '\n')

    def retry_download(self):
        retry_count = 0
        max_retry_count = 12
        self.log_and_syslog(logging.INFO, "Retry queue: {0}".format(
            " ".join([pkg_name for pkg_name,category in self.download_retry_queue])))
        while self.download_retry_queue:
            pkg_name, category = self.download_retry_queue[0]
            self.download_retry_queue = self.download_retry_queue[1:]
            retcode = self.download_package(pkg_name)
            if retcode == 0:
                self.downloaded.append(pkg_name)
                self.log_and_syslog(logging.INFO, "Package " + pkg_name + " is downloaded.")
                waagent.AppendFileContents(self.package_downloaded_path, pkg_name + ' ' + category + '\n')
            else:
                self.log_and_syslog(logging.ERROR, "Failed to download the package: " + pkg_name)
                self.log_and_syslog(logging.INFO, "Put {0} back into a retry queue".format(pkg_name))
                self.download_retry_queue.append((pkg_name,category))
                retry_count = retry_count + 1
                if retry_count > max_retry_count:
                    err_msg = ("Failed to download after {0} retries, "
                        "retry queue: {1}").format(max_retry_count,
                        " ".join([pkg_name for pkg_name,category in self.download_retry_queue]))
                    self.log_and_syslog(logging.ERROR, err_msg)
                    waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                              op=waagent.WALAEventOperation.Download,
                                              isSuccess=False,
                                              version=Version,
                                              message=err_msg)
                    break
                k = retry_count if (retry_count < 10) else 10
                interval = int(random.uniform(0, 2 ** k))
                self.log_and_syslog(logging.INFO, ("Sleep {0}s before "
                    "the next retry, current retry_count = {1}").format(interval, retry_count))
                time.sleep(interval)

    def patch(self):
        # Read the latest configuration for scheduled task
        settings = json.loads(waagent.GetFileContents(self.scheduled_configs_file))
        self.parse_settings(settings)

        if not self.check_vm_idle(StatusTest["Scheduled"]):
            return

        if self.exists_stop_flag():
            self.log_and_syslog(logging.INFO, "Installing patches is stopped/canceled")
            self.delete_stop_flag()
            return

        # Record the scheduled time
        waagent.AppendFileContents(self.history_scheduled, time.strftime("%Y-%m-%d %a", time.localtime()) + '\n' )
        # Record the open deleted files before patching
        self.open_deleted_files_before = self.check_open_deleted_files()

        retcode = self.stop_download()
        if retcode == 0:
            self.log_and_syslog(logging.WARNING, "Download time exceeded. The pending package will be downloaded in the next cycle")
            waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                      op=waagent.WALAEventOperation.Download,
                                      isSuccess=False,
                                      version=Version,
                                      message="Downloading time out")

        global start_patch_time
        start_patch_time = time.time()

        pkg_failed = []
        is_time_out = [False, False]
        patchlist = self.get_pkg_to_patch(self.category_required)
        is_time_out[0],failed = self._patch(self.category_required, patchlist)
        pkg_failed.extend(failed)
        if not self.exists_stop_flag():
            if not is_time_out[0]:
                patchlist = self.get_pkg_to_patch(self.category_all)
                if len(patchlist) == 0:
                    self.log_and_syslog(logging.INFO, "No packages are available for update. (Category:" + self.category_all + ")")
                else:
                    self.log_and_syslog(logging.INFO, "Going to sleep for " + str(self.gap_between_stage) + "s")
                    time.sleep(self.gap_between_stage)
                    is_time_out[1],failed = self._patch(self.category_all, patchlist)
                    pkg_failed.extend(failed)
        else:
            msg = "Installing patches (Category:" + self.category_all + ") is stopped/canceled"
            self.log_and_syslog(logging.INFO, msg)
        if is_time_out[0] or is_time_out[1]:
            msg = "Patching time out"
            self.log_and_syslog(logging.WARNING, msg)
            waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                      op="Patch",
                                      isSuccess=False,
                                      version=Version,
                                      message=msg)

        self.open_deleted_files_after = self.check_open_deleted_files()
        self.delete_stop_flag()
        #self.report()
        if StatusTest["Scheduled"]["Healthy"]:
            is_healthy = StatusTest["Scheduled"]["Healthy"]()
            msg = "Checking the VM is healthy after patching: " + str(is_healthy)
            self.log_and_syslog(logging.INFO, msg)
            waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                      op="Check healthy",
                                      isSuccess=is_healthy,
                                      version=Version,
                                      message=msg)
        if self.patched is not None and len(self.patched) > 0:
            self.reboot_if_required()

    def _patch(self, category, patchlist):
        if self.exists_stop_flag():
            self.log_and_syslog(logging.INFO, "Installing patches (Category:" + category + ") is stopped/canceled")
            return False,list()
        if not patchlist:
            self.log_and_syslog(logging.INFO, "No packages are available for update.")
            return False,list()
        self.log_and_syslog(logging.INFO, "Start to install " + str(len(patchlist)) +" patches (Category:" + category + ")")
        self.log_and_syslog(logging.INFO, "Patch list: " + ' '.join(patchlist))
        pkg_failed = []
        for pkg_name in patchlist:
            if pkg_name == 'walinuxagent':
                continue
            current_patch_time = time.time()
            if current_patch_time - start_patch_time > self.install_duration:
                msg = "Patching time exceeded. The pending package will be patched in the next cycle"
                self.log_and_syslog(logging.WARNING, msg)
                return True,pkg_failed
            retcode = self.patch_package(pkg_name)
            if retcode != 0:
                self.log_and_syslog(logging.ERROR, "Failed to patch the package:" + pkg_name)
                pkg_failed.append(' '.join([pkg_name, category]))
                continue
            self.patched.append(pkg_name)
            self.log_and_syslog(logging.INFO, "Package " + pkg_name + " is patched.")
            waagent.AppendFileContents(self.package_patched_path, pkg_name + ' ' + category + '\n')
        return False,pkg_failed

    def patch_one_off(self):
        """
        Called when startTime is empty string, which means a on-demand patch.
        """
        self.provide_vm_status_test(StatusTest["Oneoff"])
        if not self.check_vm_idle(StatusTest["Oneoff"]):
            return

        global start_patch_time
        start_patch_time = time.time()

        self.log_and_syslog(logging.INFO, "Going to patch one-off")
        waagent.SetFileContents(self.package_downloaded_path, '')
        waagent.SetFileContents(self.package_patched_path, '')

        # Record the open deleted files before patching
        self.open_deleted_files_before = self.check_open_deleted_files()

        pkg_failed = []
        is_time_out = [False, False]
        retcode, patchlist_required = self.check(self.category_required)
        if retcode > 0:
            msg = "Failed to check valid upgrades"
            self.log_and_syslog(logging.ERROR, msg)
            self.hutil.do_exit(1, 'Enable', 'error', '0', msg)
        if not patchlist_required:
            self.log_and_syslog(logging.INFO, "No packages are available for update. (Category:" + self.category_required + ")")
        else:
            is_time_out[0],failed = self._patch(self.category_required, patchlist_required)
            pkg_failed.extend(failed)
        if self.category == self.category_all:
            if not self.exists_stop_flag():
                if not is_time_out[0]:
                    retcode, patchlist_other = self.check(self.category_all)
                    if retcode > 0:
                        msg = "Failed to check valid upgrades"
                        self.log_and_syslog(logging.ERROR, msg)
                        self.hutil.do_exit(1, 'Enable', 'error', '0', msg)
                    patchlist_other = [pkg for pkg in patchlist_other if pkg not in patchlist_required]
                    if len(patchlist_other) == 0:
                        self.log_and_syslog(logging.INFO, "No packages are available for update. (Category:" + self.category_all + ")")
                    else:
                        self.log_and_syslog(logging.INFO, "Going to sleep for " + str(self.gap_between_stage) + "s")
                        time.sleep(self.gap_between_stage)
                        self.log_and_syslog(logging.INFO, "Going to patch one-off (Category:" + self.category_all + ")")
                        is_time_out[1],failed = self._patch(self.category_all, patchlist_other)
                        pkg_failed.extend(failed)
            else:
                self.log_and_syslog(logging.INFO, "Installing patches (Category:" + self.category_all + ") is stopped/canceled")

        if is_time_out[0] or is_time_out[1]:
            waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                      op="Oneoff Patch",
                                      isSuccess=False,
                                      version=Version,
                                      message="Patching time out")

        shutil.copy2(self.package_patched_path, self.package_downloaded_path)
        for pkg in pkg_failed:
            waagent.AppendFileContents(self.package_downloaded_path, pkg + '\n')

        self.open_deleted_files_after = self.check_open_deleted_files()
        self.delete_stop_flag()
        #self.report()
        if StatusTest["Oneoff"]["Healthy"]:
            is_healthy = StatusTest["Oneoff"]["Healthy"]()
            msg = "Checking the VM is healthy after patching: " + str(is_healthy)
            self.log_and_syslog(logging.INFO, msg)
            waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                      op="Check healthy",
                                      isSuccess=is_healthy,
                                      version=Version,
                                      message=msg)
        if self.patched is not None and len(self.patched) > 0:
            self.reboot_if_required()

    def reboot_if_required(self):
        self.check_reboot()
        self.check_needs_restart()
        msg = ''
        if self.reboot_after_patch == 'notrequired' and self.reboot_required:
            msg += 'Pending Reboot'
            if self.needs_restart:
                msg += ': ' + ' '.join(self.needs_restart)
            waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                      op="Reboot",
                                      isSuccess=False,
                                      version=Version,
                                      message=" ".join([self.reboot_after_patch, msg,
                                                       str(len(self.needs_restart)),
                                                       "packages need to restart"]))
            self.hutil.do_exit(0, 'Enable', 'success', '0', msg)
        if self.reboot_after_patch == 'required':
            msg += "System going to reboot(Required)"
        elif self.reboot_after_patch == 'auto' and self.reboot_required:
            msg += "System going to reboot(Auto)"
        elif self.reboot_after_patch == 'rebootifneed':
            if (self.reboot_required or self.needs_restart):
                msg += "System going to reboot(RebootIfNeed)"
        if msg:
            if self.needs_restart:
                msg += ': ' + ' '.join(self.needs_restart)
            self.log_and_syslog(logging.INFO, msg)
            waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                      op="Reboot",
                                      isSuccess=True,
                                      version=Version,
                                      message="Reboot")
            retcode = waagent.Run('reboot')
            if retcode != 0:
                self.log_and_syslog(logging.ERROR, "Failed to reboot")
                waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                          op="Reboot",
                                          isSuccess=False,
                                          version=Version,
                                          message="Failed to reboot")
        else:
            waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                      op="Reboot",
                                      isSuccess=False,
                                      version=Version,
                                      message="Not reboot")

    def check_needs_restart(self):
        self.needs_restart.extend(self.get_pkg_needs_restart())
        patched_files = dict()
        for pkg in self.get_pkg_patched():
            cmd = ' '.join([self.pkg_query_cmd, pkg])
            try:
                retcode, output = waagent.RunGetOutput(cmd)
                patched_files[os.path.basename(pkg)] = [filename for filename in output.split("\n") if os.path.isfile(filename)]
            except Exception:
                self.log_and_syslog(logging.ERROR, "Failed to " + cmd)
        # for k,v in patched_files.items():
        #     self.log_and_syslog(logging.INFO, k + ": " + " ".join(v))
        open_deleted_files = list()
        for filename in self.open_deleted_files_after:
            if filename not in self.open_deleted_files_before:
                open_deleted_files.append(filename)
        # self.log_and_syslog(logging.INFO, "Open deleted files: " + " ".join(open_deleted_files))
        for pkg,files in patched_files.items():
            for filename in files:
                realpath = os.path.realpath(filename)
                if realpath in open_deleted_files and pkg not in self.needs_restart:
                     self.needs_restart.append(pkg)
        msg = "Packages needs to restart: "
        pkgs = " ".join(self.needs_restart)
        if pkgs:
            msg += pkgs
        else:
            msg = "There is no package which needs to restart"
        self.log_and_syslog(logging.INFO, msg)

    def get_pkg_needs_restart(self):
        return []

    def check_open_deleted_files(self):
        ret = list()
        retcode,output = waagent.RunGetOutput('lsof | grep "DEL"')
        if retcode == 0:
            for line in output.split('\n'):
                if line:
                    filename = line.split()[-1]
                    if filename not in ret:
                        ret.append(filename)
        return ret

    def create_stop_flag(self):
        waagent.SetFileContents(self.stop_flag_path, '')

    def delete_stop_flag(self):
        if self.exists_stop_flag():
            os.remove(self.stop_flag_path)

    def exists_stop_flag(self):
        if os.path.isfile(self.stop_flag_path):
            return True
        else:
            return False

    def get_pkg_to_patch(self, category):
        if not os.path.isfile(self.package_downloaded_path):
            return []
        pkg_to_patch = waagent.GetFileContents(self.package_downloaded_path)
        if not pkg_to_patch:
            return []
        patchlist = [line.split()[0] for line in pkg_to_patch.split('\n') if line.endswith(category)]
        if patchlist is None:
            return []
        return patchlist

    def get_pkg_patched(self):
        if not os.path.isfile(self.package_patched_path):
            return []
        pkg_patched = waagent.GetFileContents(self.package_patched_path)
        if not pkg_patched:
            return []
        patchedlist = [line.split()[0] for line in pkg_patched.split('\n') if line]
        return patchedlist

    def get_current_config(self):
        current_configs = []
        for k,v in self.current_configs.items():
            current_configs.append(k + "=" + v)
        return ",".join(current_configs)

    def provide_vm_status_test(self, status_test):
        for status,provided in status_test.items():
            if provided is None:
                provided = "False"
                level = logging.WARNING
            else:
                provided = "True"
                level = logging.INFO
            msg = "The VM %s test script is provided: %s" % (status, provided)
            self.log_and_syslog(level, msg)
            waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                      op="provides %s test script" % (status,),
                                      isSuccess=provided,
                                      version=Version,
                                      message=msg)

    def check_vm_idle(self, status_test):
        is_idle = True
        if status_test["Idle"]:
            is_idle = status_test["Idle"]()
            msg = "Checking the VM is idle: " + str(is_idle)
            self.log_and_syslog(logging.INFO, msg)
            waagent.AddExtensionEvent(name=self.hutil.get_name(),
                                      op="Check idle",
                                      isSuccess=is_idle,
                                      version=Version,
                                      message=msg)
            if not is_idle:
                self.log_and_syslog(logging.WARNING, "Current Operation is skipped.")
        return is_idle

    def log_and_syslog(self, level, message):
        if level == logging.INFO:
            self.hutil.log(message)
        elif level == logging.WARNING:
            self.hutil.log(" ".join(["Warning:", message]))
        elif level == logging.ERROR:
            self.hutil.error(message)
        if self.syslogger is None:
            self.init_syslog()
        self.syslog(level, message)

    def init_syslog(self):
        self.syslogger = logging.getLogger(self.hutil.get_name())
        self.syslogger.setLevel(logging.INFO)
        formatter = logging.Formatter('%(name)s: %(levelname)s %(message)s')
        try:
            handler = logging.handlers.SysLogHandler(address='/dev/log')
            handler.setFormatter(formatter)
            self.syslogger.addHandler(handler)
        except:
            self.syslogger = None
            self.hutil.error("Syslog is not ready.")

    def syslog(self, level, message):
        if self.syslogger is None:
            return
        if level == logging.INFO:
            self.syslogger.info(message)
        elif level == logging.WARNING:
            self.syslogger.warning(message)
        elif level == logging.ERROR:
            self.syslogger.error(message)

