Server IP : 158.178.228.73 / Your IP : 80.80.80.153 Web Server : Apache/2.4.37 (Oracle Linux Server) OpenSSL/1.1.1k System : Linux ust-wp1-prod 5.15.0-308.179.6.el8uek.x86_64 #2 SMP Wed Apr 23 10:46:57 PDT 2025 x86_64 User : tomasFtp ( 1007) PHP Version : 8.4.8 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : ON Directory : /proc/thread-self/root/proc/self/root/proc/thread-self/root/usr/sbin/ |
Upload File : |
#!/usr/libexec/platform-python -s # See included LICENSE file for additional license and copyright information. from __future__ import print_function import sys from six.moves import input # Disable Launchpad's apport traceback hooks sys.excepthook = sys.__excepthook__ from io import StringIO, BytesIO # noqa: #402 if sys.version_info[0] < 3: from cStringIO import StringIO as NativeStringIO else: from io import StringIO as NativeStringIO import datetime import errno # noqa: #402 import fcntl # noqa: #402 import grp # noqa: #402 import hashlib # noqa: #402 import logging # noqa: #402 import logging.handlers # noqa: #402 import os # noqa: #402 import posixpath # noqa: #402 import pwd # noqa: #402 import pycurl # noqa: #402 import random # noqa: #402 import re # noqa: #402 import shutil # noqa: #402 import signal # noqa: #402 import stat # noqa: #402 import subprocess # noqa: #402 import tempfile # noqa: #402 import textwrap # noqa: #402 import time # noqa: #402 import traceback # noqa: #402 from six.moves.urllib import parse # noqa: #402 import six # noqa: #402 import yaml # noqa: #402 from optparse import OptionParser, Option, SUPPRESS_HELP # noqa: #402 # Specially deal with dbus have_dbus = 0 try: import dbus import dbus.service import dbus.mainloop.glib if dbus.version >= (0, 80, 0): from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) dbus.SystemBus() have_dbus = 1 except Exception: pass import Uptrack # noqa: #402 import UptrackDepSolver # noqa: #402 from Uptrack import is_offline from uptrack import security_modules_check # noqa: #402 try: from offline.Offline import * except: from OfflineStub import * __version__ = Uptrack.__version__ LOGFILE = '/var/log/uptrack.log' LOGUSER = 'root' LOGGROUP = 'adm' LOGMODE = 0o640 KEYRING = '/usr/share/uptrack/uptrack.gpg' UPTRACK_GPG_HOMEDIR = '/etc/uptrack' SERVER_KEYRING = '/usr/share/uptrack/uptrack-server.gpg' SERVER_KEY_FINGERPRINT = "9C99586684B64DE53F0885700EE0EADBD74EE7FC" API_VERSION_FILE = "/usr/share/uptrack/ksplice-tools-api-version" KSPLICE_DEBUG_FILE = '/var/run/ksplice/debug' MODPROBE_LOCK = '/var/run/.modprobe.ksplice.lock' DEPMOD_NEEDED_FILE = os.path.join(Uptrack.UPTRACK_CACHE_DIR, "depmod-needed") UPTRACK_PACKAGES_PROTOCOL_VERSION = '2' CODE_BUSY_RETRIES = 2 # for a total of 3 tries CODE_BUSY_MAX_DELAY = 5.0 # seconds MAX_RETRIES = CODE_BUSY_RETRIES + 1 # 1 for trying to remove modules # What should this be? HTTP_CODE_EXPIRED = 420 # The number of seconds to wait before giving up on acquiring the # repository lock. LOCK_TIMEOUT = 10 INIT = 'Init' UPGRADE = 'Upgrade' # Note: We depend on these constants having these particular values, # because they occasionally get shown directly to users. (Probable # future i18n implications here) INSTALL = 'Install' REMOVE = 'Remove' SHOW = 'Show' AUTOGEN_FLAG_FILE = '/var/lib/uptrack/autogen' AUTOGEN_URL = 'https://updates.ksplice.com/cgi/code?terms=1&noninteractive=1&noemail=1' TOS_FILE = '/usr/share/doc/uptrack/tos' alert = None desupported = False tray_icon_error = None config = None local = None repo = None in_offline_mode = False HASH_PRIORITIES = ['SHA-256', 'SHA-1'] now = datetime.datetime.now(datetime.timezone.utc) def hash_valid(actual, expected): for hash_type in HASH_PRIORITIES: if hash_type in expected and expected[hash_type]: return actual[hash_type] == expected[hash_type] return False def hash_file(filename): text = Uptrack.read_file(filename, 'rb') hashers = { 'SHA-1': hashlib.sha1, 'SHA-256': hashlib.sha256 } hashes = {} for name, fn in hashers.items(): hashes[name] = fn(text).hexdigest() return hashes def makeUpdate(item, local_dir, remote_dir, order): return Update(local_dir=local_dir, remote_dir=remote_dir, id=item['ID'], filename=item['Filename'], name=item['Name'], sha1=item['SHA-1'], sha256=item.get('SHA-256', None), targets=item['Targets'], order=order) def toModuleName(filename, canonicalize=True): # Logic matches smells_like_module from module-init-tools' depmod.c. if filename[-3:] != '.ko' and filename[-6:] != '.ko.gz': return None # Logic matches filename2modname from module-init-tools' modprobe.c. module_name = os.path.basename(filename).split('.')[0] if canonicalize: module_name = module_name.replace('-', '_') return module_name def getLoadedModules(): result = {} with open('/proc/modules') as f: for line in f: # Format: # # iwlmvm 1294336 - - Live 0x0000000000000000 # ksplice_pwt2yjrj_vmlinux_new 24576 1 - Live 0xffffffffa4c2d000 (O) # ksplice_pwt2yjrj 204800 2 ksplice_pwt2yjrj_vmlinux_new, Live 0xffffffffa4e09000 (O) # xen_blkback 45056 1 ksplice_ka80p57r_xen_blkback_new, Live 0xffffffffa0e6e000 (E) name, size, use_count, used_by = line.rstrip('\n').split(' ')[:4] result[name] = { 'Size': size, 'UseCount': 0 if use_count == '-' else int(use_count), 'UsedBy': [] if used_by == '-' else used_by.split(',')[:-1], } return result def rmmod(module): """ Attempt to unload the given module. Returns True if successful. """ logging.debug("Trying to rmmod %s" % module) p = subprocess.Popen(['rmmod', module], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) logging.debug(p.communicate()[0].decode('utf-8')) return (p.returncode == 0) def get_modprobe_lock(): perms = 0o600 lock = getLock(MODPROBE_LOCK, create_mode=perms) if lock is False: logging.debug("Unable to acquire the Uptrack modprobe.ksplice lock: %s", MODPROBE_LOCK) logging.error("""\ It appears that a modprobe process is currently running on this system. Please wait a minute and try again. If you are unable to resolve this issue, please contact %s.""" % (Uptrack.BUG_EMAIL,)) sys.exit(1) mode = os.fstat(lock).st_mode # Fixup permissions to 600 if they're wrong if (mode & 0o777) != perms: os.fchmod(lock, perms) return lock def modprobe_lock(func): def run_func_with_modprobe_lock(*args, **kwargs): lock = get_modprobe_lock() try: return func(*args, **kwargs) finally: releaseLock(lock) return run_func_with_modprobe_lock def python_six_unicode_hack(klass): if six.PY2 and 'python_2_unicode_compatible' in dir(six): return six.python_2_unicode_compatible(klass) return klass def get_dmesg_output(current_ts): try: result = subprocess.run(["dmesg"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, universal_newlines=True) # regex to capture timestamps and the rest of the line pattern = re.compile(r'^\[\s?([0-9]+\.[0-9]+)\]\s+(.*)$') filtered_lines = [] for line in result.stdout.splitlines(): match = pattern.match(line) if match: ts = float(match.group(1)) # capture dmesg message around timestamp if ts > (current_ts * 2): filtered_lines.append(line) return filtered_lines except subprocess.CalledProcessError as e: logging.error("Failed to capture dmesg output: %s",e) return None class TimeoutException(Exception): pass def timeout_handler(sig, frame): raise TimeoutException() def run_apply_with_timeout(update, cmd, timeout=120): """ Logic to determine if an update is in a hung (timed out) state - if no timeout occurs, this function returns the result of runKspliceCommand() - if a timeout occurs, it returns an ActionResult() with a -1 code, an unexpected abort status, and captures the dmesg message around the current timestamp """ if timeout <= 0: raise ValueError("Timeout value must be a positive integer") signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(timeout) # ensure that res is defined up front incase a timeout happens before its created res = Uptrack.ActionResult(update, cmd) try: # timestamp to filter out dmesg messages later if needed current_ts = time.clock_gettime(time.CLOCK_MONOTONIC) res = update.runKspliceCommand(cmd, ['/usr/lib/uptrack/ksplice-apply', '--partial', update.tree_path]) signal.alarm(0) # cancel the alarm since the command ran successfully return res except TimeoutException: logging.debug("The process applying the update has failed to complete, check the debug logs") dmesg_output = get_dmesg_output(current_ts) if dmesg_output is not None: logging.debug(dmesg_output) res.code = -1 res.abort_code = "timeout" return res finally: signal.alarm(0) @python_six_unicode_hack class Update(object): def __init__(self, id, name, filename, sha1, sha256, targets, local_dir, remote_dir, order): self.id = id self.name = name self.filename = filename self.verify_hashes = {} self.verify_hashes['SHA-1'] = sha1 self.verify_hashes['SHA-256'] = sha256 self.targets = targets[:] self.local_dir = local_dir self.remote_dir = remote_dir self.order = order def __str__(self): return "[%s] %s" % (self.id, self.name) def __repr__(self): return self.__str__() def __eq__(self, other): return self.id == other.id def __hash__(self): return hash(self.id) def _remote_path(self): return posixpath.join(self.remote_dir, self.filename) remote_path = property(_remote_path) def _local_path(self): return os.path.join(self.local_dir, self.filename) local_path = property(_local_path) def _tree_path(self): return os.path.join(self.local_dir, 'updates', 'ksplice-' + self.id) tree_path = property(_tree_path) def _tree_flag_path(self): return os.path.join(self.local_dir, 'updates', 'ksplice-' + self.id + '.incomplete') tree_flag_path = property(_tree_flag_path) def get_verify_hashes(self): return self.verify_hashes def checkValidFile(self): if not os.path.isfile(self.local_path): return Uptrack.Result(1, "Update file does not exist for update %s" % (self.id,)) try: file_hash = hash_file(self.local_path) except IOError: logging.debug(traceback.format_exc()) return Uptrack.Result(1, "Unable to read update file %s" % (self.local_path,)) verify_hash = self.get_verify_hashes() if not hash_valid(file_hash, verify_hash): mismatches = [] for algo in HASH_PRIORITIES: if algo in verify_hash: mismatches.append('{algo}(got {got}, expected {expected})'.format(algo=algo, got=file_hash[algo], expected=verify_hash[algo])) logging.debug("%s: invalid checksum: %s" % (self.id, ', '.join(mismatches))) return Uptrack.Result(1, "Invalid checksum for update %s." % (self.id,)) if (not os.path.isdir(self.tree_path) or os.path.isfile(self.tree_flag_path)): return Uptrack.Result(1, "Update %s has not been unpacked." % (self.id,)) return Uptrack.Result() def isValidFile(self): return self.checkValidFile().code == 0 def unpack(self): def fix_sigpipe(): signal.signal(signal.SIGPIPE, signal.SIG_DFL) open(self.tree_flag_path, "w") shutil.rmtree(self.tree_path, ignore_errors=True) p = subprocess.Popen(['tar', '--no-same-owner', '--force-local', '-xzf', self.local_path, '-C', os.path.join(self.local_dir, 'updates')], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, preexec_fn=fix_sigpipe) output = p.communicate()[0].decode('utf-8') if p.returncode: return Uptrack.Result(p.returncode, output) os.unlink(self.tree_flag_path) return Uptrack.Result() def getDetails(self): details = os.path.join(self.tree_path, 'details') if not os.path.isfile(details): details = os.path.join(self.tree_path, 'patch') try: return Uptrack.read_file(details) except IOError: logging.debug("Could not retrieve details from %s" % self) logging.debug(traceback.format_exc()) return '' def getCoreVersion(self): # update is applied using ksplice standalone ksplice_dir = '/sys/module/ksplice_%s/ksplice' % self.id if os.path.exists(ksplice_dir): core_version_file = os.path.join(ksplice_dir, 'core_version') if os.path.exists(core_version_file): return Uptrack.read_file(core_version_file) else: return '0' # update is applied using ksplice integrated ksplice_dir = '/sys/kernel/ksplice/%s' % self.id if os.path.exists(ksplice_dir): core_version_file = os.path.join(ksplice_dir, 'core_version') if os.path.exists(core_version_file): return Uptrack.read_file(core_version_file) else: return '0' # update is not applied, so grab the data from tree_path core_version_file = os.path.join(self.tree_path, 'core_version') if os.path.exists(core_version_file): return Uptrack.read_file(core_version_file) else: return '0' def lockedModules(self, loadedModules=None): locked = [] modules = loadedModules if not modules: modules = getLoadedModules() targets = [toModuleName(x) for x in self.targets] for t in targets: if t in modules and 'ksplice_%s_%s_new' % (self.id, t) not in modules: locked.append(t) return locked def isLocked(self, loadedModules=None): return len(self.lockedModules(loadedModules)) != 0 def runKspliceCommand(self, command, args): res = Uptrack.ActionResult(self, command) p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) output = p.communicate()[0].decode('utf-8') if p.returncode == 0: return res Uptrack.mkdirp(os.path.dirname(KSPLICE_DEBUG_FILE)) p = subprocess.Popen(args + ['--debugfile', KSPLICE_DEBUG_FILE, "--raw-errors"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) lines = p.communicate()[1].decode('utf-8').split("\n") if p.returncode == 0: # Cool, it worked this time. try: os.remove(KSPLICE_DEBUG_FILE) except OSError: pass return res res.code = p.returncode res.abort_code = lines[0] res.message = output try: res.debug = Uptrack.read_file(KSPLICE_DEBUG_FILE, 'rb') except IOError: pass if res.abort_code == 'code_busy': res.stack_check_processes = [line.split(" ") for line in lines[1:] if len(line.strip())] elif res.abort_code == 'cold_update_loaded': res.locked_modules = self.lockedModules() elif res.abort_code == 'failed_to_find' or res.abort_code == 'no_match': # Compute the list of modules loaded and affected by this # upgrade, for later display res.nomatch_modules = [] # This code is a bit of a hack because Ksplice itself # doesn't export which module was responsible for the # failure target_modules = [toModuleName(x) for x in self.targets] logging.debug("Run/pre matching failed; targets were:") logging.debug(target_modules) res.nomatch_modules = list(set(getLoadedModules()).intersection(target_modules)) logging.debug("Loaded target modules were:") logging.debug(res.nomatch_modules) # get list and check if there are any conflicting security modules loaded_modules = list(getLoadedModules().keys()) flagged_modules = security_modules_check.conflicting_modules_check(loaded_modules) if flagged_modules: logging.warning( "Conflicting 3rd party security module(s) found:\n* %s\n" "Please see knowledge based article 2733385.1 at support.oracle.com", "\n* ".join(flagged_modules) ) elif res.abort_code == 'module_busy': usedby_modules = set() for modname, modinfo in getLoadedModules().items(): if modname == 'ksplice_' + self.id or modname.startswith('ksplice_' + self.id + '_'): usedby_modules |= set([d for d in modinfo['UsedBy'] if not d.startswith('ksplice_')]) res.usedby_modules = list(usedby_modules) return res def shouldRetry(self, res, duration, retryData): """ Logic for deciding whether to retry a runKspliceCommand. 'retryData' is a dict which stores information on the number of times we have retried the command due to various causes. The caller can pass a new empty dict the first time it tries to execute a given command. shouldRetry() returns one of the strings 'Success', 'Failure', or 'Retry'. In the 'Retry' case, the 'retryData' argument will have been modified to reflect the retry that occurred. """ if 'DidRfcommRetry' not in retryData: # Have we tried to rmmod rfcomm and then retry? retryData['DidRfcommRetry'] = False if 'CodeBusyRetries' not in retryData: # number of previous retries due to other code_busy results retryData['CodeBusyRetries'] = 0 if 'RemoveModulesRetries' not in retryData: # number of previous retries due to no_match/failed_to_find retryData['RemoveModulesRetries'] = 0 if not res.code: return 'Success' elif res.abort_code == 'code_busy': rfcomm_stack_check = 'krfcommd' in [proc[0] for proc in res.stack_check_processes] if rfcomm_stack_check and not retryData['DidRfcommRetry']: logging.debug("Stack check against rfcomm module; trying to remove") rmmod('rfcomm') time.sleep(1) retryData['DidRfcommRetry'] = True return 'Retry' elif duration > CODE_BUSY_MAX_DELAY: logging.debug("Slow stack check failure") return 'Failure' elif retryData['CodeBusyRetries'] < CODE_BUSY_RETRIES: logging.debug("Stack check failure %d, retrying" % (retryData['CodeBusyRetries'] + 1)) time.sleep(1) retryData['CodeBusyRetries'] += 1 return 'Retry' else: return 'Failure' elif res.abort_code in ['no_match', 'failed_to_find'] \ and retryData['RemoveModulesRetries'] < 1 and config.removableModules: loadedModules = getLoadedModules() targetModules = [toModuleName(t) for t in self.targets] # Preserve the order of config.removableModules, # which is the correct order to remove the modules in removableModules = [m for m in config.removableModules if m in loadedModules and m in targetModules] modulesRemoved = [] if not config.no_rmmod: for module in removableModules: if rmmod(module): modulesRemoved += [module] if modulesRemoved: logging.debug("Removed modules " + str(modulesRemoved) + ", retrying") retryData['RemoveModulesRetries'] += 1 return 'Retry' else: return 'Failure' elif res.abort_code == 'key_not_available': logging.debug("Signing key was not found in the kernel") return 'Failure' else: return 'Failure' @modprobe_lock def applyUpdate(self): cmd = INSTALL r = self.checkValidFile() if r.code: res = Uptrack.ActionResult(self, cmd) res.code = r.code res.message = r.message return res retryData = {} for previousRetries in range(0, MAX_RETRIES + 1): starttime = time.time() res = run_apply_with_timeout(self, cmd) endtime = time.time() cont = self.shouldRetry(res, endtime - starttime, retryData) if cont == 'Success': break elif cont == 'Retry' and previousRetries < MAX_RETRIES: pass # retry else: return res # finally, on-disk application # Make sure that modprobe uses the patched modules when they # are (re-)loaded if not self.targets: res.depmod_needed = False return res modroot = "/var/run/ksplice/modules/%s" % config.release backupdir = "/var/run/ksplice/modules.old/%s" % config.release moddir = os.path.join(modroot, "ksplice") Uptrack.mkdirp(moddir) Uptrack.mkdirp(backupdir) targets = dict([(toModuleName(x), toModuleName(x, canonicalize=False)) for x in self.targets]) try: update_modroot = os.path.join(self.tree_path, 'modules') for dirname, _, filenames in os.walk(update_modroot): for filename in filenames: canonical_target = toModuleName(filename) if canonical_target is None or canonical_target not in targets: continue target_proper_casing = targets.pop(canonical_target) modpath = ("%s/ksplice/%s.ko" % (modroot, target_proper_casing)) if os.path.isfile(modpath): backup = os.path.join(backupdir, "%s_pre_%s.ko" % (target_proper_casing, self.id)) os.rename(modpath, backup) os.symlink(os.path.join(update_modroot, dirname, filename), modpath) except IOError: res.code = Uptrack.ERROR_GENERIC_ERROR res.message = "Failure in extracting modules from %s" % self logging.debug(res.message) logging.debug(traceback.format_exc()) return res # Some of the target modules that were affected by this patch # were not found in the modroot/modprobe path if targets: res.code = Uptrack.ERROR_GENERIC_ERROR res.message = ("Could not retrieve some modules from %s:\n" % self + " missing " + " ".join(targets)) logging.debug(res.message) return res depmod_dir = "/var/run/ksplice/depmod.d" Uptrack.mkdirp(depmod_dir) for target in [toModuleName(x, canonicalize=False) for x in self.targets]: depmod_file = "%s/%s.conf" % (depmod_dir, target) if not os.path.exists(depmod_file): entry = ("override %s %s.ksplice-updates ksplice\n" % (target, config.release)) logging.debug("Adding new depmod entry: %s" % entry.strip()) Uptrack.write_file(depmod_file, entry) res.depmod_needed = True return res def undoUpdate(self): cmd = REMOVE retryData = {} for previousRetries in range(0, MAX_RETRIES + 1): starttime = time.time() res = self.runKspliceCommand(cmd, ['/usr/lib/uptrack/ksplice-undo', self.id]) endtime = time.time() cont = self.shouldRetry(res, endtime - starttime, retryData) if cont == 'Success': break elif cont == 'Retry' and previousRetries < MAX_RETRIES: pass # retry else: return res # Reverse on-disk application if not self.targets: res.depmod_needed = False return res depmod_dir = "/var/run/ksplice/depmod.d" modroot = "/var/run/ksplice/modules/%s" % config.release backupdir = "/var/run/ksplice/modules.old/%s" % config.release for target in [toModuleName(x, canonicalize=False) for x in self.targets]: depmod_file = "%s/%s.conf" % (depmod_dir, target) modpath = "%s/ksplice/%s.ko" % (modroot, target) backup = os.path.join(backupdir, "%s_pre_%s.ko" % (target, self.id)) if os.path.isfile(modpath): os.remove(modpath) else: logging.warning("Missing patched module %s while undoing %s" % (modpath, self.id)) if os.path.isfile(backup): try: os.rename(backup, modpath) except IOError: logging.warning("Failed to restore old module %s -> %s" % (backup, modpath)) logging.debug(traceback.format_exc()) # Don't remove the depmod entry, since someone is using it elif os.path.isfile(depmod_file): try: logging.debug("Removing depmod entry: %s" % Uptrack.read_file(depmod_file).strip()) os.remove(depmod_file) except IOError: logging.warning("Failed to remove depmod file %s" % (depmod_file,)) logging.debug(traceback.format_exc()) else: logging.warning("Missing depmod override file %s while undoing %s" % (depmod_file, self.id)) res.depmod_needed = True return res def kspliceToolsApiVersion(): if os.path.exists(API_VERSION_FILE): return Uptrack.read_file(API_VERSION_FILE).strip() return '-1' def serverFingerprint(url): try: netloc = parse.urlparse(url)[1] if ':' not in netloc: netloc += ":443" conn = subprocess.Popen(['openssl', 's_client', '-connect', netloc], stdin=open('/dev/null'), stdout=subprocess.PIPE, stderr=subprocess.PIPE) x509 = subprocess.Popen(['openssl', 'x509', '-fingerprint', '-sha1'], stdin=conn.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE) x509.wait() for line in x509.stdout.read().decode('utf-8').split('\n'): if line.lower().startswith("sha1 fingerprint="): return line[len("SHA1 Fingerprint="):].strip() except subprocess.CalledProcessError: logging.debug("Error reading server certificate fingerprint.") logging.debug(conn.stderr.read().decode('utf-8')) logging.debug(x509.stderr.read().decode('utf-8')) return None return None def parse_version_date(s): """ Example dates that we may have to parse: #2 SMP Tue Aug 18 13:41:50 PDT 2020 #39-Ubuntu SMP Mon Mar 9 10:34:11 UTC 2020 #1 SMP Debian 4.9.88-1+deb9u1 (2018-05-07) """ months = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split() months_map = dict([(mon, 1 + i) for i, mon in enumerate(months)]) # strptime() relies on the current locale... we'll have to do the # parsing ourselves *sigh* m = re.search(r'(?P<dow>Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?P<mon>Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (?P<dom>\d{1,2}) (?P<hour>\d\d):(?P<min>\d\d):(?P<sec>\d\d) (?P<tz>.*) (?P<year>20\d\d)$', s) if m: return datetime.datetime( int(m.group('year')), months_map[m.group('mon')], int(m.group('dom')), int(m.group('hour')), int(m.group('min')), int(m.group('sec')), # This is not strictly speaking correct, but timezones is # such a trash fire it's practically impossible to handle # correctly (for example, the Olson database doesn't even # recognize "EDT" as a valid timezone!). # # Just treat everything as UTC and add a buffer of a few # hours. It's all approximate anyway. tzinfo=now.tzinfo ) m = re.search(r'\((?P<year>\d\d\d\d)-(?P<mon>\d\d)-(?P<dom>\d\d)\)', s) if m: return datetime.datetime( int(m.group('year')), int(m.group('mon')), int(m.group('dom')), tzinfo=now.tzinfo) # Tough luck, we weren't able to parse a date from the version string... return None def test_parse_version_date(): test_data = ( ('#2 SMP Tue Aug 18 13:41:50 PDT 2020', (2020, 8, 18, 13, 41, 50)), ('#39-Ubuntu SMP Mon Mar 9 10:34:11 UTC 2020', (2020, 3, 9, 10, 34, 11)), ('#1 SMP Debian 4.9.88-1+deb9u1 (2018-05-07)', (2018, 5, 7)), ) for version, expected_date in test_data: parsed_date = parse_version_date(version) assert parsed_date is not None year, month, day_of_month = expected_date[:3] assert parsed_date.year == year assert parsed_date.month == month assert parsed_date.day == day_of_month if len(expected_date) > 3: hour, minute, second = expected_date[3:] assert parsed_date.hour == hour assert parsed_date.minute == minute assert parsed_date.second == second test_data = ( '#1 SMP Debian 3.2.73-2+deb7u2~bpo60+1' ) for version in test_data: parsed_date = parse_version_date(version) assert parsed_date is None def is_new_kernel(version): date = parse_version_date(config.version) if date is None: return False delta = now - date delta_days = delta.days + delta.seconds / 86400. return delta_days < 4 class UptrackRepo(object): def __init__(self, config): self.updates = None self.protocolVersion = UPTRACK_PACKAGES_PROTOCOL_VERSION self.remote_dir = posixpath.join(config.remote, parse.quote(config.sysname), parse.quote(config.arch), parse.quote(config.release), parse.quote(config.version)) self.local_dir = config.local self.local_package_list = os.path.join(self.local_dir, "packages.yml") self.kspliceToolsApiVersion = kspliceToolsApiVersion() self.clientVersion = __version__ self.userStatus = None self.expired = False self.ignore_sig_time = ( config.options.init and config.config.has_option("Settings", "ignore-boot-time-warp") and config.config.getboolean("Settings", "ignore-boot-time-warp") ) self.is_new_kernel = is_new_kernel(config.version) def verifyKeyring(self, keyring, fingerprint): logging.debug("Verifying key fingerprint...") p = subprocess.Popen(['gpg', '--no-options', '--homedir', UPTRACK_GPG_HOMEDIR, '--no-default-keyring', '--batch', '--keyring', keyring, '--fingerprint', '--with-colons'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() stdout = stdout.decode('utf-8') stderr = stderr.decode('utf-8') if stdout: logging.debug(stdout) if stderr: logging.debug(stderr) if p.returncode: return Uptrack.Result(1, "Ksplice Uptrack failed to read fingerprint") listings = [l.split(':') for l in stdout.strip().split("\n")] fprs = [l for l in listings if l[0] == "fpr"] if len(fprs) < 1: return Uptrack.Result(1, "Ksplice Uptrack could not find any fingerprints") if len(fprs) > 1: return Uptrack.Result(1, "Ksplice Uptrack found too many fingerprints") # 10. Field: User-ID. [..] # An FPR record stores the fingerprint here. # The fingerprint of an revocation key is stored here. if fprs[0][9] != fingerprint: return Uptrack.Result(1, "Ksplice Uptrack could not verify the key fingerprint") logging.debug("Verified GPG fingerprint on %s", keyring) return None def validateServer(self): if Uptrack.server_is_oracle(config): return None res = self.verifyKeyring(SERVER_KEYRING, SERVER_KEY_FINGERPRINT) if res: return res fingerprint = serverFingerprint(config.remoteroot) logging.debug("Got a server fingerprint: %s", fingerprint) if fingerprint is None: return Uptrack.Result( Uptrack.ERROR_NO_NETWORK, "Unable to communicate with your site's Uptrack server.\n\n" "Please ensure that the update_repo_url setting is correct in\n" "/etc/uptrack/uptrack.conf, and that your Uptrack server is properly configured.\n" "If you need help resolving this issue, please contact %s." % (Uptrack.BUG_EMAIL,)) sigpath = os.path.join(config.localroot, fingerprint + ".sig") try: code = Uptrack.download(Uptrack.getCurl(), posixpath.join(config.remoteroot, 'server', fingerprint + ".sig"), sigpath, in_offline_mode) if code not in (200, 304): return Uptrack.Result( 1, "Unable to download the signature for your Uptrack server.\n" "Please contact %s for assistance." % (Uptrack.BUG_EMAIL,)) except pycurl.error as e: logging.debug("cURL error %d (%s) while checking server signature." % (e.args[0], e.args[1])) logging.debug(traceback.format_exc()) return Uptrack.resultFromPycurl(config, e) fpfile = None try: (fd, fpfile) = tempfile.mkstemp() fh = os.fdopen(fd, 'w') fh.write(fingerprint + "\n") fh.close() if not self.verifySignature(fpfile, sigpath, keyring=SERVER_KEYRING): return Uptrack.Result( 1, "Unable to verify that the server at <%s>\n" "is an authorized Ksplice Uptrack server. Please contact\n" "%s for assistance." % (config.remoteroot, Uptrack.BUG_EMAIL)) finally: try: if fpfile: os.unlink(fpfile) except OSError: pass return None def downloadUserStatus(self): err = None key_check = posixpath.join(config.remote, parse.quote('status')) localpath = os.path.join(config.localroot, 'status') logging.debug("Verifying your access key is valid by requesting: %s" % key_check) stream = BytesIO() try: request = dict(Serial=config.incrementSerial()) Uptrack.Status.addIdentity(config, request) contents = Uptrack.yaml_dump(request, version=(1, 1), explicit_start=True, explicit_end=True) c = Uptrack.getCurl() c.setopt(pycurl.URL, key_check.encode('ISO-8859-1')) c.setopt(pycurl.HTTPPOST, [('request', contents)]) c.setopt(pycurl.WRITEFUNCTION, stream.write) c.perform() code = c.getinfo(pycurl.RESPONSE_CODE) if code == 200 or code == 304: try: Uptrack.write_file_binary(localpath, stream.getvalue()) except IOError as e: logging.debug("Error writing status file.") logging.debug(traceback.format_exc()) try: self.userStatus = Uptrack.yaml_load(stream.getvalue().decode('utf-8')) except yaml.YAMLError: logging.debug("Malformed YAML response when checking the key.") # This shouldn't happen---the server should always return # valid YAML. Stop! Do not pass go. Do not collect $200. err = Uptrack.Result( Uptrack.ERROR_MALFORMED_YAML, "The server returned an unexpected response.\n" "Please try again in a few minutes. If this problem persists, \n" "please contact %s for assistance." % (Uptrack.BUG_EMAIL,)) elif 400 <= code <= 499: logging.debug("Your access key (%s) is invalid." % config.accesskey) err = Uptrack.Result(Uptrack.ERROR_INVALID_KEY, "Could not connect to the Ksplice Uptrack " "server with your access key.\n" "Please check that the key in %s is valid.\n" % Uptrack.UPTRACK_CONFIG_FILE) else: logging.debug(u"Unexpected error retrieving status file (%d)", code) logging.debug(u"The server said:") logging.debug(stream.getvalue().decode('utf-8')) if 500 <= code <= 599: err = Uptrack.server_error_exception.result else: err = Uptrack.Result( Uptrack.ERROR_INTERNAL_SERVER_ERROR, "Unexpected error communicating with the server.\n" "Please try again in a few minutes. If this problem persists, \n" "please contact %s for assistance." % (Uptrack.BUG_EMAIL,)) except pycurl.error as e: logging.debug("cURL error %d (%s) while checking key." % (e.args[0], e.args[1])) logging.debug(traceback.format_exc()) err = Uptrack.resultFromPycurl(config, e) except Uptrack.ResultException as e: return e.result return err def showUserStatus(self): if self.userStatus is None: return None try: if 'Error' in self.userStatus: err = self.userStatus['Error'] return Uptrack.Result(err['Code'], err['Message']) if config.cron and not self.userStatus.get('Cron'): return if 'Message' in self.userStatus: logging.info(self.userStatus['Message']) if 'Warning' in self.userStatus: logging.error(self.userStatus['Warning']) except (TypeError, AttributeError): logging.debug("Error parsing user status: ", exc_info=True) pass def handleStatus(self): err = self.downloadUserStatus() if err: return err try: if 'RegenerateCron' in self.userStatus: config.regenerateCron() if 'Backoff' in self.userStatus: config.updateBackoff(self.userStatus['Backoff']) if 'RegenerateUUID' in self.userStatus and not config.use_hw_uuid: # Must process this last, because it overwrites self.userStatus config.setNewUUID() err = self.downloadUserStatus() if err: return err except TypeError: # If self.userStatus is not a dict. pass return self.showUserStatus() def downloadPackageList(self): logging.debug("Getting package list and signature from " + self.remote_dir) err = None try: c = Uptrack.getCurl() for filename in ['packages.yml', 'packages.yml.sig']: url = posixpath.join(self.remote_dir, filename) filename = os.path.join(self.local_dir, filename) try: os.unlink(filename + ".expired") except OSError: pass stream = BytesIO() rcode = Uptrack.download(c, url, filename, in_offline_mode, bytesio=stream) if rcode in [200, 304]: continue elif rcode == HTTP_CODE_EXPIRED: logging.debug("Writing expired data to %s" % (filename + ".expired",)) Uptrack.write_file_binary(filename + ".expired", stream.getvalue()) continue elif rcode in [404] and self.is_new_kernel: err = Uptrack.Result(0, "Your kernel is so recent there are no updates to install.") elif rcode in [403, 404]: if in_offline_mode: err = Uptrack.Result(Uptrack.ERROR_UNSUPPORTED, "Cannot find Ksplice Uptrack information " "for your kernel version\n(%s %s).\n" "Have you installed the uptrack-updates package " "containing updates for this kernel?\n" "Please contact %s with questions." % (config.release, config.version, Uptrack.BUG_EMAIL)) else: err = Uptrack.Result(Uptrack.ERROR_UNSUPPORTED, "Cannot find Ksplice Uptrack information " "for your kernel version\n(%s %s).\n" "Your kernel is probably not yet supported " "by Ksplice Uptrack.\n" "See http://www.ksplice.com/uptrack/supported-kernels " "for a summary of\nwhat kernels are supported.\n" "Please contact %s with questions." % (config.release, config.version, Uptrack.BUG_EMAIL)) logging.debug("Error downloading package list. Checking key validity.") else: err = Uptrack.Result(Uptrack.ERROR_NO_NETWORK, "Unexpected error connecting to the " "Ksplice Uptrack server.\n" "The web server returned " "HTTP code %03d. If this error persists,\n" "please contact %s." % (rcode, Uptrack.BUG_EMAIL)) logging.debug("Received unexpected HTTP error code %03d." % rcode) return err except pycurl.error as e: logging.debug("cURL error %d (%s) while downloading package list." % (e.args[0], e.args[1])) logging.debug(traceback.format_exc()) err = Uptrack.resultFromPycurl(config, e) except IOError: err = Uptrack.Result(Uptrack.ERROR_NO_NETWORK, "Could not save the package list from the " "Ksplice Uptrack server.\n" "More details may be available in %s.\n" "If this error continues, please contact %s." % (LOGFILE, Uptrack.BUG_EMAIL)) logging.debug(traceback.format_exc()) return err def verifySignature(self, file, sigfile, keyring=None): if keyring is None: keyring = KEYRING logging.debug("Verifying signature on %s..." % (file,)) cmd = ['gpgv', '--homedir', UPTRACK_GPG_HOMEDIR, '--keyring', keyring] if self.ignore_sig_time: logging.warning("Ignoring package signature timestamps!") cmd.append('--ignore-time-conflict') cmd.append(sigfile) cmd.append(file) p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() stdout = stdout.decode('utf-8') stderr = stderr.decode('utf-8') if stdout: logging.debug(stdout) if stderr: logging.debug(stderr) if p.returncode: return False return True def readPackageList(self): if not os.path.exists(self.local_package_list): return (Uptrack.Result(Uptrack.ERROR_NO_NETWORK, "Ksplice Uptrack could not find the package list."), None) if not self.verifySignature(self.local_package_list, self.local_package_list + ".sig"): return (Uptrack.Result(1, "Ksplice Uptrack could not verify" " the package list signature."), None) logging.debug("Trying to read package list at %s" % self.local_package_list) try: pl = Uptrack.yaml_load(open(self.local_package_list)) if os.path.isfile(self.local_package_list + ".expired"): if not self.verifySignature( self.local_package_list + ".expired", self.local_package_list + ".sig.expired"): return (Uptrack.Result(1, "Ksplice Uptrack could not verify" " the package list signature."), None) expired = Uptrack.yaml_load(open( self.local_package_list + ".expired", 'r')) pl['Expired'] = expired except (IOError, yaml.YAMLError): logging.debug("Error reading the package list", exc_info=1) err = Uptrack.Result() if not config.init: err.code = Uptrack.ERROR_NO_NETWORK err.message = "Cannot load the package list.\n" # If config.allow_no_net is false, we already downloaded the # package list, so barring possible weird edge cases, this # is almost certainly a bug. # If we don't have network, instruct the user to try again # with network, in the hopes that re-downloading the # package list will fix things. if config.allow_no_net: err.message += "Please re-run the Ksplice Uptrack client with a network connection available." else: err.message += "Please report a bug to %s" % (Uptrack.BUG_EMAIL,) return err, None client = pl['Client'] version = pl['Protocol version'] # Check versions if self.protocolVersion != version: local.new_client = True return (Uptrack.Result(1, "Protocol version mismatch: %s != %s\n" "Please use your package manager to update this client." % (self.protocolVersion, version)), None) needVersion = client.get('Version to Parse', '0') if Uptrack.compareversions(self.clientVersion, needVersion) < 0: local.new_client = True return (Uptrack.Result(Uptrack.ERROR_TOO_OLD_PARSE, "Ksplice Uptrack client too old: %s, require %s\n" "Please use your package manager to update this client." % (self.clientVersion, needVersion)), None) # Sanity check kernel = pl['Kernel'] if config.release != kernel['Release']: return (Uptrack.Result(1, "Kernel release mismatch: %s != %s" % (config.release, kernel['Release'])), None) if config.version != kernel['Version']: return (Uptrack.Result(1, "Kernel version mismatch: %s != %s" % (config.version, kernel['Version'])), None) if config.arch != kernel['Architecture']: return (Uptrack.Result(1, "Wrong architecture: %s != %s" % (config.arch, kernel['Architecture'])), None) if 'Error' in pl: e = pl['Error'] return Uptrack.Result(e['Code'], e['Message']), None return None, pl def parsePackageList(self): err, pl = self.readPackageList() if pl is None: return err packages = {} for i, item in enumerate(pl['Updates']): u = makeUpdate(item, self.local_dir, self.remote_dir, i) packages[u.id] = u self.updates = packages version = pl['Client'].get('Version to Install', '0') if Uptrack.compareversions(self.clientVersion, version) < 0: local.new_client = True return Uptrack.Result(Uptrack.ERROR_TOO_OLD_INSTALL, "Ksplice Uptrack client too old: %s, require %s\n" "Please use your package manager to update this client." % (self.clientVersion, version)) version = pl['Client']['Ksplice Tools API version'] if self.kspliceToolsApiVersion == '-1': return Uptrack.Result(1, "Error: %s: No such file or directory" % (API_VERSION_FILE,)) if Uptrack.compareversions(self.kspliceToolsApiVersion, version) < 0: local.new_client = True return Uptrack.Result(1, "Ksplice Uptrack client too old: tools API version %s < %s\n" "Please use your package manager to update this client." % (self.kspliceToolsApiVersion, version)) elif self.kspliceToolsApiVersion != version: return Uptrack.Result(1, "Ksplice Uptrack client too new: tools API version %s > %s\n" "Please report this problem to %s." % (self.kspliceToolsApiVersion, version, Uptrack.BUG_EMAIL)) global alert, desupported, tray_icon_error alert = pl.get('Alert') desupported = pl.get('Desupported') tray_icon_error = pl.get('TrayIconError') if alert and (desupported or not config.cron): logging.warning(alert) expired = pl.get('Expired') if expired: self.expired = True if 'Message' in expired: logging.warning(expired['Message']) return err def downloadPackages(self): logging.debug("Downloading packages.") for u in sorted(self.updates.values(), key=lambda u: u.order): if u.isValidFile(): logging.debug("Already downloaded %s, skipping" % u) continue logging.debug("Downloading %s" % u) try: Uptrack.mkdirp(os.path.dirname(u.local_path)) rcode = Uptrack.download(Uptrack.getCurl(), u.remote_path, u.local_path, in_offline_mode, ifmodified=False) if rcode != 200: return Uptrack.Result(1, "Unexpected error downloading update %s.\n" "The web server returned HTTP code %03d.\n" "If this error persists, please contact %s." % (u.id, rcode, Uptrack.BUG_EMAIL)) except (IOError, pycurl.error): err = Uptrack.Result() err.code = Uptrack.ERROR_GENERIC_ERROR err.message = ("Couldn't download update '%s'.\n" "Please check your network connection and try " "again.\nIf this error continues, contact %s." % (u, Uptrack.BUG_EMAIL)) logging.debug("Error downloading update %s." % u.filename) logging.debug("Remote: %s" % u.remote_path) logging.debug("Local: %s" % u.local_path) logging.debug(traceback.format_exc()) return err logging.debug("Unpacking %s" % u) res = u.unpack() if res.code: return res def downloadAll(self): res = repo.downloadPackageList() if res: return res res = repo.parsePackageList() if res: return res return repo.downloadPackages() def getAllUpdates(self): if self.updates is None: raise Exception("BUG: Package list has not yet been read.") return set(self.updates.values()) def idToUpdate(self, id): """ Turns ID into Update, or returns None if we don't know about it """ if self.updates: return self.updates.get(id, None) class AlarmSignaled(Exception): pass def onAlarm(sig, frame): raise AlarmSignaled def getLock(lockfile, create_mode=0o644): lockdir = os.path.dirname(lockfile) if not os.path.isdir(lockdir): try: os.makedirs(lockdir) except IOError: return False old_handler = None try: try: old_handler = signal.signal(signal.SIGALRM, onAlarm) f = os.open(lockfile, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, create_mode) signal.alarm(LOCK_TIMEOUT) fcntl.flock(f, fcntl.LOCK_EX) except (IOError, AlarmSignaled): return False finally: signal.alarm(0) if old_handler is not None: signal.signal(signal.SIGALRM, old_handler) return f def releaseLock(lock): """ The fd is closed when the variable goes out of scope or the process is exited, so this shouldn't really be necessary, but is here in case you want to explicitly release the lock """ fcntl.flock(lock, fcntl.LOCK_UN) os.close(lock) class BaseUptrackClientConfig(object): clientConfig = None def __init__(self, program, args): # super(UptrackClientConfig, self).__init__() command = extractCommand(os.path.basename(program)) if command == INSTALL: usage = """\ usage: %prog [options] <id>... Install selected patch to a running kernel.""" elif command == REMOVE: usage = """\ usage: %prog [options] <id>... Remove selected patches from a running kernel.""" elif command == SHOW: usage = """\ usage: %prog [options] [<id>...] Display which updates have been installed.""" elif command == UPGRADE: usage = """\ usage: %prog [options] Apply available updates to a running kernel.""" parser = OptionParser(usage=usage) # Global Options parser.add_option("-q", "--quiet", action="store_const", const=-1, dest="verbose", help="don't print status messages") parser.add_option("-v", "--verbose", action="count", dest="verbose", help="provide more detail about what this program is doing", default=0) parser.add_option("-y", action="store_true", dest="answer_yes", default=False, help="answer 'yes' to all user prompts") parser.add_option("-n", action="store_true", dest="answer_no", default=False, help="answer 'no' to all user prompts") parser.add_option("--all", action="store_true", dest="all", default=False, help="take action for all updates") # Options used during startup parser.add_option("--init", action="store_const", dest="init", default=None, const='early', help=SUPPRESS_HELP) parser.add_option("--late-init", action="store_const", dest="init", const="late", help=SUPPRESS_HELP) parser.add_option("--shutdown", action="store_const", dest="init", const="shutdown", help=SUPPRESS_HELP) parser.add_option("--check-init", action="store_true", dest="check_init", default=False, help=SUPPRESS_HELP) # end startup options parser.add_option("-V", "--version", action="store_true", dest="show_version", help="print the version information and exit", default=False) parser.add_option('--i-accept-the-terms-of-service', action='store_true', dest='accept_tos', default=False, help=SUPPRESS_HELP) opt_no_net = Option("--no-network", action="store_true", dest="allow_no_net", default=False, help="Don't use the network.") opt_available = Option('--available', action="store_true", dest="available", default=False, help='Also show available updates.') opt_wait = Option('--wait', type="float", dest="wait", default=0, help="time to wait between applying updates") opt_cron = Option("--cron", action="store_true", dest="cron", default=False, help=SUPPRESS_HELP) opt_count = Option("--count", action="store_true", dest="count", default=False, help="Display the number of upgrades installed.") opt_uninstall = Option("--uninstall", action="store_true", dest="uninstall", default=False, help=SUPPRESS_HELP) # command specific options optmap = {SHOW: [opt_available, opt_count, opt_no_net, ], INSTALL: [opt_wait, ], REMOVE: [opt_no_net, opt_wait, opt_uninstall], UPGRADE: [opt_wait, opt_cron], } if command in optmap: for option in optmap[command]: parser.add_option(option) self.parser = parser self.args = args def processArgs(self): (options, args) = self.parser.parse_args(self.args) self.options = options self.args = args if self.options.answer_yes and self.options.answer_no: self.parser.error("Please supply only one of -y or -n.") if hasattr(self.options, 'wait') and self.options.wait < 0: self.parser.error("--wait argument must be positive") self.setVerbosity() self.setTask() def setVerbosity(self): self.verbose = self.options.verbose def setTask(self): self.disabled = os.path.isfile("/etc/uptrack/disable") self.disablecmd = 'nouptrack' in Uptrack.read_file("/proc/cmdline") self.answer_yes = self.options.answer_yes self.answer_no = self.options.answer_no self.allow_no_net = getattr(self.options, 'allow_no_net', False) self.all = self.options.all if hasattr(self.options, 'available'): self.available = self.options.available # Unattended operation modes self.cron = False self.uninstall = False self.init = False if hasattr(self.options, 'uninstall') and self.options.uninstall: self.uninstall = True self.all = True self.answer_yes = True self.cron_autoinstall = Uptrack.getConfigBooleanOrDie( self.config, 'Settings', 'autoinstall', False) self.install_on_reboot = Uptrack.getConfigBooleanOrDie( self.config, 'Settings', 'install_on_reboot', True) self.upgrade_on_reboot = Uptrack.getConfigBooleanOrDie( self.config, 'Settings', 'upgrade_on_reboot', False) if hasattr(self.options, 'cron') and self.options.cron: self.cron = True self.answer_yes = self.cron_autoinstall self.answer_no = not self.cron_autoinstall self.verbose = -2 if hasattr(self.options, 'init') and self.options.init: self.init = self.options.init self.verbose -= 1 if self.init == 'early': self.answer_yes = True self.answer_no = False self.allow_no_net = True elif self.init == 'late': self.answer_yes = self.upgrade_on_reboot self.answer_no = not self.answer_yes elif self.init == 'shutdown': self.answer_yes = False self.answer_no = True if hasattr(self.options, 'wait'): self.wait = self.options.wait self.accept_tos = self.options.accept_tos def checkDeprecatedOptions(self): if self.cron: return show_cron_output_warning = False for suffix in ('install', 'available', 'error'): option = 'cron_output_' + suffix if Uptrack.getConfigBooleanOrDie(self.config, 'Settings', option, False): show_cron_output_warning = True if show_cron_output_warning: logging.warning("Warning: The cron output configuration options have been removed.") logging.warning("Please visit <http://www.ksplice.com/uptrack/notification-options>") logging.warning("for more information.") # (Slight) hack to pass on requests for config items that reside in the client # config to that object def __getattr__(self, key): if self._intialized: if key in self.__dict__: return self.__dict__[key] return getattr(self.clientConfig, key) else: try: return self.__dict__[key] except KeyError: raise AttributeError(key) def __setattr__(self, key, value): if key in ['_intialized'] or not self._intialized: self.__dict__[key] = value return if key in self.__dict__: self.__dict__[key] = value else: setattr(self.clientConfig, key,value) # Slightly more of a hack. These are methods in the clientConfig called by other # parts of uptrack-upgrade. No reason for those other parts to have to really # distinguish the types of config, so just pass on those method calls def regenerateCron(self): self.clientConfig.regenerateCron() def updateBackoff(self, backoff): self.clientConfig.updateBackoff(backoff) def setNewUUID(self): self.clientConfig.olduuid = self.clientConfig.uuid self.clientConfig.setUUID(self.clientConfig.newUUID()) class OnlineUptrackClientConfig(BaseUptrackClientConfig): def __init__(self, program, args): self._intialized = False self.clientConfig = Uptrack.OnlineUptrackConfig() self.config = self.clientConfig.config super(OnlineUptrackClientConfig, self).__init__(program, args) super(OnlineUptrackClientConfig, self).processArgs() # These aren't use in the online version self.effective_version = None self.list_effective = None self._intialized = True class OfflineUptrackClientConfig(BaseUptrackClientConfig): def __init__(self, program, args): self._intialized = False self.clientConfig = Uptrack.OfflineUptrackConfig() self.config = self.clientConfig.config super(OfflineUptrackClientConfig, self).__init__(program, args) self.parser.add_option("--yum", action="store_true", dest="yum_upgrade", help="Install latest update packages with YUM", default=False) self.parser.add_option("-E", "--effective", dest="effective_version", default=None, help="Upgrade to the specified effective kernel version") self.parser.add_option("-L", "--list-effective", action="store_true", dest="list_effective", default=False, help="List available effective kernel versions") super(OfflineUptrackClientConfig, self).processArgs() if self.options.effective_version: if self.clientConfig.effective_version: logging.info('Duplicate effective version options.') logging.info('Ignoring config file option in favor of command line.') self.effective_version = self.options.effective_version else: self.effective_version = self.clientConfig.effective_version if self.options.list_effective: self.list_effective = self.options.list_effective else: self.list_effective = None self._intialized = True def setTask(self): super(OfflineUptrackClientConfig, self).setTask() class PermissionedRotatingFileHandler(logging.handlers.RotatingFileHandler): """ A subclass of logging.handlers.RotatingFileHandler which sets user, group, and permissions, and turns buffering off. """ def __init__(self, filename, **kwargs): self._ks_filename = filename try: self._ks_uid = pwd.getpwnam(LOGUSER)[2] self._ks_gid = grp.getgrnam(LOGGROUP)[2] except KeyError: self._ks_uid = None self._ks_gid = None self._ks_umask = LOGMODE ^ 0o777 old = os.umask(self._ks_umask) logging.handlers.RotatingFileHandler.__init__(self, filename, **kwargs) os.umask(old) self._ksplice_chown() def _ksplice_chown(self): # not security critical; by default the file will be root:root if self._ks_uid is None: return try: os.chown(self._ks_filename, self._ks_uid, self._ks_gid) except OSError: pass def emit(self, *args, **kwargs): old = os.umask(self._ks_umask) logging.handlers.RotatingFileHandler.emit(self, *args, **kwargs) if self.stream is not None: try: os.fsync(self.stream) except Exception: pass os.umask(old) def doRollover(self, *args, **kwargs): logging.handlers.RotatingFileHandler.doRollover(self, *args, **kwargs) self._ksplice_chown() class Logger(object): def __init__(self): my_logger = logging.getLogger('') my_logger.setLevel(logging.DEBUG) consoleLevel = logging.INFO logging.raiseExceptions = False self.console_logger = logging.StreamHandler(sys.stdout) self.console_logger.setLevel(consoleLevel) formatter = logging.Formatter('%(message)s') self.console_logger.setFormatter(formatter) fformat = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s') filelogger = PermissionedRotatingFileHandler(LOGFILE, maxBytes=2.5 * 1024 * 1024, backupCount=2) filelogger.setLevel(logging.DEBUG) filelogger.setFormatter(fformat) self.debug_log = NativeStringIO() self.debug_logger = logging.StreamHandler(self.debug_log) self.debug_logger.setLevel(logging.DEBUG) self.debug_logger.setFormatter(fformat) logging.getLogger('').addHandler(filelogger) logging.getLogger('').addHandler(self.console_logger) logging.getLogger('').addHandler(self.debug_logger) def getDebugLog(self): try: logging.getLogger('').removeHandler(self.debug_logger) return self.debug_log.getvalue() except Exception: return None def configure(self, config): consoleLevel = logging.INFO if config.verbose < -1: consoleLevel = logging.CRITICAL elif config.verbose < 0: consoleLevel = logging.ERROR elif config.verbose > 0: consoleLevel = logging.DEBUG self.console_logger.setLevel(consoleLevel) def confirm(msg): if config.answer_yes: return True if config.answer_no: return False logging.info("") take_action = 'n' try: take_action = input("%s [y/N]? " % msg) except (KeyboardInterrupt, EOFError): print() pass if len(take_action) < 1 or take_action[0].lower() != 'y': return False return True def doRemove(repo, local, wish_to_remove): if config.all: wish_to_remove = [u.id for u in local.getInstalledUpdates()] return planAndDoActions(repo, local, [(REMOVE, x) for x in wish_to_remove]) def doUpgrade(repo, local): # Shortcut the answer_no case to avoid an additional round-trip if config.answer_no: try: (_, new_upgrade, new_init, new_remove) = planActions(repo, local, []) except Uptrack.ResultException as e: return e.result if len(new_upgrade) == 0: if not config.cron: printEffective() logging.info("Nothing to be done.") else: printEffective() displayAndConfirm(new_upgrade) local.writeOutStatus(None, new_upgrade, new_init, new_remove) return Uptrack.Result() res = planAndDoActions(repo, local, [(INSTALL, 'head')]) if not res.code: if not config.cron: logging.info("Your kernel is fully up to date.") printEffective() return res def all_already_installed(update_ids): return set(update_ids).issubset(set(local.getInstalledIDs())) def listAvailableEffectiveVersionsAndQuit(): sorted_versions, unused_dict = group_updates_by_version(config, local, INSTALL) quit(Uptrack.Result(0, """Available effective kernel versions: %s""" % ('\n'.join(sorted_versions)))) def doUpgradeToEffectiveVersion(repo, local, wanted_effective_version): sorted_versions, updates_by_version = group_updates_by_version(config, local, INSTALL) if not wanted_effective_version in sorted_versions: quit(Uptrack.Result(Uptrack.ERROR_GENERIC_ERROR, """ The specified effective kernel version: '%s' is invalid or not available in the currently installed uptrack-updates package. Available effective kernel versions: %s Check the uptrack-updates package contents by running: rpm -ql $(rpm -qa uptrack-updates*) |less """ % (wanted_effective_version, '\n'.join(sorted_versions)))) to_install = updates_by_version[wanted_effective_version] kernel = local.getEffective() if kernel is None: quit(Uptrack.Result( Uptrack.ERROR_GENERIC_ERROR, "Could not determine the current effective version.\n" "Not upgrading to selected effective version.\n" "Check %s for corruption" % (Uptrack.UPTRACK_EFFECTIVE_KERNEL_FILE,))) current_effective = kernel['Release'] + '/' + kernel['Version'] if all_already_installed(to_install) and current_effective != wanted_effective_version: quit(Uptrack.Result(Uptrack.ERROR_GENERIC_ERROR, """ The current effective kernel version (%s) is already newer than the requested effective kernel version. """ % (current_effective))) res = planAndDoActions(repo, local, [(INSTALL, x) for x in updates_by_version[wanted_effective_version]]) printEffective() return res def doInstall(repo, local, wish_to_install): if config.all: return doUpgrade(repo, local) else: res = planAndDoActions(repo, local, [(INSTALL, x) for x in wish_to_install]) printEffective() return res def printEffective(blank_line=False): kernel = local.getEffective() if kernel is not None: if blank_line: logging.info("") logging.info("Effective kernel version is %s" % (kernel['PackageVersion'],)) def planActions(repo, local, actions): """:: Repo -> Local -> [Action] -> IO (Plan, Plan, Plan, Plan) data Action = Install Kid | Remove Kid -- Implemented as (INSTALL|REMOVE, Kid) -- Installing the pseudo-kid "head" means "upgrade fully". type Plan = [(INSTALL|REMOVE,Update)] Given a set of requested actions, return a set of four plans appropriate to carry out those actions and then update our state. The four returned plans are, in order: *) A plan that includes the requested actions. *) A new upgrade plan that is what one would need to do to fully update the machine assuming we perform the first plan succesfully. *) A new init-time plan that is used to take a rebooted machine back to the state it would be in after we perform the first plan succesfully. *) A new remove-all plan, assuming we perform the first plan succesfully. (this is used if we are trying to remove all updates and don't have network connectivity). """ logging.debug("Constructing plans for the actions: " + str(actions)) installed = local.getInstalledUpdates() new_actions = [] for act in actions: command, id = act update = repo.idToUpdate(id) if update is None and id != "head": logging.warning("Unknown update %s, skipping." % (id,)) elif command == INSTALL and update in installed: logging.warning("Update %s is already installed, skipping." % (id,)) elif command == REMOVE and update not in installed: logging.warning("Update %s is not installed, skipping." % (id,)) else: new_actions.append(act) actions = new_actions installed_ids = [u.id for u in installed] loaded_modules = getLoadedModules() locked_ids = [u.id for u in installed if u.isLocked(loaded_modules)] if config.in_offline_mode: augmenter = None kernel_key = "%s/%s/%s/%s" % (config.sysname, config.arch, config.release, config.version) ek = load_effective_kernel_data(kernel_key) augmenter = ek.augmentPlan(kernel_key) solver = OfflineUptrackDepSolver(local, installed_ids, locked_ids) response = solver.getPlans(actions, augmenter) else: solver = UptrackDepSolver.UptrackDepSolver(local, installed_ids, locked_ids) response = solver.getPlans(actions) plan, upgrade, init, remove = [response[k] for k in ('Steps', 'Upgrade Plan', 'Init Plan', 'Remove Plan')] if 'EffectiveKernel' in response: local.setEffective(response['EffectiveKernel']) if False in [act['ID'] in repo.updates for act in plan + upgrade + init + remove]: # Make sure packages.yml and associated state are up to date res = repo.downloadAll() if res and res.code != 0: raise Uptrack.ResultException(res.code, res.message) for act in plan + upgrade + init + remove: if act['ID'] not in repo.updates: raise Uptrack.ResultException(Uptrack.ERROR_INTERNAL_SERVER_ERROR, textwrap.fill( """Error: Server asked that we %s unknown update %s. Please report this issue to ksplice-support_ww@oracle.com. """ % (act['Command'].lower(), act['ID']))) local.unpackPlan(plan) local.unpackPlan(upgrade) local.unpackPlan(init) local.unpackPlan(remove) return (plan, upgrade, init, remove) def planAndDoActions(repo, local, actions): """:: Repo -> Local -> [Action] -> Uptrack.Result Construct a plan to perform the requested actions, and then carry out said plan. In addition, write new upgrade, init-time, and remove plans appropriate for the new state. """ try: (plan, new_update, new_init, new_remove) = planActions(repo, local, actions) except Uptrack.ResultException as e: return e.result if len(plan) == 0: if not config.cron: logging.info("Nothing to be done.") elif not displayAndConfirm(plan): return Uptrack.Result(Uptrack.ERROR_USER_NO_CONFIRM, "Aborting.") res = doActions(plan, local) if res.code: return res # We succeeded. Write out the new update, init-time, and remove # plans the server gave us. Don't write out our result, since # quit() will do that for us. local.writeOutStatus(None, new_update, new_init, new_remove) return res def doInitClean(local): dir = "/var/run/ksplice" file = "/var/run/uptrack" if os.path.isdir(dir): logging.debug("Cleaning up stale state in %s and %s" % (dir, file)) try: shutil.rmtree(dir) except Exception as e: if isinstance(e, OSError) and e.errno == errno.ENOENT: pass else: logging.error("An error occurred while removing %s" % dir) logging.debug(traceback.format_exc()) if os.path.exists(file): try: os.unlink(file) except e: if isinstance(e, OSError) and e.errno == errno.ENOENT: pass logging.error("An error occurred while removing %s" % file) logging.debug(traceback.format_exc()) def doInit(repo, local): """ Execute the init-time plan previously written to disk """ inst = local.getInstalledUpdates() if len(inst) != 0: msg = "\nError: Uptrack init script started, but some updates are already installed!\n\n" + \ textwrap.fill( "This is most likely because you manually ran '/etc/init.d/uptrack start'. " + "You never need to run the Uptrack init script manually: There is no Uptrack " + "daemon to start. This init script only exists to reinstall your updates " + "during the boot process. Please contact %s if you have any questions." % Uptrack.BUG_EMAIL) return Uptrack.Result(1, msg) doInitClean(local) if not config.install_on_reboot: logging.debug("init-time installation is disabled.") return Uptrack.Result() plan = [] try: logging.debug("Reading init-time plan...") plan = local.readInitPlan() logging.debug("Installing updates according to the init-time plan:") logging.debug(plan) except (IOError, yaml.YAMLError): logging.debug("Error reading init-time plan; doing nothing") logging.debug(traceback.format_exc()) return Uptrack.Result(1, "Error reading init-time plan") return doActions(plan, local) def doOfflineRemoveAll(repo, local): """ Execute the remove_plan previously written to disk """ res = Uptrack.Result() plan = [] try: logging.debug("Reading remove plan...") plan = local.readRemovePlan() except (IOError, yaml.YAMLError): logging.debug("Error reading remove-all plan; doing nothing") logging.debug(traceback.format_exc()) res.code = Uptrack.ERROR_GENERIC_ERROR res.message = "Error reading remove-all plan" return res if len(plan) == 0: if not config.cron: logging.info("Nothing to be done.") elif not displayAndConfirm(plan): logging.info("Aborting.") return Uptrack.Result() res = doActions(plan, local) if res.code: return res # We succeeded, so write out empty init and remove plans (since # we've successfully removed all of the updates, it's reasonable # to do nothing at boot-time). # We could potentially reverse the remove plan we just executed # and write it out as an upgrade plan, but since we will refuse to # execute it without network, anyways, we just write out an empty # one, and an error message informing the user to try again when # network is available. if not config.uninstall: res = Uptrack.Result(Uptrack.ERROR_NO_NETWORK, "New updates %s available. Please re-run " "Uptrack with a network connection available\n" "to view and install them" % ({True: 'are', False: 'may be'}[len(plan) > 0],)) # Don't write out a result, since quit() will do that for us. local.writeOutStatus(None, [], [], []) return res def displayAndConfirm(actions): if not config.cron: logging.info("The following steps will be taken:") for act in actions: logging.info("%s %s" % (act['Command'], act['Update'])) return confirm("Go ahead") def doActions(actions, local): res = Uptrack.Result() inst = local.getInstalledUpdates() count = len(actions) depmod_needed = os.path.exists(DEPMOD_NEEDED_FILE) for i in range(count): command, u = [actions[i][k] for k in ('Command', 'Update')] new_effective = actions[i].get('EffectiveKernel') commandwords = {INSTALL: 'Installing', REMOVE: 'Removing'} logging.info("%s %s" % (commandwords[command], u)) config.notify.WorkingOnUpdate(command.upper(), i, count, u.id, u.name) if command == INSTALL: if u in inst: logging.warning("...%s is already installed, so skipping" % u.id) continue else: r = u.applyUpdate() elif command == REMOVE: if u not in inst: logging.warning("...%s is not installed, so not removing" % u.id) continue else: r = u.undoUpdate() else: pass if r.code: logging.warning("Error processing %s" % u.filename) res.failed.append(r) res.code = r.code if config.debug_to_server: res.debug = r.debug break else: res.succeeded.append(r) if command == INSTALL: inst.add(u) elif command == REMOVE: inst.remove(u) depmod_needed = depmod_needed or r.depmod_needed if new_effective is not None: local.setEffective(new_effective) if config.wait: logging.debug("Sleeping %s seconds between updates, as requested" % config.wait) time.sleep(config.wait) if depmod_needed: try: os.unlink(DEPMOD_NEEDED_FILE) except OSError: pass config.notify.WorkingOnUpdate('DEPMOD', count, count, '', '') # modprobe.ksplice uses the presence of /var/run/uptrack to # decide whether it should use the modules.dep file managed # by Ksplice or the normal modules.dep file. # /var/run/uptrack is automatically removed after every # reboot if not os.path.isfile('/var/run/uptrack'): Uptrack.write_file('/var/run/uptrack', "uptrack\n") p = subprocess.Popen(["/bin/sh", "-c", "/sbin/ksplice-depmod -a"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() stdout = stdout.decode('utf-8') stderr = stderr.decode('utf-8') if p.returncode: logging.debug("Error in ksplice-depmod (%d)" % (p.returncode)) if stdout: logging.debug("stdout:") logging.debug(stdout) if stderr: logging.debug("stderr:") logging.debug(stderr) try: Uptrack.write_file(DEPMOD_NEEDED_FILE, "True") except IOError: pass if not res.code: res.code = 1 res.message = "Error running ksplice-depmod." return res return res def doShowUpgradePlan(repo, local): try: plan = local.readUpgradePlan() except IOError: logging.error("Unable to read the list of available updates.\n" "Please run 'uptrack-upgrade -n' to update " "the list of available updates.") sys.exit(1) logging.info("Available updates:") if plan: for act in plan: if act['Command'] == 'Install': logging.info(act['Update']) else: logging.info(None) def doShow(repo, local, wish_to_show): """ show has two modes of operation: When run without arguments, it lists the current status (i.e. what is installed). When arguments are passed to it, it shows you the detailed information for those updates. """ inst = local.getInstalledUpdates() if config.options.count: logging.info(len(inst)) sys.exit(0) can_show = set() for id in wish_to_show: u = repo.idToUpdate(id) if u is None: logging.warning("Don't know about update %s; skipping." % id) continue can_show.add(u) if len(wish_to_show) == 0: if config.available or config.all: doShowUpgradePlan(repo, local) if (not config.available) or config.all: logging.info("Installed updates:") if not len(inst): logging.info("None") else: inst_sorted = list(inst) inst_sorted.sort(key=lambda u: u.order) for u in inst_sorted: logging.info(u) printEffective(blank_line=True) else: for u in sorted(can_show, key=lambda u: u.order): if u in inst: logging.info("Update %s is installed on your system. Detailed description:\n" % u.id) else: logging.info("Update %s is NOT installed on your system. Detailed description:\n" % u.id) p = u.getDetails() if p == '': p = "Unable to retrieve detailed description of update %s." % u.id else: p = p.strip() + "\n" logging.info(p) return None if have_dbus: class DbusNotifications(dbus.service.Object): def __init__(self, object_path): dbus.service.Object.__init__(self, dbus.SystemBus(), object_path) def ClientStartStop(self, action): logging.debug("dbus: Action: %s" % (action)) ClientStartStop = dbus.service.signal(dbus_interface='com.ksplice.uptrack.Client', signature='s')(ClientStartStop) def WorkingOnUpdate(self, action, num, total, id, desc): logging.debug("dbus: Working on update: %s %i %i %s %s" % (action, num, total, id, desc)) WorkingOnUpdate = dbus.service.signal(dbus_interface='com.ksplice.uptrack.Client', signature='siiss')(WorkingOnUpdate) class NonDbusNotifications(object): def __init__(self, object_path): pass def ClientStartStop(self, action): pass def WorkingOnUpdate(self, action, num, total, id, desc): pass def isOk(res): if res: quit(res) def quit(result): """:: Uptrack.Result -> IO () Exit the program, performing appropriate reporting and cleanup before we do so. In particular, we must write the current status and result to disk (for the UI) and back to the server. In addition, we need to make sure we have written out an upgrade plan (for the GUI to display), an init-time plan (for next boot), and a remove plan (for if we try to remove all updates without network). If we are exiting successfully, the previous code paths have written out new plans if needed. If we're exiting with failure, however, we don't know what state we're in, so compute new plans to be safe. """ if result: if alert: result.alert = alert if desupported: result.desupported = desupported if tray_icon_error: result.tray_icon_error = tray_icon_error if result and result.code == Uptrack.ERROR_USER_NO_CONFIRM: logging.debug("User did not confirm actions. Not writing new plans.") local.writeOutStatus(result, None, None, None) elif not config.uninstall and \ not config.allow_no_net and result and result.code and repo.updates and \ result.code not in (Uptrack.ERROR_UNSUPPORTED, Uptrack.ERROR_NO_NETWORK, Uptrack.ERROR_INVALID_KEY, Uptrack.ERROR_MACHINE_NOT_ACTIVATED, Uptrack.ERROR_EXPIRED, Uptrack.ERROR_SYS_NOT_MOUNTED, Uptrack.ERROR_MISSING_KEY): # Need network because planActions may call out to network try: logging.debug("Determining new upgrade/init-time/remove plans.") (_, upgrade, init, remove) = planActions(repo, local, []) local.writeOutStatus(result, upgrade, init, remove) except Exception: if result.code == Uptrack.ERROR_INTERNAL_SERVER_ERROR: report = logging.debug else: report = logging.error report("Error making upgrade/init-time/remove plans") logging.debug(traceback.format_exc()) report("") if result and result.code != 0: local.writeOutStatus(result, None, None, None) else: # Something did go wrong. However, if we just used `result`, # the GUI wouldn't notice because the result code is zero. internal_error = Uptrack.Result() internal_error.code = Uptrack.ERROR_GENERIC_ERROR internal_error.message = ("Internal error while writing Uptrack" " status files.\nSee %s for more details.") % LOGFILE local.writeOutStatus(internal_error, None, None, None) elif result and result.code: logging.debug("Failed, but running without network. Not writing new plans.") local.writeOutStatus(result, None, None, None) else: logging.debug("Exiting with success. Not writing new plans.") local.writeOutStatus(result, None, None, None) if result and result.message: if result.code != 0 and result.code != Uptrack.ERROR_USER_NO_CONFIRM: if config.cron and result.code in (Uptrack.ERROR_NO_NETWORK, Uptrack.ERROR_INTERNAL_SERVER_ERROR, Uptrack.ERROR_EXPIRED, Uptrack.ERROR_MACHINE_NOT_ACTIVATED): # Don't send cron email about these transient failures; just log them. logging.debug(result.message) else: logging.error(result.message) else: logging.info(result.message) config.notify.ClientStartStop('STOP') code = 0 if result: code = result.code sys.exit(code) def prettyResult(act): msg = None why = act.abort_code if why: if why == 'code_busy' and len(act.stack_check_processes) == 1 and \ act.stack_check_processes[0][0] == 'krfcommd': msg = "Ksplice was unable to " + act.command.lower() + \ " the update because the rfcomm module (used for bluetooth)" + \ " is erroneously triggering a conservative Ksplice safety check. " + \ " If you are not using bluetooth on this system, you can install this" + \ " update by first unloading the rfcomm module using \"rmmod rfcomm\" and trying" + \ " again. Please contact %s if you have any questions." % Uptrack.BUG_EMAIL msg = "\n" + textwrap.fill(msg) elif why == 'code_busy': msg = "Ksplice was unable to " + act.command.lower() + \ " the update because one or more programs are constantly" + \ " using the kernel functions patched by this update. You" + \ " should be able to install this update by trying again. If" + \ " trying again does not work, please report this problem to" + \ " %s. " % Uptrack.BUG_EMAIL + \ " Even if trying again does not work, closing the following" + \ " programs should make it possible to install this update:" msg = "\n" + textwrap.fill(msg) + "\n\n" for proc in act.stack_check_processes: pid_i = 1 for i, word in enumerate(proc): if word.isdigit(): pid_i = i p = ' '.join(proc[:pid_i]) pid = proc[pid_i] msg += " - %s (pid %s)\n" % (p, pid) elif why == 'out_of_memory': msg = "Ksplice failed to " + act.command.lower() + \ " this update because your kernel is out of memory. Ksplice's memory" + \ " consumption is minimal, so this is likely caused by some other problem" + \ " on your system" msg = "\n" + textwrap.fill(msg) elif why == 'cold_update_loaded': msg = "Ksplice was unable to remove the update because modules" + \ " that Uptrack patched off-line are currently loaded. In order" + \ " to remove this update, you will need to first unload the" + \ " following kernel modules:" msg = "\n" + textwrap.fill(msg) msg += "\n\n" msg += "\n".join([" - %s" % (m,) for m in act.locked_modules]) elif why in ['no_match', 'failed_to_find']: if Uptrack.inVirtualBox(): msg = "\n" + textwrap.fill( "Ksplice was unable to " + act.command.lower() + " the update because it could not match the code to be" " patched in your running kernel. This could be caused by" " running Ksplice inside VirtualBox without the VT-x/AMD-V" " setting enabled.") msg += ( "\n" "For more information, see http://www.ksplice.com/uptrack/help/virtualbox" "\n\n") msg += textwrap.fill( "If you are not running VirtualBox, or enabling VT-x/AMD-V" " does not solve the problem, please report this bug to " + Uptrack.BUG_EMAIL + ".") elif len(act.nomatch_modules) > 0: msg = "\n" + textwrap.fill( "Ksplice was unable to install this update because the code in your running" + " kernel does not match the expected version.") + "\n" if os.path.exists('/etc/debian_version'): # This only happens on Debian / Ubuntu, due to their policy of not always # updating 'uname -r'. msg += "\n" + textwrap.fill( "You may have upgraded your on-disk kernel package and subsequently loaded" " one of the updated modules. You may also be running a backported or" " custom-compiled kernel module. The non-matching modules are:") + "\n\n" else: msg += "\n" + textwrap.fill( "You may be running a backported or custom-compiled version of" + " one or more of the following kernel modules that were provided by" " your Linux vendor:") + "\n\n" for module in act.nomatch_modules: msg += " - %s\n" % module msg += "\n" + textwrap.fill( "If you are not intentionally using a different version of these modules," + " you should be able to install this update by running the following" " commands as root:") + "\n" for module in act.nomatch_modules: msg += "\n" + "rmmod " + module msg += "\n\n" + textwrap.fill("and then trying again. " + " If you are unable to resolve this issue, please contact" + " %s for assistance. " % Uptrack.BUG_EMAIL) else: msg = "\n" + textwrap.fill( "Ksplice was unable to install this update because your running" + " kernel has been modified from the version provided by your vendor.") msg += ( "\n" "Please contact %s for help resolving this issue." % (Uptrack.BUG_EMAIL,)) elif why == 'module_busy' and act.command == REMOVE and act.usedby_modules: msg = "\n" + textwrap.fill("Ksplice was unable to remove" + " the update because it is in use by one or more kernel modules" + " that have been loaded since the update was applied. In order" + " to remove this update, you will need to first unload the" + " following kernel modules:" ) msg += "\n\n" msg += "\n".join([" - %s" % (m,) for m in act.usedby_modules]) msg += "\n\n" + textwrap.fill( "You can unload a module by running \"rmmod <module name>\" as root." + " If you are unable to resolve this issue, please contact" + " %s for assistance. " % Uptrack.BUG_EMAIL) elif why == 'key_not_available': msg = "\n" + textwrap.fill("Ksplice was unable to apply the update" + " because the kernel does not trust the key that was used to" + " sign the update. This is most likely caused by your system" + " using UEFI secure boot." + " Ksplice does not yet support secure boot on platforms other" + " than Oracle Linux." + " Please contact %s if you have any further questions." % (Uptrack.BUG_EMAIL)) if not msg: msg = "Ksplice was unable to " + act.command.lower() + " this update" + \ " due to an unexpected internal error." msg = "\n" + textwrap.fill(msg) msg += "\n\n" msg += "Please report this bug to <%s>.\n" % (Uptrack.BUG_EMAIL,) msg += "Uptrack log file: %s" % (LOGFILE,) return msg def initializeDBus(): if have_dbus and not config.init: # No dbus at init! dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) notify = DbusNotifications('/com/ksplice/uptrack/UptrackUI') else: logging.debug("D-Bus not present, will not notify uptrack-manager.") notify = NonDbusNotifications('/com/ksplice/uptrack/UptrackUI') config.notify = notify def desync(): logging.debug("Invoked by the cron job.") counterfile = config.localroot + '/backoff-counter' try: backoff = float(Uptrack.read_file(config.localroot + '/backoff')) backoff_counter = float(Uptrack.read_file(counterfile)) except (IOError, ValueError): backoff = 1 backoff_counter = 0 backoff_counter += 1 if backoff_counter < backoff: logging.debug("Counter is %s/%s, waiting for next time.", backoff_counter, backoff) Uptrack.write_file(counterfile, str(backoff_counter) + '\n') sys.exit(0) logging.debug("Counter is %s/%s, proceeding.", backoff_counter, backoff) backoff_counter -= backoff Uptrack.write_file(counterfile, str(backoff_counter) + '\n') # Sleep between 0 and 60s to desync the cron jobs across the # minute. time.sleep(random.randint(0, 59)) def extractCommand(name): command = None if name == 'uptrack-upgrade': command = UPGRADE elif name == 'uptrack-install': command = INSTALL elif name == 'uptrack-remove': command = REMOVE elif name == 'uptrack-show': command = SHOW else: logging.error("I don't know what you want me to do.") sys.exit(-1) return command def checkCommand(command): """Verify that command is allowed in combination with current options. """ if (command == REMOVE and not config.all) and config.allow_no_net: logging.error("Sorry, removing individual updates requires a network connection.") logging.error("(Run this command again without --no-network?)") logging.error("") logging.error("If you wish to remove all updates, run this command again") logging.error("with --all (this does not require a network connection).") sys.exit(1) if (command == REMOVE and not config.all) and len(config.args) == 0: logging.error("Please specify update IDs to remove or use the --all argument.") sys.exit(1) if config.uninstall and command != REMOVE: logging.error("--uninstall may only be used with uptrack-remove.") sys.exit(1) if config.all and config.args: logging.error("Specifying an update as well as --all makes no sense!") sys.exit(1) if command == SHOW: if config.all and config.options.count: logging.error("Using --all and --count at the same time is not supported.") sys.exit(1) if config.available and config.options.count: logging.error("Using --available and --count at the same time is not supported.") sys.exit(1) def needs_access_key(command, config): """Indicates if the user is both missing an access key and needs one for the current operation. Always false in offline edition, since offline edition doesn't actually talk to the server. """ if in_offline_mode or config.allow_no_net: return False elif command == REMOVE and config.uninstall: return False else: return config.accesskey in ['', 'INSERT_ACCESS_KEY'] def selinux_label(): restorecon = '/sbin/restorecon' """Restores the default SELinux labelling for the updates cache""" try: p = subprocess.Popen([restorecon, '-R', config.localroot], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() stdout = stdout.decode('utf-8') stderr = stderr.decode('utf-8') if p.returncode: logging.debug("Error executing %s" % restorecon) if stdout: logging.debug("stdout:") logging.debug(stdout) if stderr: logging.debug("stderr:") logging.debug(stderr) except OSError as e: logging.debug("Failed to execute %s" % restorecon) logging.debug(str(e)) def check_directory_perms(name, uid, gid, mode): try: result = os.stat(name) except OSError as e: if e.errno == errno.ENOENT: return raise current_mode = result.st_mode & (stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH) if result.st_uid != uid or result.st_gid != gid \ or not stat.S_ISDIR(result.st_mode) \ or current_mode != mode: shutil.rmtree(name, ignore_errors=True) def spawn_yum_upgrade(): def offline_updates_package_name(): _, _, release, _, machine = os.uname() return "uptrack-updates-" + release def yum_install_arguments(): if config.answer_yes: return ['yum', '-y', 'install', offline_updates_package_name()] return ['yum', 'install', offline_updates_package_name()] def cleanup_before_exec(): config.notify.ClientStartStop('STOP') logging.shutdown() args = yum_install_arguments() logging.debug("Spawning: %s" % (args)) cleanup_before_exec() os.execlp('yum', *args) def main(program, args): global config global local, repo global logger global in_offline_mode default_path = "/usr/sbin:/usr/bin:/sbin:/bin" if 'PATH' not in os.environ or not os.environ['PATH']: os.environ['PATH'] = default_path else: os.environ['PATH'] += ":" + default_path in_offline_mode = is_offline() try: if in_offline_mode: config = OfflineUptrackClientConfig(program, args) else: config = OnlineUptrackClientConfig(program, args) logger.configure(config) config.checkDeprecatedOptions() except Uptrack.ResultException as e: logging.error("Error loading configuration file:") logging.error(e.result.message) sys.exit(e.result.code) if config.options.show_version: print("%s" % __version__) sys.exit(0) if config.options.check_init: if config.disabled or not config.install_on_reboot: sys.exit(1) sys.exit(0) os.umask(0o022) # GUI needs to be able to read status files etc. Uptrack.initCurl(config.clientConfig) initializeDBus() logging.debug("") logging.debug("Client invoked as: %s %s" % (program, ' '.join(args))) if in_offline_mode and config.options.yum_upgrade: spawn_yum_upgrade() command = extractCommand(os.path.basename(program)) if config.init == 'early': command = INIT elif config.init == 'late' and not config.upgrade_on_reboot: # No reason to delay reboot if we aren't upgrading logging.debug("Client exiting due to early init and no upgrade_on_reboot") sys.exit(0) checkCommand(command) for name, uid, gid, mode in Uptrack.dir_perms: check_directory_perms(name, uid, gid, mode) Uptrack.mkdirp(config.local) Uptrack.mkdirp(os.path.join(config.local, 'updates')) if config.rh_derivative: selinux_label() if config.cron: desync() lock = getLock(config.lockfile) if lock is False: logging.debug("Unable to acquire the Uptrack repository lock: %s", config.lockfile) logging.error("""\ It appears that another Uptrack process is currently running on this system. Please wait a minute and try again. If you are unable to resolve this issue, please contact %s.""" % (Uptrack.BUG_EMAIL,)) sys.exit(1) # Check that we have an access key, before going any further. if needs_access_key(command, config) and os.path.exists(AUTOGEN_FLAG_FILE): accepted = False if config.accept_tos: accepted = True elif not config.init and os.isatty(sys.stdin.fileno()): tos = Uptrack.read_file(TOS_FILE) p = subprocess.Popen(['more'], stdin=subprocess.PIPE) p.communicate(('In order to use the Ksplice Uptrack service, you must \n' 'agree to the Ksplice Uptrack terms of service:\n\n%s' % tos).encode('utf-8')) while True: choice = input('Do you agree to the Ksplice Uptrack terms of service? [yes|no] ').strip().lower() if choice == 'yes': accepted = True break elif choice == 'no': accepted = False break if accepted: logging.warning('Requesting access key... (this may take a few moments)') bytesio = BytesIO() try: code = Uptrack.download(Uptrack.getCurl(), AUTOGEN_URL, '/dev/null', in_offline_mode, bytesio=bytesio) if code != 200: logging.error('The Ksplice Uptrack server gave a response code of %d\n' 'while requesting access key. Please contact\n' '%s for assistance.' % (code, Uptrack.BUG_EMAIL,)) sys.exit(1) except pycurl.error as e: logging.debug("cURL error %d (%s) while requesting access key." % (e.args[0], e.args[1])) logging.debug(traceback.format_exc()) logging.error(Uptrack.resultFromPycurl(config, e).message) sys.exit(1) p = subprocess.check_call(['sed', '-i', '-e', 's/^\\s*accesskey\\s*=.*/accesskey = %s/' % bytesio.getvalue().decode('utf-8'), '/etc/uptrack/uptrack.conf']) logging.info('Access key successfully requested') releaseLock(lock) # We're just marking ourselves as accepted for the GUI if config.accept_tos: sys.exit(0) # Restart with the updated config file os.execv(sys.argv[0], sys.argv) else: logging.info('You must accept the Ksplice Uptrack terms of service\n' 'in order to use the service.') sys.exit(Uptrack.ERROR_MISSING_KEY) # Some initialization has to happen inside the lock to serial accesses. try: config.initWithLock() except Uptrack.ResultException as e: logging.error("Error loading configuration file:") logging.error(e.result.message) sys.exit(e.result.code) config.notify.ClientStartStop('START') repo = UptrackRepo(config) local = Uptrack.LocalStatus(config, repo, logger) if needs_access_key(command, config): res = Uptrack.Result() res.code = Uptrack.ERROR_MISSING_KEY res.message = "You must specify an access key to use the service.\n" res.message += ("Please add your key to %s" % Uptrack.UPTRACK_CONFIG_FILE) quit(res) will_download = False if command != SHOW and not config.allow_no_net and not config.uninstall: if not in_offline_mode: isOk(repo.validateServer()) isOk(repo.handleStatus()) will_download = True else: res = repo.parsePackageList() if res and res.code == Uptrack.ERROR_NO_NETWORK and command == SHOW and \ config.options.count and config.allow_no_net: # uptrack-show --count --no-network (invoked by removal hooks) # # No packages.yml file exists on disk. Assuming packages.yml # wasn't deleted, there can be no updates installed. logging.info("0") sys.exit(0) if res and res.code and command == SHOW: logging.error("Unable to read the package list.\n" "Please run 'uptrack-upgrade -n' to download " "the latest package list.") sys.exit(1) if res and res.code and command == INIT: # There's no packages.yml, but we still need to clean /var/run/{ksplice,uptrack} doInitClean(local) if res.code == Uptrack.ERROR_NO_NETWORK: # User rebooted into a new kernel, so don't be alarmed # that there's no packages.yml res.code = 0 res.newkernel = True res.message = "Ksplice Uptrack: booting into a new kernel, so not installing any updates." quit(res) isOk(res) if not os.path.isdir('/sys/module') and ( command != SHOW or not config.options.count): # Put this check after they've confirmed it is a supported kernel, # so that we don't need to worry about CONFIG_MODULES being off. if os.path.exists('/proc/vz'): message = ("Error: You are running Ksplice Uptrack inside a Virtuozzo/OpenVZ container,\n" " but it needs to run on the hardware node instead.\n" "\n" " If you have purchased a virtual private server (VPS) from a hosting company,\n" " please contact them and ask them to purchase Ksplice Uptrack for their VPS\n" " systems. If you are the VPS provider, please install Ksplice Uptrack on the\n" " hardware node rather than in a container.\n" " If you need help resolving this issue, please contact %s." % (Uptrack.BUG_EMAIL,)) else: message = ("Error: The directory /sys/module was not found.\n" " You must have the /sys filesystem mounted in order to use Ksplice Uptrack.\n" " If you are running inside a chroot, you must mount /sys inside the chroot.\n" " This could also be caused by an old or non-standard system configuration.\n" " If you need help resolving this issue, please contact %s." % (Uptrack.BUG_EMAIL,)) res = Uptrack.Result(Uptrack.ERROR_SYS_NOT_MOUNTED, message) quit(res) if will_download: isOk(repo.downloadAll()) if config.disablecmd and command == INIT: open('/etc/uptrack/disable', 'w') config.disabled = True if config.disabled and command != SHOW: res = Uptrack.Result() res.code = Uptrack.ERROR_GENERIC_ERROR res.message = "Uptrack disabled by system administrator, remove /etc/uptrack/disable to enable." quit(res) if repo.expired: if command != SHOW and not (command == REMOVE and config.all): if command == REMOVE: msg = "Removing individual updates is disabled." else: msg = "Installing updates is disabled." quit(Uptrack.Result(Uptrack.ERROR_EXPIRED, msg)) if command == INIT: res = doInit(repo, local) elif command == UPGRADE: if config.list_effective: listAvailableEffectiveVersionsAndQuit() if config.effective_version: res = doUpgradeToEffectiveVersion(repo, local, config.effective_version) else: res = doUpgrade(repo, local) elif command == INSTALL: res = doInstall(repo, local, config.args) elif command == REMOVE: # Special case: We are able to remove all updates using the # saved remove_plan without going to the server. # We do this for 'remove --no-network', if our access key has # expired (in which case we won't be able to reach the # server), or when uninstalling the package, so we can be # removed without network. if config.uninstall or \ config.all and (config.allow_no_net or repo.expired): res = doOfflineRemoveAll(repo, local) else: res = doRemove(repo, local, config.args) printEffective() elif command == SHOW: res = doShow(repo, local, config.args) if res and command in [INIT, UPGRADE, INSTALL, REMOVE]: if res.code == Uptrack.ERROR_USER_NO_CONFIRM: quit(res) if len(res.succeeded): logging.debug("") logging.debug("The following actions were successful:") for act in res.succeeded: logging.debug("%s %s" % (act.command, act.update)) if len(res.failed): logging.info("") logging.error("The following actions failed:") for act in res.failed: logging.error("%s %s" % (act.command, act.update)) logging.error(prettyResult(act)) logging.debug("Message:\n" + act.message) logging.error("") # merge all failed.*.code's into one code res.code = Uptrack.ERROR_GENERIC_ERROR quit(res) if __name__ == "__main__": try: logger = Logger() except Exception: print("Unable to set up the logger.", file=sys.stderr) if os.getuid() != 0: print("The Uptrack client must be run as root.", file=sys.stderr) sys.exit(1) else: raise try: main(sys.argv[0], sys.argv[1:]) except KeyboardInterrupt: logging.error("Interrupted!") logging.debug("", exc_info=1) sys.exit(1) except SystemExit: raise except Exception: # Catch unhandled exceptions and report them to the server. res = Uptrack.Result(Uptrack.ERROR_INTERNAL_ERROR, traceback.format_exc()) if local is None: logging.error("Unexpected error starting the Uptrack client.") logging.error("Please submit a copy of %s to %s." % (LOGFILE, Uptrack.BUG_EMAIL)) logging.debug(res.message) sys.exit(-1) try: local.writeOutStatus(res, None, None, None) except SystemExit: raise except Exception: pass logging.debug("Unhandled exception", exc_info=1) logging.error("Unexpected error.") logging.error("Please submit a copy of %s to %s." % (LOGFILE, Uptrack.BUG_EMAIL)) sys.exit(-1) """ =head1 NAME uptrack - Manage Ksplice rebootless kernel updates =head1 SYNOPSIS B<uptrack-upgrade> [I<OPTION>] B<uptrack-install> [I<OPTION>] I<id>... B<uptrack-remove> [I<OPTION>] I<id>... B<uptrack-show> [I<OPTION>] [I<id>...] =head1 DESCRIPTION The Uptrack command-line tools manage the set of Ksplice rebootless kernel updates installed on your system. There are four major modes of operation: =over 4 =item B<uptrack-upgrade> Downloads and installs the latest Ksplice updates available for your system. =item B<uptrack-install> Takes as arguments the update IDs to install, and installs them, downloading them if necessary. =item B<uptrack-remove> Takes as arguments the update IDs to remove, and removes them. =item B<uptrack-show> If invoked without additional arguments, shows the list of Ksplice updates currently installed. If update IDs are passed as arguments, displays the status of those updates as well as the detailed information associated with them. =back =head1 OPTIONS =over 4 =item B<-v>, B<--verbose> Provide more detail about what the program is doing. =item B<-q>, B<--quiet> Do not print status messages. =item B<-y> Assume "yes" to all user prompts. =item B<-n> Assume "no" to all user prompts. =item B<--all> Take action for all of the IDs that Uptrack knows about, instead of specifying them at the command-line. For uptrack-show, this will list both installed updates and available updates. =item B<--available> With C<uptrack-show>, instead of showing installed updates, shows the available updates. =item B<--count> With C<uptrack-show>, instead of showing installed updates, print the number of updates installed. =item B<--wait=N> When installing or removing a sequence of updates, wait B<N> seconds after processing each update before processing the next one. =item B<-V>, B<--version> Print the version information and exit. =back =head1 FILES =over 4 =item I</etc/uptrack/uptrack.conf> Configuration file for Uptrack. =item I</etc/uptrack/disable> If this file exists, Uptrack will refuse to install or remove updates. If 'nouptrack' is passed on the kernel command line, then no updates will be installed at boot time, and I</etc/uptrack/disable> will automatically be created during the boot process. =back =head1 BUGS Please report bugs to <ksplice-support_ww@oracle.com>. =head1 AUTHORS Waseem Daher and Tim Abbott =head1 COPYRIGHT See included LICENSE file for additional license and copyright information. =cut """