#!/usr/bin/env python2
# vim: set fdm=marker:
__author__ = 'iseletsk'

# Copyright (c) Cloud Linux GmbH & Cloud Linux Software, Inc
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT

import warnings

warnings.filterwarnings("ignore", category=DeprecationWarning)

import ast
import base64
import ConfigParser
import errno
import fcntl
import httplib
import json
import logging
import logging.handlers
import os
import platform
import random
import re
import shutil
import socket
import ssl
import subprocess
import sys
import tempfile
import time
import urllib
import urllib2
import urlparse
import uuid

from argparse import ArgumentParser, SUPPRESS
from datetime import datetime

kcarelog = logging.getLogger('kcare')
kcarelog.setLevel(logging.INFO)

BLACKLIST_FILE = "kpatch.blacklist"
CACHE_ENTRIES = 3
CONFIG = '/etc/sysconfig/kcare/kcare.conf'
CPANEL_GID = 99
EFFECTIVE_LATEST = ('latest.v', 2)
FIXUPS_FILE = "kpatch.fixups"
FREEZER_BLACKLIST = '/etc/sysconfig/kcare/freezer.modules.blacklist'
GPG_KEY_DIR = '/etc/pki/kcare-gpg/'
GPG_KEY_ID = 'KernelCare <info@kernelcare.com>'
KCARE_DEV = '/dev/kcare'
KERNEL_VERSION_FILE = "/proc/version"
KMOD_BIN = "kcare.ko"
KPATCH_CTL = '/usr/libexec/kcare/kpatch_ctl'
KCDOCTOR = '/usr/libexec/kcare/kcdoctor.sh'
LEVEL = None  # a level to "stick on" (if 0 then use latest level)
PATCH_BIN = "kpatch.bin"
PATCH_CACHE = "/var/cache/kcare"
PATCH_DONE = ".done"
PATCH_INFO = "kpatch.info"
PATCH_LATEST = ('latest.v2',)
PATCH_METHOD = ''
PATCH_SERVER = "patches.kernelcare.com"
PATCH_SERVER_PROTOCOL = "https://"
PATCH_SERVER_URL = PATCH_SERVER_PROTOCOL + PATCH_SERVER
REGISTRATION_API_URL = 'https://cln.cloudlinux.com/api/kcare'
SMART_UPDATE = False
SYSCTL_CONFIG = '/etc/sysconfig/kcare/sysctl.conf'
SYSTEMID = '/etc/sysconfig/kcare/systemid'
TEST_PREFIX = ''
VERSION = "2.24-1"
VIRTWHAT = '/usr/libexec/kcare/virt-what'

if 'KCDEV' in os.environ:
    """ To simplify development, lets add environment variable.
    This would allow placing config file and system id in current dir.
    """
    CONFIG = './kcare.dev.conf'
    SYSTEMID = './systemid'

VERSION_RE = re.compile("^(\d+[.]\d+[-]\d+)")
BLACKLIST_RE = re.compile("==BLACKLIST==\n(.*)==END BLACKLIST==\n", re.DOTALL)
CONFLICTING_MODULES_RE = re.compile('(kpatch.*|ksplice.*|kpatch_livepatch.*)')
KCARE_UNAME_FILE = '/proc/kcare/effective_version'

# allowed values: REMOTE, LOCAL, LOCAL_FIRST
UPDATE_POLICY = "REMOTE"
AUTO_UPDATE = True
UPDATE_FROM_LOCAL = False
USE_SIGNATURE = True

IGNORE_UNKNOWN_KERNEL = False
LOAD_KCARE_SYSCTL = True
KPATCH_DEBUG = False
CHECK_SSL_CERTS = True
PATCH_TYPE = ''

PRINT_INFO = 1
PRINT_WARN = 2
PRINT_ERROR = 3

print_level = PRINT_INFO


def set_print_level(level):
    global print_level
    print_level = level


GPG_BIN = '/usr/bin/gpg'
GPG_OWNER_TRUST = '034327E8206469CB296AC14ECCE80D2B8B53D14B:6:\n'

SUCCESS_TIMEOUT = 5 * 60
REPORT_FQDN = False
FORCE_GID = None


# TODO: python2.6 contains functools.wraps and partial -- remove the following
# define emulate functools.wraps{{{

def partial(func, *args, **kwds):
    "Emulate Python2.6's functools.partial"
    return lambda *fargs, **fkwds: func(*(args + fargs), **dict(kwds, **fkwds))


WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__')
WRAPPER_UPDATES = ('__dict__',)


def update_wrapper(wrapper,
                   wrapped,
                   assigned=WRAPPER_ASSIGNMENTS,
                   updated=WRAPPER_UPDATES):
    """Update a wrapper function to look like the wrapped function

       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes off the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        setattr(wrapper, attr, getattr(wrapped, attr))
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper


def wraps(wrapped,
          assigned=WRAPPER_ASSIGNMENTS,
          updated=WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

       Returns a decorator that invokes update_wrapper() with the decorated
       function as the wrapper argument and the arguments to wraps() as the
       remaining arguments. Default arguments are as for update_wrapper().
       This is a convenience function to simplify applying partial() to
       update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)


# end of wraps}}}


def get_freezer_blacklist():
    result = set()
    try:
        f = open(FREEZER_BLACKLIST, 'r')
        for line in f:
            result.add(line.rstrip())
        f.close()
    except:
        pass
    return result


def _apply_ptype(ptype, filename):
    name_parts = filename.split('.')
    if ptype:
        filename = '.'.join([name_parts[0], ptype, name_parts[-1]])
    else:
        filename = '.'.join([name_parts[0], name_parts[-1]])
    return filename


def apply_ptype(ptype):
    global PATCH_BIN, PATCH_INFO, BLACKLIST_FILE, FIXUPS_FILE, PATCH_DONE
    PATCH_BIN = _apply_ptype(ptype, PATCH_BIN)
    PATCH_INFO = _apply_ptype(ptype, PATCH_INFO)
    BLACKLIST_FILE = _apply_ptype(ptype, BLACKLIST_FILE)
    FIXUPS_FILE = _apply_ptype(ptype, FIXUPS_FILE)
    PATCH_DONE = _apply_ptype(ptype, PATCH_DONE)


def printinfo(message, level):
    global print_level
    if print_level <= level:
        print(message)


def loginfo(message):
    printinfo(message, PRINT_INFO)
    kcarelog.info(message)


def logerror(message):
    printinfo(message, PRINT_ERROR)
    kcarelog.error(message)


def _timestmap_str():
    return str(int(time.time()))


def nohup_fork(func, sleep=None):
    """
    Run func in a fork in an own process group
    (will stay alive after kcarectl process death).
    :param func: function to execute
    :return:
    """
    pid = os.fork()
    if pid != 0:
        os.waitpid(pid, 0)
        return

    os.setsid()

    pid = os.fork()
    if pid != 0:
        os._exit(0)

    if sleep:
        time.sleep(sleep)

    try:
        func()
    except Exception:
        os._exit(1)
    os._exit(0)


def atomic_write(fname, content):
    tmp_fname = fname + '.tmp'
    with open(tmp_fname, 'wb') as f:
        f.write(content)
    os.rename(tmp_fname, fname)


def touch_anchor():
    """ Check the fact that there was a failed patching attempt.
    If anchor file not exists we should create an anchor with
    timestamp and schedule its deletion at $timeout.

    If anchor exists and its timestamp more than $timeout from now
    we should raise an error.
    """
    anchor_filepath = os.path.join(PATCH_CACHE, '.kcareprev.lock')
    if os.path.isfile(anchor_filepath):
        with open(anchor_filepath, 'r') as afile:
            try:
                timestamp = int(afile.read())
                # anchor was created quite recently
                # that means that something went wrong
                if timestamp + SUCCESS_TIMEOUT > time.time():
                    raise PreviousPatchFailedException(timestamp, anchor_filepath)
            except ValueError:
                pass

    atomic_write(anchor_filepath, _timestmap_str())  # write a new timestamp


def commit_update(state_data):
    """
    See touch_anchor() for detailed explanation of anchor mechanics.
    See KPT-730 for details about action registration.
    :param state_data: dict with current level, kernel_id etc.
    """
    try:
        os.remove(os.path.join(PATCH_CACHE, '.kcareprev.lock'))
    except OSError:
        pass
    register_action('done', state_data)


# source http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/
def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None):
    """Retry calling the decorated function using an exponential backoff.

    http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/
    original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry

    :param ExceptionToCheck: the exception to check. may be a tuple of
        exceptions to check
    :type ExceptionToCheck: Exception or tuple
    :param tries: number of times to try (not retry) before giving up
    :type tries: int
    :param delay: initial delay between retries in seconds
    :type delay: int
    :param backoff: backoff multiplier e.g. value of 2 will double the delay
        each retry
    :type backoff: int
    :param logger: logger to use. If None, print
    :type logger: logging.Logger instance
    """

    def deco_retry(f):

        @wraps(f)
        def f_retry(*args, **kwargs):
            mtries, mdelay = tries, delay
            while mtries > 1:
                try:
                    return f(*args, **kwargs)
                except ExceptionToCheck, e:
                    msg = "%s. Retrying in %d seconds..." % (str(e), mdelay)
                    if logger:
                        logger.warning(msg)
                    else:
                        print msg
                    time.sleep(mdelay)
                    mtries -= 1
                    mdelay *= backoff
            return f(*args, **kwargs)

        return f_retry  # true decorator

    return deco_retry


def save_to_file(response, dst):
    parent_dir = os.path.dirname(dst)
    if not os.path.exists(parent_dir):
        os.makedirs(parent_dir)
    with open(dst, "wb") as f:
        shutil.copyfileobj(response, f)


def clean_directory(directory, exclude_path):
    if not os.path.exists(directory):
        return
    data = []
    for item in os.listdir(directory):
        full_path = os.path.join(directory, item)
        if full_path != exclude_path:
            data.append((os.stat(full_path).st_mtime, full_path))
        data.sort(reverse=True)
    for entry in data[CACHE_ENTRIES:]:
        shutil.rmtree(entry[1])


def clear_cache(khash, plevel):
    clean_directory(os.path.join(PATCH_CACHE, 'modules'), get_kmod_cache_path(khash, ''))
    clean_directory(os.path.join(PATCH_CACHE, 'patches'), get_cache_path(khash, plevel, ''))


def get_cache_path(khash, plevel, fname):
    prefix = (TEST_PREFIX or 'none').strip('/')
    ptype = PATCH_TYPE or 'default'
    patch_dir = '-'.join([prefix, khash, plevel, ptype])
    result = (PATCH_CACHE, 'patches', patch_dir)
    if fname:
        result += (fname,)
    return os.path.join(*result)


def get_kmod_cache_path(khash, fname):
    prefix = (TEST_PREFIX or 'none').strip('/')
    module_dir = '-'.join([prefix, khash])
    result = (PATCH_CACHE, 'modules', module_dir)
    if fname:
        result += (fname,)
    return os.path.join(*result)


def save_cache_latest(khash, patch_level):
    with open(get_kmod_cache_path(khash, 'latest'), 'w') as out:
        out.write(patch_level)


def get_cache_latest(khash):
    path_with_latest = get_kmod_cache_path(khash, 'latest')
    if os.path.isfile(path_with_latest):
        return open(path_with_latest, 'r').read()


def check_gpg_signature(file_path, signature):
    """
    Check a file signature using the gpg tool.
    If signature is wrong BadSignatureException will be raised.

    :param file_path: path to file which signature will be checked
    :param signature: a file-like object with the signature
    :return: True in case of valid signature
    :raises: BadSignatureException
    """
    sig_dst = file_path + '.sig'
    save_to_file(signature, sig_dst)

    if gpg_exec(['--verify', sig_dst, file_path]) != 0:
        raise BadSignatureException("Bad Signature: %s" % file_path)
    return True


class CertificateError(ValueError):
    pass


class KcareError(Exception):
    """  Base kernelcare exception which will be considered as expected
    error and the full traceback will not be shown.
    """
    pass


class UnknownKernelException(KcareError):
    def __init__(self):
        Exception.__init__(self, "Unknown Kernel (%s %s)" % (get_distro()[0], platform.release()))


class UnableToGetLicenseException(KcareError):
    def __init__(self, code):
        Exception.__init__(self, "Unknown Issue when getting trial license. Error code: " + str(code))


class ApplyPatchError(KcareError):

    def __init__(self, code, freezer_style, level, patch_file, *args, **kwargs):
        super(ApplyPatchError, self).__init__(*args, **kwargs)
        self.code = code
        self.freezer_style = freezer_style
        self.level = level
        self.patch_file = patch_file
        self.distro = get_distro()[0]
        self.release = platform.release()

    def __str__(self):
        return "Unable to apply patch (%s %s %d %s %s, %s, %s, %s, %s)" % (
            (self.patch_file, self.level, self.code, self.distro, self.release) + self.freezer_style)


class AlreadyTrialedException(KcareError):

    def __init__(self, ip, created, *args, **kwargs):
        super(AlreadyTrialedException, self).__init__(*args, **kwargs)
        self.created = created[0:created.index('T')]
        self.ip = ip

    def __str__(self):
        return "The IP %s was already used for a trial license on %s " % (self.ip, self.created)


class BadSignatureException(KcareError):
    pass


# KCARE-509
class PreviousPatchFailedException(KcareError):

    def __init__(self, timestamp, anchor, *args, **kwargs):
        super(PreviousPatchFailedException, self).__init__(*args, **kwargs)
        self.timestamp = timestamp
        self.anchor = anchor

    def __str__(self):
        message = "It seems, the latest patch, applying at {0}, crashed, " \
                  "and further attempts will be suspended. " \
                  "To force patch applying, remove `{1}` file"
        return message.format(self.timestamp, self.anchor)


def http_request(url, auth_string):
    request = urllib2.Request(url)
    if not UPDATE_FROM_LOCAL and auth_string:
        request.add_header("Authorization", "Basic %s" % auth_string)
    return request


def print_cln_http_error(ex, url=None):
    url = url or '<route cannot be logged>'
    if 'code' in ex:
        logerror("Unable to fetch %s. Please try again later "
                 "(code: %d, error: %s)" % (url, ex.code, str(ex)))
    else:
        logerror("Unable to fetch %s. Please try again later "
                 "(error: %s)" % (url, str(ex)))


def parse_response_date(str_raw):
    # Try to split it by T
    str_date, sep, _ = str_raw.partition('T')
    # No success - split by space
    if not sep:
        str_date, _, _ = str_raw.partition(' ')

    if hasattr(datetime, 'strptime'):
        pdate = datetime.strptime(str_date, "%Y-%m-%d")
    else:
        pdate = datetime(*(time.strptime(str_date, "%Y-%m-%d")[:6]))
    return pdate


def register_key_for_ip_license(key):
    url = REGISTRATION_API_URL + '/nagios/register_key.plain?key=%s' % key
    try:
        response = urlopen(url)
        res = data_as_dict(response.read())
        code = int(res['code'])
        if code == 0:
            print "Key successfully registered"
        elif code == 1:
            print "Wrong key format or size"
        elif code == 2:
            print "No KernelCare license for that IP"
        else:
            print "Unknown error %d" % code
        return code
    except urllib2.HTTPError, e:
        print_cln_http_error(e, url)
    return -1


def update_config(property, value):
    cf = open(CONFIG)
    lines = cf.readlines()
    cf.close()
    updated = False
    for i in xrange(0, len(lines)):
        if lines[i].startswith(property):
            lines[i] = property + " = " + str(value) + "\n"
            updated = True
            break
    if not updated:
        lines.append(property + " = " + str(value) + "\n")
    cf = open(CONFIG, "w")
    cf.writelines(lines)
    cf.close()


def plugin_info(format=None):
    """
    The output will consist of:
    Ignore output up to the line with "--START--"
    Line 1: show if update is needed:
        0 - updated to latest,
        1 - update available,
        2 - unknown kernel
        3 - kernel doesn't need patches
        4 - no license, cannot determine
    Line 2: licensing message (can be skipped, can be more then one line)
    Line 3: LICENSE: CODE: 1: license present, 2: trial license present, 0: no license
    Line 4: Update mode (True - auto-update, False, no auto update)
    Line 5: Effective kernel version
    Line 6: Real kernel version
    Line 7: Patchset Installed # --> If None, no patchset installed
    Line 8: Uptime (in seconds)

    If *format* is 'json' return the results in JSON format.

    Any other output means error retrieving info
    :return:
    """

    try:
        pli = _patch_level_info()
        update_code = pli.code
        remote_pl = pli.remote_lvl
        loaded_pl = pli.applied_lvl
        license_info_result = -1
    except AlreadyTrialedException, ae:
        loaded_pl = loaded_patch_level()
        license_info_result = 0
        license_error_message = "Your trial license for the IP %s expired on %s" % (ae.ip, ae.created)
        update_code = 4

    if format == "json":
        results = {
            "updateCode": str(update_code),
            "autoUpdate": AUTO_UPDATE,
            "effectiveKernel": kcare_uname(),
            "realKernel": platform.release(),
            "loadedPatchLevel": loaded_pl,
            "uptime": int(get_uptime()),
        }
        if license_info_result == 0:
            results["licenseErrorMessage"] = license_error_message
            results["license"] = str(license_info_result)
        else:
            results["license"] = license_info()
        print("--START--")
        print(json.dumps(results))
    else:
        print("--START--")
        print(str(update_code))
        if license_info_result == 0:
            print(license_error_message)
        else:
            license_info_result = license_info()
        print("LICENSE: " + str(license_info_result))
        print(AUTO_UPDATE)
        print(kcare_uname())
        print(platform.release())
        print(loaded_pl)
        print(get_uptime())


def license_info():
    server_id = serverid_store.get_serverid()
    if server_id:
        url = REGISTRATION_API_URL + '/check.plain?server_id=%s' % server_id
        try:
            response = urlopen(url)
            res = data_as_dict(response.read())
            code = int(res['code'])
            if code == 0:
                print("Key-based valid license found")
                return 1
            else:
                license_type = _get_license_info_by_ip(key_checked=1)
                if license_type == 0:
                    print "No valid key-based license found"
                return license_type
        except urllib2.HTTPError, e:
            print_cln_http_error(e, url)
            return 0
    else:
        return _get_license_info_by_ip()


def _get_license_info_by_ip(key_checked=0):
    url = REGISTRATION_API_URL + '/check.plain'
    try:
        response = urlopen(url)
        res = data_as_dict(response.read())
        if res['success'].lower() == 'true':
            code = int(res['code'])
            if code == 0:
                print("Valid license found for IP %s" % (res['ip']))
                return 1  # valid license
            if code == 1:
                ip = res['ip']
                expires_str = parse_response_date(res['expire_date']).strftime("%Y-%m-%d")
                print("You have a trial license for the IP %s that will expire on %s" % (ip, expires_str))
                return 2  # trial license
            if code == 2 and key_checked == 0:
                ip = res['ip']
                expires_str = parse_response_date(res['expire_date']).strftime("%Y-%m-%d")
                print("Your trial license for the IP %s expired on %s" % (ip, expires_str))
            if code == 3 and key_checked == 0:
                if 'ip' in res:
                    print("The IP %s hasn't been licensed" % (res['ip']))
                else:
                    print("This server hasn't been licensed")
        else:
            message = res.get('message', '')
            print("Error retrieving license info: %s" % (message))
    except urllib2.HTTPError, e:
        print_cln_http_error(e, url)
    except KeyError, ke:
        print("Unknown Key: %s" % ke)
    return 0  # no valid license


def register_trial():
    try:
        response = urlopen(REGISTRATION_API_URL + '/trial.plain')
        res = data_as_dict(response.read())
        try:
            if res['success'].lower() == "true":
                if res['expired'] == 'true':
                    raise AlreadyTrialedException(res['ip'], res['created'])
                print "Requesting trial license for IP %s. Please wait..." % res['ip']
                return None
            elif res['success'] == "na":
                raise KcareError("Invalid License")
            else:
                raise UnableToGetLicenseException(-1)  # Invalid response?
        except KeyError, ke:
            raise UnableToGetLicenseException(ke)
    except urllib2.HTTPError, e:
        raise UnableToGetLicenseException(e.code)


def get_uptime():
    f = None
    try:
        f = open('/proc/uptime', 'r')
        line = f.readline()
        result = str(int(float(line.split()[0])))
        f.close()
        return result
    except:
        if f is not None:
            f.close()
        return "-1"


def get_last_stop():
    """ Returns timestamp from PATCH_CACHE/stoped.at if its exsits
    """
    stopped_at_filename = os.path.join(PATCH_CACHE, 'stopped.at')
    if os.path.exists(stopped_at_filename):
        with open(stopped_at_filename, 'r') as fh:
            return fh.read().rstrip()
    return "-1"


def get_distro():
    if hasattr(platform, 'linux_distribution'):
        return platform.linux_distribution()
    else:
        # TODO: deprecated since version 2.6
        return platform.dist()


def strip_version_timestamp(version):
    match = VERSION_RE.match(version)
    return match and match.group(1) or version


def get_backtrace():
    """ Tries to collect a backtraces.
    """
    result = []
    current_bt = []
    log = check_output(["dmesg", "-kT"]).split('\n')
    save_to_result = False
    for line in log:
        if "[ cut here ]" in line:
            save_to_result = True
        elif "[ end trace" in line:
            save_to_result = False
            result.append('\n'.join(current_bt))
            current_bt = []
        elif save_to_result:
            current_bt.append(line)
    return result


def server_info():
    data = dict()

    data['machine'] = platform.machine()
    data['processor'] = platform.processor()
    data['release'] = platform.release()
    data['system'] = platform.system()
    data['version'] = platform.version()

    distro = get_distro()
    data['distro'] = distro[0]
    data['distro_version'] = distro[1]

    data['euname'] = kcare_uname()
    data['kcare_version'] = strip_version_timestamp(VERSION)
    data['last_stop'] = get_last_stop()
    data['node'] = get_hostname()
    data['uptime'] = get_uptime()
    data['virt'] = check_output([VIRTWHAT]).strip()

    description = parse_patch_description(loaded_patch_description())
    data['ltimestamp'] = description['last-update']
    data['patch_level'] = description['patch-level']
    data['patch_type'] = description['patch-type']

    server_id = serverid_store.get_serverid()
    if server_id:
        data['server_id'] = server_id

    state_file = os.path.join(PATCH_CACHE, 'kcare.state')
    if os.path.exists(state_file):
        with open(state_file, 'rb') as f:
            state = f.read()
            data['state'] = ast.literal_eval(state)

    # Send backtrace only at the start
    if SMART_UPDATE:
        data['backtrace'] = get_backtrace()

    return data


def based_server_info():
    return base64.b16encode(str(server_info()))


def get_httpauth_string():
    server_id = serverid_store.get_serverid()
    if server_id:
        return base64.encodestring('%s:%s' % (server_id, 'kernelcare')).replace('\n', '')
    return None


class UrlParseResult(object):
    def __init__(self, scheme, netloc, url, query, fragment):
        self.scheme = scheme
        self.netloc = netloc
        self.url = url
        self.query = query
        self.fragment = fragment

    @property
    def username(self):
        netloc = self.netloc
        if "@" in netloc:
            userinfo = netloc.rsplit("@", 1)[0]
            if ":" in userinfo:
                userinfo = userinfo.split(":", 1)[0]
            return userinfo
        return None

    @property
    def password(self):
        netloc = self.netloc
        if "@" in netloc:
            userinfo = netloc.rsplit("@", 1)[0]
            if ":" in userinfo:
                return userinfo.split(":", 1)[1]
        return None

    @property
    def hostname(self):
        netloc = self.netloc.split('@')[-1]
        if '[' in netloc and ']' in netloc:
            return netloc.split(']')[0][1:].lower()
        elif ':' in netloc:
            return netloc.split(':')[0].lower()
        elif netloc == '':
            return None
        else:
            return netloc.lower()

    @property
    def port(self):
        netloc = self.netloc.split('@')[-1].split(']')[-1]
        if ':' in netloc:
            port = netloc.split(':')[1]
            if port:
                port = int(port, 10)
                # verify legal port
                if 0 <= port <= 65535:
                    return port
        if self.scheme == 'http':
            return 80
        elif self.scheme == 'https':
            return 443
        return None


# python >= 2.7.9 stdlib (with ssl.HAS_SNI) is able to process https request on its own,
# for earlier versions manual checks should be done
if not getattr(ssl, 'HAS_SNI', None):
    try:
        import distutils.version
        from OpenSSL.SSL import TLSv1_METHOD, Context, Connection, VERIFY_PEER, __version__ as openssl_version

        if (distutils.version.StrictVersion(openssl_version) < distutils.version.StrictVersion('0.13')):
            raise ImportError("No pyOpenSSL module with SNI ability.")
    except ImportError:
        pass
    else:
        def dummy_verify_callback(*args):
            # OpenSSL.SSL.Context.set_verify() requires callback
            # where additional checks could be done;
            # here is a dummy callback and a hostname check is made externally
            # to provide original exception from match_hostname()
            return True


        PureHTTPSConnection = httplib.HTTPSConnection


        class PyOpenSSLHTTPSConnection(httplib.HTTPSConnection):

            def connect(self):
                orig_host_port = self.host, self.port

                if CHECK_SSL_CERTS:
                    httplib.HTTPConnection.connect(self)
                    ctx = Context(TLSv1_METHOD)
                    ctx.set_verify(VERIFY_PEER, dummy_verify_callback)
                    ctx.set_default_verify_paths()
                    conn = Connection(ctx, self.sock)
                    conn.set_connect_state()
                    # self._tunnel_host is an original hostname in case of proxy use
                    server_host = self._tunnel_host or self.host
                    conn.set_tlsext_host_name(server_host)
                    conn.do_handshake()
                    match_hostname(conn.get_peer_certificate(), server_host)

                # the previous connection is corrupted by PyOpenSSL,
                # need to make a new clean one and provide original host-port pair
                self.host, self.port = orig_host_port
                PureHTTPSConnection.connect(self)


        httplib.HTTPSConnection = PyOpenSSLHTTPSConnection


def urlopen(url, *args, **kwargs):
    if hasattr(url, 'get_full_url'):
        request_url = url.get_full_url()
    else:
        request_url = url

    try:
        if not CHECK_SSL_CERTS and getattr(ssl, 'HAS_SNI', None):
            ctx = ssl.create_default_context()
            ctx.check_hostname = False
            ctx.verify_mode = ssl.CERT_NONE
            return urllib2.urlopen(url, *args, context=ctx, **kwargs)
        return urllib2.urlopen(url, *args, **kwargs)
    except urllib2.HTTPError:
        # HTTPError is a URLError descendant and contains URL, raise it as is
        raise
    except urllib2.URLError as ex:
        # there is no information about URL in the base URLError class, add it and raise
        ex.reason = "Request for `{0}` failed: {1}".format(request_url, ex)
        raise


def cacheable(fn):
    """Cache func result and do not call it next times."""

    def wrapper(*args, **kwargs):
        if not hasattr(fn, "cached_res"):
            fn.cached_res = fn(*args, **kwargs)
        return fn.cached_res

    return wrapper


def _handle_forbidden():
    """ In case of 403 error we should check whats happen.
    Case #1. We are trying to register unlicensed machine and should try to register trial.
    Case #2. We have a valid license but access restrictions on server are not consistent yet
             and we had to try later.
    """
    info = None
    server_id = serverid_store.get_serverid() or ""
    url = REGISTRATION_API_URL + '/check.plain?server_id=%s' % server_id
    try:
        response = urlopen(url)
        info = data_as_dict(response.read())
    except urllib2.HTTPError:
        # We've got nothing
        pass
    # 0 - valid license  1 - valid trial license
    if info is not None and info['code'] not in ['0', '1']:
        register_trial()
    else:
        # License is fine. Looks like htpasswd not updated yet.
        logerror("Unable to access server. Please try again later")


class LockExists(Exception):
    pass


def cross_process_lock(process_name):
    """Take a cross process lock basing on Linux sockets.

    Lock gets released automatically once the process is exited.
    """

    # Without holding a reference to our socket somewhere it gets garbage
    # collected when the function exits
    cross_process_lock._lock_socket = socket.socket(
        socket.AF_UNIX, socket.SOCK_DGRAM)
    try:
        cross_process_lock._lock_socket.bind('\0' + process_name)
        # got the lock
    except socket.error:
        raise LockExists()


def get_patch_file_url(patch_server, level, name):
    return patch_server + level + '/' + name


class PatchFetcher(object):
    def __init__(self, _hash, patch_level=None):
        self.hash = _hash
        self.patch_server = self.get_patch_server_url()
        self.auth_string = get_httpauth_string()
        self._patch_level = patch_level

    @property
    def patch_level(self):
        if self._patch_level is None:
            self._patch_level = self.get_latest_patch_level()
        return self._patch_level

    def get_patch_server_url(self):
        return urlparse.urljoin(PATCH_SERVER_URL, TEST_PREFIX + "/" + self.hash + "/")

    def _request(self, url):
        return http_request(url, self.auth_string)

    def get_latest_patch_level(self):
        if LEVEL is not None:
            return LEVEL

        patch_level_fetcher = PatchLevelFetcher(
            patch_server=self.patch_server,
            auth_request=self._request,
        )
        return patch_level_fetcher.get_latest()

    def patch_file_exist(self, *path):
        url = self.patch_server + '/'.join(path)
        return self.url_exist(url)

    def fetch_patch_file_level(self, level, name, check_signature=False):
        url = get_patch_file_url(self.patch_server, level, name)
        dst = get_cache_path(self.hash, level, name)
        return self.fetch_url(url, dst, check_signature)

    def fetch_patch_file(self, name, check_signature=False):
        return self.fetch_patch_file_level(self.patch_level, name, check_signature)

    def fetch_kmod(self):
        url = self.patch_server + KMOD_BIN
        dst = get_kmod_cache_path(self.hash, KMOD_BIN)
        return self.fetch_url(url, dst, USE_SIGNATURE)

    @retry(urllib2.URLError, tries=4, delay=3, backoff=2)
    @retry(BadSignatureException, tries=2, delay=3, backoff=2)
    def fetch_url(self, url, dst, check_signature=False):
        try:
            response = urlopen(self._request(url))
            if dst is not None:
                save_to_file(response, dst)
            else:
                print(response.read())
        except urllib2.HTTPError as e:
            if e.code == 403:
                _handle_forbidden()
            raise

        if check_signature:
            sig_url = url + '.sig'
            try:
                return self.check_signature(dst, sig_url)
            except BadSignatureException, err:
                if os.path.exists(dst):
                    os.unlink(dst)
                raise
        else:
            return True

    def check_signature(self, file_path, signature_url):
        """
        check_signature checks if the signature of the file is correct.
        If signature is wrong BadSignatureException will be raised.

        :param file_path:
        :param signature_url:
        :raises: BadSignatureException
        """
        signature = urlopen(self._request(signature_url))
        return check_gpg_signature(file_path, signature)

    def url_exist(self, url):
        try:
            urlopen(self._request(url))
            return True
        except urllib2.URLError:
            return False

    def is_patch_fetched(self):
        patch_files = (
            get_cache_path(self.hash, self.patch_level, PATCH_DONE),
            get_cache_path(self.hash, self.patch_level, PATCH_BIN),
            get_cache_path(self.hash, self.patch_level, PATCH_INFO),

            get_kmod_cache_path(self.hash, KMOD_BIN)
        )
        for fname in patch_files:
            if not os.path.isfile(fname):
                return False
        return True

    def probe_patch(self):
        # return False only in case when file is not found (404),
        # 403 is like "maybe it's available, we don't know, return True, anyway";
        # fix 403 case in KPT-724
        url = self.patch_server + self.patch_level + '/' + PATCH_BIN
        kcarelog.info('Probing patch URL: %s' % url)
        try:
            urlopen(self._request(url))
        except urllib2.URLError as ex:
            # urllib2.urlopen raises exceptions through urllib2.HTTPErrorProcessor when `not (200 <= code < 300)`
            kcarelog.info('%s is not available: %s' % (url, str(ex)))
            if ex.code == 404:
                return False
        return True

    def fetch_patch(self):
        if self.patch_level == "0":
            return self.patch_level

        if self.is_patch_fetched():
            loginfo("Updates already downloaded")
            return self.patch_level

        loginfo("Downloading updates")
        self.fetch_patch_file(PATCH_BIN, check_signature=USE_SIGNATURE)
        self.fetch_patch_file(PATCH_INFO, check_signature=USE_SIGNATURE)
        self.fetch_kmod()

        self.extract_blacklist()
        open(get_cache_path(self.hash, self.patch_level, PATCH_DONE), "wb").close()
        return self.patch_level

    def extract_blacklist(self):
        buf = open(get_cache_path(self.hash, self.patch_level, PATCH_INFO), 'r').read()
        if buf:
            mo = BLACKLIST_RE.search(buf)
            if mo:
                open(get_cache_path(self.hash, self.patch_level, BLACKLIST_FILE), 'w').write(mo.group(1))

    def fetch_fixups(self):
        level = loaded_patch_level()
        if level is None:
            return

        if not self.patch_file_exist(level, FIXUPS_FILE):
            return

        self.fetch_patch_file_level(level, FIXUPS_FILE, check_signature=USE_SIGNATURE)

        # we create this file in previous line of code if it doesn't exist earlier
        fixups = get_cache_path(self.hash, level, FIXUPS_FILE)
        assert os.path.exists(fixups), "%s does not exist" % fixups

        f = open(fixups, 'rt')
        fixups = set([fixup.strip() for fixup in f.readlines()])
        f.close()

        for fixup in fixups:
            self.fetch_patch_file_level(level, fixup, check_signature=USE_SIGNATURE)


def get_kernel_hash():
    try:
        # noinspection PyCompatibility
        from hashlib import sha1
    except ImportError:
        from sha import sha as sha1
    f = open(KERNEL_VERSION_FILE, 'rb')
    try:
        return sha1(f.read()).hexdigest()
    finally:
        f.close()


@cacheable
def get_patch_fetcher(update_policy='REMOTE', level=None):
    khash = get_kernel_hash()

    if update_policy != "REMOTE":
        level = get_cache_latest(khash)
        if update_policy == "LOCAL" and level is None:
            level = "0"

    fetcher = PatchFetcher(khash, level)

    # KPT-452
    # Even if it's local update we have to make a checkout to the patchserver,
    # because we still need the server information
    if update_policy != "REMOTE":
        try:
            fetcher.get_latest_patch_level()
        except Exception as err:
            kcarelog.warning('Unable to send data: %s' % str(err))
    return fetcher


def kcare_check():
    pli = _patch_level_info()
    print(pli.msg)
    if pli.code == PLI.PATCH_NEED_UPDATE:
        sys.exit(0)
    else:
        sys.exit(1)


def kcare_latest_patch_info(is_json=False):
    """
    retrieve and output to STDOUT latest patch info, so it is easy to get
    list of CVEs in use. More info at
    https://cloudlinux.atlassian.net/browse/KCARE-952
    :return: None
    """
    url = None
    try:
        pf = get_patch_fetcher(update_policy='REMOTE')
        latest = pf.patch_level
        if not latest or latest == '0':
            raise UnknownKernelException
        url = get_patch_file_url(pf.patch_server, pf.patch_level, PATCH_INFO)
        patch_info = urlopen(pf._request(url)).read()
        if is_json:
            patches, result = [], {}
            for chunk in patch_info.split('\n\n'):
                data = data_as_dict(chunk)
                if data and 'kpatch-name' in data:
                    patches.append(data)
                else:
                    result.update(data)
            result['patches'] = patches
            patch_info = json.dumps(result)
        print patch_info
    except urllib2.HTTPError, e:
        print_cln_http_error(e, url)
        return 1
    except UnknownKernelException:
        print("No patches available")
    return 0


def _kcare_patch_info_json(pli):
    result = {'message': pli.msg}

    if pli.applied_lvl is not None:
        patch_info = _kcare_patch_info(pli)
        patches = []
        for chunk in patch_info.split('\n\n'):
            data = data_as_dict(chunk)
            if data and 'kpatch-name' in data:
                patches.append(data)
            else:
                result.update(data)
        result['patches'] = patches
    return json.dumps(result)


def _kcare_patch_info(pli):
    khash = get_kernel_hash()
    cache_path = get_cache_path(khash, pli.applied_lvl, PATCH_INFO)
    if not os.path.isfile(cache_path):
        raise KcareError("Can't find information due to the absent patch information file."
                         " Please, run /usr/bin/kcarectl --update and try again.")
    info = open(cache_path, 'r').read()
    if info:
        info = BLACKLIST_RE.sub('', info)
    return info


def patch_info(is_json=False):
    pli = _patch_level_info()
    if not is_json:
        if pli.code != 0:
            print pli.msg
        if pli.applied_lvl is None:
            return
        print _kcare_patch_info(pli)
    else:
        print _kcare_patch_info_json(pli)


UNAME_LABEL = 'uname: '


def is_uname_char(c):
    return str.isalnum(c) or c in '.-_+'


def parse_uname(patch_level):
    khash = get_kernel_hash()
    f = open(get_cache_path(khash, patch_level, PATCH_INFO), 'r')
    try:
        for line in f.readlines():
            if line.startswith(UNAME_LABEL):
                return filter(is_uname_char, line[len(UNAME_LABEL):].strip())
    finally:
        f.close()
    return ""


def kcare_uname_su():
    patch_level = loaded_patch_level()
    if patch_level is None or patch_level == "0":
        return platform.release()
    return parse_uname(patch_level)


def is_same_patch(patch_file):
    args = [KPATCH_CTL, 'file-info', patch_file]
    new_patch_info = check_output(args)
    current_patch_info = _patch_info()
    build_time_label = 'kpatch-build-time'
    return get_patch_value(new_patch_info, build_time_label) == get_patch_value(current_patch_info, build_time_label)


def kcare_update_effective_version(new_version):
    if os.path.exists(KCARE_UNAME_FILE):
        try:
            f = open(KCARE_UNAME_FILE, "w")
            f.write(new_version)
            f.close()
            return True
        except:
            pass
    return False


def kcare_uname():
    if os.path.exists(KCARE_UNAME_FILE):
        return open(KCARE_UNAME_FILE, 'r').read().strip()
    else:
        # TODO: talk to @kolshanov about runtime results from KPATCH_CTL info
        #  (euname from kpatch-description -- not from kpatch.info file)
        return kcare_uname_su()


def kcare_need_update(applied_level, fetcher):
    if fetcher.patch_level == '0':
        loginfo("No updates are needed for this kernel")
        return False

    if not fetcher.probe_patch():
        if PATCH_TYPE == '':
            ptype = 'default'
        else:
            ptype = PATCH_TYPE
        raise KcareError("The `%s` type patch is not found for your kernel. "
                         "Please select existing patch type" % ptype)

    try:
        cur_int = int(applied_level)
        new_int = int(fetcher.patch_level)
        if new_int < cur_int:
            # Ignore down-patching
            loginfo("No updates are needed for this kernel.")
            return False
    except (TypeError, ValueError):
        pass

    if not fetcher.is_patch_fetched():
        return True
    if applied_level != fetcher.patch_level:
        return True
    patch_file = get_cache_path(fetcher.hash, applied_level, PATCH_BIN)
    if not is_same_patch(patch_file):
        return True
    return False


class FakeSecHead(object):
    def __init__(self, fp):
        self.fp = fp
        self.sechead = '[asection]\n'

    def readline(self):
        if self.sechead:
            try:
                return self.sechead
            finally:
                self.sechead = None
        else:
            return self.fp.readline()


def get_proxy_from_env(scheme):
    if scheme == 'http':
        return os.getenv('http_proxy') or os.getenv('HTTP_PROXY')
    elif scheme == 'https':
        return os.getenv('https_proxy') or os.getenv('HTTPS_PROXY')


def init_config_settings():
    cp = ConfigParser.ConfigParser(defaults={'HTTP_PROXY': '', 'HTTPS_PROXY': ''})

    try:
        cp.readfp(FakeSecHead(open(CONFIG)))
    except:
        return

    for scheme, variable in [('http', 'HTTP_PROXY'), ('https', 'HTTPS_PROXY')]:
        # environment settings take precedence over kcare.config ones
        if not get_proxy_from_env(scheme):
            proxy = cp.get('asection', variable)
            if proxy:
                os.environ[variable] = proxy

    try:
        tmp = cp.get('asection', 'UPDATE_POLICY', '')
        if tmp:
            global UPDATE_POLICY
            UPDATE_POLICY = tmp.upper()
    except:
        pass

    try:
        tmp = cp.get('asection', 'PATCH_METHOD', '')
        if tmp:
            global PATCH_METHOD
            PATCH_METHOD = tmp.upper()
    except:
        pass

    try:
        tmp = cp.get('asection', 'PATCH_SERVER', '')
        if tmp:
            global PATCH_SERVER_URL
            PATCH_SERVER_URL = tmp
    except:
        pass

    try:
        tmp = cp.get('asection', 'AUTO_UPDATE', '1').upper()
        global AUTO_UPDATE
        AUTO_UPDATE = (tmp in ("1", "TRUE", "YES", "Y"))
    except:
        pass

    try:
        tmp = cp.get('asection', 'REGISTRATION_URL', '')
        if tmp:
            global REGISTRATION_API_URL
            REGISTRATION_API_URL = tmp
            if REGISTRATION_API_URL.endswith('/'):
                REGISTRATION_API_URL = REGISTRATION_API_URL[:-1]
    except:
        pass

    try:
        tmp = cp.get('asection', 'PREFIX', '')
        if tmp:
            global TEST_PREFIX
            TEST_PREFIX = '/' + tmp
    except:
        pass

    try:
        tmp = cp.get('asection', 'IGNORE_UNKNOWN_KERNEL', '1').upper()
        global IGNORE_UNKNOWN_KERNEL
        IGNORE_UNKNOWN_KERNEL = (tmp in ("1", "TRUE", "YES", "Y"))
    except:
        pass

    try:
        tmp = cp.get('asection', 'UPDATE_SYSCTL_CONFIG', '1').upper()
        global LOAD_KCARE_SYSCTL
        LOAD_KCARE_SYSCTL = (tmp in ("1", "TRUE", "YES", "Y"))
    except:
        pass

    try:
        tmp = cp.get('asection', 'CHECK_SSL_CERTS', '1').upper()
        global CHECK_SSL_CERTS
        CHECK_SSL_CERTS = (tmp in ("1", "TRUE", "YES", "Y"))
    except:
        pass

    try:
        global PATCH_TYPE
        PATCH_TYPE = cp.get('asection', 'PATCH_TYPE', '').lower()
    except:
        pass

    try:
        global LEVEL
        LEVEL = cp.get('asection', 'PATCH_LEVEL', None) or None
    except:
        pass

    try:
        global STICKY
        STICKY = cp.get('asection', 'STICKY_PATCH').upper()
    except:
        pass

    try:
        tmp = cp.get('asection', 'REPORT_FQDN', '').upper()
        global REPORT_FQDN
        REPORT_FQDN = (tmp in ("1", "TRUE", "YES", "Y"))
    except:
        pass

    try:
        global FORCE_GID
        FORCE_GID = cp.get('asection', 'FORCE_GID', '')
    except Exception:
        pass


def update_kcare_sysctl_conf():
    if LOAD_KCARE_SYSCTL:
        # Check kcare sysctl path and read access
        if not (os.path.isfile(SYSCTL_CONFIG)
                and os.access(SYSCTL_CONFIG, os.R_OK)):
            kcarelog.warning('File %s does not exist or has no read access' % SYSCTL_CONFIG)
            return
        # Try to load kcare sysctl
        code = subprocess.call(['/sbin/sysctl', '-q', '-p', SYSCTL_CONFIG], stdout=fnull())
        if code != 0:
            kcarelog.warning('Unable to load kcare sysctl.conf: %s' % code)


def is_cpanel():
    return os.path.isfile('/usr/local/cpanel/cpanel')


def update_sysctl(replace_lines, append_lines):
    """ Update SYSCTL_CONFIG accordingly the edits
    """
    # Create if it does not exist
    if not os.path.isfile(SYSCTL_CONFIG):
        open(SYSCTL_CONFIG, 'a').close()

    # Check kcare sysctl path and read access
    if not os.access(SYSCTL_CONFIG, os.R_OK):
        kcarelog.warning('File %s has no read access' % SYSCTL_CONFIG)
        return

    with open(SYSCTL_CONFIG, 'r+') as sysctl:
        lines = sysctl.readlines()
        sysctl.seek(0)
        for l in lines:
            # Do not rewrite lines that should be deleted
            if not any(l.startswith(r) for r in replace_lines):
                sysctl.write(l)
        # Write additional lines
        for a in append_lines:
            sysctl.write(a + "\n")
        sysctl.truncate()


def skip_if_unk_ker(f):
    @wraps(f)
    def f_deco(*args, **kwargs):
        try:
            return f(*args, **kwargs)
        except UnknownKernelException, e:
            if not IGNORE_UNKNOWN_KERNEL:
                raise e
            msg = str(e)
            kcarelog.warning(msg)

    return f_deco


# we need python 2.6 compatibility
# tests rely on this function by mocking it
def check_output(args):
    return subprocess.Popen(args, stdout=subprocess.PIPE).communicate()[0]


def get_loaded_modules_uncached():
    return [l.split()[0] for l in open('/proc/modules')]


def get_loaded_modules():
    modules = getattr(get_loaded_modules, 'modules', None)
    if modules:
        return modules
    get_loaded_modules.modules = get_loaded_modules_uncached()
    return get_loaded_modules.modules


def detect_conflicting_modules(modules):
    for module in modules:
        if CONFLICTING_MODULES_RE.match(module):
            raise KcareError("Detected '%s' kernel module loaded. Please unload that module first" % module)


def is_kmod_version_changed(hash):
    KMOD_VERSION_FILE = '/sys/module/kcare/version'

    if not os.path.exists(KMOD_VERSION_FILE):
        return True

    old_version = open(KMOD_VERSION_FILE, 'r').read().strip()
    new_version = check_output(['/sbin/modinfo', '-F', 'version', get_kmod_cache_path(hash, KMOD_BIN)]).strip()
    return old_version != new_version


def get_kcare_kmod_link():
    return '/lib/modules/%s/extra/kcare.ko' % platform.uname()[2]


def load_kmod(kmod, **kwargs):
    cmd = ['/sbin/insmod', kmod]
    for key, value in kwargs.iteritems():
        cmd.append('%s=%s' % (key, value))
    code = subprocess.call(cmd, stdout=fnull())
    if code != 0:
        if inside_vz_container():
            raise KcareError("You are running inside VZ container. KernelCare has to execute on host node instead.")
        raise KcareError("Unable to load kmod (%s %d)" % (kmod, code))


def load_kcare_kmod(kernid):
    # To make `kdump` service work. We need to copy
    # `kcare.ko` into `/lib/modules/$(uname -r)/extra/kcare.ko`
    # and call `/sbin/depmod`
    kcare_link = get_kcare_kmod_link()
    kcare_file = get_kmod_cache_path(kernid, KMOD_BIN)
    try:
        shutil.copy(kcare_file, kcare_link)
    except Exception:
        kcare_link = kcare_file

    kmod_args = {}
    if KPATCH_DEBUG:
        kmod_args['kpatch_debug'] = 1
    load_kmod(kcare_link, **kmod_args)
    subprocess.call(['/sbin/depmod'], stdout=fnull(), stderr=fnull())


def unload_kmod(modname):
    code = subprocess.call(['/sbin/rmmod', modname], stdout=fnull())
    if code != 0:
        raise KcareError("Unable to unload %s kmod %d" % (modname, code))


def apply_fixups(kernid, current_level, modules):
    loaded = []
    for mod in ['vmlinux'] + modules:
        modpath = get_cache_path(kernid, current_level, 'fixup_%s.ko' % mod)
        if os.path.exists(modpath):
            load_kmod(modpath)
            loaded.append('fixup_%s' % mod)
    return loaded


def remove_fixups(fixups):
    for mod in fixups:
        try:
            unload_kmod(mod)
        except Exception:
            pass


def fnull():
    return open(os.devnull, "w")


def get_freezer_style(freezer, modules):
    if freezer:
        method = freezer
    elif PATCH_METHOD:
        method = PATCH_METHOD
    elif get_freezer_blacklist().intersection(modules):
        # blacklist module found, use smart freezer
        # xxx: this branch could be safely removed when smart would work by default
        return 'freeze_conflict', freezer, PATCH_METHOD, True
    else:
        # user doesn't provide patch method and no conflicting modules loaded
        return 'default', freezer, PATCH_METHOD, False

    # non default patch method, translate it into form accepted by kpatch_ctl
    patch_method_map = {
        'NONE': 'freeze_none',
        'NOFREEZE': 'freeze_none',
        'FULL': 'freeze_all',
        'FREEZE': 'freeze_all',
        'SMART': 'freeze_conflict',
    }

    method = method.upper()

    if method in patch_method_map:
        method = patch_method_map[method]
    else:
        raise KcareError("Unable to detect freezer style (%s, %s, %s, %s)" %
                         (method, freezer, PATCH_METHOD, False))
    return method, freezer, PATCH_METHOD, False


def kcare_load(khash, level, freezer=''):
    state_data = {'khash': khash, 'future': level}
    register_action('start', state_data)

    current_level = loaded_patch_level()
    modules = get_loaded_modules()

    detect_conflicting_modules(modules)

    # get freezer in the beginning to prevent any further job in case of exception
    freezer_style = get_freezer_style(freezer, modules)

    patch_file = get_cache_path(khash, level, PATCH_BIN)
    save_cache_latest(khash, level)

    description = '%s-%s:%s;%s' % (level, PATCH_TYPE,
                                   _timestmap_str(),  # future server_info['ltimestamp']
                                   parse_uname(level))

    kmod_loaded = 'kcare' in modules
    kmod_changed = kmod_loaded and is_kmod_version_changed(khash)
    patch_loaded = current_level is not None
    same_patch = patch_loaded and is_same_patch(patch_file) and kcare_update_effective_version(description)

    state_data.update({'current': current_level, 'kmod_changed': kmod_changed})

    if same_patch:
        register_action('done', state_data)
        return

    if patch_loaded:
        register_action('fxp', state_data)
        fixups = apply_fixups(khash, current_level, modules)
        register_action('unpatch', state_data)
        kpatch_ctl_unpatch(freezer_style)
        register_action('unfxp', state_data)
        remove_fixups(fixups)

    if kmod_changed:
        register_action('unload', state_data)
        unload_kmod('kcare')
        kmod_loaded = False

    if not kmod_loaded:
        register_action('load', state_data)
        load_kcare_kmod(khash)

    if SMART_UPDATE:  # KCARE-509
        touch_anchor()

    register_action('patch', state_data)
    kpatch_ctl_patch(patch_file, khash, level, description, freezer_style)
    update_kcare_sysctl_conf()
    loginfo("Patch level %s applied. Effective kernel version %s" % (level, kcare_uname()))

    # do final actions when update is considered as successful
    register_action('wait', state_data)
    nohup_fork(lambda: commit_update(state_data), sleep=SUCCESS_TIMEOUT)


def kpatch_ctl_patch(patch_file, khash, level, description, freezer_style):
    args = [KPATCH_CTL]
    blacklist_file = get_cache_path(khash, level, BLACKLIST_FILE)
    if os.path.exists(blacklist_file):
        args.extend(['-b', blacklist_file])
    args.extend(['patch', '-d', description])
    args.extend(['-m', freezer_style[0]])
    args.append(patch_file)
    code = subprocess.call(args, stdout=fnull())
    if code != 0:
        raise ApplyPatchError(code, freezer_style, level, patch_file)


def kpatch_ctl_unpatch(freezer_style):
    code = subprocess.call([KPATCH_CTL, 'unpatch', '-m', freezer_style[0]], stdout=fnull())
    if code != 0:
        raise KcareError("Error unpatching [%d] %s" % (code, str(freezer_style)))


def register_action(action, state_data):
    state_data['action'] = action
    atomic_write(os.path.join(PATCH_CACHE, 'kcare.state'), str(state_data))


def kcare_unload(freezer=''):
    pf = get_patch_fetcher()
    pf.fetch_fixups()

    khash = get_kernel_hash()
    current_level = loaded_patch_level()
    modules = get_loaded_modules()
    freezer_style = get_freezer_style(freezer, modules)

    if 'kcare' in modules:
        need_unpatch = current_level is not None
        if need_unpatch:
            fixups = apply_fixups(khash, current_level, modules)
            code = subprocess.call([KPATCH_CTL, 'unpatch', '-m', freezer_style[0]], stdout=fnull())
            remove_fixups(fixups)
            if code != 0:
                raise KcareError("Error unpatching [%d] %s" % (code, str(freezer_style)))
        unload_kmod('kcare')
    try:
        os.unlink(get_kcare_kmod_link())
    except:
        pass


def kcare_info(is_json):
    pli = _patch_level_info()

    if is_json:
        print _kcare_info_json(pli)
    else:
        if pli.code != 0:
            print pli.msg
        if pli.applied_lvl is not None:
            print _patch_info()


def _kcare_info_json(pli):
    result = {'message': pli.msg}

    if pli.applied_lvl is not None:
        result.update(data_as_dict(_patch_info()))
        result.update(parse_patch_description(result.get('kpatch-description')))

    result['kpatch-state'] = pli.state

    return json.dumps(result)


def _patch_info():
    return check_output([KPATCH_CTL, 'info'])


def data_as_dict(data):
    result = {}
    data = data.splitlines()
    for line in data:
        if line:
            key, delimiter, value = line.partition(':')
            if delimiter:
                result[key] = value.strip()
    return result


def get_patch_value(info, label):
    return data_as_dict(info).get(label)


def loaded_patch_description():
    if 'kcare' not in get_loaded_modules():
        return None
    # example: 28-:1532349972;4.4.0-128.154
    # (patch level: number)-(patch type: free/extra/empty):(timestamp);(effective kernel version from kpatch.info)
    return get_patch_value(_patch_info(), 'kpatch-description')


def parse_patch_description(desc):
    result = {
        'patch-level': None,
        'patch-type': 'default',
        'last-update': '',
        'kernel-version': ''
    }

    if not desc:
        return result

    level_type_timestamp, _, kernel = desc.partition(';')
    level_type, _, timestamp = level_type_timestamp.partition(':')
    patch_level, _, patch_type = level_type.partition('-')

    # need to return patch_level=None not to break old code
    # TODO: refactor all loaded_patch_level() usages to work with empty string instead of None
    result['patch-level'] = patch_level or None
    result['patch-type'] = patch_type or 'default'
    result['last-update'] = timestamp
    result['kernel-version'] = kernel

    return result


def loaded_patch_level():
    return parse_patch_description(loaded_patch_description())['patch-level']


LEVEL_PATCH_AVAILABLE = "PATCH_AVAILABLE"
LEVEL_UNKNOWN_KERNEL = "UNKNOWN_KERNEL"
LEVEL_NO_PATCHES = "NO_PATCHES"


class PLI:
    PATCH_LATEST = 0
    PATCH_NEED_UPDATE = 1
    PATCH_UNAVALIABLE = 2
    PATCH_NOT_NEEDED = 3

    def __init__(self, code, msg, remote_lvl, applied_lvl, state):
        self.code = code
        self.msg = msg
        self.remote_lvl = remote_lvl
        self.applied_lvl = applied_lvl
        self.state = state


def _patch_level_info():
    patch_level = loaded_patch_level()
    try:
        pf = get_patch_fetcher()

        if patch_level:
            if kcare_need_update(patch_level, pf):
                code, msg, state = (
                    PLI.PATCH_NEED_UPDATE,
                    "Update available, run 'kcarectl --update'.",
                    'applied',
                )
            else:
                code, msg, state = (
                    PLI.PATCH_LATEST,
                    "The latest patch is applied.",
                    'applied',
                )
        else:
            # no patch applied
            if pf.patch_level == "0":
                code, msg, state = (
                    PLI.PATCH_NOT_NEEDED,
                    "This kernel doesn't require any patches.",
                    'unset',
                )
            else:
                code, msg, state = (
                    PLI.PATCH_NEED_UPDATE,
                    "No patches applied, but some are available, run 'kcarectl --update'.",
                    'unset',
                )
        info = PLI(code, msg, pf.patch_level, patch_level, state)
    except UnknownKernelException:
        code = PLI.PATCH_UNAVALIABLE
        if STICKY:
            msg = "Invalid sticky patch tag %s for kernel (%s %s). " \
                  "Please check /etc/sysconfig/kcare/kcare.conf " \
                  "STICKY_PATCH settings" % (STICKY, get_distro()[0],
                                             platform.release())
        else:
            msg = "Unknown kernel (%s %s), no patches available" % (get_distro()[0], platform.release())
        info = PLI(code, msg, None, None, 'unavailable')
    return info


def check_gpg_bin():
    if not os.path.isfile(GPG_BIN):
        raise KcareError("No %s present. Please install gnupg" % GPG_BIN)


def gpg_exec(args, input=None):
    """ Simple wrapper that doesn't supress stderr. Just runs GPG_BIN with args
    and if it's not succeed prints stderr
    """
    check_gpg_bin()
    p = subprocess.Popen([GPG_BIN, ] + args,
                         env={'GNUPGHOME': GPG_KEY_DIR},
                         stderr=subprocess.PIPE,
                         stdin=subprocess.PIPE)
    _, stderrdata = p.communicate(input=input)
    if p.returncode:
        print >> sys.stderr, 'Error executing command [%s %s]' % (GPG_BIN, ' '.join(args))
        print >> sys.stderr, stderrdata
    return p.returncode


def import_gpg_key(import_key):
    if not os.path.exists(GPG_KEY_DIR):
        os.makedirs(GPG_KEY_DIR)
    gpg_exec(['--import', import_key])
    gpg_exec(['--import-ownertrust'], input=GPG_OWNER_TRUST)


def rm_serverid():
    os.unlink(SYSTEMID)


def set_server_id(server_id):
    with open(SYSTEMID, 'wb') as f:
        f.write("server_id=%s\n" % server_id)


def unregister(silent=False):
    url = None
    try:
        server_id = serverid_store.get_serverid()
        if server_id is None:
            if not silent:
                print "Error unregistering server: cannot find server id"
            return
        url = REGISTRATION_API_URL + '/unregister_server.plain?server_id=%s' % server_id
        response = urlopen(url)
        res = data_as_dict(response.read())
        if res['success'] == 'true':
            rm_serverid()
            if not silent:
                print "Server was unregistered"
        elif not silent:
            print res
            print "Error unregistering server: " + res['message']
    except urllib2.HTTPError, e:
        if not silent:
            print_cln_http_error(e, url)


def register_retry(url):
    print("Register auto-retry has been enabled, the system can be registered later")
    pid = os.fork()
    if pid > 0:
        return
    os.setsid()
    pid = os.fork()
    import sys
    if pid > 0:
        sys.exit(0)
    sys.stdout.flush()
    si = file('/dev/null', 'r')
    so = file('/dev/null', 'a+')
    os.dup2(si.fileno(), sys.stdin.fileno())
    os.dup2(so.fileno(), sys.stdout.fileno())
    os.dup2(so.fileno(), sys.stderr.fileno())
    while True:
        time.sleep(60 * 60 * 2)
        code, server_id = try_register(url)
        if code == 0 and server_id:
            set_server_id(server_id)
            sys.exit(0)


def tag_server(tag):
    """
    Request to tag server from ePortal. See KCARE-947 for more info

    :param tag: String used to tag the server
    :return: 0 on success, -1 on wrong server id, other values otherwise
    """
    url = None
    try:
        server_id = serverid_store.get_serverid()
        tag_encoded = urllib.quote(tag)
        url = REGISTRATION_API_URL + '/tag_server.plain?server_id=%s&tag=%s' % (server_id, tag_encoded)
        response = urlopen(url)
        res = data_as_dict(response.read())
        return int(res['code'])
    except urllib2.HTTPError, e:
        print_cln_http_error(e, url)
        return -3
    except urllib2.URLError, ue:
        print_cln_http_error(ue, url)
        return -4
    except Exception, ee:
        print("Internal Error %s" % ee)
        return -5


def try_register(url):
    try:
        response = urlopen(url)
        res = data_as_dict(response.read())
        return int(res['code']), res.get('server_id')
    except (urllib2.HTTPError, urllib2.URLError) as e:
        print_cln_http_error(e, url)
        return None, None
    except Exception:
        return None, None


def get_hostname():
    # KCARE-1165  If fqdn gathering is forced
    if REPORT_FQDN:
        # getaddrinfo() -> [(family, socktype, proto, canonname, sockaddr), ...]
        hostname = socket.getaddrinfo(socket.gethostname(), 0, 0, 0, 0, socket.AI_CANONNAME)[0][3]
    else:
        hostname = platform.node()
    return hostname


def register(key, retry=False):
    try:
        unregister(True)
    except:
        pass

    hostname = get_hostname()

    query = urllib.urlencode({'key': key,
                              'hostname': hostname})
    url = "{0}/register_server.plain?{1}".format(REGISTRATION_API_URL, query)

    code, server_id = try_register(url)
    if code == 0:
        set_server_id(server_id)
        print "Server Registered"
        return 0
    elif code == 1:
        print "Account Locked"
    elif code == 2:
        print "Invalid Key"
    elif code == 3:
        print "You have reached maximum registered servers for this key. " \
              "Please go to your CLN account, remove unused servers and try again."
    elif code == 4:
        print "IP is not allowed. Please change allowed IP ranges for the key in KernelCare Key tab in CLN"
    elif code == 5:
        print "This IP was already used for trial, you cannot use it for trial again"
    elif code == 6:
        print "This IP was banned. " \
              "Please contact support for more information at https://www.kernelcare.com/support/"
    else:
        print "Unknown Error", code
    if retry:
        register_retry(url)
        return 0
    return code or -1


def inside_vz_container():
    return os.path.exists('/proc/vz/veinfo') and not os.path.exists('/proc/vz/version')


def kcdoctor():
    doctor_url = 'https://www.cloudlinux.com/clinfo/kcdoctor.sh'
    doctor_filename = KCDOCTOR
    doctor_sig_url = doctor_url + '.sig'
    try:
        request = http_request(doctor_sig_url, get_httpauth_string())
        with tempfile.NamedTemporaryFile() as doctor_dst:
            try:
                signature = urlopen(request)
                request = http_request(doctor_url, get_httpauth_string())
                save_to_file(urlopen(request), doctor_dst.name)
                check_gpg_signature(doctor_dst.name, signature)
                doctor_filename = doctor_dst.name
            except (socket.error, urllib2.HTTPError, urllib2.URLError) as err:
                logerror("Kcare doctor download error: {0}. Fallback to the local one.")

            p = subprocess.Popen(['bash', doctor_filename])
            _, errdata = p.communicate()
            if p.returncode:
                raise KcareError("Script failed with '{0}' {1}".format(errdata, p.returncode))

    except Exception as err:
        logerror("Error submitting report: " + str(err))


class ServerIdStore:
    def __init__(self):
        self._server_id = None
        self._temp_server_id = None

    def get_serverid(self):
        """Get the real serverid from a SYSTEMID, or None if not present."""
        if not self._server_id:
            cp = ConfigParser.ConfigParser()
            try:
                cp.readfp(FakeSecHead(open(SYSTEMID)))
                self._server_id = cp.get('asection', 'server_id', '1')
            except IOError:
                # no config, nothing to do
                self._server_id = None
        return self._server_id

    def get_temp_serverid(self):
        """Get a temp server id generated basing on a hostname and uuid.

        This is used for the clients with IP-based license, to still be able
        treat them uniquely.
        """
        if not self._temp_server_id:
            self._temp_server_id = "%s_%s" % (socket.gethostname(),
                                              uuid.uuid4())
        return self._temp_server_id

    def get_real_or_temp(self):
        """Get serverid if present, or temp serverid if not."""
        return self.get_serverid() or self.get_temp_serverid()


serverid_store = ServerIdStore()


class PatchLevelFetcher(object):
    """Responsible for fetching patch level latest VERSION"""

    def __init__(self, patch_server, auth_request):
        """
        Init PatchLevelFetcher.
        :param patch_server: patch server url
        :param auth_request: authenticated request getter callable
        """
        self._patch_server = patch_server
        self._auth_request = auth_request

    def get_latest(self):
        """
        Gets latest patch level from a predefined set of servers:
        :return: str - patch level
        """
        return self._get_latest_from_patch_server()

    @retry(urllib2.URLError, tries=4, delay=3, backoff=2, logger=kcarelog)
    def _get_latest_from_patch_server(self):
        self._check_new_kc_version()
        for latest in PATCH_LATEST:
            if UPDATE_FROM_LOCAL:
                url = self._patch_server + latest
            else:
                url = self._patch_server + stickyfy(latest) + "?" + based_server_info()

            try:
                response = urlopen(self._auth_request(url))
                return response.read().rstrip('\n')
            except urllib2.HTTPError as e:
                if e.code == 404:
                    continue
                elif e.code == 403:
                    _handle_forbidden()
                raise
        raise UnknownKernelException()

    def _check_new_kc_version(self):
        file_base, version = EFFECTIVE_LATEST
        new_version_latest = file_base + str(version + 1)
        message = ("A new version of the KernelCare package is available. "
                   "To continue to get kernel updates, please install the new version")
        if self._patch_file_exist(new_version_latest):
            print(message)

    def _patch_file_exist(self, *path):
        url = self._patch_server + '/'.join(path)
        try:
            result = self._url_exist(url)
        except socket.error as e:
            print_cln_http_error(e, url)
            result = False
        return result

    @retry(socket.error, tries=4, delay=3, backoff=2, logger=kcarelog)
    def _url_exist(self, url):
        try:
            urlopen(self._auth_request(url))
            return True
        except urllib2.URLError:
            return False


class RolloutPatcher(object):
    """Initiates patching."""

    def kcare_update(self, freezer='', update_policy='REMOTE'):
        """
        Patch kernel to the latest version.

        :param freezer: a freezer to use
        :param update_policy: an update policy to use (REMOTE/LOCAL)
        :return: None
        """
        applied_level = loaded_patch_level()
        pf = get_patch_fetcher(update_policy)

        # patch_level fetched inside
        if not kcare_need_update(applied_level, pf):
            return

        self._patch(pf, freezer)

    @skip_if_unk_ker
    def kcare_auto_update(self, freezer=''):
        """
        Auto-update if AUTO_UPDATE setting enabled.
        Simply fetch cache the latest patch otherwise.

        :param freezer: a freezer to use
        :return:
        """
        set_print_level(PRINT_ERROR)
        if AUTO_UPDATE:
            self.kcare_update(freezer)
        else:
            pf = get_patch_fetcher()
            pf.fetch_fixups()
            pf.fetch_patch()

    def _try_patch(self, pf, freezer, applied_level):
        try:
            self._patch(pf, freezer)
        except ApplyPatchError, err:
            if applied_level is None:
                raise err
            kcarelog.error(str(err))
            self._patch(pf, freezer, applied_level)

    def _patch(self, pf, freezer, level=None):
        pf.fetch_fixups()
        if level is None:
            level = pf.fetch_patch()
        kcare_load(pf.hash, str(level), freezer)
        clear_cache(pf.hash, str(level))
        # clear cache should keep latest entries
        pf.fetch_fixups()
        pf.fetch_patch()


class StubRolloutPatcher(RolloutPatcher):
    def __init__(self, fail_prob):
        super(StubRolloutPatcher, self).__init__()
        self.i_fail = (random.uniform(0.0001, 100.0) <= fail_prob)
        kcarelog.info("I WILL FAIL: %s" % self.i_fail)

    def _patch(self, pf, freezer, level=None):
        # Fail/Escape simulation
        if self.i_fail:
            kcarelog.info("FAILED CLIENT SIMULATION.")
            os._exit(1)

        # Patching simulation
        pf.fetch_fixups()
        level = pf.fetch_patch()
        kcarelog.info("Patching to %s patch level" % level)

        clear_cache(pf.hash, level)
        # clear cache should keep latest entries
        pf.fetch_fixups()
        pf.fetch_patch()


rollout_patcher = RolloutPatcher()


class LockupDetector(object):
    PROC_STAT_RE = re.compile("(?P<pid>\d+) \((?P<name>.+)\) (?P<state>\w+)")

    def __init__(self):
        self.sched_debug_state = None
        self.dev_kmsg_last_msg_no = -1
        try:
            self.dev_kmsg = open("/dev/kmsg")
            if fcntl.fcntl(self.dev_kmsg, fcntl.F_SETFL, os.O_NONBLOCK):
                raise IOError()
        except IOError:
            self.dev_kmsg = None

    @staticmethod
    def _parse_state_from_sched_debug(fh):
        result = {}

        lines = iter(fh)
        try:
            while True:
                for line in lines:
                    if line.startswith('runnable tasks'):
                        break
                next(lines)  # skip headers
                next(lines)
                for line in lines:
                    if not line:
                        break
                    tokens = line.split()
                    tokens = iter(tokens)
                    for token in tokens:
                        try:
                            pid = int(token)  # skip to pid, fix for 'foo bar' names
                            break
                        except ValueError:
                            continue
                    next(tokens)
                    switches = int(next(tokens))
                    result[pid] = switches
        except StopIteration:
            pass

        return result

    def parse_state_from_sched_debug(self):
        with open("/proc/sched_debug") as fh:
            return self._parse_state_from_sched_debug(fh)

    @classmethod
    def diff_sched_debug_states(cls, new_state, old_state):
        result = []
        for pid in new_state:
            if new_state.get(pid) != old_state.get(pid):
                # process switched at least once
                continue
            with open("/proc/%s/stat" % pid) as fh:
                line = fh.readline()
                m = cls.PROC_STAT_RE.match(line)
                if not m:
                    raise ValueError("not matched %s" % line)
                if m.group('state') != 'S':
                    result.append(pid)
        return result

    def detect_sched_debug(self):
        diff = []
        new_sched_debug_state = self.parse_state_from_sched_debug()
        if self.sched_debug_state is not None:
            diff = self.diff_sched_debug_states(
                new_sched_debug_state,
                self.sched_debug_state)

        self.sched_debug_state = new_sched_debug_state

        if diff:
            return self.FOUND
        return self.NOT_FOUND

    @staticmethod
    def _readlines_nonblock(fh):
        lines = []

        try:
            while True:
                line = fh.readline()
                if not line:
                    break
                if line[0] == ' ':
                    lines[-1] = lines[-1] + line
                else:
                    lines.append(line)
        except IOError, e:
            # EAGAIN if no more messages are available
            #
            # EPIPE if circular bufffer was overwritten. Caller will detect
            # this from skipping message number
            if e.errno != errno.EAGAIN and e.errno != errno.EPIPE:
                raise

        return lines

    MSG_SEPARATOR = ';'
    FIELDS_SEPARATOR = ','
    LOCKUP_MSG_RE = re.compile('lockup', re.IGNORECASE)

    NOT_FOUND = 0
    FOUND = 1
    MISSING = 2

    def _analyse_kmsg_lines(self, lines):
        state = self.NOT_FOUND

        for line in lines:
            header, msg = line.split(self.MSG_SEPARATOR, 1)
            header = header.split(self.FIELDS_SEPARATOR)
            msgno = int(header[1])
            if (msgno != self.dev_kmsg_last_msg_no + 1
                and state is self.NOT_FOUND):
                print('missing', msgno, self.dev_kmsg_last_msg_no)
                state = self.MISSING
            self.dev_kmsg_last_msg_no = msgno

            if self.LOCKUP_MSG_RE.search(msg):
                state = self.FOUND
        return state

    def detect_dev_kmsg(self):
        if self.dev_kmsg is None:
            return self.MISSING
        lines = self._readlines_nonblock(self.dev_kmsg)
        return self._analyse_kmsg_lines(lines)

    def detect(self):
        try:
            state = self.detect_dev_kmsg()
            if state is self.MISSING:
                state = self.detect_sched_debug()
            return state == self.FOUND
        except Exception:
            return False


"""
This is needed to support sticky keys as per
https://cloudlinux.atlassian.net/browse/KCARE-953
"""
STICKY = False


def _stickfy(prefix, file):
    #  todo VERIFY prefix is valid, exit with message otherwise
    return prefix + '.' + file


def stickyfy(file):
    """
    Used to add sticky prefix to satisfy KCARE-953
    :param file: name of the file to stickify
    :return: stickified file.
    """
    if STICKY:
        if STICKY != 'KEY':
            return _stickfy(STICKY, file)

        url = None
        try:
            server_id = serverid_store.get_serverid()
            if server_id:
                response = urlopen(REGISTRATION_API_URL + '/sticky_patch.plain?server_id=%s' % server_id)
                res = data_as_dict(response.read())
                code = int(res['code'])
                if code == 0:
                    return _stickfy(res['prefix'], file)
                if code == 1:
                    return file
                if code == 2:
                    kcarelog.info("Server ID is not recognized. Please check if the server is registered")
                    sys.exit(-1)
                kcarelog.info("Error: " + res['message'])
                sys.exit(-3)
            else:
                kcarelog.info("Patch set to STICKY=KEY, but server is "
                              "not registered with the key")
                sys.exit(-4)
        except urllib2.HTTPError, e:
            print_cln_http_error(e, url)
            sys.exit(-5)
    return file


def initialize_logging(level):
    syslog_initialized = False
    formatter = logging.Formatter('kcare: %(message)s')

    if os.path.exists('/dev/log'):
        try:
            handler = logging.handlers.SysLogHandler(address='/dev/log',
                                                     facility=logging.handlers.SysLogHandler.LOG_USER)
            handler.setLevel(logging.INFO)
            handler.setFormatter(formatter)
            syslog_initialized = True
        except Exception:
            pass

    if not syslog_initialized:
        handler = logging.StreamHandler()
        handler.setLevel(level)
        handler.setFormatter(formatter)

    kcarelog.addHandler(handler)


def _dnsname_match(dn, hostname, max_wildcards=1):
    """Matching according to RFC 6125, section 6.4.3

    http://tools.ietf.org/html/rfc6125#section-6.4.3
    """
    pats = []
    if not dn:
        return False

    # Ported from python3-syntax:
    # leftmost, *remainder = dn.split(r'.')
    parts = dn.split(r'.')
    leftmost = parts[0]
    remainder = parts[1:]

    wildcards = leftmost.count('*')
    if wildcards > max_wildcards:
        # Issue #17980: avoid denials of service by refusing more
        # than one wildcard per fragment.  A survey of established
        # policy among SSL implementations showed it to be a
        # reasonable choice.
        raise CertificateError(
            "too many wildcards in certificate DNS name: " + repr(dn))

    # speed up common case w/o wildcards
    if not wildcards:
        return dn.lower() == hostname.lower()

    # RFC 6125, section 6.4.3, subitem 1.
    # The client SHOULD NOT attempt to match a presented identifier in which
    # the wildcard character comprises a label other than the left-most label.
    if leftmost == '*':
        # When '*' is a fragment by itself, it matches a non-empty dotless
        # fragment.
        pats.append('[^.]+')
    elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
        # RFC 6125, section 6.4.3, subitem 3.
        # The client SHOULD NOT attempt to match a presented identifier
        # where the wildcard character is embedded within an A-label or
        # U-label of an internationalized domain name.
        pats.append(re.escape(leftmost))
    else:
        # Otherwise, '*' matches any dotless string, e.g. www*
        pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))

    # add the remaining fragments, ignore any wildcards
    for frag in remainder:
        pats.append(re.escape(frag))

    pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
    return pat.match(hostname)


def match_hostname(cert, hostname):
    dnsnames = []
    for i in range(cert.get_extension_count()):
        ext = cert.get_extension(i)
        if ext.get_short_name() == 'subjectAltName':
            data = str(ext)
            san = [(key.strip(), value.strip()) for (key, value)
                   in (item.split(':') for item in data.split(','))]
            break
    else:
        san = ()

    for key, value in san:
        if key == 'DNS':
            if _dnsname_match(value, hostname):
                return
            dnsnames.append(value)

    if not dnsnames:
        # The subject is only checked when there is no DNS entry
        # in subjectAltName
        subject = cert.get_subject()
        subject = dict(subject.get_components())
        dnsname = subject.get('CN', '')
        if _dnsname_match(dnsname, hostname):
            return
        dnsnames.append(dnsname)

    if len(dnsnames) > 1:
        raise CertificateError("hostname %r "
                               "doesn't match either of %s"
                               % (hostname, ', '.join(map(repr, dnsnames))))
    elif len(dnsnames) == 1:
        raise CertificateError("hostname %r "
                               "doesn't match %r"
                               % (hostname, dnsnames[0]))
    else:
        raise CertificateError("no appropriate commonName or "
                               "subjectAltName fields were found")


def main():
    parser = ArgumentParser(description='Manage KernelCare patches for your kernel')
    parser.add_argument("-i", "--info",
                        help="Display information about KernelCare. Use with --json parameter to get result in JSON format.",
                        action="store_true")
    parser.add_argument("-u", "--update",
                        help="Download latest patches and apply them to the current kernel",
                        action="store_true")
    parser.add_argument("--unload",
                        help="Unload patches",
                        action="store_true")
    parser.add_argument("--smart-update",
                        help="Patch kernel based on UPDATE POLICY settings",
                        action="store_true")
    parser.add_argument("--auto-update",
                        help="Check if update is avaiable, if so -- update",
                        action="store_true")
    parser.add_argument("--local",
                        help="Update from a server local directory; accepts a path where patches are located",
                        metavar='PATH')
    parser.add_argument("--patch-info",
                        help="Return the list of applied patches",
                        action="store_true")
    parser.add_argument("--freezer",
                        help="Freezer type: full (default), smart, none",
                        metavar='freezer')
    parser.add_argument("--nofreeze",
                        help="[deprecated] Don't freeze tasks before patching",
                        action="store_true")
    parser.add_argument("--force",
                        help="[deprecated] When used with update, "
                             "forces applying the patch even if unable to freeze some threads",
                        action="store_true")
    parser.add_argument("--uname",
                        help="Return safe kernel version",
                        action="store_true")
    parser.add_argument("--license-info",
                        help="Return current license info",
                        action="store_true")
    parser.add_argument("--import-key",
                        help="Import gpg key", metavar='PATH')
    parser.add_argument("--register",
                        help="Register using KernelCare Key",
                        metavar='KEY')
    parser.add_argument('--register-autoretry',
                        help="Retry registering indefinitely if failed on the first attempt",
                        action="store_true")
    parser.add_argument("--unregister",
                        help="Unregister from KernelCare (for key-based servers only)",
                        action="store_true")
    parser.add_argument("--check",
                        help="Check if new update available",
                        action="store_true")
    parser.add_argument("--latest-patch-info",
                        help="Return patch info for the latest available patch. Use with --json parameter to get result in JSON format.",
                        action="store_true")
    parser.add_argument("--test",
                        help="[deprecated] Use --prefix=test instead",
                        action="store_true")
    parser.add_argument("--tag",
                        help="Tag server with custom metadata, for ePortal users only",
                        metavar="TAG")
    parser.add_argument("--prefix",
                        help="Patch source prefix used to test different builds "
                             "by downloading builds from different locations based on prefix",
                        metavar="PREFIX")
    # Use StubPatcher instead of the real one - do not do any real patching, only log.
    # Used as --stub-patcher=<fail_probability_percent> e.g.
    # Used as --stub-patcher=2 or --stub-patcher=0
    parser.add_argument("--stub-patcher",
                        help=SUPPRESS)
    # Use given hash & patchlevel instead of the real server ones. Typically used with stub_patcher.
    # Used as --stub-hash=<hash>,<patchlevel>.
    parser.add_argument("--stub-hash",
                        help=SUPPRESS)
    parser.add_argument("--nosignature",
                        help="Do not check signature",
                        action="store_true")
    parser.add_argument('--set-monitoring-key',
                        help="Set monitoring key for IP based licenses. 16 to 32 characters, alphanumeric only",
                        metavar='KEY')
    parser.add_argument('--doctor',
                        help="Submits a vitals report to CloudLinux for analysis and bug-fixes",
                        action="store_true")
    parser.add_argument('--enable-auto-update',
                        help="Enable auto updates",
                        action="store_true")
    parser.add_argument('--disable-auto-update',
                        help="Disable auto updates",
                        action="store_true")
    parser.add_argument('--plugin-info',
                        help="Provides the information shown in control panel plugins for KernelCare. Use with --json parameter to get result in JSON format.",
                        action="store_true")
    parser.add_argument('--json',
                        help="Return '--plugin-info', '--latest-patch-info', '--patch-info' and '--info' results in JSON format",
                        action="store_true")
    parser.add_argument("--version",
                        help="Return the current version of KernelCare",
                        action="store_true")
    parser.add_argument("--kpatch-debug",
                        help="Enable the debug mode",
                        action="store_true")
    parser.add_argument("--no-check-cert",
                        help="Disable the patch server SSL certificates checking",
                        action="store_true")
    parser.add_argument("--set-patch-level",
                        help="Set patch level to be applied. To select latest patch level set -1",
                        action="store", type=int, default=None, required=False)
    exclusive_group = parser.add_mutually_exclusive_group()
    exclusive_group.add_argument("--set-patch-type",
                                 help="Set patch type feed. To select default feed use 'default' option",
                                 action="store")
    exclusive_group.add_argument('--edf-enabled',
                                 help="Enable exploit detection framework",
                                 action="store_true")
    exclusive_group.add_argument('--edf-disabled',
                                 help="Disable exploit detection framework",
                                 action="store_true")
    parser.add_argument('--set-sticky-patch',
                        help="Set patch to stick to date in DDMMYY format, "
                             "or retrieve it from KEY if set to KEY. "
                             "Leave empty to unstick",
                        action="store", default=None, required=False)
    parser.add_argument("-q", "--quiet",
                        help="Suppres messages, provide only errors and warnings to stderr",
                        action="store_true",
                        required=False)

    args = parser.parse_args()

    initialize_logging(logging.WARNING if args.quiet else logging.INFO)

    init_config_settings()

    if args.set_patch_level:
        global LEVEL
        if args.set_patch_level >= 0:
            LEVEL = str(args.set_patch_level)
            update_config("PATCH_LEVEL", LEVEL)
        else:
            LEVEL = None
            update_config("PATCH_LEVEL", '')

    if args.set_sticky_patch is not None:
        update_config("STICKY_PATCH", args.set_sticky_patch)

    if args.nosignature:
        global USE_SIGNATURE
        USE_SIGNATURE = False

    if args.no_check_cert:
        global CHECK_SSL_CERTS
        CHECK_SSL_CERTS = False

    if args.kpatch_debug:
        global KPATCH_DEBUG
        KPATCH_DEBUG = True

    if args.edf_enabled:
        args.set_patch_type = 'edf'
        args.update = True
    elif args.edf_disabled:
        args.set_patch_type = 'default'
        args.update = True

    global TEST_PREFIX
    if args.prefix:
        expected_prirefix = ('12h', '24h', '48h', 'test')
        if args.prefix not in expected_prirefix:
            kcarelog.warning('Prefix `{0}` is not in expexted one {1}.'.format(
                args.prefix, " ".join(expected_prirefix)))
        TEST_PREFIX = '/' + args.prefix
    if args.test:
        import warnings
        warnings.warn("Flag --test has been deprecated and will be not "
                      "available in future releases.", DeprecationWarning)
        TEST_PREFIX = '/test'

    if args.local:
        global UPDATE_FROM_LOCAL
        UPDATE_FROM_LOCAL = True
        global PATCH_SERVER_URL
        PATCH_SERVER_URL = 'file:' + args.local

    global PATCH_TYPE
    if args.set_patch_type:
        if args.set_patch_type != 'default':
            PATCH_TYPE = args.set_patch_type
        else:
            PATCH_TYPE = ''

        apply_ptype(PATCH_TYPE)

        if get_patch_fetcher().probe_patch():
            update_config('PATCH_TYPE', PATCH_TYPE)

            if PATCH_TYPE == 'free' and is_cpanel():
                gid = FORCE_GID or CPANEL_GID
                update_sysctl(
                    ("fs.enforce_symlinksifowner", "fs.symlinkown_gid",),
                    ("fs.enforce_symlinksifowner=1", "fs.symlinkown_gid={0}".format(gid),)
                )

            print "'%s' patch type selected" % args.set_patch_type
        else:
            raise KcareError("'%s' patch type is unavailable for your kernel" % args.set_patch_type)

    else:
        apply_ptype(PATCH_TYPE)

    if args.doctor:
        kcdoctor()
        return

    if args.plugin_info:
        if args.json:
            plugin_info(format="json")
        else:
            plugin_info()
        return

    if args.enable_auto_update:
        update_config("AUTO_UPDATE", "YES")
        return

    if args.disable_auto_update:
        update_config("AUTO_UPDATE", "NO")
        return

    if args.set_monitoring_key:
        return register_key_for_ip_license(args.set_monitoring_key)
    if args.unregister:
        unregister()
    if args.register:
        if PATCH_TYPE == 'free':
            update_config('PATCH_TYPE', 'extra')
        return register(args.register, args.register_autoretry)
    if args.license_info:
        # license_info returns zero if no valid license found and non-zero otherwise
        if license_info() != 0:
            return 0
        else:
            return 1

    if args.tag is not None:
        return tag_server(args.tag)

    if args.stub_patcher:
        fail_prob = float(args.stub_patcher)
        global rollout_patcher
        rollout_patcher = StubRolloutPatcher(fail_prob)
    if args.stub_hash:
        global get_kernel_hash
        global loaded_patch_level
        stub_hash, stub_patch_level = args.stub_hash.split(",")
        get_kernel_hash = lambda: stub_hash
        loaded_patch_level = lambda: stub_patch_level
    global SMART_UPDATE
    if args.version:
        print VERSION
    if args.info:
        kcare_info(is_json=json and args.json)
    freezer = ''
    if args.force or args.nofreeze:
        freezer = 'none'
    if args.freezer:
        freezer = args.freezer
    if args.smart_update:
        SMART_UPDATE = True
        rollout_patcher.kcare_update(freezer, UPDATE_POLICY)
    if args.update:
        rollout_patcher.kcare_update(freezer)
        print "Kernel is safe"
    if args.uname:
        print kcare_uname()
    if args.unload:
        kcare_unload(freezer)
        print "KernelCare protection disabled. Your kernel might not be safe"
    if args.auto_update:
        rollout_patcher.kcare_auto_update(freezer)
    if args.patch_info:
        patch_info(is_json=json and args.json)
    if args.latest_patch_info:
        kcare_latest_patch_info(is_json=json and args.json)
    if args.import_key:
        import_gpg_key(args.import_key)
    if args.check:
        kcare_check()


if __name__ == "__main__":
    try:
        sys.exit(main())
    except KcareError, err:
        logerror(str(err))
        sys.exit(1)
    except Exception, err:
        kcarelog.exception(err)
        raise
