""" Kernel installation task """ from cStringIO import StringIO import logging import re import shlex import urllib2 import urlparse from teuthology import misc as teuthology from ..orchestra import run from ..config import config as teuth_config log = logging.getLogger(__name__) def normalize_config(ctx, config): """ Returns a config whose keys are all real roles. Generic roles (client, mon, osd, etc.) are replaced with the actual roles (client.0, client.1, etc.). If the config specifies a different version for a specific role, this is unchanged. For example, with 3 OSDs this:: osd: tag: v3.0 kdb: true osd.1: branch: new_btrfs kdb: false osd.3: deb: /path/to/linux-whatever.deb is transformed into:: osd.0: tag: v3.0 kdb: true osd.1: branch: new_btrfs kdb: false osd.2: tag: v3.0 kdb: true osd.3: deb: /path/to/linux-whatever.deb If config is None or just specifies a version to use, it is applied to all nodes. :param ctx: Context :param config: Configuration """ if config is None or \ len(filter(lambda x: x in ['tag', 'branch', 'sha1', 'kdb', 'deb'], config.keys())) == len(config.keys()): new_config = {} if config is None: config = {'branch': 'master'} for _, roles_for_host in ctx.cluster.remotes.iteritems(): new_config[roles_for_host[0]] = config return new_config new_config = {} for role, role_config in config.iteritems(): if role_config is None: role_config = {'branch': 'master'} if '.' in role: new_config[role] = role_config else: for id_ in teuthology.all_roles_of_type(ctx.cluster, role): name = '{type}.{id}'.format(type=role, id=id_) # specific overrides generic if name not in config: new_config[name] = role_config return new_config def _find_arch_and_dist(ctx): """ Return the arch and distro value as a tuple. Currently this only returns armv7l on the quantal distro or x86_64 on the precise distro :param ctx: Context :returns: arch,distro """ info = ctx.config.get('machine_type', 'plana') if teuthology.is_arm(info): return ('armv7l', 'quantal') return ('x86_64', 'precise') def validate_config(ctx, config): """ Make sure that all kernels in the list of remove kernels refer to the same kernel. :param ctx: Context :param config: Configuration """ for _, roles_for_host in ctx.cluster.remotes.iteritems(): kernel = None for role in roles_for_host: role_kernel = config.get(role, kernel) if kernel is None: kernel = role_kernel elif role_kernel is not None: assert kernel == role_kernel, \ "everything on the same host must use the same kernel" if role in config: del config[role] def _vsplitter(version): """Kernels from Calxeda are named ...ceph-...highbank. Kernels that we generate are named ...-g. This routine finds the text in front of the sha1 that is used by need_to_install() to extract information from the kernel name. :param version: Name of the kernel """ if version.endswith('highbank'): return 'ceph-' return '-g' def need_to_install(ctx, role, version): """ Check to see if we need to install a kernel. Get the version of the currently running kernel, and compare it against the value passed in. :param ctx: Context :param role: Role :param version: value to compare against (used in checking), can be either a utsrelease string (e.g. '3.13.0-rc3-ceph-00049-ge2817b3') or a sha1. """ ret = True log.info('Checking kernel version of {role}, want {ver}...'.format( role=role, ver=version)) uname_fp = StringIO() ctx.cluster.only(role).run( args=[ 'uname', '-r', ], stdout=uname_fp, ) cur_version = uname_fp.getvalue().rstrip('\n') log.debug('current kernel version is {ver}'.format(ver=cur_version)) if '.' in version: # version is utsrelease, yay if cur_version == version: log.debug('utsrelease strings match, do not need to install') ret = False else: # version is sha1, need to try to extract sha1 from cur_version splt = _vsplitter(cur_version) if splt in cur_version: _, cur_sha1 = cur_version.rsplit(splt, 1) dloc = cur_sha1.find('-') if dloc > 0: cur_sha1 = cur_sha1[0:dloc] log.debug('extracting sha1, {ver} -> {sha1}'.format( ver=cur_version, sha1=cur_sha1)) if version.startswith(cur_sha1): log.debug('extracted sha1 matches, do not need to install') ret = False else: log.debug('failed to parse current kernel version') uname_fp.close() return ret def install_firmware(ctx, config): """ Go to the github to get the latest firmware. :param ctx: Context :param config: Configuration """ linux_firmware_git_upstream = 'git://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git' uri = teuth_config.linux_firmware_git_url or linux_firmware_git_upstream fw_dir = '/lib/firmware/updates' for role in config.iterkeys(): if config[role].find('distro') >= 0: log.info('Skipping firmware on distro kernel'); return (role_remote,) = ctx.cluster.only(role).remotes.keys() log.info('Installing linux-firmware on {role}...'.format(role=role)) role_remote.run( args=[ # kludge around mysterious 0-byte .git/HEAD files 'cd', fw_dir, run.Raw('&&'), 'test', '-d', '.git', run.Raw('&&'), 'test', '!', '-s', '.git/HEAD', run.Raw('&&'), 'sudo', 'rm', '-rf', '.git', run.Raw(';'), # init 'sudo', 'install', '-d', '-m0755', fw_dir, run.Raw('&&'), 'cd', fw_dir, run.Raw('&&'), 'sudo', 'git', 'init', ], ) role_remote.run( args=[ 'sudo', 'git', '--git-dir=%s/.git' % fw_dir, 'config', '--get', 'remote.origin.url', run.Raw('>/dev/null'), run.Raw('||'), 'sudo', 'git', '--git-dir=%s/.git' % fw_dir, 'remote', 'add', 'origin', uri, ], ) role_remote.run( args=[ 'cd', fw_dir, run.Raw('&&'), 'sudo', 'git', 'fetch', 'origin', run.Raw('&&'), 'sudo', 'git', 'reset', '--hard', 'origin/master' ], ) def download_deb(ctx, config): """ Download a Debian kernel and copy the assocated linux image. :param ctx: Context :param config: Configuration """ procs = {} #Don't need to download distro kernels for role, src in config.iteritems(): (role_remote,) = ctx.cluster.only(role).remotes.keys() if src.find('distro') >= 0: log.info('Installing newest kernel distro'); return if src.find('/') >= 0: # local deb log.info('Copying kernel deb {path} to {role}...'.format(path=src, role=role)) f = open(src, 'r') proc = role_remote.run( args=[ 'python', '-c', 'import shutil, sys; shutil.copyfileobj(sys.stdin, file(sys.argv[1], "wb"))', '/tmp/linux-image.deb', ], wait=False, stdin=f ) procs[role_remote.name] = proc else: log.info('Downloading kernel {sha1} on {role}...'.format(sha1=src, role=role)) larch, ldist = _find_arch_and_dist(ctx) _, deb_url = teuthology.get_ceph_binary_url( package='kernel', sha1=src, format='deb', flavor='basic', arch=larch, dist=ldist, ) log.info('fetching kernel from {url}'.format(url=deb_url)) proc = role_remote.run( args=[ 'sudo', 'rm', '-f', '/tmp/linux-image.deb', run.Raw('&&'), 'echo', 'linux-image.deb', run.Raw('|'), 'wget', '-nv', '-O', '/tmp/linux-image.deb', '--base={url}'.format(url=deb_url), '--input-file=-', ], wait=False) procs[role_remote.name] = proc for name, proc in procs.iteritems(): log.debug('Waiting for download/copy to %s to complete...', name) proc.exitstatus.get() def _no_grub_link(in_file, remote, kernel_ver): """ Copy and link kernel related files if grub cannot be used (as is the case in Arm kernels) :param infile: kernel file or image file to be copied. :param remote: remote machine :param kernel_ver: kernel version """ boot1 = '/boot/%s' % in_file boot2 = '%s.old' % boot1 remote.run( args=[ 'if', 'test', '-e', boot1, run.Raw(';'), 'then', 'sudo', 'mv', boot1, boot2, run.Raw(';'), 'fi',], ) remote.run( args=['sudo', 'ln', '-s', '%s-%s' % (in_file, kernel_ver) , boot1, ], ) def install_and_reboot(ctx, config): """ Install and reboot the kernel. This mostly performs remote installation operations. The code does check for Arm images and skips grub operations if the kernel is Arm. Otherwise, it extracts kernel titles from submenu entries and makes the appropriate grub calls. The assumptions here are somewhat simplified in that it expects kernel entries to be present under submenu entries. :param ctx: Context :param config: Configuration """ procs = {} kernel_title = '' for role, src in config.iteritems(): (role_remote,) = ctx.cluster.only(role).remotes.keys() if src.find('distro') >= 0: log.info('Installing distro kernel on {role}...'.format(role=role)) install_distro_kernel(role_remote) continue log.info('Installing kernel {src} on {role}...'.format(src=src, role=role)) proc = role_remote.run( args=[ # install the kernel deb 'sudo', 'dpkg', '-i', '/tmp/linux-image.deb', ], ) # collect kernel image name from the .deb cmdout = StringIO() proc = role_remote.run( args=[ # extract the actual boot image name from the deb 'dpkg-deb', '--fsys-tarfile', '/tmp/linux-image.deb', run.Raw('|'), 'tar', '-t', '-v', '-f', '-', '--wildcards', '--', './boot/vmlinuz-*', run.Raw('|'), 'sed', r'-e s;.*\./boot/vmlinuz-;;', ], stdout = cmdout, ) kernel_title = cmdout.getvalue().rstrip() cmdout.close() log.info('searching for kernel {}'.format(kernel_title)) if kernel_title.endswith("-highbank"): _no_grub_link('vmlinuz', role_remote, kernel_title) _no_grub_link('initrd.img', role_remote, kernel_title) proc = role_remote.run( args=[ 'sudo', 'shutdown', '-r', 'now', ], wait=False, ) procs[role_remote.name] = proc continue # look for menuentry for our kernel, and collect any # submenu entries for their titles. Assume that if our # kernel entry appears later in the file than a submenu entry, # it's actually nested under that submenu. If it gets more # complex this will totally break. cmdout = StringIO() proc = role_remote.run( args=[ 'egrep', '(submenu|menuentry.*' + kernel_title + ').*{', '/boot/grub/grub.cfg' ], stdout = cmdout, ) submenu_title = '' default_title = '' for l in cmdout.getvalue().split('\n'): fields = shlex.split(l) if len(fields) >= 2: command, title = fields[:2] if command == 'submenu': submenu_title = title + '>' if command == 'menuentry': if title.endswith(kernel_title): default_title = title break cmdout.close() log.info('submenu_title:{}'.format(submenu_title)) log.info('default_title:{}'.format(default_title)) proc = role_remote.run( args=[ # use the title(s) to construct the content of # the grub menu entry, so we can default to it. '/bin/echo', '-e', r'cat </dev/null'), run.Raw('&&'), 'sudo', 'chmod', 'a+x', '--', '/etc/grub.d/01_ceph_kernel.tmp~', run.Raw('&&'), 'sudo', 'mv', '--', '/etc/grub.d/01_ceph_kernel.tmp~', '/etc/grub.d/01_ceph_kernel', # update grub again so it accepts our default run.Raw('&&'), 'sudo', 'update-grub', run.Raw('&&'), 'rm', '/tmp/linux-image.deb', run.Raw('&&'), 'sudo', 'shutdown', '-r', 'now', ], wait=False, ) procs[role_remote.name] = proc for name, proc in procs.iteritems(): log.debug('Waiting for install on %s to complete...', name) proc.exitstatus.get() def enable_disable_kdb(ctx, config): """ Enable kdb on remote machines in use. Disable on those that are not in use. :param ctx: Context :param config: Configuration """ for role, enable in config.iteritems(): (role_remote,) = ctx.cluster.only(role).remotes.keys() if "mira" in role_remote.name: serialdev = "ttyS2" else: serialdev = "ttyS1" if enable: log.info('Enabling kdb on {role}...'.format(role=role)) try: role_remote.run( args=[ 'echo', serialdev, run.Raw('|'), 'sudo', 'tee', '/sys/module/kgdboc/parameters/kgdboc' ]) except run.CommandFailedError: log.warn('Kernel does not support kdb') else: log.info('Disabling kdb on {role}...'.format(role=role)) # Add true pipe so command doesn't fail on kernel without kdb support. try: role_remote.run( args=[ 'echo', '', run.Raw('|'), 'sudo', 'tee', '/sys/module/kgdboc/parameters/kgdboc', run.Raw('|'), 'true', ]) except run.CommandFailedError: log.warn('Kernel does not support kdb') def wait_for_reboot(ctx, need_install, timeout, distro=False): """ Loop reconnecting and checking kernel versions until they're all correct or the timeout is exceeded. :param ctx: Context :param need_install: list of packages that we need to reinstall. :param timeout: number of second before we timeout. """ import time starttime = time.time() while need_install: teuthology.reconnect(ctx, timeout) for client in need_install.keys(): if 'distro' in need_install[client]: distro = True log.info('Checking client {client} for new kernel version...'.format(client=client)) try: if distro: assert not need_to_install_distro(ctx, client), \ 'failed to install new distro kernel version within timeout' else: assert not need_to_install(ctx, client, need_install[client]), \ 'failed to install new kernel version within timeout' del need_install[client] except Exception: log.exception("Saw exception") # ignore connection resets and asserts while time is left if time.time() - starttime > timeout: raise time.sleep(1) def need_to_install_distro(ctx, role): """ Installing kernels on rpm won't setup grub/boot into them. This installs the newest kernel package and checks its version and compares against current (uname -r) and returns true if newest != current. Similar check for deb. """ (role_remote,) = ctx.cluster.only(role).remotes.keys() system_type = teuthology.get_system_type(role_remote) output, err_mess = StringIO(), StringIO() role_remote.run(args=['uname', '-r' ], stdout=output, stderr=err_mess ) current = output.getvalue().strip() if system_type == 'rpm': role_remote.run(args=['sudo', 'yum', 'install', '-y', 'kernel' ], stdout=output, stderr=err_mess ) #reset stringIO output. output, err_mess = StringIO(), StringIO() role_remote.run(args=['rpm', '-q', 'kernel', '--last' ], stdout=output, stderr=err_mess ) newest=output.getvalue().split()[0] if system_type == 'deb': distribution = teuthology.get_system_type(role_remote, distro=True) newest = get_version_from_pkg(role_remote, distribution) output.close() err_mess.close() if current in newest: return False log.info('Not newest distro kernel. Curent: {cur} Expected: {new}'.format(cur=current, new=newest)) return True def install_distro_kernel(remote): """ RPM: Find newest kernel on the machine and update grub to use kernel + reboot. DEB: Find newest kernel. Parse grub.cfg to figure out the entryname/subentry. then modify 01_ceph_kernel to have correct entry + updategrub + reboot. """ system_type = teuthology.get_system_type(remote) distribution = '' if system_type == 'rpm': output, err_mess = StringIO(), StringIO() remote.run(args=['rpm', '-q', 'kernel', '--last' ], stdout=output, stderr=err_mess ) newest=output.getvalue().split()[0].split('kernel-')[1] log.info('Distro Kernel Version: {version}'.format(version=newest)) update_grub_rpm(remote, newest) remote.run( args=['sudo', 'shutdown', '-r', 'now'], wait=False ) output.close() err_mess.close() return if system_type == 'deb': distribution = teuthology.get_system_type(remote, distro=True) newversion = get_version_from_pkg(remote, distribution) if 'ubuntu' in distribution: grub2conf = teuthology.get_file(remote, '/boot/grub/grub.cfg', True) submenu = '' menuentry = '' for line in grub2conf.split('\n'): if 'submenu' in line: submenu = line.split('"')[1] if 'menuentry' in line: if newversion in line and 'recovery' not in line: menuentry = line.split('\'')[1] break if submenu: grubvalue = submenu + '>' + menuentry else: grubvalue = menuentry grubfile = 'cat <