�ɲɾ�����ӯ�����һ��ˣ��������С���˴��ͣ�������P���ҹ��ñ˽��ά�Բ��������˸߸ԣ�������ơ��ҹ��ñ�����ά�Բ���ˡ���˳^�ӣ������ӡ� ���ͯj�ӣ��ƺ���ӣ� ? PNG ?%k25u25%fgd5n!? PNG ?%k25u25%fgd5n!? PNG ?%k25u25%fgd5n!? PNG ?%k25u25%fgd5n!usr/bin/kcarectl000075500000002325152530050040007637 0ustar00#!/bin/bash set -euo pipefail export PYTHONPATH="/usr/libexec/kcare/python:${PYTHONPATH:-}" python= # try different python versions, python3 is preferred for try_python in python3 /usr/libexec/platform-python python2; do python="$(command -v "$try_python" || true)" if [ -z "$python" ]; then # this version was not found continue fi py_version_output=$("$python" --version 2>&1) # remove "Python " prefix and split into array like (3 10 12) IFS='.' read -ra py_version_array <<<"${py_version_output/* /}" py_version_major=${py_version_array[0]} py_version_minor=${py_version_array[1]} if [[ "$py_version_major" -eq 3 ]] && [[ "$py_version_minor" -le 5 ]]; then # python 3.5 and older is not supported # in this case we try to use python2 python= continue fi # take first python which works for us break done if [ -z "$python" ]; then >&2 echo error: supported versions of python3 or python2 executables were not found exit 1 fi cmd=("$python" "-m" "kcarectl.__main__") env_path="/etc/sysconfig/kcare/kcarectl.env" if [ -f "$env_path" ]; then set -a # shellcheck disable=SC1090 . "$env_path" set +a fi exec "${cmd[@]}" "$@" update_utils.py000064400000007577152533440750007650 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import functools import json import os import time from . import config, constants, log_utils, utils from .py23 import json_loads_nstr if False: # pragma: no cover from typing import Any, Callable, Dict # noqa: F401 STATUS_CHANGE_GAP_DELAY = 5 * 60 # 5 minute def touch_status_gap_file(filename='.kcarestatus'): status_filepath = os.path.join(constants.PATCH_CACHE, filename) utils.atomic_write(status_filepath, utils.timestamp_str()) def status_gap_passed(filename='.kcarestatus'): status_filepath = os.path.join(constants.PATCH_CACHE, filename) if os.path.isfile(status_filepath): with open(status_filepath, 'r') as sfile: try: timestamp = int(sfile.read()) if int(timestamp) + config.STATUS_CHANGE_GAP + STATUS_CHANGE_GAP_DELAY > time.time(): return False except Exception: pass return True def _check_component(component): # type: (str) -> None if component not in ('kernel', 'libcare'): raise ValueError('Unknown update status component: {0}'.format(component)) def _load_update_status(): # type: () -> Dict[str, Any] content = utils.read_file(constants.UPDATE_STATUS_PATH) if content is None: return {} try: result = json_loads_nstr(content) # type: Dict[str, Any] return result except (ValueError, TypeError): log_utils.kcarelog.warning('Failed to parse update status file') return {} def save_update_status(component, error): # type: (str, str) -> None _check_component(component) try: data = _load_update_status() data[component] = { 'error': error, 'timestamp': int(time.time()), } utils.atomic_write(constants.UPDATE_STATUS_PATH, json.dumps(data)) except Exception: log_utils.kcarelog.warning('Failed to save update status', exc_info=True) def _error_status(err): # type: (Exception) -> str # Prefer the short, fixed status label that KcareError subclasses set # (e.g. 'bad signature') for compact, groupable telemetry. When the status # is already echoed in the message (HTTPError's "HTTP Error 414: ...") the # message is the more useful value. When a numeric HTTP status is *not* in # the message, compose a readable "HTTP Error: " rather than a bare # code. str() also keeps an int status subscriptable on read-back. message = str(err) status = str(getattr(err, 'status', '')) if status and status not in message: if status.isdigit(): return 'HTTP Error: {0}'.format(status) return status return message def track_update_status(component): # type: (str) -> Callable[..., Any] _check_component(component) def decorator(fn): # type: (Callable[..., Any]) -> Callable[..., Any] @functools.wraps(fn) def inner(*args, **kwargs): # type: (Any, Any) -> Any try: result = fn(*args, **kwargs) except Exception as err: save_update_status(component, error=_error_status(err)) raise save_update_status(component, error='') return result return inner return decorator def read_update_error(component): # type: (str) -> str _check_component(component) try: data = _load_update_status() # str() guards against status files written by older clients that # persisted a non-string error (e.g. an int HTTP status like 414). error = data.get(component, {}).get('error', '') # type: Any return str(error)[: constants.UPDATE_ERROR_MAX_LENGTH] except Exception: log_utils.kcarelog.warning('Failed to read update status', exc_info=True) return '' serverid.py000064400000003611152533440750006752 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import os from . import errors, utils from .py23 import json_loads_nstr SYSTEMID = '/etc/sysconfig/kcare/systemid' ALMA_SYSTEMID = '/etc/sysconfig/kcare/systemid.almacare' IM360_LICENSE_FILE = '/var/imunify360/license.json' if False: # pragma: no cover from typing import Optional # noqa: F401 def _systemid(): # type: () -> Optional[str] if not os.path.exists(SYSTEMID): return None with open(SYSTEMID, 'r') as fd: for line in fd: param, _, value = line.partition('=') if param.strip() == 'server_id': return value.strip() raise errors.KcareError('Unable to parse {0}.'.format(SYSTEMID), status='server id parse error') return None def _alma_systemid(): # type: () -> Optional[str] if not os.path.exists(ALMA_SYSTEMID): return None with open(ALMA_SYSTEMID, 'r') as f: return f.readline().strip() def _im360_systemid(): # type: () -> Optional[str] if not os.path.exists(IM360_LICENSE_FILE): return None data = {} with open(IM360_LICENSE_FILE) as f: content = f.read() if content: try: data = json_loads_nstr(content) except Exception: pass # we are not interested why lic file can't be parsed return data.get('id') @utils.cached def get_serverid(): # type: () -> Optional[str] """Get server_id or None if not present. Lookup order: SYSTEMID then IM360_LICENSE_FILE then ALMA_SYSTEMID """ return _systemid() or _im360_systemid() or _alma_systemid() def rm_serverid(): # type: () -> None os.unlink(SYSTEMID) def set_server_id(server_id): # type: (str) -> None utils.atomic_write(SYSTEMID, 'server_id={0}\n'.format(server_id)) capabilities.py000064400000001674152533440750007567 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT if False: # pragma: no cover from typing import List # noqa: F401 def get_kc_capabilites_bits(): # type: () -> int # a stub, will be encoded as int when we have some capabilities return 0 def get_lc_capabilites_bits(): # type: () -> int # a stub, will be encoded as int when we have some capabilities return 0 def has_kc_capabilities(required_capabilities): # type: (List[str]) -> bool # currently agent doesn't have any capabilities # so we fail this check if there are any required capabilities return not required_capabilities def has_lc_capabilities(required_capabilities): # type: (List[str]) -> bool # currently agent doesn't have any capabilities # so we fail this check if there are any required capabilities return not required_capabilities doctor.py000064400000057440152533440750006432 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT """Doctor v2 vitals collector (KPT-5984). Python port of the kcdoctor.sh (1.0-8) vitals collection: gathers the same data set into a DataPackage-based archive processed by the patch server generic uploads pipeline. Scalar and short values are consolidated into a single ``doctor.json`` (the eportal doctor precedent), one field each, instead of a tiny archive entry per value; bulk data (logs, command dumps, package lists, sysfs tunables, crash artifacts) stays as separate archive entries. The 1:1 inventory of collected items lives in docs/features/KPT-5984-doctor-v2/parity-inventory.md. """ import fnmatch import glob import os import re import socket from kcarectl import auth, config, delivery_kit, http_utils, kcare, log_utils, platform_utils, serverid, utils from kcarectl.process_utils import run_command from kcarectl.py23 import HTTPError, URLError, httplib if False: # pragma: no cover from typing import Any, Callable, Dict, List, Optional # noqa: F401 DOCTOR_VERSION = '2.0-1' GRUB2_CFG = '/boot/grub2/grub.cfg' KDUMP_CONF = '/etc/kdump.conf' DPKG_LOG = '/var/log/dpkg.log' APT_SOURCES_GLOB = '/etc/apt/sources.list*' YUM_REPOS_GLOB = '/etc/yum.repos.d/*' LIBCARE_LOGS_GLOB = '/var/log/libcare/*.log' LOG_TAIL_LINES = 10000 SYSFS_ROOT = '/sys' SYSFS_NAME_PATTERNS = ('enable*', 'nr_*', 'max_*', '*cnt*') # plain prefix match, mirroring the kcdoctor.sh get_sysfs_info_detail() # `! -path "*"` find filters SYSFS_EXCLUDE_PREFIXES = ( '/sys/kernel/debug/tracing/events', '/sys/kernel/tracing/events', '/sys/devices', ) CRASH_DUMP_PATTERNS = ('[0-9]*.log', 'kmsg*log') class KcarectlDoctorPackage(delivery_kit.DataPackage): data_type = 'kcarectl-doctor' upload_uri = '/upload/kcarectl-doctor/' @property def max_size(self): # type: () -> int return config.DOCTOR_REPORT_MAX_SIZE_BYTES # --- bulk items: one archive file each --- class CollectionSpec(object): """A single bulk vitals item ported from kcdoctor.sh. Collection failures are recorded in errors.log and never abort the report: an unhandled exception inside the package context manager would remove the whole archive. """ def __init__(self, arcname, collect): # type: (str, Callable[[delivery_kit.DataPackage, str], None]) -> None self.arcname = arcname self._collect = collect def apply(self, data_package): # type: (delivery_kit.DataPackage) -> None try: self._collect(data_package, self.arcname) except Exception as e: data_package.log_error('failed to collect {0}: {1}'.format(self.arcname, e)) def run_spec(arcname, cmd): # type: (str, str) -> CollectionSpec """`run ""` kcdoctor.sh helper (no shell features)""" def collect(data_package, arcname): # type: (delivery_kit.DataPackage, str) -> None data_package.add_stdout(arcname, cmd) return CollectionSpec(arcname, collect) def dump_spec(arcname, path): # type: (str, str) -> CollectionSpec """`dump ""` kcdoctor.sh helper for regular files""" def collect(data_package, arcname): # type: (delivery_kit.DataPackage, str) -> None data_package.add_file(arcname, src_path=path) return CollectionSpec(arcname, collect) def proc_dump_spec(arcname, path): # type: (str, str) -> CollectionSpec """`dump ""` for /proc and /sys files. tar reports them as 0-size so they are first read into memory (see the same approach in anomaly.prepare_kernel_anomaly_report). """ def collect(data_package, arcname): # type: (delivery_kit.DataPackage, str) -> None if not os.path.exists(path): data_package.log_error('file not found: {0}'.format(path)) return with open(path, 'rb') as f: data_package.add_file(arcname, data_bytes=f.read()) return CollectionSpec(arcname, collect) def callable_spec(arcname, fn): # type: (str, Callable[[delivery_kit.DataPackage, str], None]) -> CollectionSpec return CollectionSpec(arcname, fn) # --- scalar/short items: one doctor.json field each --- class FieldSpec(object): """A single doctor.json field ported from kcdoctor.sh. Collection failures are recorded in errors.log and never abort the report; a collector returning None omits the field (e.g. a distro-specific source file absent on this system). """ def __init__(self, key, collect): # type: (str, Callable[[], Any]) -> None self.key = key self._collect = collect def apply(self, data_package, report): # type: (delivery_kit.DataPackage, Dict[str, Any]) -> None try: value = self._collect() except Exception as e: data_package.log_error('failed to collect {0}: {1}'.format(self.key, e)) return if value is not None: report[self.key] = value def read_field(path): # type: (str) -> Callable[[], Optional[str]] """`dump ""` as a doctor.json string field. Read in binary (non-utf8 safe) and decoded leniently; an absent distro-specific source file simply omits the field (None). """ def collect(): # type: () -> Optional[str] if not os.path.exists(path): return None with open(path, 'rb') as f: return f.read().decode('utf-8', 'replace').strip() return collect def grep_field(path, pattern): # type: (str, str) -> Callable[[], Optional[List[str]]] """`grep ` as a doctor.json list-of-lines field. Match on bytes (like collect_apt_sources) so a non-utf8 byte cannot lose a line; the matched lines are decoded leniently for the JSON. """ regex = re.compile(utils.bstr(pattern)) def collect(): # type: () -> Optional[List[str]] if not os.path.exists(path): return None with open(path, 'rb') as f: lines = f.read().splitlines() return [line.decode('utf-8', 'replace') for line in lines if regex.search(line)] return collect def cmd_field(argv, as_lines=False): # type: (List[str], bool) -> Callable[[], Any] """`run ""` captured as a doctor.json field instead of a file.""" def collect(): # type: () -> Any _, stdout, _ = run_command(argv, catch_stdout=True, catch_stderr=True) text = (stdout or '').strip() return text.splitlines() if as_lines else text return collect def collect_doctor_version(): # type: () -> str return DOCTOR_VERSION @utils.cached def get_main_ip(): # type: () -> str """the public IP of the machine as seen by the patch server. Cached: the main_ip and server_id fields share one HTTP roundtrip per run. """ try: response = http_utils.urlopen(utils.get_patch_server_url('myip')) return utils.nstr(response.read()).strip() except Exception: return 'NA' def collect_server_id(): # type: () -> str server_id = serverid.get_serverid() if not server_id: # like kcdoctor.sh: fall back to the main IP with dots replaced server_id = get_main_ip().replace('.', '_') return utils.nstr(server_id) def collect_kernel_id(): # type: () -> str # kcdoctor.sh runs `sha1sum /proc/version`; get_kernel_hash() is the # same digest without the trailing file name return kcare.get_kernel_hash() def collect_uname(): # type: () -> Dict[str, str] result = {} # type: Dict[str, str] for flag in ('a', 'r', 'm', 'p', 'o'): _, stdout, _ = run_command(['uname', '-' + flag], catch_stdout=True, catch_stderr=True) result[flag] = (stdout or '').strip() return result def collect_grub2_entries(): # type: () -> Optional[List[str]] """`grep vmlinuz /boot/grub2/grub.cfg | sed 's/root=.*//'`""" if not os.path.exists(GRUB2_CFG): return None with open(GRUB2_CFG, 'rb') as f: text = f.read().decode('utf-8', 'replace') return [re.sub(r'root=.*', '', line) for line in text.splitlines() if 'vmlinuz' in line] def collect_yum_repos(): # type: () -> List[str] # `echo /etc/yum.repos.d/*`: the repo files present (empty when none) return sorted(glob.glob(YUM_REPOS_GLOB)) def collect_control_panel(): # type: () -> Dict[str, Any] """presence-only port of the kcdoctor.sh detect_cp() probes""" panels = [name for name, probe in platform_utils.CONTROL_PANEL_PROBES if probe()] return {'cp': panels, 'softaculous': platform_utils.has_softaculous()} # --- bulk collectors (one archive file each) --- def collect_ipcs(data_package, arcname): # type: (delivery_kit.DataPackage, str) -> None """`ipcs -m | sed -e s/-/=/g`""" _, stdout, _ = run_command(['ipcs', '-m'], catch_stdout=True, catch_stderr=True) data_package.add_file(arcname, data_bytes=utils.bstr((stdout or '').replace('-', '='), encoding='utf-8')) def collect_dpkg_install_log(data_package, arcname): # type: (delivery_kit.DataPackage, str) -> None """`grep ' install ' /var/log/dpkg.log`""" if not os.path.exists(DPKG_LOG): data_package.log_error('file not found: {0}'.format(DPKG_LOG)) return with open(DPKG_LOG, 'rb') as f: lines = [line for line in f.read().splitlines() if b' install ' in line] data_package.add_file(arcname, data_bytes=b'\n'.join(lines) + b'\n') def collect_packages(data_package, arcname): # type: (delivery_kit.DataPackage, str) -> None # same package list commands as anomaly.prepare_kernel_anomaly_report if os.path.exists('/usr/bin/rpm') or os.path.exists('/bin/rpm'): packages_cmd = r'rpm -q -a --queryformat="%{N}|%{V}-%{R}|%{arch}|%{INSTALLTIME:date}\n"' elif os.path.exists('/usr/bin/dpkg'): packages_cmd = r'/usr/bin/dpkg-query -W -f "${binary:Package}|${Version}|${Architecture}\n"' # an unreadable dpkg.log must not lose the package list below # (partial results, like the other multi-source collectors) try: collect_dpkg_install_log(data_package, 'dpkg.log') except Exception as e: data_package.log_error('failed to collect dpkg.log: {0}'.format(e)) else: packages_cmd = 'echo "unknown package manager"' data_package.add_stdout(arcname, packages_cmd) def collect_apt_sources(data_package, arcname): # type: (delivery_kit.DataPackage, str) -> None """`grep -rE '^(deb|URIs)' /etc/apt/sources.list*`""" regex = re.compile(br'^(deb|URIs)') files = [] # type: List[str] for path in sorted(glob.glob(APT_SOURCES_GLOB)): if os.path.isdir(path): for root, _, names in os.walk(path): files.extend(os.path.join(root, name) for name in sorted(names)) else: files.append(path) # like the shell `grep`: report the no-match case in errors.log # instead of storing an empty entry if not files: data_package.log_error('file not found: {0}'.format(APT_SOURCES_GLOB)) return lines = [] # type: List[bytes] for path in files: # an unreadable file must not lose the lines of the others try: with open(path, 'rb') as f: content = f.read() except Exception as e: data_package.log_error('failed to read {0}: {1}'.format(path, e)) continue prefix = utils.bstr('{0}:'.format(path), encoding='utf-8') lines.extend(prefix + line for line in content.splitlines() if regex.search(line)) data_package.add_file(arcname, data_bytes=b'\n'.join(lines) + b'\n') def collect_libcare_logs(data_package, arcname): # type: (delivery_kit.DataPackage, str) -> None """`tail -n10000 /var/log/libcare/*.log`""" paths = sorted(glob.glob(LIBCARE_LOGS_GLOB)) # like the shell `tail`: report the no-match case in errors.log # instead of storing an empty entry if not paths: data_package.log_error('file not found: {0}'.format(LIBCARE_LOGS_GLOB)) return chunks = [] # type: List[bytes] for path in paths: # an unreadable log must not lose the tails of the others try: with open(path, 'rb') as f: lines = f.read().splitlines()[-LOG_TAIL_LINES:] except Exception as e: data_package.log_error('failed to read {0}: {1}'.format(path, e)) continue header = utils.bstr('==> {0} <==\n'.format(path), encoding='utf-8') chunks.append(header + b'\n'.join(lines)) data_package.add_file(arcname, data_bytes=b'\n'.join(chunks) + b'\n') def collect_crash_dumps(data_package, arcname): # type: (delivery_kit.DataPackage, str) -> None """crashreporter artifacts from /var/cache/kcare/dumps""" if not os.path.isdir(config.KDUMPS_DIR): return data_package.add_stdout(arcname, 'ls -lR {0}'.format(config.KDUMPS_DIR)) for root, _, names in os.walk(config.KDUMPS_DIR): for name in sorted(names): if any(fnmatch.fnmatch(name, pattern) for pattern in CRASH_DUMP_PATTERNS): path = os.path.join(root, name) # keep the path relative to KDUMPS_DIR in the arcname so # same-named dumps from different subdirectories do not # overwrite each other in the archive rel_path = os.path.relpath(path, config.KDUMPS_DIR) data_package.add_file('crashreporter/{0}'.format(rel_path), src_path=path) def collect_kdump(data_package, arcname): # type: (delivery_kit.DataPackage, str) -> None # kcdoctor.sh gates kdump collection on `[ -e /etc/kdump.conf ]`; without # it get_kdump_root() defaults to /var/crash, so an unconfigured host with # a stale crash dir would otherwise be collected. if not os.path.exists(KDUMP_CONF): return kdump_root = kcare.get_kdump_root() if not os.path.isdir(kdump_root): return data_package.add_stdout(arcname, 'ls -lR {0}'.format(kdump_root)) for evt in sorted(glob.glob(os.path.join(kdump_root, '*'))): # only crash event directories can hold a vmcore-dmesg.txt; # skip regular files to keep errors.log free of noise if not os.path.isdir(evt): continue data_package.add_file( 'kdump/{0}/vmcore-dmesg.txt'.format(os.path.basename(evt)), src_path=os.path.join(evt, 'vmcore-dmesg.txt'), ) def collect_sysfs_config(data_package, arcname): # type: (delivery_kit.DataPackage, str) -> None """tunables gathered by the kcdoctor.sh get_sysfs_info() `find /sys` calls""" chunks = [] for root, dirs, names in os.walk(SYSFS_ROOT): if root.startswith(SYSFS_EXCLUDE_PREFIXES): dirs[:] = [] # do not descend into excluded subtrees continue for name in sorted(names): if any(fnmatch.fnmatch(name, pattern) for pattern in SYSFS_NAME_PATTERNS): path = os.path.join(root, name) try: with open(path, 'rb') as f: content = f.read() except Exception: content = b'' chunks.append(utils.bstr('cat {0}\n'.format(path)) + content + b'\n') data_package.add_file(arcname, data_bytes=b''.join(chunks)) # the full kcdoctor.sh (1.0-8) vitals set, in the script order; see # docs/features/KPT-5984-doctor-v2/parity-inventory.md for the mapping # scalar/short items, consolidated into doctor.json (one field each) FIELD_SPECS = [ FieldSpec('doctor_version', collect_doctor_version), FieldSpec('virt_what', cmd_field(['/usr/libexec/kcare/virt-what'], as_lines=True)), FieldSpec('main_ip', get_main_ip), FieldSpec('server_id', collect_server_id), FieldSpec('kernel_id', collect_kernel_id), FieldSpec('date', cmd_field(['date'])), FieldSpec('uname', collect_uname), FieldSpec('redhat_release', read_field('/etc/redhat-release')), FieldSpec('debian_version', read_field('/etc/debian_version')), FieldSpec('os_release', read_field('/etc/os-release')), FieldSpec('issue', read_field('/etc/issue')), FieldSpec('sysconfig_kernel', read_field('/etc/sysconfig/kernel')), FieldSpec('proc_uptime', read_field('/proc/uptime')), FieldSpec('proc_loadavg', read_field('/proc/loadavg')), FieldSpec('proc_cmdline', read_field('/proc/cmdline')), FieldSpec('proc_version', read_field('/proc/version')), FieldSpec('default_grub', grep_field('/etc/default/grub', 'DEFAULT')), FieldSpec('grub2_entries', collect_grub2_entries), FieldSpec('sshd_port', grep_field('/etc/ssh/sshd_config', 'Port')), FieldSpec('yum_repos_d', collect_yum_repos), FieldSpec('control_panel', collect_control_panel), ] # bulk items, one archive file each FILE_SPECS = [ run_spec('syslog', 'tail -n{0} /var/log/syslog'.format(LOG_TAIL_LINES)), run_spec('dmesg', 'dmesg'), run_spec('messages', 'tail -n{0} /var/log/messages'.format(LOG_TAIL_LINES)), run_spec('ls_var_cache_kcare', 'ls -lR /var/cache/kcare/'), dump_spec('kcare.conf', '/etc/sysconfig/kcare/kcare.conf'), run_spec('cpuinfo', 'cat /proc/cpuinfo'), proc_dump_spec('proc_vmstat', '/proc/vmstat'), proc_dump_spec('proc_devices', '/proc/devices'), proc_dump_spec('proc_diskstats', '/proc/diskstats'), proc_dump_spec('proc_mdstat', '/proc/mdstat'), proc_dump_spec('proc_meminfo', '/proc/meminfo'), proc_dump_spec('proc_swaps', '/proc/swaps'), proc_dump_spec('proc_filesystems', '/proc/filesystems'), proc_dump_spec('proc_mounts', '/proc/mounts'), proc_dump_spec('proc_interrupts', '/proc/interrupts'), dump_spec('grub.conf', '/boot/grub/grub.conf'), proc_dump_spec('proc_modules', '/proc/modules'), dump_spec('grub2.cfg', GRUB2_CFG), proc_dump_spec('proc_zoneinfo', '/proc/zoneinfo'), run_spec('ls_boot_configs', 'ls /etc/grub.conf /boot/grub/grub.conf /boot/grub/menu.lst'), run_spec('ls_boot', 'ls -l /boot'), run_spec('printenv', 'printenv'), run_spec('dmidecode', 'dmidecode'), callable_spec('ipcs', collect_ipcs), run_spec('sysctl', 'sysctl -a'), dump_spec('sysctl.conf', '/etc/sysctl.conf'), callable_spec('packages.list', collect_packages), run_spec('lspci', 'lspci -vv'), run_spec('dpkg_l', 'dpkg -l'), callable_spec('apt_sources', collect_apt_sources), dump_spec('yum.conf', '/etc/yum.conf'), run_spec('yum_repolist', 'yum repolist'), callable_spec('libcare_logs', collect_libcare_logs), run_spec('kcarectl.log', 'tail -n{0} /var/log/kcarectl.log'.format(LOG_TAIL_LINES)), dump_spec('kdump.conf', KDUMP_CONF), dump_spec('kcare-cron', '/etc/cron.d/kcare-cron'), callable_spec('crashreporter/ls', collect_crash_dumps), callable_spec('kdump/ls', collect_kdump), run_spec('aa_status', 'aa-status'), run_spec('sestatus', 'sestatus'), run_spec('kprobes_list', 'cat /sys/kernel/debug/kprobes/list'), callable_spec('sysfs_config', collect_sysfs_config), run_spec('lsblk', 'lsblk -f'), run_spec('df', 'df -h'), ] @utils.catch_errors(logger=log_utils.logwarn) def prepare_doctor_report(): # type: () -> Optional[KcarectlDoctorPackage] # None when collection fails (catch_errors): KPT-6064/KPT-6065 # callers must handle it data_package = KcarectlDoctorPackage() with data_package: report = {} # type: Dict[str, Any] for field in FIELD_SPECS: field.apply(data_package, report) data_package.add_json('doctor.json', report) for spec in FILE_SPECS: spec.apply(data_package) return data_package def send_doctor_report(fallback, force_fallback=False): # type: (Callable[[], None], bool) -> None """Collect and upload the doctor report via the generic uploads proxy. PUTs the archive to ``/upload/kcarectl-doctor/`` -- the same modern pipeline as ``kernel-anomaly`` -- which ePortal proxies transparently to the patch server, so no ePortal change is required. Any 2xx reply is success, including the ``REDUCED_REPORT`` ``204`` where the proxy intentionally drops the report (``send()`` returns without raising, so we do not fall back). Falls back to the legacy ``kcdoctor.sh`` flow (``fallback``) when: * ``force_fallback`` (the ``--doctor --fallback`` CLI flag) or ``config.FORCE_DOCTOR_FALLBACK`` is set -- the rollout kill-switch. ``--fallback`` is the one-off, per-invocation form; the config knob is settable per host in kcare.conf or pushed fleet-wide via the ``KC-Flag-Force-Doctor-Fallback`` feature flag. Either forces the legacy flow on every host, registered or not, before the new path is even considered; * the host is not registered, i.e. there is no auth string (OQ-4: the proxy upload requires auth, so the new path is not even attempted); * report collection failed (``prepare_doctor_report`` returned None); * the new route is unavailable on an older patch server / ePortal -- a 404 / 405 / 403, surfaced as ``HTTPError``, or a connection-level failure. ``send()`` drives ``http_utils.upload_file`` (raw ``httplib``, not ``urllib``), so transport failures surface as an ``OSError`` subclass -- ``socket.timeout``, ``ConnectionRefused``, ``socket.gaierror`` (DNS), ``ssl.SSLError`` -- or an ``httplib.HTTPException``, *not* ``URLError``; all of these mean the new path is unusable. ``upload_file`` raises only on status >= 400, so a redirect is not treated as a failure; the proxy and patch server never redirect uploads. :param fallback: legacy ``kcdoctor.sh`` runner, injected by the caller (KPT-6065) to keep this module free of an ``__init__`` import cycle. :param force_fallback: when True (``--doctor --fallback``), skip the v2 path and run ``fallback`` directly; the per-invocation form of ``config.FORCE_DOCTOR_FALLBACK``. """ if force_fallback or config.FORCE_DOCTOR_FALLBACK: src = '--fallback requested' if force_fallback else 'FORCE_DOCTOR_FALLBACK is set' log_utils.loginfo( 'doctor: {0}; using the legacy kcdoctor.sh flow'.format(src), print_msg=False, ) fallback() return if auth.get_http_auth_string() is None: log_utils.loginfo( 'doctor: host is not registered; using the legacy kcdoctor.sh flow', print_msg=False, ) fallback() return data_package = prepare_doctor_report() if data_package is None: log_utils.logwarn('doctor: report collection failed; using the legacy kcdoctor.sh flow') fallback() return try: upload_name = data_package.send() log_utils.loginfo('doctor: report uploaded as {0}'.format(upload_name), print_msg=False) # surface the uploaded filename to the user, the same way the eportal # --doctor does (kc.eportal make_doctor_report -> utils.print_wrapper) # so support can locate the report; the legacy kcdoctor.sh fallback # prints its own "Key:" line instead. print_wrapper (not loginfo) so # the line is shown unconditionally -- loginfo is gated by PRINT_LEVEL # and would be dropped under --quiet/--auto-update, whereas the legacy # kcdoctor.sh echo is not. utils.print_wrapper('Please provide this filename to support: {0}'.format(upload_name)) except (HTTPError, URLError, httplib.HTTPException, socket.error) as err: # upload_file drives httplib directly, so a transport failure # surfaces as socket.error (== OSError on py3: timeout, connection # refused, DNS, TLS) or httplib.HTTPException -- not URLError. An # HTTPError (route absent: 404 / 405 / 403) means the same thing: # the new path is unusable, so fall back to the legacy flow. log_utils.logwarn('doctor: report upload unavailable ({0}); using the legacy kcdoctor.sh flow'.format(err)) fallback() finally: data_package.remove_archive() config.py000064400000004724152533440750006402 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT from . import constants if False: # pragma: no cover from typing import List # noqa: F401 AFTER_UPDATE_COMMAND = None AUTO_STICKY_PATCHSET = None AUTO_UPDATE = True AUTO_UPDATE_DELAY = None BEFORE_UPDATE_COMMAND = None BLACKLIST_FILE = 'kpatch.blacklist' CHECK_CLN_LICENSE_STATUS = True CHECK_SSL_CERTS = True FIXUPS_FILE = 'kpatch.fixups' FLAGS = ['keep-registration', 'manage-libcare'] FORCE_GID = None FORCE_JSON_SIG_V3 = True HTTP_TIMEOUT = 20 HTTP_UPLOAD_TIMEOUT = 240 IGNORE_UNKNOWN_KERNEL = False KPATCH_DEBUG = False # crash reporter ENABLE_CRASHREPORTER = False KDUMPS_DIR = constants.PATCH_CACHE + '/dumps' KMSG_OUTPUT = True KCORE_OUTPUT = True KCORE_OUTPUT_SIZE = 100 # 100Mb IGNORE_FEATURE_FLAGS = False LIBCARE_DISABLED = False LIBCARE_LIBS = ['libc', 'libssl', 'nscd', 'libm', 'libnss_dns'] # type: List[str] LIBCARE_PIDLOGS_MAX_TOTAL_SIZE_MB = 500 LIBCARE_SOCKET_TIMEOUT = 60 LIB_AUTO_UPDATE = True PATCH_BIN = 'kpatch.bin' PATCH_DONE = '.done' PATCH_INFO = 'kpatch.info' PATCH_LEVEL = None # a level to 'stick on' (if 0 then use latest level) PATCH_METHOD = '' PATCH_TYPE = '' PREFIX = '' PREV_PATCH_TYPE = 'default' PRINT_LEVEL = constants.PRINT_INFO REPORT_FQDN = False SEND_PERF_METRICS = False SILENCE_ERRORS = True STATUS_CHANGE_GAP = 4 * 60 * 60 + 5 * 60 # 4 hours STICKY_PATCH = False STICKY_PATCHSET = None UPDATE_DELAY = None UPDATE_FROM_LOCAL = False UPDATE_POLICY = constants.POLICY_REMOTE UPDATE_SYSCTL_CONFIG = True USERSPACE_PATCHES = None USE_CONTENT_FILE_V3 = True USE_SIGNATURE = True KERNEL_VERSION_FILE = '/proc/version' KCARE_UNAME_FILE = '/proc/kcare/effective_version' SUCCESS_TIMEOUT = 5 * 60 # anomaly reports KERNEL_ANOMALY_REPORT_ENABLE = False KERNEL_ANOMALY_REPORT_MAX_SIZE_BYTES = 500 * (1024**2) # 500MB # doctor reports DOCTOR_REPORT_MAX_SIZE_BYTES = 500 * (1024**2) # 500MB # force the legacy kcdoctor.sh flow instead of the v2 /upload proxy path -- a # rollout kill-switch, settable per host in kcare.conf or pushed fleet-wide via # the KC-Flag-Force-Doctor-Fallback feature flag FORCE_DOCTOR_FALLBACK = False # external services communication FORCE_IPV4 = False FORCE_IPV6 = False PATCH_SERVER = 'https://patches.kernelcare.com' PATCH_SERVER_IPV6 = 'https://ipv6.patches.kernelcare.com' REGISTRATION_URL = 'https://cln.cloudlinux.com/api/kcare' REGISTRATION_URL_IPV6 = 'https://ipv6.cln.cloudlinux.com/api/kcare' py23.py000064400000004236152533440750005730 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import json from . import constants if constants.PY2: # pragma: no py3 cover import httplib # isort: skip - skipped because isort incorrectly treats it as third-party from urllib import quote as urlquote from urllib import urlencode from ConfigParser import ConfigParser from urllib2 import HTTPError from urllib2 import Request as StdRequest from urllib2 import URLError from urllib2 import urlopen as std_urlopen from urlparse import urlparse # type: ignore class Request(StdRequest): def __init__(self, *args, **kwargs): method = kwargs.pop('method', None) StdRequest.__init__(self, *args, **kwargs) if method == 'HEAD': # Older versions of mypy supporting 2.x do not infer type of # the `method` variable correctly here. Cast to str explicitly. self.get_method = lambda: str(method) # type: ignore[assignment] # json.loads returns unicode strings and they can contaminate following # calls. Functions converts all unicode strings into native def _convert(data): dtype = type(data) if dtype is type(u''): return data.encode('utf-8') elif dtype is list: return [_convert(it) for it in data] elif dtype is dict: return dict((_convert(k), _convert(v)) for k, v in data.items()) return data def json_loads_nstr(json_str): return _convert(json.loads(json_str)) else: # pragma: no py2 cover from configparser import ConfigParser from http import client as httplib from urllib.error import HTTPError, URLError from urllib.parse import quote as urlquote from urllib.parse import urlencode, urlparse from urllib.request import Request from urllib.request import urlopen as std_urlopen json_loads_nstr = json.loads __all__ = [ "ConfigParser", "HTTPError", "Request", "URLError", "httplib", "json_loads_nstr", "std_urlopen", "urlencode", "urlparse", "urlquote", ] __main__.py000064400000001443152533440750006650 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import os import sys from . import errors, log_utils, main, send_exc from .py23 import URLError # The scanner interface should skip us os.environ['KCARE_SCANNER_INTERFACE_DO_NOTHING'] = '1' if __name__ == '__main__': try: sys.exit(main()) except URLError as err: log_utils.logerror('{0}: {1}'.format(err, getattr(err, 'url', 'unknown'))) except errors.KcareError as err: log_utils.logerror(str(err)) sys.exit(1) except Exception as err: if isinstance(err, errors.SafeExceptionWrapper): log_utils.logexc(err.inner) else: log_utils.logexc(err) send_exc() sys.exit(1) fetch.py000064400000011636152533440750006226 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import hashlib import json import os import kcsig_verify # noqa: E402 from . import auth, config, constants, errors, http_utils, selinux, utils SIG_VERIFY_ORDER = [constants.SIG, constants.SIG_JSON] GPG_BIN = '/usr/bin/gpg' GPG_KEY_DIR = '/var/lib/kcare/gpg' CONTENT_FILE = 'release.content.json' def fetch_signature(url, dst, do_auth=False): urlopen_local = http_utils.urlopen if do_auth: urlopen_local = auth.urlopen_auth if config.FORCE_JSON_SIG_V3: sig_exts = SIG_VERIFY_ORDER[::-1] else: sig_exts = SIG_VERIFY_ORDER for sig_ext in sig_exts: try: signature = urlopen_local(url + sig_ext) break except errors.NotFound as nf: if sig_ext == sig_exts[-1]: raise nf # pragma: no cover sig_dst = dst + sig_ext # pragma: no cover utils.save_to_file(signature, sig_dst) return sig_dst def check_gpg_bin(): if not os.path.isfile(GPG_BIN): raise errors.KcareError('No {0} present. Please install gnupg'.format(GPG_BIN), status='gnupg not found') def check_gpg_signature(file_path, signature): # mocked: tests/unit """ 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 with the signature :return: True in case of valid signature :raises: BadSignatureException """ check_gpg_bin() if signature.endswith(constants.SIG_JSON): root_keys = os.path.join(GPG_KEY_DIR, 'root-keys.json') try: kcsig_verify.verify(signature, file_path, root_keys) except kcsig_verify.Error as e: raise errors.BadSignatureException('Bad Signature: {0}: {1}'.format(file_path, str(e))) else: with open(signature, 'rb') as f: sigdata = f.read() keyring = os.path.join(GPG_KEY_DIR, 'kcare_pub.key') try: kcsig_verify.run_gpg_verify(keyring, file_path, sigdata) except Exception as e: raise errors.BadSignatureException('Bad Signature: {0}: {1}'.format(file_path, str(e))) # BadSignatureException is the only side effect of interrupted connection, # should retry file extraction in this case @utils.retry(errors.check_exc(errors.BadSignatureException), count=3, delay=0) def fetch_url(url, dst, check_signature=False, hash_checker=None): response = auth.urlopen_auth(url) tmp = selinux.selinux_safe_tmpname(dst) utils.save_to_file(response, tmp) if hash_checker: hash_checker.check(url, tmp) elif check_signature: signature = fetch_signature(url, tmp, do_auth=True) check_gpg_signature(tmp, signature) os.rename(tmp, dst) return response class HashChecker(object): def __init__(self, baseurl, content_file): self.content_file = content_file self.url_prefix = utils.get_patch_server_url(baseurl).rstrip('/') + '/' self.hashes = json.loads(utils.read_file(content_file))['files'] def check(self, url, fname): cfname = url[len(self.url_prefix) :] if cfname not in self.hashes: raise errors.KcareError( 'Invalid checksum: {0} not found in content file {1}'.format(cfname, self.content_file), status='invalid checksum', ) hsh = hashlib.sha256(utils.read_file_bin(fname)).hexdigest() expected_hsh = self.hashes[cfname]['sha256'] if hsh != expected_hsh: raise errors.BadSignatureException( 'Invalid checksum: {0} has invalid checksum {1}, expected {2}'.format(fname, hsh, expected_hsh) ) @utils.cached def get_hash_checker(level): if not config.USE_CONTENT_FILE_V3: return None if not level.baseurl: return None dst = level.cache_path(CONTENT_FILE) if not os.path.exists(dst): try: # here we also implicitly check content file signature fetch_url(utils.get_patch_server_url(level.baseurl, CONTENT_FILE), dst, config.USE_SIGNATURE) except errors.NotFound: return None return HashChecker(level.baseurl, dst) def wrap_with_cache_key(clbl): """Enrich request with a cache key, and save it if response had.""" def wrapper(*args, **kwargs): cache_key = utils.get_cache_key() if cache_key is not None: if 'headers' not in kwargs: kwargs['headers'] = {} kwargs['headers'][constants.CACHE_KEY_HEADER] = cache_key resp = clbl(*args, **kwargs) new_cache_key = resp.headers.get(constants.CACHE_KEY_HEADER) if new_cache_key is not None and new_cache_key != cache_key: utils.atomic_write(constants.CACHE_KEY_DUMP_PATH, new_cache_key) return resp return wrapper __pycache__/doctor.cpython-36.pyc000064400000047147152533440750012721 0ustar003 ;j _.@s dZddlZddlZddlZddlZddlZddlmZmZm Z m Z m Z m Z m Z mZmZddlmZddlmZmZmZdZdZdZd Zd Zd Zd Zd ZdZdZdZ dZ!Gddde j"Z#Gddde$Z%ddZ&ddZ'd d!Z(d"d#Z)Gd$d%d%e$Z*d&d'Z+d(d)Z,dd+d,Z-d-d.Z.ej/d/d0Z0d1d2Z1d3d4Z2d5d6Z3d7d8Z4d9d:Z5d;d<Z6d=d>Z7d?d@Z8dAdBZ9dCdDZ:dEdFZ;dGdHZe*dMe.e*dNe-dOgdPdQe*dRe0e*dSe1e*dTe2e*dUe-dUge*dVe3e*dWe+dXe*dYe+dZe*d[e+d\e*d]e+d^e*d_e+d`e*dae+dbe*dce+dde*dee+dfe*dge+dhe*die,djdke*dle4e*dme,dndoe*dpe5e*dqe6gZ?e&drdsj@ee&dtdte&dudvj@ee&dwdxe'dydze&d{d|e(d}d~e(dde(dde(dde(dde(dde(dde(dde(dde'dde(dde'dee(dde&dde&dde&dde&dde)de7e&dde'dde)de9e&dde&dde)de:e'dde&dde)de;e&ddj@ee'dee'dde)de<e)de=e&dde&dde&dde)de>e&dde&ddg,ZAejBe jCdddZDdddZEdS)aODoctor v2 vitals collector (KPT-5984). Python port of the kcdoctor.sh (1.0-8) vitals collection: gathers the same data set into a DataPackage-based archive processed by the patch server generic uploads pipeline. Scalar and short values are consolidated into a single ``doctor.json`` (the eportal doctor precedent), one field each, instead of a tiny archive entry per value; bulk data (logs, command dumps, package lists, sysfs tunables, crash artifacts) stays as separate archive entries. The 1:1 inventory of collected items lives in docs/features/KPT-5984-doctor-v2/parity-inventory.md. N) authconfig delivery_kit http_utilskcare log_utilsplatform_utilsserveridutils) run_command) HTTPErrorURLErrorhttplibz2.0-1z/boot/grub2/grub.cfgz/etc/kdump.confz/var/log/dpkg.logz/etc/apt/sources.list*z/etc/yum.repos.d/*z/var/log/libcare/*.logi'z/sysenable*nr_*max_**cnt* /sys/kernel/debug/tracing/events/sys/kernel/tracing/events /sys/devices [0-9]*.logkmsg*logc@s eZdZdZdZeddZdS)KcarectlDoctorPackagezkcarectl-doctorz/upload/kcarectl-doctor/cCstjS)N)rDOCTOR_REPORT_MAX_SIZE_BYTES)selfr,/usr/libexec/kcare/python/kcarectl/doctor.pymax_size;szKcarectlDoctorPackage.max_sizeN)__name__ __module__ __qualname__ data_type upload_uripropertyrrrrrr7src@s eZdZdZddZddZdS)CollectionSpeczA single bulk vitals item ported from kcdoctor.sh. Collection failures are recorded in errors.log and never abort the report: an unhandled exception inside the package context manager would remove the whole archive. cCs||_||_dS)N)arcname_collect)rr%collectrrr__init__LszCollectionSpec.__init__cCsNy|j||jWn6tk rH}z|jdj|j|WYdd}~XnXdS)Nzfailed to collect {0}: {1})r&r% Exception log_errorformat)r data_packageerrrapplyQszCollectionSpec.applyN)rrr __doc__r(r.rrrrr$Dsr$csfdd}t||S)z4`run ""` kcdoctor.sh helper (no shell features)cs|j|dS)N) add_stdout)r,r%)cmdrrr']szrun_spec..collect)r$)r%r1r'r)r1rrun_specYs r2csfdd}t||S)z4`dump ""` kcdoctor.sh helper for regular filescs|j|ddS)N)src_path)add_file)r,r%)pathrrr'hszdump_spec..collect)r$)r%r5r'r)r5r dump_specds r6csfdd}t||S)z`dump ""` for /proc and /sys files. tar reports them as 0-size so they are first read into memory (see the same approach in anomaly.prepare_kernel_anomaly_report). c sLtjjs |jdjdStd}|j||jdWdQRXdS)Nzfile not found: {0}rb) data_bytes)osr5existsr*r+openr4read)r,r%f)r5rrr'ws   zproc_dump_spec..collect)r$)r%r5r'r)r5rproc_dump_specos r>cCs t||S)N)r$)r%fnrrr callable_specsr@c@s eZdZdZddZddZdS) FieldSpeczA single doctor.json field ported from kcdoctor.sh. Collection failures are recorded in errors.log and never abort the report; a collector returning None omits the field (e.g. a distro-specific source file absent on this system). cCs||_||_dS)N)keyr&)rrBr'rrrr(szFieldSpec.__init__cCsXy |j}Wn4tk r@}z|jdj|j|dSd}~XnX|dk rT|||j<dS)Nzfailed to collect {0}: {1})r&r)r*r+rB)rr,reportvaluer-rrrr.s zFieldSpec.applyN)rrr r/r(r.rrrrrAsrAcsfdd}|S)z`dump ""` as a doctor.json string field. Read in binary (non-utf8 safe) and decoded leniently; an absent distro-specific source file simply omits the field (None). c s:tjjsdStd}|jjddjSQRXdS)Nr7zutf-8replace)r9r5r:r;r<decodestrip)r=)r5rrr's  zread_field..collectr)r5r'r)r5r read_fields rHcs"tjtj|fdd}|S)z`grep ` as a doctor.json list-of-lines field. Match on bytes (like collect_apt_sources) so a non-utf8 byte cannot lose a line; the matched lines are decoded leniently for the JSON. c sDtjjsdStd}|jj}WdQRXfdd|DS)Nr7cs"g|]}j|r|jddqS)zutf-8rE)searchrF).0line)regexrr sz/grep_field..collect..)r9r5r:r;r< splitlines)r=lines)r5rLrrr's   zgrep_field..collect)recompiler bstr)r5patternr'r)r5rLr grep_fields rTFcsfdd}|S)z@`run ""` captured as a doctor.json field instead of a file.cs0tddd\}}}|pdj}r,|jS|S)NT) catch_stdout catch_stderr)r rGrN)_stdouttext)argvas_linesrrr's zcmd_field..collectr)r[r\r'r)r[r\r cmd_fieldsr]cCstS)N)DOCTOR_VERSIONrrrrcollect_doctor_versionsr_c Cs<y"tjtjd}tj|jjStk r6dSXdS)zthe public IP of the machine as seen by the patch server. Cached: the main_ip and server_id fields share one HTTP roundtrip per run. ZmyipZNAN)rurlopenr get_patch_server_urlnstrr<rGr))responserrr get_main_ips rdcCs$tj}|stjdd}tj|S)N.rX)r get_serveridrdrEr rb) server_idrrrcollect_server_idsrhcCstjS)N)rget_kernel_hashrrrrcollect_kernel_idsrjcCsBi}x8d D]0}tdd|gddd \}}}|p0d j||<q W|S) Narmpouname-T)rUrVrW)rkrlrmrnro)r rG)resultflagrXrYrrr collect_unames  rtc CsHtjjtsdSttd}|jjdd}WdQRXdd|jDS)z7`grep vmlinuz /boot/grub2/grub.cfg | sed 's/root=.*//'`Nr7zutf-8rEcSs"g|]}d|krtjdd|qS)Zvmlinuzzroot=.*rW)rPsub)rJrKrrrrMsz)collect_grub2_entries..)r9r5r: GRUB2_CFGr;r<rFrN)r=rZrrrcollect_grub2_entries s   rwcCsttjtS)N)sortedglobYUM_REPOS_GLOBrrrrcollect_yum_repossr{cCsddtjD}|tjdS)z8presence-only port of the kcdoctor.sh detect_cp() probescSsg|]\}}|r|qSrr)rJnameproberrrrMsz)collect_control_panel..)cpZ softaculous)rCONTROL_PANEL_PROBEShas_softaculous)Zpanelsrrrcollect_control_panelsrcCs@tddgddd\}}}|j|tj|p(djdddd d d S) z`ipcs -m | sed -e s/-/=/g`ipcsz-mT)rUrVrWrq=zutf-8)encoding)r8N)r r4r rRrE)r,r%rXrYrrr collect_ipcs%src Cshtjjts |jdjtdSttd}dd|jjD}WdQRX|j |dj |dddS)z$`grep ' install ' /var/log/dpkg.log`zfile not found: {0}Nr7cSsg|]}d|kr|qS)s install r)rJrKrrrrM4sz,collect_dpkg_install_log.. )r8) r9r5r:DPKG_LOGr*r+r;r<rNr4join)r,r%r=rOrrrcollect_dpkg_install_log,s    rcCstjjdstjjdrd}nXtjjdrrd}yt|dWqvtk rn}z|jdj|WYdd}~XqvXnd}|j||dS) Nz /usr/bin/rpmz/bin/rpmzFrpm -q -a --queryformat="%{N}|%{V}-%{R}|%{arch}|%{INSTALLTIME:date}\n"z /usr/bin/dpkgzJ/usr/bin/dpkg-query -W -f "${binary:Package}|${Version}|${Architecture}\n"zdpkg.logzfailed to collect dpkg.log: {0}zecho "unknown package manager")r9r5r:rr)r*r+r0)r,r% packages_cmdr-rrrcollect_packages9s $rc sFtjdg}xdttjtD]R}tjj|rfx@tj|D]&\}}|j fddt|Dq:Wq|j |qW|s|j dj tdSg}x|D]}y"t |d}|j}WdQRXWn6tk r} z|j dj || wWYdd} ~ XnXtjdj |d d |j fd d|jDqW|j|d j|d d dS)z/`grep -rE '^(deb|URIs)' /etc/apt/sources.list*`s ^(deb|URIs)c3s|]}tjj|VqdS)N)r9r5r)rJr|)rootrr Usz&collect_apt_sources..zfile not found: {0}Nr7zfailed to read {0}: {1}z{0}:zutf-8)rc3s |]}j|r|VqdS)N)rI)rJrK)prefixrLrrrjsr)r8)rPrQrxryAPT_SOURCES_GLOBr9r5isdirwalkextendappendr*r+r;r<r)r rRrNr4r) r,r%filesr5rXnamesrOr=contentr-r)rrLrrcollect_apt_sourcesLs*  "  "rc Csttjt}|s&|jdjtdSg}x|D]}y0t|d}|jjt d}WdQRXWn6t k r}z|jdj||w0WYdd}~XnXt j dj|dd}|j |dj |q0W|j|dj |dd dS) z%`tail -n10000 /var/log/libcare/*.log`zfile not found: {0}Nr7zfailed to read {0}: {1}z ==> {0} <== zutf-8)rr)r8)rxryLIBCARE_LOGS_GLOBr*r+r;r<rNLOG_TAIL_LINESr)r rRrrr4) r,r%pathschunksr5r=rOr-headerrrrcollect_libcare_logsos  $rcstjjtjsdS|j|djtjxttjtjD]d\}}}xXt|D]Lt fddt DrHtjj |}tjj |tj}|j dj||dqHWq4WdS)z3crashreporter artifacts from /var/cache/kcare/dumpsNz ls -lR {0}c3s|]}tj|VqdS)N)fnmatch)rJrS)r|rrrsz&collect_crash_dumps..zcrashreporter/{0})r3)r9r5rr KDUMPS_DIRr0r+rrxanyCRASH_DUMP_PATTERNSrrelpathr4)r,r%rrXrr5Zrel_pathr)r|rcollect_crash_dumpssrcCstjjtsdStj}tjj|s(dS|j|dj|xTt t j tjj |dD]8}tjj|sfqT|j djtjj |tjj |ddqTWdS)Nz ls -lR {0}*zkdump/{0}/vmcore-dmesg.txtzvmcore-dmesg.txt)r3)r9r5r: KDUMP_CONFrget_kdump_rootrr0r+rxryrr4basename)r,r% kdump_rootZevtrrr collect_kdumps   rc sg}xtjtD]\}}}|jtr2g|dd<qxt|D]tfddtDr.r7zcat {0} r)r8)r9r SYSFS_ROOT startswithSYSFS_EXCLUDE_PREFIXESrxrSYSFS_NAME_PATTERNSr5rr;r<r)rr rRr+r4) r,r%rrdirsrr5r=rr)r|rcollect_sysfs_configs    &rZdoctor_versionZ virt_whatz/usr/libexec/kcare/virt-whatT)r\Zmain_iprgZ kernel_iddaterpZredhat_releasez/etc/redhat-releaseZdebian_versionz/etc/debian_versionZ os_releasez/etc/os-releaseZissuez /etc/issueZsysconfig_kernelz/etc/sysconfig/kernelZ proc_uptimez /proc/uptimeZ proc_loadavgz /proc/loadavgZ proc_cmdlinez /proc/cmdline proc_versionz /proc/versionZ default_grubz/etc/default/grubDEFAULTZ grub2_entriesZ sshd_portz/etc/ssh/sshd_configZPortZ yum_repos_dZ control_panelsyslogztail -n{0} /var/log/syslogdmesgmessagesztail -n{0} /var/log/messagesls_var_cache_kcarezls -lR /var/cache/kcare/z kcare.confz/etc/sysconfig/kcare/kcare.confcpuinfozcat /proc/cpuinfoZ proc_vmstatz /proc/vmstatZ proc_devicesz /proc/devicesZproc_diskstatsz/proc/diskstatsZ proc_mdstatz /proc/mdstatZ proc_meminfoz /proc/meminfoZ proc_swapsz /proc/swapsZproc_filesystemsz/proc/filesystemsZ proc_mountsz /proc/mountsZproc_interruptsz/proc/interruptsz grub.confz/boot/grub/grub.conf proc_modulesz /proc/modulesz grub2.cfgZ proc_zoneinfoz/proc/zoneinfoZls_boot_configsz:ls /etc/grub.conf /boot/grub/grub.conf /boot/grub/menu.lstZls_bootz ls -l /bootZprintenvZ dmidecodersysctlz sysctl -az sysctl.confz/etc/sysctl.confz packages.listZlspciz lspci -vvZdpkg_lzdpkg -lZ apt_sourceszyum.confz /etc/yum.confZ yum_repolistz yum repolistZ libcare_logsz kcarectl.logz tail -n{0} /var/log/kcarectl.logz kdump.confz kcare-cronz/etc/cron.d/kcare-cronzcrashreporter/lszkdump/lsZ aa_statusz aa-statusZsestatusZ kprobes_listz"cat /sys/kernel/debug/kprobes/listZ sysfs_configZlsblkzlsblk -fZdfzdf -h)loggerc Cs\t}|Hi}xtD]}|j||qW|jd|xtD]}|j|q= 400, so a redirect is not treated as a failure; the proxy and patch server never redirect uploads. :param fallback: legacy ``kcdoctor.sh`` runner, injected by the caller (KPT-6065) to keep this module free of an ``__init__`` import cycle. :param force_fallback: when True (``--doctor --fallback``), skip the v2 path and run ``fallback`` directly; the per-invocation form of ``config.FORCE_DOCTOR_FALLBACK``. z--fallback requestedzFORCE_DOCTOR_FALLBACK is setz.doctor: {0}; using the legacy kcdoctor.sh flowF) print_msgNzAdoctor: host is not registered; using the legacy kcdoctor.sh flowzCdoctor: report collection failed; using the legacy kcdoctor.sh flowzdoctor: report uploaded as {0}z,Please provide this filename to support: {0}zJdoctor: report upload unavailable ({0}); using the legacy kcdoctor.sh flow)rFORCE_DOCTOR_FALLBACKrloginfor+rget_http_auth_stringrlogwarnsendr print_wrapperr r r HTTPExceptionsocketerrorremove_archive)fallbackforce_fallbacksrcr, upload_nameerrrrrsend_doctor_report-s4)    r)rrrr)rrr)rr)F)F)Fr/rryr9rPrkcarectlrrrrrrrr r Zkcarectl.process_utilsr Z kcarectl.py23r r rr^rvrrrrzrrrrrr DataPackagerobjectr$r2r6r>r@rArHrTr]r_cachedrdrhrjrtrwr{rrrrrrrrrrr+r catch_errorsrrrrrrrs,          #           __pycache__/constants.cpython-36.pyc000064400000002355152533440750013433 0ustar003 ;j@sddlZddlZdZdZdZdZdZejddkZdZ ej j e dZ ej j e dZ d Zd Zd Zd Zd ZdZdZdZdZdZdZdZdZdZdZdZdZdZdZ dZ!dZ"dZ#dZ$dZ%dS)Nz/var/cache/kcarezfeature_flags.jsonzupdate_status.jsondz Kc-Cache-Keyz/etc/sysconfig/kcare/cache_keyZREMOTEZLOCALZ LOCAL_FIRST3z/var/log/kcarectl.logz.sigz .json-sigz Kc-Auth-Tokenz/etc/sysconfig/kcare/auth_tokenFz/usr/bin/systemctlZmanualautoZsmartzkcare.koz/usr/libexec/kcare/kpatch_ctlz 3.8-1.el8)&ossys PRINT_DEBUG PRINT_INFOZ PRINT_WARN PRINT_ERRORPRINT_CRITICAL version_infoZPY2 PATCH_CACHEpathjoinZFEATURE_FLAGS_CACHEZUPDATE_STATUS_PATHZUPDATE_ERROR_MAX_LENGTHZCACHE_KEY_HEADERZCACHE_KEY_DUMP_PATH POLICY_REMOTE POLICY_LOCALPOLICY_LOCAL_FIRSTZKC_PATCH_VERSIONLOG_FILESIGZSIG_JSONAUTH_TOKEN_HEADERAUTH_TOKEN_DUMP_PATHZ RETRY_DELAYZRETRY_MAX_DELAYZ RETRY_BACKOFFZ RETRY_COUNTZSKIP_SYSTEMCTL_CHECKZ SYSTEMCTLUPDATE_MODE_MANUALUPDATE_MODE_AUTOUPDATE_MODE_SMARTKMOD_BIN KPATCH_CTLVERSIONr!r!//usr/libexec/kcare/python/kcarectl/constants.pysD__pycache__/ipv6_support.cpython-36.pyc000064400000012035152533440750014073 0ustar003 ;j@sddlZddlZddlZddlZddlmZmZmZmZm Z m Z ddl m Z dZ dZdZejjejdZdZGd d d eZeZd d Zd dZddZddZddZdS)N)config constants http_utils log_utilsserveridutils)json_loads_nstrzipv6_preference.json<c@s,eZdZddZeddZeddZdS)IPProtoSelectorcCsbtjrtjddStjjds.tjddStjrBtjddSt}|dk rltjdj|r`dnd |Sd}|j tj stjd d}n2|j tjstjd d}nt j rtjd d}|dk rt ||S|jdd }|jdd }|tkrtjdd}nR|tkrtjdd}n8|tkr8tjdd}n|tkrRtjdd}nd}t ||S)ak Choose ipv6 if it is more suitable. Checks order: - check config values (it is faster) - eportal setup and FORCE_IPVx - then check each proto availability using HEAD requests - then check if we have server_id, it means we don't expect an ip license - and finally we need to check if there is an ip license z,decided to use ipv4 because of config valuesFzkernelcare.comz,decided to use ipv6 because of config valuesTNz%decided to use {0} from on-disk cacheipv6Zipv4z1decided to use ipv4 because ipv6 is not availablez1decided to use ipv6 because ipv4 is not availablez/decided to use ipv4 because server id was found)rz2decided to use ipv4 because ipv4 license was foundz2decided to use ipv6 because ipv6 license was foundz8decided to use ipv4 because ipv4 trial license was foundz8decided to use ipv6 because ipv6 trial license was found)r FORCE_IPV4rlogdebug PATCH_SERVERendswith FORCE_IPV6 _read_cacheformat_is_url_reachablePATCH_SERVER_IPV6r get_serverid _write_cache_get_cln_licenseCLN_VALID_LICENSECLN_TRIAL_ACTIVE_LICENSE)selfcachedresultZ ipv4_licenseZ ipv6_licenser 2/usr/libexec/kcare/python/kcarectl/ipv6_support.pyis_ipv6_preferredsV                    z!IPProtoSelector.is_ipv6_preferredcCsbtj|ddd}ytj|dddddStk r\}ztjdj|t|dSd}~XnXdS) NHEAD)method auth_string F)timeout retry_on_500 retry_countTz%error during HEAD request to {0}: {1})r http_requesturlopen Exceptionrrrstr)urlrequester r r!r`sz!IPProtoSelector._is_url_reachablec Cs|r tjntj}|d}tjtj|ddj}tj|}| sL|j d rbt j j dj |tSy t|dStk rtSXdS)Nz /check.plainF)r)codezUnexpected CLN response: {0})rREGISTRATION_URL_IPV6REGISTRATION_URLrnstrrr,read data_as_dictgetrkcarelogerrorrCLN_NO_LICENSEint ValueError)rZbase_urlr/contentinfor r r!rks  z IPProtoSelector._get_cln_licenseN)__name__ __module__ __qualname__r" staticmethodrrr r r r!r sA r c Cstjt}|sdSy t|}Wn,ttfk rJtjdj|dddSXt |t sntjdj|dddS|j d}t |t stjdj|dddS|j d}t |t j st |t rtjdj|dddStjt|}|d kp|tkr tjd j|tdS|S) Nz+ipv6 preference cache: malformed json {0!r}F) print_msgz/ipv6 preference cache: unexpected payload {0!r} prefer_ipv6z3ipv6 preference cache: unexpected prefer_ipv6 {0!r}tsz)ipv6 preference cache: malformed ts {0!r}rz;ipv6 preference cache: stale entry (age={0:.0f}s, ttl={1}s))r try_to_read CACHE_FILEr r= TypeErrorrlogwarnr isinstancedictr8boolnumbersZIntegraltimer<CACHE_TTL_SECONDSr)r>datarEZ cached_tsZager r r!rs0      rcCsl|ttjd}ytjttj|ddWn:ttfk rf}zt j dj |ddWYdd}~XnXdS)N)rErFT) ensure_dirz*failed to write ipv6 preference cache: {0}F)rD) r<rOr atomic_writerHjsondumpsOSErrorIOErrorrrJr)rrQr1r r r!rs rc Cs(ytjtWntk r"YnXdS)z'Drop the on-disk ipv6 preference cache.N)osunlinkrHrVr r r r! clear_cachesrZcCstjrtjStjS)N)ip_proto_selectorr"rrrr r r r!get_patch_serversr\cCstjrtjStjS)N)r[r"rr3r4r r r r!get_registration_urlsr]iiQ)rTrNrXrOrrrrrrpy23r rrr;pathjoin PATCH_CACHErHrPobjectr r[rrrZr\r]r r r r!s"  b'  __pycache__/anomaly.cpython-36.pyc000064400000007174152533440750013063 0ustar003 ;j@sddlZddlZddlmZmZmZmZejejdddZ ddZ ejejddd Z ejejd d d d Z e dkre idS)N) delivery_kitkcare log_utilsutils)loggercCs|jS)aSend the DataPackage archive to the patch server. Upload errors are logged and swallowed (catch_errors), preserving the historical kernel-anomaly behavior. :param data_package: DataPackage instance to send :return: Upload name (package identifier) )send) data_packager -/usr/libexec/kcare/python/kcarectl/anomaly.pysend_data_packages r cCsTtj}xFtj|D]8\}}||dkr*Pdj||jdd}|j||dqWdS)zZadds recent files for the last hour to the given data package starting from the newest oneiz{0}/{1}/_)src_pathN)timerZsort_files_by_ctimeformatreplaceadd_file)filesrZarchive_prefixnowpathctimeZarcnamer r r copy_recent_filess  rc3Cstj}|tjjdr(|jddtjjdr@|jdd|jdd|jd d |jd d tjjd s|tjjd rd}n$tjjdrd}|jdddnd}|jd|td}|jdtj |j dWdQRXtd}|jdtj |j dWdQRX|jddd|j d||jddd|jd d!j t jytt j|d"Wn4tk r}z|jd#j |WYdd}~XnXytt j|d$Wn4tk r}z|jd%j |WYdd}~XnXWdQRX|S)&Nz/var/log/messagesmessagesztail -n10000 /var/log/messagesz/var/log/syslogZsyslogztail -n10000 /var/log/syslogz kcarectl.logz"tail -n10000 /var/log/kcarectl.logZdmesgZls_var_cache_kcarezls -lR /var/cache/kcare/z /usr/bin/rpmz/bin/rpmzFrpm -q -a --queryformat="%{N}|%{V}-%{R}|%{arch}|%{INSTALLTIME:date}\n"z /usr/bin/dpkgzJ/usr/bin/dpkg-query -W -f "${binary:Package}|${Version}|${Architecture}\n"zdpkg.logz/var/log/dpkg.log)rzecho "unknown package manager"z packages.listz /proc/versionZ proc_version)Z data_bytesz /proc/modulesZ proc_modulesz kcare.confz/etc/sysconfig/kcare/kcare.confzserver_info.jsonz kdump.confz/etc/kdump.confZls_kdumpz ls -lR {0}Zkdumpzfailed to copy kdumps: {0}Z crashreporterz+failed to copy crashreporter artifacts: {0})rZKernelAnomalyPackageosrexistsZ add_stdoutropenrbstrreadZadd_jsonrrZget_kdump_rootrZlist_kdump_txt_files ExceptionZ log_errorZlist_crashreporter_log_files) server_inforZ packages_cmdfer r r prepare_kernel_anomaly_report*sB          " " ",r"F)rZdefault_returnc Cs|d}t|d}t|jdp"d}t|d}|d}yt|dd}Wntttfk rld}YnX||||||g}t|sd Sd }d } d } |d kr|dkrd }|||ko||d knr|dkrd } ||krd } || | g} t| rtjdj|d Sd S)z3taken from eportal - anomalies::detect_agent_rebootreasonuptime patch_levelz-1 last_stoptsstaterFi,TiupdatezAgent anomaly detected: {0}) intgetKeyError TypeError ValueErrorallrloginfor) rr#r$r%r&r'Zstate_tsZfieldsZ first_update_after_reboot_markerZcrash_soon_after_update_markerZno_proper_shutdown_markermarkersr r r detect_anomaly_s8   , r4__main__)rrkcarectlrrrrZ catch_errorslogwarnr rr"r4__name__r r r r s 5-__pycache__/libcare.cpython-36.pyc000064400000044740152533440750013024 0ustar003 ;jb@s ddlZddlZddlZddlZddlZddlmZmZmZm Z m Z m Z m Z m Z mZmZmZmZmZmZddlmZmZmZdZdOZdZd Zd Zd d d d Zd dedgfddedgfgZej ddZ!ddZ"ddZ#ddZ$Gddde%Z&ddZ'dPddZ(dd Z)d!d"Z*d#d$Z+dQd&d'Z,d(d)Z-e$d*d+Z.e$d,d-Z/e$d.d/Z0d0d1Z1d2d3Z2d4d5Z3d6d7Z4d8d9Z5dRd:d;Z6e$e'ej7dd?Z9ej7ej:d@ej;e$e'e jdEdFZ?dGdHZ@dSdIdJZAdKdLZBdMdNZCdS)TN)auth capabilitiesconfigconfig_handlers constantserrorsfetch log_utilsplatform_utils process_utilsselinux server_info update_utilsutils) HTTPErrorjson_loads_nstrurlquotez!/usr/libexec/kcare/libcare-client/run/libcare/libcare.sock/var/run/libcare.sockz /var/cache/kcare/libcare_patchesz /var/cache/kcare/libcare_cvelistz&/etc/sysconfig/kcare/libcare.logrotatedb)mysqldmariadbdpostgresZubuntuz16. libnss_dnsz7.c sttj}ty tj}|dj}|d}Wn0tk r`xtD]\}}}|OqFWYn>X|d}x2tD]*\}}}|dks||krp|j |rp|PqpWfdd|DS)Nrr.csg|]}|kr|qSr).0lib) blacklistedr-/usr/libexec/kcare/python/kcarectl/libcare.py Rsz/_get_effective_libcare_libs..) listr LIBCARE_LIBSsetr get_distrolower ExceptionLIBCARE_LIBS_BLACKLIST startswith) libsZ distro_infodistroversion_Zbl_libsZversion_dottedZ bl_distroZ bl_versionr)rr _get_effective_libcare_libs;s   r.cCsdddgtdS)Nrrr)rr*)r.rrrr get_userspace_mapUsr/cGstjjtjd|f|S)N userspace)ospathjoinr PATCH_CACHE)libnamepartsrrr get_userspace_cache_path\sr7csfdd}|S)NcsVz ||Sy tdWn6tk rN}ztjdj|ddWYdd}~XnXXdS)N clearcachez$Libcare cache clearing failed: '{0}'F) print_msg)libcare_clientr'r logerrorformat)argskwargserr)clblrr wrapperas   z$clear_libcare_cache..wrapperr)r@rAr)r@r clear_libcare_cache`s rBcs0eZdZdfdd Zd ddZddZZS) UserspacePatchLevelNcst||j||S)N)super__new__)clsr5buildidlevelbaseurl) __class__rr rEoszUserspacePatchLevel.__new__cCs||_||_||_||_dS)N)rHr5rGrI)selfr5rGrHrIrrr __init__rszUserspacePatchLevel.__init__cGst|j|jt|f|S)N)r7r5rGstr)rKr6rrr cache_pathxszUserspacePatchLevel.cache_path)N)N)__name__ __module__ __qualname__rErLrN __classcell__rr)rJr rCns rCcsddfdd}|S)NcSsd \}}z|dkrt}i}g}x8t|D],}|jdd||jd<|j|jdgq*Wtj|dd}djd d |jD}djt|}Wdtj t |d d tj t |d d XdS)z(KPT-1543 Save info about applied patchesNzlatest-versionpackagepatchescve) cve_field cSsg|]}dj|qS) )r3)rrecrrr r!szLrefresh_applied_patches_list..save_current_state..T) ensure_dir)rSrS) _libcare_info_get_patches_infogetextendrextract_unique_cvesr3itemssorted atomic_writeLIBCARE_PATCHESLIBCARE_CVE_LIST)infoZversionsZcvesZpackagesZ all_patchesrZZcves_setrrr save_current_state}sz8refresh_applied_patches_list..save_current_statec s"d}z||}|S|XdS)Nr)r=r>rf)r@rgrr rAs  z-refresh_applied_patches_list..wrapperr)r@rAr)r@rgr refresh_applied_patches_list|srhc"Cstjpd}t|}t|j}tjtj|d|||d}|dtj d|7}tj|d}|rlt j j d||}i}|rd|i|d <y t jtj|fd d i|}Wntjk r|rtjd j||d d dStjt||ddYnLtk r4} z.| jdkr"|r"tjdj|ddWYdd} ~ XnXtj|jttj |j!} | jdg} t"j#| s|tj$dj|t%||| d| jd} t&| d} t||| d}t j j'| st j j(|dkr:tj| d}yt j)||tj*t j+| dWn<tk r8} z| jd(kr&tj,dWYdd} ~ XnXt||| }dd |d!|d"g}t-j.|ddd#\}}}|rtjd$j|||d%dt||d&}t j j/| rt j j0|rtj|t j1| |d't j2|d'|| S))Nmainuz latest.v1z?info=updater*tagsZStickyheaders check_licenseFz;No libcare patchset available for {0} at tag {1}, skipping.)r9T) ignore_errorsiz8Invalid sticky patch tag {0}: server rejected the value.zinvalid lib sticky patch)statusrzkLatest LibCare patchset for {0} is incompatible with the current kernecare package version, please upgrade.rHrIz patch.tar.gzrZ patch_url)check_signature hash_checkerzKC+ licence is requiredtarZxfz-Cz--no-same-owner) catch_stdout catch_stderrz(Patches unpacking error: '{0}' '{1}' {2}zpatches unpacking errorlatestz.tmp)rsrt)3rPREFIXrstriprget_patch_server_url LIBNAME_MAPr^rZencoded_server_lib_infor1r2r3r wrap_with_cache_keyr urlopen_authrNotFoundr loginfor<shutilrmtreer7rcode KcareErrorrset_feature_flags_from_headersrmrnstrreadrhas_lc_capabilitiesCapabilitiesMismatchrCrMexistsgetsize fetch_url USE_SIGNATUREget_hash_checkerNoLibcareLicenseExceptionr run_commandislinkisdirsymlinkrename)r5build_id patch_leveltagprefixurl cache_dstZ extra_kwargsresponseexmetarequired_capabilitiesrHplevelZ patch_pathdstcmdrstdoutstderrZ link_namerrr fetch_userspace_patchsr          "     rcCsL| t_|sttj|rdndd|r0ttjjd|r@dnddS)NFALSEYES)LIBCARE_DISABLEDzlibcare service is enableddisabled) rrlibcare_server_stopr update_configlibcare_server_startr kcarelogrf)rrrr set_libcare_statussrc Cstjstjjtjr^tjtjddgtjtjddgtjtjddgtjtjddgn6ytjdd ddg}Wnt k rdSXtj|dS) Nstopzlibcare.socketzlibcare.servicez reset-failedlibcareservice /usr/sbin//sbin/)rr) rSKIP_SYSTEMCTL_CHECKr1r2r SYSTEMCTLr rfind_cmdr')rrrr rsrc Cstjstjjtjrtjtjdddgddd\}}}|dkr@dStjtjddgtjtjddgtjtjd d gtjtjd dgtjtjd dgn@trdSytj d dd d g}Wnt k rdSXtj|dS)Nz is-activez--quietzlibcare.serviceT)rvrwrrzlibcare.socketz reset-failedrstartr /usr/sbin//sbin/)rr) rrr1r2rrr rlibcare_server_startedrr')rr-rrrr rs& rTcsdjddt|pgD}ddg}s6|dd|g7}y t|}Wn6tk rx}ztjdj|d d WYdd}~XnXg}x@|jd D]2}|ry|jt j |Wqt k rYqXqWd d |D}x.|D]&}t fdd|dj D|d<qW|S)N|css|]}djtj|VqdS)z({0})N)r<reescape)rprocrrr $sz _libcare_info..rfz-jz-lz-rz/Gathering userspace libraries info error: '{0}'zuserspace libs info error)rprXcSs$g|]}|jd|jd|dqS)commpid)rrr*)pop)rlinerrr r!:sz!_libcare_info..c3s(|] \}}d|ks r||fVqdS)patchlvlNr)rkv)patchedrr r=sr*)r3rbr:r'rrr<splitappendjsonloads ValueErrordictra)rlimitZregexprlinesr?resultrr)rr r\ s& &  &r\c Cst}x<|D]4}x.|djD]\}}|j|d|dfqWq Wg}t}xb|D]Z}xT|D]L\}} t||t| d} tjj| r^t | d} |j t j | WdQRXq^WqTW|S)Nr*rGrz info.jsonr) r$raaddr/r7rMr1r2isfileopenrrload) rfrUrZr-datarumaprrrZpatch_info_filenamefdrrr r]Bs    "r]cCs ttS)N)r]r\rrrr libcare_patch_info_basicRsrcCs"t}|stjdtjd|iS)NzNo patched processes.r)rr r;rdumps)rrrr libcare_patch_infoWs rcCs"t}|stjdtjd|iS)NzNo patched processes.r)r\r r;rr)rrrr libcare_info_s rcCs.i}x$tD]}|jdd||jd<q W|S)Nzlatest-versionrSrT)rr^)rrZrrr _libcare_versiongs rcCs*x$tjD]\}}|j|r |Sq WdS)NrS)rrar))r5rTr,rrr libcare_versionns rcCsdjdd|DdS)Ncss|]}tj|dVqdS)N)rbstr)rprrr rvsz(libcare_client_format..r)r3)paramsrrr libcare_client_formatusrcCs,xtD]}tjj|r|SqWtjddS)NzLibcare socket is not found.)LIBCARE_SOCKETr1r2rrr)Zlibcare_socketrrr get_available_libcare_socketys  rc Gstjrtjdtjtjtjd}|jdd}z||jt |jtj t |}t j dj|d|j|x|jd}|sP||7}qpW|jdd }t j d j|d |S|jXdS) NzLibcare is disabled.r rzLibcare socket send: {cmd})rizutf-8replacez!Libcare socket recieved: {result})r)rrrrsocketAF_UNIX SOCK_STREAM settimeoutconnectrLIBCARE_SOCKET_TIMEOUTrr logdebugr<sendallrecvdecodeclose)rsockresrrrrrr r:s(        r:cCs|rzxp|D]h}td||}tjj|s(q ytdd|Wq tk rp}ztjdj||ddWYdd}~Xq Xq WdSx|D]}ytdt|Wn6tk r}ztjdj|d dWYdd}~XnXy tdWqtk r}ztjd j|ddWYdd}~XqXqWdS) Nrlrkz --storagez3Userspace patch applying error (storage={0}): '{1}'zuserspace patch apply error)rpZstoragez(Userspace storage switching error: '{0}'zuserspace storage switch errorz%Userspace patch applying error: '{0}') r7r1r2rr:r'rrr<)rrrZ storage_pathr?rrr libcare_patch_applys*     & rcCsHy tdWn6tk rB}ztjdj|ddWYdd}~XnXdS)Nunloadz&Userspace patch unloading error: '{0}'zuserspace patch unload error)rp)r:r'rrr<)r?rrr libcare_unloads rcCsDy tdWn2tk r>}ztjdj|WYdd}~XnXdS)z;Reload libcare-server plugin without restarting the server.ZrepluginzUserspace replugin error: '{0}'N)r:r'rrr<)r?rrr libcare_replugins rrcCst|tjkrtj rdSt}|dkr6t|j}g}x|D]}|j|j |gq@W|srt j dj |dSt ||d\}}}} |rtjd|st j ddStjtjjtjdtyt||dWn>tjk r} zt jt| tjdWYdd} ~ XnXt} t| } ttd d | Ds.zPatched before: {before})beforezPatched after: {after})aftercss|]}|D] }|Vq qdS)Nr)rrarrrr r scss|]}|D] }|Vq qdS)Nr)rrarrrr r scss|]}t|VqdS)N)len)rrrrr rszThe patches have been successfully applied to {count} newly discovered processes. The overall amount of applied patches is {overall}.)countoverallz*Object `{0}` is patched for {1} processes.)"rotate_libcare_logsrUPDATE_MODE_AUTOrLIB_AUTO_UPDATEr/r"keysr_r^r rr<check_userspace_updatesrrr restore_selinux_contextr1r2r3r4rr;rMr\_get_userspace_procsanyrr$valuessumrra)moderrrZprocess_filterZuserspace_patchfailedsomething_foundr-rrZ data_afterrZuniq_procs_afterZuniq_procs_beforeZdiffrrrrrr do_userspace_updatesR    r cCs\z.yt\}}}}Wntjk r*dSXWdtX|r@dS|rHdStjddrXdSdS)Nrz.libcarestatus)filenamer)rrrrrstatus_gap_passed)r r-libs_not_patchedrrr get_userspace_update_statuss rcCsdi}xZ|D]R}xL|djD]<\}}|jdr||kr>g||<||j|d|dfqWq W|S)Nr*rrr)rar^r)rfrrr5rZrrr r.s  "rcCsNt}xB|D]:}x4|djD]$\}}|j||d|jddfqWq W|S)Nr*rGrr)r$rarr^)rfrrr5rZrrr _get_userspace_libs9s  $rcs\s$t}gfdd|jDtdd}t|}d}}d}xt|D]}|\} } } y2t| | | |d} |r|| dkr|wNd}| dkrd}WqNtjk r} zd}tj t | WYdd} ~ XqNtj tj fk rYqNtj k rYqNtjk r>}z,t|ddd krd}tjt |WYdd}~XqNXqNWtjd d ||||fS) Ncsg|]}j|qSr)r_)rr*)rrr r!Esz+check_userspace_updates..F)rrT)rrrpzinvalid lib sticky patchz.libcarestatus)r)r/rr\rrrrrr logwarnrMrrAlreadyTrialedExceptionrgetattrr;rtouch_status_gap_file)rrrZ data_beforerr r rrZr5rrrerr)rr rAs>   $ rc sfd}d}tjddd}|rytj|tgdd\}}}Wn.tk rd}zd}t|}WYdd}~XnX|rtjd j|dd ntj d dd d t j j sdSt jd}yt j}tjdfdd|D}dd|D}|jddd} xD|D]<\}} | t j j| 7} | |krt j| tjjd| qWWn$tk r`tjddd YnXdS)NrrSZ logrotateF) raise_excT)rwrz5failed to run logrotate for libcare logs, stderr: {0})r9zlogrotate utility wasn't foundz/var/log/libcare/irz ^\d+\.log.*cs$g|]}j|rtjj|qSr)matchr1r2r3)rfn)libcare_log_directory pidlog_rerr r!sz'rotate_libcare_logs..cSsg|]}tjj||fqSr)r1r2getctime)rfprrr r!s)reversez%Removed %s because of logs size limitz)Failed to cleanup libcare server logfilesi)r rrLIBCARE_LOGROTATE_CONFIGr'rMr r;r<rr1r2rr!LIBCARE_PIDLOGS_MAX_TOTAL_SIZE_MBlistdirrcompilesortrremoverrflogexc) rcrZlogrotate_pathr-rZmax_total_sizeZ log_filesZ pidlog_filesZpidlog_files_with_ctZ total_sizefilepathr)rrr rrs<       rc CsJytjdd ddg}Wntk r*dSXtj|ddd\}}}|d kS) zKAssume that whenever the service is not running, we did not patch anything.r /usr/sbin//sbin/rrpFT)rvrwr)r+r,)r rr'r)rrr-rrr rs r)rr)NN)TN)N)NN)Drr1rrrrSrrrrrrr r r r r rrrpy23rrrZLIBCARE_CLIENTrrdrer"r|r$r(cachedr.r/r7rBintrCrhrrrrr\r]rrrrrrrr:rlog_all_parent_processesrrtrack_update_statusskip_if_no_selinux_moduleUPDATE_MODE_MANUALr rrrrrrrrrr sh@  L  "    $ H   1*__pycache__/py23.cpython-36.pyc000064400000003704152533440750012213 0ustar003 ;j @s"ddlZddlmZejrddlZddlmZddlmZddl m Z ddl m Z ddl m Z dd l mZdd l mZdd lmZGd d d e Z ddZddZnbddlm Z ddlmZddlm Z mZddlmZddlmZmZddlm Z dd lmZejZddd dddddddg ZdS)N) constants)quote) urlencode) ConfigParser) HTTPError)Request)URLError)urlopen)urlparsec@seZdZddZdS)rcs8|jddtj|f||dkr4fdd|_dS)NmethodHEADcstS)N)str)r r*/usr/libexec/kcare/python/kcarectl/py23.pysz"Request.__init__..)pop StdRequest__init__Z get_method)selfargskwargsr)r rrs zRequest.__init__N)__name__ __module__ __qualname__rrrrrrsrcCsVt|}|tdkr|jdS|tkr4dd|DS|tkrRtdd|jDS|S)Nzutf-8cSsg|] }t|qSr)_convert).0itrrr %sz_convert..css"|]\}}t|t|fVqdS)N)r)rkvrrr 'sz_convert..)typeencodelistdictitems)dataZdtyperrrr s  rcCsttj|S)N)rjsonloads)Zjson_strrrrjson_loads_nstr*sr+)client)rr )rr rrr httplib std_urlopenrr urlquote)r)rrPY2r-Zurllibrr/rrZurllib2rrrr r r.r rr+ configparserZhttpr,Z urllib.errorZ urllib.parseZurllib.requestr*__all__rrrrs@                __pycache__/__main__.cpython-36.pyc000064400000001374152533440750013137 0ustar003 ;j#@s*ddlZddlZddlmZmZmZmZddlmZdej d<e dkr&yej eWnek rZ z ej dje ee d d WYddZ [ Xnejk rZ zej ee ej dWYddZ [ XnXek r$Z z:ee ejreje jn eje eej dWYddZ [ XnXdS) N)errors log_utilsmainsend_exc)URLError1Z"KCARE_SCANNER_INTERFACE_DO_NOTHING__main__z{0}: {1}urlunknown)ossysrrrrpy23renviron__name__exiterrlogerrorformatgetattr KcareErrorstr Exception isinstanceSafeExceptionWrapperlogexcinnerrr./usr/libexec/kcare/python/kcarectl/__main__.pys$   *  __pycache__/serverid.cpython-36.pyc000064400000003446152533440750013244 0ustar003 ;j@sjddlZddlmZmZddlmZdZdZdZdd Z d d Z d d Z ej ddZ ddZddZdS)N)errorsutils)json_loads_nstrz/etc/sysconfig/kcare/systemidz&/etc/sysconfig/kcare/systemid.almacarez/var/imunify360/license.jsonc CsptjjtsdSttdL}xD|D]<}|jd\}}}|jdkrJ|jStjdj tddq"WWdQRXdS)Nr= server_idzUnable to parse {0}.zserver id parse error)status) ospathexistsSYSTEMIDopen partitionstripr KcareErrorformat)fdlineparam_valuer./usr/libexec/kcare/python/kcarectl/serverid.py _systemids    "rc Cs2tjjtsdSttd}|jjSQRXdS)Nr)r r r ALMA_SYSTEMIDrreadliner)frrr_alma_systemid!s  rcCs`tjjtsdSi}tt4}|j}|rLy t|}Wntk rJYnXWdQRX|jdS)Nid) r r r IM360_LICENSE_FILErreadr Exceptionget)datarcontentrrr_im360_systemid)s   r&cCstptptS)zqGet server_id or None if not present. Lookup order: SYSTEMID then IM360_LICENSE_FILE then ALMA_SYSTEMID )rr&rrrrr get_serverid9sr'cCstjtdS)N)r unlinkr rrrr rm_serveridCsr)cCstjtdj|dS)Nzserver_id={0} )r atomic_writer r)rrrr set_server_idHsr+)r rrpy23rr rr rrr&cachedr'r)r+rrrrs  __pycache__/kcare.cpython-36.pyc000064400000025654152533440750012513 0ustar003 ;j)@sddlZddlZddlZddlZddlZddlZddlZddlZddlm Z m Z m Z m Z m Z ddlmZddlmZdZddZd d Zd d Zd dZddZddZddZGdddeZGdddeZGdddeZddZddZdd Z d!d"Z!d#d$Z"d%d&Z#d'd(Z$d)d*Z%e j&d+d,Z'e j&d-d.Z(e j&d/d0Z)d1d2Z*d3d4Z+d5d6Z,d7d8Z-d9d:Z.d;d<Z/e j&d=d>Z0d?d@Z1dAdBZ2dCdDZ3dEdFZ4dS)GN)config constants log_utils process_utilsutils)SafeExceptionWrapper)json_loads_nstrzuname: cCstj|p|dkS)Nz.-_+)strisalnum)cr +/usr/libexec/kcare/python/kcarectl/kcare.py is_uname_charsrcCsft}tt||tjdB}x:|jD].}|jtr&djt t |t tdj Sq&WWdQRXdS)Nr) get_kernel_hashopenget_cache_pathr PATCH_INFO readlines startswith UNAME_LABELjoinfilterrlenstrip) patch_levelkhashfliner r r parse_unames  .r!c CsJtjjtjrFy"ttjd}|j||jdStk rDYnXdS)NwTF) ospathexistsrKCARE_UNAME_FILErwriteclose Exception) new_versionrr r rkcare_update_effective_version%s  r+c Cs.ttjd}ztj|jjS|jXdS)Nrb)rrKERNEL_VERSION_FILEhashlibsha1read hexdigestr()rr r rr1s rcCstjjtjd}tjj|rt|dV}|jj}y t |Wn6t k rdt t tjj |St k rvdSX|SQRXdS)z:Returns timestamp from PATCH_CACHE/stoped.at if its exsitsz stopped.atrerrorNz-1)r#r$rr PATCH_CACHEr%rr0rstripint ValueErrorr getctimer))Zstopped_at_filenameZfhvaluer r r get_last_stop<s     r9cCsPtjpd}tjpd}dj||t||g}tjd|f}|rD||f7}tjj|S)Nnonedefault-patches) rPREFIX PATCH_TYPErr rr3r#r$)rplevelfnameprefixptypeZ patch_dirresultr r rrMs    rcGstjtjf|S)N)rget_patch_server_urlrr>)partsr r rget_kernel_prefixed_urlWsrGc@seZdZddZddZdS)BaseKernelPatchLevelcGst|jt|f|S)N)rrr )selfrFr r r cache_path\szBaseKernelPatchLevel.cache_pathcCs|j|j|j|jdS)N)levelrbaseurlrelease)rKrrLrM)rIr r ras_dict_szBaseKernelPatchLevel.as_dictN)__name__ __module__ __qualname__rJrNr r r rrH[srHcs8eZdZd fdd Zd ddZddZdd ZZS) KernelPatchLevelNcst||j||S)N)super__new__)clsrrKrLrM) __class__r rrTiszKernelPatchLevel.__new__cCs||_||_||_||_dS)N)rKrrLrM)rIrrKrLrMr r r__init__lszKernelPatchLevel.__init__cGstj|j|jf|S)N)rrErLr)rIrFr r rkmod_urlrszKernelPatchLevel.kmod_urlcGstj|j|jt|f|S)N)rrErLrr )rIrFr r rfile_urluszKernelPatchLevel.file_url)N)N)rOrPrQrTrWrXrY __classcell__r r )rVrrRhs rRcs<eZdZfddZddZddZddZd d ZZS) LegacyKernelPatchLevelcsByt||j||Stk r<}zt|WYdd}~XnXdS)N)rSrTr6r)rUrrKexc)rVr rrTzszLegacyKernelPatchLevel.__new__cCs||_||_d|_d|_dS)N)rKrrLrM)rIrrKr r rrWszLegacyKernelPatchLevel.__init__cGs0dtjkr t|jt|f|St|jf|S)Nzpatches.kernelcare.com)r PATCH_SERVERrGrr )rIrFr r rrXs zLegacyKernelPatchLevel.kmod_urlcGst|jt|f|S)N)rGrr )rIrFr r rrYszLegacyKernelPatchLevel.file_urlcCst|jt||S)N)rRrr5)rIrLr r rupgradeszLegacyKernelPatchLevel.upgrade) rOrPrQrTrWrXrYr^rZr r )rVrr[ys  r[cCs^y6ttjjtjdd}tj|j|WdQRXWn"t k rXt j dddYnXdS)Nzkernel_patch_level.jsonr"z!failed to dump kernel patch levelF) print_msg) rr#r$rrr3jsondumprNr)rlogexc)Zkernel_patch_levelrr r rdump_kernel_patch_levels rccCsTy,ttjjtjd}t|jSQRXWn"tk rNt j dddYnXdS)Nzkernel_patch_level.jsonz(failed to read dumped kernel patch levelF)r_) rr#r$rrr3r r0r)rrb)rr r rread_dumped_kernel_patch_levels rdcCstdd|DddddS)NcSsg|]}|tjj|fqSr )r#r$r7).0itr r r sz'sort_files_by_ctime..cSs|dS)Nrr )pairr r rsz%sort_files_by_ctime..T)keyreverse)sorted)Z files_listr r rsort_files_by_ctimes rmc Cs\d}tjjds|Std6}x.|D]&}|j}|jdr$|jdd\}}q$WWdQRX|S)Nz /var/crashz/etc/kdump.confzpath r)r#r$isfilerrrsplit)Z kdump_pathZ kdump_confr _r r rget_kdump_roots    rqcCs*t}tjj|sgStjtjj|dS)Nz*/vmcore)rqr#r$isdirglobr) kdump_rootr r rlist_kdump_vcore_filess rucCs*t}tjj|sgStjtjj|dS)Nz*/*.txt)rqr#r$rrrsr)rtr r rlist_kdump_txt_filess rvcCs(tjjtjsgStjtjjtjdS)Nz*.log)r#r$rrr KDUMPS_DIRrsrr r r rlist_crashreporter_log_filessrxcCs(tjjtjsgSddtjtjDS)NcSsg|]}tjjtj|qSr )r#r$rrrw)rerfr r rrgsz0list_crashreporter_artifacts..)r#r$rrrrwlistdirr r r rlist_crashreporter_artifactssrzcCst}|sdSt|ddS)Nrr)rurm)Zkdumpsr r rkdumps_latest_event_timestampsr{cCs>tjstjjtjr:tjtjddgddd\}}}|jSdS)Nz is-activekdumpT) catch_stdout catch_stderrzsystemd-absent) rSKIP_SYSTEMCTL_CHECKr#r$rn SYSTEMCTLr run_commandr)rpstdoutr r r kdump_statussrcCst}|sdSt|ddS)Nrr)rzrm)Z artifactsr r r$crashreporter_latest_event_timestampsrc Cs:d}tjj|sdSt|d}|jj}WdQRX|S)Nz/sys/module/kcare/versionr)r#r$r%rr0r)Zkmod_version_filerversionr r rget_current_kmod_versions   rcCs6t}|sdStjdddt||tjgj}||kS)NTz /sbin/modinfoz-Fr)rr check_outputrrKMOD_BINr)rr@Z old_versionr*r r ris_kmod_version_changeds rcCst}|stjSt|S)N)loaded_patch_levelplatformrMr!)rr r rkcare_uname_susrcCs,tjjtjr"ttjdjjStSdS)Nr) r#r$r%rr&rr0rrr r r r kcare_unamesrcCs`ttd}|r\y t|Wn0tk rN}zt|dtWYdd}~XnXtt|SdS)Nz patch-levelzUnexpected patch state)parse_patch_descriptionloaded_patch_descriptionr5r6r _patch_infor[r)pler r rrs  rcCsJytjtjdgddStjk rD}z|jtjkr6dSd}~XnXdS)NinfoT)checkr) rrr KPATCH_CTL subprocessCalledProcessError returncodeerrnoZEBUSY)rr r rr!s  rcCsRyddtdDSttfk rL}ztjdt|ddgSd}~XnXdS)NcSsg|]}|jdqS)r)ro)rer r r rrg-sz&get_loaded_modules..z /proc/modulesz#Error getting loaded modules list: F)r_)rOSErrorIOErrorrlogerrorr )exr r rget_loaded_modules*s rcCsdtkrdSttdS)Nkcarezkpatch-description)rget_patch_valuerr r r rr3s rcCstj|j|S)N)r data_as_dictget)rZlabelr r rr;src Csrddddd}|s|S|jd\}}}|jd\}}}|jd\}}}|pLd|d<|pXd|d<||d <||d <|S) Nr;r)z patch-levelz patch-typez last-updatezkernel-version;:r<z patch-levelz patch-typez last-updatezkernel-version) partition) descrDZlevel_type_timestamprpkernelZ level_type timestamprZ patch_typer r rr?s  rcCshtjjtjd}tjj|rdt|d8}y|j}tj |St t t t tfk rXYnXWdQRXdS)Nz kcare.stater)r#r$rrr3r%rr0astZ literal_eval SyntaxErrorrr6 TypeErrorUnicodeDecodeError)Z state_filerstater r r get_stateSs   r)5rrrsr.r`r#rrrrrrrrerrorsrpy23r rrr!r+rr9rrGr5rHrRr[rcrdrmrqrurvrxrzcachedr{rrrrrrrrrrrrrr r r rsV               __pycache__/process_utils.cpython-36.pyc000064400000006253152533440750014316 0ustar003 ;j4@spddlZddlZddlZddlZddlmZmZejdddZddd Z dd d Z d d Z ddZ ddZ dS)N) log_utilsutilsTcCsR|pd}x*|D]"}tjj||}tjj|r|SqW|rJtdj||ndSdS)N /usr/sbin/sbin/usr/bin/binz{0} could not be found at {1})rrrr)ospathjoinisfile Exceptionformat)namepathsZ raise_excitfnamer3/usr/libexec/kcare/python/kcarectl/process_utils.pyfind_cmds  rFc Cs|r tjnd}|rtjnd}tj||||d}|j\}} |j} |dk rRtj|}|dk rdtj| } tjt j dj |j|| dj |d|r| rtj | |} || _| | _| | || fS)N)stdoutstderrshellz Call result for `{cmd}`: exit code {exit_code} === STDOUT === {stdout} === STDERR === {stderr} === END ===  )Z exit_coderrcmd) subprocessPIPEPopen communicate returncodernstrrlogdebugtextwrapdedentrr CalledProcessErroroutputr) command catch_stdout catch_stderrrcheckrrpZstdout_capturedZstderr_capturedcodeexcrrr run_commands&     r-cCst|d|d\}}}|S)NT)r'r))r-)argsr)_rrrr check_outputDsr0c Csydddddt|g}t|dd\}}}|rFtjdj|d d d dS|j}dddd dt|g}t|dd\}}}|rtjd j|d d d dS|j}t||fStk r}ztjdj||dd d dSd}~XnXdS)Npsz --no-headersz-oppidz-pT)r'z3Could not retrieve process parent PID for PID {pid})pidF) print_msgZcommz-Could not retrieve process name for PID {pid}zJCould not retrieve process name and parent PID for PID {pid}, error: {err})r3err)NN)NN)NN)strr-rloginforstripintr ) r3Zcmd_ppidr+rr/r2Zcmd_commrerrr _get_parent_pid_and_process_nameIs$ r;cstjfdd}|S)zMDecorator that logs parent process chain before calling the wrapped function.cst||S)N)_log_all_parent_processes)r.kwargs)funcrrwrapperfsz)log_all_parent_processes..wrapper) functoolswraps)r>r?r)r>rlog_all_parent_processescsrBcCsg}tj}x<|dkrH|dkrHt|\}}|j||f|dkrBP|}qWtjdddxJtt|D]:\}\}}d|d}tjdj||pd |pd d ddqfWdS) NrrzAgent parent processes chain:F)r4-z->z{prefix} "{name}" (pid: {pid})unknown)prefixrr3) r getpidr;appendrr7 enumeratereversedr)Z process_chainZ current_pidr2Z process_namelevelr3rrErrrr<ns  r<)NT)FFFF)F)r@r rr"rrcachedrr-r0r;rBr<rrrrs  %  __pycache__/server_info.cpython-36.pyc000064400000006067152533440750013744 0ustar003 ;j@sddlZddlZddlZddlZddlZddlZddlmZmZm Z m Z m Z m Z m Z mZd ddZdddZdd d Zd d ZdS)N) capabilities constants http_utilskcareplatform_utilsserverid update_utilsutilsFc Cst}t|ptj|d<||d<tj|d<tj|d<tj|d<tj|d<tj|d<t j }|d|d <|d |d <t j |d <t jtj|d <t j|d<t j|d<t j|d<t j|d<tj|d<t jt j}|d|d<|d|d<|d|d<t jpd|d<t j|d<t j|d<tj|d<yt j|d<Wntk rdYnXt j!}|r|||d<t j"}|dk r||d <t#j$d!|d"<|rt j%|d#<|rt j&|d$<|S)%Ntsreasonmachine processorreleasesystemversionrdistrordistro_versionZeuname kcare_version last_stopnodeuptimevirtproxyz last-updateZ ltimestampz patch-level patch_levelz patch-type patch_typekmodZcrashreporter_ts kdump_statusrZkdump_ts server_idstatekernel update_errorZ secure_boot perf_metrics)'dictinttimeplatformr rrrrr get_distror kcare_unamer strip_version_timestamprVERSION get_last_stop get_hostname get_uptimeget_virtr proxy_is_usedparse_patch_descriptionloaded_patch_descriptionget_current_kmod_version$crashreporter_latest_event_timestamprrget_kc_capabilites_bitskdumps_latest_event_timestamp Exceptionr get_serverid get_stater read_update_errorsecure_boot_infoget_performance_metrics) r nowr;r#datar descriptionrr r@1/usr/libexec/kcare/python/kcarectl/server_info.py server_infosT                      rBcCst}t|ptj|d<||d<||d<tj}|d|d<|d|d<tj|d<tjt j |d <tj |d <tj |d <tj |d <tj|d <d}tjjdrtjjd}||d<tjd|d<|S)Nr r rrrrrr rrrrrgz/var/lib/libcare/stopstop_tslibcarer")r$r%r&rr(r'r r r*rr+r-r.r/rget_lc_capabilites_bitsospathexistsgetctimer r:)r rr=r>rrCr@r@rAserver_lib_infoIs&         rJcCst|||d}t|ddS)N)r rr=T) b64_encoding)rJencode_checkin_payload)r rr=infor@r@rAencoded_server_lib_infodsrNcCsP|r2tj|ddd}tjtjtjtj|dStjtj tjt |SdS)NF,:) ensure_ascii separatorszutf-8)rOrP) jsondumpsr nstrbase64urlsafe_b64encodezlibcompressbstr b16encodestr)r>rKZdata_strr@r@rArLisrL)NFF)N)N)rVrSrFr'r&rXrrrrrrrr r rBrJrNrLr@r@r@rAs( 7  __pycache__/utils.cpython-36.pyc000064400000017065152533440750012563 0ustar003 ;j[#@sdddlmZddlZddlZddlZddlZddlZddlZddlZddl Z ddl m Z ddl m Z ej dZdZedZed ZedZdddZd?ddZddZd@ddZdedfddZddZdd Zd!d"Zd#d$Zd%d&Zd'd(Z ed)d*Z!d+d,Z"dAd-d.Z#dBd/d0Z$d1d2Z%dCd4d5Z&d6d7Z'd8d9Z(de)fdfd:d;Z*dS)D)print_functionN)datetime) constantsz^(\d+[.]\d+[-]\d+)Fwc Cstjj|}|r(tjj| r(tj|ytj|j}Wntk rP|}YnXtj ||tjj |ddd<}tj |j ||j ||jtj|j |j}WdQRXtj|tj} ztj| Wdtj| Xtj||dS)N.F)modedirprefixdelete)ospathdirnameexistsmakedirsstatst_mode ExceptiontempfileNamedTemporaryFilebasenamefchmodfilenowriteflushfsyncnameopenO_RDONLYcloserename) fnamecontent ensure_dirr Z create_modeZdnamerfZ tmp_fnameZ folder_fdr)+/usr/libexec/kcare/python/kcarectl/utils.py atomic_write"s$      r+utf-8cCs4t|tkr|St|tkr&|j|S|j|SdS)N)typentypebtypedecodeencode)dataencodingr)r)r*nstr>s    r4latin1cCst|tkr|j|}|S)N)r-utyper1)r2r3r)r)r*bstrHs  r7cCst|tkr|j|}|S)N)r-r/r0)r2r3r)r)r*ustrOs  r8cs4itjfdd}|_j|_|_|S)Nc sH|tt|jf}y|Stk r0YnX||}|<|S)N)tuplesorteditemsKeyError)argskwargsZ cache_keyresult)cachefnr)r*innerZszcached..inner) functoolswrapsr@clearorig)rArBr))r@rAr*cachedVs  rGcsFdkrtjdkrtjdkr*tjifdd}|S)Ncsfdd}|S)Ncs}xltD]`}y ||Stk rJ}z|s:WYdd}~XnXtj|t|tjdtj}qWy ||Stk r}zt |dWYdd}~XnXdS)Nrattempts) rangertimesleepminrandomuniformrRETRY_MAX_DELAYsetattr)r=r>Zldelay_exZfinal_ex)backoff check_retrycountdelayrAstater)r*rBvs     z'retry..decorator..innerr))rArB)rSrTrUrVrW)rAr* decoratoruszretry..decorator)r RETRY_DELAY RETRY_COUNT RETRY_BACKOFF)rTrUrVrSrXr))rSrTrUrVrWr*retryjsr\c Cstjj|sdSg}tj|}|dk r2tj||}xD|D]<}tjj||}||krh|jtj|j |f|j ddq8WxF||dD]6\}} tjj | stjj | rtj | qtj| qWdS)NT)reverse)rrrlistdirfnmatchfilterjoinappendrst_mtimesortisfileislinkremoveshutilrmtree) Z directory exclude_pathkeep_npatternr2r;item full_pathrQentryr)r)r*clean_directorys     rpcCsNttjjtjdddttjjtjdddtjjtjrJtjtjdS)Nmodulesr)rkpatches) rprrrar PATCH_CACHErCACHE_KEY_DUMP_PATHunlinkr)r)r)r*clear_all_cachesrvc Cs^tjj|}tjj|s"tj|t|d(}tj|||jtj |j WdQRXdS)Nwb) rrrrrr!rh copyfileobjrrr)responsedstZ parent_dirr(r)r)r* save_to_files     r{cCstj|}|r|jdp|S)Nr) VERSION_REmatchgroup)versionr}r)r)r*strip_version_timestamps rcCs0|jd\}}}|s$|jd\}}}tj|dS)NT z%Y-%m-%d) partitionrstrptime)Zstr_rawZstr_dateseprQr)r)r*parse_response_datesrcGs0ddlm}djddtd|jf|DS)Nr) ipv6_support/css|]}|jdVqdS)rN)strip).0itr)r)r* sz'get_patch_server_url..)rrrar`get_patch_server)partsrr)r)r*get_patch_server_urls rc Cs0tjj|sdSt|}|jjSQRXdS)N)rrrr!readr)filenamer(r)r)r* try_to_reads  rcCs ttjS)N)rrrtr)r)r)r* get_cache_keysrc Cs.tjj|s|St|| }|jSQRXdS)N)rrrr!r)r%r defaultr(r)r)r* _read_files  rcCst|d|}|S)Nr)r)r%rr?r)r)r* read_files rcCst|d|}|S)Nrb)r)r%rr?r)r)r* read_file_bins rcCsBi}|j}x0|D](}|r|jd\}}}|r|j||<qW|S)N:) splitlinesrr)r2r?Z data_lineslinekey delimitervaluer)r)r* data_as_dicts rcvecCs0t}x$|D]}|j|}|r |j|q W|S)a Extract unique CVEs from a list of patches. Args: patches: List of patch dictionaries cve_field: Field name to extract CVE from (default 'cve' for userspace, 'kpatch-cve' for kernel) Returns: Set of unique CVE strings )setgetadd)rr cve_fieldZ unique_cvespatchrr)r)r*extract_unique_cvess   rcCstttjS)N)strintrJr)r)r)r* timestamp_strsrcGs t|dS)za workaround to fix T201N)print)valuesr)r)r* print_wrappersrcsfdd}|S)Ncs tjfdd}|S)Ncsny ||Sk rh}zBrZdd|Ddd|jD}djjdj||Sd}~XnXdS)NcSsg|] }t|qSr))r)rar)r)r* &szBcatch_errors..decorator..inner..cSsg|]\}}dj||qS)z{0}={1})format)rkvr)r)r*r&sz{0}({1}) failed: {2}z, )r;r__name__ra)r=r>earg_list)default_returnerrorsrAloggerr)r*rBs  z.catch_errors..decorator..inner)rCrD)rArB)rrr)rAr*rXs zcatch_errors..decoratorr))rrrrXr))rrrr* catch_errorssr)Fr r )r,)r5)r5)NNN)N)N)r)+ __future__rr_rCrrMrerhrrJrrrcompiler|Z CACHE_ENTRIESr-r.r/r6r+r4r7r8rGr\rprvr{rrrrrrrrrrrrrrr)r)r)r*sJ        $      __pycache__/errors.cpython-36.pyc000064400000006245152533440750012735 0ustar003 ;j@sddlmZGdddeZGdddeZGdddeZGdd d eZGd d d eZGd d d eZGdddeZ GdddeZ ddZ dS)) HTTPErrorc@seZdZdddZdS)SafeExceptionWrapperNcCs||_||_||_dS)N)inneretypedetails)selfrrrr,/usr/libexec/kcare/python/kcarectl/errors.py__init__ szSafeExceptionWrapper.__init__)NN)__name__ __module__ __qualname__r rrrr rsrcs$eZdZdZdZfddZZS) KcareErrora4Base kernelcare exception which will be considered as expected error and the full traceback will not be shown. Subclasses may set a class-level ``status`` to provide a short, fixed label for error reporting. Individual raise sites can override it per-instance via the ``status`` kwarg. cs2|jdd}|dk rt||_tt|j|dS)Nstatus)popstrrsuperrr )rargskwargsr) __class__rr r s  zKcareError.__init__)r r r __doc__rr __classcell__rr)rr rsrc@s eZdZdS)NotFoundN)r r r rrrr r!src@seZdZdZdS)NoLibcareLicenseExceptionzno libcare licenseN)r r r rrrrr r%src@seZdZdZdS)CapabilitiesMismatchzcapabilities mismatchN)r r r rrrrr r)srcs(eZdZdZfddZddZZS)AlreadyTrialedExceptionzalready trialedcs0tt|j|||d|jd|_||_dS)NT)rrr indexcreatedip)rr!r rr)rrr r 0sz AlreadyTrialedException.__init__cCsdj|j|jS)Nz6The IP {0} was already used for a trial license on {1})formatr!r )rrrr __str__5szAlreadyTrialedException.__str__)r r r rr r#rrr)rr r-s rcs eZdZdZfddZZS)UnableToGetLicenseExceptionzunable to get licensec s tt|jdt|f|dS)Nz6Unknown Issue when getting trial license. Error code: )rr$r r)rcoder)rrr r <s z$UnableToGetLicenseException.__init__)r r r rr rrr)rr r$9sr$c@seZdZdZdS)BadSignatureExceptionz bad signatureN)r r r rrrrr r&Bsr&csfdd}|S)Ncs t|S)N) isinstance)estate)exc_listrr rGszcheck_exc..innerr)r*rr)r*r check_excFs r+N) py23r Exceptionrrrrrrr$r&r+rrrr s   __pycache__/log_utils.cpython-36.pyc000064400000005540152533440750013417 0ustar003 ;j~ @sddlmZddlZddlZddlZddlZddlZddlmZm Z ej dZ ddZ dd d Z dd d Zdd dZdddZdddZddZddZddZd ddZdS)!)print_functionN)config constantskcarecCst|tjtj|dS)N) _printlvlr PRINT_DEBUGkcarelogdebug)messager //usr/libexec/kcare/python/kcarectl/log_utils.pylogdebugs rTcCs|rt|tjtj|dS)N)rr PRINT_INFOr info)r print_msgr r r loginfos rcCs$|rt|tjtjdtj|dS)N)file)rr PRINT_WARNsysstderrr warning)r rr r r logwarnsrcCs$|rt|tjtjdtj|dS)N)r)rr PRINT_ERRORrrr error)r rr r r logerror&srcCs&|rtjtjkrtjtj|dS)N)rrr PRINT_LEVEL traceback print_excr exception)r rr r r logexc,sr cCs|tjkrt||ddS)N)r)rrprint)r levelrr r r r2s rcCs:tjd}tjjdtjjjd}|jtj|j||S)Nz kcare %(levelname)s: %(message)sz/dev/log)addressZfacility)logging FormatterhandlersZ SysLogHandlerZLOG_USERsetLevelINFO setFormatter)Zsyslog_formattersyslog_handlerr r r get_syslog_handler7s    r+cCsntjd}tjdkrJtjjtjddd}|jt |tj |j ||Stj }|j||j ||SdS)Nz&%(asctime)s %(levelname)s: %(message)sri)ZmaxBytesZ backupCounti) r$r%osgetuidr&ZRotatingFileHandlerrLOG_FILEr'minr(r) StreamHandler)r"Zkcare_formatter kcare_handlerr r r get_kcare_handler?s     r3cCsgtjdd<yt|}tj|Wn,tk rP}ztj|WYdd}~XnXtjjdryt }tj|Wn,tk r}ztj|WYdd}~XnXdS)Nz/dev/log) r r&r3 addHandler Exceptionrr-pathexistsr+)r"r2exr*r r r initialize_loggingPs r9cCs"|pd}tdj|t||dS)Nzz8Unable to fetch {0}. Please try again later (error: {1}))rformatstr)r8urlstdoutr r r print_cln_http_errorasr>)T)T)T)T)N)NT) __future__rr$Zlogging.handlersr-rrrr getLoggerr rrrrr rr+r3r9r>r r r r s"       __pycache__/capabilities.cpython-36.pyc000064400000001146152533440750014045 0ustar003 ;j@s$ddZddZddZddZdS) cCsdS)Nrrr2/usr/libexec/kcare/python/kcarectl/capabilities.pyget_kc_capabilites_bits srcCsdS)Nrrrrrrget_lc_capabilites_bitssrcCs| S)Nr)required_capabilitiesrrrhas_kc_capabilitiessrcCs| S)Nr)rrrrhas_lc_capabilities srN)rrrrrrrrs __pycache__/selinux.cpython-36.pyc000064400000003523152533440750013104 0ustar003 ;j@sRddlZddlmZmZmZmZddZddZdd Zd d Z ej d d Z dS)N)errors log_utils process_utilsutilscCs"tjj|\}}tjj|d|S)Nztmp.)ospathsplitjoin)fnameheadtailr-/usr/libexec/kcare/python/kcarectl/selinux.pyselinux_safe_tmpname srcCsNtrJtjdd d|g}tj|ddd\}}}|rJtjdj|||dd dS) NZ restorecon /usr/sbin/sbinz-RT) catch_stdout catch_stderrz8SELinux context restoration for {0} failed with {1}: {2}F) print_msg)rr)is_selinux_enabledrfind_cmd run_commandrlogerrorformat)dnamecmdcode_stderrrrrrestore_selinux_contexts r cCsVtjddgdd\}}}|r2tjdj||ddx|jdD]}||kr>dSq>Wd S) Nz/usr/sbin/semodulez-lT)rz/SELinux modules list gathering error: '{0}' {1}zselinux modules error)status F)rrr KcareErrorrr )Z semodule_namerouterrlinerrris_selinux_module_presentsr'csfdd}|S)Ncs$trtd rtjd||S)Nlibcarez:SELinux is enabled but libcare policy module is not loaded)rr'rr#)argskwargs)clblrrwrapper%s z*skip_if_no_selinux_module..wrapperr)r+r,r)r+rskip_if_no_selinux_module$s r-cCs,tjjdr tjdg\}}}ndS|dkS)Nz/usr/sbin/selinuxenabledFr)rrisfilerr)rrrrrr-s r) rrrrrrr r'r-cachedrrrrrs    __pycache__/config.cpython-36.pyc000064400000004141152533440750012657 0ustar003 ;j @s ddlmZdZdZdZdZdZdZdZdZ dZ ddgZ dZ dZ dZd Zd Zd Zd Zejd ZdZdZd Zd Zd Zd ddddgZdZdZdZdZdZdZ dZ!dZ"dZ#dZ$dZ%ej&Z'd Z(d Z)dZ*d&Z+d Z,dZ-dZ.d Z/ej0Z1dZ2dZ3dZ4dZ5dZ6dZ7d'Z8d Z9d)Z:d+Z;d ZdZ?d Z@d!ZAd"ZBdS),) constantsNTzkpatch.blacklistz kpatch.fixupszkeep-registrationzmanage-libcareFz/dumpsdlibcZlibsslZnscdZlibmZ libnss_dnsi<z kpatch.binz.donez kpatch.infodefaultz /proc/versionz/proc/kcare/effective_versionizhttps://patches.kernelcare.comz#https://ipv6.patches.kernelcare.comz$https://cln.cloudlinux.com/api/kcarez)https://ipv6.cln.cloudlinux.com/api/kcareri@8i,il9i,ii@ii@)CrrAFTER_UPDATE_COMMANDAUTO_STICKY_PATCHSET AUTO_UPDATEAUTO_UPDATE_DELAYBEFORE_UPDATE_COMMANDBLACKLIST_FILECHECK_CLN_LICENSE_STATUSCHECK_SSL_CERTS FIXUPS_FILEFLAGS FORCE_GIDZFORCE_JSON_SIG_V3Z HTTP_TIMEOUTZHTTP_UPLOAD_TIMEOUTIGNORE_UNKNOWN_KERNEL KPATCH_DEBUGENABLE_CRASHREPORTER PATCH_CACHE KDUMPS_DIR KMSG_OUTPUT KCORE_OUTPUTKCORE_OUTPUT_SIZEIGNORE_FEATURE_FLAGSLIBCARE_DISABLEDZ LIBCARE_LIBSZ!LIBCARE_PIDLOGS_MAX_TOTAL_SIZE_MBZLIBCARE_SOCKET_TIMEOUTZLIB_AUTO_UPDATE PATCH_BIN PATCH_DONE PATCH_INFO PATCH_LEVEL PATCH_METHOD PATCH_TYPEPREFIXPREV_PATCH_TYPEZ PRINT_INFO PRINT_LEVELZ REPORT_FQDNSEND_PERF_METRICSSILENCE_ERRORSZSTATUS_CHANGE_GAP STICKY_PATCHSTICKY_PATCHSET UPDATE_DELAYUPDATE_FROM_LOCAL POLICY_REMOTE UPDATE_POLICYUPDATE_SYSCTL_CONFIGUSERSPACE_PATCHESZUSE_CONTENT_FILE_V3 USE_SIGNATUREZKERNEL_VERSION_FILEZKCARE_UNAME_FILESUCCESS_TIMEOUTKERNEL_ANOMALY_REPORT_ENABLE$KERNEL_ANOMALY_REPORT_MAX_SIZE_BYTESZDOCTOR_REPORT_MAX_SIZE_BYTESZFORCE_DOCTOR_FALLBACKZ FORCE_IPV4Z FORCE_IPV6 PATCH_SERVERZPATCH_SERVER_IPV6ZREGISTRATION_URLZREGISTRATION_URL_IPV6r:r:,/usr/libexec/kcare/python/kcarectl/config.pys|  __pycache__/http_utils.cpython-36.pyc000064400000012257152533440750013620 0ustar003 ;jE@sddlZddlZddlZddlZddlmZddlmZmZmZm Z m Z ddl m Z m Z mZmZmZmZddZdd d Zd d Zd dZdddZddZddZeZe jeddddZdS)N)SSLError)config constantserrors log_utilsutils) HTTPErrorRequestURLErrorhttplib std_urlopenurlparsec Ost|dr|j}n |}t|}|jdi}|jtjtjdx |jD]\}}|j ||qJWt j dj ||ydd|krt j|d<t j rttddrtj}d|_tj|_||d<t|f||St|f||Stk r&}z0|jd krtj|j|j|j|j|jWYdd}~Xn~tk r}z`|j r|t|j d d r||j d j!t!j"kr|tj|d t#|ddd j |||_$||_WYdd}~XnXdS) N get_full_urlheaders)z KC-VersionzKC-Patch-Versionz#Requesting url: `{0}`. Headers: {1}timeoutHAS_SNIFcontextirerrnozRequest for `{0}` failed: {1})%hasattrrr popupdaterVERSIONKC_PATCH_VERSIONitems add_headerrlogdebugformatr HTTP_TIMEOUTCHECK_SSL_CERTSgetattrsslcreate_default_contextcheck_hostname CERT_NONE verify_moder r coderNotFoundurlmsghdrsfpr argsrENOENTstrreason) r(r,kwargsZ request_urlrheadervaluectxexr50/usr/libexec/kcare/python/kcarectl/http_utils.py urlopen_bases>      .r7Tcsfdd}|S)Ncsbt|tro|jdkSt|ttjttjfr2dSt |dr^t |j dkr^|j dt j kr^dSdS)NiTr,r) isinstancer r&r r HTTPExceptionrsocketrrlenr,rZ ECONNRESET)estate) retry_on_500r5r6check_functionCs  (z3check_urlopen_retry_factory..check_functionr5)r?r@r5)r?r6check_urlopen_retry_factoryBs rAcCst|dr|j}|jdS)Nrzfile:)rr startswith)r(r5r5r6 is_local_urlOs rCcOsV|jdd}|jdtj}t|r2t|f||Stjt|d|dt|f||S)Nr?T retry_count)r?)count)rr RETRY_COUNTrCr7rretryrA)r(r,r0r?rDr5r5r6urlopenUs  rHcCsHt||d}tj r*|r*|jddj|tj rD|rD|jtj||S)N)method Authorizationz Basic {0})r rUPDATE_FROM_LOCALrrrAUTH_TOKEN_HEADER)r( auth_string auth_tokenrIrequestr5r5r6 http_request]s    rPcCs<|dkrtjdptjdS|dkr8tjdp6tjdSdS)NhttpZ http_proxyZ HTTP_PROXYhttpsZ https_proxyZ HTTPS_PROXY)osgetenv)schemer5r5r6get_proxy_from_envhsrVcCsttdpttdS)NrQrR)boolrVr5r5r5r6 proxy_is_usedosrX)Z check_retrycCstjj|}|stdj|ttj|}|j}|j }|jp@d}|dkrRtd|dkrl|j dkrhdnd}|j dkr~t j }n|j dkrt j }ntd |||tjd } i} |rd j|| d <d | d<t|| d<tj| d<z| jd|x | jD]\} } | j| | qW| jt|d} | j| WdQRX| j}|jdkry |j}Wntk rnd}YnXdj|j}|dk r|dj|7}tj|t||j|ddWd| j XdS)aUpload a file to the given URL using HTTP PUT with chunked streaming. Note: The standard library urllib doesn't support PUT with data We need to use httplib directly for this This function uses streaming upload to support large files up to 1GB without loading the entire file into memory. :param file_path: Path to the file to upload :param upload_url: Full URL to upload the file to. Query params are ignored. :param auth_string: Optional authentication string for Basic Auth :return: None if upload succeeded :raises HTTPError: If upload fails with HTTP status >= 400 :raises ValueError: If URL is invalid z"Refusing to upload empty file: {0}/NzInvalid URL: missing hostnamerRiPrQzInvalid URL: unsupported scheme)rz Basic {0}rJzapplication/octet-streamz Content-TypezContent-Lengthz KC-VersionPUTrbizFailed to upload file: HTTP {0}z - {0})!rSpathgetsize ValueErrorrrrnstrhostnameportrUr HTTPConnectionHTTPSConnectionrHTTP_UPLOAD_TIMEOUTr.rr putrequestr putheader endheadersopensend getresponsestatusread Exceptionrlogerrorr close)Z file_path upload_urlrMZ file_sizeparsedhostrbZurl_pathZconn_clsconnrr1r2fresponseZ error_body error_msgr5r5r6 upload_filevsT              rx)T)NN)N)rrSr;r!rrrrrrpy23r r r r r rr7rArCrHrPrVrXcheck_urlopen_retryrGrxr5r5r5r6s   0   __pycache__/update_utils.cpython-36.pyc000064400000006035152533440750014120 0ustar003 ;j@sddlZddlZddlZddlZddlmZmZmZmZddl m Z dZ ddd Z dd d Z d d ZddZddZddZddZddZdS)N)config constants log_utilsutils)json_loads_nstr< .kcarestatuscCs$tjjtj|}tj|tjdS)N)ospathjoinr PATCH_CACHEr atomic_write timestamp_str)filenamestatus_filepathr2/usr/libexec/kcare/python/kcarectl/update_utils.pytouch_status_gap_filesrcCsztjjtj|}tjj|rvt|dJ}y.t|j}t|t j t t j krTdSWnt k rjYnXWdQRXdS)NrFT)r r r rrisfileopenintreadrSTATUS_CHANGE_GAPSTATUS_CHANGE_GAP_DELAYtime Exception)rrsfile timestamprrrstatus_gap_passeds   r!cCs|dkrtdj|dS)Nkernellibcarez$Unknown update status component: {0})r"r#) ValueErrorformat) componentrrr_check_component%sr'c CsNtjtj}|dkriSy t|}|Sttfk rHtjj diSXdS)Nz"Failed to parse update status file) r read_filerUPDATE_STATUS_PATHrr$ TypeErrorrkcarelogwarning)contentresultrrr_load_update_status+s  r/c Csft|y4t}|ttjd||<tjtjtj |Wn$t k r`t j j dddYnXdS)N)errorr zFailed to save update statusT)exc_info)r'r/rrrrrr)jsondumpsrrr+r,)r&r0datarrrsave_update_status9sr5cCs>t|}tt|dd}|r:||kr:|jr6dj|S|S|S)NstatuszHTTP Error: {0})strgetattrisdigitr%)errmessager6rrr _error_statusGs  r=cstfdd}|S)Ncstjfdd}|S)NcsTy||}Wn4tk rB}ztt|dWYdd}~XnXtdd|S)N)r0r7)rr5r=)argskwargsr.r;)r&fnrrinner_s z5track_update_status..decorator..inner) functoolswraps)r@rA)r&)r@r decorator^s z&track_update_status..decorator)r')r&rDr)r&rtrack_update_statusZs rEc Cs^t|y,t}|j|ijdd}t|dtjStk rXtjj ddddSXdS)Nr0r7zFailed to read update statusT)r1) r'r/getr8rUPDATE_ERROR_MAX_LENGTHrrr+r,)r&r4r0rrrread_update_errorosrHi,)r )r )rBr2r rr7rrrrpy23rrrr!r'r/r5r=rErHrrrrs   __pycache__/config_handlers.cpython-36.pyc000064400000017652152533440750014552 0ustar003 ;j&5@sddlZddlZddlZddlmZmZmZmZmZddl m Z dZ dddd d d d d gZ e ZddZddZejdZddZdddedddeeeedeeeeeeeeeddeeeeeeeeddejddddejdejddddeeeeejddejeeeddeed4Zdd Zd!d"ZGd#d$d$eZd%d&Zd'd(Zd)d*Zd+d,Z ej!ej"d-d.d/Z#ej!ej"d-d0d1Z$dS)2N)config constants http_utils log_utilsutils) ConfigParserz/etc/sysconfig/kcare/kcare.confUSE_CONTENT_FILE_V3FORCE_JSON_SIG_V3ENABLE_CRASHREPORTER KCORE_OUTPUT KMSG_OUTPUTSEND_PERF_METRICSKERNEL_ANOMALY_REPORT_ENABLEFORCE_DOCTOR_FALLBACKcCs |jdkS)N1TRUEYESY)rrrr)upper)valuer5/usr/libexec/kcare/python/kcarectl/config_handlers.pybool_converter#srcCsdd|jdDS)NcSs g|]}|jr|jjqSr)striplower).0itemrrr (szcomma_list..,)split)rrrr comma_list'sr!z^[a-z][a-z0-9_.+\-]*$cCsRgg}}x(t|D]}tj|r&|n|j|qW|rNtjdjdj||S)Nz3LIBCARE_LIBS: ignoring invalid library name(s): {0}z, )r!_LIBCARE_LIB_NAME_REmatchappendrlogwarnformatjoin)rZvalidinvalidtokenrrrlibcare_libs_list0s  r*cCs|jS)N)r)vrrr@sr,cCs|jS)N)r)r+rrrr,DscCs |jdS)N/)rstrip)r+rrrr,SscCs|pdS)Nr)r+rrrr,\scCs |jdS)Nr-)r.)r+rrrr,^scCs |jdS)Nr-)r.)r+rrrr,_scCs |jdS)Nr-)r.)r+rrrr,cscCs |jdS)Nr-)r.)r+rrrr,ds)4AFTER_UPDATE_COMMANDAUTO_STICKY_PATCHSET AUTO_UPDATEAUTO_UPDATE_DELAYBEFORE_UPDATE_COMMANDCHECK_SSL_CERTSDOCTOR_REPORT_MAX_SIZE_BYTESr r FORCE_GID FORCE_IPV4 FORCE_IPV6r HTTP_TIMEOUTHTTP_UPLOAD_TIMEOUTIGNORE_FEATURE_FLAGSIGNORE_UNKNOWN_KERNELr KCORE_OUTPUT_SIZE KDUMPS_DIRr$KERNEL_ANOMALY_REPORT_MAX_SIZE_BYTESr LIBCARE_DISABLED LIBCARE_LIBS!LIBCARE_PIDLOGS_MAX_TOTAL_SIZE_MBLIBCARE_SOCKET_TIMEOUTLIB_AUTO_UPDATE PATCH_LEVEL PATCH_METHOD PATCH_SERVERPATCH_SERVER_IPV6 PATCH_TYPEPREFIXPREV_PATCH_TYPEREGISTRATION_URLREGISTRATION_URL_IPV6 PRINT_LEVEL REPORT_FQDNSILENCE_ERRORSSTATUS_CHANGE_GAP STICKY_PATCHSTICKY_PATCHSET UPDATE_DELAY UPDATE_POLICYUPDATE_SYSCTL_CONFIGUSERSPACE_PATCHESr KERNEL_VERSION_FILEKCARE_UNAME_FILESUCCESS_TIMEOUTrc Kstt}|j}|jx|jD]\}}d}|d}|d}x^tt|D]N}||j|sl||j|rL|dkr|||=n|dt|d||<d}PqLW|s"|j |dt|dq"Wt j tdj |dS)NF= z =  T) openCONFIG readlinescloseitemsrangelen startswithstrr$r atomic_writer') kwargsZcflinesZproprupdatedZprop_eqZprop_spirrr update_configws"rmc Csi}tjd}xD|D]<}|j|}|r<|j\}}|sHd}n td||||<qWt|tt}|r~tddjt|x`|j D]T\}}t|} |dks| dkrqy | |Wqt k rtd||fYqXqWt f|dS)Nz^([^=]+)=([^=]*)$z8Invalid parameter format: %s. Format should be KEY=VALUEzUnknown parameter: %sz, zBad value for %s: %s) recompiler#groups SystemExitsetPOSSIBLE_CONFIG_OPTIONSr'sortedrc Exceptionrm) paramsZparams_for_updatepatternparamr#keyrZunknown_paramsvar_nameconvertrrrupdate_config_from_argss,       r|c@s$eZdZddZddZddZdS) FakeSecHeadcCs||_d|_dS)Nz [asection] )fpsechead)selfr~rrr__init__szFakeSecHead.__init__c Cs&|jrz|jSd|_Xn |jjSdS)N)rr~readline)rrrrrs  zFakeSecHead.readlineccs.|jr|jVd|_x|jD] }|VqWdS)N)rr~)rlinerrr__iter__s  zFakeSecHead.__iter__N)__name__ __module__ __qualname__rrrrrrrr}s r}c sitddddy,ttt}tjr4j|n j|Wntk rTiSXd fdd }x4dD],\}}t j |slj d |}|rl|t j |<qlWx tjD]\}}|||d qWS)Nr^) HTTP_PROXY HTTPS_PROXY)defaultsc sJyjd|}Wntk r(|}YnX|dk rF|r>||}||<dS)Nasection)getru)namedefaultr{r)cpresultrrread_vars z%get_config_settings..read_varhttprhttpsrr)r{)NNrrrr)rr)rr}r_r`rPY2readfp read_filerurget_proxy_from_envrosenvironrsrc)rrschemevariableproxyrzr{r)rrrget_config_settingss$     rcCs(tjt}tjj|tj|dS)N)_CONFIG_OPTIONSclearrr__dict__update)Zsettingsrrrset_settings_from_config_files rc Csi}xv|jD]j\}}|j}|jds*q|jddjdd}ytt|||<Wqtk rvtjj d||YqXqW|S)ak Checking headers for feature flags which start with 'KC-Flag-' and reformat it to dictionary with keys in upper case and without 'KC-Flag-' prefix and dashes replaced with underscores. For unification all header keys are checked in upper case. For example: 'KC-Flag-Some-Value' -> 'SOME_VALUE' :return: dict {'SOME_VALUE': bool, ...} zKC-FLAG-r^-_z(Invalid feature flag header value %s: %s) rcrrfreplaceboolint ValueErrorrkcarelogerror)headersflagsZhdr_nameZ hdr_valueZ upper_name param_namerrr convert_headers_to_feature_flagss  rcCst|tjstdS)N)save_feature_flags_cacherr;set_feature_flags_from_cache)rrrrset_feature_flags_from_headerssr)loggercCs"t|}tjtjtj|ddS)N)content)rrrhrFEATURE_FLAGS_CACHEjsondumps)r feature_flagsrrrr src CsztjjtjsdSttj}tj|}WdQRXxB|jD]6\}}|t krNq<|t krXq<|t j |<t jjd||qs  % __pycache__/delivery_kit.cpython-36.pyc000064400000016616152533440750014116 0ustar003 ;j+@sddlZddlZddlZddlZddlZddlZddlZddlmZddl m Z m Z m Z m Z mZmZddlmZddZGdddeZGd d d eZdS) N)NamedTemporaryFile)authconfig http_utils ipv6_support log_utilsutils) run_commandcCs<t|}x(dD] }|dkr&dj||S|d}qWdj|S) zBRender a byte count as a short human-readable string for the logs.BKiBMiBGiBg@z {0:.1f} {1}z {0:.1f} TiB)r r r r )floatformat)Z num_bytessizeZunitr2/usr/libexec/kcare/python/kcarectl/delivery_kit.py format_sizes    rc@seZdZdZdZdZddZeddZeddZ d d Z d(d dZ ddZ ddZ ddZddZddZddZddZddZdd Zejejd!d"d#Zd$d%Zd&d'Zd S)) DataPackagezGeneric archive package for the patch server uploads pipeline. Based on DataPackage from eportal (delivery_kit.py). Subclasses supply the manifest `data_type`, the `upload_uri` to PUT the archive to and the `max_size` payload limit. cCsd|_g|_d|_dS)Nr)_tar_errors_buffer_total_payload_size)selfrrr__init__-szDataPackage.__init__cCstdS)N)NotImplementedError)rrrrmax_size3szDataPackage.max_sizecCs|j}t|jS)N)_ensure_tar_createdstrname)rtarrrr archive_path8szDataPackage.archive_pathcCsd}d}yttj|ddd\}}}Wn*tk rP}zt|}WYdd}~XnX|rh|jdj|||dk r|j|tj |ddddS)NT) catch_stdout catch_stderrz!failed to dump stdout of {0}: {1}zutf-8)encoding) data_bytes) r shlexsplit Exceptionr log_errorradd_filerbstr)rarcnamecmdstdoutstderr_errr add_stdout>szDataPackage.add_stdoutNFc Csh|dkr|dkrtd|j}d}|dk rZtjj|sL|jdj|dStjj|}nt|}|j |s|jdj|dS| r|j | r|j dj|t |t |j t |jdSyp|r|j||dn"tj|}||_|j|tj||s*|j|7_tjdj|t |t |jdd Wn6tk rb}z|jd j||WYdd}~XnXdS) Nz"No src_path or data_bytes providedrzfile not found: {0}z no available space to store: {0}zPskipping {0}: {1} would exceed the {2} report size limit (already collected {3}))r,z%collected {0}: {1} (report total {2})F) print_msgzfailed to store {0}: {1}) ValueErrorrospathexistsr)rgetsizelen_check_required_space_check_total_payload_limit log_warningrrraddtarfileTarInforaddfileioBytesIOrloginfor() rr,src_pathr%skip_limit_checkr entry_sizeinfor1rrrr*NsJ     zDataPackage.add_filecCsbytjtj|dddd}Wn2tk rN}z|jdj||dSd}~XnX|j||ddS)N)indentzutf-8)r$zfailed to dump {0}: {1})r%)rr+jsondumps TypeErrorr)rr*)rr,datar%r1rrradd_jsons zDataPackage.add_jsoncCstj|j}|j|j|kS)N)r5statvfsr!f_frsizef_bfree)rrFrOrrrr:s z!DataPackage._check_required_spacecCs|j|j|kS)N)rr)rrFrrrr;sz&DataPackage._check_total_payload_limitcCsd|jttjdS)N)Zschema_versiontypeZ time_created) data_typeinttime)rrrr make_manifestszDataPackage.make_manifestcCsNtjtj|jdddd}|j}tjd}t||_ |j |t j |dS)NrH)rIzutf-8)r$z manifest.json) rr+rJrKrWrr>r?r9rr@rArB)rr%r rGrrr _add_manifests   zDataPackage._add_manifestcCs&|j}tj|dd|jj|dS)NF)r3)striprlogerrorrappend)rZ error_msgrrrr)szDataPackage.log_errorcCs&|j}tj|dd|jj|dS)NF)r3)rYrlogwarnrr[)rZ warning_msgrrrr<szDataPackage.log_warningcCsxd D]}tdj|dddd}|jy6tjdj|jdd tj|j|d d |_|j |St k r}zb|jdk ry|jjWnt k rYnXd|_t j j |jrt j|jt|tjsЂWYdd}~XqXqWtjd dS)Nw:xzw:bz2w:gzz.tar.{0}F)suffixdeletezCreating DataPackage: {0})r3T)rmodeZ dereferencez%No supported compression method found)r]r^r_)rrcloserrCrr>openrrXr(r5r6r7unlink isinstanceZCompressionError)rZcompression_modeZtmpfileerrrrr __enter__s(    zDataPackage.__enter__cCsP|jr,dj|jd}|jdtj|dd|jrL|jj|rL|jdSdS)N z errors.logT)r%rEF)rjoinr*rr+rrdremove_archive)rexc_typeZexc_valexc_tberrorsrrr__exit__s zDataPackage.__exit__)loggercCs$|jr tjj|jr tj|jdS)N)rr5r6r7r!rf)rrrrrlszDataPackage.remove_archivecCs|jstd|jS)Nz/DataPackage should be used as a context manager)r RuntimeError)rrrrrszDataPackage._ensure_tar_createdcCst|jjtjj|j}d|kr4||jddnd}ttj |}t j |j |}t j|j|tjd|S)aSend the package archive to the patch server. Upload errors propagate to the caller (see the eportal precedent): wrap with utils.catch_errors where a silent failure is acceptable. :return: Upload name (package identifier) .Nr) upload_urlZ auth_string)rrdr5r6basenamer!findruuidZuuid4rget_patch_server upload_urirZ upload_filerget_http_auth_string)rruext upload_namertrrrsends  zDataPackage.send)NNF)__name__ __module__ __qualname____doc__rTryrpropertyrr!r2r*rNr:r;rWrXr)r<rirpr catch_errorsrr\rlrr}rrrrr"s(   ;    rc@s eZdZdZdZeddZdS)KernelAnomalyPackagezkernel-anomalyz/upload/kernel-anomaly/cCstjS)N)rZ$KERNEL_ANOMALY_REPORT_MAX_SIZE_BYTES)rrrrrszKernelAnomalyPackage.max_sizeN)r~rrrTryrrrrrrrsr)rArJr5r&r>rVrwtempfilerkcarectlrrrrrrZkcarectl.process_utilsr robjectrrrrrrs    y__pycache__/platform_utils.cpython-36.pyc000064400000024247152533440750014467 0ustar003 ;j(@sddlZddlZddlZddlZddlZddlZddlZddlmZm Z m Z m Z m Z m Z dZdZddZe jdd Zd d ZdQd dZdZdddZdRddZddZddZddZe jddZe jddZe jd d!Zd"d#Zd$d%Zd&d'Z d(d)Z!d*d+Z"d,d-Z#d.d/Z$d0d1Z%d2efd3efd4e fd5e!fd6e"fd7e#fd8e$ffZ&d9d:Z'd;d<Z(d=d>Z)e j*e j+d?d@dAZ,e j*e j+d?dBdCZ-e je j*e j+d?dDdEZ.e j*e j+ddFdGdHZ/e j*e j+ddFdIdJZ0e j*e j+d?dKdLZ1e j*e j+dSdFdMdNZ2dOdPZ3dS)TN)config constants log_utils process_utilsselinuxutilsz/usr/libexec/kcare/virt-whatz/proccCs2tjdddkrtjSddl}|jddSdS)NrF)full_distribution_name)r r )sys version_infoplatformlinux_distributiondistro)rr4/usr/libexec/kcare/python/kcarectl/platform_utils.py get_distrosrcCs tjdS)Nr )runamerrrrget_system_unamesrcCsdtjdtjdfS)Nz%s.%srr)r rrrrrget_python_version$srFc Csttjd}tjr^tjddddttj gdd\}}}|sN|j d}nd |}||d <|rlt j |St |}d }x |D]}|d |||f7}q~W|jS) N)python_version agent_versionZpsz-Zz --no-headersz--pidT) catch_stdoutrz error: %sselinux_contextz%s: %s )rrVERSIONrZis_selinux_enabledr run_commandstrosgetpidsplitjsondumpssortedrstrip) is_jsoninforcstdoutstderrrZ info_keysZinfo_strZinfo_keyrrrapp_info(s &  r,z/sys/firmware/efi/efivarsz$8be4df61-93ca-11d2-aa0d-00e098032b8cz$605dab50-e046-4300-abb6-3dd810dd8b23)globalshimc CsFtjjtd||f}tjj|s&dSt|d }|j|SQRXdS)Nz%s-%srb)r pathjoin EFIVARS_PATHexistsopenread)namevendorZ max_bytesZvar_pathvarrrr_read_uefi_varIs   r:c Cs@y&tdtd}|r$|dddkSWntk r:YnXdS)N SecureBootr-rF)r: EFI_VENDORS Exception)Zsecure_boot_varrrris_secure_bootRsr@cCsZyt||}|dkrdSWn.tk rH}zt|j}WYdd}~XnXtjtj|S)N)r:r?rencodernstrbase64urlsafe_b64encode)r7r8Z value_byteserrr_get_uefi_var_encoded]s rFcCstjtjjtd}|r0t|dkr0|dd}|tjjtjjt d}|dsV|Syt dddD|d <t d d tj t D}d |i|d <t ddddg}x<|D]4}|dks|jdr||krt|td |d |<qWWn2tk r}ztjt|WYdd}~XnX|S)Ncmdlinei)rGhas_efirHcss |]}|t|tdfVqdS)r-N)rFr>).0r9rrr rsz#secure_boot_info..r; SetupModer-cSs4g|],}|jtdr|dttd dqS)r.rr)endswithr>len)rIr9rrr tsz$secure_boot_info..varsr.Z MokListRTZ MokListXRTZMokListTrustedRTZ SbatLevelRT HSIStatus MokIgnoreDBZRT)r;rK)rPrQ)r try_to_readr r1r2PROC_DIRrMr4dirnamer3dictr%listdirsetrLrFr>r?rlogwarnr)rGr(Z shim_varsZshim_exclude_varsr9errrrrsecure_boot_infohs$    rZcCsrtjrfy&tjtjddddtjdd}Wqntjk rb}ztj|t j }WYdd}~XqnXnt j }|S)Nrr ) r REPORT_FQDNsocket getaddrinfo gethostname AI_CANONNAMEgaierrorrlogerrorrnode)hostnamegerrr get_hostnames& recCsTtjjtd}tjj|rPt|d}|j}ttt |j d}|j |SdS)Nuptimerrz-1) r r1r2rSisfiler5readlinerintfloatr"close)Z uptime_fileflineresultrrr get_uptimes  rpcCs tjjtrtjtgjSdS)Nz no-virt-what)r r1rhVIRTWHATr check_outputstriprrrrget_virts rtcCs tjjdS)Nz/usr/local/cpanel/cpanel)r r1rhrrrr is_cpanelsrucCs tjjdS)Nz/usr/local/psa/admin/)r r1isdirrrrris_plesksrwcCs tjjdS)Nz/usr/local/interworx/)r r1rvrrrr is_interworxsrxcCs tjjdS)Nz/usr/local/ispmgr/)r r1rvrrrr is_ispmanagersrycCs tjjdS)Nz/usr/local/directadmin/plugins/)r r1rvrrrris_directadminsrzcCs tjjdS)Nz/usr/local/hostingcontroller/)r r1rvrrrris_hosting_controllersr{cCs tjjdS)Nz/hsphere/shared)r r1rvrrrr is_hspheresr|cCstdddDS)z:Softaculous markers checked by the kcdoctor.sh detect_cp()css|]}tjj|VqdS)N)r r1r4)rIr1rrrrJsz"has_softaculous../usr/local/softaculous*/usr/local/cpanel/whostmgr/cgi/softaculous*/usr/local/directadmin/plugins/softaculous)r}r~r)anyrrrrhas_softaculouss rZPleskZcPanelZ InterWorxZ ISPmanagerZ DirectAdminzHosting ControllerzH-SpherecCs2tjjtjjtddo0tjjtjjtdd S)NZvzZveinfoversion)r r1r4r2rSrrrrinside_vz_containersrcCsdttjjtddjkS)Nz/lxc/1Zcgroup)r5r r1r2rSr6rrrrinside_lxc_containersrcCs tjjdS)Nz /.dockerenv)r r1rhrrrrinside_docker_containersr)loggercCsFtjtjjtd}|sdS|jdd\}}}}t|t|t|fS)Nloadavg r )rrRr r1r2rSr"rk)rm1Zm5Zm15_rrrget_load_averages rcsDtjtjjtd}|sdSd tfddd d|jDDS) z!Returns dict of memory info in kBmeminfoNMemTotalMemFree SwapTotalSwapFreec3s&|]\}}|kr|t|fVqdS)N)rj)rIkv) filter_paramsrrrJszget_mem_info..css"|]}tjd|ddVqdS)z[\s:]+Nr )rer")rIrnrrrrJs)rrrr)rrRr r1r2rSrU splitlines)rr)rr get_mem_infos rc Cstjtjjtd}|sdSdd|jdD}t|ttdd|D|dj dt |dj d d|dj d t |dj d dt |dj d d|dj d |dj ddjd S)NcpuinfocSs&g|]}|rtdd|jDqS)css|]}tjd|VqdS)z\s*:\s*N)rr")rIrnrrrrJ sz*get_cpu_info...)rUr)rIZ cpu_linesrrrrN sz get_cpu_info..z css"|]}|jd|jdfVqdS)z physical idzcore idN)get)rIZcpurrrrJszget_cpu_info..r vendor_idmodelz model namez cpu familystepping microcodeflagsr) Z logical_coresZphysical_coresrrZ model_nameZ cpu_familyrrr) rrRr r1r2rSr"rMrWrrj)rZcpusrrr get_cpu_infos   r)rdefault_returncCstddtjtDS)NcSsg|]}|jr|qSr)isdigit)rIdrrrrN sz%get_process_count..)rMr rVrSrrrrget_process_countsrcCs(tjtjjtd}t|pdjdS)Nzsys/fs/file-nr0r)rrRr r1r2rSrjr")Zfd_inforrrget_opened_files_count#srcCs$xtjdD]\}}}t|SWdS)Nz/sys/kernel/debug/kvm)r walkrM)Z_rootdirs_filesrrrget_vm_count_kvm*s rcCs(dd}|d|d|d|dfS)z8Return tuple of total numbers of TCP and UDP connectionscSs4tjtjjtd|}|sdStt|jddS)Nnetrr) rrRr r1r2rSmaxrMr)protorecordsrrrconn_records_count7sz9get_network_connections_count..conn_records_countZtcpZtcp6ZudpZudp6r)rrrrget_network_connections_count2src Cs,t\}}tttttt||dS)N)Z load_averageZmem_infoZcpu_infoZvm_countZ processesZ open_filesZtcp_connectionsZudp_connections)rrrrrrr)Zconn_tcpZconn_udprrrget_performance_metricsAs r)F)r/)rr)4rCr#r rrr\r rrrrrrrrqrSrcachedrrr,r3r>r:r@rFrZrerprtrurwrxryrzr{r|rZCONTROL_PANEL_PROBESrrr catch_errorsrXrrrrrrrrrrrrsd          __pycache__/auth.cpython-36.pyc000064400000021020152533440750012346 0ustar003 ;j/@sddlZddlZddlZddlZddlmZmZmZmZm Z m Z m Z m Z m Z ddlmZmZmZd"ddZdd Zd d Zd d Zd#ddZe jddZddZddZddZddZddZddZd$ddZd d!Z dS)%N) config constantserrors http_utils ipv6_support log_utilsplatform_utilsserveridutils) HTTPErrorURLError urlencodeFcCsd}ytj}|dkr(|s$tjddStjdj|}tj|}t j |j }t j |}|ddkrtj tj|stjdn |stj|tjd|dWn2tk r}z|stj||WYdd}~XnXdS)Nz1Error unregistering server: cannot find server idz&/unregister_server.plain?server_id={0}successtruezServer was unregisteredzError unregistering server: message)r get_serveridrlogerrorrget_registration_urlformatrurlopenr nstrread data_as_dictZ rm_serverid clear_cacheloginfor print_cln_http_error)Zsilenturl server_idresponsecontentreser#*/usr/libexec/kcare/python/kcarectl/auth.py unregisters,      r%cCstjdtj}|dkrdStjtj}ddl}|dkrH|jd|jjt dd}t dd}tj |j |j j tj |j |jj tj |j |j j xLtjd t|\}}}|dkr|rtj|t|tj|jdqWdS) NzHRegister auto-retry has been enabled, the system can be registered laterrz /dev/nullrza+<ii )r print_wrapperosforksetsidsysexitstdoutflushopendup2filenostdinstderrtimesleep _try_registerr set_server_id_set_auth_tokenrr)rpidr-sisocoder auth_tokenr#r#r$_register_retry)s.        r@cCs&|dk r"tjd| r"td||S)Nz ^[\w.-]+$zInvalid value received: %s)rematch ValueError)valuer#r#r$_validate_urlsafe_encodingGs rEcCsyNtj|}|jjtjd}tjtj|j }t |dt |jdt |fSt t fk r~}ztj||dSd}~Xn$tk rtjjd|dSXdS)Nr>rz)Exception while trying to register URL %s)NNN)NNN)rrheadersgetrZAUTH_TOKEN_HEADERr rrrintrEr r rr Exceptionkcarelog exception)rrr?r!r"r#r#r$r8Ms   r8c Cs>y tdWn tk r,tjjdYnXtj}td|fd|fg}djt j |}t |\}}}|dkrt j |t|t jtjddS|dkrtjd nv|d krtjd nb|d krtjd nN|dkrtjdn:|dkrtjdn&|dkrtjdntjdj||r4t|dS|prr?r#r#r$register[s@         rUcCs tjtjS)N)r try_to_readrAUTH_TOKEN_DUMP_PATHr#r#r#r$_get_auth_tokensrXcCs|sdStjtj|dS)N)r atomic_writerrW)r?r#r#r$r:sr:cOsv|jdd}|jddrt}ntj}tj|r@tj|f||Stj|tt|d}t j |ddtj|f||S)Nmethod check_licenseT)rZ)count) pop_check_auth_retryrcheck_urlopen_retryZ is_local_url urlopen_base http_requestget_http_auth_stringrXr rS)rargskwargsrZcheckrequestr#r#r$ urlopen_auths   rhcCs.tj}|r*tjtjtjdj|dSdS)Nz{0}:{1}Z kernelcare)r rr rbase64 b64encodebstrr)rr#r#r$rcsrccCs(t|tr|jdkrt|Stj||S)N)rlrm) isinstancer r>_handle_forbiddenrr`)r"stater#r#r$r_sr_cCsd|kr dStjrtj}tjd}|r8|dj|7}y&tjt j |ddj }tj |}Wn0t k r}ztj||dddSd}~XnX| s|jd  rtjjd j|dS|d dkrd|d<tjd dStdS)aIn case of 403 error we should check what's 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. licenseTz /check.plainz?server_id={0}F)Z retry_on_500)r/Nr>zUnexpected CLN response: {0}01z$Unable to access server. Retrying...)rrrs)rCHECK_CLN_LICENSE_STATUSr rrrrr rrrrrr rrrGrJerrorr_register_trial)rprrr infoexr#r#r$ros*   rocCstj}|rtjdj|}ytj|}tj|j }tj |}| sT|j d rhtj dj|dSt |d}|dkrtj ddStdd}|dkrtj d|SWqtk r}ztj||dSd}~XqXntSdS) Nz/check.plain?server_id={0}r>zUnexpected CLN response: {0}rrzKey-based valid license found) key_checkedz No valid key-based license found)r rrrrrrr rrrrGr)rH_get_license_info_by_ipr rr)rrrr r!r>Z license_typer"r#r#r$ license_infos,       r{c Cstjd}y6tj|}tj|j}tj|}|djdkr&t |d}|dkrptj dj |ddS|dkr|d}tj |d j d }tj d j ||d S|d kr|dkr|d}tj |d j d }tj d j |||dko|dkrBd|krtj dj |dn tj dn|jdd}tj dj |Wnhtk rt} ztj| |WYdd} ~ Xn:tk r} ztj dj | |jWYdd} ~ XnXdS)Nz /check.plainrrr>rzValid license found for IP {0}iprZ expire_datez%Y-%m-%dz?You have a trial license for the IP {0} that will expire on {1}r(z0Your trial license for the IP {0} expired on {1}rNzThe IP {0} hasn't been licensedz This server hasn't been licensedrz"Error retrieving license info: {0}z1Unexpected CLN response, cannot find {0} key: {1})rrrrr rrrlowerrHr)rZparse_response_datestrftimerGr rrKeyErrorstrip) ryrrr r!r>r|Z expires_strrr"rMr#r#r$rzs<       (rzcCsFtjjtjd}tjj|r dSytjtj d}t j t j |j }y|djdkrt j|dddtj|ddkrtj|d |d tjd j|d dS|dd krt j|dddtjd n tjdWn.tk r }ztj|WYdd}~XnXWn0tk r@}ztj|jWYdd}~XnXdS)Nztrial-requestedz /trial.plainrrr}T) ensure_dirZexpiredr|createdz3Requesting trial license for IP {0}. Please wait...ZnazInvalid LicenserrR)r*pathjoinr PATCH_CACHEexistsrrrrr rrrr~rYrrZAlreadyTrialedExceptionrrr KcareErrorZUnableToGetLicenseExceptionrr r>)Z trial_markrr!Zker"r#r#r$rvs,     rv)F)F)r)!rir*rAr6r}rrrrrrr r r py23r r rr%r@rEr8rUcachedrXr:rhrcr_ror{rzrvr#r#r#r$s&,  . ) #__pycache__/fetch.cpython-36.pyc000064400000010555152533440750012511 0ustar003 ;j@sddlZddlZddlZddlZddlmZmZmZmZm Z m Z m Z ej ej gZdZdZdZddd Zd d Zd d Ze jejejddddddZGdddeZe jddZddZdS)N)authconfig constantserrors http_utilsselinuxutilsz /usr/bin/gpgz/var/lib/kcare/gpgzrelease.content.jsonFc Cstj}|rtj}tjr&tddd}nt}xT|D]L}y|||}PWq0tjk rz}z||dkrj|WYdd}~Xq0Xq0W||}t j |||S)Nrr ) rurlopenr urlopen_authrFORCE_JSON_SIG_V3SIG_VERIFY_ORDERrNotFoundr save_to_file) urldstdo_authZ urlopen_localZsig_extsZsig_ext signatureZnfZsig_dstr+/usr/libexec/kcare/python/kcarectl/fetch.pyfetch_signatures     rcCs$tjjts tjdjtdddS)Nz$No {0} present. Please install gnupgzgnupg not found)status)ospathisfileGPG_BINr KcareErrorformatrrrr check_gpg_bin)s rcCst|jtjrptjjtd}ytj |||Wqtj k rl}zt j dj |t|WYdd}~XqXnxt|d}|j}WdQRXtjjtd}ytj|||Wn8tk r}zt j dj |t|WYdd}~XnXdS)a8 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 with the signature :return: True in case of valid signature :raises: BadSignatureException zroot-keys.jsonzBad Signature: {0}: {1}Nrbz kcare_pub.key)rendswithrSIG_JSONrrjoin GPG_KEY_DIR kcsig_verifyZverifyErrorrBadSignatureExceptionrstropenreadZrun_gpg_verify Exception) file_pathrZ root_keysefZsigdataZkeyringrrrcheck_gpg_signature.s  * r/)countdelaycCs^tj|}tj|}tj|||r2|j||n|rNt||dd}t||t j |||S)NT)r) rr rselinux_safe_tmpnamer rcheckrr/rrename)rrZcheck_signature hash_checkerresponsetmprrrr fetch_urlMs     r9c@seZdZddZddZdS) HashCheckercCs6||_tj|jdd|_tjtj|d|_dS)N/files) content_filer get_patch_server_urlrstrip url_prefixjsonloads read_filehashes)selfbaseurlr=rrr__init__^szHashChecker.__init__cCsv|t|jd}||jkr4tjdj||jddtjt j |j }|j|d}||krrtj dj|||dS)Nz3Invalid checksum: {0} not found in content file {1}zinvalid checksum)rsha256z USE_SIGNATURErrr:)levelrrrrget_hash_checkerts  rWcsfdd}|S)z=Enrich request with a cache key, and save it if response had.cshtj}|dk r.d|kr i|d<||dtj<||}|jjtj}|dk rd||krdtjtj||S)Nheaders)r get_cache_keyrCACHE_KEY_HEADERrXget atomic_writeCACHE_KEY_DUMP_PATH)argskwargs cache_keyrespZ new_cache_key)clblrrwrappers z$wrap_with_cache_key..wrapperr)rbrcr)rbrwrap_with_cache_keys rd)F)FN)rJrArr%rrrrrrr SIGr"rrr$rSrrr/retry check_excr'r9objectr:cachedrWrdrrrrs $   __pycache__/__init__.cpython-36.pyc000064400000163036152533440750013162 0ustar003 ;j*: @sddlmZddlZddlZddlZddlZddlZddlZddlZddl Z ddl Z ddl Z ddl Z ddl Z ddlZddlZddlZddlmZddlmZddlmZddlmZmZmZmZmZmZmZmZmZm Z m!Z!m"Z"m#Z#m$Z$m%Z%m&Z&m'Z'm(Z(m)Z)m*Z*m+Z+ddlm,Z,m-Z-m.Z.dd l/m0Z0m1Z1m2Z2m3Z3m4Z4d Z5d Z6dZ7dZ8dZ9dZ:dZ;dZZ?ej=dZ@ejAjBdre jAjCddejDdeEde$jFjGejHddZIddZJdd ZKd!d"ZLd#d$ZMdd%d&ZNd'd(ZOd)d*ZPd+d,ZQd-d.ZRd/d0ZSd1d2ZTGd3d4d4eUZVGd5d6d6e,ZWGd7d8d8e,ZXGd9d:d:e,ZYd;d<ZZed=d>Z[dd?d@Z\dAdBZ]dCdDZ^iZ_dEdFZ`e`e2ja_bece dGdsy8ddldZeddlfZgeejhjiegjjeejhjidHkrekdIWnekk rYn8XdJdKZle2jmZnGdLdMdMeoZpGdNdOdOe2jmZqeqe2_mdPdQZrejsfdRdSZtdTdUZudVdWZvGdXdYdYeoZwdZd[Zxd\d]Zydd_d`ZzdadbZ{dcddZ|ddedfZ}dgdhZ~didjZdkdlZdmdnZdodpZdqdrZdsdtZdudvZdwdxZdydzZd{d|Zd}d~ZddZddZddZddZdddZddZddZddZddZdddZddZddZddZddZe&jdddZddZddZGdddZddZddZddZddZejejsfddZddZe&je*jdejfddZddZddZddZdddZddZddZdS))print_functionN)ArgumentParser)contextmanager)datetime)anomalyauth capabilitiesconfigconfig_handlers constantsdoctorerrorsfetch http_utils ipv6_supportkcarelibcare log_utilsplatform_utils process_utilsselinux server_infoserverid update_utilsutils) KcareErrorNotFoundSafeExceptionWrapper) HTTPErrorURLErrorhttplibjson_loads_nstr urlencodecZv312h24h48htestz./etc/sysconfig/kcare/freezer.modules.blacklistz/usr/libexec/kcare/kcdoctor.sh latest.v3 latest.v2z /etc/sysconfig/kcare/sysctl.conf z$==BLACKLIST== (.*)==END BLACKLIST== z'(kpatch.*|ksplice.*|kpatch_livepatch.*)z/usr/libexec/kcare/pythonignore)categorycCsDt}tjjtr@ttd}x|D]}|j|jq"W|j|S)Nr) setospathisfileFREEZER_BLACKLISTopenaddrstripclose)resultfliner;./usr/libexec/kcare/python/kcarectl/__init__.pyget_freezer_blacklistSs   r=cCsB|jd}|r(dj|d||dg}ndj|d|dg}|S)N.rrr?)splitjoin)ptypefilenameZ name_partsr;r;r< _apply_ptype]s  rDcCsJt|tjt_t|tjt_t|tjt_t|tjt_t|tjt_dS)N)rDr PATCH_BIN PATCH_INFOBLACKLIST_FILE FIXUPS_FILE PATCH_DONE)rBr;r;r< apply_ptypefs rJcCstj\}}}d}t|trbt|t rbyd|jtj|j|jf}Wqt t fk r^YqXnPt|t t t frt|t rd|}n*t|t r|jpt|j}|jpd|j}tj}tjtj|d|dt|dt||djtj|dt|ddd S) Nz[Errno %i] %s: '%s'z%srr__name__dattempts)Z agent_versionZpython_versiondistroZdistro_versionerrordetails tracebackrN)sysexc_info isinstanceOSErrorr errnor0strerrorrCAttributeError TypeErrorKeyErrorIOErrorretypetypeinnerrQr get_distror VERSIONget_python_versiongetattrstrrArRZ format_tb)r]valuetbZdetails_sanitizedrOr;r;r< format_exception_without_detailsns*  rgc Csvtjr dStjt}tjtjtj |}tj dd|}t j |t j}yt j|Wntk rpYnXdS)Nz/api/kcarectl-tracez?trace=)r UPDATE_FROM_LOCALjsondumpsrgrnstrbase64Zurlsafe_b64encodeZbstrget_patch_server_urlrZ http_requestrZget_http_auth_stringZ urlopen_base Exception)ZtraceZ encoded_traceurlZrequestr;r;r<send_excs rpcCstj}|dkr tj|ddStjtj}|dkrBtjdtjdttjd&}tj |j dtj |j dWdQRX|rt j |y |Wn*t k rtjjdtjdYnXtjddS)z Run func in a fork in an own process group (will stay alive after kcarectl process death). :param func: function to execute :return: rNarzWait exception)r0forkwaitpidsetsid_exitr7r4r ZLOG_FILEdup2filenotimesleeprnrkcarelog exception)funcrzpidfdr;r;r< nohup_forks(      rcCstjjtjd}tjj|rtt|dH}y,t|j}|t j t j krRt ||Wnt k rhYnXWdQRXtj|tjdS)aCheck 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. z.kcareprev.lockr.N)r0r1rAr PATCH_CACHEr2r4intreadr SUCCESS_TIMEOUTryPreviousPatchFailedException ValueErrorr atomic_write timestamp_str)Zanchor_filepathZafile timestampr;r;r< touch_anchors   rcCsxytjtjjtjdWntk r.YnXtd|tj j yt ddWn t k rrt jjdYnXdS)z 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. z.kcareprev.lockdone)reasonzCannot send update info!N)r0remover1rAr rrVregister_actionrget_loaded_modulesclearget_latest_patch_levelrnrr{r|) state_datar;r;r< commit_updates  rcCs(tjtjjtjdtj||dddS)NpatchesrK)Z exclude_path) rclean_directoryr0r1rAr rrget_cache_path)khashZplevelr;r;r< clear_cachesrcCs>tjpd}dj||g}tjd|f}|r2||f7}tjj|S)Nnone-modules)r PREFIXrAr rr0r1)rfnameprefixZ module_dirr8r;r;r<get_current_level_paths    rcCstjt|dt|dddS)NlatestT)Z ensure_dir)rrrrd)r patch_levelr;r;r<save_cache_latestsrc CsVt|d}tjj|rRy"tt|djj}tj ||St t fk rPYnXdS)Nrr.) rr0r1r2rr4rstriprLegacyKernelPatchLevelrrZ)rZpath_with_latestplr;r;r<get_cache_latests   rc@s eZdZdS)CertificateErrorN)rL __module__ __qualname__r;r;r;r<r srcs eZdZdZfddZZS)UnknownKernelExceptionzunknown kernelc s6djtjdtjtj}tt|j |f|dS)NzLNew kernel detected ({0} {1} {2}). There are no updates for this kernel yet.r) formatrr`platformreleaserget_kernel_hashsuperr__init__)selfkwargsmsg) __class__r;r<rszUnknownKernelException.__init__)rLrrstatusr __classcell__r;r;)rr<rsrcs(eZdZdZfddZddZZS)ApplyPatchErrorzpatch apply errorcsFtt|j||||_||_||_||_tjd|_ t j |_ dS)Nr) rrrcode freezer_stylelevel patch_filerr`rOrr)rrrrrargsr)rr;r<rszApplyPatchError.__init__c Cs0dj|j|j|j|j|jdjdd|jDS)Nz0Unable to apply patch ({0} {1} {2} {3} {4}, {5})z, cSsg|] }t|qSr;)rd).0ir;r;r< -sz+ApplyPatchError.__str__..)rrrrrOrrAr)rr;r;r<__str__&szApplyPatchError.__str__)rLrrrrrrr;r;)rr<rs rcs(eZdZdZfddZddZZS)rzprevious patch failedcs"tt|j||||_||_dS)N)rrrranchor)rrrrr)rr;r<r5sz%PreviousPatchFailedException.__init__cCsd}|j|j|jS)NzIt seems, the latest patch, applying at {0}, crashed, and further attempts will be suspended. To force patch applying, remove `{1}` file)rrr)rmessager;r;r<r:sz$PreviousPatchFailedException.__str__)rLrrrrrrr;r;)rr<r2s rcCstjdj|}yztj|}tjtj|j}t |d}|dkrRtj dn8|dkrftj dn$|dkrztj dntj d j||St k r}zt j ||WYdd}~XnXd S) Nz"/nagios/register_key.plain?key={0}rrzKey successfully registeredrzWrong key format or sizerrz!No KernelCare license for that IPzUnknown error {0}r?)rget_registration_urlrrurlopenr data_as_dictrkrr print_wrapperrrprint_cln_http_error)keyroresponseresrer;r;r<!set_monitoring_key_for_ip_licenseCs      rc cs>tjrtjtjddz dVWdtjr8tjtjddXdS)NT)shell)r ZBEFORE_UPDATE_COMMANDr run_commandZAFTER_UPDATE_COMMANDr;r;r;r< execute_hooksWs  rcCst}|j}|j}tj}|dkrht|tjtj t j |t t j|d}tjdtjtj|njtjdtjt|tjdt|tjtjtjtj tjt j tj|tjt jdS)a1 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: ri)Z updateCodeZ autoUpdateZeffectiveKernelZ realKernelZloadedPatchLevelZuptimelicensez --START--z LICENSE: N)_patch_level_infor applied_lvlr license_infordr AUTO_UPDATEr kcare_unamerrrrZ get_uptimerrrirj)fmtpliZ update_codeZ loaded_plZlicense_info_resultZresultsr;r;r< plugin_infocs,     rc Cs^tj}ytdd}Wntk r4tjr0dSdSX|dkrBdS||krNdStjrZdSdS)Ninfo)rrrrr)rloaded_patch_levelrrr IGNORE_UNKNOWN_KERNELrZstatus_gap_passed) current_levelZlatest_patch_levelr;r;r<get_update_statussrcCs2tjdd\}}|dkr*|jdr*dSdSdS)NrrZ CloudLinuxz7.extrarK)rr` startswith)rOversionr;r;r<edf_fallback_ptypesrcCsl|j|jf}tj||}tj||j|_|jjtj tj d|tkrZ|jj ddt|<|j rh|j dS)zFunction remembers IP address of host connected to and uses it for later connections. Replaces stdlib version of httplib.HTTPConnection.connect rNrr)hostZportCONNECTION_STICKY_MAPgetsocketZcreate_connectionZtimeoutsockZ setsockoptZ IPPROTO_TCPZ TCP_NODELAYZ getpeername _tunnel_hostZ_tunnel)rZaddrZ resolved_addrr;r;r<sticky_connects  rZHAS_SNIz0.13z%No pyOpenSSL module with SNI ability.cGsdS)NTr;)rr;r;r<dummy_verify_callbacksrc@s,eZdZddZddZddZddZd S) SSLSockcCs||_d|_dS)Nr) _ssl_conn_makefile_refs)rrr;r;r<rszSSLSock.__init__cGs&|jd7_tj|jf|ddiS)Nrr7T)rrZ _fileobjectr)rrr;r;r<makefileszSSLSock.makefilecCs"|j r|jr|jjd|_dS)N)rrr7)rr;r;r<r7s z SSLSock.closecGs |jj|S)N)rsendall)rrr;r;r<rszSSLSock.sendallN)rLrrrrr7rr;r;r;r<rsrc@seZdZddZdS)PyOpenSSLHTTPSConnectioncCstjj|tjjtjj}|jtjjtjj Bt j rJ|j tjj tn|j tjjt|jtjj||j}|j|jp|j}|j|j|jt j rt|j|t||_dS)N)r!HTTPConnectionconnectOpenSSLZSSLZContextZ SSLv23_METHODZ set_optionsZ OP_NO_SSLv2Z OP_NO_SSLv3r CHECK_SSL_CERTSZ set_verifyZ VERIFY_PEERrZ VERIFY_NONEZset_default_verify_pathsZ ConnectionrZset_connect_staterrZset_tlsext_host_nameencodeZ do_handshakematch_hostnameZget_peer_certificater)rZctxZconnZ server_hostr;r;r<rs  z PyOpenSSLHTTPSConnection.connectN)rLrrrr;r;r;r<rsrcCstjr&tj||}tjtj|ddS|dk}tjo6|}xrd|fd|fdgD]Z\}}t j |||d} t j | |d} |rdj | } tj|t ||d | }d } |s|rt || kr|rd nd } tjd j | qNyxtjtj|dd} tjrJtj| rJtj| }tj|}|r.tjdj |ddntjddd|rJ|j| Stk r}z>|sl|r|jdks|jdkrtjdj |wNWYdd}~XqNXqNWdS)NF) check_license latest.v1 latest.v2T)secure_boot_info perf_metrics) b64_encodingzinfo={0}?iXzsecure boot infoz perf metricsz/Check-in URL param is too large, discarding {0}z:Automatic kernel anomaly report uploaded successfully: {0}) print_msgz$Failed to send kernel anomaly reportizCCheck-in request failed with error: {0}, retrying with reduced info)rr)FF)rr)r rhrZget_kernel_prefixed_urlrZwrap_with_cache_keyr urlopen_authZSEND_PERF_METRICSrZencode_checkin_payloadrstickyfylenrlogwarnZKERNEL_ANOMALY_REPORT_ENABLErZdetect_anomalyprepare_kernel_anomaly_reportsend_data_packageloginforemove_archiverr)rrrmoderorZ perf_enabledrrZsinfoZ request_paramZmax_url_lengthZ discard_infor8 data_package upload_nameexr;r;r<_fetch_patch_level_requestsB       $r c Cs8tj}tjdk r$tj|ttjSxtD]}yt||||}tj |j t t j |jj}tjdj||dd|r|jdrt|}|jdg}tj|stjdtj||d|d|d Stj|t|Stk rYq,tk r(}z|jd krtd WYdd}~Xq,Xq,WtdS)Nz;fetch patch level, reason: {0}, kernel latest response: {1}F)r{r zeLatest KernelCare patchset is incompatible with the current kernecare package version, please upgraderbaseurlrzKC licence is required)rr) rrr PATCH_LEVELrr PATCH_LATESTr r Zset_feature_flags_from_headersheadersupdate_all_kmod_paramsrrkrrrrrrr"rr Zhas_kc_capabilitiesrCapabilitiesMismatchZKernelPatchLevelrrrrr) rr rrrrZ latest_infoZrequired_capabilitiesr r;r;r<fetch_patch_level;s2     rcCs<|jt|tj}tjjdj|ytj |ddddSt k r^tjjdj|dSt k r}ztjj dj|t |WYdd}~XnX|jt|tjtj}tjjdj|ytj |ddWnbt k rtjjdj|dStk r6}ztjjd j|t |WYdd}~XnXdS) NzProbing patch URL: {0}FHEAD)rmethodTz{0} is not available: 404zFHEAD request for {0} raised an error, fallback to the GET request: {1})rz{0} is not available: {1})file_urlrDr rErr{rrrrrrndebugrdr ZSIGr )rrBZbin_urlr ror;r;r< probe_patch\s(**rcCsF|tjkr|jtj}n |j|}|j|}tj||tjtj |dS)N)Z hash_checker) r KMOD_BINZkmod_urlr cache_pathrZ fetch_urlr USE_SIGNATUREZget_hash_checker)rnameroZdstr;r;r<fetch_and_verify_kernel_fileus    r!c@s>eZdZdddZddZddZdd Zd d Zd d ZdS) PatchFetcherNcCs ||_dS)N)r)rrr;r;r<rszPatchFetcher.__init__cCs t|j|S)N)r!r)rr r;r;r<_fetchszPatchFetcher._fetchcCsr|jjtj}|jjtj}|jjtj}|jjtj}tdd||||fDopt j j |dkopt j j |dkS)Ncss|]}tjj|VqdS)N)r0r1r2)rr1r;r;r< sz0PatchFetcher.is_patch_fetched..r) rrr rIrErFr rallr0r1getsize)rZpatch_done_pathZpatch_bin_pathZpatch_info_pathZ kmod_bin_pathr;r;r<is_patch_fetchedszPatchFetcher.is_patch_fetchedcCs4|jdkrtd|js|jS|jr6tjd|jStjdt|jtjrytj |jj t j dd}Wnt k r~Yn(X|jjdd}|r|jjtj||_y|jt j Wn0t k rtdj|jt jpdd d YnX|jt j|jtj|jtj|jjt jd d d tjtj |jS)Nz+Cannot fetch patch as no patch level is setzUpdates already downloadedzDownloading updatesr)rz KC-Base-UrlzfThe `{0}` patch level is not found for `{1}` patch type. Please select valid patch type or patch leveldefaultzpatch level not found)rwb)r )!rrr'rrrUrrrrrr rErrrupgraderrkr#rr PATCH_TYPErFr rextract_blacklistrrrIrrestore_selinux_contextr)rresprr;r;r< fetch_patchs:      zPatchFetcher.fetch_patchcCsJt|jjtjdj}|rFtj|}|rFtj |jjtj |j ddS)Nr.r) r4rrr rFr BLACKLIST_REsearchrrrGgroup)rZbufZmor;r;r<r-s  zPatchFetcher.extract_blacklistcCs|dkr dSyt|tj}Wntk r0dSX|jjdd}|rT|jtj|}|j tj}t |d}t dd|j D}WdQRXx|D]}t||qWt jtjdS)z Download fixup files for defined patch level :param level: download fixups for this patch level (usually it's a level of loaded patch) :return: None Nz KC-Base-Urlr.cSsg|] }|jqSr;)r)rfixupr;r;r<rsz-PatchFetcher.fetch_fixups..)r!r rHrrrr+rrkrr4r/ readlinesrr.r r)rrr/rZ fixups_fnamer9fixupsr4r;r;r< fetch_fixupss    zPatchFetcher.fetch_fixups)N) rLrrrr#r'r0r-r7r;r;r;r<r"~s   )r"cCs8t}tj|j|jtjkr*tjdn tjddS)Nrr) rrrrrPLIPATCH_NEED_UPDATErSexit)rr;r;r< kcare_checks    r;c Cs\t}t|}y tj}Wntk r2i}YnXtj}d}|dk r\tj|dj d}tj }|j dg}t t j|dd}t |}dd|D} t t j| d d} td d |D} || } tj} | st jd n t jd t jdj|t jdj||dkr t jdj|| dkr:t jdj| | dkrNt jdt jddS)NZUnknowntsz%Y-%m-%drz kpatch-cve)Z cve_fieldcSs"g|]}|jdgD]}|qqS)r)r)rrecpatchr;r;r<rsz%show_generic_info..Zcvecss|]}t|jdgVqdS)rN)rr)rr=r;r;r<r$sz$show_generic_info..z$KernelCare live patching is disabledz"KernelCare live patching is activez - Last updated on {0}z - Effective kernel version {0}rz* - {0} kernel vulnerabilities live patchedz- - {0} userspace vulnerabilities live patchedz% - This system has no applied patchesz(Type kcarectl --patch-info to learn more)r_kcare_patch_info_jsonrZlibcare_patch_info_basicrrZ get_staterZ fromtimestampZstrftimerrrrZextract_unique_cvessumrrr)r kcare_info libcare_infostateZ latest_updateZeffective_versionZkernel_patchesZkernel_vulnerabilitiesZkernel_patches_countZuserspace_patchesZuserspace_vulnerabilitiesZuserspace_patches_countZtotal_patches_countrr;r;r<show_generic_infos>         rDFc Csytdtjd}|st|jtj}tjt j |j }|rgi}}x>|j dD]0}tj |}|rxd|krx|j|qR|j|qRW||d<tj|}tj|WnJtk r}ztj||jdSd}~Xntk rtjdYnXd S) z 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 r)rpolicyz z kpatch-namerrNzNo patches availabler)rr POLICY_REMOTErrr rFrrkrrrr@rappendupdaterirjrrrrro) is_jsonrro patch_inforr8chunkdatarr;r;r<kcare_latest_patch_infos,      rMcCsd|ji}|jdk rt|}g}x>|jdD]0}tj|}|rRd|krR|j|q,|j|q,W||d<tj }|r||dnd|d<|S)Nrz z kpatch-namerrunknown) rr_kcare_patch_infor@rrrGrHrZread_dumped_kernel_patch_level)rr8rJrrKrLZsaved_patch_levelr;r;r<r?4s     r?cCsTtj}tj||jtj}tjj|s2t dddt |dj }|rPt j d|}|S)NzvCan't find information due to the absent patch information file. Please, run /usr/bin/kcarectl --update and try again.zpatch info not found)rr.rK)rrrrr rFr0r1r2rr4rr1sub)rrrrr;r;r<rOHs  rOcCsZt}|s>|jdkr tj|j|jdkr.dStjt|ntjtjt |dddS)NrT)Z sort_keys) rrrrrrrOrirjr?)rIrr;r;r<rJWs   rJcCs:tjd|g}tj|}tj}d}tj||tj||kS)Nz file-infozkpatch-build-time)r KPATCH_CTLr check_outputr _patch_infoZget_patch_value)new_patch_filerZnew_patch_infoZcurrent_patch_infoZbuild_time_labelr;r;r< is_same_patchcs   rUcCsL|dkr dS|r||krdS||kr(dStjtj|tj}t|sHdSdS)NrFT)rrrr rErU) applied_level new_levelrTr;r;r<kcare_need_updateks rXcCsptjrltjjtotjttjs6tj j dj tdSt j dddtgdd\}}}|dkrltj j dj |dS) Nz-File {0} does not exist or has no read accessz /sbin/sysctlz-qz-pT) catch_stdoutrz%Unable to load kcare sysctl.conf: {0})r ZUPDATE_SYSCTL_CONFIGr0r1r2 SYSCTL_CONFIGaccessR_OKrr{warningrrr)r_r;r;r< update_sysctl}sr_c stjjtsttdjtjttjs>tj j dj tdSttdj}|j }|j dx,|D]$tfdd|Dsb|jqbWx|D]}|j|dqW|jWdQRXdS) z*Update SYSCTL_CONFIG accordingly the editsrqzFile {0} has no read accessNzr+rc3s|]}j|VqdS)N)r)rr.)r:r;r<r$sz#edit_sysctl_conf.. )r0r1r2rZr4r7r[r\rr{r]rr5seekanywritetruncate)rrGZsysctllinesrqr;)r:r<edit_sysctl_confs     rfcCs.x(|D] }tj|rtdj|ddqWdS)NzDDetected '{0}' kernel module loaded. Please unload that module firstzconflicting kernel module)r)CONFLICTING_MODULES_REmatchrr)rmoduler;r;r<detect_conflicting_moduless   rjcCsdjtjS)Nz/lib/modules/{0}/extra/kcare.ko)rrZget_system_unamer;r;r;r<get_kcare_kmod_linksrkc CsXtdd}tjtj|tj}tjj|s.dSt |d}|j dddkSQRXdS)Nr)rrbs~Module signature appended~ i) rrrrr rr0r1r2r4r)rZ kmod_fileZvfdr;r;r<kmod_is_signeds    rncs4tjddkrdSddg}tfdd|DS)Nz /proc/keysZ(12ff0613c0f80cfba3b2f8eba71ebc27c5a76170Z(69a6d9eed3f620d5c2e13a1d211c46510a5ad9f5c3s|]}|kVqdS)Nr;)rr) system_keysr;r<r$sz'kcare_certs_enrolled..)rZ try_to_readrb)Z kcare_keysr;)ror<kcare_certs_enrolleds  rpcKsdd|g}x&|jD]\}}|jdj||qWtj|dd\}}}|dkr`tdj||dddS) Nz /sbin/insmodz{0}={1}T)rYrzLUnable to load kmod ({0} {1}). Try to run with `--check-compatibility` flag.zkmod load error)r)itemsrGrrrr)Zkmodrcmdrrerr^r;r;r< load_kmods rscCsTtjr,tdkrtdtdkr,tdtjsDtjsDtjrPtddddS)NFz4Secure boot is enabled. Not supported by KernelCare.z|}YnXtj rbt j j tj  rbt j tj t}t|tfdd|jD}t|f|tdS)Nc3s"|]\}}|kr||fVqdS)Nr;)rkv)available_kmod_paramsr;r<r$!sz"load_kcare_kmod..)rkrrr rshutilcopyrnr rr0r1rrrr}dictrqrs update_depmod)rrr{Z kcare_fileZ kmod_paramsr;)rr<load_kcare_kmods   rcCsXdg}|dk r|jd|gtj|ddd\}}}|rTtjdjdj|||dddS) Nz /sbin/depmodz-aT)rYruz%Running of `{0}` failed with {1}: {2} F)r)extendrrrrwrrA)unamerrrr^stderrr;r;r<r's rcCs8tjd|gdd\}}}|dkr4tdj||dddS)Nz /sbin/rmmodT)rYrzUnable to unload {0} kmod {1}zkmod unload error)r)rrrr)modnamerr^r;r;r< unload_kmod3srcCsTg}xJdg|D]<}tj||dj|}tjj|rt||jdj|qW|S)NZvmlinuxz fixup_{0}.koz fixup_{0})rrrr0r1rrsrG)rrrZloadedmodZmodpathr;r;r< apply_fixups9s rc CsDx>|D]6}y t|Wqtk r:tjjd|YqXqWdS)Nz$Exception while unloading module %s.)rrnrr{r|)r6rr;r;r< remove_fixupsCs   rcCs|r |}n6tjrtj}n(tj|r2d|tjdfSd|tjdfSdddddd}|j}||krj||}ntdj||tjdd d ||tjdfS) NZfreeze_conflictTr(FZ freeze_noneZ freeze_all)ZNONEZNOFREEZEZFULLZFREEZEZSMARTz3Unable to detect freezer style ({0}, {1}, {2}, {3})zfreezer style detection error)r)r Z PATCH_METHODr= intersectionupperrr)freezerrrZpatch_method_mapr;r;r<get_freezer_styleKs&  rrKcs|||dtdtj}tj}t|t||}tj||tj}t ||dj |tj t j tj|} d|k} | otj||} |dk } | ot|otj| } j|| d| rtddS| rtdt|||}tdt|td t|| r"td tdd } | sz5Patch level {0} applied. Effective kernel version {1}waitcstS)N)rr;)rr;r<szkcare_load..)rz)"rrrrrjrrr rErrr,rrZ parse_unameZis_kmod_version_changedrUZkcare_update_effective_versionrHrkpatch_ctl_unpatchrrrrkpatch_ctl_patchr_rrrrZtouch_status_gap_filerr)rrr r use_anchorrrrr descriptionZ kmod_loadedrZ patch_loadedZ same_patchr6r;)rr< kcare_loadmsR              rc Cstjg}tj||tj}tjj|r2|j d|g|j dd|g|j d|dg|j |t j |dd\}}}|dkrt ||||dS)Nz-br>z-dz-mrT)rY)r rQrrr rGr0r1rrrGrrr) rrrrrrZblacklist_filerr^r;r;r<rs  rcCs^tjtjdd|dgddd\}}}|dkrZtjdj||ddtd j|t|d d dS) Nrz-mrT)rYruz4Error unpatching, kpatch_ctl stdout: {0} stderr: {1}F)rzError unpatching [{0}] {1}z unpatch error)r) rrr rQrrwrrrd)rrr|rr;r;r<rs  rcCs8||d<ttj|d<tjtjjtjdt |dS)Nactionr<z kcare.state) rryrrr0r1rAr rrd)rrr;r;r<rsrcCspd}tjj|sdSxVtj|D]H}tjj||dd}tjj|sDq tj|}||kr tj|t|q WdS)Nz/usr/lib/modules/z weak-updateszkcare.ko) r0r1isdirlistdirrAislinkreadlinkunlinkr) kmod_linkZ modules_pathentryZ sym_link_pathZ target_pathr;r;r<update_weak_moduless    rc CsJtj}t}y|j|Wn8tk rT}z|sDtdj|ddWYdd}~XnXtj}t||}t d|kr|dk }|rt tj ||}t j tjdd|dgddd \} } } t|| dkrtjd j| | d d td j| t|ddtjtjtdtdtdt} tjj| r4tj| t| WdQRXdS)NzUnable to retrieve fixups: '{0}'. The unloading of patches has been interrupted. To proceed without fixups, use the --force flag.zfixups retrieval error)rrrz-mrT)rYruz4Error unpatching, kpatch_ctl stdout: {0} stderr: {1}F)rzError unpatching [{0}] {1}z unpatch errorr)countdelay) rrr"r7rnrrrrrrrrrr rQrrrwrdrZretryrZ check_excUNLOAD_RETRY_DELAYrrkr0r1r2rr) rforcerpferrrrZ need_unpatchr6rr|rrr;r;r< kcare_unloads:    rcCs8t}|rt|S|jdkr"|jS|jdk r4tjSdS)Nr)r_kcare_info_jsonrrrrrS)rIrr;r;r<rAs  rAcCsRd|ji}|jdk r>|jtjtj|jtj|jd|j |d<t j |S)Nrzkpatch-descriptionz kpatch-state) rrrHrrrrSZparse_patch_descriptionrrCrirj)rr8r;r;r<rs    rc@s$eZdZdZdZdZdZddZdS)r8rrrrrcCs"||_||_||_||_||_dS)N)rr remote_lvlrrC)rrrrrrCr;r;r<r%s z PLI.__init__N)rLrrrr9PATCH_UNAVALIABLEPATCH_NOT_NEEDEDrr;r;r;r<r8s r8c Cstj}ytdd}|rJt||r6tjdd}}}qxtjdd}}}n.|dkrftjdd}}}ntjd d}}}t|||||}Wnltk rtj }t j rd j t j t jdtj}nd j t jdtjtj}t||ddd }YnX|S) Nr)rz*Update available, run 'kcarectl --update'.ZappliedzThe latest patch is applied.rz(This kernel doesn't require any patches.ZunsetzDNo patches applied, but some are available, run 'kcarectl --update'.zuInvalid sticky patch tag {0} for kernel ({1} {2}). Please check /etc/sysconfig/kcare/kcare.conf STICKY_PATCH settingszLNew kernel detected ({0} {1} {2}). There are no updates for this kernel yet.Z unavailable)rrrrXr8r9rrrrr STICKY_PATCHrrr`rrr)Zcurrent_patch_levelZnew_patch_levelrrrCrr;r;r<r-s8   rc Csd}yXtj}td|fd|fg}tjdj|}tj|}tj tj |j }t |dSt k r}ztj||d Sd}~XnZtk r}ztj||d Sd}~Xn0tk r}ztjdj|d Sd}~XnXdS) z 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 N server_idtagz/tag_server.plain?{0}rrzInternal Error {0})r get_serveridr#rrrrrrrrkrrrrrr rnrw) rrorZqueryrrrZueZeer;r;r< tag_server_s"    rcCstjd}tjdj|t}tj}y:tj ||j }tj t j ||j tj|j ||j }Wn2tk r}ztjdj|WYdd}~XnXtjd|tjgdd\}}}|rtdj||dd WdQRXdS) Nz doctor.shz#Requesting doctor script from `{0}`z3Kcare doctor error: {0}. Fallback to the local one.ZbashT)ruzScript failed with '{0}' {1}zdoctor script failed)r)rrmrZlogdebugrKCDOCTORtempfileZNamedTemporaryFilerZfetch_signaturer Z save_to_filerrZcheck_gpg_signaturernrwrrrZget_patch_serverr)Z doctor_urlZdoctor_filenameZ doctor_dstZ signaturerrr^rr;r;r<kcdoctorzs   "rc CsBtjdjt}ytj|Wntk r2dSXtjddS)Nz{0}-new-versionFzwA new version of the KernelCare package is available. To continue to get kernel updates, please install the new versionT) rrmrEFFECTIVE_LATESTrrr rr)ror;r;r<check_new_kc_versionsrc Cstj}t|}|tjkp*|tjko*|dk}yt||}Wntjk r}z.|dkrXt j t |t j dtj }WYdd}~Xn<t k r}z |rnt jjdj|WYdd}~XnX|tjkr|} n@|} |dkr|tj krtj|d} n|tjkr |} ntd| S)a Get patch level to apply. :param reason: what was the source of request (update, info etc.) :param policy: REMOTE -- get latest patch_level from patchserver, LOCAL -- use cached latest, LOCAL_FIRST -- if cached level is None get latest from patchserver, use cache otherwise :param mode: constants.UPDATE_MODE_MANUAL, constants.UPDATE_MODE_AUTO or constants.UPDATE_MODE_SMART :return: patch_level string Nz#Using previously downloaded patcheszUnable to send data: {0}rz9Unknown policy, choose one of: REMOTE, LOCAL, LOCAL_FIRST)rrrr rFZPOLICY_LOCAL_FIRSTrrrrrrdZ POLICY_LOCALrnr{r]rrr) rrEr rZ cached_levelZconsider_remote_exZ remote_levelrr rr;r;r<rs2  $    rcCs|dkr dS|dkrdn|t_ttddtjrtjtjdtjdkrntjrntjpXt }t dd d j |ft j d j |ntdj |dddS)Nedfr(rKZprobe)r)r,rvrfs.enforce_symlinksifownerfs.symlinkown_gidzfs.enforce_symlinksifowner=1zfs.symlinkown_gid={0}z'{0}' patch type selectedz/'{0}' patch type is unavailable for your kernelzpatch type unavailable)r)rvr)rr)r r,rrr update_configrZ is_cpanelZ FORCE_GID CPANEL_GIDrfrrrr)rBZgidr;r;r<update_patch_types rZkernelc $Cshttj|tjkrtytd||d}WnRtk r~}z6|tjtj fkrltj rlt |}t j j|dSWYdd}~XnXtj}|tjkrtj rdSt|}|jt||dst jddSy(tjtjdddtjtjdd dWn"tk rt j jd YnXtj}t(|j|t|||||tj kd WdQRXtj|t ||dS) ax :param mode: constants.UPDATE_MODE_MANUAL, constants.UPDATE_MODE_AUTO or constants.UPDATE_MODE_SMART :param policy: REMOTE -- download latest and patches from patchserver, LOCAL -- use cached files, LOCAL_FIRST -- download latest and patches if cached level is None, use cache in other cases :param freezer: freezer mode rH)rrEr N)rVrWz%No updates are needed for this kernelrz kcore*.dump)Zkeep_nZpatternz kmsg*.logz#Error during crash reporter cleanup)r)!rxr r,r rFrrrUPDATE_MODE_AUTOUPDATE_MODE_SMARTrrdrr{r]rrrr"r0rXrrrrrnr|rrr7rZdump_kernel_patch_levelr) rr rErrrrrrr;r;r< do_updates<      " rcCstttjttjptjttjp$tjf}|dkr@tdddtjrLtjS|t j krptjp`tj}tjpltj}n tj}tj}|r|S|rd|SdS)NrzInvalid configuration: conflicting settings STICKY_PATCH, [AUTO_]UPDATE_DELAY or [AUTO_]STICKY_PATCHSET. There should be only one of themzconflicting sticky settings)rzrelease-) r@boolr rZ UPDATE_DELAYZAUTO_UPDATE_DELAYZSTICKY_PATCHSETZAUTO_STICKY_PATCHSETrr UPDATE_MODE_MANUAL)r rrZpatchsetr;r;r< get_stickys&  rcCs |d|S)Nr>r;)rrr;r;r< _stickyfy<srcCs t|}|s|S|dkr"t||Stj}|sDtjjdtjdyt j t j dj |}Wn:tk r}ztj||jtjdWYdd}~XnXtjtj|j}t|d}|dkrt|d |S|d kr|S|d krtjjd tjdtjjd |dtjddS)z Used to add sticky prefix to satisfy KCARE-953 :param file: name of the file to stickify :return: stickified file. KEYzHPatch set to STICKY_PATCH=KEY, but server is not registered with the keyrz!/sticky_patch.plain?server_id={0}rNrrrrrrzEServer ID is not recognized. Please check if the server is registeredzError: rrrrr?r)rrrrrr{rrSr:rrrrrrrrorrrkrr)filer srrrrrr;r;r<r@s2       rc Csg}|s dS|jd}|d}|dd}|jd}||krLtdt||s`|j|jkS|dkrt|jdn>|jd s|jd r|jtj|n|jtj|j d d x|D]}|jtj|qWtj d d j |dtj } | j |S)zhMatching according to RFC 6125, section 6.4.3 http://tools.ietf.org/html/rfc6125#section-6.4.3 Fr>rrN*z,too many wildcards in certificate DNS name: z[^.]+zxn--z\*z[^.]*z\Az\.z\Z)r@rrreprlowerrGrreescapereplacecompilerAZ IGNORECASErh) ZdnhostnameZ max_wildcardsZpatspiecesZleftmostZ remainderZ wildcardsZfragZpatr;r;r<_dnsname_matchls(     rc Cs g}xBt|jD]2}|j|}|jdkrddt|jdD}qW|sTtdg}x0|D](\}}|dkr^t||r|dS|j|q^W|s|j j }t||rdS|j|t |dkrt dj |d jtt|n,t |dkrt d j ||d nt d dS) NZsubjectAltNamecSsg|]}|jjddqS)ryr)rr@)ritr;r;r<rsz"match_hostname..,ztempty or no certificate, match_hostname needs a SSL socket or SSL context with either CERT_OPTIONAL or CERT_REQUIREDZDNSrz(hostname {0} doesn't match either of {1}z, zhostname {0} doesn't match {1}rz=no appropriate commonName or subjectAltName fields were found)rangeZget_extension_countZ get_extensionZget_short_namerdr@rrrGZ get_subjectZ commonNamerrrrAmapr) ZcertrZsanrrZdnsnamesrreZcnr;r;r<rs0       rc Cs tddd}|jdddd|jdd d dd|jd d dd|jd dddd|jdddd|jdddd|jdddd|jdddd|jdddd|jdddd|jdd dd|jd!d"dd|jd#d$dd|jd%d&dd|jd'd(d)d|jd*d+dd|jd,d-dd|jd.d/dd|jd0d1dd|jd2d3dd|jd4d5d6d|jd7d8d9d|jd:d;dd|jdd?dd|jd@dAdd|jdBdCdd|jdDdEddFdG|jdHdIdd|jdJdKdd|jdLdMdd|jdNdOdd|jdPdQdd|jdRdSdd|jdTdUdd|jdVdWdd|jdXdYdd|jdZd[d\tdd]d^|jd_d`dd|jdadbdd|j}|jdcddd\d|jdedfdd|jdgdhdd|jdidjd\dd]dk|jdldmdndd]do|jdpdqdr|jdsdtdd|jdudvdwdxdytjs|jdzd{d|d}d]d~|jddd|d}dd~|jdddd|jddddd|jddddd|jdddd|jddddd|jddddd|jddddd|jdddddd|jdddd|jdddd|jddd6dd\dd]d|j}tjtjsFtj dg7_ |j dk rzt t d|j j djtj rvdSdS|js|jrtjrtjt_ntjt_n|jrtjt_|jstjdkrtdtjddStj}|jrtj }n|jrtj!}t"j#|tj$stj%|j&r,t'j(|j)rn|j)dkr\t*|j)t_+tj,tj+dndt_+tj,dd|j-dk rtj,|j-d|j-t_.|j/rd]t_0|j1rd]t_2|j3rdt_4|j5rt5|j6rt7j8dt9n8|j:rtj;dkrtj|j?r&|j?t_@|jAr@t7j8dt9dt_@tj@jBdt_@tj@r~tj@tCkr~t"jDjEdjFtj@djGtC|jHrdt_Id|jHt_J|j=rtK|j=tj;dkrtLt_;t7j8djFtj;pdt9|jMrt'jNtOjM|jPddS|jQr$tQjQdddd}t'jNtPjR|dStStj;|jTrJtTjUtV|jWddS|jXrtYjZtQjQdddd}djF|j[}|j\rt'jN|nRtYj]|}|rt"j^djF|nt"j_ddd|j`rt'jN|n|r|ja|jbr|jPrtbddntbdS|jcrtj,dddS|jdr0tj,dddS|jerHtjf|jedS|jgrZth|jgS|jirjtjji|jkrtj;dkrtj,ddtjjk|jk|jlS|jmrtjjmdkrdSdS|jndk rto|jnS|jprt'jNtjqtr|d|ddk rtsjt|judStj s|jv rd|jvini}|jw r2tsjxS|jy rVtsjzf|dk rVt"j^dă|j{ rvtsjzfdtj|i|n|j} rtsj~t"j^dƃ|j rtsjt"j^dǃ|j rt'jNtsj|j rt'jNtsj|j rtsj rt'jNtsj|j|jdk rj|jdk r,tj p(ttsjj} nddɄ|jj dD} tsjzfdt| i|dk rjt"j^dă|j rtsjzftj|dd˜||j rt'jNt|jPdd} |j rt7j8dt9d} |j r|j} |j rt| tjtjd΍|j> rt| tjdύt"j^dЃ|j rt'jNtj|j r>t| |jdэt"j^d҃|j rld]t_tjtjddӃt| tj|dύ|j rt|jPd|j rtS|j rt|jPd|j rtttjdk rtdS)NZkcarectlz)Manage KernelCare patches for your kernel)Zprogrz--debugrKZ store_true)helprz-iz--infoz]Display information about KernelCare. Use with --json parameter to get result in JSON format.z --app-infozcDisplay information about KernelCare agent. Use with --json parameter to get result in JSON format.z-uz--updatez) into an isolated cache, leaving the default storage untouched. Use together with --lib-update or --userspace-update.)rrrr(rzlibcare-enabledrrrzPlease run as root)r)r)rzTFlag --edf-enabled has been deprecated and will be not available in future releases.rr(zMFlag --test has been deprecated and will be not available in future releases.r(/z(Prefix `{0}` is not in expected one {1}.rzfile:z+edf patches are deprecated. Fallback to {0})rIr)rrr)Zforce_fallbackz)Kernel anomaly report file generated: {0}z0Kernel anomaly report uploaded successfully: {0}z$Failed to send kernel anomaly report)rri)rZYES)rZNOrvr)r,rzUserspace patches are applied.r zUserspace patches are unloaded.zLibcare plugin reloaded.cSsg|]}|jjqSr;)rr)rZptchr;r;r<r5szmain..limit)r rzQFlag --nofreeze has been deprecated and will be not available in future releases.r)r rE)r zKernel is safe)rz=KernelCare protection disabled. Your kernel might not be safe<)rZ add_argumentrZadd_mutually_exclusive_groupr ZLIBCARE_DISABLEDZ parse_argsr Zset_settings_from_config_fileZFLAGSZ has_flagsr/filterr@issubsetquietZ auto_updateZSILENCE_ERRORSr ZPRINT_CRITICALZ PRINT_LEVELZ PRINT_ERRORrZ PRINT_DEBUGrr0getuidprintrSrloggingZINFOZWARNINGDEBUGrZinitialize_loggingZIGNORE_FEATURE_FLAGSZset_feature_flags_from_cacherrZclear_all_cacheZset_patch_levelrdrrZset_sticky_patchrZ nosignaturerZ no_check_certrr~rrtZ edf_enabledwarningswarnDeprecationWarningZ edf_disabledr,ZPREV_PATCH_TYPEZset_patch_typerHrrr(rEXPECTED_PREFIXr{r]rrAZlocalrhZ PATCH_SERVERrrZapp_inforrrirrjrJr Zsend_doctor_reportrZfallbackZkernel_anomaly_reportrrZ archive_pathrrrrZ keep_localrrZenable_auto_updateZdisable_auto_updateZ set_configZupdate_config_from_argsZset_monitoring_keyrZ unregisterrregisterZregister_autoretryrrrrrarcrZset_libcare_statusrZlib_tagZuserspace_statusZget_userspace_update_statusZ lib_updateZdo_userspace_updateZlib_auto_updaterZ lib_unloadZlibcare_unloadZ lib_repluginZlibcare_repluginZlib_inforBZlib_patch_infoZlibcare_patch_infoZ lib_versionZlibcare_server_startedZlibcare_versionZuserspace_updaterlistZget_userspace_mapkeyssortedZuserspace_auto_updaterrAZnofreezerZ smart_updaterrZ UPDATE_POLICYrrrrrrZCHECK_CLN_LICENSE_STATUSryrzrandomZuniformrJrrZlatest_patch_inforMZcheckr;rargvrD) ZparserZexclusive_grouprrrr Zlocal_path_messager Z lib_tag_kwrrr;r;r<mainsr                                               r)r%r&r'r()r)r*)N)N)F)F)N)rKF)rKF)r)Z __future__rrlrirr0rrrrrZsslrSrryrRrZargparser contextlibrrrKrrr r r r r rrrrrrrrrrrrrrrrrZpy23rr r!r"r#rrrr3rrrZrrZDOTALLr1rgr1rinsertfilterwarningsrr{ZsetLevelrr=rDrJrgrprrrrrrrrrrrrrrrrrrrrrrcZdistutils.versionZ distutilsZ OpenSSL.SSLrrZ StrictVersionZ __version__ ImportErrorrZHTTPSConnectionZPureHTTPSConnectionobjectrrr rrrr!r"r;rDrMr?rOrJrUrXr_rfrjrkrnrprsrtrxr}rrrrrrrrrrrrrrZlog_all_parent_processesrrArr8rrrrrFrrZtrack_update_statusrrrrrrrr;r;r;r<s    \    &    4  -! b +             " ?   -  2 .= , 3)config_handlers.py000064400000023222152533440750010254 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import json import os import re from . import config, constants, http_utils, log_utils, utils from .py23 import ConfigParser if False: # pragma: no cover from typing import Dict, List, Optional, Set # noqa: F401 CONFIG = '/etc/sysconfig/kcare/kcare.conf' FEATURE_FLAGS_WHITELIST = [ # signatures-related things 'USE_CONTENT_FILE_V3', 'FORCE_JSON_SIG_V3', # kernel module - related things 'ENABLE_CRASHREPORTER', 'KCORE_OUTPUT', 'KMSG_OUTPUT', # checkin-related things 'SEND_PERF_METRICS', # anomaly reports 'KERNEL_ANOMALY_REPORT_ENABLE', # doctor reports 'FORCE_DOCTOR_FALLBACK', ] _CONFIG_OPTIONS = set() # type: Set[str] # options were read from config file def bool_converter(value): # type: (str) -> bool return value.upper() in ('1', 'TRUE', 'YES', 'Y') def comma_list(value): # type: (str) -> List[str] return [item.strip().lower() for item in value.split(',') if item.strip()] # A sane library-name shape: starts with a letter, then letters/digits and the # punctuation that occurs in real soname stems (libnss_dns, libstdc++, libc-2.x). _LIBCARE_LIB_NAME_RE = re.compile(r'^[a-z][a-z0-9_.+\-]*$') def libcare_libs_list(value): # type: (str) -> List[str] # Reuse comma_list for splitting/normalising, then drop tokens that cannot # be a library name (typos/garbage would otherwise reach libcare-server as # part of the match regex and silently never match anything). Warns once at # config-load time so a misconfigured LIBCARE_LIBS is visible to the user. valid, invalid = [], [] # type: (List[str], List[str]) for token in comma_list(value): (valid if _LIBCARE_LIB_NAME_RE.match(token) else invalid).append(token) if invalid: log_utils.logwarn('LIBCARE_LIBS: ignoring invalid library name(s): {0}'.format(', '.join(invalid))) return valid POSSIBLE_CONFIG_OPTIONS = { # name: convert function (could be None) 'AFTER_UPDATE_COMMAND': lambda v: v.strip(), 'AUTO_STICKY_PATCHSET': None, 'AUTO_UPDATE': bool_converter, 'AUTO_UPDATE_DELAY': None, 'BEFORE_UPDATE_COMMAND': lambda v: v.strip(), 'CHECK_SSL_CERTS': bool_converter, 'DOCTOR_REPORT_MAX_SIZE_BYTES': int, 'ENABLE_CRASHREPORTER': bool_converter, 'FORCE_DOCTOR_FALLBACK': bool_converter, 'FORCE_GID': None, 'FORCE_IPV4': bool_converter, 'FORCE_IPV6': bool_converter, 'FORCE_JSON_SIG_V3': bool_converter, 'HTTP_TIMEOUT': int, 'HTTP_UPLOAD_TIMEOUT': int, 'IGNORE_FEATURE_FLAGS': bool_converter, 'IGNORE_UNKNOWN_KERNEL': bool_converter, 'KCORE_OUTPUT': bool_converter, 'KCORE_OUTPUT_SIZE': int, 'KDUMPS_DIR': lambda v: v.rstrip('/'), 'KERNEL_ANOMALY_REPORT_ENABLE': bool_converter, 'KERNEL_ANOMALY_REPORT_MAX_SIZE_BYTES': int, 'KMSG_OUTPUT': bool_converter, 'LIBCARE_DISABLED': bool_converter, 'LIBCARE_LIBS': libcare_libs_list, 'LIBCARE_PIDLOGS_MAX_TOTAL_SIZE_MB': int, 'LIBCARE_SOCKET_TIMEOUT': int, 'LIB_AUTO_UPDATE': bool_converter, 'PATCH_LEVEL': lambda v: v or None, 'PATCH_METHOD': str.upper, 'PATCH_SERVER': lambda v: v.rstrip('/'), 'PATCH_SERVER_IPV6': lambda v: v.rstrip('/'), 'PATCH_TYPE': str.lower, 'PREFIX': None, 'PREV_PATCH_TYPE': str.lower, 'REGISTRATION_URL': lambda v: v.rstrip('/'), 'REGISTRATION_URL_IPV6': lambda v: v.rstrip('/'), 'PRINT_LEVEL': int, 'REPORT_FQDN': bool_converter, 'SILENCE_ERRORS': bool_converter, 'STATUS_CHANGE_GAP': int, 'STICKY_PATCH': str.upper, 'STICKY_PATCHSET': None, 'UPDATE_DELAY': None, 'UPDATE_POLICY': str.upper, 'UPDATE_SYSCTL_CONFIG': bool_converter, 'USERSPACE_PATCHES': comma_list, 'USE_CONTENT_FILE_V3': bool_converter, 'KERNEL_VERSION_FILE': None, 'KCARE_UNAME_FILE': None, 'SUCCESS_TIMEOUT': int, 'SEND_PERF_METRICS': bool_converter, } # pragma: no py2 cover def update_config(**kwargs): cf = open(CONFIG) lines = cf.readlines() cf.close() for prop, value in kwargs.items(): updated = False prop_eq = prop + '=' prop_sp = prop + ' ' for i in range(len(lines)): if lines[i].startswith(prop_eq) or lines[i].startswith(prop_sp): if value is None: del lines[i] else: lines[i] = prop + ' = ' + str(value) + '\n' updated = True break if not updated: lines.append(prop + ' = ' + str(value) + '\n') utils.atomic_write(CONFIG, ''.join(lines)) def update_config_from_args(params): # type: (List[str]) -> None params_for_update = {} pattern = re.compile(r'^([^=]+)=([^=]*)$') for param in params: match = pattern.match(param) if match: key, value = match.groups() if not value: value = None else: raise SystemExit('Invalid parameter format: %s. Format should be KEY=VALUE' % param) params_for_update[key] = value unknown_params = set(params_for_update) - set(POSSIBLE_CONFIG_OPTIONS) if unknown_params: raise SystemExit('Unknown parameter: %s' % ', '.join(sorted(unknown_params))) for var_name, value in params_for_update.items(): convert = POSSIBLE_CONFIG_OPTIONS[var_name] if value is None or convert is None: continue try: convert(value) except Exception: raise SystemExit('Bad value for %s: %s' % (var_name, value)) update_config(**params_for_update) class FakeSecHead(object): def __init__(self, fp): self.fp = fp self.sechead = '[asection]\n' # type: Optional[str] def readline(self): # pragma: no py3 cover if self.sechead: try: return self.sechead finally: self.sechead = None else: return self.fp.readline() def __iter__(self): # pragma: no py2 cover if self.sechead: yield self.sechead self.sechead = None for line in self.fp: yield line def get_config_settings(): result = {} cp = ConfigParser(defaults={'HTTP_PROXY': '', 'HTTPS_PROXY': ''}) try: config = FakeSecHead(open(CONFIG)) if constants.PY2: # pragma: no py3 cover cp.readfp(config) else: # pragma: no py2 cover cp.read_file(config) except Exception: return {} def read_var(name, default=None, convert=None): try: value = cp.get('asection', name) except Exception: value = default if value is not None: if convert: value = convert(value) result[name] = value for scheme, variable in [('http', 'HTTP_PROXY'), ('https', 'HTTPS_PROXY')]: # environment settings take precedence over kcare.config ones if not http_utils.get_proxy_from_env(scheme): proxy = cp.get('asection', variable) if proxy: os.environ[variable] = proxy for var_name, convert in POSSIBLE_CONFIG_OPTIONS.items(): read_var(var_name, convert=convert) return result def set_settings_from_config_file(): _CONFIG_OPTIONS.clear() settings = get_config_settings() config.__dict__.update(settings) # memorize keys defined in the config file _CONFIG_OPTIONS.update(settings) def convert_headers_to_feature_flags(headers): # type: (Dict[str, str]) -> Dict[str, bool] """ Checking headers for feature flags which start with 'KC-Flag-' and reformat it to dictionary with keys in upper case and without 'KC-Flag-' prefix and dashes replaced with underscores. For unification all header keys are checked in upper case. For example: 'KC-Flag-Some-Value' -> 'SOME_VALUE' :return: dict {'SOME_VALUE': bool, ...} """ flags = {} for hdr_name, hdr_value in headers.items(): upper_name = hdr_name.upper() if not upper_name.startswith('KC-FLAG-'): continue param_name = upper_name.replace('KC-FLAG-', '').replace('-', '_') try: flags[param_name] = bool(int(hdr_value)) except ValueError: log_utils.kcarelog.error('Invalid feature flag header value %s: %s', upper_name, hdr_value) return flags def set_feature_flags_from_headers(headers): # type: (Dict[str, str]) -> None save_feature_flags_cache(headers) if not config.IGNORE_FEATURE_FLAGS: set_feature_flags_from_cache() @utils.catch_errors(logger=log_utils.logwarn) def save_feature_flags_cache(headers): # type: (Dict[str, str]) -> None feature_flags = convert_headers_to_feature_flags(headers) utils.atomic_write(constants.FEATURE_FLAGS_CACHE, content=json.dumps(feature_flags)) @utils.catch_errors(logger=log_utils.logwarn) def set_feature_flags_from_cache(): # type: () -> None """ Set global variables using feature flag from cached values received with patchserver headers. Checks that option is allowed by whitelist and update global variable using globals() :return: None """ if not os.path.exists(constants.FEATURE_FLAGS_CACHE): return with open(constants.FEATURE_FLAGS_CACHE) as f: feature_flags = json.load(f) for key, value in feature_flags.items(): if key not in FEATURE_FLAGS_WHITELIST: continue if key in _CONFIG_OPTIONS: # param was already set from config file, it has higher priority continue config.__dict__[key] = value log_utils.kcarelog.info('feature flags config override: %s=%s', key, value) libcare.py000064400000061267152533440750006543 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import json import os import re import shutil import socket from . import ( auth, capabilities, config, config_handlers, constants, errors, fetch, log_utils, platform_utils, process_utils, selinux, server_info, update_utils, utils, ) from .py23 import HTTPError, json_loads_nstr, urlquote if False: # pragma: no cover from typing import Dict, List, Optional, Set, Tuple # noqa: F401 LIBCARE_CLIENT = '/usr/libexec/kcare/libcare-client' LIBCARE_SOCKET = ( "/run/libcare/libcare.sock", "/var/run/libcare.sock", ) LIBCARE_PATCHES = '/var/cache/kcare/libcare_patches' LIBCARE_CVE_LIST = '/var/cache/kcare/libcare_cvelist' LIBCARE_LOGROTATE_CONFIG = '/etc/sysconfig/kcare/libcare.logrotate' LIBNAME_MAP = {'mysqld': 'db', 'mariadbd': 'db', 'postgres': 'db'} # Exclude libs on distros with cross-library patches (libnss_dns + libresolv) already deployed: # - EL7 (all distros): glibc-rh1296031.patch (CVE-2015-7547) # - Ubuntu 16.04: revert-CVE-2015-5180.diff # See LIBCARE-2943 for details. # Entries: (distro_substring | None for any, version_prefix, blacklisted_libs). # version_prefix is matched as a dotted component prefix: '7.' matches '7', # '7.9' and '7.9.2009' but not '70'. CentOS 7's VERSION_ID is bare '7'. # Order matters: more specific rules first; first match wins. # NOTE: distro is matched by substring (`bl_distro in distro`) -- pick a substring # that is unique enough to avoid accidentally matching unrelated distros. LIBCARE_LIBS_BLACKLIST = [ ('ubuntu', '16.', set(['libnss_dns'])), (None, '7.', set(['libnss_dns'])), ] # type: List[Tuple[Optional[str], str, Set[str]]] @utils.cached def _get_effective_libcare_libs(): # type: () -> List[str] libs = list(config.LIBCARE_LIBS) blacklisted = set() # type: Set[str] try: distro_info = platform_utils.get_distro() distro = distro_info[0].lower() version = distro_info[1] except Exception: # Distro unknown: exclude the union of all blacklisted libs so we # never ship a lib that would conflict on *some* affected distro. for _, _, bl_libs in LIBCARE_LIBS_BLACKLIST: blacklisted |= bl_libs else: # Append a trailing dot so a bare major version like '7' matches # a prefix like '7.' without also matching '70'. version_dotted = version + '.' for bl_distro, bl_version, bl_libs in LIBCARE_LIBS_BLACKLIST: if (bl_distro is None or bl_distro in distro) and version_dotted.startswith(bl_version): blacklisted = bl_libs break return [lib for lib in libs if lib not in blacklisted] def get_userspace_map(): # type: () -> Dict[str, List[str]] return { 'db': ['mysqld', 'mariadbd', 'postgres'], 'libs': _get_effective_libcare_libs(), } def get_userspace_cache_path(libname, *parts): return os.path.join(constants.PATCH_CACHE, 'userspace', libname, *parts) def clear_libcare_cache(clbl): def wrapper(*args, **kwargs): try: return clbl(*args, **kwargs) finally: try: libcare_client('clearcache') except Exception as err: # We don't want to show the error to the user but want to see it in logs log_utils.logerror("Libcare cache clearing failed: '{0}'".format(err), print_msg=False) return wrapper class UserspacePatchLevel(int): def __new__(cls, libname, buildid, level, baseurl=None): return super(cls, cls).__new__(cls, level) def __init__(self, libname, buildid, level, baseurl=None): self.level = level self.libname = libname self.buildid = buildid self.baseurl = baseurl def cache_path(self, *parts): return get_userspace_cache_path(self.libname, self.buildid, str(self), *parts) def refresh_applied_patches_list(clbl): def save_current_state(info): """KPT-1543 Save info about applied patches""" versions, cves = '', '' try: if info is None: info = _libcare_info() packages = {} all_patches = [] for rec in _get_patches_info(info): packages[rec.get('package')] = rec.get('latest-version', '') all_patches.extend(rec.get('patches', [])) cves_set = utils.extract_unique_cves(all_patches, cve_field='cve') versions = '\n'.join([' '.join(rec) for rec in packages.items()]) cves = '\n'.join(sorted(cves_set)) # sort for consistent output finally: utils.atomic_write(LIBCARE_PATCHES, versions, ensure_dir=True) utils.atomic_write(LIBCARE_CVE_LIST, cves, ensure_dir=True) def wrapper(*args, **kwargs): info = None try: info = clbl(*args, **kwargs) return info finally: save_current_state(info) return wrapper def fetch_userspace_patch(libname, build_id, patch_level=None, tag=None): prefix = config.PREFIX or 'main' libname = urlquote(libname) build_id = urlquote(build_id.strip()) url = utils.get_patch_server_url(LIBNAME_MAP.get(libname, 'u'), prefix, libname, build_id, 'latest.v1') url += '?info=' + server_info.encoded_server_lib_info('update', patch_level) cache_dst = LIBNAME_MAP.get(libname, 'libs') if tag: cache_dst = os.path.join('tags', tag, cache_dst) extra_kwargs = {} if tag: extra_kwargs['headers'] = {'Sticky': tag} try: response = fetch.wrap_with_cache_key(auth.urlopen_auth)(url, check_license=False, **extra_kwargs) except errors.NotFound: if tag: # In transient tag mode the user explicitly asked for a snapshot; # libraries without a patchset on that date are simply not patched # - skip silently instead of failing the whole update. log_utils.loginfo( 'No libcare patchset available for {0} at tag {1}, skipping.'.format(libname, tag), print_msg=False, ) return None # There is no latest info, so we need to clear cache for corresponding # build_id to prevent updates by "-ctl" utility. shutil.rmtree(get_userspace_cache_path(cache_dst, build_id), ignore_errors=True) raise except HTTPError as ex: if ex.code == 400 and tag: raise errors.KcareError( 'Invalid sticky patch tag {0}: server rejected the value.'.format(tag), status='invalid lib sticky patch', ) raise config_handlers.set_feature_flags_from_headers(response.headers) meta = json_loads_nstr(utils.nstr(response.read())) required_capabilities = meta.get('capabilities', []) if not capabilities.has_lc_capabilities(required_capabilities): raise errors.CapabilitiesMismatch( 'Latest LibCare patchset for {0} is incompatible ' 'with the current kernecare package version, please upgrade.'.format(libname) ) level = UserspacePatchLevel(cache_dst, build_id, meta['level'], meta.get('baseurl')) plevel = str(meta['level']) patch_path = get_userspace_cache_path(cache_dst, build_id, plevel, 'patch.tar.gz') if not os.path.exists(patch_path) or os.path.getsize(patch_path) == 0: url = utils.get_patch_server_url(meta['patch_url']) try: fetch.fetch_url(url, patch_path, check_signature=config.USE_SIGNATURE, hash_checker=fetch.get_hash_checker(level)) except HTTPError as ex: # No license - no access if ex.code in (403, 401): raise errors.NoLibcareLicenseException('KC+ licence is required') raise dst = get_userspace_cache_path(cache_dst, build_id, plevel) cmd = ['tar', 'xf', patch_path, '-C', dst, '--no-same-owner'] code, stdout, stderr = process_utils.run_command(cmd, catch_stdout=True, catch_stderr=True) if code: raise errors.KcareError( "Patches unpacking error: '{0}' '{1}' {2}".format(stderr, stdout, code), status='patches unpacking error' ) link_name = get_userspace_cache_path(cache_dst, build_id, 'latest') if not os.path.islink(link_name) and os.path.isdir(link_name): shutil.rmtree(link_name) os.symlink(plevel, link_name + '.tmp') os.rename(link_name + '.tmp', link_name) return level def set_libcare_status(enabled): config.LIBCARE_DISABLED = not enabled if not enabled: libcare_server_stop() config_handlers.update_config(LIBCARE_DISABLED=('FALSE' if enabled else 'YES')) if enabled: libcare_server_start() log_utils.kcarelog.info('libcare service is ' + ('enabled' if enabled else 'disabled')) def libcare_server_stop(): # Stop the socket first to break cascading restart loops on # systemd 219 (CentOS 7) where a pending connection in the # socket backlog re-activates a failed service. if constants.SKIP_SYSTEMCTL_CHECK or os.path.exists(constants.SYSTEMCTL): process_utils.run_command([constants.SYSTEMCTL, 'stop', 'libcare.socket']) process_utils.run_command([constants.SYSTEMCTL, 'stop', 'libcare.service']) process_utils.run_command([constants.SYSTEMCTL, 'reset-failed', 'libcare']) process_utils.run_command([constants.SYSTEMCTL, 'reset-failed', 'libcare.socket']) else: # pragma: no cover unit try: cmd = [process_utils.find_cmd('service', ('/usr/sbin/', '/sbin/')), 'libcare', 'stop'] except Exception: return process_utils.run_command(cmd) def libcare_server_start(): # KPT-2884: skip restart when libcare.service is already active -- systemd # refuses to activate a socket whose service is already running if constants.SKIP_SYSTEMCTL_CHECK or os.path.exists(constants.SYSTEMCTL): code, _, _ = process_utils.run_command( [constants.SYSTEMCTL, 'is-active', '--quiet', 'libcare.service'], catch_stdout=True, catch_stderr=True, ) if code == 0: return # Break out of a possible cascading restart loop on systemd 219 # (CentOS 7) by stopping socket first, then service, then clearing # failed state. process_utils.run_command([constants.SYSTEMCTL, 'stop', 'libcare.socket']) process_utils.run_command([constants.SYSTEMCTL, 'stop', 'libcare.service']) process_utils.run_command([constants.SYSTEMCTL, 'reset-failed', 'libcare']) process_utils.run_command([constants.SYSTEMCTL, 'reset-failed', 'libcare.socket']) process_utils.run_command([constants.SYSTEMCTL, 'start', 'libcare.socket']) else: if libcare_server_started(): return try: cmd = [process_utils.find_cmd('service', ('/usr/sbin/', '/sbin/')), 'libcare', 'start'] except Exception: # pragma: no cover unit return process_utils.run_command(cmd) def _libcare_info(patched=True, limit=None): # Escape each name: library/process names contain regex metacharacters # (e.g. '.'/'+' in libstdc++ and libc-2.17.so, or '()' in # 'libcrypto.so.1.0.1e (deleted)') that must match literally, not as a regex. regexp = '|'.join("({0})".format(re.escape(proc)) for proc in sorted(limit or [])) cmd = ['info', '-j'] if not patched: cmd += ['-l', '-r', regexp] try: lines = libcare_client(*cmd) except Exception as err: raise errors.KcareError("Gathering userspace libraries info error: '{0}'".format(err), status='userspace libs info error') result = [] for line in lines.split('\n'): if line: try: result.append(json.loads(line)) except ValueError: # We have to do that because socket's output isn't separated to stderr and stdout # so there are chances that will be non-json lines pass # FIXME: remove that libe when library names will be separated to lower # level from process name and pid result = [{'comm': line.pop('comm'), 'pid': line.pop('pid'), 'libs': line} for line in result] for line in result: line['libs'] = dict((k, v) for k, v in line['libs'].items() if ('patchlvl' in v or not patched)) return result def _get_patches_info(info): patches = set() for rec in info: for _, data in rec['libs'].items(): patches.add((data['buildid'], data['patchlvl'])) result = [] umap = get_userspace_map() for cache_dst in umap: for build_id, patchlvl in patches: patch_info_filename = get_userspace_cache_path(cache_dst, build_id, str(patchlvl), 'info.json') if os.path.isfile(patch_info_filename): with open(patch_info_filename, 'r') as fd: result.append(json.load(fd)) return result @clear_libcare_cache def libcare_patch_info_basic(): return _get_patches_info(_libcare_info()) @clear_libcare_cache def libcare_patch_info(): result = libcare_patch_info_basic() if not result: log_utils.logerror("No patched processes.") return json.dumps({'result': result}) @clear_libcare_cache def libcare_info(): result = _libcare_info() if not result: log_utils.logerror("No patched processes.") return json.dumps({'result': result}) def _libcare_version(): result = {} for rec in libcare_patch_info_basic(): result[rec.get('package')] = rec.get('latest-version', '') return result def libcare_version(libname): for package, version in _libcare_version().items(): if libname.startswith(package): return version return '' def libcare_client_format(params): return b''.join(utils.bstr(p) + b'\0' for p in params) + b'\0' def get_available_libcare_socket(): for libcare_socket in LIBCARE_SOCKET: if os.path.exists(libcare_socket): return libcare_socket raise errors.KcareError("Libcare socket is not found.") def libcare_client(*params): if config.LIBCARE_DISABLED: raise errors.KcareError('Libcare is disabled.') sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0) sock.settimeout(10) # connect timeout res = b'' try: sock.connect(get_available_libcare_socket()) sock.settimeout(config.LIBCARE_SOCKET_TIMEOUT) cmd = libcare_client_format(params) log_utils.logdebug("Libcare socket send: {cmd}".format(cmd=params)) sock.sendall(cmd) while True: data = sock.recv(4096) if not data: break res += data result = res.decode('utf-8', 'replace') log_utils.logdebug("Libcare socket recieved: {result}".format(result=result)) return result finally: sock.close() def libcare_patch_apply(limit, tag=None): if tag: # Atomic apply from an isolated, tag-specific storage. Leaves the # default storage untouched, so a transient `--lib-tag` update does # not interfere with the normal `--lib-update` flow. for dst in limit: storage_path = get_userspace_cache_path('tags', tag, dst) # check_userspace_updates only creates the tag-specific storage # directory for groups that had running processes with available # patchsets. If a group (e.g. 'db' on a host without a DB) had # nothing fetched, the directory does not exist - skip the apply # for it explicitly so we don't rely on libcare-server's # behavior of returning 0 on "cannot open storage". if not os.path.isdir(storage_path): continue try: libcare_client('update', '--storage', storage_path) except Exception as err: raise errors.KcareError( "Userspace patch applying error (storage={0}): '{1}'".format(storage_path, err), status='userspace patch apply error', ) return for dst in limit: try: libcare_client('storage', get_userspace_cache_path(dst)) except Exception as err: raise errors.KcareError("Userspace storage switching error: '{0}'".format(err), status='userspace storage switch error') try: libcare_client('update') except Exception as err: raise errors.KcareError("Userspace patch applying error: '{0}'".format(err), status='userspace patch apply error') @clear_libcare_cache @refresh_applied_patches_list @process_utils.log_all_parent_processes def libcare_unload(): try: libcare_client('unload') except Exception as err: raise errors.KcareError("Userspace patch unloading error: '{0}'".format(err), status='userspace patch unload error') def libcare_replugin(): # pragma: no cover unit """Reload libcare-server plugin without restarting the server.""" try: libcare_client('replugin') except Exception as err: raise errors.KcareError("Userspace replugin error: '{0}'".format(err)) # log_all_parent_processes must be outermost to run first @process_utils.log_all_parent_processes @update_utils.track_update_status('libcare') @selinux.skip_if_no_selinux_module @clear_libcare_cache @refresh_applied_patches_list def do_userspace_update(mode=constants.UPDATE_MODE_MANUAL, limit=None, tag=None): """Patch userspace processes to the latest version.""" rotate_libcare_logs() # Auto-update means cron-initiated run and if no # LIB_AUTO_UPDATE flag in the config - nothing will happen. if mode == constants.UPDATE_MODE_AUTO and not config.LIB_AUTO_UPDATE: return None umap = get_userspace_map() if limit is None: limit = list(umap.keys()) process_filter = [] for userspace_patch in limit: process_filter.extend(umap.get(userspace_patch, [])) if not process_filter: # Unknown limits were defined. Do nothing log_utils.loginfo('No such userspace patches: {0}'.format(limit)) return None failed, something_found, _, before = check_userspace_updates(limit=process_filter, tag=tag) if failed: raise errors.KcareError('There were errors while patches downloading (unpacking).') if not something_found: log_utils.loginfo('No patches were found.') return None selinux.restore_selinux_context(os.path.join(constants.PATCH_CACHE, 'userspace')) rotate_libcare_logs() try: # Batch apply for all collected patches libcare_patch_apply(limit, tag=tag) # TODO: clear userspace cache. We need the same logic as for kernel, lets do # it later to reduce this patch size. except errors.KcareError as ex: log_utils.logerror(str(ex)) raise errors.KcareError('There were errors while patches applying.') data_after = _libcare_info() after = _get_userspace_procs(data_after) if not any(list(item['libs'] for item in data_after)): # No patches were applied return None # Info on how many patches were actually patched via before and after diff log_utils.logdebug("Patched before: {before}".format(before=before)) log_utils.logdebug("Patched after: {after}".format(after=after)) uniq_procs_after = set(v for items in after.values() for v in items) uniq_procs_before = set(v for items in before.values() for v in items) diff = uniq_procs_after - uniq_procs_before overall = sum(len(v) for v in after.values()) log_utils.loginfo( "The patches have been successfully applied to {count} newly " "discovered processes. The overall amount of applied patches " "is {overall}.".format(count=len(diff), overall=overall) ) for k, v in after.items(): log_utils.loginfo("Object `{0}` is patched for {1} processes.".format(k, len(v))) return data_after @clear_libcare_cache def get_userspace_update_status(): try: failed, _, libs_not_patched, _ = check_userspace_updates() except errors.KcareError: return 3 finally: rotate_libcare_logs() if failed: return 3 if libs_not_patched: return 1 return 2 if update_utils.status_gap_passed(filename='.libcarestatus') else 0 def _get_userspace_procs(info): result = {} # type: Dict[str, List[Tuple[int, str]]] for item in info: for libname, rec in item['libs'].items(): if rec.get('patchlvl'): if libname not in result: result[libname] = [] result[libname].append((item['pid'], item['comm'])) return result def _get_userspace_libs(info): result = set() for item in info: for libname, rec in item['libs'].items(): result.add((libname, rec['buildid'], rec.get('patchlvl', 0))) return result def check_userspace_updates(limit=None, tag=None): if not limit: umap = get_userspace_map() limit = [] [limit.extend(libs) for libs in umap.values()] data_before = _libcare_info(patched=False, limit=limit) before = _get_userspace_procs(data_before) failed = something_found = False libs_not_patched = True for rec in _get_userspace_libs(data_before): # Download and unpack patches libname, build_id, patchlvl = rec try: result = fetch_userspace_patch(libname, build_id, patchlvl, tag=tag) # In tag mode a None result means the patchset was missing for # this library on the requested date - that's a legitimate skip, # not a "found update". if tag and result is None: continue something_found = True if patchlvl != 0: libs_not_patched = False except errors.CapabilitiesMismatch as e: failed = True log_utils.logwarn(str(e)) except (errors.NotFound, errors.NoLibcareLicenseException): pass except errors.AlreadyTrialedException: raise except errors.KcareError as ex: # An invalid sticky tag is rejected by the server for every # build_id (the 400 is a property of the tag value, not of # the library). Continuing the loop would just hammer the # server with N identical 400s and bury the specific error # under a generic "There were errors while patches downloading" # at the end. Fail fast with the original message instead, # mirroring AlreadyTrialedException handling above. if getattr(ex, 'status', None) == 'invalid lib sticky patch': raise failed = True log_utils.logerror(str(ex)) update_utils.touch_status_gap_file(filename='.libcarestatus') return failed, something_found, libs_not_patched, before def rotate_libcare_logs(): rc = 0 stderr = '' logrotate_path = process_utils.find_cmd('logrotate', raise_exc=False) if logrotate_path: try: rc, _, stderr = process_utils.run_command([logrotate_path, LIBCARE_LOGROTATE_CONFIG], catch_stderr=True) except Exception as e: rc = 1 stderr = str(e) if rc: log_utils.logerror('failed to run logrotate for libcare logs, stderr: {0}'.format(stderr), print_msg=False) else: log_utils.logwarn("logrotate utility wasn't found", print_msg=False) libcare_log_directory = '/var/log/libcare/' if not os.path.isdir(libcare_log_directory): return max_total_size = config.LIBCARE_PIDLOGS_MAX_TOTAL_SIZE_MB * (1024**2) try: log_files = os.listdir(libcare_log_directory) pidlog_re = re.compile(r'^\d+\.log.*') # both .log and .log.x.gz pidlog_files = [os.path.join(libcare_log_directory, fn) for fn in log_files if pidlog_re.match(fn)] pidlog_files_with_ct = [(os.path.getctime(fp), fp) for fp in pidlog_files] pidlog_files_with_ct.sort(reverse=True) # newest files first # delete old files if we have overflow total_size = 0 for _, filepath in pidlog_files_with_ct: total_size += os.path.getsize(filepath) if total_size >= max_total_size: os.remove(filepath) log_utils.kcarelog.info('Removed %s because of logs size limit', filepath) except Exception: # pragma: no cover log_utils.logexc('Failed to cleanup libcare server logfiles', print_msg=False) def libcare_server_started(): """Assume that whenever the service is not running, we did not patch anything.""" try: cmd = [process_utils.find_cmd('service', ('/usr/sbin/', '/sbin/')), 'libcare', 'status'] except Exception: # pragma: no cover unit return False code, _, _ = process_utils.run_command(cmd, catch_stdout=True, catch_stderr=True) return code == 0 auth.py000064400000027732152533440750006102 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import base64 import os import re import time from . import config, constants, errors, http_utils, ipv6_support, log_utils, platform_utils, serverid, utils from .py23 import HTTPError, URLError, urlencode def unregister(silent=False): url = None try: server_id = serverid.get_serverid() if server_id is None: if not silent: log_utils.logerror('Error unregistering server: cannot find server id') return url = ipv6_support.get_registration_url() + '/unregister_server.plain?server_id={0}'.format(server_id) response = http_utils.urlopen(url) content = utils.nstr(response.read()) res = utils.data_as_dict(content) if res['success'] == 'true': serverid.rm_serverid() # License state transitioned (licensed -> none); the cached ipv6 decision # may have been "ipv4 because server id was found" and no longer applies. ipv6_support.clear_cache() if not silent: log_utils.loginfo('Server was unregistered') elif not silent: log_utils.logerror(content) log_utils.logerror('Error unregistering server: ' + res['message']) except HTTPError as e: if not silent: log_utils.print_cln_http_error(e, url) def _register_retry(url): # pragma: no cover unit utils.print_wrapper('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 = open('/dev/null', 'r') so = open('/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, auth_token = _try_register(url) if code == 0 and server_id: serverid.set_server_id(server_id) _set_auth_token(auth_token) # Daemonized retry: this success runs in a forked child hours after # register() returned, so it can't piggy-back on the synchronous # clear_cache() in register() -- it has to invalidate the cache itself. ipv6_support.clear_cache() sys.exit(0) def _validate_urlsafe_encoding(value): if value is not None and not re.match(r'^[\w.-]+$', value): raise ValueError('Invalid value received: %s' % value) return value def _try_register(url): try: response = http_utils.urlopen(url) auth_token = response.headers.get(constants.AUTH_TOKEN_HEADER, None) res = utils.data_as_dict(utils.nstr(response.read())) return int(res['code']), _validate_urlsafe_encoding(res.get('server_id')), _validate_urlsafe_encoding(auth_token) except (HTTPError, URLError) as e: log_utils.print_cln_http_error(e, url) return None, None, None except Exception: log_utils.kcarelog.exception('Exception while trying to register URL %s' % url) return None, None, None def register(key, retry=False): try: unregister(True) except Exception: log_utils.kcarelog.exception('Exception while trying to unregister URL before register.') hostname = platform_utils.get_hostname() query = urlencode([('hostname', hostname), ('key', key)]) url = '{0}/register_server.plain?{1}'.format(ipv6_support.get_registration_url(), query) code, server_id, auth_token = _try_register(url) if code == 0: serverid.set_server_id(server_id) _set_auth_token(auth_token) # License state transitioned (none -> licensed) and a serverid now exists, # so the next is_ipv6_preferred() will hit a different branch than the # cached "no license yet" decision -- invalidate after the transition, # not before (re-evaluating pre-register would just repeat the probes # and return the same answer since no license exists yet). ipv6_support.clear_cache() log_utils.loginfo('Server Registered') return 0 elif code == 1: log_utils.logerror('Account Locked') elif code == 2: log_utils.logerror('Invalid Key') elif code == 3: log_utils.logerror( 'You have reached maximum registered servers for this key. ' 'Please go to your CLN account, remove unused servers and try again.' ) elif code == 4: log_utils.logerror('IP is not allowed. Please change allowed IP ranges for the key in KernelCare Key tab in CLN') elif code == 5: log_utils.logerror('This IP was already used for trial, you cannot use it for trial again') elif code == 6: log_utils.logerror('This IP was banned. Please contact support for more information at https://www.kernelcare.com/support/') else: log_utils.logerror('Unknown Error {0}'.format(code)) if retry: # pragma: no cover _register_retry(url) return 0 return code or -1 @utils.cached def _get_auth_token(): return utils.try_to_read(constants.AUTH_TOKEN_DUMP_PATH) def _set_auth_token(auth_token): if not auth_token: return utils.atomic_write(constants.AUTH_TOKEN_DUMP_PATH, auth_token) def urlopen_auth(url, *args, **kwargs): method = kwargs.pop('method', None) if kwargs.pop('check_license', True): check = _check_auth_retry else: check = http_utils.check_urlopen_retry if http_utils.is_local_url(url): return http_utils.urlopen_base(url, *args, **kwargs) request = http_utils.http_request(url, get_http_auth_string(), _get_auth_token(), method=method) return utils.retry(check, count=8)(http_utils.urlopen_base)(request, *args, **kwargs) def get_http_auth_string(): server_id = serverid.get_serverid() if server_id: return utils.nstr(base64.b64encode(utils.bstr('{0}:{1}'.format(server_id, 'kernelcare')))) return None def _check_auth_retry(e, state): if isinstance(e, HTTPError) and e.code in (403, 401): return _handle_forbidden(state) return http_utils.check_urlopen_retry(e, state) def _handle_forbidden(state): """In case of 403 error we should check what's 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. """ if 'license' in state: # license has already been checked and is valid, no need to ask CLN again return True if config.CHECK_CLN_LICENSE_STATUS: server_id = serverid.get_serverid() url = ipv6_support.get_registration_url() + '/check.plain' if server_id: url += '?server_id={0}'.format(server_id) try: # do not retry in case of 500 from CLN! # otherwise, CLN will die in pain because of too many requests content = utils.nstr(http_utils.urlopen(url, retry_on_500=False).read()) info = utils.data_as_dict(content) except URLError as ex: log_utils.print_cln_http_error(ex, url, stdout=False) return if not info or not info.get('code'): log_utils.kcarelog.error('Unexpected CLN response: {0}'.format(content)) return if info['code'] in ['0', '1']: # license is fine: 0 - valid license, 1 - valid trial license; # looks like htpasswd not updated yet; # mark state as licensed to avoid repeated requests to CLN state['license'] = True log_utils.logerror('Unable to access server. Retrying...') return True else: _register_trial() def license_info(): server_id = serverid.get_serverid() if server_id: url = ipv6_support.get_registration_url() + '/check.plain?server_id={0}'.format(server_id) try: response = http_utils.urlopen(url) content = utils.nstr(response.read()) res = utils.data_as_dict(content) if not res or not res.get('code'): utils.print_wrapper('Unexpected CLN response: {0}'.format(content)) return 1 code = int(res['code']) if code == 0: utils.print_wrapper('Key-based valid license found') return 1 else: license_type = _get_license_info_by_ip(key_checked=1) if license_type == 0: utils.print_wrapper('No valid key-based license found') return license_type except URLError as e: log_utils.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 = ipv6_support.get_registration_url() + '/check.plain' try: response = http_utils.urlopen(url) content = utils.nstr(response.read()) res = utils.data_as_dict(content) if res['success'].lower() == 'true': code = int(res['code']) if code == 0: utils.print_wrapper('Valid license found for IP {0}'.format(res['ip'])) return 1 # valid license if code == 1: ip = res['ip'] expires_str = utils.parse_response_date(res['expire_date']).strftime('%Y-%m-%d') utils.print_wrapper('You have a trial license for the IP {0} that will expire on {1}'.format(ip, expires_str)) return 2 # trial license if code == 2 and key_checked == 0: ip = res['ip'] expires_str = utils.parse_response_date(res['expire_date']).strftime('%Y-%m-%d') utils.print_wrapper('Your trial license for the IP {0} expired on {1}'.format(ip, expires_str)) if code == 3 and key_checked == 0: if 'ip' in res: utils.print_wrapper("The IP {0} hasn't been licensed".format(res['ip'])) else: utils.print_wrapper("This server hasn't been licensed") else: message = res.get('message', '') utils.print_wrapper('Error retrieving license info: {0}'.format(message)) except URLError as e: log_utils.print_cln_http_error(e, url) except KeyError as key: utils.print_wrapper('Unexpected CLN response, cannot find {0} key:\n{1}'.format(key, content.strip())) return 0 # no valid license def _register_trial(): trial_mark = os.path.join(constants.PATCH_CACHE, 'trial-requested') if os.path.exists(trial_mark): return try: response = http_utils.urlopen(ipv6_support.get_registration_url() + '/trial.plain') res = utils.data_as_dict(utils.nstr(response.read())) try: if res['success'].lower() == 'true': utils.atomic_write(trial_mark, '', ensure_dir=True) # Trial just granted: license state flipped (none -> trial), so # the cached ipv4 decision may no longer reflect the right branch # in is_ipv6_preferred(). ipv6_support.clear_cache() if res['expired'] == 'true': raise errors.AlreadyTrialedException(res['ip'], res['created']) log_utils.loginfo('Requesting trial license for IP {0}. Please wait...'.format(res['ip'])) return None elif res['success'] == 'na': utils.atomic_write(trial_mark, '', ensure_dir=True) raise errors.KcareError('Invalid License') else: # TODO: make sane exception messages raise errors.UnableToGetLicenseException(-1) # Invalid response? except KeyError as ke: raise errors.UnableToGetLicenseException(ke) except HTTPError as e: raise errors.UnableToGetLicenseException(e.code) errors.py000064400000003761152533440750006451 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT from .py23 import HTTPError class SafeExceptionWrapper(Exception): def __init__(self, inner, etype=None, details=None): self.inner = inner self.etype = etype self.details = details class KcareError(Exception): """Base kernelcare exception which will be considered as expected error and the full traceback will not be shown. Subclasses may set a class-level ``status`` to provide a short, fixed label for error reporting. Individual raise sites can override it per-instance via the ``status`` kwarg. """ status = '' # type: str def __init__(self, *args, **kwargs): # type: (*object, **object) -> None status = kwargs.pop('status', None) if status is not None: self.status = str(status) super(KcareError, self).__init__(*args) class NotFound(HTTPError): pass class NoLibcareLicenseException(KcareError): status = 'no libcare license' class CapabilitiesMismatch(KcareError): status = 'capabilities mismatch' class AlreadyTrialedException(KcareError): status = 'already trialed' 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 {0} was already used for a trial license on {1}'.format(self.ip, self.created) class UnableToGetLicenseException(KcareError): status = 'unable to get license' def __init__(self, code, **kwargs): super(UnableToGetLicenseException, self).__init__( 'Unknown Issue when getting trial license. Error code: ' + str(code), **kwargs ) class BadSignatureException(KcareError): status = 'bad signature' def check_exc(*exc_list): def inner(e, state): return isinstance(e, exc_list) return inner platform_utils.py000064400000024006152533440750010174 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import base64 import json import os import platform import re import socket import sys from . import config, constants, log_utils, process_utils, selinux, utils if False: # pragma: no cover from typing import Any, Dict, Optional, Tuple # noqa: F401 VIRTWHAT = '/usr/libexec/kcare/virt-what' PROC_DIR = '/proc' def get_distro(): if sys.version_info[:2] < (3, 6): # pragma: no py3 cover return platform.linux_distribution() else: # pragma: no distro cover import distro return distro.linux_distribution(full_distribution_name=False) @utils.cached def get_system_uname(): return platform.uname()[2] def get_python_version(): # type: () -> str return '%s.%s' % (sys.version_info[0], sys.version_info[1]) def app_info(is_json=False): # type: (bool) -> str info = { 'python_version': get_python_version(), 'agent_version': constants.VERSION, } if selinux.is_selinux_enabled(): rc, stdout, stderr = process_utils.run_command(['ps', '-Z', '--no-headers', '--pid', str(os.getpid())], catch_stdout=True) if not rc: selinux_context = stdout.split()[0] else: selinux_context = 'error: %s' % stderr info['selinux_context'] = selinux_context if is_json: return json.dumps(info) info_keys = sorted(info) info_str = '' for info_key in info_keys: info_str += '%s: %s\n' % (info_key, info[info_key]) return info_str.rstrip() EFIVARS_PATH = '/sys/firmware/efi/efivars' EFI_VENDORS = { 'global': '8be4df61-93ca-11d2-aa0d-00e098032b8c', 'shim': '605dab50-e046-4300-abb6-3dd810dd8b23', } def _read_uefi_var(name, vendor, max_bytes=256): # type: (str, str, Optional[int]) -> Optional[bytes] var_path = os.path.join(EFIVARS_PATH, '%s-%s' % (name, vendor)) if not os.path.exists(var_path): return None with open(var_path, 'rb') as var: return var.read(max_bytes) def is_secure_boot(): # mocked: tests/unit/test_load_kmod.py # type: () -> bool try: secure_boot_var = _read_uefi_var('SecureBoot', EFI_VENDORS['global']) if secure_boot_var: return secure_boot_var[-1:] == b'\x01' # Get last byte except Exception: # pragma: no cover pass return False def _get_uefi_var_encoded(name, vendor): # type: (str, str) -> Optional[str] try: value_bytes = _read_uefi_var(name, vendor) if value_bytes is None: return None except Exception as e: value_bytes = str(e).encode() return utils.nstr(base64.urlsafe_b64encode(value_bytes)) def secure_boot_info(): # type: () -> dict[str, Any] cmdline = utils.try_to_read(os.path.join(PROC_DIR, 'cmdline')) if cmdline and len(cmdline) > 1024: # pragma: no cover cmdline = cmdline[:1024] info = {'cmdline': cmdline, 'has_efi': os.path.exists(os.path.dirname(EFIVARS_PATH))} # type: dict[str, Any] if not info['has_efi']: return info try: info['global'] = dict((var, _get_uefi_var_encoded(var, EFI_VENDORS['global'])) for var in ('SecureBoot', 'SetupMode')) shim_vars = sorted( [var[0 : -len(EFI_VENDORS['shim']) - 1] for var in os.listdir(EFIVARS_PATH) if var.endswith(EFI_VENDORS['shim'])] ) info['shim'] = {'vars': shim_vars} shim_exclude_vars = set(['MokListRT', 'MokListXRT', 'MokListTrustedRT', 'SbatLevelRT']) for var in shim_vars: if var in ('HSIStatus', 'MokIgnoreDB') or (var.endswith('RT') and var not in shim_exclude_vars): info['shim'][var] = _get_uefi_var_encoded(var, EFI_VENDORS['shim']) except Exception as err: log_utils.logwarn(str(err)) return info @utils.cached def get_hostname(): # type: () -> str # KCARE-1165 If fqdn gathering is forced if config.REPORT_FQDN: try: # getaddrinfo() -> [(family, socktypeget_hostname, proto, canonname, sockaddr), ...] hostname = socket.getaddrinfo(socket.gethostname(), 0, 0, 0, 0, socket.AI_CANONNAME)[0][3] except socket.gaierror as ge: log_utils.logerror(ge) hostname = platform.node() else: hostname = platform.node() return hostname @utils.cached def get_uptime(): # type: () -> str uptime_file = os.path.join(PROC_DIR, 'uptime') if os.path.isfile(uptime_file): f = open(uptime_file, 'r') line = f.readline() result = str(int(float(line.split()[0]))) f.close() return result return '-1' @utils.cached def get_virt(): if os.path.isfile(VIRTWHAT): return process_utils.check_output([VIRTWHAT]).strip() return 'no-virt-what' # pragma: no cover def is_cpanel(): # type: () -> bool return os.path.isfile('/usr/local/cpanel/cpanel') def is_plesk(): # type: () -> bool return os.path.isdir('/usr/local/psa/admin/') def is_interworx(): # type: () -> bool return os.path.isdir('/usr/local/interworx/') def is_ispmanager(): # type: () -> bool return os.path.isdir('/usr/local/ispmgr/') def is_directadmin(): # type: () -> bool return os.path.isdir('/usr/local/directadmin/plugins/') def is_hosting_controller(): # type: () -> bool return os.path.isdir('/usr/local/hostingcontroller/') def is_hsphere(): # type: () -> bool return os.path.isdir('/hsphere/shared') def has_softaculous(): # type: () -> bool """Softaculous markers checked by the kcdoctor.sh detect_cp()""" return any( os.path.exists(path) for path in ( '/usr/local/softaculous', '/usr/local/cpanel/whostmgr/cgi/softaculous', '/usr/local/directadmin/plugins/softaculous', ) ) # control panels reported by the doctor report, ported from the # kcdoctor.sh detect_cp() probes (presence only, no version detection) CONTROL_PANEL_PROBES = ( ('Plesk', is_plesk), ('cPanel', is_cpanel), ('InterWorx', is_interworx), ('ISPmanager', is_ispmanager), ('DirectAdmin', is_directadmin), ('Hosting Controller', is_hosting_controller), ('H-Sphere', is_hsphere), ) def inside_vz_container(): # mocked: tests/unit/test_load_kmod.py return os.path.exists(os.path.join(PROC_DIR, 'vz', 'veinfo')) and not os.path.exists(os.path.join(PROC_DIR, 'vz', 'version')) def inside_lxc_container(): # mocked: tests/unit/test_load_kmod.py return '/lxc/' in open(os.path.join(PROC_DIR, '1', 'cgroup')).read() def inside_docker_container(): # mocked: tests/unit/test_load_kmod.py return os.path.isfile('/.dockerenv') @utils.catch_errors(logger=log_utils.logwarn) def get_load_average(): # type: () -> Optional[Tuple[float, float, float]] loadavg = utils.try_to_read(os.path.join(PROC_DIR, 'loadavg')) if not loadavg: return None m1, m5, m15, _ = loadavg.split(' ', 3) return (float(m1), float(m5), float(m15)) @utils.catch_errors(logger=log_utils.logwarn) def get_mem_info(): # type: () -> Optional[Dict[str, int]] """Returns dict of memory info in kB""" meminfo = utils.try_to_read(os.path.join(PROC_DIR, 'meminfo')) if not meminfo: return None filter_params = ('MemTotal', 'MemFree', 'SwapTotal', 'SwapFree') # optional units are ignored (assumed to be always kB for mem size) return dict((k, int(v)) for k, v in (re.split(r'[\s:]+', line)[:2] for line in meminfo.splitlines()) if k in filter_params) @utils.cached @utils.catch_errors(logger=log_utils.logwarn) def get_cpu_info(): # type: () -> Optional[Dict[str, Any]] cpuinfo = utils.try_to_read(os.path.join(PROC_DIR, 'cpuinfo')) if not cpuinfo: return None cpus = [ dict(re.split(r'\s*:\s*', line) for line in cpu_lines.splitlines()) for cpu_lines in cpuinfo.split('\n\n') # cpu records are separated by an empty line if cpu_lines ] return { 'logical_cores': len(cpus), 'physical_cores': len(set((cpu.get('physical id'), cpu.get('core id')) for cpu in cpus)), 'vendor_id': cpus[0].get('vendor_id'), 'model': int(cpus[0].get('model', 0)), 'model_name': cpus[0].get('model name'), 'cpu_family': int(cpus[0].get('cpu family', 0)), 'stepping': int(cpus[0].get('stepping', 0)), 'microcode': cpus[0].get('microcode'), 'flags': cpus[0].get('flags', '').split(), } @utils.catch_errors(logger=log_utils.logwarn, default_return=0) def get_process_count(): # type: () -> int return len([d for d in os.listdir(PROC_DIR) if d.isdigit()]) @utils.catch_errors(logger=log_utils.logwarn, default_return=0) def get_opened_files_count(): # type: () -> int fd_info = utils.try_to_read(os.path.join(PROC_DIR, 'sys/fs/file-nr')) return int((fd_info or '0').split()[0]) @utils.catch_errors(logger=log_utils.logwarn) def get_vm_count_kvm(): # type: () -> Optional[int] for _root, dirs, _files in os.walk('/sys/kernel/debug/kvm'): return len(dirs) return None @utils.catch_errors(logger=log_utils.logwarn, default_return=(0, 0)) def get_network_connections_count(): # type: () -> Tuple[int, int] """Return tuple of total numbers of TCP and UDP connections""" def conn_records_count(proto): # type: (str) -> int records = utils.try_to_read(os.path.join(PROC_DIR, 'net', proto)) if not records: return 0 return max(len(records.splitlines()) - 1, 0) return conn_records_count('tcp') + conn_records_count('tcp6'), conn_records_count('udp') + conn_records_count('udp6') def get_performance_metrics(): # type: () -> Dict[str, Any] conn_tcp, conn_udp = get_network_connections_count() return { 'load_average': get_load_average(), 'mem_info': get_mem_info(), 'cpu_info': get_cpu_info(), 'vm_count': get_vm_count_kvm(), 'processes': get_process_count(), 'open_files': get_opened_files_count(), 'tcp_connections': conn_tcp, 'udp_connections': conn_udp, } delivery_kit.py000064400000025435152533440750007631 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import io import json import os import shlex import tarfile import time import uuid from tempfile import NamedTemporaryFile from kcarectl import auth, config, http_utils, ipv6_support, log_utils, utils from kcarectl.process_utils import run_command if False: # pragma: no cover from typing import Any, Dict, List, Optional # noqa: F401 from typing_extensions import Self # noqa: F401 def format_size(num_bytes): # type: (int) -> str """Render a byte count as a short human-readable string for the logs.""" size = float(num_bytes) for unit in ('B', 'KiB', 'MiB', 'GiB'): if size < 1024.0: return '{0:.1f} {1}'.format(size, unit) size /= 1024.0 return '{0:.1f} TiB'.format(size) class DataPackage(object): """Generic archive package for the patch server uploads pipeline. Based on DataPackage from eportal (delivery_kit.py). Subclasses supply the manifest `data_type`, the `upload_uri` to PUT the archive to and the `max_size` payload limit. """ data_type = '' # type: str upload_uri = '' # type: str def __init__(self): # type: () -> None self._tar = None # type: Optional[tarfile.TarFile] self._errors_buffer = [] # type: List[str] self._total_payload_size = 0 @property def max_size(self): # type: () -> int raise NotImplementedError # pragma: no cover @property def archive_path(self): # type: () -> str tar = self._ensure_tar_created() return str(tar.name) def add_stdout(self, arcname, cmd): # type: (str, str) -> None stdout = None stderr = None try: _, stdout, stderr = run_command(shlex.split(cmd), catch_stdout=True, catch_stderr=True) except Exception as e: stderr = str(e) if stderr: self.log_error('failed to dump stdout of {0}:\n{1}'.format(cmd, stderr)) if stdout is not None: self.add_file(arcname, data_bytes=utils.bstr(stdout, encoding='utf-8')) def add_file(self, arcname, src_path=None, data_bytes=None, skip_limit_check=False): # type: (str, Optional[str], Optional[bytes], bool) -> None if src_path is None and data_bytes is None: raise ValueError('No src_path or data_bytes provided') tar = self._ensure_tar_created() entry_size = 0 if src_path is not None: if not os.path.exists(src_path): self.log_error('file not found: {0}'.format(src_path)) return entry_size = os.path.getsize(src_path) else: entry_size = len(data_bytes) # type: ignore[arg-type] if not self._check_required_space(entry_size): self.log_error('no available space to store: {0}'.format(arcname)) return if not skip_limit_check and not self._check_total_payload_limit(entry_size): # a single artifact that would push the report past the size # budget is dropped, not the whole report: warn (so the omission # is visible in kcarectl.log and recorded in errors.log) and skip self.log_warning( 'skipping {0}: {1} would exceed the {2} report size limit (already collected {3})'.format( arcname, format_size(entry_size), format_size(self.max_size), format_size(self._total_payload_size), ) ) return try: if src_path: tar.add(src_path, arcname=arcname) else: info = tarfile.TarInfo(arcname) info.size = entry_size tar.addfile(info, io.BytesIO(data_bytes)) # type: ignore[arg-type] if not skip_limit_check: # entries exempt from the limit (manifest.json, errors.log) # are not counted toward the payload size either self._total_payload_size += entry_size # per-artifact size accounting so an oversized report can be # traced to the item(s) that bloated it (kcarectl.log only) log_utils.loginfo( 'collected {0}: {1} (report total {2})'.format( arcname, format_size(entry_size), format_size(self._total_payload_size), ), print_msg=False, ) except Exception as e: self.log_error('failed to store {0}: {1}'.format(arcname, e)) def add_json(self, arcname, data): # type: (str, Dict[str, Any]) -> None try: data_bytes = utils.bstr(json.dumps(data, indent=4), encoding='utf-8') except TypeError as e: self.log_error('failed to dump {0}:\n{1}'.format(arcname, e)) return self.add_file(arcname, data_bytes=data_bytes) def _check_required_space(self, entry_size): # type: (int) -> bool # here we simplify the check and ignore that the compressed file size will be less statvfs = os.statvfs(self.archive_path) return statvfs.f_frsize * statvfs.f_bfree > entry_size def _check_total_payload_limit(self, entry_size): # type: (int) -> bool # here we simplify the check and ignore that the compressed file size will be less return self.max_size > self._total_payload_size + entry_size def make_manifest(self): # type: () -> Dict[str, Any] return { "schema_version": 1, "type": self.data_type, "time_created": int(time.time()), } def _add_manifest(self): # type: () -> None # manifest.json must always be present (the patch server dispatches # uploads by its `type`), so it bypasses the lenient add_file: write # failures propagate to __enter__ and abort the package creation # instead of being downgraded to errors.log; the entry is exempt # from the payload limit and not counted toward it data_bytes = utils.bstr(json.dumps(self.make_manifest(), indent=4), encoding='utf-8') tar = self._ensure_tar_created() info = tarfile.TarInfo('manifest.json') info.size = len(data_bytes) tar.addfile(info, io.BytesIO(data_bytes)) def log_error(self, error_msg): # type: (str) -> None error_msg = error_msg.strip() log_utils.logerror(error_msg, print_msg=False) self._errors_buffer.append(error_msg) def log_warning(self, warning_msg): # type: (str) -> None # warn to kcarectl.log (print_msg=False: no console noise, like # log_error) and keep the note in the archived errors.log so the # uploaded report records what was dropped warning_msg = warning_msg.strip() log_utils.logwarn(warning_msg, print_msg=False) self._errors_buffer.append(warning_msg) def __enter__(self): # type: () -> Self for compression_mode in ('w:xz', 'w:bz2', 'w:gz'): # pragma: no branch tmpfile = NamedTemporaryFile(suffix='.tar.{0}'.format(compression_mode[2:]), delete=False) tmpfile.close() try: log_utils.loginfo('Creating DataPackage: {0}'.format(tmpfile.name), print_msg=False) # dereference=True mirrors the kcdoctor.sh `dump` (`cat "$1"`): # a symlinked source added via add_file(src_path=...) -- e.g. # /etc/yum.conf -> dnf/dnf.conf on EL8+, /boot/grub2/grub.cfg # on EFI -- is archived by content, not as a dangling link. self._tar = tarfile.open(name=tmpfile.name, mode=compression_mode, dereference=True) self._add_manifest() return self except Exception as err: if self._tar is not None: # the manifest write may fail after a successful open; # don't leak the open handle try: self._tar.close() except Exception: # pragma: no cover pass self._tar = None if os.path.exists(tmpfile.name): # pragma: no branch os.unlink(tmpfile.name) if not isinstance(err, tarfile.CompressionError): raise raise tarfile.CompressionError('No supported compression method found') # pragma: no cover def __exit__(self, exc_type, exc_val, exc_tb): # type: (Optional[type[BaseException]], Optional[BaseException], Any) -> bool if self._errors_buffer: # pragma: no branch errors = '\n'.join(self._errors_buffer) + '\n' self.add_file('errors.log', data_bytes=utils.bstr(errors), skip_limit_check=True) if self._tar: # pragma: no branch self._tar.close() if exc_val: self.remove_archive() return False return True @utils.catch_errors(logger=log_utils.logwarn) def remove_archive(self): # type: () -> None if self._tar and os.path.exists(self.archive_path): # pragma: no branch os.unlink(self.archive_path) def _ensure_tar_created(self): # type: () -> tarfile.TarFile if not self._tar: raise RuntimeError('DataPackage should be used as a context manager') return self._tar def send(self): # type: () -> str """Send the package archive to the patch server. Upload errors propagate to the caller (see the eportal precedent): wrap with utils.catch_errors where a silent failure is acceptable. :return: Upload name (package identifier) """ # flush buffered tar data even when called inside the `with` # block (close() is a no-op on an already closed tar) self._ensure_tar_created().close() # Generate a unique package name # Use find('.') to get extension from first dot to preserve .tar.xz/.tar.bz2/.tar.gz basename = os.path.basename(self.archive_path) ext = basename[basename.find('.') :] if '.' in basename else '' upload_name = str(uuid.uuid4()) + ext upload_url = ipv6_support.get_patch_server() + self.upload_uri + upload_name http_utils.upload_file( self.archive_path, upload_url=upload_url, auth_string=auth.get_http_auth_string(), ) return upload_name class KernelAnomalyPackage(DataPackage): data_type = 'kernel-anomaly' upload_uri = '/upload/kernel-anomaly/' @property def max_size(self): # type: () -> int return config.KERNEL_ANOMALY_REPORT_MAX_SIZE_BYTES http_utils.py000064400000016105152533440750007330 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import errno import os import socket import ssl from ssl import SSLError if False: # pragma: no cover from typing import Optional, Union # noqa: F401 from . import config, constants, errors, log_utils, utils from .py23 import HTTPError, Request, URLError, httplib, std_urlopen, urlparse def urlopen_base(url, *args, **kwargs): # mocked: tests/unit if hasattr(url, 'get_full_url'): request_url = url.get_full_url() else: request_url = url url = Request(url) headers = kwargs.pop('headers', {}) headers.update( { 'KC-Version': constants.VERSION, 'KC-Patch-Version': constants.KC_PATCH_VERSION, } ) for header, value in headers.items(): url.add_header(header, value) log_utils.logdebug("Requesting url: `{0}`. Headers: {1}".format(request_url, headers)) try: if 'timeout' not in kwargs: kwargs['timeout'] = config.HTTP_TIMEOUT # bandit warns about use of file: in urlopen which can happen here but is secure if not config.CHECK_SSL_CERTS and getattr(ssl, 'HAS_SNI', None): # pragma: no cover unit ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE kwargs['context'] = ctx return std_urlopen(url, *args, **kwargs) # nosec B310 return std_urlopen(url, *args, **kwargs) # nosec B310 except HTTPError as ex: if ex.code == 404: raise errors.NotFound(ex.url, ex.code, ex.msg, ex.hdrs, ex.fp) # HTTPError is a URLError descendant and contains URL, raise it as is raise except URLError as ex: # Local patches OSError(No such file) should be interpreted as Not found(404) # It was done as a chain because when it implemented with "duck-typing" it will mess # with error context if ex.args and hasattr(ex.args[0], 'errno') and ex.args[0].errno == errno.ENOENT: raise errors.NotFound(url, 404, str(ex), None, None) # type: ignore[arg-type] # 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) ex.url = request_url # type: ignore[attr-defined] raise def check_urlopen_retry_factory(retry_on_500=True): def check_function(e, state): if isinstance(e, HTTPError): return retry_on_500 and e.code >= 500 elif isinstance(e, (URLError, httplib.HTTPException, SSLError, socket.timeout)): return True elif hasattr(e, 'args') and len(e.args) == 2 and e.args[0] == errno.ECONNRESET: # pragma: no cover unit # SysCallError "Connection reset by peer" from PyOpenSSL return True return check_function def is_local_url(url): if hasattr(url, 'get_full_url'): url = url.get_full_url() return url.startswith('file:') def urlopen(url, *args, **kwargs): retry_on_500 = kwargs.pop('retry_on_500', True) retry_count = kwargs.pop('retry_count', constants.RETRY_COUNT) if is_local_url(url): return urlopen_base(url, *args, **kwargs) return utils.retry(check_urlopen_retry_factory(retry_on_500=retry_on_500), count=retry_count)(urlopen_base)(url, *args, **kwargs) def http_request(url, auth_string, auth_token=None, method=None): request = Request(url, method=method) if not config.UPDATE_FROM_LOCAL and auth_string: request.add_header('Authorization', 'Basic {0}'.format(auth_string)) if not config.UPDATE_FROM_LOCAL and auth_token: request.add_header(constants.AUTH_TOKEN_HEADER, auth_token) return request 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 proxy_is_used(): return bool(get_proxy_from_env('http')) or bool(get_proxy_from_env('https')) check_urlopen_retry = check_urlopen_retry_factory() @utils.retry(check_retry=check_urlopen_retry) def upload_file(file_path, upload_url, auth_string=None): # type: (str, str, Optional[str]) -> None """Upload a file to the given URL using HTTP PUT with chunked streaming. Note: The standard library urllib doesn't support PUT with data We need to use httplib directly for this This function uses streaming upload to support large files up to 1GB without loading the entire file into memory. :param file_path: Path to the file to upload :param upload_url: Full URL to upload the file to. Query params are ignored. :param auth_string: Optional authentication string for Basic Auth :return: None if upload succeeded :raises HTTPError: If upload fails with HTTP status >= 400 :raises ValueError: If URL is invalid """ file_size = os.path.getsize(file_path) if not file_size: raise ValueError('Refusing to upload empty file: {0}'.format(file_path)) parsed = urlparse(utils.nstr(upload_url)) host = parsed.hostname port = parsed.port url_path = parsed.path or '/' if host is None: raise ValueError('Invalid URL: missing hostname') if port is None: # pragma: no cover port = 443 if parsed.scheme == 'https' else 80 if parsed.scheme == 'http': conn_cls = httplib.HTTPConnection # type: Union[type[httplib.HTTPConnection], type[httplib.HTTPSConnection]] elif parsed.scheme == 'https': # pragma: no cover conn_cls = httplib.HTTPSConnection else: raise ValueError('Invalid URL: unsupported scheme') conn = conn_cls(host, port, timeout=config.HTTP_UPLOAD_TIMEOUT) headers = {} if auth_string: headers['Authorization'] = 'Basic {0}'.format(auth_string) headers['Content-Type'] = 'application/octet-stream' headers['Content-Length'] = str(file_size) headers['KC-Version'] = constants.VERSION try: # Use the lower-level API to support streaming conn.putrequest('PUT', url_path) # Send headers for header, value in headers.items(): conn.putheader(header, value) conn.endheaders() # Stream file data in chunks with open(file_path, 'rb') as f: conn.send(f) response = conn.getresponse() if response.status >= 400: # Read response body for error details try: error_body = response.read() except Exception: # pragma: no cover error_body = None error_msg = 'Failed to upload file: HTTP {0}'.format(response.status) if error_body is not None: # pragma: no branch error_msg += ' - {0}'.format(error_body) # type: ignore[str-bytes-safe] log_utils.logerror(error_msg) # HTTPError compatible with retry mechanism raise HTTPError(upload_url, response.status, error_msg, None, None) # type: ignore[arg-type] finally: conn.close() log_utils.py000064400000005576152533440750007144 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT from __future__ import print_function import logging import logging.handlers import os import sys import traceback from . import config, constants kcarelog = logging.getLogger('kcare') # mocked: tests/unit def logdebug(message): # type: (str) -> None _printlvl(message, constants.PRINT_DEBUG) kcarelog.debug(message) def loginfo(message, print_msg=True): # type: (str, bool) -> None if print_msg: _printlvl(message, constants.PRINT_INFO) kcarelog.info(message) def logwarn(message, print_msg=True): # type: (str, bool) -> None if print_msg: _printlvl(message, constants.PRINT_WARN, file=sys.stderr) # pragma: no cover kcarelog.warning(message) def logerror(message, print_msg=True): if print_msg: _printlvl(message, constants.PRINT_ERROR, file=sys.stderr) kcarelog.error(message) def logexc(message, print_msg=True): if print_msg and constants.PRINT_ERROR >= config.PRINT_LEVEL: traceback.print_exc() kcarelog.exception(message) def _printlvl(message, level, file=None): if level >= config.PRINT_LEVEL: print(message, file=file) # noqa: T201 def get_syslog_handler(): syslog_formatter = logging.Formatter('kcare %(levelname)s: %(message)s') syslog_handler = logging.handlers.SysLogHandler(address='/dev/log', facility=logging.handlers.SysLogHandler.LOG_USER) syslog_handler.setLevel(logging.INFO) syslog_handler.setFormatter(syslog_formatter) return syslog_handler def get_kcare_handler(level): kcare_formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s') if os.getuid() == 0: kcare_handler = logging.handlers.RotatingFileHandler( constants.LOG_FILE, maxBytes=1024**2, backupCount=2 ) # type: logging.Handler # We need at least INFO level logs at all times kcare_handler.setLevel(min(level, logging.INFO)) kcare_handler.setFormatter(kcare_formatter) return kcare_handler else: kcare_handler = logging.StreamHandler() kcare_handler.setLevel(level) kcare_handler.setFormatter(kcare_formatter) return kcare_handler def initialize_logging(level): kcarelog.handlers[:] = [] try: kcare_handler = get_kcare_handler(level) kcarelog.addHandler(kcare_handler) except Exception as ex: kcarelog.exception(ex) if os.path.exists('/dev/log'): try: syslog_handler = get_syslog_handler() kcarelog.addHandler(syslog_handler) except Exception as ex: kcarelog.exception(ex) def print_cln_http_error(ex, url=None, stdout=True): url = url or '' logerror('Unable to fetch {0}. Please try again later (error: {1})'.format(url, str(ex)), stdout) selinux.py000064400000003264152533440750006622 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import os from . import errors, log_utils, process_utils, utils def selinux_safe_tmpname(fname): head, tail = os.path.split(fname) return os.path.join(head, 'tmp.' + tail) def restore_selinux_context(dname): if is_selinux_enabled(): # Try to restore selinux context cmd = [process_utils.find_cmd('restorecon', ('/usr/sbin', '/sbin')), '-R', dname] code, _, stderr = process_utils.run_command(cmd, catch_stdout=True, catch_stderr=True) if code: log_utils.logerror( "SELinux context restoration for {0} failed with {1}: {2}".format(dname, code, stderr), print_msg=False ) def is_selinux_module_present(semodule_name): code, out, err = process_utils.run_command(['/usr/sbin/semodule', '-l'], catch_stdout=True) if code: raise errors.KcareError("SELinux modules list gathering error: '{0}' {1}".format(err, code), status='selinux modules error') for line in out.split('\n'): if semodule_name in line: return True return False def skip_if_no_selinux_module(clbl): def wrapper(*args, **kwargs): if is_selinux_enabled() and not is_selinux_module_present('libcare'): raise errors.KcareError('SELinux is enabled but libcare policy module is not loaded') return clbl(*args, **kwargs) return wrapper @utils.cached def is_selinux_enabled(): if os.path.isfile('/usr/sbin/selinuxenabled'): code, _, _ = process_utils.run_command(['/usr/sbin/selinuxenabled']) else: return False return code == 0 utils.py000064400000021533152533440750006272 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT from __future__ import print_function import fnmatch import functools import os import random import re import shutil import tempfile import time from datetime import datetime from . import constants if False: # pragma: no cover from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TypeVar, Union # noqa: F401 T = TypeVar('T', bound=Callable[..., Any]) VERSION_RE = re.compile(r'^(\d+[.]\d+[-]\d+)') CACHE_ENTRIES = 3 ntype = type('') btype = type(b'') utype = type(u'') def atomic_write(fname, content, ensure_dir=False, mode='w', create_mode=0o644): # type: (str, Union[str, bytes], bool, str, int) -> None dname = os.path.dirname(fname) if ensure_dir and not os.path.exists(dname): os.makedirs(dname) try: st_mode = os.stat(fname).st_mode except Exception: st_mode = create_mode with tempfile.NamedTemporaryFile(mode=mode, dir=dname, prefix=os.path.basename(fname) + '.', delete=False) as f: os.fchmod(f.fileno(), st_mode) f.write(content) f.flush() os.fsync(f.fileno()) tmp_fname = f.name # ensure folder is also updated # https://www.quora.com/When-should-you-fsync-the-containing-directory-in-addition-to-the-file-itself # https://www.reddit.com/r/kernel/comments/1du6ot8/comment/lbgu46i/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button folder_fd = os.open(dname, os.O_RDONLY) try: os.fsync(folder_fd) finally: os.close(folder_fd) os.rename(tmp_fname, fname) def nstr(data, encoding='utf-8'): # pragma: no py2 cover # type: (Union[str, bytes, None], str) -> str if type(data) is ntype: return data elif type(data) is btype: return data.decode(encoding) else: return data.encode(encoding) # type: ignore # pragma: no py3 cover def bstr(data, encoding='latin1'): # pragma: no py2 cover # type: (Union[str, bytes], str) -> bytes if type(data) is utype: data = data.encode(encoding) return data # type: ignore def ustr(data, encoding='latin1'): # pragma: no py2 cover # type: (Union[str, bytes], str) -> str if type(data) is btype: data = data.decode(encoding) return data # type: ignore def cached(fn): # type: (T) -> Any cache = {} # type: dict[tuple[Any, ...], Any] @functools.wraps(fn) def inner(*args, **kwargs): # type: (Any, Any) -> Any cache_key = (args, tuple(sorted(kwargs.items()))) try: return cache[cache_key] except KeyError: pass result = cache[cache_key] = fn(*args, **kwargs) return result inner.cache = cache # type: ignore[attr-defined] inner.clear = cache.clear # type: ignore[attr-defined] inner.orig = fn # type: ignore[attr-defined] return inner def retry(check_retry, count=None, delay=None, backoff=None): # type: (Callable[[Exception, dict[str, Any]], bool], Optional[int], Optional[float], Optional[float]) -> Callable[..., Any] if delay is None: delay = constants.RETRY_DELAY if count is None: count = constants.RETRY_COUNT if backoff is None: backoff = constants.RETRY_BACKOFF state = {} # type: dict[str, Any] def decorator(fn): # type: (Callable[..., Any]) -> Callable[..., Any] def inner(*args, **kwargs): # type: (Any, Any) -> Any ldelay = delay for _ in range(count): try: return fn(*args, **kwargs) except Exception as ex: if not check_retry(ex, state): raise time.sleep(ldelay) # bandit warns about using random.uniform for security which is not the case here ldelay = min(ldelay * random.uniform(1, backoff), constants.RETRY_MAX_DELAY) # nosec B311 # last try try: return fn(*args, **kwargs) except Exception as final_ex: setattr(final_ex, 'attempts', count) raise return inner return decorator def clean_directory(directory, exclude_path=None, keep_n=CACHE_ENTRIES, pattern=None): # type: (str, Optional[str], int, Optional[str]) -> None if not os.path.exists(directory): return data = [] items = os.listdir(directory) if pattern is not None: items = fnmatch.filter(items, pattern) for item in items: 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[keep_n:]: if os.path.isfile(entry) or os.path.islink(entry): os.remove(entry) else: shutil.rmtree(entry) def clear_all_cache(): # type: () -> None clean_directory(os.path.join(constants.PATCH_CACHE, 'modules'), keep_n=0) clean_directory(os.path.join(constants.PATCH_CACHE, 'patches'), keep_n=0) if os.path.exists(constants.CACHE_KEY_DUMP_PATH): os.unlink(constants.CACHE_KEY_DUMP_PATH) def save_to_file(response, dst): # type: (Any, str) -> None 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) f.flush() os.fsync(f.fileno()) def strip_version_timestamp(version): # type: (str) -> str match = VERSION_RE.match(version) return match and match.group(1) or version def parse_response_date(str_raw): # type: (str) -> datetime # 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(' ') return datetime.strptime(str_date, '%Y-%m-%d') def get_patch_server_url(*parts): # type: (*str) -> str from . import ipv6_support # TODO fix circular import return '/'.join(it.strip('/') for it in filter(None, (ipv6_support.get_patch_server(),) + parts)) def try_to_read(filename): # type: (str) -> Optional[str] if not os.path.exists(filename): return None with open(filename) as f: return f.read().strip() @cached def get_cache_key(): # type: () -> Optional[str] return try_to_read(constants.CACHE_KEY_DUMP_PATH) def _read_file(fname, mode, default): # type: (str, str, Optional[Union[str, bytes]]) -> Union[str, bytes] if not os.path.exists(fname): return default # type: ignore with open(fname, mode) as f: return f.read() # type: ignore def read_file(fname, default=None): # type: (str, Optional[str]) -> str result = _read_file(fname, 'r', default) # type: str # type: ignore[assignment] return result def read_file_bin(fname, default=None): # type: (str, Optional[bytes]) -> bytes result = _read_file(fname, 'rb', default) # type: bytes # type: ignore[assignment] return result def data_as_dict(data): # type: (str) -> dict[str, str] result = {} data_lines = data.splitlines() # type: list[str] for line in data_lines: if line: key, delimiter, value = line.partition(':') if delimiter: result[key] = value.strip() return result def extract_unique_cves(patches, cve_field='cve'): # type: (List[Dict[str, Any]], str) -> Set[str] """ Extract unique CVEs from a list of patches. Args: patches: List of patch dictionaries cve_field: Field name to extract CVE from (default 'cve' for userspace, 'kpatch-cve' for kernel) Returns: Set of unique CVE strings """ unique_cves = set() for patch in patches: cve = patch.get(cve_field) if cve: # only add non-empty CVEs unique_cves.add(cve) return unique_cves def timestamp_str(): # type: () -> str return str(int(time.time())) def print_wrapper(*values): # type: (object) -> None """a workaround to fix T201""" print(*values) # noqa: T201 def catch_errors(logger=None, errors=(Exception,), default_return=None): # type: (Optional[Callable[[str], None]], Tuple[Any, ...], Any) -> Callable[..., Any] def decorator(fn): # type: (T) -> Callable[..., Any] @functools.wraps(fn) def inner(*args, **kwargs): # type: (Any, Any) -> Any try: return fn(*args, **kwargs) except errors as e: if logger: # pragma: no branch arg_list = [str(a) for a in args] + ['{0}={1}'.format(k, v) for k, v in kwargs.items()] logger('{0}({1}) failed: {2}'.format(fn.__name__, ', '.join(arg_list), e)) return default_return return inner return decorator server_info.py000064400000007350152533440750007454 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import base64 import json import os import platform import time import zlib from . import capabilities, constants, http_utils, kcare, platform_utils, serverid, update_utils, utils if False: # pragma: no cover from typing import Any, Dict # noqa: F401 def server_info(reason, now=None, secure_boot_info=False, perf_metrics=False): data = dict() # type: Dict[str, Any] data['ts'] = int(now or time.time()) data['reason'] = reason data['machine'] = platform.machine() data['processor'] = platform.processor() data['release'] = platform.release() data['system'] = platform.system() data['version'] = platform.version() distro = platform_utils.get_distro() data['distro'] = distro[0] data['distro_version'] = distro[1] data['euname'] = kcare.kcare_uname() data['kcare_version'] = utils.strip_version_timestamp(constants.VERSION) data['last_stop'] = kcare.get_last_stop() data['node'] = platform_utils.get_hostname() data['uptime'] = platform_utils.get_uptime() data['virt'] = platform_utils.get_virt() data['proxy'] = http_utils.proxy_is_used() description = kcare.parse_patch_description(kcare.loaded_patch_description()) data['ltimestamp'] = description['last-update'] data['patch_level'] = description['patch-level'] data['patch_type'] = description['patch-type'] data['kmod'] = kcare.get_current_kmod_version() or '' data['crashreporter_ts'] = kcare.crashreporter_latest_event_timestamp() data['kdump_status'] = kcare.kdump_status() data['capabilities'] = capabilities.get_kc_capabilites_bits() try: data['kdump_ts'] = kcare.kdumps_latest_event_timestamp() except Exception: # Not critical data pass server_id = serverid.get_serverid() if server_id: data['server_id'] = server_id state = kcare.get_state() if state is not None: data['state'] = state data['update_error'] = update_utils.read_update_error('kernel') if secure_boot_info: data['secure_boot'] = platform_utils.secure_boot_info() if perf_metrics: data['perf_metrics'] = platform_utils.get_performance_metrics() return data def server_lib_info(reason, patch_level, now=None): data = dict() # type: Dict[str, Any] data['ts'] = int(now or time.time()) data['reason'] = reason data['patch_level'] = patch_level distro = platform_utils.get_distro() data['distro'] = distro[0] data['distro_version'] = distro[1] data['machine'] = platform.machine() data['kcare_version'] = utils.strip_version_timestamp(constants.VERSION) data['node'] = platform_utils.get_hostname() data['uptime'] = platform_utils.get_uptime() data['virt'] = platform_utils.get_virt() data['capabilities'] = capabilities.get_lc_capabilites_bits() stop_ts = 0.0 if os.path.exists('/var/lib/libcare/stop'): stop_ts = os.path.getctime('/var/lib/libcare/stop') data['stop_ts'] = stop_ts data['update_error'] = update_utils.read_update_error('libcare') return data def encoded_server_lib_info(reason, patch_level, now=None): info = server_lib_info(reason=reason, patch_level=patch_level, now=now) return encode_checkin_payload(info, b64_encoding=True) def encode_checkin_payload(data, b64_encoding): # type: (Dict[str, Any], bool) -> str if b64_encoding: data_str = json.dumps(data, ensure_ascii=False, separators=(',', ':')) return utils.nstr(base64.urlsafe_b64encode(zlib.compress(utils.bstr(data_str, 'utf-8')))) else: # legacy serialization return utils.nstr(base64.b16encode(utils.bstr(str(data)))) kcare.py000064400000024714152533440750006223 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import ast import errno import glob import hashlib import json import os import platform import subprocess from . import config, constants, log_utils, process_utils, utils from .errors import SafeExceptionWrapper from .py23 import json_loads_nstr if False: # pragma: no cover from typing import List, Optional, Tuple # noqa: F401 UNAME_LABEL = 'uname: ' def is_uname_char(c): # type: (str) -> bool return str.isalnum(c) or c in '.-_+' def parse_uname(patch_level): khash = get_kernel_hash() with open(get_cache_path(khash, patch_level, config.PATCH_INFO), 'r') as f: for line in f.readlines(): if line.startswith(UNAME_LABEL): return ''.join(filter(is_uname_char, line[len(UNAME_LABEL) :].strip())) return '' def kcare_update_effective_version(new_version): if os.path.exists(config.KCARE_UNAME_FILE): try: f = open(config.KCARE_UNAME_FILE, 'w') f.write(new_version) f.close() return True except Exception: pass return False def get_kernel_hash(): # type: () -> str f = open(config.KERNEL_VERSION_FILE, 'rb') try: # sha1 is not used for security, turn off bandit warning # bandit issues a warning that B324 has no test when `nosec B324` is # set here. Using broad `nosec` here to bypass the warning. return hashlib.sha1(f.read()).hexdigest() # nosec B324 finally: f.close() def get_last_stop(): # type: () -> str """Returns timestamp from PATCH_CACHE/stoped.at if its exsits""" stopped_at_filename = os.path.join(constants.PATCH_CACHE, 'stopped.at') if os.path.exists(stopped_at_filename): with open(stopped_at_filename, 'r') as fh: value = fh.read().rstrip() try: int(value) except ValueError: return str(int(os.path.getctime(stopped_at_filename))) except Exception: # pragma: no cover, it should not happen return 'error' return value return '-1' def get_cache_path(khash, plevel, fname): prefix = config.PREFIX or 'none' ptype = config.PATCH_TYPE or 'default' patch_dir = '-'.join([prefix, khash, str(plevel), ptype]) result = (constants.PATCH_CACHE, 'patches', patch_dir) # type: Tuple[str, ...] if fname: result += (fname,) return os.path.join(*result) def get_kernel_prefixed_url(*parts): return utils.get_patch_server_url(config.PREFIX, *parts) class BaseKernelPatchLevel(int): def cache_path(self, *parts): return get_cache_path(self.khash, str(self), *parts) # type: ignore[attr-defined] def as_dict(self): return { 'level': self.level, 'khash': self.khash, 'baseurl': self.baseurl, 'release': self.release, } class KernelPatchLevel(BaseKernelPatchLevel): def __new__(cls, khash, level, baseurl, release=None): return super(cls, cls).__new__(cls, level) def __init__(self, khash, level, baseurl, release=None): self.level = level self.khash = khash self.baseurl = baseurl self.release = release def kmod_url(self, *parts): return utils.get_patch_server_url(self.baseurl, self.khash, *parts) def file_url(self, *parts): return utils.get_patch_server_url(self.baseurl, self.khash, str(self), *parts) class LegacyKernelPatchLevel(BaseKernelPatchLevel): def __new__(cls, khash, level): try: return super(cls, cls).__new__(cls, level) except ValueError as exc: # common error with this class raise SafeExceptionWrapper(exc) def __init__(self, khash, level): self.level = level self.khash = khash self.baseurl = None self.release = None def kmod_url(self, *parts): if 'patches.kernelcare.com' in config.PATCH_SERVER: return get_kernel_prefixed_url(self.khash, str(self), *parts) # ePortal workaround, it doesn't support leveled links to kmod return get_kernel_prefixed_url(self.khash, *parts) def file_url(self, *parts): return get_kernel_prefixed_url(self.khash, str(self), *parts) def upgrade(self, baseurl): return KernelPatchLevel(self.khash, int(self), baseurl) def dump_kernel_patch_level(kernel_patch_level): # type: (BaseKernelPatchLevel) -> None try: with open(os.path.join(constants.PATCH_CACHE, 'kernel_patch_level.json'), 'w') as f: json.dump(kernel_patch_level.as_dict(), f) except Exception: log_utils.logexc('failed to dump kernel patch level', print_msg=False) def read_dumped_kernel_patch_level(): try: with open(os.path.join(constants.PATCH_CACHE, 'kernel_patch_level.json')) as f: return json_loads_nstr(f.read()) except Exception: log_utils.logexc('failed to read dumped kernel patch level', print_msg=False) def sort_files_by_ctime(files_list): # type: (List[str]) -> List[Tuple[str, float]] return sorted( [(it, os.path.getctime(it)) for it in files_list], key=lambda pair: pair[1], reverse=True, ) def get_kdump_root(): # type: () -> str kdump_path = "/var/crash" if not os.path.isfile("/etc/kdump.conf"): return kdump_path with open("/etc/kdump.conf") as kdump_conf: for line in kdump_conf: line = line.strip() if line.startswith('path '): _, kdump_path = line.split(None, 1) return kdump_path def list_kdump_vcore_files(): # type: () -> List[str] kdump_root = get_kdump_root() if not os.path.isdir(kdump_root): return [] return glob.glob(os.path.join(kdump_root, '*/vmcore')) def list_kdump_txt_files(): # type: () -> List[str] kdump_root = get_kdump_root() if not os.path.isdir(kdump_root): return [] return glob.glob(os.path.join(kdump_root, '*/*.txt')) def list_crashreporter_log_files(): # type: () -> List[str] if not os.path.isdir(config.KDUMPS_DIR): return [] return glob.glob(os.path.join(config.KDUMPS_DIR, '*.log')) def list_crashreporter_artifacts(): # type: () -> List[str] if not os.path.isdir(config.KDUMPS_DIR): return [] return [os.path.join(config.KDUMPS_DIR, it) for it in os.listdir(config.KDUMPS_DIR)] @utils.cached def kdumps_latest_event_timestamp(): # type: () -> Optional[float] kdumps = list_kdump_vcore_files() if not kdumps: return None return sort_files_by_ctime(kdumps)[0][1] @utils.cached def kdump_status(): if constants.SKIP_SYSTEMCTL_CHECK or os.path.isfile(constants.SYSTEMCTL): _, stdout, _ = process_utils.run_command([constants.SYSTEMCTL, 'is-active', 'kdump'], catch_stdout=True, catch_stderr=True) return stdout.strip() return 'systemd-absent' @utils.cached def crashreporter_latest_event_timestamp(): # type: () -> Optional[float] artifacts = list_crashreporter_artifacts() if not artifacts: return None return sort_files_by_ctime(artifacts)[0][1] def get_current_kmod_version(): kmod_version_file = '/sys/module/kcare/version' if not os.path.exists(kmod_version_file): return with open(kmod_version_file, 'r') as f: version = f.read().strip() return version def is_kmod_version_changed(khash, plevel): old_version = get_current_kmod_version() if not old_version: return True new_version = process_utils.check_output( ['/sbin/modinfo', '-F', 'version', get_cache_path(khash, plevel, constants.KMOD_BIN)] ).strip() return old_version != new_version def kcare_uname_su(): patch_level = loaded_patch_level() if not patch_level: return platform.release() return parse_uname(patch_level) def kcare_uname(): if os.path.exists(config.KCARE_UNAME_FILE): return open(config.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 loaded_patch_level(): # mocked: tests/unit pl = parse_patch_description(loaded_patch_description())['patch-level'] if pl: try: int(pl) except ValueError as e: raise SafeExceptionWrapper(e, 'Unexpected patch state', _patch_info()) return LegacyKernelPatchLevel(get_kernel_hash(), pl) def _patch_info(): try: return process_utils.check_output([constants.KPATCH_CTL, 'info'], check=True) except subprocess.CalledProcessError as e: if e.returncode == errno.EBUSY: raise return '' @utils.cached def get_loaded_modules(): try: return [line.split()[0] for line in open('/proc/modules')] except (OSError, IOError) as ex: log_utils.logerror('Error getting loaded modules list: ' + str(ex), print_msg=False) return [] 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 get_patch_value(info, label): return utils.data_as_dict(info).get(label) 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 get_state(): state_file = os.path.join(constants.PATCH_CACHE, 'kcare.state') if os.path.exists(state_file): with open(state_file, 'r') as f: try: state = f.read() return ast.literal_eval(state) except (SyntaxError, OSError, ValueError, TypeError, UnicodeDecodeError): pass anomaly.py000064400000011771152533440750006575 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import os import time from kcarectl import delivery_kit, kcare, log_utils, utils if False: # pragma: no cover from typing import Any, Dict, List # noqa: F401 @utils.catch_errors(logger=log_utils.logwarn) def send_data_package(data_package): # type: (delivery_kit.DataPackage) -> str """Send the DataPackage archive to the patch server. Upload errors are logged and swallowed (catch_errors), preserving the historical kernel-anomaly behavior. :param data_package: DataPackage instance to send :return: Upload name (package identifier) """ return data_package.send() def copy_recent_files(files, data_package, archive_prefix): # type: (List[str], delivery_kit.DataPackage, str) -> None """adds recent files for the last hour to the given data package starting from the newest one""" now = time.time() for path, ctime in kcare.sort_files_by_ctime(files): if now - ctime > 3600: break arcname = '{0}/{1}'.format(archive_prefix, path.replace('/', '_')) data_package.add_file(arcname, src_path=path) @utils.catch_errors(logger=log_utils.logwarn) def prepare_kernel_anomaly_report(server_info): # pragma: no cover # type: (Dict[str, Any]) -> delivery_kit.KernelAnomalyPackage data_package = delivery_kit.KernelAnomalyPackage() with data_package: if os.path.exists('/var/log/messages'): data_package.add_stdout('messages', 'tail -n10000 /var/log/messages') if os.path.exists('/var/log/syslog'): data_package.add_stdout('syslog', 'tail -n10000 /var/log/syslog') data_package.add_stdout('kcarectl.log', 'tail -n10000 /var/log/kcarectl.log') data_package.add_stdout('dmesg', 'dmesg') data_package.add_stdout('ls_var_cache_kcare', 'ls -lR /var/cache/kcare/') if os.path.exists('/usr/bin/rpm') or os.path.exists('/bin/rpm'): packages_cmd = r'rpm -q -a --queryformat="%{N}|%{V}-%{R}|%{arch}|%{INSTALLTIME:date}\n"' elif os.path.exists('/usr/bin/dpkg'): packages_cmd = r'/usr/bin/dpkg-query -W -f "${binary:Package}|${Version}|${Architecture}\n"' data_package.add_file('dpkg.log', src_path='/var/log/dpkg.log') else: packages_cmd = 'echo "unknown package manager"' data_package.add_stdout('packages.list', packages_cmd) # tar fails to add files from /proc directly so we first read them to memory with open('/proc/version') as f: data_package.add_file('proc_version', data_bytes=utils.bstr(f.read())) with open('/proc/modules') as f: data_package.add_file('proc_modules', data_bytes=utils.bstr(f.read())) data_package.add_file('kcare.conf', src_path='/etc/sysconfig/kcare/kcare.conf') data_package.add_json('server_info.json', server_info) # kdump data_package.add_file('kdump.conf', src_path='/etc/kdump.conf') data_package.add_stdout('ls_kdump', 'ls -lR {0}'.format(kcare.get_kdump_root())) try: copy_recent_files(kcare.list_kdump_txt_files(), data_package, 'kdump') except Exception as e: data_package.log_error('failed to copy kdumps:\n{0}'.format(e)) try: copy_recent_files(kcare.list_crashreporter_log_files(), data_package, 'crashreporter') except Exception as e: data_package.log_error('failed to copy crashreporter artifacts:\n{0}'.format(e)) return data_package @utils.catch_errors(logger=log_utils.logwarn, default_return=False) def detect_anomaly(server_info): # type: (Dict[str, Any]) -> bool """taken from eportal - anomalies::detect_agent_reboot""" reason = server_info['reason'] # type: str uptime = int(server_info['uptime']) patch_level = int(server_info.get('patch_level') or '-1') last_stop = int(server_info['last_stop']) ts = server_info['ts'] # type: int try: state_ts = int(server_info['state']['ts']) except (KeyError, TypeError, ValueError): state_ts = 0 fields = [reason, uptime, patch_level, state_ts, last_stop, ts] if not all(fields): return False first_update_after_reboot_marker = False crash_soon_after_update_marker = False no_proper_shutdown_marker = False if uptime < 300 and patch_level == -1: first_update_after_reboot_marker = True if (ts - uptime) > state_ts > (ts - uptime - 1800) and reason == 'update': crash_soon_after_update_marker = True if last_stop < state_ts: # pragma: no branch no_proper_shutdown_marker = True markers = [ first_update_after_reboot_marker, crash_soon_after_update_marker, no_proper_shutdown_marker, ] if all(markers): log_utils.loginfo('Agent anomaly detected: {0}'.format(server_info)) return True return False if __name__ == '__main__': # pragma: no cover prepare_kernel_anomaly_report({}) __init__.py000064400000235052152533440750006674 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT from __future__ import print_function import base64 import json import logging import os import platform import random import re import shutil import socket import ssl import sys import tempfile import time import traceback import warnings from argparse import ArgumentParser from contextlib import contextmanager from datetime import datetime from . import ( anomaly, auth, capabilities, config, config_handlers, constants, doctor, errors, fetch, http_utils, ipv6_support, kcare, libcare, log_utils, platform_utils, process_utils, selinux, server_info, serverid, update_utils, utils, ) from .errors import KcareError, NotFound, SafeExceptionWrapper from .py23 import HTTPError, URLError, httplib, json_loads_nstr, urlencode CPANEL_GID = 99 EFFECTIVE_LATEST = 'v3' EXPECTED_PREFIX = ('12h', '24h', '48h', 'test') FREEZER_BLACKLIST = '/etc/sysconfig/kcare/freezer.modules.blacklist' KCDOCTOR = '/usr/libexec/kcare/kcdoctor.sh' PATCH_LATEST = ( 'latest.v3', 'latest.v2', ) SYSCTL_CONFIG = '/etc/sysconfig/kcare/sysctl.conf' UNLOAD_RETRY_DELAY = 10 BLACKLIST_RE = re.compile('==BLACKLIST==\n(.*)==END BLACKLIST==\n', re.DOTALL) CONFLICTING_MODULES_RE = re.compile('(kpatch.*|ksplice.*|kpatch_livepatch.*)') if os.path.isdir('/usr/libexec/kcare/python'): # pragma: no cover sys.path.insert(0, '/usr/libexec/kcare/python') warnings.filterwarnings('ignore', category=DeprecationWarning) if False: # pragma: no cover from typing import Any, Dict, Optional, Set, Tuple, Union # noqa: F401 log_utils.kcarelog.setLevel(logging.DEBUG) def get_freezer_blacklist(): result = set() if os.path.isfile(FREEZER_BLACKLIST): f = open(FREEZER_BLACKLIST, 'r') for line in f: result.add(line.rstrip()) f.close() 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): config.PATCH_BIN = _apply_ptype(ptype, config.PATCH_BIN) config.PATCH_INFO = _apply_ptype(ptype, config.PATCH_INFO) config.BLACKLIST_FILE = _apply_ptype(ptype, config.BLACKLIST_FILE) config.FIXUPS_FILE = _apply_ptype(ptype, config.FIXUPS_FILE) config.PATCH_DONE = _apply_ptype(ptype, config.PATCH_DONE) def format_exception_without_details(): etype, value, tb = sys.exc_info() details_sanitized = '' if isinstance(value, OSError) and not isinstance(value, URLError): try: # reconstruct for safety, it may be any IO-related subclass details_sanitized = "[Errno %i] %s: '%s'" % (value.errno, os.strerror(value.errno), value.filename) except (AttributeError, TypeError): pass elif isinstance(value, (KeyError, TypeError, IOError)) and not isinstance(value, URLError): details_sanitized = '%s' % value elif isinstance(value, SafeExceptionWrapper): etype = value.etype or type(value.inner) details_sanitized = value.details or ('%s' % value.inner) distro = platform_utils.get_distro() return { 'agent_version': constants.VERSION, 'python_version': platform_utils.get_python_version(), 'distro': distro[0], 'distro_version': distro[1], 'error': getattr(etype, '__name__', str(etype)), 'details': details_sanitized, 'traceback': ''.join(traceback.format_tb(tb, 100)), 'attempts': getattr(value, 'attempts', 0), } def send_exc(): if config.UPDATE_FROM_LOCAL: # pragma: no cover return trace = json.dumps(format_exception_without_details()) encoded_trace = utils.nstr(base64.urlsafe_b64encode(utils.bstr(trace))) # type: str url = utils.get_patch_server_url('/api/kcarectl-trace') + '?trace=' + encoded_trace request = http_utils.http_request(url, auth.get_http_auth_string()) try: http_utils.urlopen_base(request) except Exception: # import traceback # traceback.print_exc() # we really don't interested in exception pass def nohup_fork(func, sleep=None): # pragma: no cover """ Run func in a fork in an own process group (will stay alive after kcarectl process death). :param func: function to execute :return: """ # TODO move to process_utils.py pid = os.fork() if pid != 0: os.waitpid(pid, 0) return os.setsid() pid = os.fork() if pid != 0: os._exit(0) # close standard files to release TTY os.close(0) # redirect stdout/stdin into log file with open(constants.LOG_FILE, 'a') as fd: os.dup2(fd.fileno(), 1) os.dup2(fd.fileno(), 2) if sleep: time.sleep(sleep) try: func() except Exception: log_utils.kcarelog.exception('Wait exception') os._exit(1) os._exit(0) 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(constants.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 + config.SUCCESS_TIMEOUT > time.time(): raise PreviousPatchFailedException(timestamp, anchor_filepath) except ValueError: pass utils.atomic_write(anchor_filepath, utils.timestamp_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(constants.PATCH_CACHE, '.kcareprev.lock')) except OSError: pass register_action('done', state_data) # reset module cache, to allow server_info get fresh data kcare.get_loaded_modules.clear() try: get_latest_patch_level(reason='done') except Exception: log_utils.kcarelog.exception('Cannot send update info!') def clear_cache(khash, plevel): utils.clean_directory(os.path.join(constants.PATCH_CACHE, 'patches'), exclude_path=kcare.get_cache_path(khash, plevel, '')) def get_current_level_path(khash, fname): prefix = config.PREFIX or 'none' module_dir = '-'.join([prefix, khash]) result = (constants.PATCH_CACHE, 'modules', module_dir) # type: Tuple[str, ...] if fname: result += (fname,) return os.path.join(*result) def save_cache_latest(khash, patch_level): utils.atomic_write(get_current_level_path(khash, 'latest'), str(patch_level), ensure_dir=True) def get_cache_latest(khash): # type: (str) -> Optional[kcare.LegacyKernelPatchLevel] path_with_latest = get_current_level_path(khash, 'latest') if os.path.isfile(path_with_latest): try: pl = int(open(path_with_latest, 'r').read().strip()) return kcare.LegacyKernelPatchLevel(khash, pl) except (ValueError, TypeError): pass return None class CertificateError(ValueError): pass class UnknownKernelException(KcareError): status = 'unknown kernel' def __init__(self, **kwargs): msg = 'New kernel detected ({0} {1} {2}).\nThere are no updates for this kernel yet.'.format( platform_utils.get_distro()[0], platform.release(), kcare.get_kernel_hash() ) super(UnknownKernelException, self).__init__(msg, **kwargs) class ApplyPatchError(KcareError): status = 'patch apply error' 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 = platform_utils.get_distro()[0] self.release = platform.release() def __str__(self): return 'Unable to apply patch ({0} {1} {2} {3} {4}, {5})'.format( self.patch_file, self.level, self.code, self.distro, self.release, ', '.join([str(i) for i in self.freezer_style]), ) # KCARE-509 class PreviousPatchFailedException(KcareError): status = 'previous patch failed' 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 set_monitoring_key_for_ip_license(key): url = ipv6_support.get_registration_url() + '/nagios/register_key.plain?key={0}'.format(key) try: response = http_utils.urlopen(url) res = utils.data_as_dict(utils.nstr(response.read())) code = int(res['code']) if code == 0: utils.print_wrapper('Key successfully registered') elif code == 1: utils.print_wrapper('Wrong key format or size') elif code == 2: utils.print_wrapper('No KernelCare license for that IP') else: utils.print_wrapper('Unknown error {0}'.format(code)) return code except HTTPError as e: log_utils.print_cln_http_error(e, url) return -1 @contextmanager def execute_hooks(): if config.BEFORE_UPDATE_COMMAND: process_utils.run_command(config.BEFORE_UPDATE_COMMAND, shell=True) try: yield finally: if config.AFTER_UPDATE_COMMAND: process_utils.run_command(config.AFTER_UPDATE_COMMAND, shell=True) def plugin_info(fmt=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: """ pli = _patch_level_info() update_code = pli.code loaded_pl = pli.applied_lvl license_info_result = auth.license_info() if fmt == 'json': results = { 'updateCode': str(update_code), 'autoUpdate': config.AUTO_UPDATE, 'effectiveKernel': kcare.kcare_uname(), 'realKernel': platform.release(), 'loadedPatchLevel': loaded_pl, 'uptime': int(platform_utils.get_uptime()), 'license': license_info_result, } utils.print_wrapper('--START--') utils.print_wrapper(json.dumps(results)) else: utils.print_wrapper('--START--') utils.print_wrapper(str(update_code)) utils.print_wrapper('LICENSE: ' + str(license_info_result)) utils.print_wrapper(config.AUTO_UPDATE) utils.print_wrapper(kcare.kcare_uname()) utils.print_wrapper(platform.release()) utils.print_wrapper(loaded_pl) utils.print_wrapper(platform_utils.get_uptime()) def get_update_status(): current_level = kcare.loaded_patch_level() try: latest_patch_level = get_latest_patch_level(reason='info') except UnknownKernelException: return 0 if config.IGNORE_UNKNOWN_KERNEL else 3 if current_level is None: return 1 if current_level >= latest_patch_level: return 0 return 2 if update_utils.status_gap_passed() else 0 def edf_fallback_ptype(): distro, version = platform_utils.get_distro()[:2] # From talk with @kolshanov if distro == 'CloudLinux' and version.startswith('7.'): return 'extra' else: return '' # addr -> resolved_peer_addr map CONNECTION_STICKY_MAP = {} # type: Dict[Tuple[str, int], Tuple[Optional[str], int]] def sticky_connect(self): """Function remembers IP address of host connected to and uses it for later connections. Replaces stdlib version of httplib.HTTPConnection.connect """ addr = self.host, self.port resolved_addr = CONNECTION_STICKY_MAP.get(addr, addr) # type: Tuple[Optional[str], int] self.sock = socket.create_connection(resolved_addr, self.timeout) self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) if addr not in CONNECTION_STICKY_MAP: CONNECTION_STICKY_MAP[addr] = self.sock.getpeername()[:2] if self._tunnel_host: self._tunnel() httplib.HTTPConnection.connect = sticky_connect # type: ignore[method-assign] # 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): # pragma: no cover unit # TODO move to http_utils or py23 try: import distutils.version import OpenSSL.SSL if distutils.version.StrictVersion(OpenSSL.__version__) < distutils.version.StrictVersion('0.13'): # type: ignore[attr-defined] 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 SSLSock(object): def __init__(self, sock): self._ssl_conn = sock self._makefile_refs = 0 def makefile(self, *args): self._makefile_refs += 1 return socket._fileobject(self._ssl_conn, *args, close=True) # type: ignore[attr-defined] def close(self): if not self._makefile_refs and self._ssl_conn: self._ssl_conn.close() self._ssl_conn = None def sendall(self, *args): return self._ssl_conn.sendall(*args) class PyOpenSSLHTTPSConnection(httplib.HTTPSConnection): def connect(self): httplib.HTTPConnection.connect(self) # workaround to force pyopenssl to use TLSv1.2 ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) ctx.set_options(OpenSSL.SSL.OP_NO_SSLv2 | OpenSSL.SSL.OP_NO_SSLv3) if config.CHECK_SSL_CERTS: ctx.set_verify(OpenSSL.SSL.VERIFY_PEER, dummy_verify_callback) else: ctx.set_verify(OpenSSL.SSL.VERIFY_NONE, dummy_verify_callback) ctx.set_default_verify_paths() conn = OpenSSL.SSL.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 # type: ignore[attr-defined] conn.set_tlsext_host_name(server_host.encode()) conn.do_handshake() if config.CHECK_SSL_CERTS: match_hostname(conn.get_peer_certificate(), server_host) self.sock = SSLSock(conn) httplib.HTTPSConnection = PyOpenSSLHTTPSConnection # type: ignore[misc] def _fetch_patch_level_request(khash, latest, reason, mode): if config.UPDATE_FROM_LOCAL: url = kcare.get_kernel_prefixed_url(khash, latest) return fetch.wrap_with_cache_key(auth.urlopen_auth)(url, check_license=False) b64_encoding = latest not in ['latest.v1', 'latest.v2'] perf_enabled = config.SEND_PERF_METRICS and b64_encoding for secure_boot_info, perf_metrics in [(True, perf_enabled), (False, perf_enabled), (False, False)]: # pragma: no branch sinfo = server_info.server_info(reason, secure_boot_info=secure_boot_info, perf_metrics=perf_metrics) request_param = server_info.encode_checkin_payload(sinfo, b64_encoding=b64_encoding) if b64_encoding: request_param = 'info={0}'.format(request_param) url = kcare.get_kernel_prefixed_url(khash, stickyfy(latest, mode)) + '?' + request_param max_url_length = 7000 # the entire request should fit in 8K if (secure_boot_info or perf_metrics) and len(url) > max_url_length: discard_info = 'secure boot info' if secure_boot_info else 'perf metrics' log_utils.logwarn('Check-in URL param is too large, discarding {0}'.format(discard_info)) continue try: result = fetch.wrap_with_cache_key(auth.urlopen_auth)(url, check_license=False) if config.KERNEL_ANOMALY_REPORT_ENABLE and anomaly.detect_anomaly(sinfo): # possible errors in prepare_kernel_anomaly_report and send_data_package are silently ignored data_package = anomaly.prepare_kernel_anomaly_report(sinfo) upload_name = anomaly.send_data_package(data_package) if upload_name: # will be None in case of errors in send_data_package log_utils.loginfo( 'Automatic kernel anomaly report uploaded successfully: {0}'.format(upload_name), print_msg=False ) else: log_utils.logwarn('Failed to send kernel anomaly report', print_msg=False) if data_package: data_package.remove_archive() return result except HTTPError as ex: if (secure_boot_info or perf_metrics) and (ex.code in (413, 414) or ex.code >= 500): log_utils.logwarn('Check-in request failed with error: {0}, retrying with reduced info'.format(ex)) continue raise def fetch_patch_level(reason, mode=constants.UPDATE_MODE_MANUAL): khash = kcare.get_kernel_hash() if config.PATCH_LEVEL is not None: return kcare.LegacyKernelPatchLevel(khash, int(config.PATCH_LEVEL)) for latest in PATCH_LATEST: try: response = _fetch_patch_level_request(khash, latest, reason, mode) config_handlers.set_feature_flags_from_headers(response.headers) update_all_kmod_params() pl = utils.nstr(response.read()).strip() log_utils.loginfo('fetch patch level, reason: {0}, kernel latest response: {1}'.format(reason, pl), print_msg=False) if pl and pl.startswith('{'): latest_info = json_loads_nstr(pl) required_capabilities = latest_info.get('capabilities', []) if not capabilities.has_kc_capabilities(required_capabilities): raise errors.CapabilitiesMismatch( 'Latest KernelCare patchset is incompatible with the current kernecare package version, please upgrade' ) return kcare.KernelPatchLevel(khash, latest_info['level'], latest_info['baseurl'], latest_info['release']) return kcare.LegacyKernelPatchLevel(khash, int(pl)) except NotFound: # old ePortal version < 2.20 returns 404 for latest.v3 requests pass except HTTPError as ex: # No license - no access if ex.code in (403, 401): raise KcareError('KC licence is required') raise raise UnknownKernelException() def probe_patch(level, ptype): bin_url = level.file_url(_apply_ptype(ptype, config.PATCH_BIN)) log_utils.kcarelog.info('Probing patch URL: {0}'.format(bin_url)) try: auth.urlopen_auth(bin_url, check_license=False, method='HEAD') return True except NotFound: log_utils.kcarelog.info('{0} is not available: 404'.format(bin_url)) return False except Exception as ex: # Fallback to GET in case of any error. log_utils.kcarelog.debug('HEAD request for {0} raised an error, fallback to the GET request: {1}'.format(bin_url, str(ex))) url = level.file_url(_apply_ptype(ptype, config.PATCH_BIN) + constants.SIG) log_utils.kcarelog.info('Probing patch URL: {0}'.format(url)) try: auth.urlopen_auth(url, check_license=False) except NotFound: log_utils.kcarelog.info('{0} is not available: 404'.format(url)) return False except URLError as ex: log_utils.kcarelog.info('{0} is not available: {1}'.format(url, str(ex))) return True def fetch_and_verify_kernel_file(level, name): if name == constants.KMOD_BIN: url = level.kmod_url(constants.KMOD_BIN) else: url = level.file_url(name) dst = level.cache_path(name) return fetch.fetch_url(url, dst, config.USE_SIGNATURE, hash_checker=fetch.get_hash_checker(level)) class PatchFetcher(object): # TODO move to fetch.py def __init__(self, patch_level=None): self.patch_level = patch_level # LegacyKernelPatchLevel or KernelPatchLevel def _fetch(self, name): return fetch_and_verify_kernel_file(self.patch_level, name) def is_patch_fetched(self): patch_done_path = self.patch_level.cache_path(config.PATCH_DONE) patch_bin_path = self.patch_level.cache_path(config.PATCH_BIN) patch_info_path = self.patch_level.cache_path(config.PATCH_INFO) kmod_bin_path = self.patch_level.cache_path(constants.KMOD_BIN) return ( all(os.path.isfile(path) for path in (patch_done_path, patch_bin_path, patch_info_path, kmod_bin_path)) and os.path.getsize(patch_bin_path) > 0 and os.path.getsize(kmod_bin_path) > 0 ) def fetch_patch(self): if self.patch_level is None: raise ValueError("Cannot fetch patch as no patch level is set") if not self.patch_level: # level is 0, do nothing return self.patch_level if self.is_patch_fetched(): log_utils.loginfo('Updates already downloaded') return self.patch_level log_utils.loginfo('Downloading updates') # try to upgrade patch level if isinstance(self.patch_level, kcare.LegacyKernelPatchLevel): try: resp = auth.urlopen_auth(self.patch_level.file_url(config.PATCH_BIN), method='HEAD') except NotFound: pass else: baseurl = resp.headers.get('KC-Base-Url', None) if baseurl: self.patch_level = self.patch_level.upgrade(utils.nstr(baseurl)) try: self._fetch(config.PATCH_BIN) except NotFound: raise KcareError( 'The `{0}` patch level is not found for `{1}` patch type. ' 'Please select valid patch type or patch level'.format(self.patch_level, config.PATCH_TYPE or 'default'), status='patch level not found', ) self._fetch(config.PATCH_INFO) self._fetch(constants.KMOD_BIN) self.extract_blacklist() utils.atomic_write(self.patch_level.cache_path(config.PATCH_DONE), b'', mode='wb') selinux.restore_selinux_context(constants.PATCH_CACHE) return self.patch_level def extract_blacklist(self): buf = open(self.patch_level.cache_path(config.PATCH_INFO), 'r').read() if buf: mo = BLACKLIST_RE.search(buf) if mo: utils.atomic_write(self.patch_level.cache_path(config.BLACKLIST_FILE), mo.group(1)) def fetch_fixups(self, level): """ Download fixup files for defined patch level :param level: download fixups for this patch level (usually it's a level of loaded patch) :return: None """ if level is None: return try: # never use cache for fixup files, must be downloaded from scratch resp = fetch_and_verify_kernel_file(level, config.FIXUPS_FILE) except NotFound: return # Upgrade level to a new format with baseurl to fetch fixup files baseurl = resp.headers.get('KC-Base-Url', None) if baseurl: level = level.upgrade(utils.nstr(baseurl)) fixups_fname = level.cache_path(config.FIXUPS_FILE) with open(fixups_fname, 'r') as f: fixups = set([fixup.strip() for fixup in f.readlines()]) for fixup in fixups: fetch_and_verify_kernel_file(level, fixup) selinux.restore_selinux_context(constants.PATCH_CACHE) def kcare_check(): pli = _patch_level_info() utils.print_wrapper(pli.msg) if pli.code == PLI.PATCH_NEED_UPDATE: sys.exit(0) else: sys.exit(1) def show_generic_info(): pli = _patch_level_info() kcare_info = _kcare_patch_info_json(pli) try: libcare_info = libcare.libcare_patch_info_basic() except KcareError: libcare_info = {} state = kcare.get_state() latest_update = "Unknown" if state is not None: latest_update = datetime.fromtimestamp(state['ts']).strftime('%Y-%m-%d') effective_version = kcare.kcare_uname() # count unique CVEs for accurate vulnerability counts kernel_patches = kcare_info.get('patches', []) kernel_vulnerabilities = len(utils.extract_unique_cves(kernel_patches, cve_field='kpatch-cve')) kernel_patches_count = len(kernel_patches) userspace_patches = [patch for rec in libcare_info for patch in rec.get('patches', [])] userspace_vulnerabilities = len(utils.extract_unique_cves(userspace_patches, cve_field='cve')) userspace_patches_count = sum(len(rec.get('patches', [])) for rec in libcare_info) total_patches_count = kernel_patches_count + userspace_patches_count patch_level = kcare.loaded_patch_level() if not patch_level: utils.print_wrapper("KernelCare live patching is disabled") else: utils.print_wrapper("KernelCare live patching is active") utils.print_wrapper(" - Last updated on {0}".format(latest_update)) utils.print_wrapper(" - Effective kernel version {0}".format(effective_version)) if kernel_vulnerabilities > 0: utils.print_wrapper(" - {0} kernel vulnerabilities live patched".format(kernel_vulnerabilities)) if userspace_vulnerabilities > 0: utils.print_wrapper(" - {0} userspace vulnerabilities live patched".format(userspace_vulnerabilities)) if total_patches_count == 0: utils.print_wrapper(" - This system has no applied patches") utils.print_wrapper("Type kcarectl --patch-info to learn more") 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 """ try: latest = get_latest_patch_level(reason='info', policy=constants.POLICY_REMOTE) if not latest: raise UnknownKernelException url = latest.file_url(config.PATCH_INFO) patch_info = utils.nstr(auth.urlopen_auth(url).read()) if is_json: patches, result = [], {} for chunk in patch_info.split('\n\n'): data = utils.data_as_dict(chunk) if data and 'kpatch-name' in data: patches.append(data) else: result.update(data) result['patches'] = patches # type: ignore[assignment] patch_info = json.dumps(result) utils.print_wrapper(patch_info) except HTTPError as e: log_utils.print_cln_http_error(e, e.url) return 1 except UnknownKernelException: utils.print_wrapper('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 = utils.data_as_dict(chunk) if data and 'kpatch-name' in data: patches.append(data) else: result.update(data) result['patches'] = patches saved_patch_level = kcare.read_dumped_kernel_patch_level() result['release'] = saved_patch_level['release'] if saved_patch_level else 'unknown' return result def _kcare_patch_info(pli): khash = kcare.get_kernel_hash() cache_path = kcare.get_cache_path(khash, pli.applied_lvl, config.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.", status='patch info not found', ) 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: utils.print_wrapper(pli.msg) if pli.applied_lvl is None: return utils.print_wrapper(_kcare_patch_info(pli)) else: utils.print_wrapper(json.dumps(_kcare_patch_info_json(pli), sort_keys=True)) def is_same_patch(new_patch_file): # mocked: tests/unit args = [constants.KPATCH_CTL, 'file-info', new_patch_file] new_patch_info = process_utils.check_output(args) current_patch_info = kcare._patch_info() build_time_label = 'kpatch-build-time' return kcare.get_patch_value(new_patch_info, build_time_label) == kcare.get_patch_value(current_patch_info, build_time_label) def kcare_need_update(applied_level, new_level): if new_level == 0: return False # ignore down-patching if applied_level and new_level < applied_level: return False if applied_level != new_level: return True new_patch_file = kcare.get_cache_path(kcare.get_kernel_hash(), new_level, config.PATCH_BIN) if not is_same_patch(new_patch_file): return True return False def update_sysctl(): # TODO move to platform_utils.py if config.UPDATE_SYSCTL_CONFIG: if not (os.path.isfile(SYSCTL_CONFIG) and os.access(SYSCTL_CONFIG, os.R_OK)): log_utils.kcarelog.warning('File {0} does not exist or has no read access'.format(SYSCTL_CONFIG)) return code, _, _ = process_utils.run_command(['/sbin/sysctl', '-q', '-p', SYSCTL_CONFIG], catch_stdout=True) if code != 0: log_utils.kcarelog.warning('Unable to load kcare sysctl.conf: {0}'.format(code)) def edit_sysctl_conf(remove, append): """Update SYSCTL_CONFIG accordingly the edits""" # TODO move to platform_utils.py # 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): log_utils.kcarelog.warning('File {0} has no read access'.format(SYSCTL_CONFIG)) return with open(SYSCTL_CONFIG, 'r+') as sysctl: lines = sysctl.readlines() sysctl.seek(0) for line in lines: # Do not rewrite lines that should be deleted if not any(line.startswith(r) for r in remove): sysctl.write(line) # Write additional lines for a in append: sysctl.write(a + '\n') sysctl.truncate() def detect_conflicting_modules(modules): for module in modules: if CONFLICTING_MODULES_RE.match(module): raise KcareError( "Detected '{0}' kernel module loaded. Please unload that module first".format(module), status='conflicting kernel module', ) def get_kcare_kmod_link(): return '/lib/modules/{0}/extra/kcare.ko'.format(platform_utils.get_system_uname()) def kmod_is_signed(): level = get_latest_patch_level(reason='info') kmod_file = kcare.get_cache_path(kcare.get_kernel_hash(), level, constants.KMOD_BIN) if not os.path.isfile(kmod_file): return None with open(kmod_file, 'rb') as vfd: return vfd.read()[-28:] == b'~Module signature appended~\n' def kcare_certs_enrolled(): # pragma: no cover system_keys = utils.try_to_read('/proc/keys') if system_keys is None: return None kcare_keys = [ '12ff0613c0f80cfba3b2f8eba71ebc27c5a76170', # CloudLinux key '69a6d9eed3f620d5c2e13a1d211c46510a5ad9f5', # AlmaLinux kpatch signing key ] return any(key in system_keys for key in kcare_keys) def load_kmod(kmod, **kwargs): cmd = ['/sbin/insmod', kmod] for key, value in kwargs.items(): cmd.append('{0}={1}'.format(key, value)) code, _, _ = process_utils.run_command(cmd, catch_stdout=True) if code != 0: raise KcareError( 'Unable to load kmod ({0} {1}). Try to run with `--check-compatibility` flag.'.format(kmod, code), status='kmod load error', ) def check_compatibility(): if platform_utils.is_secure_boot(): if kmod_is_signed() is False: raise KcareError('Secure boot is enabled. Not supported by KernelCare.') if kcare_certs_enrolled() is False: raise KcareError('Secure boot is enabled. No KernelCare certificates enrolled.') if platform_utils.inside_vz_container() or platform_utils.inside_lxc_container() or platform_utils.inside_docker_container(): raise KcareError( 'You are running inside a container. Kernelcare should be executed on host side instead.', status='running in container', ) def check_patch_type_compatibility(ptype): # type: (str) -> None # TODO KPT-4474 patch type autodetection cmd = process_utils.find_cmd('modinfo') has_kmodlve = process_utils.run_command([cmd, 'kmodlve'], catch_stdout=True, catch_stderr=True)[0] == 0 if has_kmodlve and ptype in ('free', 'extra'): # KPT-4269 kcarectl: free/extra patches confict with kmodlve log_utils.logerror('{0} patch type conflicts with kmodlve kernel module'.format(ptype)) sys.exit(1) def get_kmod_available_params(kcare_link): stdout = process_utils.check_output(["/sbin/modinfo", "-F", "parm", kcare_link]) available_params = [] for line in stdout.split('\n'): if line.strip(): param_name, _, _ = line.partition(':') available_params.append(param_name) return available_params def make_kmod_new_params(): return { 'kpatch_debug': 1 if config.KPATCH_DEBUG else 0, 'kmsg_output': 1 if config.KMSG_OUTPUT else 0, 'kcore_output': config.KCORE_OUTPUT_SIZE if config.KCORE_OUTPUT else 0, 'kdumps_dir': config.KDUMPS_DIR if isinstance(config.KDUMPS_DIR, str) else "", 'enable_crashreporter': 1 if config.ENABLE_CRASHREPORTER else 0, } def update_all_kmod_params(): if config.KDUMPS_DIR and not os.path.exists(config.KDUMPS_DIR): os.makedirs(config.KDUMPS_DIR) for param, val in make_kmod_new_params().items(): update_kmod_param(param, val) def update_kmod_param(kmod_param_name, param_value): params_root = '/sys/module/kcare/parameters' param_path = os.path.join(params_root, kmod_param_name) if not os.path.exists(param_path): return try: with open(param_path, 'w') as f: f.write(str(param_value)) except Exception: # pragma: no cover log_utils.kcarelog.error('failed to set %s kmod param to %s', kmod_param_name, param_value) def load_kcare_kmod(khash, level): # 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 = kcare.get_cache_path(khash, level, constants.KMOD_BIN) try: shutil.copy(kcare_file, kcare_link) except Exception: kcare_link = kcare_file if config.KDUMPS_DIR and not os.path.exists(config.KDUMPS_DIR): os.makedirs(config.KDUMPS_DIR) kmod_params = make_kmod_new_params() available_kmod_params = get_kmod_available_params(kcare_link) kmod_params = dict((k, v) for k, v in kmod_params.items() if k in available_kmod_params) load_kmod(kcare_link, **kmod_params) update_depmod() def update_depmod(uname=None): cmd = [ '/sbin/depmod', ] if uname is not None: cmd.extend(['-a', uname]) code, _, stderr = process_utils.run_command(cmd, catch_stdout=True, catch_stderr=True) if code: # We don't want to show the error to the user but want to see it in logs log_utils.logerror('Running of `{0}` failed with {1}: {2}'.format(' '.join(cmd), code, stderr), print_msg=False) def unload_kmod(modname): code, _, _ = process_utils.run_command(['/sbin/rmmod', modname], catch_stdout=True) if code != 0: raise KcareError('Unable to unload {0} kmod {1}'.format(modname, code), status='kmod unload error') def apply_fixups(khash, current_level, modules): loaded = [] for mod in ['vmlinux'] + modules: modpath = kcare.get_cache_path(khash, current_level, 'fixup_{0}.ko'.format(mod)) if os.path.exists(modpath): load_kmod(modpath) loaded.append('fixup_{0}'.format(mod)) return loaded def remove_fixups(fixups): for mod in fixups: try: unload_kmod(mod) except Exception: log_utils.kcarelog.exception('Exception while unloading module %s.' % mod) def get_freezer_style(freezer, modules): if freezer: method = freezer elif config.PATCH_METHOD: method = config.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, config.PATCH_METHOD, True else: # user doesn't provide patch method and no conflicting modules loaded return 'default', freezer, config.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 ({0}, {1}, {2}, {3})'.format(method, freezer, config.PATCH_METHOD, False), status='freezer style detection error', ) return method, freezer, config.PATCH_METHOD, False def kcare_load(khash, level, mode, freezer='', use_anchor=False): state_data = {'khash': khash, 'future': level, 'mode': mode} register_action('start', state_data) current_level = kcare.loaded_patch_level() modules = kcare.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 = kcare.get_cache_path(khash, level, config.PATCH_BIN) save_cache_latest(khash, level) description = '{0}-{1}:{2};{3}'.format( level, config.PATCH_TYPE, utils.timestamp_str(), kcare.parse_uname(level) # future server_info['ltimestamp'] ) kmod_loaded = 'kcare' in modules kmod_changed = kmod_loaded and kcare.is_kmod_version_changed(khash, level) patch_loaded = current_level is not None same_patch = patch_loaded and is_same_patch(patch_file) and kcare.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, level) if use_anchor: # KCARE-509 touch_anchor() register_action('patch', state_data) kpatch_ctl_patch(patch_file, khash, level, description, freezer_style) update_sysctl() log_utils.loginfo('Patch level {0} applied. Effective kernel version {1}'.format(level, kcare.kcare_uname())) # Update last status check timestamp update_utils.touch_status_gap_file() # do final actions when update is considered as successful register_action('wait', state_data) nohup_fork(lambda: commit_update(state_data), sleep=config.SUCCESS_TIMEOUT) def kpatch_ctl_patch(patch_file, khash, level, description, freezer_style): args = [constants.KPATCH_CTL] blacklist_file = kcare.get_cache_path(khash, level, config.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, _, _ = process_utils.run_command(args, catch_stdout=True) if code != 0: raise ApplyPatchError(code, freezer_style, level, patch_file) def kpatch_ctl_unpatch(freezer_style): # TODO KPT-4001 refactoring, extract kpatch_ctl run to a class with patch/unpatch/etc methods with logging and error handling code, stdout, stderr = process_utils.run_command( [constants.KPATCH_CTL, 'unpatch', '-m', freezer_style[0]], catch_stdout=True, catch_stderr=True ) if code != 0: log_utils.logerror('Error unpatching, kpatch_ctl stdout:\n{0}\nstderr:\n{1}'.format(stdout, stderr), print_msg=False) raise KcareError('Error unpatching [{0}] {1}'.format(code, str(freezer_style)), status='unpatch error') def register_action(action, state_data): state_data['action'] = action state_data['ts'] = int(time.time()) utils.atomic_write(os.path.join(constants.PATCH_CACHE, 'kcare.state'), str(state_data)) def update_weak_modules(kmod_link): modules_path = '/usr/lib/modules/' if not os.path.isdir(modules_path): return for entry in os.listdir(modules_path): sym_link_path = os.path.join(modules_path, entry, 'weak-updates', 'kcare.ko') if not os.path.islink(sym_link_path): continue target_path = os.readlink(sym_link_path) if target_path == kmod_link: os.unlink(sym_link_path) update_depmod(entry) @process_utils.log_all_parent_processes def kcare_unload(freezer='', force=False): current_level = kcare.loaded_patch_level() pf = PatchFetcher() try: pf.fetch_fixups(current_level) except Exception as err: if not force: raise KcareError( "Unable to retrieve fixups: '{0}'. The unloading of patches has been " "interrupted. To proceed without fixups, use the --force flag.".format(err), status='fixups retrieval error', ) modules = kcare.get_loaded_modules() freezer_style = get_freezer_style(freezer, modules) with execute_hooks(): if 'kcare' in modules: need_unpatch = current_level is not None if need_unpatch: fixups = apply_fixups(kcare.get_kernel_hash(), current_level, modules) code, stdout, stderr = process_utils.run_command( [constants.KPATCH_CTL, 'unpatch', '-m', freezer_style[0]], catch_stdout=True, catch_stderr=True ) remove_fixups(fixups) if code != 0: log_utils.logerror( 'Error unpatching, kpatch_ctl stdout:\n{0}\nstderr:\n{1}'.format(stdout, stderr), print_msg=False ) raise KcareError('Error unpatching [{0}] {1}'.format(code, str(freezer_style)), status='unpatch error') # Unload kcare module and retry once after 10 seconds if failed # Kernel module could be loaded even if patch is not applyed utils.retry(errors.check_exc(KcareError), count=1, delay=UNLOAD_RETRY_DELAY)(unload_kmod)('kcare') kmod_link = get_kcare_kmod_link() if os.path.isfile(kmod_link): os.unlink(kmod_link) # KPT-3469 fix the case with kernel upgrade where third party modules are linked under # /usr/lib/modules/{uname_r}/weak-updates/kcare.ko so that kdump work good update_weak_modules(kmod_link) def kcare_info(is_json): pli = _patch_level_info() if is_json: return _kcare_info_json(pli) else: if pli.code != 0: return pli.msg if pli.applied_lvl is not None: return kcare._patch_info() def _kcare_info_json(pli): result = {'message': pli.msg} if pli.applied_lvl is not None: result.update(utils.data_as_dict(kcare._patch_info())) result.update(kcare.parse_patch_description(result.get('kpatch-description'))) result['kpatch-state'] = pli.state return json.dumps(result) 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(): current_patch_level = kcare.loaded_patch_level() try: # this line raises UnknownKernel from the bottom of this try new_patch_level = get_latest_patch_level(reason='info') if current_patch_level: if kcare_need_update(current_patch_level, new_patch_level): 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 new_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, new_patch_level, current_patch_level, state) except UnknownKernelException: code = PLI.PATCH_UNAVALIABLE if config.STICKY_PATCH: msg = ( 'Invalid sticky patch tag {0} for kernel ({1} {2}). ' 'Please check /etc/sysconfig/kcare/kcare.conf ' 'STICKY_PATCH settings'.format(config.STICKY_PATCH, platform_utils.get_distro()[0], platform.release()) ) else: msg = 'New kernel detected ({0} {1} {2}).\nThere are no updates for this kernel yet.'.format( platform_utils.get_distro()[0], platform.release(), kcare.get_kernel_hash() ) info = PLI(code, msg, None, None, 'unavailable') return info 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: # TODO: is it ok to send request in case when no server_id found? (machine is not registered in ePortal) server_id = serverid.get_serverid() query = urlencode([('server_id', server_id), ('tag', tag)]) url = ipv6_support.get_registration_url() + '/tag_server.plain?{0}'.format(query) response = http_utils.urlopen(url) res = utils.data_as_dict(utils.nstr(response.read())) return int(res['code']) except HTTPError as e: log_utils.print_cln_http_error(e, url) return -3 except URLError as ue: log_utils.print_cln_http_error(ue, url) return -4 except Exception as ee: log_utils.logerror('Internal Error {0}'.format(ee)) return -5 def kcdoctor(): doctor_url = utils.get_patch_server_url("doctor.sh") log_utils.logdebug("Requesting doctor script from `{0}`".format(doctor_url)) doctor_filename = KCDOCTOR with tempfile.NamedTemporaryFile() as doctor_dst: try: signature = fetch.fetch_signature(doctor_url, doctor_dst.name) utils.save_to_file(http_utils.urlopen(doctor_url), doctor_dst.name) fetch.check_gpg_signature(doctor_dst.name, signature) doctor_filename = doctor_dst.name except Exception as err: log_utils.logerror('Kcare doctor error: {0}. Fallback to the local one.'.format(err)) code, _, stderr = process_utils.run_command(['bash', doctor_filename, ipv6_support.get_patch_server()], catch_stderr=True) if code: raise KcareError("Script failed with '{0}' {1}".format(stderr, code), status='doctor script failed') def check_new_kc_version(): url = utils.get_patch_server_url('{0}-new-version'.format(EFFECTIVE_LATEST)) try: http_utils.urlopen(url) except URLError: return False log_utils.loginfo( 'A new version of the KernelCare package is available. To continue to get kernel updates, please install the new version' ) return True # mocked: tests/unit/test_patch_level_info.py def get_latest_patch_level(reason, policy=constants.POLICY_REMOTE, mode=constants.UPDATE_MODE_MANUAL): """ Get patch level to apply. :param reason: what was the source of request (update, info etc.) :param policy: REMOTE -- get latest patch_level from patchserver, LOCAL -- use cached latest, LOCAL_FIRST -- if cached level is None get latest from patchserver, use cache otherwise :param mode: constants.UPDATE_MODE_MANUAL, constants.UPDATE_MODE_AUTO or constants.UPDATE_MODE_SMART :return: patch_level string """ khash = kcare.get_kernel_hash() cached_level = get_cache_latest(khash) consider_remote_ex = policy == constants.POLICY_REMOTE or (policy == constants.POLICY_LOCAL_FIRST and cached_level is None) try: remote_level = fetch_patch_level(reason, mode) except errors.CapabilitiesMismatch as e: if cached_level is None: # error will be logged in __main__.py raise log_utils.logwarn(str(e)) log_utils.logwarn('Using previously downloaded patches') # force using already downloaded patches policy = constants.POLICY_LOCAL except Exception as ex: if consider_remote_ex: raise else: log_utils.kcarelog.warning('Unable to send data: {0}'.format(ex)) if policy == constants.POLICY_REMOTE: level = remote_level else: level = cached_level if cached_level is None: if policy == constants.POLICY_LOCAL: level = kcare.LegacyKernelPatchLevel(khash, 0) elif policy == constants.POLICY_LOCAL_FIRST: level = remote_level else: raise KcareError('Unknown policy, choose one of: REMOTE, LOCAL, LOCAL_FIRST') return level def update_patch_type(ptype): if ptype == 'edf': # The only way user can get here if call kcarectl --set-patch-type # we don't support this anyway and can silently ignore return config.PATCH_TYPE = '' if ptype == 'default' else ptype if probe_patch(fetch_patch_level(reason='probe'), config.PATCH_TYPE): config_handlers.update_config(PATCH_TYPE=config.PATCH_TYPE) if config.PATCH_TYPE in ('free', 'extra') and platform_utils.is_cpanel(): gid = config.FORCE_GID or CPANEL_GID edit_sysctl_conf( ('fs.enforce_symlinksifowner', 'fs.symlinkown_gid'), ('fs.enforce_symlinksifowner=1', 'fs.symlinkown_gid={0}'.format(gid)), ) log_utils.loginfo("'{0}' patch type selected".format(ptype)) else: raise KcareError("'{0}' patch type is unavailable for your kernel".format(ptype), status='patch type unavailable') # log_all_parent_processes must be outermost to run first @process_utils.log_all_parent_processes @update_utils.track_update_status('kernel') def do_update(freezer, mode, policy=constants.POLICY_REMOTE): """ :param mode: constants.UPDATE_MODE_MANUAL, constants.UPDATE_MODE_AUTO or constants.UPDATE_MODE_SMART :param policy: REMOTE -- download latest and patches from patchserver, LOCAL -- use cached files, LOCAL_FIRST -- download latest and patches if cached level is None, use cache in other cases :param freezer: freezer mode """ check_patch_type_compatibility(config.PATCH_TYPE) if policy == constants.POLICY_REMOTE: check_new_kc_version() try: level = get_latest_patch_level(reason='update', policy=policy, mode=mode) except UnknownKernelException as e: if mode in (constants.UPDATE_MODE_AUTO, constants.UPDATE_MODE_SMART) and config.IGNORE_UNKNOWN_KERNEL: msg = str(e) log_utils.kcarelog.warning(msg) return raise current_level = kcare.loaded_patch_level() # Cron triggers `kcarectl -q --auto-update` regardless of AUTO_UPDATE. # When AUTO_UPDATE=NO, return before touching the on-disk cache so the # currently-applied patch's cache directory is not rotated out by # subsequent fetches (CACHE_ENTRIES rotation would otherwise evict it # and break --patch-info). if mode == constants.UPDATE_MODE_AUTO and not config.AUTO_UPDATE: return pf = PatchFetcher(level) pf.fetch_patch() if not kcare_need_update(applied_level=current_level, new_level=level): log_utils.loginfo('No updates are needed for this kernel') return # Rotate crash report dumps try: utils.clean_directory(config.KDUMPS_DIR, keep_n=3, pattern="kcore*.dump") utils.clean_directory(config.KDUMPS_DIR, keep_n=3, pattern="kmsg*.log") except Exception: log_utils.kcarelog.exception('Error during crash reporter cleanup') khash = kcare.get_kernel_hash() with execute_hooks(): pf.fetch_fixups(current_level) kcare_load(khash, level, mode, freezer, use_anchor=mode == constants.UPDATE_MODE_SMART) kcare.dump_kernel_patch_level(level) clear_cache(khash, level) """ This is needed to support sticky keys as per https://cloudlinux.atlassian.net/browse/KCARE-953 """ def get_sticky(mode): count = sum( ( bool(config.STICKY_PATCH), bool(config.UPDATE_DELAY or config.AUTO_UPDATE_DELAY), bool(config.STICKY_PATCHSET or config.AUTO_STICKY_PATCHSET), ) ) if count > 1: raise KcareError( 'Invalid configuration: conflicting settings STICKY_PATCH,' ' [AUTO_]UPDATE_DELAY or [AUTO_]STICKY_PATCHSET. There should be only one of them', status='conflicting sticky settings', ) if config.STICKY_PATCH: return config.STICKY_PATCH if mode != constants.UPDATE_MODE_MANUAL: delay = config.AUTO_UPDATE_DELAY or config.UPDATE_DELAY patchset = config.AUTO_STICKY_PATCHSET or config.STICKY_PATCHSET else: delay = config.UPDATE_DELAY patchset = config.STICKY_PATCHSET if delay: return delay if patchset: return 'release-' + patchset def _stickyfy(prefix, fname): return prefix + '.' + fname def stickyfy(file, mode): """ Used to add sticky prefix to satisfy KCARE-953 :param file: name of the file to stickify :return: stickified file. """ s = get_sticky(mode) if not s: return file if s != 'KEY': return _stickyfy(s, file) server_id = serverid.get_serverid() if not server_id: log_utils.kcarelog.info('Patch set to STICKY_PATCH=KEY, but server is not registered with the key') sys.exit(-4) try: response = http_utils.urlopen(ipv6_support.get_registration_url() + '/sticky_patch.plain?server_id={0}'.format(server_id)) except HTTPError as e: log_utils.print_cln_http_error(e, e.url) sys.exit(-5) res = utils.data_as_dict(utils.nstr(response.read())) code = int(res['code']) if code == 0: return _stickyfy(res['prefix'], file) elif code == 1: return file elif code == 2: log_utils.kcarelog.info('Server ID is not recognized. Please check if the server is registered') sys.exit(-1) log_utils.kcarelog.info('Error: ' + res['message']) sys.exit(-3) ################################# # from python 2.7.17 ssl stdlib # ################################# def _dnsname_match(dn, hostname, max_wildcards=1): # pragma: no cover """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 pieces = dn.split(r'.') leftmost = pieces[0] remainder = pieces[1:] wildcards = leftmost.count('*') if wildcards > max_wildcards: # Issue #17980: avoid denials of service by refusing more # than one wildcard per fragment. A survery 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) # match_hostname tweaked to get dns names from pyopenssl x509 cert object def match_hostname(cert, hostname): # pragma: no cover san = [] for i in range(cert.get_extension_count()): e = cert.get_extension(i) if e.get_short_name() == 'subjectAltName': san = [it.strip().split(':', 1) for it in str(e).split(',')] if not cert: raise ValueError( 'empty or no certificate, match_hostname needs a ' 'SSL socket or SSL context with either ' 'CERT_OPTIONAL or CERT_REQUIRED' ) dnsnames = [] 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 dNSName entry # in subjectAltName cn = cert.get_subject().commonName if _dnsname_match(cn, hostname): return dnsnames.append(value) if len(dnsnames) > 1: raise CertificateError("hostname {0} doesn't match either of {1}".format(hostname, ', '.join(map(repr, dnsnames)))) elif len(dnsnames) == 1: raise CertificateError("hostname {0} doesn't match {1}".format(hostname, dnsnames[0])) else: raise CertificateError('no appropriate commonName or subjectAltName fields were found') ##################### # end of ssl stdlib # ##################### def main(): parser = ArgumentParser(prog="kcarectl", description='Manage KernelCare patches for your kernel') parser.add_argument('--debug', help='', action='store_true') 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( '--app-info', help='Display information about KernelCare agent. 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 available, 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('--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('--status', help='Return status of updates', action='store_true') 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', ) 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( '--fallback', help='With --doctor, force the legacy kcdoctor.sh flow instead of the v2 upload path', action='store_true', ) parser.add_argument( '--kernel-anomaly-report', help='Submits a kernel anomaly report to CloudLinux for analysis and bug-fixes', action='store_true', ) parser.add_argument('--no-send', help='Skip sending artifacts', action='store_true', dest='save_only') parser.add_argument('--keep-local', help="Don't delete generated kernel anomaly report after sending", 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( '--server-info', help='Provides information about the host in JSON format.', action='store_true', ) parser.add_argument( '--json', help="Return '--plugin-info', '--latest-patch-info', '--patch-info', '--app-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, ) parser.add_argument('--check-compatibility', help='Check compatibility.', action='store_true') parser.add_argument('--clear-cache', help='Clear all cached files', action='store_true') 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='Suppress messages, provide only errors and warnings to stderr', action='store_true', required=False ) parser.add_argument('--has-flags', help='Check agent features') parser.add_argument('--force', help='Force action and ignore several restristions.', action="store_true") parser.add_argument('--set-config', help='Change configuration option', action='append', metavar='KEY=VALUE') if not config.LIBCARE_DISABLED: parser.add_argument( '--disable-libcare', help='Disable libcare services', dest='enable_libcare', action='store_const', const=False ) parser.add_argument( '--enable-libcare', help='Enable libcare services', dest='enable_libcare', action='store_const', const=True ) parser.add_argument( '--lib-update', help='Download latest patches and apply them to the current userspace libraries', action='store_true' ) parser.add_argument('--lib-unload', '--userspace-unload', help='Unload userspace patches', action='store_true') parser.add_argument('--lib-replugin', '--userspace-replugin', help='Reload libcare-server plugin', action='store_true') parser.add_argument('--lib-auto-update', help='Check if update is available, if so -- update', action='store_true') parser.add_argument('--lib-info', '--userspace-info', help='Display information about KernelCare+.', action='store_true') parser.add_argument( '--lib-patch-info', '--userspace-patch-info', help='Return the list of applied userspace patches', action='store_true' ) parser.add_argument('--lib-version', '--userspace-version', help='Return safe package version', metavar='PACKAGENAME') parser.add_argument( '--userspace-update', metavar='USERSPACE_PATCHES', nargs='?', const="", help='Download latest patches and apply them to the corresponding userspace processes', ) parser.add_argument( '--userspace-auto-update', help='Download latest patches and apply them to the corresponding userspace processes', action='store_true', ) parser.add_argument('--userspace-status', help='Return status of userspace updates', action='store_true') parser.add_argument( '--lib-tag', '--userspace-tag', metavar='TAG', help='Apply userspace patches for a specific tag (DDMMYY, ' 'YYYY-MM-DD, Nd, Nh, release-) into an isolated ' 'cache, leaving the default storage untouched. Use together with ' '--lib-update or --userspace-update.', action='store', default=None, required=False, ) args = parser.parse_args() config_handlers.set_settings_from_config_file() if not config.LIBCARE_DISABLED: config.FLAGS += ['libcare-enabled'] if args.has_flags is not None: if set(filter(None, args.has_flags.split(','))).issubset(config.FLAGS): return 0 else: return 1 # do not remove args.auto_update! # once added to machine, kcare-cron is never changed by package update; # old clients has no -q option in their cron, # so auto_update default silent mode must be saved forever if args.quiet or args.auto_update: if config.SILENCE_ERRORS: config.PRINT_LEVEL = constants.PRINT_CRITICAL else: config.PRINT_LEVEL = constants.PRINT_ERROR elif args.debug: config.PRINT_LEVEL = constants.PRINT_DEBUG if not args.uname: if os.getuid() != 0: print('Please run as root', file=sys.stderr) # noqa: T201 return 1 level = logging.INFO if args.quiet: level = logging.WARNING elif args.debug: level = logging.DEBUG # should be after root role check to create a log file with correct rights log_utils.initialize_logging(level) if not config.IGNORE_FEATURE_FLAGS: # run this after initialize_logging to log settings overrides config_handlers.set_feature_flags_from_cache() if args.clear_cache: utils.clear_all_cache() if args.set_patch_level: if args.set_patch_level >= 0: config.PATCH_LEVEL = str(args.set_patch_level) # type: ignore config_handlers.update_config(PATCH_LEVEL=config.PATCH_LEVEL) else: config.PATCH_LEVEL = None config_handlers.update_config(PATCH_LEVEL='') if args.set_sticky_patch is not None: config_handlers.update_config(STICKY_PATCH=args.set_sticky_patch) config.STICKY_PATCH = args.set_sticky_patch if args.nosignature: config.USE_SIGNATURE = False if args.no_check_cert: config.CHECK_SSL_CERTS = False if args.kpatch_debug: config.KPATCH_DEBUG = True if args.check_compatibility: check_compatibility() # EDF do nothing if args.edf_enabled: warnings.warn('Flag --edf-enabled has been deprecated and will be not available in future releases.', DeprecationWarning) elif args.edf_disabled: if config.PATCH_TYPE == 'edf': args.set_patch_type = ('' if config.PREV_PATCH_TYPE == 'edf' else config.PREV_PATCH_TYPE) or 'default' args.update = True if args.prefix: config.PREFIX = args.prefix if args.test: warnings.warn('Flag --test has been deprecated and will be not available in future releases.', DeprecationWarning) config.PREFIX = 'test' config.PREFIX = config.PREFIX.strip('/') if config.PREFIX and config.PREFIX not in EXPECTED_PREFIX: log_utils.kcarelog.warning('Prefix `{0}` is not in expected one {1}.'.format(config.PREFIX, ' '.join(EXPECTED_PREFIX))) if args.local: config.UPDATE_FROM_LOCAL = True config.PATCH_SERVER = 'file:' + args.local if args.set_patch_type: update_patch_type(args.set_patch_type) if config.PATCH_TYPE == 'edf': config.PATCH_TYPE = edf_fallback_ptype() warnings.warn('edf patches are deprecated. Fallback to {0}'.format(config.PATCH_TYPE or 'default'), DeprecationWarning) if args.app_info: utils.print_wrapper(platform_utils.app_info(is_json=args.json)) return if args.server_info: info = server_info.server_info(reason='debug', secure_boot_info=True, perf_metrics=True) utils.print_wrapper(json.dumps(info)) return apply_ptype(config.PATCH_TYPE) if args.doctor: doctor.send_doctor_report(kcdoctor, force_fallback=args.fallback) return if args.kernel_anomaly_report: data_package = anomaly.prepare_kernel_anomaly_report( server_info.server_info(reason='debug', secure_boot_info=True, perf_metrics=True) ) local_path_message = 'Kernel anomaly report file generated: {0}'.format(data_package.archive_path) if args.save_only: utils.print_wrapper(local_path_message) else: upload_name = anomaly.send_data_package(data_package) if upload_name: log_utils.loginfo('Kernel anomaly report uploaded successfully: {0}'.format(upload_name)) else: # pragma: no cover log_utils.logwarn('Failed to send kernel anomaly report', print_msg=True) if args.keep_local: utils.print_wrapper(local_path_message) elif data_package: # pragma: no branch data_package.remove_archive() if args.plugin_info: if args.json: plugin_info(fmt='json') else: plugin_info() return if args.enable_auto_update: config_handlers.update_config(AUTO_UPDATE='YES') return if args.disable_auto_update: config_handlers.update_config(AUTO_UPDATE='NO') return if args.set_config: config_handlers.update_config_from_args(args.set_config) return if args.set_monitoring_key: return set_monitoring_key_for_ip_license(args.set_monitoring_key) if args.unregister: auth.unregister() if args.register: if config.PATCH_TYPE == 'free': config_handlers.update_config(PATCH_TYPE='extra') return auth.register(args.register, args.register_autoretry) if args.license_info: # license_info returns zero if no valid license found and non-zero otherwise if auth.license_info() != 0: return 0 else: return 1 if args.tag is not None: return tag_server(args.tag) if args.version: utils.print_wrapper(constants.VERSION) if getattr(args, 'enable_libcare', None) is not None: libcare.set_libcare_status(args.enable_libcare) return 0 if not config.LIBCARE_DISABLED: lib_tag_kw = {'tag': args.lib_tag} if args.lib_tag else {} if args.userspace_status: return libcare.get_userspace_update_status() if args.lib_update: if libcare.do_userspace_update(**lib_tag_kw) is not None: log_utils.loginfo('Userspace patches are applied.') if args.lib_auto_update: libcare.do_userspace_update(mode=constants.UPDATE_MODE_AUTO, **lib_tag_kw) elif args.lib_unload: libcare.libcare_unload() log_utils.loginfo('Userspace patches are unloaded.') if args.lib_replugin: # pragma: no cover unit libcare.libcare_replugin() log_utils.loginfo('Libcare plugin reloaded.') if args.lib_info: utils.print_wrapper(libcare.libcare_info()) if args.lib_patch_info: utils.print_wrapper(libcare.libcare_patch_info()) if args.lib_version and libcare.libcare_server_started(): utils.print_wrapper(libcare.libcare_version(args.lib_version)) if args.userspace_update is not None: if args.userspace_update == '': # Get from config or defaults limit = config.USERSPACE_PATCHES or list(libcare.get_userspace_map().keys()) else: limit = [ptch.strip().lower() for ptch in args.userspace_update.split(',')] if libcare.do_userspace_update(limit=sorted(limit), **lib_tag_kw) is not None: log_utils.loginfo('Userspace patches are applied.') if args.userspace_auto_update: libcare.do_userspace_update(mode=constants.UPDATE_MODE_AUTO, limit=None, **lib_tag_kw) if args.info: utils.print_wrapper(kcare_info(is_json=args.json)) freezer = '' if args.nofreeze: warnings.warn('Flag --nofreeze has been deprecated and will be not available in future releases.', DeprecationWarning) freezer = 'none' if args.freezer: freezer = args.freezer if args.smart_update: do_update(freezer, mode=constants.UPDATE_MODE_SMART, policy=config.UPDATE_POLICY) if args.update: do_update(freezer, mode=constants.UPDATE_MODE_MANUAL) log_utils.loginfo('Kernel is safe') if args.uname: utils.print_wrapper(kcare.kcare_uname()) if args.unload: kcare_unload(freezer, force=args.force) log_utils.loginfo('KernelCare protection disabled. Your kernel might not be safe') if args.auto_update: config.CHECK_CLN_LICENSE_STATUS = False # wait to prevent spikes at the beginning of each minute KPT-1874 # bandit warns about using random.uniform for security which is not the case here time.sleep(random.uniform(0, 60)) # nosec B311 do_update(freezer, mode=constants.UPDATE_MODE_AUTO) if args.patch_info: patch_info(is_json=args.json) if args.status: return get_update_status() if args.latest_patch_info: kcare_latest_patch_info(is_json=args.json) if args.check: kcare_check() # No arg were provided if len(sys.argv) == 1: show_generic_info() ipv6_support.py000064400000016303152533440750007611 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import json import numbers import os import time from . import config, constants, http_utils, log_utils, serverid, utils from .py23 import json_loads_nstr if False: # pragma: no cover from typing import Optional # noqa: F401 # from CLN sources: # code: 0 -> valid license (for key based, or ip based) # code: 1 -> trial license, unexpired # code: 2 -> no valid license, but there is expired trial license # code: 3 -> no valid or trial license CLN_VALID_LICENSE = 0 CLN_TRIAL_ACTIVE_LICENSE = 1 CLN_NO_LICENSE = 3 CACHE_FILE = os.path.join(constants.PATCH_CACHE, 'ipv6_preference.json') CACHE_TTL_SECONDS = 24 * 60 * 60 class IPProtoSelector(object): def is_ipv6_preferred(self): # type: () -> bool """ Choose ipv6 if it is more suitable. Checks order: - check config values (it is faster) - eportal setup and FORCE_IPVx - then check each proto availability using HEAD requests - then check if we have server_id, it means we don't expect an ip license - and finally we need to check if there is an ip license """ # each case is in a separate block for better coverage check if config.FORCE_IPV4: log_utils.logdebug('decided to use ipv4 because of config values') return False elif not config.PATCH_SERVER.endswith('kernelcare.com'): # eportal setup, we don't need to change urls log_utils.logdebug('decided to use ipv4 because of config values') return False elif config.FORCE_IPV6: log_utils.logdebug('decided to use ipv6 because of config values') return True # further checks are more expensive, use cached value if it is set cached = _read_cache() if cached is not None: log_utils.logdebug('decided to use {0} from on-disk cache'.format('ipv6' if cached else 'ipv4')) return cached result = None if not self._is_url_reachable(config.PATCH_SERVER_IPV6): log_utils.logdebug('decided to use ipv4 because ipv6 is not available') result = False elif not self._is_url_reachable(config.PATCH_SERVER): log_utils.logdebug('decided to use ipv6 because ipv4 is not available') result = True elif serverid.get_serverid(): log_utils.logdebug('decided to use ipv4 because server id was found') result = False if result is not None: _write_cache(result) return result ipv4_license = self._get_cln_license(ipv6=False) ipv6_license = self._get_cln_license(ipv6=True) if ipv4_license == CLN_VALID_LICENSE: log_utils.logdebug('decided to use ipv4 because ipv4 license was found') result = False elif ipv6_license == CLN_VALID_LICENSE: log_utils.logdebug('decided to use ipv6 because ipv6 license was found') result = True elif ipv4_license == CLN_TRIAL_ACTIVE_LICENSE: log_utils.logdebug('decided to use ipv4 because ipv4 trial license was found') result = False elif ipv6_license == CLN_TRIAL_ACTIVE_LICENSE: log_utils.logdebug('decided to use ipv6 because ipv6 trial license was found') result = True else: # we don't have any license yet result = False _write_cache(result) return result @staticmethod def _is_url_reachable(url): # type: (str) -> bool request = http_utils.http_request(url, method='HEAD', auth_string=None) # type: ignore[no-untyped-call] try: http_utils.urlopen(request, timeout=10, retry_on_500=False, retry_count=2) # type: ignore[no-untyped-call] return True except Exception as e: log_utils.logdebug('error during HEAD request to {0}: {1}'.format(url, str(e))) return False @staticmethod def _get_cln_license(ipv6): # type: (bool) -> int base_url = config.REGISTRATION_URL_IPV6 if ipv6 else config.REGISTRATION_URL # a comment from auth.py: # do not retry in case of 500 from CLN! # otherwise, CLN will die in pain because of too many requests url = base_url + '/check.plain' content = utils.nstr(http_utils.urlopen(url, retry_on_500=False).read()) # type: ignore[no-untyped-call] info = utils.data_as_dict(content) if not info or not info.get('code'): log_utils.kcarelog.error('Unexpected CLN response: {0}'.format(content)) return CLN_NO_LICENSE try: return int(info['code']) except ValueError: return CLN_NO_LICENSE ip_proto_selector = IPProtoSelector() def _read_cache(): # type: () -> Optional[bool] content = utils.try_to_read(CACHE_FILE) if not content: return None try: data = json_loads_nstr(content) except (ValueError, TypeError): log_utils.logwarn('ipv6 preference cache: malformed json {0!r}'.format(content), print_msg=False) return None if not isinstance(data, dict): log_utils.logwarn('ipv6 preference cache: unexpected payload {0!r}'.format(data), print_msg=False) return None prefer_ipv6 = data.get('prefer_ipv6') if not isinstance(prefer_ipv6, bool): log_utils.logwarn('ipv6 preference cache: unexpected prefer_ipv6 {0!r}'.format(prefer_ipv6), print_msg=False) return None cached_ts = data.get('ts') # numbers.Integral covers int and Python 2 long (the latter matters # once a 32-bit Python 2 host crosses the 2038 sys.maxint boundary); # exclude bool explicitly since it would silently round-trip as 0/1 if not isinstance(cached_ts, numbers.Integral) or isinstance(cached_ts, bool): log_utils.logwarn('ipv6 preference cache: malformed ts {0!r}'.format(cached_ts), print_msg=False) return None # negative age means the system clock jumped backwards (NTP correction etc.) # since we wrote the cache; treat it as a miss rather than as a fresh entry. age = time.time() - int(cached_ts) if age < 0 or age > CACHE_TTL_SECONDS: log_utils.logdebug('ipv6 preference cache: stale entry (age={0:.0f}s, ttl={1}s)'.format(age, CACHE_TTL_SECONDS)) return None return prefer_ipv6 def _write_cache(result): # type: (bool) -> None data = { 'prefer_ipv6': result, 'ts': int(time.time()), } try: utils.atomic_write(CACHE_FILE, json.dumps(data), ensure_dir=True) except (OSError, IOError) as e: log_utils.logwarn('failed to write ipv6 preference cache: {0}'.format(e), print_msg=False) def clear_cache(): # type: () -> None """Drop the on-disk ipv6 preference cache.""" try: os.unlink(CACHE_FILE) except OSError: pass def get_patch_server(): # type: () -> str return config.PATCH_SERVER_IPV6 if ip_proto_selector.is_ipv6_preferred() else config.PATCH_SERVER def get_registration_url(): # type: () -> str return config.REGISTRATION_URL_IPV6 if ip_proto_selector.is_ipv6_preferred() else config.REGISTRATION_URL process_utils.py000064400000010464152533440750010031 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import functools import os import subprocess import textwrap from . import log_utils, utils if False: # pragma: no cover from typing import Any, List, Optional, Tuple, Union # noqa: F401 @utils.cached def find_cmd(name, paths=None, raise_exc=True): # type: (str, Optional[tuple[str, ...]], bool) -> Optional[str] paths = paths or ('/usr/sbin', '/sbin', '/usr/bin', '/bin') for it in paths: fname = os.path.join(it, name) if os.path.isfile(fname): return fname if raise_exc: raise Exception('{0} could not be found at {1}'.format(name, paths)) else: return None def run_command(command, catch_stdout=False, catch_stderr=False, shell=False, check=False): # mocked: tests/unit/conftest.py stdout = subprocess.PIPE if catch_stdout else None stderr = subprocess.PIPE if catch_stderr else None # We need to eventually keep shell=True as it might break customer's hooks, skip this bandit check. p = subprocess.Popen(command, stdout=stdout, stderr=stderr, shell=shell) # nosec B602 stdout_captured, stderr_captured = p.communicate() # type: Union[bytes, str, None], Union[bytes, str, None] code = p.returncode if stdout_captured is not None: stdout_captured = utils.nstr(stdout_captured) if stderr is not None: stderr_captured = utils.nstr(stderr_captured) log_utils.logdebug( textwrap.dedent( """ Call result for `{cmd}`: exit code {exit_code} === STDOUT === {stdout} === STDERR === {stderr} === END === """ ).format(exit_code=p.returncode, stdout=stdout_captured, stderr=stderr_captured, cmd=' '.join(command)) ) if check and code: exc = subprocess.CalledProcessError(code, command) exc.output = stdout_captured exc.stderr = stderr_captured raise exc return code, stdout_captured, stderr_captured def check_output(args, check=False): # type: (list[str], bool) -> Any _, stdout, _ = run_command(args, catch_stdout=True, check=check) return stdout def _get_parent_pid_and_process_name(pid): # type: (int) -> tuple[Optional[int], Optional[str]] try: # use two subprocesses for ppid and comm to prevent parsing problems (there could be any symbols in comm) cmd_ppid = ['ps', '--no-headers', '-o', 'ppid', '-p', str(pid)] code, stdout, _ = run_command(cmd_ppid, catch_stdout=True) if code: log_utils.loginfo("Could not retrieve process parent PID for PID {pid}".format(pid=pid), print_msg=False) return None, None ppid = stdout.strip() cmd_comm = ['ps', '--no-headers', '-o', 'comm', '-p', str(pid)] code, stdout, _ = run_command(cmd_comm, catch_stdout=True) if code: log_utils.loginfo("Could not retrieve process name for PID {pid}".format(pid=pid), print_msg=False) return None, None name = stdout.strip() return int(ppid), name except Exception as e: log_utils.loginfo( "Could not retrieve process name and parent PID for PID {pid}, error: {err}".format(pid=pid, err=e), print_msg=False ) return None, None def log_all_parent_processes(func): """Decorator that logs parent process chain before calling the wrapped function.""" @functools.wraps(func) def wrapper(*args, **kwargs): _log_all_parent_processes() return func(*args, **kwargs) return wrapper def _log_all_parent_processes(): # type: () -> None process_chain = [] # type: list[tuple[int, Optional[str]]] current_pid = os.getpid() while current_pid != 1 and current_pid != 0: ppid, process_name = _get_parent_pid_and_process_name(current_pid) process_chain.append((current_pid, process_name)) if ppid is None: break current_pid = ppid log_utils.loginfo("Agent parent processes chain:", print_msg=False) for level, (pid, name) in enumerate(reversed(process_chain)): prefix = "-" * level + "->" log_utils.loginfo( '{prefix} "{name}" (pid: {pid})'.format(prefix=prefix, name=name or 'unknown', pid=pid or 'unknown'), print_msg=False ) constants.py000064400000002636152533440750007151 0ustar00# Copyright (c) Cloud Linux Software, Inc # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT import os import sys PRINT_DEBUG = 0 PRINT_INFO = 1 PRINT_WARN = 2 PRINT_ERROR = 3 PRINT_CRITICAL = 4 PY2 = sys.version_info[0] == 2 PATCH_CACHE = '/var/cache/kcare' FEATURE_FLAGS_CACHE = os.path.join(PATCH_CACHE, 'feature_flags.json') UPDATE_STATUS_PATH = os.path.join(PATCH_CACHE, 'update_status.json') UPDATE_ERROR_MAX_LENGTH = 100 CACHE_KEY_HEADER = 'Kc-Cache-Key' CACHE_KEY_DUMP_PATH = '/etc/sysconfig/kcare/cache_key' POLICY_REMOTE = 'REMOTE' POLICY_LOCAL = 'LOCAL' POLICY_LOCAL_FIRST = 'LOCAL_FIRST' KC_PATCH_VERSION = '3' LOG_FILE = '/var/log/kcarectl.log' SIG = '.sig' SIG_JSON = '.json-sig' # false positives by bandit based on the `TOKEN` keyword in name AUTH_TOKEN_HEADER = 'Kc-Auth-Token' # nosec hardcoded_password_string AUTH_TOKEN_DUMP_PATH = '/etc/sysconfig/kcare/auth_token' # nosec hardcoded_password_string # urlopen retry options RETRY_DELAY = 3 RETRY_MAX_DELAY = 30 RETRY_BACKOFF = 2 RETRY_COUNT = 4 # helper vars for tests SKIP_SYSTEMCTL_CHECK = False SYSTEMCTL = '/usr/bin/systemctl' UPDATE_MODE_MANUAL = 'manual' # update is launched manually by `kcarectl -u` UPDATE_MODE_AUTO = 'auto' # update is launched by cron UPDATE_MODE_SMART = 'smart' # update is launched by kcare daemon KMOD_BIN = 'kcare.ko' KPATCH_CTL = '/usr/libexec/kcare/kpatch_ctl' VERSION = '3.8-1.el8'